From f819076b3a59a094a00c192b1018c75cab76d25b Mon Sep 17 00:00:00 2001 From: jyejare Date: Thu, 1 Jun 2023 14:41:03 +0530 Subject: [PATCH 001/586] Stream tests removed from 6.14 branch --- tests/foreman/api/test_contentviewfilter.py | 1 - tests/foreman/maintain/test_service.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/foreman/api/test_contentviewfilter.py b/tests/foreman/api/test_contentviewfilter.py index 9bfc61778e8..e07fcd0d6f4 100644 --- a/tests/foreman/api/test_contentviewfilter.py +++ b/tests/foreman/api/test_contentviewfilter.py @@ -664,7 +664,6 @@ def test_negative_update_repo(self, module_product, sync_repo, content_view): with pytest.raises(HTTPError): cvf.update(['repository']) - @pytest.mark.stream @pytest.mark.tier2 @pytest.mark.parametrize( 'filter_type', ['erratum', 'package_group', 'rpm', 'modulemd', 'docker'] diff --git a/tests/foreman/maintain/test_service.py b/tests/foreman/maintain/test_service.py index 248c0d4f3f4..170eb2f7f5c 100644 --- a/tests/foreman/maintain/test_service.py +++ b/tests/foreman/maintain/test_service.py @@ -130,7 +130,6 @@ def test_positive_service_stop_restart(sat_maintain): assert result.status == 0 -@pytest.mark.stream @pytest.mark.include_capsule def test_positive_service_enable_disable(sat_maintain): """Enable/Disable services using satellite-maintain service subcommand From 7b3d1f0ea906a7f54ecb26eca4a5fb14801b0504 Mon Sep 17 00:00:00 2001 From: Jitendra Yejare Date: Thu, 1 Jun 2023 19:38:14 +0530 Subject: [PATCH 002/586] Update requirements.txt Updating 6.14 reqs to use 6.14 nailgun and airgun branches --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 1488d8f89aa..65dff54a08c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,6 +26,6 @@ wait-for==1.2.0 wrapanapi==3.5.15 # Get airgun, nailgun and upgrade from master -git+https://github.com/SatelliteQE/airgun.git@master#egg=airgun -git+https://github.com/SatelliteQE/nailgun.git@master#egg=nailgun +git+https://github.com/SatelliteQE/airgun.git@6.14.z#egg=airgun +git+https://github.com/SatelliteQE/nailgun.git@6.14.z#egg=nailgun --editable . From 30c1018117d953c5af90d5cda373a5d80bc76c77 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 1 Jun 2023 13:37:03 -0400 Subject: [PATCH 003/586] [6.14.z] Bump cryptography from 40.0.2 to 41.0.0 (#11577) Bump cryptography from 40.0.2 to 41.0.0 (#11562) Bumps [cryptography](https://github.com/pyca/cryptography) from 40.0.2 to 41.0.0. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/40.0.2...41.0.0) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit abac2150fc954f4416278c88e97112fabf700b73) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 65dff54a08c..1a35f2865f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ betelgeuse==1.10.0 broker[docker]==0.3.2 -cryptography==40.0.2 +cryptography==41.0.0 deepdiff==6.3.0 dynaconf[vault]==3.1.12 fauxfactory==3.1.0 From fe980248ba9926a49a7bccc7235aace53e4e5bc9 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 5 Jun 2023 04:46:19 -0400 Subject: [PATCH 004/586] [6.14.z] Adding better coverage to enable/disable stream test (#11578) Adding better coverage to enable/disable stream test (#11514) (cherry picked from commit 1d475bb49088e83bf8ae7d948b6ed9d6b31255ee) Co-authored-by: Griffin Sullivan <48397354+Griffin-Sullivan@users.noreply.github.com> --- tests/foreman/maintain/test_service.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/foreman/maintain/test_service.py b/tests/foreman/maintain/test_service.py index 170eb2f7f5c..78ca8129fc7 100644 --- a/tests/foreman/maintain/test_service.py +++ b/tests/foreman/maintain/test_service.py @@ -148,12 +148,19 @@ def test_positive_service_enable_disable(sat_maintain): :customerscenario: true """ + result = sat_maintain.cli.Service.stop() + assert 'FAIL' not in result.stdout + assert result.status == 0 result = sat_maintain.cli.Service.disable() assert 'FAIL' not in result.stdout assert result.status == 0 result = sat_maintain.cli.Service.enable() assert 'FAIL' not in result.stdout assert result.status == 0 + sat_maintain.power_control(state='reboot') + result = sat_maintain.cli.Service.status(options={'brief': True, 'only': 'foreman.service'}) + assert 'FAIL' not in result.stdout + assert result.status == 0 def test_positive_foreman_service(request, sat_maintain): From e45b64b51f82c6c7fcf18d32aaf7afd198dd0b83 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 5 Jun 2023 07:02:11 -0400 Subject: [PATCH 005/586] [6.14.z] Close looop BZ1896628 (#11589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close looop BZ1896628 (#11570) Close loop BZ1896628 (cherry picked from commit 3b0fd1f4984fb826e48b38c946aac0823a9c5b68) Co-authored-by: Lukáš Hellebrandt --- tests/foreman/cli/test_remoteexecution.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index e00ff969a35..682fbc57274 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -122,7 +122,7 @@ class TestRemoteExecution: @pytest.mark.pit_client @pytest.mark.pit_server @pytest.mark.rhel_ver_list([8]) - def test_positive_run_default_job_template_by_ip(self, rex_contenthost): + def test_positive_run_default_job_template_by_ip(self, module_org, rex_contenthost): """Run default template on host connected by ip and list task :id: 811c7747-bec6-4a2d-8e5c-b5045d3fbc0d @@ -130,7 +130,7 @@ def test_positive_run_default_job_template_by_ip(self, rex_contenthost): :expectedresults: Verify the job was successfully ran against the host and task can be listed by name and ID - :BZ: 1647582 + :BZ: 1647582, 1896628 :customerscenario: true @@ -149,6 +149,15 @@ def test_positive_run_default_job_template_by_ip(self, rex_contenthost): task = Task.list_tasks({'search': command})[0] search = Task.list_tasks({'search': f'id={task["id"]}'}) assert search[0]['action'] == task['action'] + out = JobInvocation.get_output( + { + 'id': invocation_command['id'], + 'host': client.hostname, + 'organization-id': module_org.id, + } + ) + assert 'Exit' in out + assert 'Internal Server Error' not in out @pytest.mark.tier3 @pytest.mark.pit_client From e0692c8d19303e455843462329c045cedc10304a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 6 Jun 2023 03:50:40 -0400 Subject: [PATCH 006/586] [6.14.z] Add CLI test for applied filters (#11595) Add CLI test for applied filters (#11542) Satellite 6.14.0 is going to implement indication and listing of filters applied to a CVV. This PR adds a test to ensure the correct functionality in hammer CLI. (cherry picked from commit aa4743941d5c99388a68c8f40b15c49b57f146b3) Co-authored-by: vsedmik <46570670+vsedmik@users.noreply.github.com> --- tests/foreman/cli/test_contentviewfilter.py | 86 +++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/tests/foreman/cli/test_contentviewfilter.py b/tests/foreman/cli/test_contentviewfilter.py index 4881b8ee28f..8fd42b121cf 100644 --- a/tests/foreman/cli/test_contentviewfilter.py +++ b/tests/foreman/cli/test_contentviewfilter.py @@ -16,6 +16,8 @@ :Upstream: No """ +import random + import pytest from fauxfactory import gen_string @@ -973,3 +975,87 @@ def test_negative_delete_by_name(self, content_view): ContentView.filter.delete( {'content-view-id': content_view['id'], 'name': gen_string('utf8')} ) + + @pytest.mark.stream + @pytest.mark.tier2 + def test_positive_check_filters_applied(self, target_sat, module_org, content_view): + """Ensure the applied filters are indicated and listed correctly in the CVV info. + + :id: ab72af3f-6bee-4aa8-a74a-637fe9b7e34a + + :steps: + 1. Publish first CVV with no filters applied, assert no applied filters are indicated + nor listed. + 2. Randomly add filters to the CV, publish new CVVs and assert that the applied filters + are indicated and the correct filters are listed. + 3. Randomly remove filters from the CV, publish new CVVs and assert that the applied + filters are indicated when at least one filter exists and the correct filters were + removed. + + :expectedresults: + 1. Hammer shows correctly if a CVV has filter(s) applied, no matter the filter type, + count, nor order. + 2. Hammer lists correctly the applied filter(s), no matter the filter type, count + nor order. + """ + f_types = ['rpm', 'package_group', 'erratum', 'modulemd', 'docker'] + filters_applied = [] + + # Publish first CVV with no filters applied + target_sat.cli.ContentView.publish({'id': content_view['id']}) + cvv = target_sat.cli.ContentView.info({'id': content_view['id']})['versions'][0] + cvv_info = target_sat.cli.ContentView.version_info( + {'id': cvv['id'], 'include-applied-filters': 'true'} + ) + assert cvv_info['has-applied-filters'] == 'no' + assert 'applied-filters' not in cvv_info + + # Randomly add filters to the CV, assert correct CVV info values + for f_type in random.choices(f_types, k=random.randint(1, 5)): + cvf = target_sat.cli.ContentView.filter.create( + { + 'content-view-id': content_view['id'], + 'name': gen_string('alpha'), + 'organization-id': module_org.id, + 'type': f_type, + 'inclusion': random.choice(['true', 'false']), + }, + ) + filters_applied.append(cvf) + + target_sat.cli.ContentView.publish({'id': content_view['id']}) + cvv = max( + target_sat.cli.ContentView.info({'id': content_view['id']})['versions'], + key=lambda x: int(x['id']), + ) + cvv_info = target_sat.cli.ContentView.version_info( + {'id': cvv['id'], 'include-applied-filters': 'true'} + ) + assert cvv_info['has-applied-filters'] == 'yes' + assert len(cvv_info['applied-filters']) == len(filters_applied) + f_listed = [f for f in cvv_info['applied-filters'] if f['id'] == cvf['filter-id']] + assert len(f_listed) == 1 + assert f_listed[0]['name'] == cvf['name'] + + # Randomly remove filters from the CV, assert correct CVV info values + random.shuffle(filters_applied) + for _ in range(len(filters_applied)): + cvf = filters_applied.pop() + target_sat.cli.ContentView.filter.delete({'id': cvf['filter-id']}) + + target_sat.cli.ContentView.publish({'id': content_view['id']}) + cvv = max( + target_sat.cli.ContentView.info({'id': content_view['id']})['versions'], + key=lambda x: int(x['id']), + ) + cvv_info = target_sat.cli.ContentView.version_info( + {'id': cvv['id'], 'include-applied-filters': 'true'} + ) + if len(filters_applied) > 0: + assert cvv_info['has-applied-filters'] == 'yes' + assert len(cvv_info['applied-filters']) == len(filters_applied) + assert cvf['filter-id'] not in [f['id'] for f in cvv_info['applied-filters']] + assert cvf['name'] not in [f['name'] for f in cvv_info['applied-filters']] + else: + assert cvv_info['has-applied-filters'] == 'no' + assert 'applied-filters' not in cvv_info From 41fb6c353553e9a42ffa36ce3c956a7d3b3f5331 Mon Sep 17 00:00:00 2001 From: vsedmik <46570670+vsedmik@users.noreply.github.com> Date: Tue, 6 Jun 2023 11:16:12 +0200 Subject: [PATCH 007/586] [6.14.z] Remove @stream decorator (#11599) Remove @stream decorator --- tests/foreman/cli/test_contentviewfilter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/foreman/cli/test_contentviewfilter.py b/tests/foreman/cli/test_contentviewfilter.py index 8fd42b121cf..40326ad9ba6 100644 --- a/tests/foreman/cli/test_contentviewfilter.py +++ b/tests/foreman/cli/test_contentviewfilter.py @@ -976,7 +976,6 @@ def test_negative_delete_by_name(self, content_view): {'content-view-id': content_view['id'], 'name': gen_string('utf8')} ) - @pytest.mark.stream @pytest.mark.tier2 def test_positive_check_filters_applied(self, target_sat, module_org, content_view): """Ensure the applied filters are indicated and listed correctly in the CVV info. From 5f1ddd39edfe8c734ece549b4ae84cad1e9d462b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 6 Jun 2023 06:43:04 -0400 Subject: [PATCH 008/586] [6.14.z] Remove unnecessary failing assertion api/test_http_proxy.py (#11600) Remove unnecessary failing assertion api/test_http_proxy.py (#11597) Removed unnecessary failing assertion (cherry picked from commit 16b7cd53331894fd5b88548eb340f36eed2d270d) Co-authored-by: David Moore <109112035+damoore044@users.noreply.github.com> --- tests/foreman/api/test_http_proxy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/foreman/api/test_http_proxy.py b/tests/foreman/api/test_http_proxy.py index 34d398b25f6..5244450d409 100644 --- a/tests/foreman/api/test_http_proxy.py +++ b/tests/foreman/api/test_http_proxy.py @@ -313,7 +313,6 @@ def test_positive_sync_proxy_with_certificate(request, target_sat, module_org, m assert repo.http_proxy_policy == 'use_selected_http_proxy' assert repo.http_proxy_id == http_proxy.id - assert http_proxy.cacert == cacert_path response = repo.sync() assert response.get('errors') is None From 57082ac399bea1d0300fcae69f1f5745dc6d738d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 6 Jun 2023 10:14:57 -0400 Subject: [PATCH 009/586] [6.14.z] Close loop - Ansible job invocation shows wrong info after remote execution job (#11601) Close loop - Ansible job invocation shows wrong info after remote execution job (#11491) Signed-off-by: Adarsh Dubey (cherry picked from commit be6c36dc3d091efeb3e3f817a679349115adc8ea) Co-authored-by: Adarsh dubey --- pytest_fixtures/core/contenthosts.py | 7 +++ tests/foreman/api/test_ansible.py | 72 ++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/pytest_fixtures/core/contenthosts.py b/pytest_fixtures/core/contenthosts.py index f743601852c..435a1446d89 100644 --- a/pytest_fixtures/core/contenthosts.py +++ b/pytest_fixtures/core/contenthosts.py @@ -89,6 +89,13 @@ def rhel6_contenthost(request): yield host +@pytest.fixture(params=[{'rhel_version': '9'}]) +def rhel9_contenthost(request): + """A fixture that provides a rhel9 content host object""" + with Broker(**host_conf(request), host_class=ContentHost) as host: + yield host + + @pytest.fixture() def content_hosts(request): """A function-level fixture that provides two rhel content hosts object""" diff --git a/tests/foreman/api/test_ansible.py b/tests/foreman/api/test_ansible.py index c3afd6362cb..84ffd656528 100644 --- a/tests/foreman/api/test_ansible.py +++ b/tests/foreman/api/test_ansible.py @@ -123,3 +123,75 @@ def test_positive_ansible_job_on_host(target_sat, module_org, rhel_contenthost): ) result = target_sat.api.JobInvocation(id=job['id']).read() assert result.succeeded == 1 + + +@pytest.mark.no_containers +def test_positive_ansible_job_on_multiple_host( + target_sat, + module_org, + rhel9_contenthost, + rhel8_contenthost, + rhel7_contenthost, + module_location, + module_ak_with_synced_repo, +): + """Test execution of Ansible job on multiple hosts simultaneously. + + :id: 9369feef-466c-40d3-9d0d-65520d7f21ef + + :customerscenario: true + + :steps: + 1. Register multiple content hosts with satellite + 2. Import a role into satellite + 3. Assign that role to all host + 4. Trigger ansible job keeping all host in a single query + 5. Check the passing and failing of individual hosts + 6. Check if one of the job on a host is failed resulting into whole job is marked as failed. + + :expectedresults: + 1. One of the jobs failing on a single host must impact the overall result as failed. + + :BZ: 2167396, 2190464, 2184117 + + :CaseAutomation: Automated + """ + hosts = [rhel9_contenthost, rhel8_contenthost, rhel7_contenthost] + SELECTED_ROLE = 'RedHatInsights.insights-client' + for host in hosts: + result = host.register( + module_org, module_location, module_ak_with_synced_repo.name, target_sat + ) + assert result.status == 0, f'Failed to register host: {result.stderr}' + id = target_sat.nailgun_smart_proxy.id + target_host = host.nailgun_host + target_sat.api.AnsibleRoles().sync(data={'proxy_id': id, 'role_names': [SELECTED_ROLE]}) + target_sat.cli.Host.ansible_roles_assign( + {'id': target_host.id, 'ansible-roles': SELECTED_ROLE} + ) + host_roles = target_host.list_ansible_roles() + assert host_roles[0]['name'] == SELECTED_ROLE + + template_id = ( + target_sat.api.JobTemplate() + .search(query={'search': 'name="Ansible Roles - Ansible Default"'})[0] + .id + ) + job = target_sat.api.JobInvocation().run( + synchronous=False, + data={ + 'job_template_id': template_id, + 'targeting_type': 'static_query', + 'search_query': f'name ^ ({hosts[0].hostname} && {hosts[1].hostname} ' + f'&& {hosts[2].hostname})', + }, + ) + target_sat.wait_for_tasks( + f'resource_type = JobInvocation and resource_id = {job["id"]}', + poll_timeout=1000, + must_succeed=False, + ) + result = target_sat.api.JobInvocation(id=job['id']).read() + assert result.succeeded == 2 # SELECTED_ROLE working on rhel8/rhel9 clients + assert result.failed == 1 # SELECTED_ROLE failing on rhel7 client + assert result.status_label == 'failed' From 508b81702931eb6329df9824d0a244d174c5f7c2 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 7 Jun 2023 07:40:09 -0400 Subject: [PATCH 010/586] [6.14.z] Automate BZ 1969263 (#11617) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automate BZ 1969263 (#11557) (cherry picked from commit 8a2b45da05a2430588c0ebc89161c9e95fa083fc) Co-authored-by: Lukáš Hellebrandt --- tests/foreman/api/test_host.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/foreman/api/test_host.py b/tests/foreman/api/test_host.py index 5982918c8b0..30341389ed0 100644 --- a/tests/foreman/api/test_host.py +++ b/tests/foreman/api/test_host.py @@ -1387,6 +1387,31 @@ def test_positive_read_puppet_ca_proxy_name( assert session_puppet_enabled_proxy.name == host['puppet_ca_proxy_name'] +@pytest.mark.tier2 +def test_positive_list_hosts_thin_all(module_target_sat): + """List hosts with thin=true and per_page=all + + :id: 00b7e603-aed5-4b19-bfec-1a179fad6743 + + :expectedresults: Hosts listed without ISE + + :BZ: 1969263 + + :customerscenario: true + """ + hosts = module_target_sat.api.Host().search(query={'thin': 'true', 'per_page': 'all'}) + assert module_target_sat.hostname in [host.name for host in hosts] + keys = dir(hosts[0]) + assert 'id' in keys + assert 'name' in keys + # Can't check for only id and name being present because the framework adds + # some data the API doesn't actually return (currently, it adds empty Environment). + # Instead, check for some data to be missing, as opposed to non-thin. + assert 'domain' not in keys + assert 'ip' not in keys + assert 'architecture' not in keys + + class TestHostInterface: """Tests for Host Interfaces""" From 19474496d3cbfbb1c800d96c4e93232a9207f1b8 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 7 Jun 2023 11:43:40 -0400 Subject: [PATCH 011/586] [6.14.z] fixed the context name for PRT status (#11622) fixed the context name for PRT status (#11621) fixed the context name for prt status (cherry picked from commit 3cf9d7a909210057d21cd6bad2fcab6be5ddcc11) Co-authored-by: Omkar Khatavkar --- .github/workflows/auto_cherry_pick_merge.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto_cherry_pick_merge.yaml b/.github/workflows/auto_cherry_pick_merge.yaml index 303b6caa037..924bc9fccfd 100644 --- a/.github/workflows/auto_cherry_pick_merge.yaml +++ b/.github/workflows/auto_cherry_pick_merge.yaml @@ -53,7 +53,7 @@ jobs: uses: omkarkhatavkar/wait-for-status-checks@main with: ref: ${{ github.head_ref }} - context: 'Robottelo Runner' + context: 'Robottelo-Runner' wait-interval: 60 count: 100 From 63637205aba822d8eb969b7f7d2a2aa3ed1bbddb Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 7 Jun 2023 11:52:35 -0400 Subject: [PATCH 012/586] [6.14.z] Fix configure_puppet (#11612) Fix configure_puppet (#11567) Signed-off-by: Gaurav Talreja (cherry picked from commit d8d8fcc0df32044f0339dda61221cf546083e102) Co-authored-by: Gaurav Talreja --- robottelo/hosts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robottelo/hosts.py b/robottelo/hosts.py index e261b24ad89..f3f1d1ba337 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -878,7 +878,7 @@ def configure_puppet(self, proxy_hostname=None): # sat6 under the capsule --> certifcates or on capsule via cli "puppetserver # ca list", so that we sign it. self.execute('/opt/puppetlabs/bin/puppet agent -t') - proxy_host = Host(proxy_hostname) + proxy_host = Host(hostname=proxy_hostname) proxy_host.execute(f'puppetserver ca sign --certname {cert_name}') # This particular puppet run would create the host entity under # 'All Hosts' and let's redirect stderr to /dev/null as errors at From 8831478c3fd4a97a12c09678c78cc286aa3d0885 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 7 Jun 2023 14:25:12 -0400 Subject: [PATCH 013/586] [6.14.z] Add test for add/remove ansible_role to host/hostgroup via API (#11623) Add test for add/remove ansible_role to host/hostgroup via API (#11545) Signed-off-by: Gaurav Talreja (cherry picked from commit 707e671b81ab579e7b98e1f8ace1d8a30e3c18a4) Co-authored-by: Gaurav Talreja --- tests/foreman/api/test_ansible.py | 78 +++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/tests/foreman/api/test_ansible.py b/tests/foreman/api/test_ansible.py index 84ffd656528..3a9b8f0a726 100644 --- a/tests/foreman/api/test_ansible.py +++ b/tests/foreman/api/test_ansible.py @@ -17,6 +17,7 @@ :Upstream: No """ import pytest +from fauxfactory import gen_string from robottelo.config import settings @@ -68,7 +69,9 @@ def test_fetch_and_sync_ansible_playbooks(target_sat): @pytest.mark.e2e @pytest.mark.no_containers @pytest.mark.rhel_ver_match('[^6].*') -def test_positive_ansible_job_on_host(target_sat, module_org, rhel_contenthost): +def test_positive_ansible_job_on_host( + target_sat, module_org, module_location, module_ak_with_synced_repo, rhel_contenthost +): """ Test successful execution of Ansible Job on host. @@ -86,6 +89,8 @@ def test_positive_ansible_job_on_host(target_sat, module_org, rhel_contenthost): 1. Host should be assigned the proper role. 2. Job execution must be successful. + :BZ: 2164400 + :CaseAutomation: Automated :CaseImportance: Critical @@ -94,17 +99,19 @@ def test_positive_ansible_job_on_host(target_sat, module_org, rhel_contenthost): if rhel_contenthost.os_version.major <= 7: rhel_contenthost.create_custom_repos(rhel7=settings.repos.rhel7_os) assert rhel_contenthost.execute('yum install -y insights-client').status == 0 - rhel_contenthost.install_katello_ca(target_sat) - rhel_contenthost.register_contenthost(module_org.label, force=True) - assert rhel_contenthost.subscribed - rhel_contenthost.add_rex_key(satellite=target_sat) + result = rhel_contenthost.register( + module_org, module_location, module_ak_with_synced_repo.name, target_sat + ) + assert result.status == 0, f'Failed to register host: {result.stderr}' proxy_id = target_sat.nailgun_smart_proxy.id target_host = rhel_contenthost.nailgun_host target_sat.api.AnsibleRoles().sync(data={'proxy_id': proxy_id, 'role_names': [SELECTED_ROLE]}) - target_sat.cli.Host.ansible_roles_assign({'id': target_host.id, 'ansible-roles': SELECTED_ROLE}) + role_id = target_sat.api.AnsibleRoles().search(query={'search': f'name={SELECTED_ROLE}'})[0].id + target_sat.api.Host(id=target_host.id).add_ansible_role(data={'ansible_role_id': role_id}) host_roles = target_host.list_ansible_roles() assert host_roles[0]['name'] == SELECTED_ROLE assert target_host.name == rhel_contenthost.hostname + template_id = ( target_sat.api.JobTemplate() .search(query={'search': 'name="Ansible Roles - Ansible Default"'})[0] @@ -123,6 +130,9 @@ def test_positive_ansible_job_on_host(target_sat, module_org, rhel_contenthost): ) result = target_sat.api.JobInvocation(id=job['id']).read() assert result.succeeded == 1 + target_sat.api.Host(id=target_host.id).remove_ansible_role(data={'ansible_role_id': role_id}) + host_roles = target_host.list_ansible_roles() + assert len(host_roles) == 0 @pytest.mark.no_containers @@ -163,12 +173,15 @@ def test_positive_ansible_job_on_multiple_host( module_org, module_location, module_ak_with_synced_repo.name, target_sat ) assert result.status == 0, f'Failed to register host: {result.stderr}' - id = target_sat.nailgun_smart_proxy.id + proxy_id = target_sat.nailgun_smart_proxy.id target_host = host.nailgun_host - target_sat.api.AnsibleRoles().sync(data={'proxy_id': id, 'role_names': [SELECTED_ROLE]}) - target_sat.cli.Host.ansible_roles_assign( - {'id': target_host.id, 'ansible-roles': SELECTED_ROLE} + target_sat.api.AnsibleRoles().sync( + data={'proxy_id': proxy_id, 'role_names': [SELECTED_ROLE]} ) + role_id = ( + target_sat.api.AnsibleRoles().search(query={'search': f'name={SELECTED_ROLE}'})[0].id + ) + target_sat.api.Host(id=target_host.id).add_ansible_role(data={'ansible_role_id': role_id}) host_roles = target_host.list_ansible_roles() assert host_roles[0]['name'] == SELECTED_ROLE @@ -195,3 +208,48 @@ def test_positive_ansible_job_on_multiple_host( assert result.succeeded == 2 # SELECTED_ROLE working on rhel8/rhel9 clients assert result.failed == 1 # SELECTED_ROLE failing on rhel7 client assert result.status_label == 'failed' + + +@pytest.mark.e2e +@pytest.mark.tier2 +def test_add_and_remove_ansible_role_hostgroup(target_sat): + """ + Test add and remove functionality for ansible roles in hostgroup via API + + :id: 7672cf86-fa31-11ed-855a-0fd307d2d66b + + :Steps: + 1. Create a hostgroup + 2. Sync few ansible roles + 3. Assign a few ansible roles with the host group + 4. Add some ansible role with the host group + 5. Remove the added ansible roles from the host group + + :expectedresults: + 1. Ansible role assign/add/remove functionality should work as expected in API + + :BZ: 2164400 + """ + ROLE_NAMES = [ + 'theforeman.foreman_scap_client', + 'redhat.satellite.hostgroups', + 'RedHatInsights.insights-client', + ] + hg = target_sat.api.HostGroup(name=gen_string('alpha')).create() + proxy_id = target_sat.nailgun_smart_proxy.id + target_sat.api.AnsibleRoles().sync(data={'proxy_id': proxy_id, 'role_names': ROLE_NAMES}) + ROLES = [ + target_sat.api.AnsibleRoles().search(query={'search': f'name={role}'})[0].id + for role in ROLE_NAMES + ] + target_sat.api.HostGroup(id=hg.id).assign_ansible_roles(data={'ansible_role_ids': ROLES[:2]}) + for r1, r2 in zip(target_sat.api.HostGroup(id=hg.id).list_ansible_roles(), ROLE_NAMES[:2]): + assert r1['name'] == r2 + target_sat.api.HostGroup(id=hg.id).add_ansible_role(data={'ansible_role_id': ROLES[2]}) + for r1, r2 in zip(target_sat.api.HostGroup(id=hg.id).list_ansible_roles(), ROLE_NAMES): + assert r1['name'] == r2 + + for role in ROLES: + target_sat.api.HostGroup(id=hg.id).remove_ansible_role(data={'ansible_role_id': role}) + host_roles = target_sat.api.HostGroup(id=hg.id).list_ansible_roles() + assert len(host_roles) == 0 From fe12706ce2029d09b9f1c0cd33e462fe28b51229 Mon Sep 17 00:00:00 2001 From: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> Date: Thu, 8 Jun 2023 19:01:52 +0530 Subject: [PATCH 014/586] [6.14.z] Add Infoblox end to end test (#11632) Add Infoblox end to end test --- conf/infoblox.yaml.template | 18 +++ tests/foreman/destructive/test_infoblox.py | 179 +++++++++++++++++++++ tests/foreman/installer/test_infoblox.py | 142 +--------------- 3 files changed, 199 insertions(+), 140 deletions(-) create mode 100644 conf/infoblox.yaml.template diff --git a/conf/infoblox.yaml.template b/conf/infoblox.yaml.template new file mode 100644 index 00000000000..69398945eb0 --- /dev/null +++ b/conf/infoblox.yaml.template @@ -0,0 +1,18 @@ +INFOBLOX: + HOSTNAME: infoblox.example.com + # username: Login for Infoblox instance + USERNAME: + # password: Password for Infoblox instance + PASSWORD: + #Domain + DOMAIN: + #Network Address + NETWORK: + #Network Prefix + NETWORK_PREFIX: + #Network Netmask + NETMASK: + #Starting range of IP + START_RANGE: + #Ending range of IP + END_RANGE: diff --git a/tests/foreman/destructive/test_infoblox.py b/tests/foreman/destructive/test_infoblox.py index 27221f861b9..c54b3fad31e 100644 --- a/tests/foreman/destructive/test_infoblox.py +++ b/tests/foreman/destructive/test_infoblox.py @@ -15,8 +15,14 @@ :Upstream: No """ import pytest +import requests +from fauxfactory import gen_mac +from fauxfactory import gen_string +from requests.exceptions import HTTPError +from robottelo.config import settings from robottelo.utils.installer import InstallerCommand +from robottelo.utils.issue_handlers import is_open pytestmark = pytest.mark.destructive @@ -50,6 +56,34 @@ ), ] +infoblox_plugin_enable = [ + 'enable-foreman-proxy-plugin-dhcp-infoblox', + 'enable-foreman-proxy-plugin-dns-infoblox', +] + +infoblox_plugin_disable = [ + 'no-enable-foreman-proxy-plugin-dhcp-infoblox', + 'no-enable-foreman-proxy-plugin-dns-infoblox', +] + +infoblox_plugin_opts = { + 'foreman-proxy-dhcp': 'true', + 'foreman-proxy-dhcp-managed': 'false', + 'foreman-proxy-dhcp-provider': 'infoblox', + 'foreman-proxy-dhcp-server': settings.infoblox.hostname, + 'foreman-proxy-plugin-dhcp-infoblox-dns-view': 'default', + 'foreman-proxy-plugin-dhcp-infoblox-network-view': 'default', + 'foreman-proxy-plugin-dhcp-infoblox-username': settings.infoblox.username, + 'foreman-proxy-plugin-dhcp-infoblox-password': settings.infoblox.password, + 'foreman-proxy-plugin-dhcp-infoblox-record-type': 'fixedaddress', + 'foreman-proxy-dns': 'true', + 'foreman-proxy-dns-provider': 'infoblox', + 'foreman-proxy-plugin-dns-infoblox-username': settings.infoblox.username, + 'foreman-proxy-plugin-dns-infoblox-password': settings.infoblox.password, + 'foreman-proxy-plugin-dns-infoblox-dns-server': settings.infoblox.hostname, + 'foreman-proxy-plugin-dns-infoblox-dns-view': 'default', +} + @pytest.mark.tier4 @pytest.mark.parametrize( @@ -76,3 +110,148 @@ def test_plugin_installation(target_sat, command_args, command_opts, rpm_command installer = target_sat.install(InstallerCommand(command_args, **command_opts)) assert 'Success!' in installer.stdout assert target_sat.execute(rpm_command).status == 0 + + +@pytest.mark.e2e +@pytest.mark.parametrize('module_sync_kickstart_content', [8], indirect=True) +def test_infoblox_end_to_end( + request, + module_sync_kickstart_content, + module_provisioning_capsule, + module_target_sat, + module_sca_manifest_org, + module_location, + module_default_org_view, + module_lce_library, + default_architecture, + default_partitiontable, +): + """Verify end to end infoblox plugin integration and host creation with + Infoblox as DHCP and DNS provider + + :id: 0aebdf39-84ba-4182-9a17-6eb523f1695f + + :steps: + 1. Run installer to integrate Satellite and Infoblox. + 2. Assert if DHCP, DNS provider and server is properly set. + 3. Create a host with proper domain and subnet. + 4. Check if A record is created with host ip and hostname on Infoblox + 5. Delete the host, domain, subnet + + :expectedresults: Satellite and Infoblox are integrated properly with DHCP, DNS + provider and server.Also, A record is created for the host created. + + :BZ: 1813953, 2210256 + + :customerscenario: true + """ + enable_infoblox_plugin = InstallerCommand( + installer_args=infoblox_plugin_enable, + installer_opts=infoblox_plugin_opts, + ) + result = module_target_sat.install(enable_infoblox_plugin) + assert result.status == 0 + assert 'Success!' in result.stdout + + installer = module_target_sat.install( + InstallerCommand(help='| grep enable-foreman-proxy-plugin-dhcp-infoblox') + ) + assert 'default: true' in installer.stdout + + installer = module_target_sat.install( + InstallerCommand(help='| grep enable-foreman-proxy-plugin-dns-infoblox') + ) + assert 'default: true' in installer.stdout + + installer = module_target_sat.install( + InstallerCommand(help='| grep foreman-proxy-dhcp-provider') + ) + assert 'current: "infoblox"' in installer.stdout + + installer = module_target_sat.install( + InstallerCommand(help='| grep foreman-proxy-dns-provider') + ) + assert 'current: "infoblox"' in installer.stdout + + installer = module_target_sat.install(InstallerCommand(help='| grep foreman-proxy-dhcp-server')) + assert f'current: "{settings.infoblox.hostname}"' in installer.stdout + + installer = module_target_sat.install( + InstallerCommand(help='| grep foreman-proxy-plugin-dns-infoblox-dns-server') + ) + assert f'current: "{settings.infoblox.hostname}"' in installer.stdout + + macaddress = gen_mac(multicast=False) + # using the domain name as defined in Infoblox DNS + domain = module_target_sat.api.Domain( + name=settings.infoblox.domain, + location=[module_location], + dns=module_provisioning_capsule.id, + organization=[module_sca_manifest_org], + ).create() + subnet = module_target_sat.api.Subnet( + location=[module_location], + organization=[module_sca_manifest_org], + network=settings.infoblox.network, + cidr=settings.infoblox.network_prefix, + mask=settings.infoblox.netmask, + from_=settings.infoblox.start_range, + to=settings.infoblox.end_range, + boot_mode='DHCP', + ipam='DHCP', + dhcp=module_provisioning_capsule.id, + tftp=module_provisioning_capsule.id, + dns=module_provisioning_capsule.id, + discovery=module_provisioning_capsule.id, + remote_execution_proxy=[module_provisioning_capsule.id], + domain=[domain.id], + ).create() + host = module_target_sat.api.Host( + organization=module_sca_manifest_org, + location=module_location, + name=gen_string('alpha').lower(), + mac=macaddress, + operatingsystem=module_sync_kickstart_content.os, + architecture=default_architecture, + domain=domain, + subnet=subnet, + root_pass=settings.provisioning.host_root_password, + ptable=default_partitiontable, + content_facet_attributes={ + 'content_source_id': module_provisioning_capsule.id, + 'content_view_id': module_default_org_view.id, + 'lifecycle_environment_id': module_lce_library.id, + }, + ).create() + # check if A Record is created for the host IP on Infoblox + url = f'https://{settings.infoblox.hostname}/wapi/v2.0/ipv4address?ip_address={host.ip}' + auth = (settings.infoblox.username, settings.infoblox.password) + result = requests.get(url, auth=auth, verify=False) + assert result.status_code == 200 + # check hostname and ip is present in A record + assert host.name in result.text + assert host.ip in result.text + # disable dhcp and dns plugin + if not is_open('BZ:2210256'): + disable_infoblox_plugin = InstallerCommand(installer_args=infoblox_plugin_disable) + result = module_target_sat.install(disable_infoblox_plugin) + assert result.status == 0 + assert 'Success!' in result.stdout + installer = module_target_sat.install( + InstallerCommand(help='| grep enable-foreman-proxy-plugin-dhcp-infoblox') + ) + assert 'default: false' in installer.stdout + + installer = module_target_sat.install( + InstallerCommand(help='| grep enable-foreman-proxy-plugin-dns-infoblox') + ) + assert 'default: false' in installer.stdout + + @request.addfinalizer + def _finalize(): + module_target_sat.api.Subnet(id=subnet.id, domain=[]).update() + module_target_sat.api.Host(id=host.id).delete() + module_target_sat.api.Subnet(id=subnet.id).delete() + module_target_sat.api.Domain(id=domain.id).delete() + with pytest.raises(HTTPError): + host.read() diff --git a/tests/foreman/installer/test_infoblox.py b/tests/foreman/installer/test_infoblox.py index 0bdafe25d82..e8b77fcc67d 100644 --- a/tests/foreman/installer/test_infoblox.py +++ b/tests/foreman/installer/test_infoblox.py @@ -4,6 +4,8 @@ :CaseLevel: System +:CaseAutomation: Automated + :CaseComponent: DHCPDNS :Team: Rocket @@ -17,146 +19,6 @@ import pytest -@pytest.mark.stubbed -@pytest.mark.tier3 -@pytest.mark.upgrade -def test_set_dns_provider(): - """Check Infoblox DNS plugin is set as provider - - :id: 23f76fa8-79bb-11e6-a3d4-68f72889dc7f - - :Steps: Set infoblox as dns provider with options - --foreman-proxy-dns=true - --foreman-proxy-plugin-provider=infoblox - --enable-foreman-proxy-plugin-dns-infoblox - --foreman-proxy-plugin-dns-infoblox-dns-server= - --foreman-proxy-plugin-dns-infoblox-username= - --foreman-proxy-plugin-dns-infoblox-password= - - :expectedresults: Check inflobox is set as DNS provider - - :CaseLevel: System - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -@pytest.mark.upgrade -def test_set_dhcp_provider(): - """Check Infoblox DHCP plugin is set as provider - - :id: 40783976-7e68-11e6-b728-68f72889dc7f - - :Steps: Set infoblox as dhcp provider with options - --foreman-proxy-dhcp=true - --foreman-proxy-plugin-dhcp-provider=infoblox - --enable-foreman-proxy-plugin-dhcp-infoblox - --foreman-proxy-plugin-dhcp-infoblox-dhcp-server= - --foreman-proxy-plugin-dhcp-infoblox-username= - --foreman-proxy-plugin-dhcp-infoblox-password= - - :expectedresults: Check inflobox is set as DHCP provider - - :CaseLevel: System - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_update_dns_appliance_credentials(): - """Check infoblox appliance credentials are updated - - :id: 2e84a8b4-79b6-11e6-8bf8-68f72889dc7f - - :Steps: Pass appliance credentials via installer options - --foreman-proxy-plugin-dns-infoblox-username= - --foreman-proxy-plugin-dns-infoblox-password= - - :expectedresults: config/dns_infoblox.yml should be updated with - infoblox_hostname, username & password - - :CaseLevel: System - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -@pytest.mark.upgrade -def test_enable_dns_plugin(): - """Check Infoblox DNS plugin can be enabled on server - - :id: f8be8c34-79b2-11e6-8992-68f72889dc7f - - :Steps: Enable Infoblox plugin via installer options - --enable-foreman-proxy-plugin-dns-infoblox - - :CaseLevel: System - - :expectedresults: Check DNS plugin is enabled on host - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_disable_dns_plugin(): - """Check Infoblox DNS plugin can be disabled on host - - :id: c5f563c6-79b3-11e6-8cb6-68f72889dc7f - - :Steps: Disable Infoblox plugin via installer - - :expectedresults: Check DNS plugin is disabled on host - - :CaseLevel: System - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -@pytest.mark.upgrade -def test_enable_dhcp_plugin(): - """Check Infoblox DHCP plugin can be enabled on host - - :id: 75650c06-79b6-11e6-ad91-68f72889dc7f - - :Steps: Enable Infoblox plugin via installer option - --enable-foreman-proxy-plugin-dhcp-infoblox - - :expectedresults: Check DHCP plugin is enabled on host - - :CaseLevel: System - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_disable_dhcp_plugin(): - """Check Infoblox DHCP plugin can be disabled on host - - :id: ea347f34-79b7-11e6-bb03-68f72889dc7f - - :Steps: Disable Infoblox plugin via installer - - :expectedresults: Check DHCP plugin is disabled on host - - :CaseLevel: System - - :CaseAutomation: NotAutomated - """ - - @pytest.mark.stubbed @pytest.mark.tier3 @pytest.mark.upgrade From 6a668efa1c52e4ed17ffb846886ef26e2dfea5f9 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 8 Jun 2023 17:31:08 -0400 Subject: [PATCH 015/586] [6.14.z] Close-loop 1757394 (#11625) --- tests/foreman/api/test_user.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/foreman/api/test_user.py b/tests/foreman/api/test_user.py index 05a6a649b92..2932e3b1ae8 100644 --- a/tests/foreman/api/test_user.py +++ b/tests/foreman/api/test_user.py @@ -399,6 +399,37 @@ def test_negative_create_with_blank_authorized_by(self): with pytest.raises(HTTPError): entities.User(auth_source='').create() + @pytest.mark.tier1 + def test_positive_table_preferences(self, module_target_sat): + """Create a user, create their Table Preferences, read it + + :id: 10fda3f1-4fee-461b-a413-a4d1fa098a94 + + :expectedresults: Table Preferences can be accessed + + :CaseImportance: Medium + + :customerscenario: true + + :BZ: 1757394 + """ + existing_roles = entities.Role().search() + password = gen_string('alpha') + user = entities.User(role=existing_roles, password=password).create() + name = "hosts" + columns = ["power_status", "name", "comment"] + sc = ServerConfig(auth=(user.login, password), url=module_target_sat.url, verify=False) + entities.TablePreferences(sc, user=user, name=name, columns=columns).create() + table_preferences = entities.TablePreferences(sc, user=user).search() + assert len(table_preferences) == 1 + tp = table_preferences[0] + assert hasattr(tp, 'name') + assert hasattr(tp, 'columns') + assert tp.name == 'hosts' + assert len(tp.columns) == len(columns) + for column in columns: + assert column in tp.columns + class TestUserRole: """Test associations between users and roles.""" From a8f8912d09e262e8912d02cca886cf62feaeb8ae Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 9 Jun 2023 04:48:20 -0400 Subject: [PATCH 016/586] [6.14.z] Add customercase (#11643) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add customercase (#11631) (cherry picked from commit 59b0b5db03213e70f111af4abf5cfeca557f962e) Co-authored-by: Lukáš Hellebrandt --- tests/foreman/api/test_host.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/api/test_host.py b/tests/foreman/api/test_host.py index 30341389ed0..5b9d0ef7c5f 100644 --- a/tests/foreman/api/test_host.py +++ b/tests/foreman/api/test_host.py @@ -1395,7 +1395,7 @@ def test_positive_list_hosts_thin_all(module_target_sat): :expectedresults: Hosts listed without ISE - :BZ: 1969263 + :BZ: 1969263, 1644750 :customerscenario: true """ From 148a72b287f516d50b859636a774ffa190d97ff5 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 12 Jun 2023 03:17:08 -0400 Subject: [PATCH 017/586] [6.14.z] Bump pytest from 7.3.1 to 7.3.2 (#11656) Bump pytest from 7.3.1 to 7.3.2 (#11647) (cherry picked from commit 92281f23641c53032e7497a0f669b148adcd882b) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1a35f2865f5..f1908c89bc8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ navmazing==1.1.6 productmd==1.35 pyotp==2.8.0 python-box==7.0.1 -pytest==7.3.1 +pytest==7.3.2 pytest-services==2.2.1 pytest-mock==3.10.0 pytest-reportportal==5.1.8 From dd47e3606e181918adc9b4147cdb9cfa04d8f9b8 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 12 Jun 2023 03:30:13 -0400 Subject: [PATCH 018/586] [6.14.z] Bump sphinx-autoapi from 2.1.0 to 2.1.1 (#11649) Bump sphinx-autoapi from 2.1.0 to 2.1.1 (#11648) (cherry picked from commit 22103ee873a7452e4f43e24fd2a33b4851020f09) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 59cb7353114..c3804b82d2f 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -6,7 +6,7 @@ pre-commit==3.3.2 # For generating documentation. sphinx==7.0.1 -sphinx-autoapi==2.1.0 +sphinx-autoapi==2.1.1 # For 'manage' interactive shell manage>=0.1.13 From 9c6631fa119e3cf1dd30fc862a2ebc59b0a9c574 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 12 Jun 2023 08:51:13 -0400 Subject: [PATCH 019/586] [6.14.z] [Req Assist] Update more reqs names deviation and other improvements (#11659) [Req Assist] Update more reqs names deviation and other improvements (#11563) Fix reqs name deviation and other improvements (cherry picked from commit 463a0050e9297e6187678a555df5911ef7ce927e) Co-authored-by: Jitendra Yejare --- pytest_plugins/requirements/req_updater.py | 2 ++ pytest_plugins/requirements/update_requirements.py | 4 ++++ requirements-optional.txt | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pytest_plugins/requirements/req_updater.py b/pytest_plugins/requirements/req_updater.py index 52b8a822bfa..a647fc51e26 100644 --- a/pytest_plugins/requirements/req_updater.py +++ b/pytest_plugins/requirements/req_updater.py @@ -9,6 +9,8 @@ class ReqUpdater: 'Betelgeuse': 'betelgeuse', 'broker': 'broker[docker]', 'dynaconf': 'dynaconf[vault]', + 'Jinja2': 'jinja2', + 'Sphinx': 'sphinx', } @cached_property diff --git a/pytest_plugins/requirements/update_requirements.py b/pytest_plugins/requirements/update_requirements.py index df16c4694a2..a930d95fe33 100644 --- a/pytest_plugins/requirements/update_requirements.py +++ b/pytest_plugins/requirements/update_requirements.py @@ -36,6 +36,8 @@ def pytest_report_header(config): if config.getoption('update_required_reqs') or config.getoption('update_all_reqs'): print('Updating the mandatory requirements on demand ....') updater.install_req_deviations() + else: + print(f'{Colored.GREEN}Mandatory Requirements are up to date!{Colored.RESET}') if updater.opt_deviation: print( @@ -45,3 +47,5 @@ def pytest_report_header(config): if config.getoption('update_all_reqs'): print('Updating the optional requirements on demand ....') updater.install_opt_deviations() + else: + print(f'{Colored.GREEN}Optional Requirements are up to date!{Colored.RESET}') diff --git a/requirements-optional.txt b/requirements-optional.txt index c3804b82d2f..357338866ae 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -9,4 +9,4 @@ sphinx==7.0.1 sphinx-autoapi==2.1.1 # For 'manage' interactive shell -manage>=0.1.13 +manage==0.1.15 From 2bbd5781bf705294ebb8262464e74f519a2a868a Mon Sep 17 00:00:00 2001 From: Omkar Khatavkar Date: Mon, 12 Jun 2023 20:12:30 +0530 Subject: [PATCH 020/586] [6.14.z] autodetect and add prt tests for dependabot PR's (#11646) --- .github/dependabot.yml | 6 ---- .github/dependency_tests.yaml | 2 +- .github/workflows/dependency_merge.yml | 44 ++++++++++---------------- 3 files changed, 17 insertions(+), 35 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7a8fb51c6cd..9ad35498875 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,9 +12,6 @@ updates: labels: - "CherryPick" - "dependencies" - - "6.13.z" - - "6.12.z" - - "6.11.z" # Maintain dependencies for our GitHub Actions - package-ecosystem: "github-actions" @@ -24,6 +21,3 @@ updates: labels: - "CherryPick" - "dependencies" - - "6.13.z" - - "6.12.z" - - "6.11.z" diff --git a/.github/dependency_tests.yaml b/.github/dependency_tests.yaml index 43715259676..ae8b1133199 100644 --- a/.github/dependency_tests.yaml +++ b/.github/dependency_tests.yaml @@ -9,4 +9,4 @@ pytest: "tests/foreman/ -m 'build_sanity'" python-box: "tests/foreman/cli/test_webhook.py -k 'test_positive_end_to_end'" requests: "tests/foreman/cli/test_repository.py -k 'test_file_repo_contains_only_newer_file'" wait-for: "tests/foreman/api/test_reporttemplates.py -k 'test_positive_schedule_entitlements_report'" -wrapanapi: "tests/foreman/longrun/test_provisioning_computeresource.py -k 'test_positive_provision_vmware_with_host_group'" +wrapanapi: "tests/foreman/api/test_computeresource_gce.py -k 'test_positive_gce_host_provisioned'" diff --git a/.github/workflows/dependency_merge.yml b/.github/workflows/dependency_merge.yml index 7a85be38583..f21f70fb201 100644 --- a/.github/workflows/dependency_merge.yml +++ b/.github/workflows/dependency_merge.yml @@ -8,39 +8,27 @@ jobs: dependabot: name: dependabot-auto-merge runs-on: ubuntu-latest - if: github.event.pull_request.user.login == 'dependabot[bot]' - steps: - - name: Dependabot metadata - id: metadata - uses: dependabot/fetch-metadata@v1 - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 + if: | + (github.event.pull_request.user.login == 'dependabot[bot]' || + github.event.pull_request.user.login == 'Satellite-QE') && + contains( github.event.pull_request.labels.*.name, 'dependencies') - - name: Find the tests for the dependency requirement - id: yaml - uses: mikefarah/yq@master + steps: + - id: find-prt-comment + name: Find the prt comment + uses: peter-evans/find-comment@v2 with: - cmd: yq e ".${{ steps.metadata.outputs.dependency-names }}" ./.github/dependency_tests.yaml + issue-number: ${{ github.event.number }} + body-includes: "trigger: test-robottelo" + direction: last - - name: Add the PRT Comment - if: steps.yaml.outputs.result != 'null' - uses: peter-evans/create-or-update-comment@v3 - with: - issue-number: ${{ github.event.pull_request.number }} - body: | - trigger: test-robottelo\r - pytest: ${{ steps.yaml.outputs.result }} - name: Wait for PRT checks to get initiated - if: steps.yaml.outputs.result != 'null' + if: steps.find-prt-comment.outputs.comment-body != '' run: | echo "Waiting for ~ 10 mins, PRT to be initiated." && sleep 600 + - name: Fetch and Verify the PRT status - if: steps.yaml.outputs.result != 'null' + if: steps.find-prt-comment.outputs.comment-body != '' id: outcome uses: omkarkhatavkar/wait-for-status-checks@main with: @@ -54,7 +42,7 @@ jobs: uses: lewagon/wait-on-check-action@v1.3.1 with: ref: ${{ github.head_ref }} - repo-token: ${{ secrets.CHERRYPICK_PAT }} + repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 60 running-workflow-name: 'dependabot-auto-merge' allowed-conclusions: success,skipped @@ -63,7 +51,7 @@ jobs: name: Auto merge of dependabot PRs. uses: "pascalgn/automerge-action@v0.15.6" env: - GITHUB_TOKEN: "${{ secrets.CHERRYPICK_PAT }}" + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" MERGE_LABELS: "dependencies" MERGE_METHOD: "squash" MERGE_RETRIES: 5 From 7fa328b68cd7639277a915e334a465166a5aea25 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 12 Jun 2023 10:50:52 -0400 Subject: [PATCH 021/586] [6.14.z] Add Closed loop BZ#2173199 (#11665) --- robottelo/cli/bootdisk.py | 41 +++++++++++++ robottelo/constants/__init__.py | 2 + tests/foreman/cli/test_bootdisk.py | 92 ++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 robottelo/cli/bootdisk.py create mode 100644 tests/foreman/cli/test_bootdisk.py diff --git a/robottelo/cli/bootdisk.py b/robottelo/cli/bootdisk.py new file mode 100644 index 00000000000..50df399f7f3 --- /dev/null +++ b/robottelo/cli/bootdisk.py @@ -0,0 +1,41 @@ +""" +Usage:: + + hammer bootdisk [OPTIONS] SUBCOMMAND [ARG] ... + +Parameters:: + + SUBCOMMAND Subcommand + [ARG] ... Subcommand arguments + +Subcommands:: + + generic Download generic image + host Download host image + subnet Download subnet generic image +""" +from robottelo.cli.base import Base + + +class Bootdisk(Base): + """Manipulates Bootdisk.""" + + command_base = 'bootdisk' + + @classmethod + def generic(cls, options=None): + """Download generic image""" + cls.command_sub = 'generic' + return cls.execute(cls._construct_command(options), output_format='json') + + @classmethod + def host(cls, options=None): + """Download host image""" + cls.command_sub = 'host' + return cls.execute(cls._construct_command(options), output_format='json') + + @classmethod + def subnet(cls, options=None): + """Download subnet generic image""" + cls.command_sub = 'subnet' + return cls.execute(cls._construct_command(options), output_format='json') diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index c2f305a7a15..f146c899c41 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -199,6 +199,8 @@ class Colored(Box): INSTALL_MEDIUM_URL = "http://mirror.fakeos.org/%s/$major.$minor/os/$arch" +HTTPS_MEDIUM_URL = "https://partha.fedorapeople.org/test-repos/kickstart-zoo" + VALID_GPG_KEY_FILE = "valid_gpg_key.txt" ZOO_CUSTOM_GPG_KEY = "zoo_custom_gpgkey.txt" diff --git a/tests/foreman/cli/test_bootdisk.py b/tests/foreman/cli/test_bootdisk.py new file mode 100644 index 00000000000..698ebbe1a75 --- /dev/null +++ b/tests/foreman/cli/test_bootdisk.py @@ -0,0 +1,92 @@ +"""Tests for BootdiskPlugin + +:Requirement: Bootdisk + +:CaseAutomation: Automated + +:CaseLevel: System + +:CaseComponent: BootdiskPlugin + +:Team: Rocket + +:TestType: Functional + +:CaseImportance: High + +:Upstream: No +""" +import pytest +from fauxfactory import gen_mac +from fauxfactory import gen_string + +from robottelo.config import settings +from robottelo.constants import HTTPS_MEDIUM_URL + + +@pytest.mark.parametrize('module_sync_kickstart_content', [7, 8, 9], indirect=True) +def test_positive_bootdisk_download_https( + module_location, + module_sync_kickstart_content, + module_provisioning_capsule, + module_target_sat, + module_sca_manifest_org, + module_default_org_view, + module_lce_library, + default_architecture, + default_partitiontable, +): + """Verify bootdisk is able to download using https url media . + + :id: ebced3cf-99e8-4ed6-a41d-d53789e90f8e + + :steps: + 1. Create a host with https url media + 2. Download the full host bootdisk + 3. Check if bootdisk is downloaded properly + + :expectedresults: Full host bootdisk is downloaded properly + + :BZ: 2173199 + + :customerscenario: true + + :parametrized: yes + """ + + macaddress = gen_mac(multicast=False) + capsule = module_target_sat.nailgun_smart_proxy + # create medium with https url + media = module_target_sat.cli_factory.make_medium( + { + 'name': gen_string('alpha'), + 'operatingsystem-ids': module_sync_kickstart_content.os.id, + 'location-ids': module_provisioning_capsule.id, + 'organization-ids': module_sca_manifest_org.id, + 'path': HTTPS_MEDIUM_URL, + 'os-family': 'Redhat', + } + ) + host = module_target_sat.cli_factory.make_host( + { + 'organization-id': module_sca_manifest_org.id, + 'location-id': module_location.id, + 'name': gen_string('alpha').lower(), + 'mac': macaddress, + 'operatingsystem-id': module_sync_kickstart_content.os.id, + 'medium-id': media['id'], + 'architecture-id': default_architecture.id, + 'domain-id': module_sync_kickstart_content.domain.id, + 'build': 'true', + 'root-password': settings.provisioning.host_root_password, + 'partition-table-id': default_partitiontable.id, + 'content-source-id': capsule.id, + 'content-view-id': module_default_org_view.id, + 'lifecycle-environment-id': module_lce_library.id, + } + ) + # Check if full-host bootdisk can be downloaded. + bootdisk = module_target_sat.cli.Bootdisk.host({'host-id': host['id'], 'full': 'true'}) + assert 'Successfully downloaded host disk image' in bootdisk['message'] + module_target_sat.api.Host(id=host.id).delete() + module_target_sat.api.Media(id=media['id']).delete() From 721b9623f14108d5acf0e94f881c0cc75bece714 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 13 Jun 2023 03:22:29 -0400 Subject: [PATCH 022/586] [6.14.z] remove rhel6 support from scap test and skip test due to bz 2211437 (#11593) --- tests/foreman/longrun/test_oscap.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/foreman/longrun/test_oscap.py b/tests/foreman/longrun/test_oscap.py index 1a4feef9288..907fca35e69 100644 --- a/tests/foreman/longrun/test_oscap.py +++ b/tests/foreman/longrun/test_oscap.py @@ -132,10 +132,11 @@ def update_scap_content(module_org): Scapcontent.update({'title': content['title'], 'organization-ids': organization_ids}) +@pytest.mark.skip_if_open('BZ:2211437') @pytest.mark.e2e @pytest.mark.upgrade @pytest.mark.tier4 -@pytest.mark.parametrize('distro', ['rhel6', 'rhel7', 'rhel8']) +@pytest.mark.parametrize('distro', ['rhel7', 'rhel8']) def test_positive_oscap_run_via_ansible( module_org, default_proxy, content_view, lifecycle_env, distro, target_sat ): @@ -164,10 +165,7 @@ def test_positive_oscap_run_via_ansible( :CaseImportance: Critical """ - if distro == 'rhel6': - rhel_repo = settings.repos.rhel6_os - profile = OSCAP_PROFILE['dsrhel6'] - elif distro == 'rhel7': + if distro == 'rhel7': rhel_repo = settings.repos.rhel7_os profile = OSCAP_PROFILE['security7'] else: @@ -219,7 +217,7 @@ def test_positive_oscap_run_via_ansible( 'parameter-type': 'boolean', } ) - if distro not in ('rhel6', 'rhel7'): + if distro not in ('rhel7'): vm.create_custom_repos(**rhel_repo) else: vm.create_custom_repos(**{distro: rhel_repo}) @@ -257,6 +255,7 @@ def test_positive_oscap_run_via_ansible( assert result is not None +@pytest.mark.skip_if_open('BZ:2211437') @pytest.mark.tier4 def test_positive_oscap_run_via_ansible_bz_1814988( module_org, default_proxy, content_view, lifecycle_env, target_sat From 6ad93c4a728d76cd00b2456cd5df051a9c8cb3b2 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 13 Jun 2023 05:08:19 -0400 Subject: [PATCH 023/586] [6.14.z] Bump pytest-reportportal from 5.1.8 to 5.1.9 (#11636) Bump pytest-reportportal from 5.1.8 to 5.1.9 (#11634) (cherry picked from commit 6fc759d60a7e4f976869bc98dec2a3d5428da167) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f1908c89bc8..9f682b6b5fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ python-box==7.0.1 pytest==7.3.2 pytest-services==2.2.1 pytest-mock==3.10.0 -pytest-reportportal==5.1.8 +pytest-reportportal==5.1.9 pytest-xdist==3.3.1 pytest-ibutsu==2.2.4 PyYAML==6.0 From 7c3e81da285760a7a9db3e943dccd1e98d76c9ea Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 13 Jun 2023 08:57:40 -0400 Subject: [PATCH 024/586] [6.14.z] Fix RHSSO bugs. (#11627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix RHSSO bugs. (#11546) 1) Fix setting a redirect URL to the actual Satellite 2) Fix issues setting client access to public (cherry picked from commit b634ac28362cf107e10e2d5db1b686402ad27cd1) Co-authored-by: Lukáš Hellebrandt --- pytest_fixtures/component/satellite_auth.py | 4 ++-- robottelo/hosts.py | 13 ++++++++++--- tests/foreman/destructive/test_ldapauthsource.py | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/pytest_fixtures/component/satellite_auth.py b/pytest_fixtures/component/satellite_auth.py index 73522eb4c33..e37c6cb9609 100644 --- a/pytest_fixtures/component/satellite_auth.py +++ b/pytest_fixtures/component/satellite_auth.py @@ -374,9 +374,9 @@ def rhsso_setting_setup(module_target_sat): rhhso_settings = { 'authorize_login_delegation': True, 'authorize_login_delegation_auth_source_user_autocreate': 'External', - 'login_delegation_logout_url': f'https://{settings.server.hostname}/users/extlogout', + 'login_delegation_logout_url': f'https://{module_target_sat.hostname}/users/extlogout', 'oidc_algorithm': 'RS256', - 'oidc_audience': [f'{settings.server.hostname}-foreman-openidc'], + 'oidc_audience': [f'{module_target_sat.hostname}-foreman-openidc'], 'oidc_issuer': f'{settings.rhsso.host_url}/auth/realms/{settings.rhsso.realm}', 'oidc_jwks_url': f'{settings.rhsso.host_url}/auth/realms' f'/{settings.rhsso.realm}/protocol/openid-connect/certs', diff --git a/robottelo/hosts.py b/robottelo/hosts.py index f3f1d1ba337..14412638e5a 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -2054,9 +2054,16 @@ def get_rhsso_groups_details(self, group_name): return query_group[0] def upload_rhsso_entity(self, json_content, entity_name): - """Helper method upload the entity json request as file on RHSSO Server""" + """Helper method to upload the RHSSO entity file on RHSSO Server. + Overwrites already existing file with the same name. + """ with open(entity_name, "w") as file: json.dump(json_content, file) + # Before uploading a file, remove the file of the same name. In sftp_write, + # if uploading a file of length n when there was already uploaded a file with + # the same name of length m, for n Date: Tue, 13 Jun 2023 17:37:45 -0400 Subject: [PATCH 025/586] [6.14.z] Fix activation key setup (#11670) --- pytest_fixtures/component/repository.py | 4 ++- robottelo/host_helpers/repository_mixins.py | 30 +++++++++++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/pytest_fixtures/component/repository.py b/pytest_fixtures/component/repository.py index fa7f479aafe..87164fe5f32 100644 --- a/pytest_fixtures/component/repository.py +++ b/pytest_fixtures/component/repository.py @@ -229,5 +229,7 @@ def module_repos_collection_with_manifest(request, module_target_sat, module_org for repo_name, repo_params in repo.items() ], ) - _repos_collection.setup_content(module_org.id, module_lce.id, upload_manifest=True) + _repos_collection.setup_content( + module_org.id, module_lce.id, upload_manifest=True, override=True + ) return _repos_collection diff --git a/robottelo/host_helpers/repository_mixins.py b/robottelo/host_helpers/repository_mixins.py index 9d032c4727d..e10db0b50f8 100644 --- a/robottelo/host_helpers/repository_mixins.py +++ b/robottelo/host_helpers/repository_mixins.py @@ -590,7 +590,9 @@ def setup_content_view(self, org_id, lce_id=None): content_view = self.satellite.cli.ContentView.info({'id': content_view['id']}) return content_view, lce - def setup_activation_key(self, org_id, content_view_id, lce_id, subscription_names=None): + def setup_activation_key( + self, org_id, content_view_id, lce_id, subscription_names=None, override=None + ): """Create activation and associate content-view, lifecycle environment and subscriptions""" if subscription_names is None: @@ -602,6 +604,19 @@ def setup_activation_key(self, org_id, content_view_id, lce_id, subscription_nam 'content-view-id': content_view_id, } ) + if override is not None: + for repo in self.satellite.cli.ActivationKey.product_content( + {'id': activation_key['id'], 'content-access-mode-all': 1} + ): + self.satellite.cli.ActivationKey.content_override( + { + 'id': activation_key['id'], + 'content-label': repo['label'], + 'value': int(override), + } + ) + if self.satellite.is_sca_mode_enabled(org_id): + return activation_key # Add subscriptions to activation-key # Get organization subscriptions subscriptions = self.satellite.cli.Subscription.list( @@ -646,17 +661,18 @@ def setup_content( upload_manifest=False, download_policy='on_demand', rh_subscriptions=None, + override=None, ): """ Setup content view and activation key of all the repositories. :param org_id: The organization id - :param lce_id: The lifecycle environment id + :param lce_id: The lifecycle environment id :param upload_manifest: Whether to upload the manifest (The manifest is uploaded only if needed) :param download_policy: The repositories download policy - :param rh_subscriptions: The RH subscriptions to be added to activation - key + :param rh_subscriptions: The RH subscriptions to be added to activation key + :param override: Content override (True = enable, False = disable, None = no action) """ if self._repos_info: raise RepositoryAlreadyCreated('Repositories already created can not setup content') @@ -676,7 +692,11 @@ def setup_content( if custom_product_name: subscription_names.append(custom_product_name) activation_key = self.setup_activation_key( - org_id, content_view['id'], lce_id, subscription_names=subscription_names + org_id, + content_view['id'], + lce_id, + subscription_names=subscription_names, + override=override, ) setup_content_data = dict( activation_key=activation_key, From e8d97802dc216ffa728b18acebd5d4a28f89aa05 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 14 Jun 2023 02:58:07 -0400 Subject: [PATCH 026/586] [6.14.z] Bump pre-commit from 3.3.2 to 3.3.3 (#11674) Bump pre-commit from 3.3.2 to 3.3.3 (#11671) (cherry picked from commit ea6df1c77714e5bc17b9b3b0cbe56fe5b30a1e61) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 357338866ae..674c469fa95 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -2,7 +2,7 @@ flake8==6.0.0 pytest-cov==3.0.0 redis==4.5.5 -pre-commit==3.3.2 +pre-commit==3.3.3 # For generating documentation. sphinx==7.0.1 From a6e3f5ae2bdc7406b5c910a4f5417adf4550e392 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 14 Jun 2023 10:38:57 -0400 Subject: [PATCH 027/586] [6.14.z] `rp_api_key` replaced `rp_uuid` in RP pytest plugin config (#11679) `rp_api_key` replaced `rp_uuid` in RP pytest plugin config (#11678) rp_api_key replaced rp_uuid in RP pytest plugin config (cherry picked from commit cff4255d4415a9b73e4df1409188cf1f0b7040db) Co-authored-by: Jitendra Yejare --- pytest_plugins/rerun_rp/rerun_rp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest_plugins/rerun_rp/rerun_rp.py b/pytest_plugins/rerun_rp/rerun_rp.py index b2d92951419..5b14873a885 100644 --- a/pytest_plugins/rerun_rp/rerun_rp.py +++ b/pytest_plugins/rerun_rp/rerun_rp.py @@ -73,7 +73,7 @@ def pytest_collection_modifyitems(items, config): failed/skipped and user-specific tests in Report Portal """ rp_url = settings.report_portal.portal_url or config.getini('rp_endpoint') - rp_uuid = config.getini('rp_uuid') or settings.report_portal.api_key + rp_api_key = config.getini('rp_api_key') or settings.report_portal.api_key # prefer dynaconf setting before ini config as pytest-reportportal plugin uses default value # for `rp_launch` if none is set there rp_launch_name = settings.report_portal.launch_name or config.getini('rp_launch') @@ -87,7 +87,7 @@ def pytest_collection_modifyitems(items, config): tests = [] if not any([fail_args, skip_arg, user_arg]): return - rp = ReportPortal(rp_url=rp_url, rp_api_key=rp_uuid, rp_project=rp_project) + rp = ReportPortal(rp_url=rp_url, rp_api_key=rp_api_key, rp_project=rp_project) if ref_launch_uuid: logger.info(f'Fetching A reference Report Portal launch {ref_launch_uuid}') From 65c913d9da2661938a034c05983f7d9912c8945a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 15 Jun 2023 09:40:01 -0400 Subject: [PATCH 028/586] [6.14.z] Close-loop BZ 1955421 (#11686) --- tests/foreman/cli/test_host.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/foreman/cli/test_host.py b/tests/foreman/cli/test_host.py index 211c37b3f10..8c4ad9469c6 100644 --- a/tests/foreman/cli/test_host.py +++ b/tests/foreman/cli/test_host.py @@ -55,6 +55,7 @@ from robottelo.constants import REPOSET from robottelo.constants import SM_OVERALL_STATUS from robottelo.hosts import ContentHostError +from robottelo.logging import logger from robottelo.utils.datafactory import invalid_values_list from robottelo.utils.datafactory import valid_data_list from robottelo.utils.datafactory import valid_hosts_list @@ -872,7 +873,7 @@ def test_positive_list_with_nested_hostgroup(target_sat): :expectedresults: Host is successfully listed and has both parent and nested host groups names in its hostgroup parameter - :BZ: 1427554 + :BZ: 1427554, 1955421 :CaseLevel: System """ @@ -886,11 +887,13 @@ def test_positive_list_with_nested_hostgroup(target_sat): content_view.publish() content_view.read().version[0].promote(data={'environment_ids': lce.id, 'force': False}) parent_hg = target_sat.api.HostGroup( - name=parent_hg_name, organization=[options.organization] + name=parent_hg_name, + organization=[options.organization], + content_view=content_view, + ptable=options.ptable, ).create() nested_hg = target_sat.api.HostGroup( architecture=options.architecture, - content_view=content_view, domain=options.domain, lifecycle_environment=lce, location=[options.location], @@ -899,7 +902,6 @@ def test_positive_list_with_nested_hostgroup(target_sat): operatingsystem=options.operatingsystem, organization=[options.organization], parent=parent_hg, - ptable=options.ptable, ).create() make_host( { @@ -911,6 +913,16 @@ def test_positive_list_with_nested_hostgroup(target_sat): ) hosts = Host.list({'organization-id': options.organization.id}) assert f'{parent_hg_name}/{nested_hg_name}' == hosts[0]['host-group'] + host = Host.info({'id': hosts[0]['id']}) + logger.info(f'Host info: {host}') + assert host['operating-system']['medium'] == options.medium.name + assert host['operating-system']['partition-table'] == options.ptable.name # inherited + if not is_open('BZ:2215294') or target_sat.version != 'stream': + assert 'id' in host['content-information']['lifecycle-environment'] + assert int(host['content-information']['lifecycle-environment']['id']) == int(lce.id) + assert int(host['content-information']['content-view']['id']) == int( + content_view.id + ) # inherited @pytest.mark.cli_host_create From f1730197b600a73e0ce04b88b801a0dcbe6ac169 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 19 Jun 2023 03:13:07 -0400 Subject: [PATCH 029/586] [6.14.z] CI declines colorcodes and so for req assist plugin (#11700) CI declines colorcodes and so for req assist plugin (#11677) (cherry picked from commit fe3e5c96a64ca5257dbb84dc06473d1be7788f72) Co-authored-by: Jitendra Yejare --- .../requirements/update_requirements.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/pytest_plugins/requirements/update_requirements.py b/pytest_plugins/requirements/update_requirements.py index a930d95fe33..964b1025289 100644 --- a/pytest_plugins/requirements/update_requirements.py +++ b/pytest_plugins/requirements/update_requirements.py @@ -1,6 +1,5 @@ """Plugin enables pytest to notify and update the requirements""" from .req_updater import ReqUpdater -from robottelo.constants import Colored updater = ReqUpdater() @@ -29,23 +28,23 @@ def pytest_report_header(config): # pytest tests/foreman --collect-only --upgrade-all-reqs """ if updater.req_deviation: - print( - f"{Colored.REDLIGHT}Mandatory Requirements Available: " - f"{' '.join(updater.req_deviation)}{Colored.RESET}" - ) + print(f"Mandatory Requirements Mismatch: {' '.join(updater.req_deviation)}") if config.getoption('update_required_reqs') or config.getoption('update_all_reqs'): - print('Updating the mandatory requirements on demand ....') updater.install_req_deviations() + print('Mandatory requirements are installed to be up-to-date.') else: - print(f'{Colored.GREEN}Mandatory Requirements are up to date!{Colored.RESET}') + print('Mandatory Requirements are up to date.') if updater.opt_deviation: - print( - f"{Colored.REDLIGHT}Optional Requirements Available: " - f"{' '.join(updater.opt_deviation)}{Colored.RESET}" - ) + print(f"Optional Requirements Mismatch: {' '.join(updater.opt_deviation)}") if config.getoption('update_all_reqs'): - print('Updating the optional requirements on demand ....') updater.install_opt_deviations() + print('Optional requirements are installed to be up-to-date.') else: - print(f'{Colored.GREEN}Optional Requirements are up to date!{Colored.RESET}') + print('Optional Requirements are up to date.') + + if updater.req_deviation or updater.opt_deviation: + print( + "To update mismatched requirements, run the pytest command with " + "'--upgrade-required-reqs' OR '--upgrade-all-reqs' option." + ) From fba8fe23dcd5df6e0a32191e38ae8400035c5445 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 19 Jun 2023 03:17:00 -0400 Subject: [PATCH 030/586] [6.14.z] Bump pytest-mock from 3.10.0 to 3.11.1 (#11690) Bump pytest-mock from 3.10.0 to 3.11.1 (#11689) (cherry picked from commit 7926cededdb90ae3ba0406051631d7411a7cd23b) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9f682b6b5fa..8698888a9b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ pyotp==2.8.0 python-box==7.0.1 pytest==7.3.2 pytest-services==2.2.1 -pytest-mock==3.10.0 +pytest-mock==3.11.1 pytest-reportportal==5.1.9 pytest-xdist==3.3.1 pytest-ibutsu==2.2.4 From e41d8949e8ddb11d8f5bcf6184f1a6ff3c8999db Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 19 Jun 2023 03:59:33 -0400 Subject: [PATCH 031/586] [6.14.z] Add test for orphaned content removal from capsule (#11703) --- tests/foreman/api/test_capsulecontent.py | 90 ++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/tests/foreman/api/test_capsulecontent.py b/tests/foreman/api/test_capsulecontent.py index b75784f6ed5..732aac21655 100644 --- a/tests/foreman/api/test_capsulecontent.py +++ b/tests/foreman/api/test_capsulecontent.py @@ -1276,3 +1276,93 @@ def test_positive_capsule_sync_status_persists( # Check sync status again, and ensure last_sync_time is still correct sync_status = module_capsule_configured.nailgun_capsule.content_get_sync() assert sync_status['last_sync_time'] >= timestamp + + @pytest.mark.tier4 + @pytest.mark.skip_if_not_set('capsule') + def test_positive_remove_capsule_orphans( + self, + target_sat, + capsule_configured, + function_entitlement_manifest_org, + function_lce_library, + ): + """Synchronize RPM content to the capsule, disassociate the capsule form the content + source and resync, run orphan cleanup and ensure the RPM artifacts were removed. + + :id: 7089a36e-ea68-47ad-86ac-9945b732b0c4 + + :setup: + 1. A blank external capsule that has not been synced yet with immediate download policy. + + :steps: + 1. Enable RHST repo and sync it to the Library LCE. + 2. Set immediate download policy to the capsule, assign it the Library LCE and sync it. + Ensure the RPM artifacts were created. + 3. Remove the Library LCE from the capsule and resync it. + 4. Run orphan cleanup for the capsule. + 5. Ensure the artifacts were removed. + + :expectedresults: + 1. RPM artifacts are created after capsule sync. + 2. RPM artifacts are removed after orphan cleanup. + + :customerscenario: true + + :BZ: 22043089, 2211962 + + """ + # Enable RHST repo and sync it to the Library LCE. + repo_id = target_sat.api_factory.enable_rhrepo_and_fetchid( + basearch='x86_64', + org_id=function_entitlement_manifest_org.id, + product=constants.REPOS['rhst8']['product'], + repo=constants.REPOS['rhst8']['name'], + reposet=constants.REPOSET['rhst8'], + ) + repo = target_sat.api.Repository(id=repo_id).read() + repo.sync() + + # Set immediate download policy to the capsule, assign it the Library LCE and sync it. + proxy = capsule_configured.nailgun_smart_proxy.read() + proxy.download_policy = 'immediate' + proxy.update(['download_policy']) + + capsule_configured.nailgun_capsule.content_add_lifecycle_environment( + 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 + + sync_status = capsule_configured.nailgun_capsule.content_sync() + assert sync_status['result'] == 'success', 'Capsule sync task failed.' + + # Ensure the RPM artifacts were created. + result = capsule_configured.execute( + 'ls /var/lib/pulp/media/artifact/*/* | xargs file | grep RPM' + ) + assert not result.status, 'RPM artifacts are missing after capsule sync.' + + # Remove the Library LCE from the capsule and resync it. + capsule_configured.nailgun_capsule.content_delete_lifecycle_environment( + data={'environment_id': function_lce_library.id} + ) + sync_status = capsule_configured.nailgun_capsule.content_sync() + assert sync_status['result'] == 'success', 'Capsule sync task failed.' + + # Run orphan cleanup for the capsule. + target_sat.execute( + 'foreman-rake katello:delete_orphaned_content RAILS_ENV=production ' + f'SMART_PROXY_ID={capsule_configured.nailgun_capsule.id}' + ) + target_sat.wait_for_tasks( + search_query=('label = Actions::Katello::OrphanCleanup::RemoveOrphans'), + search_rate=5, + max_tries=10, + ) + + # Ensure the artifacts were removed. + result = capsule_configured.execute( + 'ls /var/lib/pulp/media/artifact/*/* | xargs file | grep RPM' + ) + assert result.status, 'RPM artifacts are still present. They should be gone.' From 38149e726a6043540c2aad90c1b5e69c18360b43 Mon Sep 17 00:00:00 2001 From: vsedmik <46570670+vsedmik@users.noreply.github.com> Date: Mon, 19 Jun 2023 13:32:08 +0200 Subject: [PATCH 032/586] [6.14.z] Add treeinfo ignore test (#11705) Add treeinfo ignore test --- tests/foreman/api/test_repository.py | 51 ++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index 25753b46a8a..db450b4b231 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -1249,6 +1249,57 @@ def test_positive_mirroring_policy(self, target_sat): assert len(files) == packages_count assert constants.RPM_TO_UPLOAD not in files + @pytest.mark.tier3 + @pytest.mark.parametrize('policy', ['additive', 'mirror_content_only']) + def test_positive_sync_with_treeinfo_ignore( + self, target_sat, function_entitlement_manifest_org, policy + ): + """Verify that the treeinfo file is not synced when added to ignorable content + and synced otherwise. Check for applicable mirroring policies. + + :id: d7becf1d-3883-468d-88c4-d513a2e2e90a + + :parametrized: yes + + :steps: + 1. Enable RHEL8 BaseOS KS repo. + 2. Add `treeinfo` to ignorable content and sync, check it's missing. + 3. Remove the `treeinfo` from ignorable content, resync, check again. + + :expectedresults: + 1. The sync should succeed. + 2. The treeinfo file should be missing when in ignorable content and present otherwise. + + :customerscenario: true + + :BZ: 2174912, 2135215 + + """ + distro = 'rhel8_bos' + repo_id = target_sat.api_factory.enable_rhrepo_and_fetchid( + basearch='x86_64', + org_id=function_entitlement_manifest_org.id, + product=constants.REPOS['kickstart'][distro]['product'], + reposet=constants.REPOS['kickstart'][distro]['reposet'], + repo=constants.REPOS['kickstart'][distro]['name'], + releasever=constants.REPOS['kickstart'][distro]['version'], + ) + repo = target_sat.api.Repository(id=repo_id).read() + + repo.mirroring_policy = policy + repo.ignorable_content = ['treeinfo'] + repo = repo.update(['mirroring_policy', 'ignorable_content']) + repo.sync() + with pytest.raises(AssertionError): + target_sat.md5_by_url(f'{repo.full_path}.treeinfo') + + repo.ignorable_content = [] + repo = repo.update(['ignorable_content']) + repo.sync() + assert target_sat.md5_by_url( + f'{repo.full_path}.treeinfo' + ), 'The treeinfo file is missing in the KS repo but it should be there.' + @pytest.mark.run_in_one_thread class TestRepositorySync: From bf0989723361a51b38526a023c292b6a6fa599a1 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 19 Jun 2023 11:21:18 -0400 Subject: [PATCH 033/586] [6.14.z] Update foreman-installer command line options list (#11709) --- tests/foreman/installer/test_installer.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/foreman/installer/test_installer.py b/tests/foreman/installer/test_installer.py index 5e67a993ddd..7f66211f96d 100644 --- a/tests/foreman/installer/test_installer.py +++ b/tests/foreman/installer/test_installer.py @@ -496,9 +496,10 @@ '--puppet-agent-default-schedules', '--puppet-agent-noop', '--puppet-agent-restart-command', + '--puppet-agent-server-hostname', + '--puppet-agent-server-port', '--puppet-allow-any-crl-auth', '--puppet-auth-allowed', - '--puppet-auth-template', '--puppet-autosign', '--puppet-autosign-content', '--puppet-autosign-entries', @@ -530,11 +531,9 @@ '--puppet-package-source', '--puppet-pluginfactsource', '--puppet-pluginsource', - '--puppet-port', '--puppet-postrun-command', '--puppet-prerun-command', '--puppet-puppetconf-mode', - '--puppet-puppetmaster', '--puppet-report', '--puppet-run-hour', '--puppet-run-minute', @@ -648,7 +647,6 @@ '--puppet-server-storeconfigs', '--puppet-server-strict-variables', '--puppet-server-trusted-external-command', - '--puppet-server-use-legacy-auth-conf', '--puppet-server-user', '--puppet-server-version', '--puppet-server-versioned-code-content', @@ -1072,9 +1070,10 @@ '--reset-puppet-agent-default-schedules', '--reset-puppet-agent-noop', '--reset-puppet-agent-restart-command', + '--reset-puppet-agent-server-hostname', + '--reset-puppet-agent-server-port', '--reset-puppet-allow-any-crl-auth', '--reset-puppet-auth-allowed', - '--reset-puppet-auth-template', '--reset-puppet-autosign', '--reset-puppet-autosign-content', '--reset-puppet-autosign-entries', @@ -1106,11 +1105,9 @@ '--reset-puppet-package-source', '--reset-puppet-pluginfactsource', '--reset-puppet-pluginsource', - '--reset-puppet-port', '--reset-puppet-postrun-command', '--reset-puppet-prerun-command', '--reset-puppet-puppetconf-mode', - '--reset-puppet-puppetmaster', '--reset-puppet-report', '--reset-puppet-run-hour', '--reset-puppet-run-minute', @@ -1224,7 +1221,6 @@ '--reset-puppet-server-storeconfigs', '--reset-puppet-server-strict-variables', '--reset-puppet-server-trusted-external-command', - '--reset-puppet-server-use-legacy-auth-conf', '--reset-puppet-server-user', '--reset-puppet-server-version', '--reset-puppet-server-versioned-code-content', From 5442124611f86e902c2f9c11ee590d3994eda0ba Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 20 Jun 2023 09:41:30 -0400 Subject: [PATCH 034/586] [6.14.z] Fix failing foreman/maintain/advanced test (#11711) --- tests/foreman/maintain/test_advanced.py | 35 +++++++++++-------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/tests/foreman/maintain/test_advanced.py b/tests/foreman/maintain/test_advanced.py index ab027452d48..a95e6b6adb9 100644 --- a/tests/foreman/maintain/test_advanced.py +++ b/tests/foreman/maintain/test_advanced.py @@ -45,6 +45,11 @@ 'satellite-maintenance-6.13-for-rhel-8-x86_64-rpms', ] + common_repos +sat_614_repos = [ + 'satellite-6.14-for-rhel-8-x86_64-rpms', + 'satellite-maintenance-6.14-for-rhel-8-x86_64-rpms', +] + common_repos + # Capsule repositories cap_611_repos = [ 'satellite-capsule-6.11-for-rhel-8-x86_64-rpms', @@ -61,15 +66,22 @@ 'satellite-maintenance-6.13-for-rhel-8-x86_64-rpms', ] + common_repos +cap_614_repos = [ + 'satellite-capsule-6.14-for-rhel-8-x86_64-rpms', + 'satellite-maintenance-6.14-for-rhel-8-x86_64-rpms', +] + common_repos + sat_repos = { '6.11': sat_611_repos, '6.12': sat_612_repos, '6.13': sat_613_repos, + '6.14': sat_614_repos, } cap_repos = { '6.11': cap_611_repos, '6.12': cap_612_repos, '6.13': cap_613_repos, + '6.14': cap_614_repos, } @@ -379,7 +391,7 @@ def test_positive_satellite_repositories_setup(sat_maintain): :expectedresults: Required Satellite repositories for install/upgrade should get enabled """ - supported_versions = ['6.11', '6.12'] + supported_versions = ['6.11', '6.12', '6.13'] for ver in supported_versions: result = sat_maintain.cli.Advanced.run_repositories_setup(options={'version': ver}) assert result.status == 0 @@ -388,26 +400,11 @@ def test_positive_satellite_repositories_setup(sat_maintain): for repo in sat_repos[ver]: assert repo in result.stdout - # 6.13 till not GA - result = sat_maintain.cli.Advanced.run_repositories_setup(options={'version': '6.13'}) + # 6.14 till not GA + result = sat_maintain.cli.Advanced.run_repositories_setup(options={'version': '6.14'}) assert result.status == 1 assert 'FAIL' in result.stdout - for repo in sat_repos['6.13']: - assert repo in result.stdout - - # Verify that all required beta repositories gets enabled - # maintain beta repo is unavailable for EL8 https://bugzilla.redhat.com/show_bug.cgi?id=2106750 - sat_beta_repo = ['satellite-6-beta-for-rhel-8-x86_64-rpms'] + common_repos - missing_beta_el8_repos = ['satellite-maintenance-6-beta-for-rhel-8-x86_64-rpms'] - result = sat_maintain.cli.Advanced.run_repositories_setup( - options={'version': '6.12'}, env_var='FOREMAN_MAINTAIN_USE_BETA=1' - ) - assert result.status != 0 - assert 'FAIL' in result.stdout - for repo in missing_beta_el8_repos: - assert f"Error: '{repo}' does not match a valid repository ID" in result.stdout - result = sat_maintain.execute('yum repolist') - for repo in sat_beta_repo: + for repo in sat_repos['6.14']: assert repo in result.stdout From e5193b921992c8e77fbc2e80b0c307d4d9a8d3ee Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 20 Jun 2023 10:10:34 -0400 Subject: [PATCH 035/586] [6.14.z] Fix Katello-tracer component case (#11713) Fix Katello-tracer component case (#11708) (cherry picked from commit cc45f55c51218732db2c39b8cbe58a9efc9a555f) Co-authored-by: vsedmik <46570670+vsedmik@users.noreply.github.com> --- testimony.yaml | 2 +- tests/foreman/cli/test_host.py | 4 +++- tests/foreman/ui/test_host.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/testimony.yaml b/testimony.yaml index 6771b38ebc4..e698eed5979 100644 --- a/testimony.yaml +++ b/testimony.yaml @@ -65,7 +65,7 @@ CaseComponent: - Installer - InterSatelliteSync - katello-agent - - katello-tracer + - Katello-tracer - LDAP - Leappintegration - LifecycleEnvironments diff --git a/tests/foreman/cli/test_host.py b/tests/foreman/cli/test_host.py index 8c4ad9469c6..4842966f020 100644 --- a/tests/foreman/cli/test_host.py +++ b/tests/foreman/cli/test_host.py @@ -2504,7 +2504,9 @@ def test_positive_tracer_list_and_resolve(tracer_host): :CaseImportance: Medium - :CaseComponent: katello-tracer + :CaseComponent: Katello-tracer + + :Team: Phoenix-subscriptions :bz: 2186188 """ diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index bad6f2f5307..2c715d490a0 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -2599,9 +2599,9 @@ def test_positive_tracer_enable_reload(tracer_install_host, target_sat): :id: c9ebd4a8-6db3-4d0e-92a2-14951c26769b - :caseComponent: katello-tracer + :CaseComponent: Katello-tracer - :Team: Phoenix + :Team: Phoenix-subscriptions :CaseLevel: System From bdcfbd02e987c2acdf741965612a65472c8fbdc7 Mon Sep 17 00:00:00 2001 From: omkarkhatavkar Date: Fri, 16 Jun 2023 16:19:58 +0530 Subject: [PATCH 036/586] added the fix for the automerge cherry-pick dependabot PR's --- .github/workflows/auto_cherry_pick.yml | 13 +++++++++++++ .github/workflows/dependency_merge.yml | 3 +-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto_cherry_pick.yml b/.github/workflows/auto_cherry_pick.yml index a419a5bc476..c8f087bf3a0 100644 --- a/.github/workflows/auto_cherry_pick.yml +++ b/.github/workflows/auto_cherry_pick.yml @@ -59,6 +59,19 @@ jobs: No-CherryPick assignees: ${{ env.assignee }} + - name: Add dependencies label, if merged pr author is dependabot[bot] + id: dependencies + if: | + contains(github.event.pull_request.labels.*.name, 'dependencies') && + github.event.pull_request.user.login == 'dependabot[bot]' + uses: jyejare/github-cherry-pick-action@main + with: + token: ${{ secrets.CHERRYPICK_PAT }} + branch: ${{ matrix.label }} + labels: | + dependencies + assignees: ${{ env.assignee }} + - name: Add Parent PR's PRT comment to Auto_Cherry_Picked PR's id: add-parent-prt-comment if: ${{ always() && steps.cherrypick.outcome == 'success' }} diff --git a/.github/workflows/dependency_merge.yml b/.github/workflows/dependency_merge.yml index f21f70fb201..ebe22b3ac74 100644 --- a/.github/workflows/dependency_merge.yml +++ b/.github/workflows/dependency_merge.yml @@ -9,8 +9,7 @@ jobs: name: dependabot-auto-merge runs-on: ubuntu-latest if: | - (github.event.pull_request.user.login == 'dependabot[bot]' || - github.event.pull_request.user.login == 'Satellite-QE') && + github.event.pull_request.user.login == 'Satellite-QE' && contains( github.event.pull_request.labels.*.name, 'dependencies') steps: From df29935e41727dad127b485a46bd30eca380ba34 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 21 Jun 2023 08:53:37 -0400 Subject: [PATCH 037/586] [6.14.z] Convert remaining tests to manifester (#11720) Convert remaining tests to manifester (#11669) * Convert remaining test to manifester This PR converts to manifester all tests using manifests to that are not contained in the API, CLI, or UI namespaces. * Convert CLI end-to-end test to manifester * Disable SCA in API end-to-end test * Convert longrun test to manifester (cherry picked from commit aa04b7d9edeeaa36c57f05661b7c0dcd6d8a83a7) Co-authored-by: synkd <48261305+synkd@users.noreply.github.com> --- tests/foreman/endtoend/test_api_endtoend.py | 45 +++++++-------- tests/foreman/endtoend/test_cli_endtoend.py | 64 ++++++++++----------- tests/foreman/longrun/test_inc_updates.py | 48 ++++++++++------ 3 files changed, 82 insertions(+), 75 deletions(-) diff --git a/tests/foreman/endtoend/test_api_endtoend.py b/tests/foreman/endtoend/test_api_endtoend.py index e8a166d3206..6cd332e3e54 100644 --- a/tests/foreman/endtoend/test_api_endtoend.py +++ b/tests/foreman/endtoend/test_api_endtoend.py @@ -1058,7 +1058,9 @@ def test_positive_find_admin_user(self): @pytest.mark.skipif( (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) - def test_positive_end_to_end(self, fake_manifest_is_set, target_sat, rhel7_contenthost): + def test_positive_end_to_end( + self, function_entitlement_manifest, target_sat, rhel7_contenthost + ): """Perform end to end smoke tests using RH and custom repos. 1. Create a new user with admin permissions @@ -1097,11 +1099,10 @@ def test_positive_end_to_end(self, fake_manifest_is_set, target_sat, rhel7_conte # step 2.1: Create a new organization user_cfg = user_nailgun_config(login, password) org = target_sat.api.Organization(server_config=user_cfg).create() + org.sca_disable() - # step 2.2: Clone and upload manifest - if fake_manifest_is_set: - with clone() as manifest: - target_sat.upload_manifest(org.id, manifest.content) + # step 2.2: Upload manifest + target_sat.upload_manifest(org.id, function_entitlement_manifest.content) # step 2.3: Create a new lifecycle environment le1 = target_sat.api.LifecycleEnvironment(server_config=user_cfg, organization=org).create() @@ -1117,17 +1118,16 @@ def test_positive_end_to_end(self, fake_manifest_is_set, target_sat, rhel7_conte repositories.append(custom_repo) # step 2.6: Enable a Red Hat repository - if fake_manifest_is_set: - rhel_repo = target_sat.api.Repository( - id=target_sat.api_factory.enable_rhrepo_and_fetchid( - basearch='x86_64', - org_id=org.id, - product=constants.PRDS['rhel'], - repo=constants.REPOS['rhst7']['name'], - reposet=constants.REPOSET['rhst7'], - ) + rhel_repo = target_sat.api.Repository( + id=target_sat.api_factory.enable_rhrepo_and_fetchid( + basearch='x86_64', + org_id=org.id, + product=constants.PRDS['rhel'], + repo=constants.REPOS['rhst7']['name'], + reposet=constants.REPOSET['rhst7'], ) - repositories.append(rhel_repo) + ) + repositories.append(rhel_repo) # step 2.7: Synchronize these two repositories for repo in repositories: @@ -1166,14 +1166,13 @@ def test_positive_end_to_end(self, fake_manifest_is_set, target_sat, rhel7_conte activation_key.add_subscriptions(data={'quantity': 1, 'subscription_id': sub.id}) break # step 2.13.1: Enable product content - if fake_manifest_is_set: - activation_key.content_override( - data={ - 'content_overrides': [ - {'content_label': constants.REPOS['rhst7']['id'], 'value': '1'} - ] - } - ) + activation_key.content_override( + data={ + 'content_overrides': [ + {'content_label': constants.REPOS['rhst7']['id'], 'value': '1'} + ] + } + ) # BONUS: Create a content host and associate it with promoted # content view and last lifecycle where it exists diff --git a/tests/foreman/endtoend/test_cli_endtoend.py b/tests/foreman/endtoend/test_cli_endtoend.py index 0baab0e65ab..d65fcd883f4 100644 --- a/tests/foreman/endtoend/test_cli_endtoend.py +++ b/tests/foreman/endtoend/test_cli_endtoend.py @@ -40,7 +40,6 @@ from robottelo.config import setting_is_set from robottelo.config import settings from robottelo.constants.repos import CUSTOM_RPM_REPO -from robottelo.utils.manifest import clone @pytest.fixture(scope='module') @@ -93,7 +92,7 @@ def test_positive_cli_find_admin_user(): @pytest.mark.e2e @pytest.mark.upgrade @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_cli_end_to_end(fake_manifest_is_set, target_sat, rhel7_contenthost): +def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel7_contenthost): """Perform end to end smoke tests using RH and custom repos. 1. Create a new user with admin permissions @@ -131,12 +130,13 @@ def test_positive_cli_end_to_end(fake_manifest_is_set, target_sat, rhel7_content # step 2.1: Create a new organization org = _create(user, Org, {'name': gen_alphanumeric()}) + target_sat.cli.SimpleContentAccess.disable({'organization-id': org['id']}) # step 2.2: Clone and upload manifest - if fake_manifest_is_set: - with clone() as manifest: - target_sat.put(manifest, manifest.filename) - Subscription.upload({'file': manifest.filename, 'organization-id': org['id']}) + target_sat.put(f'{function_entitlement_manifest.path}', f'{function_entitlement_manifest.name}') + Subscription.upload( + {'file': f'{function_entitlement_manifest.name}', 'organization-id': org['id']} + ) # step 2.3: Create a new lifecycle environment lifecycle_environment = _create( @@ -164,24 +164,23 @@ def test_positive_cli_end_to_end(fake_manifest_is_set, target_sat, rhel7_content repositories.append(custom_repo) # step 2.6: Enable a Red Hat repository - if fake_manifest_is_set: - RepositorySet.enable( - { - 'basearch': 'x86_64', - 'name': constants.REPOSET['rhst7'], - 'organization-id': org['id'], - 'product': constants.PRDS['rhel'], - 'releasever': None, - } - ) - rhel_repo = Repository.info( - { - 'name': constants.REPOS['rhst7']['name'], - 'organization-id': org['id'], - 'product': constants.PRDS['rhel'], - } - ) - repositories.append(rhel_repo) + RepositorySet.enable( + { + 'basearch': 'x86_64', + 'name': constants.REPOSET['rhst7'], + 'organization-id': org['id'], + 'product': constants.PRDS['rhel'], + 'releasever': None, + } + ) + rhel_repo = Repository.info( + { + 'name': constants.REPOS['rhst7']['name'], + 'organization-id': org['id'], + 'product': constants.PRDS['rhel'], + } + ) + repositories.append(rhel_repo) # step 2.7: Synchronize these two repositories for repo in repositories: @@ -255,15 +254,14 @@ def test_positive_cli_end_to_end(fake_manifest_is_set, target_sat, rhel7_content ) # step 2.13.1: Enable product content - if fake_manifest_is_set: - ActivationKey.with_user(user['login'], user['password']).content_override( - { - 'content-label': constants.REPOS['rhst7']['id'], - 'id': activation_key['id'], - 'organization-id': org['id'], - 'value': '1', - } - ) + ActivationKey.with_user(user['login'], user['password']).content_override( + { + 'content-label': constants.REPOS['rhst7']['id'], + 'id': activation_key['id'], + 'organization-id': org['id'], + 'value': '1', + } + ) # BONUS: Create a content host and associate it with promoted # content view and last lifecycle where it exists diff --git a/tests/foreman/longrun/test_inc_updates.py b/tests/foreman/longrun/test_inc_updates.py index fc3c7bf8c90..cb960fb0d5c 100644 --- a/tests/foreman/longrun/test_inc_updates.py +++ b/tests/foreman/longrun/test_inc_updates.py @@ -35,36 +35,41 @@ @pytest.fixture(scope='module') -def module_lce_library(module_manifest_org): +def module_lce_library(module_entitlement_manifest_org): """Returns the Library lifecycle environment from chosen organization""" return ( entities.LifecycleEnvironment() .search( - query={'search': f'name={ENVIRONMENT} and organization_id={module_manifest_org.id}'} + query={ + 'search': f'name={ENVIRONMENT} and ' + f'organization_id={module_entitlement_manifest_org.id}' + } )[0] .read() ) @pytest.fixture(scope='module') -def dev_lce(module_manifest_org): - return entities.LifecycleEnvironment(name='DEV', organization=module_manifest_org).create() +def dev_lce(module_entitlement_manifest_org): + return entities.LifecycleEnvironment( + name='DEV', organization=module_entitlement_manifest_org + ).create() @pytest.fixture(scope='module') -def qe_lce(module_manifest_org, dev_lce): +def qe_lce(module_entitlement_manifest_org, dev_lce): qe_lce = entities.LifecycleEnvironment( - name='QE', prior=dev_lce, organization=module_manifest_org + name='QE', prior=dev_lce, organization=module_entitlement_manifest_org ).create() return qe_lce @pytest.fixture(scope='module') -def rhel7_sat6tools_repo(module_manifest_org, module_target_sat): +def rhel7_sat6tools_repo(module_entitlement_manifest_org, module_target_sat): """Enable Sat tools repository""" rhel7_sat6tools_repo_id = module_target_sat.api_factory.enable_rhrepo_and_fetchid( basearch=DEFAULT_ARCHITECTURE, - org_id=module_manifest_org.id, + org_id=module_entitlement_manifest_org.id, product=PRDS['rhel'], repo=REPOS['rhst7']['name'], reposet=REPOSET['rhst7'], @@ -76,21 +81,22 @@ def rhel7_sat6tools_repo(module_manifest_org, module_target_sat): @pytest.fixture(scope='module') -def custom_repo(module_manifest_org): +def custom_repo(module_entitlement_manifest_org): """Enable custom errata repository""" custom_repo = entities.Repository( url=settings.repos.yum_9.url, - product=entities.Product(organization=module_manifest_org).create(), + product=entities.Product(organization=module_entitlement_manifest_org).create(), ).create() assert custom_repo.sync()['result'] == 'success' return custom_repo @pytest.fixture(scope='module') -def module_cv(module_manifest_org, rhel7_sat6tools_repo, custom_repo): +def module_cv(module_entitlement_manifest_org, rhel7_sat6tools_repo, custom_repo): """Publish both repos into module CV""" module_cv = entities.ContentView( - organization=module_manifest_org, repository=[rhel7_sat6tools_repo.id, custom_repo.id] + organization=module_entitlement_manifest_org, + repository=[rhel7_sat6tools_repo.id, custom_repo.id], ).create() module_cv.publish() module_cv = module_cv.read() @@ -98,15 +104,15 @@ def module_cv(module_manifest_org, rhel7_sat6tools_repo, custom_repo): @pytest.fixture(scope='module') -def module_ak(module_manifest_org, module_cv, custom_repo, module_lce_library): +def module_ak(module_entitlement_manifest_org, module_cv, custom_repo, module_lce_library): """Create a module AK in Library LCE""" ak = entities.ActivationKey( content_view=module_cv, environment=module_lce_library, - organization=module_manifest_org, + organization=module_entitlement_manifest_org, ).create() # Fetch available subscriptions - subs = entities.Subscription(organization=module_manifest_org).search() + subs = entities.Subscription(organization=module_entitlement_manifest_org).search() assert len(subs) > 0 # Add default subscription to activation key sub_found = False @@ -121,7 +127,7 @@ def module_ak(module_manifest_org, module_cv, custom_repo, module_lce_library): ) # Add custom subscription to activation key prod = custom_repo.product.read() - custom_sub = entities.Subscription(organization=module_manifest_org).search( + custom_sub = entities.Subscription(organization=module_entitlement_manifest_org).search( query={'search': f'name={prod.name}'} ) ak.add_subscriptions(data={'subscription_id': custom_sub[0].id}) @@ -131,7 +137,7 @@ def module_ak(module_manifest_org, module_cv, custom_repo, module_lce_library): @pytest.fixture(scope='module') def host( rhel7_contenthost_module, - module_manifest_org, + module_entitlement_manifest_org, dev_lce, qe_lce, custom_repo, @@ -142,7 +148,9 @@ def host( # Create client machine and register it to satellite with rhel_7_partial_ak rhel7_contenthost_module.install_katello_ca(module_target_sat) # Register, enable tools repo and install katello-host-tools. - rhel7_contenthost_module.register_contenthost(module_manifest_org.label, module_ak.name) + rhel7_contenthost_module.register_contenthost( + module_entitlement_manifest_org.label, module_ak.name + ) rhel7_contenthost_module.enable_repo(REPOS['rhst7']['id']) rhel7_contenthost_module.install_katello_host_tools() # make a note of time for later wait_for_tasks, and include 4 mins margin of safety. @@ -176,7 +184,9 @@ def get_applicable_errata(repo): @pytest.mark.tier4 @pytest.mark.upgrade -def test_positive_noapply_api(module_manifest_org, module_cv, custom_repo, host, dev_lce): +def test_positive_noapply_api( + module_entitlement_manifest_org, module_cv, custom_repo, host, dev_lce +): """Check if api incremental update can be done without actually applying it From 797db704fa3914afc83012235ebf0c6909731f30 Mon Sep 17 00:00:00 2001 From: Peter Ondrejka Date: Thu, 22 Jun 2023 11:08:38 +0200 Subject: [PATCH 038/586] hammer commands updated for 6.14 (#11725) --- tests/foreman/data/hammer_commands.json | 217 +++++++++++++++++------- 1 file changed, 157 insertions(+), 60 deletions(-) diff --git a/tests/foreman/data/hammer_commands.json b/tests/foreman/data/hammer_commands.json index 7b449e5e9a0..eb1c144e14b 100644 --- a/tests/foreman/data/hammer_commands.json +++ b/tests/foreman/data/hammer_commands.json @@ -746,7 +746,13 @@ "value": null }, { - "help": "Print help --------------------------------|-----|---------|----- | ALL | DEFAULT | THIN --------------------------------|-----|---------|----- | x | x | x | x | x | x | x | x | limit | x | x | attach | x | x | version | x | x | environment | x | x | view | x | x | collections/id | x | x | collections/name | x | x | overrides/content label | x | x | overrides/name | x | x | overrides/value | x | x | purpose/service level | x | x | purpose/purpose usage | x | x | purpose/purpose role | x | x | purpose/purpose addons | x | x | --------------------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", + "help": "Show hosts associated to an activation key", + "name": "show-hosts", + "shortname": null, + "value": "BOOLEAN" + }, + { + "help": "Print help --------------------------------|-----|---------|----- | ALL | DEFAULT | THIN --------------------------------|-----|---------|----- | x | x | x | x | x | x | x | x | limit | x | x | attach | x | x | version | x | x | environment | x | x | view | x | x | hosts/id | x | x | hosts/name | x | x | collections/id | x | x | collections/name | x | x | overrides/content label | x | x | overrides/name | x | x | overrides/value | x | x | purpose/service level | x | x | purpose/purpose usage | x | x | purpose/purpose role | x | x | purpose/purpose addons | x | x | --------------------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", "name": "help", "shortname": "h", "value": null @@ -1612,7 +1618,7 @@ "value": "VALUE" }, { - "help": "Print help ------------------------------|-----|---------|----- | ALL | DEFAULT | THIN ------------------------------|-----|---------|----- | x | x | x | x | x | x | x | x | | x | x | url | x | x | type | x | x | content source type | x | x | username | x | x | Subpaths/ | x | x | Products/id | x | x | Products/organization id | x | x | Products/name | x | x | Products/label | x | x | proxies/id | x | x | proxies/name | x | x | proxies/url | x | x | proxies/download policy | x | x | ------------------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", + "help": "Print help ------------------------------|-----|---------|----- | ALL | DEFAULT | THIN ------------------------------|-----|---------|----- | x | x | x | x | x | x | x | x | | x | x | url | x | x | type | x | x | content source type | x | x | username | x | x | ssl | x | x | ca cert/id | x | x | ca cert/name | x | x | client cert/id | x | x | client cert/name | x | x | client key/id | x | x | client key/name | x | x | Subpaths/ | x | x | Products/id | x | x | Products/organization id | x | x | Products/name | x | x | Products/label | x | x | proxies/id | x | x | proxies/name | x | x | proxies/url | x | x | proxies/download policy | x | x | ------------------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", "name": "help", "shortname": "h", "value": null @@ -4108,7 +4114,7 @@ "value": "VALUE" }, { - "help": "Print help --------------------|-----|---------|----- | ALL | DEFAULT | THIN --------------------|-----|---------|----- | x | x | x at | x | x | name | x | x | x proxy name | x | x | name | x | x | | x | x | | x | x | | x | x | --------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string string Values: compliant, incompliant, inconclusive string Values: true, false string string integer integer string string string integer string Values: host, policy datetime string string integer text string string string integer string string datetime text string text string string", + "help": "Print help --------------------|-----|---------|----- | ALL | DEFAULT | THIN --------------------|-----|---------|----- | x | x | x at | x | x | name | x | x | x proxy name | x | x | name | x | x | | x | x | | x | x | | x | x | --------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string string Values: compliant, incompliant, inconclusive string Values: true, false string string integer integer string string string integer string Values: host, policy datetime lifecycle_environment string integer text string string string integer string string datetime text string text string string", "name": "help", "shortname": "h", "value": null @@ -4321,6 +4327,31 @@ ], "subcommands": [] }, + { + "description": "Authenticate against external source (IPA/PAM) with credentials", + "name": "basic-external", + "options": [ + { + "help": "Print help", + "name": "help", + "shortname": "h", + "value": null + }, + { + "help": "Password to access the remote system", + "name": "password", + "shortname": "p", + "value": "VALUE" + }, + { + "help": "Username to access the remote system you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", + "name": "username", + "shortname": "u", + "value": "VALUE" + } + ], + "subcommands": [] + }, { "description": "Negotiate the login credentials from the auth ticket (Kerberos)", "name": "negotiate", @@ -6816,7 +6847,7 @@ "value": null }, { - "help": "Print help -----------------|-----|---------|----- | ALL | DEFAULT | THIN -----------------|-----|---------|----- | x | x | x | x | x | x | x | x | | x | x | | x | x | | x | x | | x | x | Features/name | x | x | Features/version | x | x | Locations/ | x | x | Organizations/ | x | x | at | x | x | at | x | x | -----------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", + "help": "Print help -----------------|-----|---------|----- | ALL | DEFAULT | THIN -----------------|-----|---------|----- | x | x | x | x | x | x | x | x | | x | x | | x | x | | x | x | count | x | x | Features/name | x | x | Features/version | x | x | Locations/ | x | x | Organizations/ | x | x | at | x | x | at | x | x | -----------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", "name": "help", "shortname": "h", "value": null @@ -7602,7 +7633,7 @@ "value": "KEY_VALUE_LIST" }, { - "help": "Print help you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string \u001b[1mNOTE:\u001b[0m Bold attributes are required. EC2: --volume: Libvirt: --volume: \u001b[1mpool_name\u001b[0m One of available storage pools \u001b[1mcapacity\u001b[0m String value, e.g. 10G allocation Initial allocation, e.g. 0G format_type Possible values: raw, qcow2 OpenStack: --volume: oVirt: --volume: size_gb Volume size in GB, integer value storage_domain ID or name of storage domain bootable Boolean, set 1 for bootable, only one volume can be bootable preallocate Boolean, set 1 to preallocate wipe_after_delete Boolean, set 1 to wipe disk after delete interface Disk interface name, must be ide, virto or virto_scsi Rackspace: --volume: VMware: --volume: name storage_pod Storage Pod ID from VMware datastore Datastore ID from VMware mode persistent/independent_persistent/independent_nonpersistent size_gb Integer number, volume size in GB thin true/false eager_zero true/false controller_key Associated SCSI controller key AzureRM: --volume: disk_size_gb Volume Size in GB (integer value) data_disk_caching Data Disk Caching (None, ReadOnly, ReadWrite) GCE: --volume: size_gb Volume size in GB, integer value", + "help": "Print help you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string \u001b[1mNOTE:\u001b[0m Bold attributes are required. EC2: --volume: Libvirt: --volume: \u001b[1mpool_name\u001b[0m One of available storage pools \u001b[1mcapacity\u001b[0m String value, e.g. 10G allocation Initial allocation, e.g. 0G format_type Possible values: raw, qcow2 OpenStack: --volume: oVirt: --volume: size_gb Volume size in GB, integer value storage_domain ID or name of storage domain bootable Boolean, set 1 for bootable, only one volume can be bootable preallocate Boolean, set 1 to preallocate wipe_after_delete Boolean, set 1 to wipe disk after delete interface Disk interface name, must be ide, virtio or virtio_scsi Rackspace: --volume: VMware: --volume: name storage_pod Storage Pod ID from VMware datastore Datastore ID from VMware mode persistent/independent_persistent/independent_nonpersistent size_gb Integer number, volume size in GB thin true/false eager_zero true/false controller_key Associated SCSI controller key AzureRM: --volume: disk_size_gb Volume Size in GB (integer value) data_disk_caching Data Disk Caching (None, ReadOnly, ReadWrite) GCE: --volume: size_gb Volume size in GB, integer value", "name": "help", "shortname": "h", "value": null @@ -7693,7 +7724,7 @@ "value": "KEY_VALUE_LIST" }, { - "help": "Print help you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string \u001b[1mNOTE:\u001b[0m Bold attributes are required. EC2: --volume: --interface: --compute-attributes: availability_zone flavor_id groups security_group_ids managed_ip Libvirt: --volume: \u001b[1mpool_name\u001b[0m One of available storage pools \u001b[1mcapacity\u001b[0m String value, e.g. 10G allocation Initial allocation, e.g. 0G format_type Possible values: raw, qcow2 --interface: compute_type Possible values: bridge, network compute_bridge Name of interface according to type compute_model Possible values: virtio, rtl8139, ne2k_pci, pcnet, e1000 compute_network Libvirt instance network, e.g. default --compute-attributes: \u001b[1mcpus\u001b[0m Number of CPUs \u001b[1mmemory\u001b[0m String, amount of memory, value in bytes cpu_mode Possible values: default, host-model, host-passthrough boot_order Device names to specify the boot order OpenStack: --volume: --interface: --compute-attributes: availability_zone boot_from_volume flavor_ref image_ref tenant_id security_groups network oVirt: --volume: size_gb Volume size in GB, integer value storage_domain ID or name of storage domain bootable Boolean, set 1 for bootable, only one volume can be bootable preallocate Boolean, set 1 to preallocate wipe_after_delete Boolean, set 1 to wipe disk after delete interface Disk interface name, must be ide, virto or virto_scsi --interface: compute_name Compute name, e.g. eth0 compute_network Select one of available networks for a cluster, must be an ID or a name compute_interface Interface type compute_vnic_profile Vnic Profile --compute-attributes: cluster ID or name of cluster to use template Hardware profile to use cores Integer value, number of cores sockets Integer value, number of sockets memory Amount of memory, integer value in bytes ha Boolean, set 1 to high availability display_type Possible values: VNC, SPICE keyboard_layout Possible values: ar, de-ch, es, fo, fr-ca, hu, ja, mk, no, pt-br, sv, da, en-gb, et, fr, fr-ch, is, lt, nl, pl, ru, th, de, en-us, fi, fr-be, hr, it, lv, nl-be, pt, sl, tr. Not usable if display type is SPICE. Rackspace: --volume: --interface: --compute-attributes: flavor_id VMware: --volume: name storage_pod Storage Pod ID from VMware datastore Datastore ID from VMware mode persistent/independent_persistent/independent_nonpersistent size_gb Integer number, volume size in GB thin true/false eager_zero true/false controller_key Associated SCSI controller key --interface: compute_type Type of the network adapter, for example one of: VirtualVmxnet3 VirtualE1000 See documentation center for your version of vSphere to find more details about available adapter types: https://www.vmware.com/support/pubs/ compute_network Network ID or Network Name from VMware --compute-attributes: \u001b[1mcluster\u001b[0m Cluster ID from VMware \u001b[1mcorespersocket\u001b[0m Number of cores per socket (applicable to hardware versions < 10 only) \u001b[1mcpus\u001b[0m CPU count \u001b[1mmemory_mb\u001b[0m Integer number, amount of memory in MB \u001b[1mpath\u001b[0m Path to folder \u001b[1mresource_pool\u001b[0m Resource Pool ID from VMware firmware automatic/bios/efi guest_id Guest OS ID form VMware hardware_version Hardware version ID from VMware memoryHotAddEnabled Must be a 1 or 0, lets you add memory resources while the machine is on cpuHotAddEnabled Must be a 1 or 0, lets you add CPU resources while the machine is on add_cdrom Must be a 1 or 0, Add a CD-ROM drive to the virtual machine annotation Annotation Notes scsi_controllers List with SCSI controllers definitions type - ID of the controller from VMware key - Key of the controller (e.g. 1000) boot_order Device names to specify the boot order AzureRM: --volume: disk_size_gb Volume Size in GB (integer value) data_disk_caching Data Disk Caching (None, ReadOnly, ReadWrite) --interface: compute_network Select one of available Azure Subnets, must be an ID compute_public_ip Public IP (None, Static, Dynamic) compute_private_ip Static Private IP (expressed as true or false) --compute-attributes: resource_group Existing Azure Resource Group of user vm_size VM Size, eg. Standard_A0 etc. username The Admin username password The Admin password platform OS type eg. Linux ssh_key_data SSH key for passwordless authentication os_disk_caching OS disk caching premium_os_disk Premium OS Disk, Boolean as 0 or 1 script_command Custom Script Command script_uris Comma seperated file URIs GCE: --volume: size_gb Volume size in GB, integer value --interface: --compute-attributes: machine_type network associate_external_ip", + "help": "Print help you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string \u001b[1mNOTE:\u001b[0m Bold attributes are required. EC2: --volume: --interface: --compute-attributes: availability_zone flavor_id groups security_group_ids managed_ip Libvirt: --volume: \u001b[1mpool_name\u001b[0m One of available storage pools \u001b[1mcapacity\u001b[0m String value, e.g. 10G allocation Initial allocation, e.g. 0G format_type Possible values: raw, qcow2 --interface: compute_type Possible values: bridge, network compute_bridge Name of interface according to type compute_model Possible values: virtio, rtl8139, ne2k_pci, pcnet, e1000 compute_network Libvirt instance network, e.g. default --compute-attributes: \u001b[1mcpus\u001b[0m Number of CPUs \u001b[1mmemory\u001b[0m String, amount of memory, value in bytes cpu_mode Possible values: default, host-model, host-passthrough boot_order Device names to specify the boot order OpenStack: --volume: --interface: --compute-attributes: availability_zone boot_from_volume flavor_ref image_ref tenant_id security_groups network oVirt: --volume: size_gb Volume size in GB, integer value storage_domain ID or name of storage domain bootable Boolean, set 1 for bootable, only one volume can be bootable preallocate Boolean, set 1 to preallocate wipe_after_delete Boolean, set 1 to wipe disk after delete interface Disk interface name, must be ide, virtio or virtio_scsi --interface: compute_name Compute name, e.g. eth0 compute_network Select one of available networks for a cluster, must be an ID or a name compute_interface Interface type compute_vnic_profile Vnic Profile --compute-attributes: cluster ID or name of cluster to use template Hardware profile to use cores Integer value, number of cores sockets Integer value, number of sockets memory Amount of memory, integer value in bytes ha Boolean, set 1 to high availability display_type Possible values: VNC, SPICE keyboard_layout Possible values: ar, de-ch, es, fo, fr-ca, hu, ja, mk, no, pt-br, sv, da, en-gb, et, fr, fr-ch, is, lt, nl, pl, ru, th, de, en-us, fi, fr-be, hr, it, lv, nl-be, pt, sl, tr. Not usable if display type is SPICE. Rackspace: --volume: --interface: --compute-attributes: flavor_id VMware: --volume: name storage_pod Storage Pod ID from VMware datastore Datastore ID from VMware mode persistent/independent_persistent/independent_nonpersistent size_gb Integer number, volume size in GB thin true/false eager_zero true/false controller_key Associated SCSI controller key --interface: compute_type Type of the network adapter, for example one of: VirtualVmxnet3 VirtualE1000 See documentation center for your version of vSphere to find more details about available adapter types: https://www.vmware.com/support/pubs/ compute_network Network ID or Network Name from VMware --compute-attributes: \u001b[1mcluster\u001b[0m Cluster ID from VMware \u001b[1mcorespersocket\u001b[0m Number of cores per socket (applicable to hardware versions < 10 only) \u001b[1mcpus\u001b[0m CPU count \u001b[1mmemory_mb\u001b[0m Integer number, amount of memory in MB \u001b[1mpath\u001b[0m Path to folder \u001b[1mresource_pool\u001b[0m Resource Pool ID from VMware firmware automatic/bios/efi guest_id Guest OS ID form VMware hardware_version Hardware version ID from VMware memoryHotAddEnabled Must be a 1 or 0, lets you add memory resources while the machine is on cpuHotAddEnabled Must be a 1 or 0, lets you add CPU resources while the machine is on add_cdrom Must be a 1 or 0, Add a CD-ROM drive to the virtual machine annotation Annotation Notes scsi_controllers List with SCSI controllers definitions type - ID of the controller from VMware key - Key of the controller (e.g. 1000) boot_order Device names to specify the boot order AzureRM: --volume: disk_size_gb Volume Size in GB (integer value) data_disk_caching Data Disk Caching (None, ReadOnly, ReadWrite) --interface: compute_network Select one of available Azure Subnets, must be an ID compute_public_ip Public IP (None, Static, Dynamic) compute_private_ip Static Private IP (expressed as true or false) --compute-attributes: resource_group Existing Azure Resource Group of user vm_size VM Size, eg. Standard_A0 etc. username The Admin username password The Admin password platform OS type eg. Linux ssh_key_data SSH key for passwordless authentication os_disk_caching OS disk caching premium_os_disk Premium OS Disk, Boolean as 0 or 1 script_command Custom Script Command script_uris Comma seperated file URIs GCE: --volume: size_gb Volume size in GB, integer value --interface: --compute-attributes: machine_type network associate_external_ip", "name": "help", "shortname": "h", "value": null @@ -7942,7 +7973,7 @@ "value": "KEY_VALUE_LIST" }, { - "help": "Print help you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string \u001b[1mNOTE:\u001b[0m Bold attributes are required. EC2: --volume: --interface: --compute-attributes: availability_zone flavor_id groups security_group_ids managed_ip Libvirt: --volume: \u001b[1mpool_name\u001b[0m One of available storage pools \u001b[1mcapacity\u001b[0m String value, e.g. 10G allocation Initial allocation, e.g. 0G format_type Possible values: raw, qcow2 --interface: compute_type Possible values: bridge, network compute_bridge Name of interface according to type compute_model Possible values: virtio, rtl8139, ne2k_pci, pcnet, e1000 compute_network Libvirt instance network, e.g. default --compute-attributes: \u001b[1mcpus\u001b[0m Number of CPUs \u001b[1mmemory\u001b[0m String, amount of memory, value in bytes cpu_mode Possible values: default, host-model, host-passthrough boot_order Device names to specify the boot order OpenStack: --volume: --interface: --compute-attributes: availability_zone boot_from_volume flavor_ref image_ref tenant_id security_groups network oVirt: --volume: size_gb Volume size in GB, integer value storage_domain ID or name of storage domain bootable Boolean, set 1 for bootable, only one volume can be bootable preallocate Boolean, set 1 to preallocate wipe_after_delete Boolean, set 1 to wipe disk after delete interface Disk interface name, must be ide, virto or virto_scsi --interface: compute_name Compute name, e.g. eth0 compute_network Select one of available networks for a cluster, must be an ID or a name compute_interface Interface type compute_vnic_profile Vnic Profile --compute-attributes: cluster ID or name of cluster to use template Hardware profile to use cores Integer value, number of cores sockets Integer value, number of sockets memory Amount of memory, integer value in bytes ha Boolean, set 1 to high availability display_type Possible values: VNC, SPICE keyboard_layout Possible values: ar, de-ch, es, fo, fr-ca, hu, ja, mk, no, pt-br, sv, da, en-gb, et, fr, fr-ch, is, lt, nl, pl, ru, th, de, en-us, fi, fr-be, hr, it, lv, nl-be, pt, sl, tr. Not usable if display type is SPICE. Rackspace: --volume: --interface: --compute-attributes: flavor_id VMware: --volume: name storage_pod Storage Pod ID from VMware datastore Datastore ID from VMware mode persistent/independent_persistent/independent_nonpersistent size_gb Integer number, volume size in GB thin true/false eager_zero true/false controller_key Associated SCSI controller key --interface: compute_type Type of the network adapter, for example one of: VirtualVmxnet3 VirtualE1000 See documentation center for your version of vSphere to find more details about available adapter types: https://www.vmware.com/support/pubs/ compute_network Network ID or Network Name from VMware --compute-attributes: \u001b[1mcluster\u001b[0m Cluster ID from VMware \u001b[1mcorespersocket\u001b[0m Number of cores per socket (applicable to hardware versions < 10 only) \u001b[1mcpus\u001b[0m CPU count \u001b[1mmemory_mb\u001b[0m Integer number, amount of memory in MB \u001b[1mpath\u001b[0m Path to folder \u001b[1mresource_pool\u001b[0m Resource Pool ID from VMware firmware automatic/bios/efi guest_id Guest OS ID form VMware hardware_version Hardware version ID from VMware memoryHotAddEnabled Must be a 1 or 0, lets you add memory resources while the machine is on cpuHotAddEnabled Must be a 1 or 0, lets you add CPU resources while the machine is on add_cdrom Must be a 1 or 0, Add a CD-ROM drive to the virtual machine annotation Annotation Notes scsi_controllers List with SCSI controllers definitions type - ID of the controller from VMware key - Key of the controller (e.g. 1000) boot_order Device names to specify the boot order AzureRM: --volume: disk_size_gb Volume Size in GB (integer value) data_disk_caching Data Disk Caching (None, ReadOnly, ReadWrite) --interface: compute_network Select one of available Azure Subnets, must be an ID compute_public_ip Public IP (None, Static, Dynamic) compute_private_ip Static Private IP (expressed as true or false) --compute-attributes: resource_group Existing Azure Resource Group of user vm_size VM Size, eg. Standard_A0 etc. username The Admin username password The Admin password platform OS type eg. Linux ssh_key_data SSH key for passwordless authentication os_disk_caching OS disk caching premium_os_disk Premium OS Disk, Boolean as 0 or 1 script_command Custom Script Command script_uris Comma seperated file URIs GCE: --volume: size_gb Volume size in GB, integer value --interface: --compute-attributes: machine_type network associate_external_ip", + "help": "Print help you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string \u001b[1mNOTE:\u001b[0m Bold attributes are required. EC2: --volume: --interface: --compute-attributes: availability_zone flavor_id groups security_group_ids managed_ip Libvirt: --volume: \u001b[1mpool_name\u001b[0m One of available storage pools \u001b[1mcapacity\u001b[0m String value, e.g. 10G allocation Initial allocation, e.g. 0G format_type Possible values: raw, qcow2 --interface: compute_type Possible values: bridge, network compute_bridge Name of interface according to type compute_model Possible values: virtio, rtl8139, ne2k_pci, pcnet, e1000 compute_network Libvirt instance network, e.g. default --compute-attributes: \u001b[1mcpus\u001b[0m Number of CPUs \u001b[1mmemory\u001b[0m String, amount of memory, value in bytes cpu_mode Possible values: default, host-model, host-passthrough boot_order Device names to specify the boot order OpenStack: --volume: --interface: --compute-attributes: availability_zone boot_from_volume flavor_ref image_ref tenant_id security_groups network oVirt: --volume: size_gb Volume size in GB, integer value storage_domain ID or name of storage domain bootable Boolean, set 1 for bootable, only one volume can be bootable preallocate Boolean, set 1 to preallocate wipe_after_delete Boolean, set 1 to wipe disk after delete interface Disk interface name, must be ide, virtio or virtio_scsi --interface: compute_name Compute name, e.g. eth0 compute_network Select one of available networks for a cluster, must be an ID or a name compute_interface Interface type compute_vnic_profile Vnic Profile --compute-attributes: cluster ID or name of cluster to use template Hardware profile to use cores Integer value, number of cores sockets Integer value, number of sockets memory Amount of memory, integer value in bytes ha Boolean, set 1 to high availability display_type Possible values: VNC, SPICE keyboard_layout Possible values: ar, de-ch, es, fo, fr-ca, hu, ja, mk, no, pt-br, sv, da, en-gb, et, fr, fr-ch, is, lt, nl, pl, ru, th, de, en-us, fi, fr-be, hr, it, lv, nl-be, pt, sl, tr. Not usable if display type is SPICE. Rackspace: --volume: --interface: --compute-attributes: flavor_id VMware: --volume: name storage_pod Storage Pod ID from VMware datastore Datastore ID from VMware mode persistent/independent_persistent/independent_nonpersistent size_gb Integer number, volume size in GB thin true/false eager_zero true/false controller_key Associated SCSI controller key --interface: compute_type Type of the network adapter, for example one of: VirtualVmxnet3 VirtualE1000 See documentation center for your version of vSphere to find more details about available adapter types: https://www.vmware.com/support/pubs/ compute_network Network ID or Network Name from VMware --compute-attributes: \u001b[1mcluster\u001b[0m Cluster ID from VMware \u001b[1mcorespersocket\u001b[0m Number of cores per socket (applicable to hardware versions < 10 only) \u001b[1mcpus\u001b[0m CPU count \u001b[1mmemory_mb\u001b[0m Integer number, amount of memory in MB \u001b[1mpath\u001b[0m Path to folder \u001b[1mresource_pool\u001b[0m Resource Pool ID from VMware firmware automatic/bios/efi guest_id Guest OS ID form VMware hardware_version Hardware version ID from VMware memoryHotAddEnabled Must be a 1 or 0, lets you add memory resources while the machine is on cpuHotAddEnabled Must be a 1 or 0, lets you add CPU resources while the machine is on add_cdrom Must be a 1 or 0, Add a CD-ROM drive to the virtual machine annotation Annotation Notes scsi_controllers List with SCSI controllers definitions type - ID of the controller from VMware key - Key of the controller (e.g. 1000) boot_order Device names to specify the boot order AzureRM: --volume: disk_size_gb Volume Size in GB (integer value) data_disk_caching Data Disk Caching (None, ReadOnly, ReadWrite) --interface: compute_network Select one of available Azure Subnets, must be an ID compute_public_ip Public IP (None, Static, Dynamic) compute_private_ip Static Private IP (expressed as true or false) --compute-attributes: resource_group Existing Azure Resource Group of user vm_size VM Size, eg. Standard_A0 etc. username The Admin username password The Admin password platform OS type eg. Linux ssh_key_data SSH key for passwordless authentication os_disk_caching OS disk caching premium_os_disk Premium OS Disk, Boolean as 0 or 1 script_command Custom Script Command script_uris Comma seperated file URIs GCE: --volume: size_gb Volume size in GB, integer value --interface: --compute-attributes: machine_type network associate_external_ip", "name": "help", "shortname": "h", "value": null @@ -8112,7 +8143,7 @@ "value": "NUMBER" }, { - "help": "Print help you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string \u001b[1mNOTE:\u001b[0m Bold attributes are required. EC2: --volume: Libvirt: --volume: \u001b[1mpool_name\u001b[0m One of available storage pools \u001b[1mcapacity\u001b[0m String value, e.g. 10G allocation Initial allocation, e.g. 0G format_type Possible values: raw, qcow2 OpenStack: --volume: oVirt: --volume: size_gb Volume size in GB, integer value storage_domain ID or name of storage domain bootable Boolean, set 1 for bootable, only one volume can be bootable preallocate Boolean, set 1 to preallocate wipe_after_delete Boolean, set 1 to wipe disk after delete interface Disk interface name, must be ide, virto or virto_scsi Rackspace: --volume: VMware: --volume: name storage_pod Storage Pod ID from VMware datastore Datastore ID from VMware mode persistent/independent_persistent/independent_nonpersistent size_gb Integer number, volume size in GB thin true/false eager_zero true/false controller_key Associated SCSI controller key AzureRM: --volume: disk_size_gb Volume Size in GB (integer value) data_disk_caching Data Disk Caching (None, ReadOnly, ReadWrite) GCE: --volume: size_gb Volume size in GB, integer value", + "help": "Print help you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string \u001b[1mNOTE:\u001b[0m Bold attributes are required. EC2: --volume: Libvirt: --volume: \u001b[1mpool_name\u001b[0m One of available storage pools \u001b[1mcapacity\u001b[0m String value, e.g. 10G allocation Initial allocation, e.g. 0G format_type Possible values: raw, qcow2 OpenStack: --volume: oVirt: --volume: size_gb Volume size in GB, integer value storage_domain ID or name of storage domain bootable Boolean, set 1 for bootable, only one volume can be bootable preallocate Boolean, set 1 to preallocate wipe_after_delete Boolean, set 1 to wipe disk after delete interface Disk interface name, must be ide, virtio or virtio_scsi Rackspace: --volume: VMware: --volume: name storage_pod Storage Pod ID from VMware datastore Datastore ID from VMware mode persistent/independent_persistent/independent_nonpersistent size_gb Integer number, volume size in GB thin true/false eager_zero true/false controller_key Associated SCSI controller key AzureRM: --volume: disk_size_gb Volume Size in GB (integer value) data_disk_caching Data Disk Caching (None, ReadOnly, ReadWrite) GCE: --volume: size_gb Volume size in GB, integer value", "name": "help", "shortname": "h", "value": null @@ -10749,7 +10780,7 @@ "value": null }, { - "help": "Print help --------------------------------|-----|---------|----- | ALL | DEFAULT | THIN --------------------------------|-----|---------|----- | x | x | x | x | x | at | x | x | | x | x | status/applied | x | x | status/restarted | x | x | status/failed | x | x | status/restart failures | x | x | status/skipped | x | x | status/pending | x | x | metrics/config_retrieval | x | x | metrics/exec | x | x | metrics/file | x | x | metrics/package | x | x | metrics/service | x | x | metrics/user | x | x | metrics/yumrepo | x | x | metrics/filebucket | x | x | metrics/cron | x | x | metrics/total | x | x | Logs/resource | x | x | Logs/message | x | x | --------------------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", + "help": "Print help --------------------------------|-----|---------|----- | ALL | DEFAULT | THIN --------------------------------|-----|---------|----- | x | x | x | x | x | at | x | x | | x | x | status/applied | x | x | status/restarted | x | x | status/failed | x | x | status/restart failures | x | x | status/skipped | x | x | status/pending | x | x | metrics/config retrieval | x | x | metrics/exec | x | x | metrics/file | x | x | metrics/package | x | x | metrics/service | x | x | metrics/user | x | x | metrics/yumrepo | x | x | metrics/filebucket | x | x | metrics/cron | x | x | metrics/total | x | x | Logs/resource | x | x | Logs/message | x | x | --------------------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", "name": "help", "shortname": "h", "value": null @@ -11216,7 +11247,7 @@ "value": null }, { - "help": "Export formats. Choose syncable if content is to be imported via repository sync. Choose importable if content is to be imported via hammer content-import. Defaults to importable. Possible value(s): 'syncable', 'importable'", + "help": "Export formats.Choose syncable if the exported content needs to be in a yum format. This option is only available for yum, file repositories. Choose importable if the importing server uses the same version and exported content needs to be one of yum, file, ansible_collection, docker repositories. Possible value(s): 'syncable', 'importable'", "name": "format", "shortname": null, "value": "ENUM" @@ -11271,7 +11302,7 @@ "value": "NUMBER" }, { - "help": "Export formats. Choose syncable if content is to be imported via repository sync. Choose importable if content is to be imported via hammer content-import. Defaults to importable. Possible value(s): 'syncable', 'importable'", + "help": "Export formats.Choose syncable if the exported content needs to be in a yum format. This option is only available for yum, file repositories. Choose importable if the importing server uses the same version and exported content needs to be one of yum, file, ansible_collection, docker repositories. Possible value(s): 'syncable', 'importable'", "name": "format", "shortname": null, "value": "ENUM" @@ -11374,7 +11405,7 @@ "value": null }, { - "help": "Export formats. Choose syncable if content is to be imported via repository sync. Choose importable if content is to be imported via hammer content-import. Defaults to importable. Possible value(s): 'syncable', 'importable'", + "help": "Export formats.Choose syncable if the exported content needs to be in a yum format. This option is only available for yum, file repositories. Choose importable if the importing server uses the same version and exported content needs to be one of yum, file, ansible_collection, docker repositories. Possible value(s): 'syncable', 'importable'", "name": "format", "shortname": null, "value": "ENUM" @@ -11528,6 +11559,12 @@ "shortname": null, "value": null }, + { + "help": "Export formats.Choose syncable if the exported content needs to be in a yum format. This option is only available for yum, file repositories. Choose importable if the importing server uses the same version and exported content needs to be one of yum, file, ansible_collection, docker repositories. Possible value(s): 'syncable', 'importable'", + "name": "format", + "shortname": null, + "value": "ENUM" + }, { "help": "Export history id used for incremental export. If not provided the most recent export history will be used.", "name": "from-history-id", @@ -11583,6 +11620,12 @@ "shortname": null, "value": "NUMBER" }, + { + "help": "Export formats.Choose syncable if the exported content needs to be in a yum format. This option is only available for yum, file repositories. Choose importable if the importing server uses the same version and exported content needs to be one of yum, file, ansible_collection, docker repositories. Possible value(s): 'syncable', 'importable'", + "name": "format", + "shortname": null, + "value": "ENUM" + }, { "help": "Export history id used for incremental export. If not provided the most recent export history will be used.", "name": "from-history-id", @@ -11686,6 +11729,12 @@ "shortname": null, "value": null }, + { + "help": "Export formats.Choose syncable if the exported content needs to be in a yum format. This option is only available for yum, file repositories. Choose importable if the importing server uses the same version and exported content needs to be one of yum, file, ansible_collection, docker repositories. Possible value(s): 'syncable', 'importable'", + "name": "format", + "shortname": null, + "value": "ENUM" + }, { "help": "Export history id used for incremental export. If not provided the most recent export history will be used.", "name": "from-history-id", @@ -14419,6 +14468,12 @@ "shortname": null, "value": "VALUE" }, + { + "help": "Check audited changes and proceed only if content or filters have changed since last publish", + "name": "publish-only-if-needed", + "shortname": null, + "value": "BOOLEAN" + }, { "help": "Specify the list of units in each repo", "name": "repos-units", @@ -15148,6 +15203,12 @@ "shortname": null, "value": "NUMBER" }, + { + "help": "Whether or not to return filters applied to the content view version", + "name": "include-applied-filters", + "shortname": null, + "value": "BOOLEAN" + }, { "help": "VALUE/NUMBER Name/Id of associated lifecycle environment", "name": "lifecycle-environment", @@ -15191,7 +15252,7 @@ "value": "VALUE" }, { - "help": "Print help -----------------------------|-----|---------|----- | ALL | DEFAULT | THIN -----------------------------|-----|---------|----- | x | x | x | x | x | | x | x | x | x | x | view id | x | x | view name | x | x | view label | x | x | environments/id | x | x | environments/name | x | x | environments/label | x | x | Repositories/id | x | x | Repositories/name | x | x | Repositories/label | x | x | -----------------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", + "help": "Print help -----------------------------|-----|---------|----- | ALL | DEFAULT | THIN -----------------------------|-----|---------|----- | x | x | x | x | x | | x | x | x | x | x | view id | x | x | view name | x | x | view label | x | x | environments/id | x | x | environments/name | x | x | environments/label | x | x | Repositories/id | x | x | Repositories/name | x | x | Repositories/label | x | x | applied filters | x | x | filters/id | x | x | filters/name | x | x | solving | x | x | -----------------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", "name": "help", "shortname": "h", "value": null @@ -15245,6 +15306,12 @@ "shortname": null, "value": "BOOLEAN" }, + { + "help": "Whether or not to return filters applied to the content view version", + "name": "include-applied-filters", + "shortname": null, + "value": "BOOLEAN" + }, { "help": "VALUE/NUMBER Filter versions by environment", "name": "lifecycle-environment", @@ -21265,7 +21332,7 @@ "value": "KEY_VALUE_LIST" }, { - "help": "Print help you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string parameters accept format defined by its schema (bold are required; <> contains acceptable type; [] contains acceptable value): \"\u001b[1mname\u001b[0m=\\,\u001b[1mvalue\u001b[0m=\\,parameter_type=[string|boolean|integer|real|array|hash|yaml|json]\\,hidden_value=[true|false|1|0], ... \" \"product_id=\\,product_name=\\,arch=\\,version=, ... \" mac ip Possible values: interface, bmc, bond, bridge name subnet_id domain_id identifier true/false true/false, each managed hosts needs to have one primary interface. true/false true/false virtual=true: tag VLAN tag, this attribute has precedence over the subnet VLAN ID. Only for virtual interfaces. attached_to Identifier of the interface to which this interface belongs, e.g. eth1. type=bond: mode Possible values: balance-rr, active-backup, balance-xor, broadcast, 802.3ad, balance-tlb, balance-alb attached_devices Identifiers of slave interfaces, e.g. [eth1,eth2] bond_options type=bmc: provider always IPMI username password \u001b[1mNOTE:\u001b[0m Bold attributes are required. EC2: --volume: --interface: --compute-attributes: availability_zone flavor_id groups security_group_ids managed_ip Libvirt: --volume: \u001b[1mpool_name\u001b[0m One of available storage pools \u001b[1mcapacity\u001b[0m String value, e.g. 10G allocation Initial allocation, e.g. 0G format_type Possible values: raw, qcow2 --interface: compute_type Possible values: bridge, network compute_bridge Name of interface according to type compute_model Possible values: virtio, rtl8139, ne2k_pci, pcnet, e1000 compute_network Libvirt instance network, e.g. default --compute-attributes: \u001b[1mcpus\u001b[0m Number of CPUs \u001b[1mmemory\u001b[0m String, amount of memory, value in bytes cpu_mode Possible values: default, host-model, host-passthrough boot_order Device names to specify the boot order start Boolean (expressed as 0 or 1), whether to start the machine or not OpenStack: --volume: --interface: --compute-attributes: availability_zone boot_from_volume flavor_ref image_ref tenant_id security_groups network oVirt: --volume: size_gb Volume size in GB, integer value storage_domain ID or name of storage domain bootable Boolean, set 1 for bootable, only one volume can be bootable preallocate Boolean, set 1 to preallocate wipe_after_delete Boolean, set 1 to wipe disk after delete interface Disk interface name, must be ide, virto or virto_scsi --interface: compute_name Compute name, e.g. eth0 compute_network Select one of available networks for a cluster, must be an ID or a name compute_interface Interface type compute_vnic_profile Vnic Profile --compute-attributes: cluster ID or name of cluster to use template Hardware profile to use cores Integer value, number of cores sockets Integer value, number of sockets memory Amount of memory, integer value in bytes ha Boolean, set 1 to high availability display_type Possible values: VNC, SPICE keyboard_layout Possible values: ar, de-ch, es, fo, fr-ca, hu, ja, mk, no, pt-br, sv, da, en-gb, et, fr, fr-ch, is, lt, nl, pl, ru, th, de, en-us, fi, fr-be, hr, it, lv, nl-be, pt, sl, tr. Not usable if display type is SPICE. start Boolean, set 1 to start the vm Rackspace: --volume: --interface: --compute-attributes: flavor_id VMware: --volume: name storage_pod Storage Pod ID from VMware datastore Datastore ID from VMware mode persistent/independent_persistent/independent_nonpersistent size_gb Integer number, volume size in GB thin true/false eager_zero true/false controller_key Associated SCSI controller key --interface: compute_type Type of the network adapter, for example one of: VirtualVmxnet3 VirtualE1000 See documentation center for your version of vSphere to find more details about available adapter types: https://www.vmware.com/support/pubs/ compute_network Network ID or Network Name from VMware --compute-attributes: \u001b[1mcluster\u001b[0m Cluster ID from VMware \u001b[1mcorespersocket\u001b[0m Number of cores per socket (applicable to hardware versions < 10 only) \u001b[1mcpus\u001b[0m CPU count \u001b[1mmemory_mb\u001b[0m Integer number, amount of memory in MB \u001b[1mpath\u001b[0m Path to folder \u001b[1mresource_pool\u001b[0m Resource Pool ID from VMware firmware automatic/bios/efi guest_id Guest OS ID form VMware hardware_version Hardware version ID from VMware memoryHotAddEnabled Must be a 1 or 0, lets you add memory resources while the machine is on cpuHotAddEnabled Must be a 1 or 0, lets you add CPU resources while the machine is on add_cdrom Must be a 1 or 0, Add a CD-ROM drive to the virtual machine annotation Annotation Notes scsi_controllers List with SCSI controllers definitions type - ID of the controller from VMware key - Key of the controller (e.g. 1000) boot_order Device names to specify the boot order start Must be a 1 or 0, whether to start the machine or not AzureRM: --volume: disk_size_gb Volume Size in GB (integer value) data_disk_caching Data Disk Caching (None, ReadOnly, ReadWrite) --interface: compute_network Select one of available Azure Subnets, must be an ID compute_public_ip Public IP (None, Static, Dynamic) compute_private_ip Static Private IP (expressed as true or false) --compute-attributes: resource_group Existing Azure Resource Group of user vm_size VM Size, eg. Standard_A0 etc. username The Admin username password The Admin password platform OS type eg. Linux ssh_key_data SSH key for passwordless authentication os_disk_caching OS disk caching premium_os_disk Premium OS Disk, Boolean as 0 or 1 script_command Custom Script Command script_uris Comma seperated file URIs GCE: --volume: size_gb Volume size in GB, integer value --interface: --compute-attributes: machine_type network associate_external_ip", + "help": "Print help you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string parameters accept format defined by its schema (bold are required; <> contains acceptable type; [] contains acceptable value): \"\u001b[1mname\u001b[0m=\\,\u001b[1mvalue\u001b[0m=\\,parameter_type=[string|boolean|integer|real|array|hash|yaml|json]\\,hidden_value=[true|false|1|0], ... \" \"product_id=\\,product_name=\\,arch=\\,version=, ... \" mac ip Possible values: interface, bmc, bond, bridge name subnet_id domain_id identifier true/false true/false, each managed hosts needs to have one primary interface. true/false true/false virtual=true: tag VLAN tag, this attribute has precedence over the subnet VLAN ID. Only for virtual interfaces. attached_to Identifier of the interface to which this interface belongs, e.g. eth1. type=bond: mode Possible values: balance-rr, active-backup, balance-xor, broadcast, 802.3ad, balance-tlb, balance-alb attached_devices Identifiers of slave interfaces, e.g. [eth1,eth2] bond_options type=bmc: provider always IPMI username password \u001b[1mNOTE:\u001b[0m Bold attributes are required. EC2: --volume: --interface: --compute-attributes: availability_zone flavor_id groups security_group_ids managed_ip Libvirt: --volume: \u001b[1mpool_name\u001b[0m One of available storage pools \u001b[1mcapacity\u001b[0m String value, e.g. 10G allocation Initial allocation, e.g. 0G format_type Possible values: raw, qcow2 --interface: compute_type Possible values: bridge, network compute_bridge Name of interface according to type compute_model Possible values: virtio, rtl8139, ne2k_pci, pcnet, e1000 compute_network Libvirt instance network, e.g. default --compute-attributes: \u001b[1mcpus\u001b[0m Number of CPUs \u001b[1mmemory\u001b[0m String, amount of memory, value in bytes cpu_mode Possible values: default, host-model, host-passthrough boot_order Device names to specify the boot order start Boolean (expressed as 0 or 1), whether to start the machine or not OpenStack: --volume: --interface: --compute-attributes: availability_zone boot_from_volume flavor_ref image_ref tenant_id security_groups network oVirt: --volume: size_gb Volume size in GB, integer value storage_domain ID or name of storage domain bootable Boolean, set 1 for bootable, only one volume can be bootable preallocate Boolean, set 1 to preallocate wipe_after_delete Boolean, set 1 to wipe disk after delete interface Disk interface name, must be ide, virtio or virtio_scsi --interface: compute_name Compute name, e.g. eth0 compute_network Select one of available networks for a cluster, must be an ID or a name compute_interface Interface type compute_vnic_profile Vnic Profile --compute-attributes: cluster ID or name of cluster to use template Hardware profile to use cores Integer value, number of cores sockets Integer value, number of sockets memory Amount of memory, integer value in bytes ha Boolean, set 1 to high availability display_type Possible values: VNC, SPICE keyboard_layout Possible values: ar, de-ch, es, fo, fr-ca, hu, ja, mk, no, pt-br, sv, da, en-gb, et, fr, fr-ch, is, lt, nl, pl, ru, th, de, en-us, fi, fr-be, hr, it, lv, nl-be, pt, sl, tr. Not usable if display type is SPICE. start Boolean, set 1 to start the vm Rackspace: --volume: --interface: --compute-attributes: flavor_id VMware: --volume: name storage_pod Storage Pod ID from VMware datastore Datastore ID from VMware mode persistent/independent_persistent/independent_nonpersistent size_gb Integer number, volume size in GB thin true/false eager_zero true/false controller_key Associated SCSI controller key --interface: compute_type Type of the network adapter, for example one of: VirtualVmxnet3 VirtualE1000 See documentation center for your version of vSphere to find more details about available adapter types: https://www.vmware.com/support/pubs/ compute_network Network ID or Network Name from VMware --compute-attributes: \u001b[1mcluster\u001b[0m Cluster ID from VMware \u001b[1mcorespersocket\u001b[0m Number of cores per socket (applicable to hardware versions < 10 only) \u001b[1mcpus\u001b[0m CPU count \u001b[1mmemory_mb\u001b[0m Integer number, amount of memory in MB \u001b[1mpath\u001b[0m Path to folder \u001b[1mresource_pool\u001b[0m Resource Pool ID from VMware firmware automatic/bios/efi guest_id Guest OS ID form VMware hardware_version Hardware version ID from VMware memoryHotAddEnabled Must be a 1 or 0, lets you add memory resources while the machine is on cpuHotAddEnabled Must be a 1 or 0, lets you add CPU resources while the machine is on add_cdrom Must be a 1 or 0, Add a CD-ROM drive to the virtual machine annotation Annotation Notes scsi_controllers List with SCSI controllers definitions type - ID of the controller from VMware key - Key of the controller (e.g. 1000) boot_order Device names to specify the boot order start Must be a 1 or 0, whether to start the machine or not AzureRM: --volume: disk_size_gb Volume Size in GB (integer value) data_disk_caching Data Disk Caching (None, ReadOnly, ReadWrite) --interface: compute_network Select one of available Azure Subnets, must be an ID compute_public_ip Public IP (None, Static, Dynamic) compute_private_ip Static Private IP (expressed as true or false) --compute-attributes: resource_group Existing Azure Resource Group of user vm_size VM Size, eg. Standard_A0 etc. username The Admin username password The Admin password platform OS type eg. Linux ssh_key_data SSH key for passwordless authentication os_disk_caching OS disk caching premium_os_disk Premium OS Disk, Boolean as 0 or 1 script_command Custom Script Command script_uris Comma seperated file URIs GCE: --volume: size_gb Volume size in GB, integer value --interface: --compute-attributes: machine_type network associate_external_ip", "name": "help", "shortname": "h", "value": null @@ -21575,7 +21642,7 @@ ], "subcommands": [ { - "description": "Schedule errata for installation using katello-agent. NOTE: Katello-agent is deprecated and will be removed in a future release. Consider using remote execution instead.", + "description": "Schedule errata for installation using katello-agent. WARNING: Katello-agent is deprecated and will be removed in Katello 4.10. Migrate to remote execution now.", "name": "apply", "options": [ { @@ -21585,7 +21652,7 @@ "value": null }, { - "help": "List of Errata ids to install. Will be removed in a future release", + "help": "List of Errata ids to install. Will be removed in Katello 4.10", "name": "errata-ids", "shortname": null, "value": "LIST" @@ -22812,7 +22879,7 @@ "value": "BOOLEAN" }, { - "help": "Print help -----------------------|-----|---------|----- | ALL | DEFAULT | THIN -----------------------|-----|---------|----- | x | x | x | x | x | x system | x | x | group | x | x | | x | x | | x | x | status | x | x | | x | | | x | | information | x | | view | x | x | environment | x | x | | x | | | x | | | x | | status | x | x | -----------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string string string string Values: mismatched, matched, not_specified string string string date string string boolean boot_time Values: true, false Values: built, pending, token_expired, build_failed text string integer string string integer datetime integer string integer Values: security_needed, errata_needed, updated, unknown Values: ok, error string Values: ok, warning, error string string string string string integer string string boolean string integer string infrastructure_facet.foreman infrastructure_facet.smart_proxy_id Values: reporting, no_report Values: disconnect, sync integer string datetime string string job_invocation.id string job_invocation.result Values: cancelled, failed, pending, success datetime datetime string integer string integer string Values: true, false string string string integer string string string integer string string string string integer string string string string string integer string Values: mismatched, matched, not_specified string integer datetime string string reported.boot_time reported.cores reported.disks_total reported.kernel_version reported.ram reported.sockets reported.virtual Values: true, false string string text Values: mismatched, matched, not_specified string Values: mismatched, matched, not_specified string status.applied integer status.enabled Values: true, false status.failed integer status.failed_restarts integer status.interesting Values: true, false status.pending integer status.restarted integer status.skipped integer string subnet.name text string subnet6.name text string string Values: valid, partial, invalid, unknown, disabled, unsubscribed_hypervisor string Values: reboot_needed, process_restart_needed, updated string string text Values: mismatched, matched, not_specified user.firstname string user.lastname string user.login string user.mail string string usergroup.name string string", + "help": "Print help -----------------------|-----|---------|----- | ALL | DEFAULT | THIN -----------------------|-----|---------|----- | x | x | x | x | x | x system | x | x | group | x | x | | x | x | | x | x | status | x | x | | x | | | x | | information | x | | view | x | x | environment | x | x | | x | | | x | | | x | | status | x | x | -----------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string string string string Values: mismatched, matched, not_specified string string string date string string boolean boot_time Values: true, false Values: built, pending, token_expired, build_failed text string integer configuration_status.applied integer configuration_status.enabled Values: true, false configuration_status.failed integer configuration_status.failed_restarts integer configuration_status.interesting Values: true, false configuration_status.pending integer configuration_status.restarted integer configuration_status.skipped integer string string datetime integer string integer Values: security_needed, errata_needed, updated, unknown Values: ok, error string Values: ok, warning, error string string string string string integer string string boolean string integer string infrastructure_facet.foreman Values: true, false infrastructure_facet.smart_proxy_id Values: reporting, no_report Values: disconnect, sync integer string datetime string string job_invocation.id string job_invocation.result Values: cancelled, failed, pending, success datetime datetime string string integer string Values: true, false string string string integer string string string integer string string string string integer string string string string string integer string Values: mismatched, matched, not_specified Values: PXELinux_BIOS, PXELinux_UEFI, Grub_UEFI, Grub2_BIOS, Grub2_ELF, Grub2_UEFI, Grub2_UEFI_SecureBoot, Grub2_UEFI_HTTP, Grub2_UEFI_HTTPS, Grub2_UEFI_HTTPS_SecureBoot, iPXE_Embedded, iPXE_UEFI_HTTP, iPXE_Chain_BIOS, iPXE_Chain_UEFI string integer datetime string string reported.bios_release_date reported.bios_vendor reported.bios_version reported.boot_time reported.cores reported.disks_total reported.kernel_version reported.ram reported.sockets reported.virtual Values: true, false string string text Values: mismatched, matched, not_specified string Values: mismatched, matched, not_specified string status.applied integer status.enabled Values: true, false status.failed integer status.failed_restarts integer status.interesting Values: true, false status.pending integer status.restarted integer status.skipped integer string subnet.name text string subnet6.name text string string Values: valid, partial, invalid, unknown, disabled, unsubscribed_hypervisor string Values: reboot_needed, process_restart_needed, updated string string text Values: mismatched, matched, not_specified user.firstname string user.lastname string user.login string user.mail string string usergroup.name string string", "name": "help", "shortname": "h", "value": null @@ -22833,7 +22900,7 @@ ], "subcommands": [ { - "description": "Install packages remotely using katello-agent. NOTE: Katello-agent is deprecated and will be removed in a future release. Consider using remote execution instead.", + "description": "Install packages remotely using katello-agent. WARNING: Katello-agent is deprecated and will be removed in Katello 4.10. Migrate to remote execution now.", "name": "install", "options": [ { @@ -22943,7 +23010,7 @@ "subcommands": [] }, { - "description": "Uninstall packages remotely using katello-agent. NOTE: Katello-agent is deprecated and will be removed in a future release. Consider using remote execution instead.", + "description": "Uninstall packages remotely using katello-agent. WARNING: Katello-agent is deprecated and will be removed in Katello 4.10. Migrate to remote execution now.", "name": "remove", "options": [ { @@ -22980,7 +23047,7 @@ "subcommands": [] }, { - "description": "Update packages remotely using katello-agent. NOTE: Katello-agent is deprecated and will be removed in a future release. Consider using remote execution instead.", + "description": "Update packages remotely using katello-agent. WARNING: Katello-agent is deprecated and will be removed in Katello 4.10. Migrate to remote execution now.", "name": "upgrade", "options": [ { @@ -23017,7 +23084,7 @@ "subcommands": [] }, { - "description": "Update packages remotely using katello-agent. NOTE: Katello-agent is deprecated and will be removed in a future release. Consider using remote execution instead.", + "description": "Update packages remotely using katello-agent. WARNING: Katello-agent is deprecated and will be removed in Katello 4.10. Migrate to remote execution now.", "name": "upgrade-all", "options": [ { @@ -23062,7 +23129,7 @@ ], "subcommands": [ { - "description": "Install packages remotely using katello-agent. NOTE: Katello-agent is deprecated and will be removed in a future release. Consider using remote execution instead.", + "description": "Install packages remotely using katello-agent. WARNING: Katello-agent is deprecated and will be removed in Katello 4.10. Migrate to remote execution now.", "name": "install", "options": [ { @@ -23099,7 +23166,7 @@ "subcommands": [] }, { - "description": "Uninstall packages remotely using katello-agent. NOTE: Katello-agent is deprecated and will be removed in a future release. Consider using remote execution instead.", + "description": "Uninstall packages remotely using katello-agent. WARNING: Katello-agent is deprecated and will be removed in Katello 4.10. Migrate to remote execution now.", "name": "remove", "options": [ { @@ -24749,7 +24816,7 @@ "value": "KEY_VALUE_LIST" }, { - "help": "Print help you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string parameters accept format defined by its schema (bold are required; <> contains acceptable type; [] contains acceptable value): \"name=\\,value=\\,parameter_type=[string|boolean|integer|real|array|hash|yaml|json]\\,hidden_value=[true|false|1|0], ... \" \"product_id=\\,product_name=\\,arch=\\,version=, ... \" mac ip Possible values: interface, bmc, bond, bridge name subnet_id domain_id identifier true/false true/false, each managed hosts needs to have one primary interface. true/false true/false virtual=true: tag VLAN tag, this attribute has precedence over the subnet VLAN ID. Only for virtual interfaces. attached_to Identifier of the interface to which this interface belongs, e.g. eth1. type=bond: mode Possible values: balance-rr, active-backup, balance-xor, broadcast, 802.3ad, balance-tlb, balance-alb attached_devices Identifiers of slave interfaces, e.g. [eth1,eth2] bond_options type=bmc: provider always IPMI username password \u001b[1mNOTE:\u001b[0m Bold attributes are required. EC2: --volume: --interface: --compute-attributes: availability_zone flavor_id groups security_group_ids managed_ip Libvirt: --volume: \u001b[1mpool_name\u001b[0m One of available storage pools \u001b[1mcapacity\u001b[0m String value, e.g. 10G allocation Initial allocation, e.g. 0G format_type Possible values: raw, qcow2 --interface: compute_type Possible values: bridge, network compute_bridge Name of interface according to type compute_model Possible values: virtio, rtl8139, ne2k_pci, pcnet, e1000 compute_network Libvirt instance network, e.g. default --compute-attributes: \u001b[1mcpus\u001b[0m Number of CPUs \u001b[1mmemory\u001b[0m String, amount of memory, value in bytes cpu_mode Possible values: default, host-model, host-passthrough boot_order Device names to specify the boot order start Boolean (expressed as 0 or 1), whether to start the machine or not OpenStack: --volume: --interface: --compute-attributes: availability_zone boot_from_volume flavor_ref image_ref tenant_id security_groups network oVirt: --volume: size_gb Volume size in GB, integer value storage_domain ID or name of storage domain bootable Boolean, set 1 for bootable, only one volume can be bootable preallocate Boolean, set 1 to preallocate wipe_after_delete Boolean, set 1 to wipe disk after delete interface Disk interface name, must be ide, virto or virto_scsi --interface: compute_name Compute name, e.g. eth0 compute_network Select one of available networks for a cluster, must be an ID or a name compute_interface Interface type compute_vnic_profile Vnic Profile --compute-attributes: cluster ID or name of cluster to use template Hardware profile to use cores Integer value, number of cores sockets Integer value, number of sockets memory Amount of memory, integer value in bytes ha Boolean, set 1 to high availability display_type Possible values: VNC, SPICE keyboard_layout Possible values: ar, de-ch, es, fo, fr-ca, hu, ja, mk, no, pt-br, sv, da, en-gb, et, fr, fr-ch, is, lt, nl, pl, ru, th, de, en-us, fi, fr-be, hr, it, lv, nl-be, pt, sl, tr. Not usable if display type is SPICE. start Boolean, set 1 to start the vm Rackspace: --volume: --interface: --compute-attributes: flavor_id VMware: --volume: name storage_pod Storage Pod ID from VMware datastore Datastore ID from VMware mode persistent/independent_persistent/independent_nonpersistent size_gb Integer number, volume size in GB thin true/false eager_zero true/false controller_key Associated SCSI controller key --interface: compute_type Type of the network adapter, for example one of: VirtualVmxnet3 VirtualE1000 See documentation center for your version of vSphere to find more details about available adapter types: https://www.vmware.com/support/pubs/ compute_network Network ID or Network Name from VMware --compute-attributes: \u001b[1mcluster\u001b[0m Cluster ID from VMware \u001b[1mcorespersocket\u001b[0m Number of cores per socket (applicable to hardware versions < 10 only) \u001b[1mcpus\u001b[0m CPU count \u001b[1mmemory_mb\u001b[0m Integer number, amount of memory in MB \u001b[1mpath\u001b[0m Path to folder \u001b[1mresource_pool\u001b[0m Resource Pool ID from VMware firmware automatic/bios/efi guest_id Guest OS ID form VMware hardware_version Hardware version ID from VMware memoryHotAddEnabled Must be a 1 or 0, lets you add memory resources while the machine is on cpuHotAddEnabled Must be a 1 or 0, lets you add CPU resources while the machine is on add_cdrom Must be a 1 or 0, Add a CD-ROM drive to the virtual machine annotation Annotation Notes scsi_controllers List with SCSI controllers definitions type - ID of the controller from VMware key - Key of the controller (e.g. 1000) boot_order Device names to specify the boot order start Must be a 1 or 0, whether to start the machine or not AzureRM: --volume: disk_size_gb Volume Size in GB (integer value) data_disk_caching Data Disk Caching (None, ReadOnly, ReadWrite) --interface: compute_network Select one of available Azure Subnets, must be an ID compute_public_ip Public IP (None, Static, Dynamic) compute_private_ip Static Private IP (expressed as true or false) --compute-attributes: resource_group Existing Azure Resource Group of user vm_size VM Size, eg. Standard_A0 etc. username The Admin username password The Admin password platform OS type eg. Linux ssh_key_data SSH key for passwordless authentication os_disk_caching OS disk caching premium_os_disk Premium OS Disk, Boolean as 0 or 1 script_command Custom Script Command script_uris Comma seperated file URIs GCE: --volume: size_gb Volume size in GB, integer value --interface: --compute-attributes: machine_type network associate_external_ip", + "help": "Print help you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string parameters accept format defined by its schema (bold are required; <> contains acceptable type; [] contains acceptable value): \"name=\\,value=\\,parameter_type=[string|boolean|integer|real|array|hash|yaml|json]\\,hidden_value=[true|false|1|0], ... \" \"product_id=\\,product_name=\\,arch=\\,version=, ... \" mac ip Possible values: interface, bmc, bond, bridge name subnet_id domain_id identifier true/false true/false, each managed hosts needs to have one primary interface. true/false true/false virtual=true: tag VLAN tag, this attribute has precedence over the subnet VLAN ID. Only for virtual interfaces. attached_to Identifier of the interface to which this interface belongs, e.g. eth1. type=bond: mode Possible values: balance-rr, active-backup, balance-xor, broadcast, 802.3ad, balance-tlb, balance-alb attached_devices Identifiers of slave interfaces, e.g. [eth1,eth2] bond_options type=bmc: provider always IPMI username password \u001b[1mNOTE:\u001b[0m Bold attributes are required. EC2: --volume: --interface: --compute-attributes: availability_zone flavor_id groups security_group_ids managed_ip Libvirt: --volume: \u001b[1mpool_name\u001b[0m One of available storage pools \u001b[1mcapacity\u001b[0m String value, e.g. 10G allocation Initial allocation, e.g. 0G format_type Possible values: raw, qcow2 --interface: compute_type Possible values: bridge, network compute_bridge Name of interface according to type compute_model Possible values: virtio, rtl8139, ne2k_pci, pcnet, e1000 compute_network Libvirt instance network, e.g. default --compute-attributes: \u001b[1mcpus\u001b[0m Number of CPUs \u001b[1mmemory\u001b[0m String, amount of memory, value in bytes cpu_mode Possible values: default, host-model, host-passthrough boot_order Device names to specify the boot order start Boolean (expressed as 0 or 1), whether to start the machine or not OpenStack: --volume: --interface: --compute-attributes: availability_zone boot_from_volume flavor_ref image_ref tenant_id security_groups network oVirt: --volume: size_gb Volume size in GB, integer value storage_domain ID or name of storage domain bootable Boolean, set 1 for bootable, only one volume can be bootable preallocate Boolean, set 1 to preallocate wipe_after_delete Boolean, set 1 to wipe disk after delete interface Disk interface name, must be ide, virtio or virtio_scsi --interface: compute_name Compute name, e.g. eth0 compute_network Select one of available networks for a cluster, must be an ID or a name compute_interface Interface type compute_vnic_profile Vnic Profile --compute-attributes: cluster ID or name of cluster to use template Hardware profile to use cores Integer value, number of cores sockets Integer value, number of sockets memory Amount of memory, integer value in bytes ha Boolean, set 1 to high availability display_type Possible values: VNC, SPICE keyboard_layout Possible values: ar, de-ch, es, fo, fr-ca, hu, ja, mk, no, pt-br, sv, da, en-gb, et, fr, fr-ch, is, lt, nl, pl, ru, th, de, en-us, fi, fr-be, hr, it, lv, nl-be, pt, sl, tr. Not usable if display type is SPICE. start Boolean, set 1 to start the vm Rackspace: --volume: --interface: --compute-attributes: flavor_id VMware: --volume: name storage_pod Storage Pod ID from VMware datastore Datastore ID from VMware mode persistent/independent_persistent/independent_nonpersistent size_gb Integer number, volume size in GB thin true/false eager_zero true/false controller_key Associated SCSI controller key --interface: compute_type Type of the network adapter, for example one of: VirtualVmxnet3 VirtualE1000 See documentation center for your version of vSphere to find more details about available adapter types: https://www.vmware.com/support/pubs/ compute_network Network ID or Network Name from VMware --compute-attributes: \u001b[1mcluster\u001b[0m Cluster ID from VMware \u001b[1mcorespersocket\u001b[0m Number of cores per socket (applicable to hardware versions < 10 only) \u001b[1mcpus\u001b[0m CPU count \u001b[1mmemory_mb\u001b[0m Integer number, amount of memory in MB \u001b[1mpath\u001b[0m Path to folder \u001b[1mresource_pool\u001b[0m Resource Pool ID from VMware firmware automatic/bios/efi guest_id Guest OS ID form VMware hardware_version Hardware version ID from VMware memoryHotAddEnabled Must be a 1 or 0, lets you add memory resources while the machine is on cpuHotAddEnabled Must be a 1 or 0, lets you add CPU resources while the machine is on add_cdrom Must be a 1 or 0, Add a CD-ROM drive to the virtual machine annotation Annotation Notes scsi_controllers List with SCSI controllers definitions type - ID of the controller from VMware key - Key of the controller (e.g. 1000) boot_order Device names to specify the boot order start Must be a 1 or 0, whether to start the machine or not AzureRM: --volume: disk_size_gb Volume Size in GB (integer value) data_disk_caching Data Disk Caching (None, ReadOnly, ReadWrite) --interface: compute_network Select one of available Azure Subnets, must be an ID compute_public_ip Public IP (None, Static, Dynamic) compute_private_ip Static Private IP (expressed as true or false) --compute-attributes: resource_group Existing Azure Resource Group of user vm_size VM Size, eg. Standard_A0 etc. username The Admin username password The Admin password platform OS type eg. Linux ssh_key_data SSH key for passwordless authentication os_disk_caching OS disk caching premium_os_disk Premium OS Disk, Boolean as 0 or 1 script_command Custom Script Command script_uris Comma seperated file URIs GCE: --volume: size_gb Volume size in GB, integer value --interface: --compute-attributes: machine_type network associate_external_ip", "name": "help", "shortname": "h", "value": null @@ -25197,7 +25264,7 @@ "value": "BOOLEAN" }, { - "help": "Print help ------------|-----|---------|----- | ALL | DEFAULT | THIN ------------|-----|---------|----- | x | x | x | x | x | x | x | | | x | | | x | | ------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string string string string Values: mismatched, matched, not_specified string string string date string string boolean boot_time Values: true, false Values: built, pending, token_expired, build_failed text string integer string string integer datetime integer string integer Values: security_needed, errata_needed, updated, unknown Values: ok, error string Values: ok, warning, error string string string string string integer string string boolean string integer string infrastructure_facet.foreman infrastructure_facet.smart_proxy_id Values: reporting, no_report Values: disconnect, sync integer string datetime string string job_invocation.id string job_invocation.result Values: cancelled, failed, pending, success datetime datetime string integer string integer string Values: true, false string string string integer string string string integer string string string string integer string string string string string integer string Values: mismatched, matched, not_specified string integer datetime string string reported.boot_time reported.cores reported.disks_total reported.kernel_version reported.ram reported.sockets reported.virtual Values: true, false string string text Values: mismatched, matched, not_specified string Values: mismatched, matched, not_specified string status.applied integer status.enabled Values: true, false status.failed integer status.failed_restarts integer status.interesting Values: true, false status.pending integer status.restarted integer status.skipped integer string subnet.name text string subnet6.name text string string Values: valid, partial, invalid, unknown, disabled, unsubscribed_hypervisor string Values: reboot_needed, process_restart_needed, updated string string text Values: mismatched, matched, not_specified user.firstname string user.lastname string user.login string user.mail string string usergroup.name string string", + "help": "Print help ------------|-----|---------|----- | ALL | DEFAULT | THIN ------------|-----|---------|----- | x | x | x | x | x | x | x | | | x | | | x | | ------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string string string string Values: mismatched, matched, not_specified string string string date string string boolean boot_time Values: true, false Values: built, pending, token_expired, build_failed text string integer configuration_status.applied integer configuration_status.enabled Values: true, false configuration_status.failed integer configuration_status.failed_restarts integer configuration_status.interesting Values: true, false configuration_status.pending integer configuration_status.restarted integer configuration_status.skipped integer string string datetime integer string integer Values: security_needed, errata_needed, updated, unknown Values: ok, error string Values: ok, warning, error string string string string string integer string string boolean string integer string infrastructure_facet.foreman Values: true, false infrastructure_facet.smart_proxy_id Values: reporting, no_report Values: disconnect, sync integer string datetime string string job_invocation.id string job_invocation.result Values: cancelled, failed, pending, success datetime datetime string string integer string Values: true, false string string string integer string string string integer string string string string integer string string string string string integer string Values: mismatched, matched, not_specified Values: PXELinux_BIOS, PXELinux_UEFI, Grub_UEFI, Grub2_BIOS, Grub2_ELF, Grub2_UEFI, Grub2_UEFI_SecureBoot, Grub2_UEFI_HTTP, Grub2_UEFI_HTTPS, Grub2_UEFI_HTTPS_SecureBoot, iPXE_Embedded, iPXE_UEFI_HTTP, iPXE_Chain_BIOS, iPXE_Chain_UEFI string integer datetime string string reported.bios_release_date reported.bios_vendor reported.bios_version reported.boot_time reported.cores reported.disks_total reported.kernel_version reported.ram reported.sockets reported.virtual Values: true, false string string text Values: mismatched, matched, not_specified string Values: mismatched, matched, not_specified string status.applied integer status.enabled Values: true, false status.failed integer status.failed_restarts integer status.interesting Values: true, false status.pending integer status.restarted integer status.skipped integer string subnet.name text string subnet6.name text string string Values: valid, partial, invalid, unknown, disabled, unsubscribed_hypervisor string Values: reboot_needed, process_restart_needed, updated string string text Values: mismatched, matched, not_specified user.firstname string user.lastname string user.login string user.mail string string usergroup.name string string", "name": "help", "shortname": "h", "value": null @@ -28392,6 +28459,12 @@ "shortname": null, "value": "NUMBER" }, + { + "help": "Override the global time to pickup interval for this invocation only", + "name": "time-to-pickup", + "shortname": null, + "value": "NUMBER" + }, { "help": "Print help you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", "name": "help", @@ -28442,7 +28515,7 @@ "value": null }, { - "help": "Print help --------------------|-----|-------- | ALL | DEFAULT --------------------|-----|-------- | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x ordering | x | x | x | x category | x | x | x | x line | x | x logic id | x | x | x | x --------------------|-----|-------- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", + "help": "Print help --------------------|-----|-------- | ALL | DEFAULT --------------------|-----|-------- | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x ordering | x | x | x | x category | x | x | x | x line | x | x logic id | x | x to pickup | x | x | x | x --------------------|-----|-------- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", "name": "help", "shortname": "h", "value": null @@ -28663,6 +28736,12 @@ "description": "Create a job template", "name": "create", "options": [ + { + "help": "Enable the callback plugin for this template", + "name": "ansible-callback-enabled", + "shortname": null, + "value": "BOOLEAN" + }, { "help": "", "name": "audit-comment", @@ -29131,7 +29210,7 @@ "value": null }, { - "help": "Print help ---------------|-----|---------|----- | ALL | DEFAULT | THIN ---------------|-----|---------|----- | x | x | x | x | x | x category | x | x | | x | x | | x | x | | x | x | | x | x | Locations/ | x | x | Organizations/ | x | x | ---------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", + "help": "Print help -------------------------|-----|---------|----- | ALL | DEFAULT | THIN -------------------------|-----|---------|----- | x | x | x | x | x | x category | x | x | | x | x | | x | x | callback enabled | x | x | | x | x | | x | x | Locations/ | x | x | Organizations/ | x | x | -------------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", "name": "help", "shortname": "h", "value": null @@ -29222,6 +29301,12 @@ "description": "Update a job template", "name": "update", "options": [ + { + "help": "Enable the callback plugin for this template", + "name": "ansible-callback-enabled", + "shortname": null, + "value": "BOOLEAN" + }, { "help": "", "name": "audit-comment", @@ -29607,10 +29692,10 @@ "value": "VALUE" }, { - "help": "Set true if you want to see only library environments Possible value(s): 'true', 'false'", + "help": "Set true if you want to see only library environments", "name": "library", "shortname": null, - "value": "ENUM" + "value": "BOOLEAN" }, { "help": "Filter only environments containing this name", @@ -29679,6 +29764,12 @@ "description": "List environment paths", "name": "paths", "options": [ + { + "help": "Show whether each lifecycle environment is associated with the given Capsule id.", + "name": "content-source-id", + "shortname": null, + "value": "NUMBER" + }, { "help": "Show specified fields or predefined field sets only. (See below)", "name": "fields", @@ -34523,6 +34614,12 @@ "shortname": null, "value": "VALUE" }, + { + "help": "Whether Simple Content Access should be enabled for the organization.", + "name": "simple-content-access", + "shortname": null, + "value": "BOOLEAN" + }, { "help": "Capsule names/ids", "name": "smart-proxies", @@ -37378,7 +37475,7 @@ "value": "BOOLEAN" }, { - "help": "Print help -----------------------|-----|---------|----- | ALL | DEFAULT | THIN -----------------------|-----|---------|----- | x | x | x | x | x | x system | x | x | group | x | x | | x | x | | x | x | status | x | x | | x | | | x | | information | x | | view | x | x | environment | x | x | | x | | | x | | | x | | status | x | x | -----------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string string string string Values: mismatched, matched, not_specified string string string date string string boolean boot_time Values: true, false Values: built, pending, token_expired, build_failed text string integer string string integer datetime integer string integer Values: security_needed, errata_needed, updated, unknown Values: ok, error string Values: ok, warning, error string string string string string integer string string boolean string integer string infrastructure_facet.foreman infrastructure_facet.smart_proxy_id Values: reporting, no_report Values: disconnect, sync integer string datetime string string job_invocation.id string job_invocation.result Values: cancelled, failed, pending, success datetime datetime string integer string integer string Values: true, false string string string integer string string string integer string string string string integer string string string string string integer string Values: mismatched, matched, not_specified string integer datetime string string reported.boot_time reported.cores reported.disks_total reported.kernel_version reported.ram reported.sockets reported.virtual Values: true, false string string text Values: mismatched, matched, not_specified string Values: mismatched, matched, not_specified string status.applied integer status.enabled Values: true, false status.failed integer status.failed_restarts integer status.interesting Values: true, false status.pending integer status.restarted integer status.skipped integer string subnet.name text string subnet6.name text string string Values: valid, partial, invalid, unknown, disabled, unsubscribed_hypervisor string Values: reboot_needed, process_restart_needed, updated string string text Values: mismatched, matched, not_specified user.firstname string user.lastname string user.login string user.mail string string usergroup.name string string", + "help": "Print help -----------------------|-----|---------|----- | ALL | DEFAULT | THIN -----------------------|-----|---------|----- | x | x | x | x | x | x system | x | x | group | x | x | | x | x | | x | x | status | x | x | | x | | | x | | information | x | | view | x | x | environment | x | x | | x | | | x | | | x | | status | x | x | -----------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string string string string Values: mismatched, matched, not_specified string string string date string string boolean boot_time Values: true, false Values: built, pending, token_expired, build_failed text string integer configuration_status.applied integer configuration_status.enabled Values: true, false configuration_status.failed integer configuration_status.failed_restarts integer configuration_status.interesting Values: true, false configuration_status.pending integer configuration_status.restarted integer configuration_status.skipped integer string string datetime integer string integer Values: security_needed, errata_needed, updated, unknown Values: ok, error string Values: ok, warning, error string string string string string integer string string boolean string integer string infrastructure_facet.foreman Values: true, false infrastructure_facet.smart_proxy_id Values: reporting, no_report Values: disconnect, sync integer string datetime string string job_invocation.id string job_invocation.result Values: cancelled, failed, pending, success datetime datetime string string integer string Values: true, false string string string integer string string string integer string string string string integer string string string string string integer string Values: mismatched, matched, not_specified Values: PXELinux_BIOS, PXELinux_UEFI, Grub_UEFI, Grub2_BIOS, Grub2_ELF, Grub2_UEFI, Grub2_UEFI_SecureBoot, Grub2_UEFI_HTTP, Grub2_UEFI_HTTPS, Grub2_UEFI_HTTPS_SecureBoot, iPXE_Embedded, iPXE_UEFI_HTTP, iPXE_Chain_BIOS, iPXE_Chain_UEFI string integer datetime string string reported.bios_release_date reported.bios_vendor reported.bios_version reported.boot_time reported.cores reported.disks_total reported.kernel_version reported.ram reported.sockets reported.virtual Values: true, false string string text Values: mismatched, matched, not_specified string Values: mismatched, matched, not_specified string status.applied integer status.enabled Values: true, false status.failed integer status.failed_restarts integer status.interesting Values: true, false status.pending integer status.restarted integer status.skipped integer string subnet.name text string subnet6.name text string string Values: valid, partial, invalid, unknown, disabled, unsubscribed_hypervisor string Values: reboot_needed, process_restart_needed, updated string string text Values: mismatched, matched, not_specified user.firstname string user.lastname string user.login string user.mail string string usergroup.name string string", "name": "help", "shortname": "h", "value": null @@ -39227,7 +39324,7 @@ "value": null }, { - "help": "Print help -----------------|-----|---------|----- | ALL | DEFAULT | THIN -----------------|-----|---------|----- | x | x | x | x | x | x | x | x | | x | x | | x | x | | x | x | | x | x | Features/name | x | x | Features/version | x | x | Locations/ | x | x | Organizations/ | x | x | at | x | x | at | x | x | -----------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", + "help": "Print help -----------------|-----|---------|----- | ALL | DEFAULT | THIN -----------------|-----|---------|----- | x | x | x | x | x | x | x | x | | x | x | | x | x | | x | x | count | x | x | Features/name | x | x | Features/version | x | x | Locations/ | x | x | Organizations/ | x | x | at | x | x | at | x | x | -----------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", "name": "help", "shortname": "h", "value": null @@ -40106,7 +40203,7 @@ "value": null }, { - "help": "Print help ----------------|-----|-------- | ALL | DEFAULT ----------------|-----|-------- | x | x line | x | x | x | x occurrence | x | x occurrence | x | x | x | x limit | x | x until | x | x | x | x ----------------|-----|-------- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", + "help": "Print help ----------------|-----|-------- | ALL | DEFAULT ----------------|-----|-------- | x | x line | x | x | x | x occurrence | x | x occurrence | x | x count | x | x | x | x occurrence | x | x occurrence | x | x | x | x limit | x | x limit | x | x until | x | x | x | x | x | x ----------------|-----|-------- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", "name": "help", "shortname": "h", "value": null @@ -40185,7 +40282,7 @@ "value": "VALUE" }, { - "help": "Print help ----------|-----|-------- | ALL | DEFAULT ----------|-----|-------- | x | x line | x | x | x | x time | x | x | x | x ----------|-----|-------- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", + "help": "Print help ----------------|-----|-------- | ALL | DEFAULT ----------------|-----|-------- | x | x line | x | x count | x | x | x | x occurrence | x | x occurrence | x | x | x | x limit | x | x time | x | x | x | x | x | x ----------------|-----|-------- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", "name": "help", "shortname": "h", "value": null @@ -40530,7 +40627,7 @@ "value": null }, { - "help": "Print help --------------------------------|-----|---------|----- | ALL | DEFAULT | THIN --------------------------------|-----|---------|----- | x | x | x | x | x | at | x | x | | x | x | status/applied | x | x | status/restarted | x | x | status/failed | x | x | status/restart failures | x | x | status/skipped | x | x | status/pending | x | x | metrics/config_retrieval | x | x | metrics/exec | x | x | metrics/file | x | x | metrics/package | x | x | metrics/service | x | x | metrics/user | x | x | metrics/yumrepo | x | x | metrics/filebucket | x | x | metrics/cron | x | x | metrics/total | x | x | Logs/resource | x | x | Logs/message | x | x | --------------------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", + "help": "Print help --------------------------------|-----|---------|----- | ALL | DEFAULT | THIN --------------------------------|-----|---------|----- | x | x | x | x | x | at | x | x | | x | x | status/applied | x | x | status/restarted | x | x | status/failed | x | x | status/restart failures | x | x | status/skipped | x | x | status/pending | x | x | metrics/config retrieval | x | x | metrics/exec | x | x | metrics/file | x | x | metrics/package | x | x | metrics/service | x | x | metrics/user | x | x | metrics/yumrepo | x | x | metrics/filebucket | x | x | metrics/cron | x | x | metrics/total | x | x | Logs/resource | x | x | Logs/message | x | x | --------------------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", "name": "help", "shortname": "h", "value": null @@ -41844,7 +41941,7 @@ "value": "ENUM" }, { - "help": "List of content units to ignore while syncing a yum repository. Must be subset of srpm", + "help": "List of content units to ignore while syncing a yum repository. Must be subset of srpm,treeinfo", "name": "ignorable-content", "shortname": null, "value": "LIST" @@ -41868,10 +41965,10 @@ "value": "VALUE" }, { - "help": "True if this repository when synced has to be mirrored from the source and stale rpms removed (Deprecated)", - "name": "mirror-on-sync", + "help": "Time to expire yum metadata in seconds. Only relevant for custom yum repositories.", + "name": "metadata-expire", "shortname": null, - "value": "BOOLEAN" + "value": "NUMBER" }, { "help": "Policy to set for mirroring content. Must be one of additive. Possible value(s): 'additive', 'mirror_complete', 'mirror_content_only'", @@ -42122,7 +42219,7 @@ "value": null }, { - "help": "Print help ----------------------------------------------|-----|---------|----- | ALL | DEFAULT | THIN ----------------------------------------------|-----|---------|----- | x | x | x | x | x | x | x | x | | x | x | | x | x | hat repository | x | x | type | x | x | type | x | x | policy | x | x | | x | x | via http | x | x | at | x | x | path | x | x | policy | x | x | repository name | x | x | image tags filter | x | x | repository name | x | x | content units | x | x | proxy/id | x | x | proxy/name | x | x | proxy/http proxy policy | x | x | Product/id | x | x | Product/name | x | x | key/id | x | x | key/name | x | x | Sync/status | x | x | Sync/last sync date | x | x | | x | x | | x | x | counts/packages | x | x | counts/source rpms | x | x | counts/package groups | x | x | counts/errata | x | x | counts/container image manifest lists | x | x | counts/container image manifests | x | x | counts/container image tags | x | x | counts/files | x | x | counts/module streams | x | x | ----------------------------------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", + "help": "Print help ----------------------------------------------|-----|---------|----- | ALL | DEFAULT | THIN ----------------------------------------------|-----|---------|----- | x | x | x | x | x | x | x | x | | x | x | | x | x | hat repository | x | x | type | x | x | label | x | x | type | x | x | policy | x | x | | x | x | via http | x | x | at | x | x | path | x | x | policy | x | x | expiration | x | x | repository name | x | x | image tags filter | x | x | repository name | x | x | content units | x | x | proxy/id | x | x | proxy/name | x | x | proxy/http proxy policy | x | x | Product/id | x | x | Product/name | x | x | key/id | x | x | key/name | x | x | Sync/status | x | x | Sync/last sync date | x | x | | x | x | | x | x | counts/packages | x | x | counts/source rpms | x | x | counts/package groups | x | x | counts/errata | x | x | counts/container image manifest lists | x | x | counts/container image manifests | x | x | counts/container image tags | x | x | counts/files | x | x | counts/module streams | x | x | ----------------------------------------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string", "name": "help", "shortname": "h", "value": null @@ -42357,7 +42454,7 @@ "value": "VALUE" }, { - "help": "Print help -------------|-----|---------|----- | ALL | DEFAULT | THIN -------------|-----|---------|----- | x | x | x | x | x | x | x | x | type | x | x | | x | x | -------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string string string string integer text string boolean string string string string string string string integer string Values: true, false", + "help": "Print help --------------|-----|---------|----- | ALL | DEFAULT | THIN --------------|-----|---------|----- | x | x | x | x | x | x | x | x | type | x | x | label | x | x | | x | x | --------------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string string string string integer text string boolean string string string string string string string integer string Values: true, false", "name": "help", "shortname": "h", "value": null @@ -42661,6 +42758,12 @@ "description": "Show the available repository types", "name": "types", "options": [ + { + "help": "When set to 'True' repository types that are creatable will be returned", + "name": "creatable", + "shortname": null, + "value": "BOOLEAN" + }, { "help": "Show specified fields or predefined field sets only. (See below)", "name": "fields", @@ -42819,7 +42922,7 @@ "value": "NUMBER" }, { - "help": "List of content units to ignore while syncing a yum repository. Must be subset of srpm", + "help": "List of content units to ignore while syncing a yum repository. Must be subset of srpm,treeinfo", "name": "ignorable-content", "shortname": null, "value": "LIST" @@ -42837,10 +42940,10 @@ "value": "LIST" }, { - "help": "True if this repository when synced has to be mirrored from the source and stale rpms removed (Deprecated)", - "name": "mirror-on-sync", + "help": "Time to expire yum metadata in seconds. Only relevant for custom yum repositories.", + "name": "metadata-expire", "shortname": null, - "value": "BOOLEAN" + "value": "NUMBER" }, { "help": "Policy to set for mirroring content. Must be one of additive. Possible value(s): 'additive', 'mirror_complete', 'mirror_content_only'", @@ -43486,6 +43589,12 @@ "shortname": null, "value": null }, + { + "help": "Limit content to Red Hat / custom Possible value(s): 'redhat', 'custom'", + "name": "repository-type", + "shortname": null, + "value": "ENUM" + }, { "help": "Search string", "name": "search", @@ -43505,13 +43614,13 @@ "value": "BOOLEAN" }, { - "help": "If true, return custom repository sets along with redhat repos", + "help": "If true, return custom repository sets along with redhat repos. Will be ignored if repository_type is supplied.", "name": "with-custom", "shortname": null, "value": "BOOLEAN" }, { - "help": "Print help -------|-----|---------|----- | ALL | DEFAULT | THIN -------|-----|---------|----- | x | x | x | x | x | | x | x | x -------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string string string Values: true, false string string string string integer string", + "help": "Print help -------|-----|---------|----- | ALL | DEFAULT | THIN -------|-----|---------|----- | x | x | x | x | x | | x | x | x -------|-----|---------|----- you can find option types and the value an option can accept: One of true/false, yes/no, 1/0 Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format Possible values are described in the option's description Path to a file Comma-separated list of key=value. JSON is acceptable and preferred way for such parameters Comma separated list of values. Values containing comma should be quoted or escaped with backslash. JSON is acceptable and preferred way for such parameters Any combination of possible values described in the option's description Numeric value. Integer Comma separated list of values defined by a schema. JSON is acceptable and preferred way for such parameters Value described in the option's description. Mostly simple string string string Values: true, false string string string string integer string Values: true, false", "name": "help", "shortname": "h", "value": null @@ -52788,12 +52897,6 @@ "shortname": null, "value": "BOOLEAN" }, - { - "help": "The frequency of VM-to-host mapping updates for AHV(in seconds)", - "name": "ahv-update-interval", - "shortname": null, - "value": "NUMBER" - }, { "help": "Hypervisor blacklist, applicable only when filtering mode is set to 2. Wildcards and regular expressions are supported, multiple records must be separated by comma.", "name": "blacklist", @@ -53298,12 +53401,6 @@ "shortname": null, "value": "BOOLEAN" }, - { - "help": "The frequency of VM-to-host mapping updates for AHV(in seconds)", - "name": "ahv-update-interval", - "shortname": null, - "value": "NUMBER" - }, { "help": "Hypervisor blacklist, applicable only when filtering mode is set to 2. Wildcards and regular expressions are supported, multiple records must be separated by comma.", "name": "blacklist", @@ -53500,7 +53597,7 @@ "value": "BOOLEAN" }, { - "help": "Possible value(s): 'actions.katello.content_view.promote_succeeded', 'actions.katello.content_view.publish_succeeded', 'actions.katello.repository.sync_succeeded', 'actions.remote_execution.run_host_job_ansible_configure_cloud_connector_succeeded', 'actions.remote_execution.run_host_job_ansible_enable_web_console_succeeded', 'actions.remote_execution.run_host_job_ansible_run_capsule_upgrade_succeeded', 'actions.remote_execution.run_host_job_ansible_run_host_succeeded', 'actions.remote_execution.run_host_job_ansible_run_insights_plan_succeeded', 'actions.remote_execution.run_host_job_ansible_run_playbook_succeeded', 'actions.remote_execution.run_host_job_foreman_openscap_run_oval_scans_succeeded', 'actions.remote_execution.run_host_job_foreman_openscap_run_scans_succeeded', 'actions.remote_execution.run_host_job_katello_errata_install_by_search_succeeded', 'actions.remote_execution.run_host_job_katello_errata_install_succeeded', 'actions.remote_execution.run_host_job_katello_group_install_succeeded', 'actions.remote_execution.run_host_job_katello_group_remove_succeeded', 'actions.remote_execution.run_host_job_katello_group_update_succeeded', 'actions.remote_execution.run_host_job_katello_host_tracer_resolve_succeeded', 'actions.remote_execution.run_host_job_katello_module_stream_action_succeeded', 'actions.remote_execution.run_host_job_katello_package_install_by_search_succeeded', 'actions.remote_execution.run_host_job_katello_package_install_succeeded', 'actions.remote_execution.run_host_job_katello_package_remove_succeeded', 'actions.remote_execution.run_host_job_katello_package_update_succeeded', 'actions.remote_execution.run_host_job_katello_packages_remove_by_search_succeeded', 'actions.remote_execution.run_host_job_katello_packages_update_by_search_succeeded', 'actions.remote_execution.run_host_job_katello_service_restart_succeeded', 'actions.remote_execution.run_host_job_leapp_preupgrade_succeeded', 'actions.remote_execution.run_host_job_leapp_remediation_plan_succeeded', 'actions.remote_execution.run_host_job_leapp_upgrade_succeeded', 'actions.remote_execution.run_host_job_puppet_run_host_succeeded', 'actions.remote_execution.run_host_job_rh_cloud_connector_run_playbook_succeeded', 'actions.remote_execution.run_host_job_rh_cloud_remediate_hosts_succeeded', 'actions.remote_execution.run_host_job_succeeded', 'build_entered', 'build_exited', 'content_view_created', 'content_view_destroyed', 'content_view_updated', 'domain_created', 'domain_destroyed', 'domain_updated', 'host_created', 'host_destroyed', 'host_updated', 'hostgroup_created', 'hostgroup_destroyed', 'hostgroup_updated', 'model_created', 'model_destroyed', 'model_updated', 'status_changed', 'subnet_created', 'subnet_destroyed', 'subnet_updated', 'user_created', 'user_destroyed', 'user_updated'", + "help": "Possible value(s): 'actions.katello.content_view.promote_succeeded', 'actions.katello.content_view.publish_succeeded', 'actions.katello.repository.sync_succeeded', 'actions.remote_execution.run_host_job_ansible_configure_cloud_connector_succeeded', 'actions.remote_execution.run_host_job_ansible_enable_web_console_succeeded', 'actions.remote_execution.run_host_job_ansible_run_capsule_upgrade_succeeded', 'actions.remote_execution.run_host_job_ansible_run_host_succeeded', 'actions.remote_execution.run_host_job_ansible_run_insights_plan_succeeded', 'actions.remote_execution.run_host_job_ansible_run_playbook_succeeded', 'actions.remote_execution.run_host_job_foreman_openscap_run_oval_scans_succeeded', 'actions.remote_execution.run_host_job_foreman_openscap_run_scans_succeeded', 'actions.remote_execution.run_host_job_katello_errata_install_by_search_succeeded', 'actions.remote_execution.run_host_job_katello_errata_install_succeeded', 'actions.remote_execution.run_host_job_katello_group_install_succeeded', 'actions.remote_execution.run_host_job_katello_group_remove_succeeded', 'actions.remote_execution.run_host_job_katello_group_update_succeeded', 'actions.remote_execution.run_host_job_katello_host_tracer_resolve_succeeded', 'actions.remote_execution.run_host_job_katello_module_stream_action_succeeded', 'actions.remote_execution.run_host_job_katello_package_install_by_search_succeeded', 'actions.remote_execution.run_host_job_katello_package_install_succeeded', 'actions.remote_execution.run_host_job_katello_package_remove_succeeded', 'actions.remote_execution.run_host_job_katello_package_update_succeeded', 'actions.remote_execution.run_host_job_katello_packages_remove_by_search_succeeded', 'actions.remote_execution.run_host_job_katello_packages_update_by_search_succeeded', 'actions.remote_execution.run_host_job_katello_service_restart_succeeded', 'actions.remote_execution.run_host_job_leapp_preupgrade_succeeded', 'actions.remote_execution.run_host_job_leapp_remediation_plan_succeeded', 'actions.remote_execution.run_host_job_leapp_upgrade_succeeded', 'actions.remote_execution.run_host_job_puppet_run_host_succeeded', 'actions.remote_execution.run_host_job_rh_cloud_connector_run_playbook_succeeded', 'actions.remote_execution.run_host_job_rh_cloud_remediate_hosts_succeeded', 'actions.remote_execution.run_host_job_run_script_succeeded', 'actions.remote_execution.run_host_job_succeeded', 'actions.remote_execution.run_hosts_job_running', 'actions.remote_execution.run_hosts_job_succeeded', 'build_entered', 'build_exited', 'content_view_created', 'content_view_destroyed', 'content_view_updated', 'domain_created', 'domain_destroyed', 'domain_updated', 'host_created', 'host_destroyed', 'host_updated', 'hostgroup_created', 'hostgroup_destroyed', 'hostgroup_updated', 'model_created', 'model_destroyed', 'model_updated', 'status_changed', 'subnet_created', 'subnet_destroyed', 'subnet_updated', 'user_created', 'user_destroyed', 'user_updated'", "name": "event", "shortname": null, "value": "ENUM" @@ -53840,7 +53937,7 @@ "value": "BOOLEAN" }, { - "help": "Possible value(s): 'actions.katello.content_view.promote_succeeded', 'actions.katello.content_view.publish_succeeded', 'actions.katello.repository.sync_succeeded', 'actions.remote_execution.run_host_job_ansible_configure_cloud_connector_succeeded', 'actions.remote_execution.run_host_job_ansible_enable_web_console_succeeded', 'actions.remote_execution.run_host_job_ansible_run_capsule_upgrade_succeeded', 'actions.remote_execution.run_host_job_ansible_run_host_succeeded', 'actions.remote_execution.run_host_job_ansible_run_insights_plan_succeeded', 'actions.remote_execution.run_host_job_ansible_run_playbook_succeeded', 'actions.remote_execution.run_host_job_foreman_openscap_run_oval_scans_succeeded', 'actions.remote_execution.run_host_job_foreman_openscap_run_scans_succeeded', 'actions.remote_execution.run_host_job_katello_errata_install_by_search_succeeded', 'actions.remote_execution.run_host_job_katello_errata_install_succeeded', 'actions.remote_execution.run_host_job_katello_group_install_succeeded', 'actions.remote_execution.run_host_job_katello_group_remove_succeeded', 'actions.remote_execution.run_host_job_katello_group_update_succeeded', 'actions.remote_execution.run_host_job_katello_host_tracer_resolve_succeeded', 'actions.remote_execution.run_host_job_katello_module_stream_action_succeeded', 'actions.remote_execution.run_host_job_katello_package_install_by_search_succeeded', 'actions.remote_execution.run_host_job_katello_package_install_succeeded', 'actions.remote_execution.run_host_job_katello_package_remove_succeeded', 'actions.remote_execution.run_host_job_katello_package_update_succeeded', 'actions.remote_execution.run_host_job_katello_packages_remove_by_search_succeeded', 'actions.remote_execution.run_host_job_katello_packages_update_by_search_succeeded', 'actions.remote_execution.run_host_job_katello_service_restart_succeeded', 'actions.remote_execution.run_host_job_leapp_preupgrade_succeeded', 'actions.remote_execution.run_host_job_leapp_remediation_plan_succeeded', 'actions.remote_execution.run_host_job_leapp_upgrade_succeeded', 'actions.remote_execution.run_host_job_puppet_run_host_succeeded', 'actions.remote_execution.run_host_job_rh_cloud_connector_run_playbook_succeeded', 'actions.remote_execution.run_host_job_rh_cloud_remediate_hosts_succeeded', 'actions.remote_execution.run_host_job_succeeded', 'build_entered', 'build_exited', 'content_view_created', 'content_view_destroyed', 'content_view_updated', 'domain_created', 'domain_destroyed', 'domain_updated', 'host_created', 'host_destroyed', 'host_updated', 'hostgroup_created', 'hostgroup_destroyed', 'hostgroup_updated', 'model_created', 'model_destroyed', 'model_updated', 'status_changed', 'subnet_created', 'subnet_destroyed', 'subnet_updated', 'user_created', 'user_destroyed', 'user_updated'", + "help": "Possible value(s): 'actions.katello.content_view.promote_succeeded', 'actions.katello.content_view.publish_succeeded', 'actions.katello.repository.sync_succeeded', 'actions.remote_execution.run_host_job_ansible_configure_cloud_connector_succeeded', 'actions.remote_execution.run_host_job_ansible_enable_web_console_succeeded', 'actions.remote_execution.run_host_job_ansible_run_capsule_upgrade_succeeded', 'actions.remote_execution.run_host_job_ansible_run_host_succeeded', 'actions.remote_execution.run_host_job_ansible_run_insights_plan_succeeded', 'actions.remote_execution.run_host_job_ansible_run_playbook_succeeded', 'actions.remote_execution.run_host_job_foreman_openscap_run_oval_scans_succeeded', 'actions.remote_execution.run_host_job_foreman_openscap_run_scans_succeeded', 'actions.remote_execution.run_host_job_katello_errata_install_by_search_succeeded', 'actions.remote_execution.run_host_job_katello_errata_install_succeeded', 'actions.remote_execution.run_host_job_katello_group_install_succeeded', 'actions.remote_execution.run_host_job_katello_group_remove_succeeded', 'actions.remote_execution.run_host_job_katello_group_update_succeeded', 'actions.remote_execution.run_host_job_katello_host_tracer_resolve_succeeded', 'actions.remote_execution.run_host_job_katello_module_stream_action_succeeded', 'actions.remote_execution.run_host_job_katello_package_install_by_search_succeeded', 'actions.remote_execution.run_host_job_katello_package_install_succeeded', 'actions.remote_execution.run_host_job_katello_package_remove_succeeded', 'actions.remote_execution.run_host_job_katello_package_update_succeeded', 'actions.remote_execution.run_host_job_katello_packages_remove_by_search_succeeded', 'actions.remote_execution.run_host_job_katello_packages_update_by_search_succeeded', 'actions.remote_execution.run_host_job_katello_service_restart_succeeded', 'actions.remote_execution.run_host_job_leapp_preupgrade_succeeded', 'actions.remote_execution.run_host_job_leapp_remediation_plan_succeeded', 'actions.remote_execution.run_host_job_leapp_upgrade_succeeded', 'actions.remote_execution.run_host_job_puppet_run_host_succeeded', 'actions.remote_execution.run_host_job_rh_cloud_connector_run_playbook_succeeded', 'actions.remote_execution.run_host_job_rh_cloud_remediate_hosts_succeeded', 'actions.remote_execution.run_host_job_run_script_succeeded', 'actions.remote_execution.run_host_job_succeeded', 'actions.remote_execution.run_hosts_job_running', 'actions.remote_execution.run_hosts_job_succeeded', 'build_entered', 'build_exited', 'content_view_created', 'content_view_destroyed', 'content_view_updated', 'domain_created', 'domain_destroyed', 'domain_updated', 'host_created', 'host_destroyed', 'host_updated', 'hostgroup_created', 'hostgroup_destroyed', 'hostgroup_updated', 'model_created', 'model_destroyed', 'model_updated', 'status_changed', 'subnet_created', 'subnet_destroyed', 'subnet_updated', 'user_created', 'user_destroyed', 'user_updated'", "name": "event", "shortname": null, "value": "ENUM" From 20de2a2eced61384647fccc064e161038e52f2bf Mon Sep 17 00:00:00 2001 From: Peter Ondrejka Date: Thu, 22 Jun 2023 13:05:02 +0200 Subject: [PATCH 039/586] api changes in 6.14 (#11719) api changes --- tests/foreman/endtoend/test_api_endtoend.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/foreman/endtoend/test_api_endtoend.py b/tests/foreman/endtoend/test_api_endtoend.py index 6cd332e3e54..5500e556ef6 100644 --- a/tests/foreman/endtoend/test_api_endtoend.py +++ b/tests/foreman/endtoend/test_api_endtoend.py @@ -431,6 +431,8 @@ '/api/hostgroups/:id/play_roles', '/api/hostgroups/multiple_play_roles', '/api/hostgroups/:id/ansible_roles', + '/api/hostgroups/:id/ansible_roles/:ansible_role_id', + '/api/hostgroups/:id/ansible_roles/:ansible_role_id', '/api/hostgroups/:id/assign_ansible_roles', ), 'hosts': ( @@ -453,6 +455,8 @@ '/api/hosts/:id/play_roles', '/api/hosts/multiple_play_roles', '/api/hosts/:id/ansible_roles', + '/api/hosts/:id/ansible_roles/:ansible_role_id', + '/api/hosts/:id/ansible_roles/:ansible_role_id', '/api/hosts/:id/assign_ansible_roles', '/api/hosts/:host_id/host_collections', '/api/hosts/:id/policies_enc', @@ -634,8 +638,6 @@ ), 'oval_reports': ('/api/compliance/oval_reports/:cname/:oval_policy_id/:date',), 'package_groups': ( - '/katello/api/package_group', - '/katello/api/package_group', '/katello/api/package_groups/:id', '/katello/api/package_groups/compare', ), From 0bcbfb36112c9b01098c5edbad114b23ff8d05d5 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 23 Jun 2023 04:01:00 -0400 Subject: [PATCH 040/586] [6.14.z] Adding service status coverage to backup and restore (#11726) Adding service status coverage to backup and restore (#11716) (cherry picked from commit 82d5162dfecd6e9525e021baa0438cf482595290) Co-authored-by: Griffin Sullivan <48397354+Griffin-Sullivan@users.noreply.github.com> --- tests/foreman/maintain/test_backup_restore.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/foreman/maintain/test_backup_restore.py b/tests/foreman/maintain/test_backup_restore.py index 5d0ea7e0e5d..29a48da2361 100644 --- a/tests/foreman/maintain/test_backup_restore.py +++ b/tests/foreman/maintain/test_backup_restore.py @@ -127,6 +127,10 @@ def test_positive_backup_split_pulp_tar( 1. backup succeeds 2. expected files are present in the backup 3. size of the pulp_data.tar smaller than provided value + + :customerscenario: true + + :BZ: 2164413 """ subdir = f'{BACKUP_DIR}backup-{gen_string("alpha")}' instance = 'satellite' if type(sat_maintain) is Satellite else 'capsule' @@ -523,7 +527,9 @@ def test_positive_backup_restore( 4. system health check succeeds 5. content is present after restore - :BZ: 2172540 + :customerscenario: true + + :BZ: 2172540, 1978764, 1979045 """ subdir = f'{BACKUP_DIR}backup-{gen_string("alpha")}' instance = 'satellite' if type(sat_maintain) is Satellite else 'capsule' @@ -562,6 +568,10 @@ def test_positive_backup_restore( ) assert result.status == 0 + result = sat_maintain.cli.Service.status() + assert 'FAIL' not in result.stdout + assert result.status == 0 + # Check that content is present after restore if type(sat_maintain) is Satellite: repo = sat_maintain.api.Repository().search( From 48a2742bdac452ddf6f54ee65ee2372fdfb07459 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 23 Jun 2023 05:39:16 -0400 Subject: [PATCH 041/586] [6.14.z] add delete org before run virt-who config upgrade cases (#11738) add delete org before run virt-who config upgrade cases (#11706) * add delete org before run virt-who config upgrade cases * Use dynamic name and save_test_data (cherry picked from commit 169dbd7ee07611a5feeb855874c4bcce8a05b21d) Co-authored-by: yanpliu --- tests/upgrades/test_virtwho.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/upgrades/test_virtwho.py b/tests/upgrades/test_virtwho.py index a0d7654badc..f950d45e520 100644 --- a/tests/upgrades/test_virtwho.py +++ b/tests/upgrades/test_virtwho.py @@ -48,7 +48,7 @@ def form_data(target_sat): } -ORG_DATA = {'name': 'virtwho_upgrade_org_name'} +ORG_DATA = {'name': f'virtwho_upgrade_{gen_string("alpha")}'} class TestScenarioPositiveVirtWho: @@ -78,6 +78,9 @@ def test_pre_create_virt_who_configuration( 3. Report is sent to satellite. 4. Virtual sku can be generated and attached. """ + org = target_sat.api.Organization().search(query={'search': f'name={ORG_DATA["name"]}'}) + if org: + target_sat.api.Organization(id=org[0].id).delete() default_loc_id = ( target_sat.api.Location().search(query={'search': f'name="{DEFAULT_LOC}"'})[0].id ) @@ -127,6 +130,7 @@ def test_pre_create_virt_who_configuration( { 'hypervisor_name': hypervisor_name, 'guest_name': guest_name, + 'org_id': org.id, } ) @@ -148,10 +152,10 @@ def test_post_crud_virt_who_configuration(self, form_data, pre_upgrade_data, tar 2. the config and guest connection have the same status. 3. virt-who config should update and delete successfully. """ - org = target_sat.api.Organization().search(query={'search': f'name={ORG_DATA["name"]}'})[0] + org_id = pre_upgrade_data.get('org_id') # Post upgrade, Verify virt-who exists and has same status. - vhd = target_sat.api.VirtWhoConfig(organization_id=org.id).search( + vhd = target_sat.api.VirtWhoConfig(organization_id=org_id).search( query={'search': f'name={form_data["name"]}'} )[0] if not is_open('BZ:1802395'): @@ -166,7 +170,7 @@ def test_post_crud_virt_who_configuration(self, form_data, pre_upgrade_data, tar hosts = [hypervisor_name, guest_name] for hostname in hosts: result = ( - target_sat.api.Host(organization=org.id) + target_sat.api.Host(organization=org_id) .search(query={'search': hostname})[0] .read_json() ) @@ -183,6 +187,6 @@ def test_post_crud_virt_who_configuration(self, form_data, pre_upgrade_data, tar # Delete virt-who config vhd.delete() - assert not target_sat.api.VirtWhoConfig(organization_id=org.id).search( + assert not target_sat.api.VirtWhoConfig(organization_id=org_id).search( query={'search': f'name={modify_name}'} ) From 874e939f6535d16d364e3e271663184f3bbd6f10 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 23 Jun 2023 08:40:31 -0400 Subject: [PATCH 042/586] [6.14.z] Fix repo scraping (#11742) --- robottelo/content_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robottelo/content_info.py b/robottelo/content_info.py index df9a3806f93..1e8fd01ed64 100644 --- a/robottelo/content_info.py +++ b/robottelo/content_info.py @@ -46,7 +46,7 @@ def get_repo_files_urls_by_url(url, extension='rpm'): if result.status_code != 200: raise requests.HTTPError(f'{url} is not accessible') - links = re.findall(r'(?<=href=").*?(?=">)', result.text) + links = re.findall(r'(?<=href=")(?!\.\.).*?(?=">)', result.text) if 'Packages/' not in links: files = sorted(line for line in links if extension in line) return [f'{url}{file}' for file in files] From 1b30bccc7264b6d71fb1076145f5c2d2d0d2ecf3 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 23 Jun 2023 08:44:21 -0400 Subject: [PATCH 043/586] [6.14.z] extended cronline tests for rex (#11734) extended cronline tests for rex (#11657) (cherry picked from commit 6e1da61020b2458bbc0e756b2ed98bcda89a50d2) Co-authored-by: Peter Ondrejka --- tests/foreman/cli/test_remoteexecution.py | 86 +++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index 682fbc57274..fc030a1216c 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -16,12 +16,15 @@ :Upstream: No """ +from calendar import monthrange from datetime import datetime from datetime import timedelta from time import sleep import pytest from broker import Broker +from dateutil.relativedelta import FR +from dateutil.relativedelta import relativedelta from fauxfactory import gen_string from nailgun import entities from wait_for import wait_for @@ -353,6 +356,89 @@ def test_positive_run_recurring_job_with_max_iterations_by_ip(self, rex_contenth assert rec_logic['state'] == 'finished' assert rec_logic['iteration'] == '2' + @pytest.mark.tier3 + @pytest.mark.rhel_ver_list([8]) + def test_positive_time_expressions(self, rex_contenthost): + """Test various expressions for extended cronline syntax + + :id: 584e7b27-9484-436a-b850-11acb900a7d8 + + :expectedresults: Verify the job was scheduled to the expected + iteration + + :bz: 1967030 + + :customerscenario: true + + """ + client = rex_contenthost + today = datetime.today() + hour = datetime.utcnow().hour + last_day_of_month = monthrange(today.year, today.month)[1] + days_to = (2 - today.weekday()) % 7 + # cronline uses https://github.com/floraison/fugit + fugit_expressions = [ + ['@yearly', f'{today.year + 1}/01/01 00:00:00'], + [ + '@monthly', + f'{(today + relativedelta(months=+1)).strftime("%Y/%m")}/01 00:00:00', + ], + [ + '@weekly', + f'{(today + timedelta(days=-today.weekday() +6)).strftime("%Y/%m/%d")} 00:00:00', + ], + [ + '@midnight', + f'{(today + timedelta(days=1)).strftime("%Y/%m/%d")} 00:00:00', + ], + [ + '@hourly', + f'{(datetime.utcnow() + timedelta(hours=1)).strftime("%Y/%m/%d %H")}:00:00', + ], + [ + '0 0 * * wed-fri', + f'{(today + timedelta(days=(days_to if days_to > 0 else 1))).strftime("%Y/%m/%d")} ' + '00:00:00', + ], + # 23 mins after every other hour + [ + '23 0-23/2 * * *', + f'{today.strftime("%Y/%m/%d")} ' + f'{ (str(hour if hour % 2 == 0 else hour + 1)).rjust(2,"0") }:23:00', + ], + # last day of month + [ + '0 0 last * *', + f'{today.strftime("%Y/%m")}/{last_day_of_month} 00:00:00', + ], + # last 7 days of month + [ + '0 0 -7-L * *', + f'{today.strftime("%Y/%m")}/{last_day_of_month-6} 00:00:00', + ], + # last friday of month at 7 + [ + '0 7 * * fri#-1', + f'{(today+relativedelta(day=31, weekday=FR(-1))).strftime("%Y/%m/%d")} 07:00:00', + ], + ] + for exp in fugit_expressions: + invocation_command = make_job_invocation( + { + 'job-template': 'Run Command - Script Default', + 'inputs': 'command=ls', + 'search-query': f"name ~ {client.hostname}", + 'cron-line': exp[0], + 'max-iteration': 1, + } + ) + result = JobInvocation.info({'id': invocation_command['id']}) + assert_job_invocation_status(invocation_command['id'], client.hostname, 'queued') + rec_logic = RecurringLogic.info({'id': result['recurring-logic-id']}) + assert ( + rec_logic['next-occurrence'] == exp[1] + ), f'Job was not scheduled as expected using {exp[0]}' + @pytest.mark.tier3 @pytest.mark.rhel_ver_list([8]) def test_positive_run_scheduled_job_template_by_ip(self, rex_contenthost, target_sat): From 3c7b71079fb52c4088b63e2a789220e7e3082663 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 26 Jun 2023 02:38:54 -0400 Subject: [PATCH 044/586] [6.14.z] Bump pytest from 7.3.2 to 7.4.0 (#11755) Bump pytest from 7.3.2 to 7.4.0 (#11749) (cherry picked from commit 49a6304d721b5e8e34ae556176ee33013fb71af0) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8698888a9b3..1ab31c4e242 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ navmazing==1.1.6 productmd==1.35 pyotp==2.8.0 python-box==7.0.1 -pytest==7.3.2 +pytest==7.4.0 pytest-services==2.2.1 pytest-mock==3.11.1 pytest-reportportal==5.1.9 From 768f10234e91dac02bbb717c8f6de96f2dcddab6 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 26 Jun 2023 02:43:20 -0400 Subject: [PATCH 045/586] [6.14.z] Add test for the Capsule sync deadlock issue (#11745) Add test for the Capsule sync deadlock issue (#10424) (cherry picked from commit f65c95f4f19977093402a722cb2380bd625e1aee) Co-authored-by: vsedmik <46570670+vsedmik@users.noreply.github.com> --- conf/capsule.yaml.template | 7 +- conf/flavors.yaml.template | 9 ++ pytest_fixtures/core/contenthosts.py | 2 +- pytest_fixtures/core/sat_cap_factory.py | 16 ++++ robottelo/config/validators.py | 1 - .../destructive/test_capsulecontent.py | 93 +++++++++++++++++++ 6 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 conf/flavors.yaml.template create mode 100644 tests/foreman/destructive/test_capsulecontent.py diff --git a/conf/capsule.yaml.template b/conf/capsule.yaml.template index cde2ecf41b5..e6747faa5eb 100644 --- a/conf/capsule.yaml.template +++ b/conf/capsule.yaml.template @@ -1,7 +1,4 @@ CAPSULE: - # Capsule name to be used in the CapsuleVirtualMachine class (will be deprecated) - SATELLITE_VERSION_TAG: "@jinja {{this.robottelo.satellite_version | replace('.', '')}}" - INSTANCE_NAME: "@format qe-sat{this[capsule].satellite_version_tag}-rhel7-tierX-capsule" VERSION: # The full release version (6.9.2) RELEASE: # populate with capsule version @@ -13,6 +10,6 @@ CAPSULE: # The base os rhel version where the capsule installed # RHEL_VERSION: # The Ansible Tower workflow used to deploy a capsule - DEPLOY_WORKFLOW: deploy-sat-capsule + DEPLOY_WORKFLOW: deploy-capsule # Dictionary of arguments which should be passed along to the deploy workflow - # DEPLOY_ARGUMENTS: + DEPLOY_ARGUMENTS: diff --git a/conf/flavors.yaml.template b/conf/flavors.yaml.template new file mode 100644 index 00000000000..73695663cc8 --- /dev/null +++ b/conf/flavors.yaml.template @@ -0,0 +1,9 @@ +FLAVORS: + # 6 CPU, 24 GB RAM, 100 GB disk + DEFAULT: satqe-ssd.standard.std + # 16 CPU, 32 GB RAM, 160 GB disk + LARGE: satqe-ssd.standard.xxxl + # 6 CPU, 24 GB RAM, 500 GB disk + UPGRADE: satqe-ssd.upgrade.std + # 8 CPU, 32 GB RAM, 500 GB disk + CUSTOM_DB: satqe-ssd.customerdb.std diff --git a/pytest_fixtures/core/contenthosts.py b/pytest_fixtures/core/contenthosts.py index 435a1446d89..2379ca108b2 100644 --- a/pytest_fixtures/core/contenthosts.py +++ b/pytest_fixtures/core/contenthosts.py @@ -224,7 +224,7 @@ def oracle_host(request, version): def sat_ready_rhel(request): deploy_args = { 'deploy_rhel_version': request.param, - 'deploy_flavor': 'satqe-ssd.standard.std', + 'deploy_flavor': settings.flavors.default, 'promtail_config_template_file': 'config_sat.j2', 'workflow': 'deploy-rhel', } diff --git a/pytest_fixtures/core/sat_cap_factory.py b/pytest_fixtures/core/sat_cap_factory.py index d8a6891d06b..d4808eeeaa5 100644 --- a/pytest_fixtures/core/sat_cap_factory.py +++ b/pytest_fixtures/core/sat_cap_factory.py @@ -44,6 +44,15 @@ def capsule_host(capsule_factory): Broker(hosts=[new_cap]).checkin() +@pytest.fixture +def large_capsule_host(capsule_factory): + """A fixture that provides a Capsule based on config settings""" + new_cap = capsule_factory(deploy_flavor=settings.flavors.custom_db) + yield new_cap + new_cap.teardown() + Broker(hosts=[new_cap]).checkin() + + @pytest.fixture(scope='module') def module_capsule_host(capsule_factory): """A fixture that provides a Capsule based on config settings""" @@ -69,6 +78,13 @@ def capsule_configured(capsule_host, target_sat): yield capsule_host +@pytest.fixture +def large_capsule_configured(large_capsule_host, target_sat): + """Configure the capsule instance with the satellite from settings.server.hostname""" + large_capsule_host.capsule_setup(sat_host=target_sat) + yield large_capsule_host + + @pytest.fixture(scope='module') def module_capsule_configured(module_capsule_host, module_target_sat): """Configure the capsule instance with the satellite from settings.server.hostname""" diff --git a/robottelo/config/validators.py b/robottelo/config/validators.py index 7a8760608ad..dcd242f2b5b 100644 --- a/robottelo/config/validators.py +++ b/robottelo/config/validators.py @@ -66,7 +66,6 @@ Validator('bugzilla.api_key', must_exist=True), ], capsule=[ - Validator('capsule.instance_name', must_exist=True), Validator('capsule.version.release', must_exist=True), Validator('capsule.version.source', must_exist=True), Validator('capsule.deploy_workflow', must_exist=True), diff --git a/tests/foreman/destructive/test_capsulecontent.py b/tests/foreman/destructive/test_capsulecontent.py new file mode 100644 index 00000000000..6efd1c641b6 --- /dev/null +++ b/tests/foreman/destructive/test_capsulecontent.py @@ -0,0 +1,93 @@ +"""Capsule-Content related tests, which require destructive Satellite + +:Requirement: Capsule-Content + +:CaseAutomation: Automated + +:CaseLevel: System + +:CaseComponent: Capsule-Content + +:team: Phoenix-content + +:TestType: Functional + +:CaseImportance: High + +:Upstream: No +""" +import pytest +from fauxfactory import gen_alpha + +from robottelo import constants + +pytestmark = [pytest.mark.destructive] + + +@pytest.mark.tier4 +@pytest.mark.skip_if_not_set('capsule') +def test_positive_sync_without_deadlock( + target_sat, large_capsule_configured, function_entitlement_manifest_org +): + """Synchronize one bigger repo published in multiple CVs to a blank Capsule. + Assert that the sync task succeeds and no deadlock happens. + + :id: 91c6eec9-a582-46ea-9898-bdcaebcea2f0 + + :setup: + 1. A blank external capsule that has not been synced yet (!) with immediate download + policy and running multiple (4 and more) pulpcore workers. + + :steps: + 1. Sync one bigger repository to the Satellite. + 2. Create a Content View, add the repository and publish it. + 3. Create several copies of the CV and publish them. + 4. Add the Library LCE to the Capsule. + 5. Sync the Capsule. + + :expectedresults: + 1. Sync passes without deadlock. + + :customerscenario: true + + :BZ: 2062526 + + """ + # Note: As of now BZ#2122872 prevents us to use the originally intended RHEL7 repo because + # of a memory leak causing Satellite OOM crash in this scenario. Therefore, for now we use + # smaller RHSCL repo instead, which was also capable to hit the deadlock issue, regardless + # the lower rpms count. When the BZ is fixed, reconsider upscale to RHEL7 repo or similar. + repo_id = target_sat.api_factory.enable_rhrepo_and_fetchid( + basearch='x86_64', + org_id=function_entitlement_manifest_org.id, + product=constants.REPOS['rhscl7']['product'], + repo=constants.REPOS['rhscl7']['name'], + reposet=constants.REPOSET['rhscl7'], + releasever=constants.REPOS['rhscl7']['releasever'], + ) + repo = target_sat.api.Repository(id=repo_id).read() + repo.sync(timeout='60m') + + cv = target_sat.publish_content_view(function_entitlement_manifest_org, repo) + + for i in range(4): + copy_id = target_sat.api.ContentView(id=cv.id).copy(data={'name': gen_alpha()})['id'] + copy_cv = target_sat.api.ContentView(id=copy_id).read() + copy_cv.publish() + + proxy = large_capsule_configured.nailgun_smart_proxy.read() + proxy.download_policy = 'immediate' + proxy.update(['download_policy']) + + lce = target_sat.api.LifecycleEnvironment( + organization=function_entitlement_manifest_org + ).search(query={'search': f'name={constants.ENVIRONMENT}'})[0] + large_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( + data={'environment_id': lce.id} + ) + result = large_capsule_configured.nailgun_capsule.content_lifecycle_environments() + assert len(result['results']) == 1 + assert result['results'][0]['id'] == lce.id + + sync_status = large_capsule_configured.nailgun_capsule.content_sync(timeout='90m') + assert sync_status['result'] == 'success', 'Capsule sync task failed.' From d2c460c1feaa2e608cd119b7d40e3267de3b9c96 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 26 Jun 2023 09:10:14 -0400 Subject: [PATCH 046/586] [6.14.z] Pulp test fix (#11763) --- tests/foreman/api/test_repositories.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/foreman/api/test_repositories.py b/tests/foreman/api/test_repositories.py index 844b53928fa..12b43cb880b 100644 --- a/tests/foreman/api/test_repositories.py +++ b/tests/foreman/api/test_repositories.py @@ -144,7 +144,7 @@ def test_positive_epel_repositories_with_mirroring_policy( @pytest.mark.tier4 -def test_positive_sync_kickstart_repo(self, module_entitlement_manifest_org, target_sat): +def test_positive_sync_kickstart_repo(module_entitlement_manifest_org, target_sat): """No encoding gzip errors on kickstart repositories sync. @@ -175,7 +175,7 @@ def test_positive_sync_kickstart_repo(self, module_entitlement_manifest_org, tar basearch='x86_64', org_id=module_entitlement_manifest_org.id, product=constants.REPOS['kickstart'][distro]['product'], - reposet=constants.REPOSET['kickstart'][distro], + reposet=constants.REPOS['kickstart'][distro]['reposet'], repo=constants.REPOS['kickstart'][distro]['name'], releasever=constants.REPOS['kickstart'][distro]['version'], ) From 84c85e3ffe147a1dd06fe213ebd37d03fc49d0d6 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 26 Jun 2023 11:57:32 -0400 Subject: [PATCH 047/586] [6.14.z] Applied errata template test and custom product fix (#11768) --- robottelo/cli/factory.py | 5 ++ robottelo/host_helpers/cli_factory.py | 6 +++ tests/foreman/api/test_reporttemplates.py | 63 +++++++++++++++++++++-- 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/robottelo/cli/factory.py b/robottelo/cli/factory.py index fb111fdbd6e..3eeca63b3a2 100644 --- a/robottelo/cli/factory.py +++ b/robottelo/cli/factory.py @@ -1730,6 +1730,11 @@ def setup_org_for_a_custom_repo(options=None): 'subscription': custom_product['name'], } ) + # Override custom product to true ( turned off by default in 6.14 ) + custom_repo = Repository.info({'id': custom_repo['id']}) + ActivationKey.content_override( + {'id': activationkey_id, 'content-label': custom_repo['content-label'], 'value': 'true'} + ) return { 'activationkey-id': activationkey_id, 'content-view-id': cv_id, diff --git a/robottelo/host_helpers/cli_factory.py b/robottelo/host_helpers/cli_factory.py index 4a8e947d759..beae91759c4 100644 --- a/robottelo/host_helpers/cli_factory.py +++ b/robottelo/host_helpers/cli_factory.py @@ -585,6 +585,7 @@ def setup_org_for_a_custom_repo(self, options=None): 4. Checks if activation key was given, otherwise creates a new one and associates it with the content view. 5. Adds the custom repo subscription to the activation key + 6. Override custom product to true ( turned off by default in 6.14 ) :return: A dictionary with the entity ids of Activation key, Content view, Lifecycle Environment, Organization, Product and Repository @@ -663,6 +664,11 @@ def setup_org_for_a_custom_repo(self, options=None): 'subscription': custom_product['name'], } ) + # Override custom product to true ( turned off by default in 6.14 ) + custom_repo = self._satellite.cli.Repository.info({'id': custom_repo['id']}) + self._satellite.cli.ActivationKey.content_override( + {'id': activationkey_id, 'content-label': custom_repo['content-label'], 'value': 'true'} + ) return { 'activationkey-id': activationkey_id, 'content-view-id': cv_id, diff --git a/tests/foreman/api/test_reporttemplates.py b/tests/foreman/api/test_reporttemplates.py index 21f08882740..173e32fbd2c 100644 --- a/tests/foreman/api/test_reporttemplates.py +++ b/tests/foreman/api/test_reporttemplates.py @@ -23,7 +23,9 @@ from requests import HTTPError from wait_for import wait_for +from robottelo.config import settings from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME +from robottelo.constants import FAKE_1_CUSTOM_PACKAGE from robottelo.constants import PRDS from robottelo.constants import REPOS from robottelo.constants import REPOSET @@ -369,22 +371,75 @@ def test_negative_create_report_without_name(): @pytest.mark.tier2 -@pytest.mark.stubbed -def test_positive_applied_errata(): +@pytest.mark.rhel_ver_match(r'^(?!6$)\d+$') +@pytest.mark.no_containers +def test_positive_applied_errata( + module_org, module_location, module_cv, module_lce, rhel_contenthost, target_sat +): """Generate an Applied Errata report :id: a4b577db-141e-4871-a42e-e93887464986 - :setup: User with reporting access rights, some host with applied errata + :setup: A Host with some applied errata. :steps: - 1. POST /api/report_templates/:id/generate + 1. Generate an Applied Errata report :expectedresults: A report is generated with all applied errata listed :CaseImportance: Medium """ + activation_key = target_sat.api.ActivationKey( + environment=module_lce, organization=module_org + ).create() + ERRATUM_ID = str(settings.repos.yum_6.errata[2]) + target_sat.cli_factory.setup_org_for_a_custom_repo( + { + 'url': settings.repos.yum_9.url, + 'organization-id': module_org.id, + 'content-view-id': module_cv.id, + 'lifecycle-environment-id': module_lce.id, + 'activationkey-id': activation_key.id, + } + ) + result = rhel_contenthost.register(module_org, module_location, activation_key.name, target_sat) + assert f'The registered system name is: {rhel_contenthost.hostname}' in result.stdout + assert rhel_contenthost.subscribed + assert rhel_contenthost.execute(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}').status == 0 + assert rhel_contenthost.execute(f'rpm -q {FAKE_1_CUSTOM_PACKAGE}').status == 0 + task_id = target_sat.api.JobInvocation().run( + data={ + 'feature': 'katello_errata_install', + 'inputs': {'errata': ERRATUM_ID}, + 'targeting_type': 'static_query', + 'search_query': f'name = {rhel_contenthost.hostname}', + 'organization_id': module_org.id, + }, + )['id'] + target_sat.wait_for_tasks( + search_query=(f'label = Actions::RemoteExecution::RunHostsJob and id = {task_id}'), + search_rate=15, + max_tries=10, + ) + rt = ( + target_sat.api.ReportTemplate() + .search(query={'search': 'name="Host - Applied Errata"'})[0] + .read() + ) + res = rt.generate( + data={ + 'organization_id': module_org.id, + 'report_format': 'json', + 'input_values': { + 'Filter Errata Type': 'all', + 'Include Last Reboot': 'no', + 'Status': 'all', + }, + } + ) + assert res[0]['erratum_id'] == ERRATUM_ID + assert res[0]['issued'] @pytest.mark.tier2 From 258c4290547dc28bbbb253a8d6ba8326f629379a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 27 Jun 2023 05:53:32 -0400 Subject: [PATCH 048/586] [6.14.z] Enable client_repo in AK for provisioning (#11770) Enable client_repo in AK for provisioning (#11735) * Enable client_repo in AK for provisioning Signed-off-by: Gaurav Talreja * Update pytest_fixtures/component/provision_pxe.py Co-authored-by: Adarsh dubey --------- Signed-off-by: Gaurav Talreja Co-authored-by: Adarsh dubey (cherry picked from commit dce81057fdfb8f4d2565deb3829943dbde601e57) Co-authored-by: Gaurav Talreja --- pytest_fixtures/component/provision_pxe.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pytest_fixtures/component/provision_pxe.py b/pytest_fixtures/component/provision_pxe.py index 5913000f051..32175b4c8d4 100644 --- a/pytest_fixtures/component/provision_pxe.py +++ b/pytest_fixtures/component/provision_pxe.py @@ -124,6 +124,12 @@ def module_provisioning_rhel_content( 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) From c479de54fd5a0255cac1a668d82b1253774c1e6f Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 27 Jun 2023 06:11:15 -0400 Subject: [PATCH 049/586] [6.14.z] Fix container repo tests (#11777) --- tests/foreman/api/test_repository.py | 2 +- tests/foreman/ui/test_containerimagetag.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index db450b4b231..bf52acb803c 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -2552,7 +2552,7 @@ def clean_repo(): repo = repo.read() for field in 'name', 'docker_upstream_name', 'content_type', 'upstream_username': assert getattr(repo, field) == repo_options[field] - repo.sync(timeout=600) + repo.sync(timeout=900) assert repo.read().content_counts['docker_manifest'] > 1 try: diff --git a/tests/foreman/ui/test_containerimagetag.py b/tests/foreman/ui/test_containerimagetag.py index 8c5517c8935..dc3dba99bcd 100644 --- a/tests/foreman/ui/test_containerimagetag.py +++ b/tests/foreman/ui/test_containerimagetag.py @@ -43,7 +43,7 @@ def module_repository(module_product): product=module_product, url=CONTAINER_REGISTRY_HUB, ).create() - repo.sync() + repo.sync(timeout=1440) return repo From fb26a2069d2d147bd9640c89223db961522857da Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 27 Jun 2023 10:59:19 -0400 Subject: [PATCH 050/586] [6.14.z] e2e tests updated a bit (#11780) --- tests/foreman/endtoend/test_api_endtoend.py | 45 ++++++++++------- tests/foreman/endtoend/test_cli_endtoend.py | 55 ++++++++++++--------- 2 files changed, 57 insertions(+), 43 deletions(-) diff --git a/tests/foreman/endtoend/test_api_endtoend.py b/tests/foreman/endtoend/test_api_endtoend.py index 5500e556ef6..83816d2e61a 100644 --- a/tests/foreman/endtoend/test_api_endtoend.py +++ b/tests/foreman/endtoend/test_api_endtoend.py @@ -1055,14 +1055,14 @@ def test_positive_find_admin_user(self): @pytest.mark.skip_if_not_set('libvirt') @pytest.mark.tier4 + @pytest.mark.no_containers + @pytest.mark.rhel_ver_match('7') @pytest.mark.e2e @pytest.mark.upgrade @pytest.mark.skipif( (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) - def test_positive_end_to_end( - self, function_entitlement_manifest, target_sat, rhel7_contenthost - ): + def test_positive_end_to_end(self, function_entitlement_manifest, target_sat, rhel_contenthost): """Perform end to end smoke tests using RH and custom repos. 1. Create a new user with admin permissions @@ -1091,6 +1091,8 @@ def test_positive_end_to_end( :expectedresults: All tests should succeed and Content should be successfully fetched by client. + :bz: 2216461 + :parametrized: yes """ # step 1: Create a new user with admin permissions @@ -1178,17 +1180,22 @@ def test_positive_end_to_end( # BONUS: Create a content host and associate it with promoted # content view and last lifecycle where it exists - content_host = target_sat.api.Host( - content_facet_attributes={ - 'content_view_id': content_view.id, - 'lifecycle_environment_id': le1.id, - }, - organization=org, - ).create() - # check that content view matches what we passed - assert content_host.content_facet_attributes['content_view_id'] == content_view.id - # check that lifecycle environment matches - assert content_host.content_facet_attributes['lifecycle_environment_id'] == le1.id + if not is_open('BZ:2216461'): + content_host = target_sat.api.Host( + content_facet_attributes={ + 'content_view_id': content_view.id, + 'lifecycle_environment_id': le1.id, + }, + organization=org, + ).create() + # check that content view matches what we passed + assert ( + content_host.content_facet_attributes['content_views'][0]['id'] == content_view.id + ) + # check that lifecycle environment matches + assert ( + content_host.content_facet_attributes['lifecycle_environments'][0]['id'] == le1.id + ) # step 2.14: Create a new libvirt compute resource target_sat.api.LibvirtComputeResource( @@ -1208,14 +1215,14 @@ def test_positive_end_to_end( # step 2.18: Provision a client # TODO this isn't provisioning through satellite as intended # Note it wasn't well before the change that added this todo - rhel7_contenthost.install_katello_ca(target_sat) + rhel_contenthost.install_katello_ca(target_sat) # Register client with foreman server using act keys - rhel7_contenthost.register_contenthost(org.label, activation_key_name) - assert rhel7_contenthost.subscribed + rhel_contenthost.register_contenthost(org.label, activation_key_name) + assert rhel_contenthost.subscribed # Install rpm on client package_name = 'katello-agent' - result = rhel7_contenthost.execute(f'yum install -y {package_name}') + result = rhel_contenthost.execute(f'yum install -y {package_name}') assert result.status == 0 # Verify that the package is installed by querying it - result = rhel7_contenthost.run(f'rpm -q {package_name}') + result = rhel_contenthost.run(f'rpm -q {package_name}') assert result.status == 0 diff --git a/tests/foreman/endtoend/test_cli_endtoend.py b/tests/foreman/endtoend/test_cli_endtoend.py index d65fcd883f4..98aeaafab64 100644 --- a/tests/foreman/endtoend/test_cli_endtoend.py +++ b/tests/foreman/endtoend/test_cli_endtoend.py @@ -40,6 +40,7 @@ from robottelo.config import setting_is_set from robottelo.config import settings from robottelo.constants.repos import CUSTOM_RPM_REPO +from robottelo.utils.issue_handlers import is_open @pytest.fixture(scope='module') @@ -87,12 +88,13 @@ def test_positive_cli_find_admin_user(): assert result['admin'] == 'yes' -@pytest.mark.skip_if_not_set('libvirt') +@pytest.mark.no_containers +@pytest.mark.rhel_ver_match('7') @pytest.mark.tier4 @pytest.mark.e2e @pytest.mark.upgrade @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel7_contenthost): +def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel_contenthost): """Perform end to end smoke tests using RH and custom repos. 1. Create a new user with admin permissions @@ -121,6 +123,8 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel :expectedresults: All tests should succeed and Content should be successfully fetched by client. + :bz: 2216461 + :parametrized: yes """ # step 1: Create a new user with admin permissions @@ -265,25 +269,28 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel # BONUS: Create a content host and associate it with promoted # content view and last lifecycle where it exists - content_host_name = gen_alphanumeric() - content_host = Host.with_user(user['login'], user['password']).subscription_register( - { - 'content-view-id': content_view['id'], - 'lifecycle-environment-id': lifecycle_environment['id'], - 'name': content_host_name, - 'organization-id': org['id'], - } - ) + if not is_open('BZ:2216461'): + content_host_name = gen_alphanumeric() + content_host = Host.with_user(user['login'], user['password']).subscription_register( + { + 'content-view-id': content_view['id'], + 'lifecycle-environment-id': lifecycle_environment['id'], + 'name': content_host_name, + 'organization-id': org['id'], + } + ) - content_host = Host.with_user(user['login'], user['password']).info({'id': content_host['id']}) - # check that content view matches what we passed - assert content_host['content-information']['content-view']['name'] == content_view['name'] + content_host = Host.with_user(user['login'], user['password']).info( + {'id': content_host['id']} + ) + # check that content view matches what we passed + assert content_host['content-information']['content-view']['name'] == content_view['name'] - # check that lifecycle environment matches - assert ( - content_host['content-information']['lifecycle-environment']['name'] - == lifecycle_environment['name'] - ) + # check that lifecycle environment matches + assert ( + content_host['content-information']['lifecycle-environment']['name'] + == lifecycle_environment['name'] + ) # step 2.14: Create a new libvirt compute resource _create( @@ -328,16 +335,16 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel # step 2.18: Provision a client # TODO this isn't provisioning through satellite as intended # Note it wasn't well before the change that added this todo - rhel7_contenthost.install_katello_ca(target_sat) + rhel_contenthost.install_katello_ca(target_sat) # Register client with foreman server using act keys - rhel7_contenthost.register_contenthost(org['label'], activation_key['name']) - assert rhel7_contenthost.subscribed + rhel_contenthost.register_contenthost(org['label'], activation_key['name']) + assert rhel_contenthost.subscribed # Install rpm on client package_name = 'katello-agent' - result = rhel7_contenthost.execute(f'yum install -y {package_name}') + result = rhel_contenthost.execute(f'yum install -y {package_name}') assert result.status == 0 # Verify that the package is installed by querying it - result = rhel7_contenthost.run(f'rpm -q {package_name}') + result = rhel_contenthost.run(f'rpm -q {package_name}') assert result.status == 0 From ac6c7d74d561a4db2f0e3465157f9b7055ca3351 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 27 Jun 2023 12:39:35 -0400 Subject: [PATCH 051/586] [6.14.z] check recurring logic cli keys (#11782) --- tests/foreman/cli/test_remoteexecution.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index fc030a1216c..5119b9fa013 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -631,6 +631,10 @@ def test_positive_run_reccuring_job(self, rex_contenthost): :CaseAutomation: Automated + :customerscenario: true + + :bz: 2129432 + :CaseLevel: System :parametrized: yes @@ -650,6 +654,15 @@ def test_positive_run_reccuring_job(self, rex_contenthost): rec_logic = RecurringLogic.info({'id': result['recurring-logic-id']}) assert rec_logic['state'] == 'finished' assert rec_logic['iteration'] == '2' + # 2129432 + rec_logic_keys = rec_logic.keys() + assert 'action' in rec_logic_keys + assert 'last-occurrence' in rec_logic_keys + assert 'next-occurrence' in rec_logic_keys + assert 'state' in rec_logic_keys + assert 'purpose' in rec_logic_keys + assert 'iteration' in rec_logic_keys + assert 'iteration-limit' in rec_logic_keys @pytest.mark.tier3 @pytest.mark.no_containers From 072cd0d5e0a3eb175b66c8b1b5274f1d5dc4f591 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 28 Jun 2023 08:58:58 -0400 Subject: [PATCH 052/586] [6.14.z] Remove redundant rhev provisioning test (#11775) Remove redundant rhev provisioning test (#11737) * Remove redundant rhev provisioning test Signed-off-by: Gaurav Talreja * Cleanup host in rhevm when deleted from satellite Signed-off-by: Gaurav Talreja * Fix settings_update indirect parametrization to accept values Signed-off-by: Gaurav Talreja (cherry picked from commit 8697d09cd799f9b0bae10d1ec9e8e2cfff764dec) Signed-off-by: Gaurav Talreja --------- Signed-off-by: Gaurav Talreja (cherry picked from commit 15a8795c607ea1357c3a7e9e8ec7bed2a7cc3714) Co-authored-by: Gaurav Talreja --- pytest_fixtures/component/settings.py | 14 ++-- .../foreman/cli/test_computeresource_rhev.py | 5 +- .../test_provisioning_computeresource.py | 84 ------------------- tests/foreman/ui/test_host.py | 13 +-- 4 files changed, 12 insertions(+), 104 deletions(-) diff --git a/pytest_fixtures/component/settings.py b/pytest_fixtures/component/settings.py index 8f5b340bbc7..0a7acbb075a 100644 --- a/pytest_fixtures/component/settings.py +++ b/pytest_fixtures/component/settings.py @@ -1,19 +1,21 @@ # Settings Fixtures import pytest -from nailgun import entities -@pytest.fixture(scope="function") -def setting_update(request): +@pytest.fixture() +def setting_update(request, target_sat): """ This fixture is used to create an object of the provided settings parameter that we use in each test case to update their attributes and once the test case gets completed it helps to restore their default value """ - setting_object = entities.Setting().search(query={'search': f'name={request.param}'})[0] + key_val = request.param + setting, new_value = tuple(key_val.split('=')) if '=' in key_val else (key_val, None) + setting_object = target_sat.api.Setting().search(query={'search': f'name={setting}'})[0] default_setting_value = setting_object.value - if default_setting_value is None: - default_setting_value = '' + if new_value is not None: + setting_object.value = new_value + setting_object.update({'value'}) yield setting_object setting_object.value = default_setting_value setting_object.update({'value'}) diff --git a/tests/foreman/cli/test_computeresource_rhev.py b/tests/foreman/cli/test_computeresource_rhev.py index f3dcac7ee01..35c60e0860e 100644 --- a/tests/foreman/cli/test_computeresource_rhev.py +++ b/tests/foreman/cli/test_computeresource_rhev.py @@ -380,8 +380,10 @@ def test_negative_add_image_rhev_with_invalid_name(rhev, module_os): @pytest.mark.on_premises_provisioning @pytest.mark.tier3 @pytest.mark.rhel_ver_match('[^6]') +@pytest.mark.parametrize('setting_update', ['destroy_vm_on_host_delete=True'], indirect=True) def test_positive_provision_rhev_with_host_group( request, + setting_update, module_provisioning_sat, rhev, module_sca_manifest_org, @@ -546,7 +548,7 @@ def test_positive_provision_rhev_without_host_group(rhev): @pytest.mark.on_premises_provisioning @pytest.mark.tier3 @pytest.mark.rhel_ver_match('[^6]') -@pytest.mark.parametrize('setting_update', ['destroy_vm_on_host_delete'], indirect=True) +@pytest.mark.parametrize('setting_update', ['destroy_vm_on_host_delete=True'], indirect=True) def test_positive_provision_rhev_image_based_and_disassociate( request, module_provisioning_sat, @@ -674,7 +676,6 @@ def test_positive_provision_rhev_image_based_and_disassociate( assert 'compute-resource' not in host_info finally: - # Now, let's just remove the host if host is not None: cli.Host.delete({'id': host['id']}) diff --git a/tests/foreman/longrun/test_provisioning_computeresource.py b/tests/foreman/longrun/test_provisioning_computeresource.py index f6dade88d5a..3825a2c6e93 100644 --- a/tests/foreman/longrun/test_provisioning_computeresource.py +++ b/tests/foreman/longrun/test_provisioning_computeresource.py @@ -15,7 +15,6 @@ from fauxfactory import gen_string from wrapanapi import VMWareSystem -from robottelo.cli.computeresource import ComputeResource from robottelo.cli.factory import make_compute_resource from robottelo.cli.factory import make_host from robottelo.cli.host import Host @@ -88,89 +87,6 @@ def tear_down(provisioning): Host.delete({'id': host['id']}) -@pytest.mark.on_premises_provisioning -@pytest.mark.vlan_networking -@pytest.mark.tier3 -def test_positive_provision_rhev_with_host_group(rhev, provisioning, target_sat, tear_down): - """Provision a host on RHEV compute resource with - the help of hostgroup. - - :Requirement: Computeresource RHV - - :CaseComponent: ComputeResources-RHEV - - :Team: Rocket - - :id: ba78868f-5cff-462f-a55d-f6aa4d11db52 - - :setup: Hostgroup and provisioning setup like domain, subnet etc. - - :steps: - - 1. Create a RHEV compute resource. - 2. Create a host on RHEV compute resource using the Hostgroup - 3. Use compute-attributes parameter to specify key-value parameters - regarding the virtual machine. - 4. Provision the host. - - :expectedresults: The host should be provisioned with host group - - :BZ: 1777992 - - :customerscenario: true - - :CaseAutomation: Automated - """ - name = gen_string('alpha') - rhv_cr = ComputeResource.create( - { - 'name': name, - 'provider': 'Ovirt', - 'user': rhev.rhev_username, - 'password': rhev.rhev_password, - 'datacenter': rhev.rhev_datacenter, - 'url': rhev.rhev_url, - 'ovirt-quota': rhev.quota, - 'organizations': provisioning.org_name, - 'locations': provisioning.loc_name, - } - ) - assert rhv_cr['name'] == name - host_name = gen_string('alpha').lower() - host = make_host( - { - 'name': f'{host_name}', - 'root-password': gen_string('alpha'), - 'organization': provisioning.org_name, - 'location': provisioning.loc_name, - 'pxe-loader': 'PXELinux BIOS', - 'hostgroup': provisioning.config_env['host_group'], - 'compute-resource-id': rhv_cr.get('id'), - 'compute-attributes': "cluster={}," - "cores=1," - "memory=1073741824," - "start=1".format(rhev.cluster_id), - 'ip': None, - 'mac': None, - 'interface': f"compute_name=nic1, compute_network={rhev.network_id}", - 'volume': "size_gb=10," "storage_domain={}," "bootable=True".format(rhev.storage_id), - 'provision-method': 'build', - } - ) - hostname = '{}.{}'.format(host_name, provisioning.config_env['domain']) - assert hostname == host['name'] - host_info = Host.info({'name': hostname}) - host_ip = host_info.get('network').get('ipv4-address') - # Check on RHV, if VM exists - assert rhev.rhv_api.does_vm_exist(hostname) - # Get the information of created VM - rhv_vm = rhev.rhv_api.get_vm(hostname) - # Assert of Satellite mac address for VM and Mac of VM created is same - assert host_info.get('network').get('mac') == rhv_vm.get_nics()[0].mac.address - # Start to run a ping check if network was established on VM - target_sat.ping_host(host=host_ip) - - @pytest.mark.on_premises_provisioning @pytest.mark.vlan_networking @pytest.mark.tier3 diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index 2c715d490a0..7ef4330fd90 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -249,16 +249,6 @@ def module_activation_key(module_entitlement_manifest_org, module_target_sat): return activation_key -@pytest.fixture(scope='function') -def remove_vm_on_delete(target_sat, setting_update): - setting_update.value = 'true' - setting_update.update({'value'}) - assert ( - target_sat.api.Setting().search(query={'search': 'name=destroy_vm_on_host_delete'})[0].value - ) - yield - - @pytest.fixture def tracer_install_host(rex_contenthost, target_sat): """Sets up a contenthost with katello-host-tools-tracer enabled, @@ -1848,7 +1838,7 @@ def test_positive_provision_end_to_end( @pytest.mark.on_premises_provisioning @pytest.mark.run_in_one_thread @pytest.mark.tier4 -@pytest.mark.parametrize('setting_update', ['destroy_vm_on_host_delete'], indirect=True) +@pytest.mark.parametrize('setting_update', ['destroy_vm_on_host_delete=True'], indirect=True) def test_positive_delete_libvirt( session, module_org, @@ -1857,7 +1847,6 @@ def test_positive_delete_libvirt( module_libvirt_hostgroup, module_libvirt_resource, setting_update, - remove_vm_on_delete, target_sat, ): """Create a new Host on libvirt compute resource and delete it From 1b256400eeba34c939774510338ed35cf7ecb1ec Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 28 Jun 2023 11:22:24 -0400 Subject: [PATCH 053/586] [6.14.z] Content View Audit Tests (#11718) Content View Audit Tests (#11433) * First test for content-view-audit * First test for content-view-audit * add more test cases and a test for composite content views * Add test for publish_only_if_needed flag * Remove unnecessary file. * Finish test for composite content views * address review comments * Remove stream markers, change entity to target_sat (cherry picked from commit e7b264dc6a6c9643cfc807d3ca2f592f0cc8c563) Co-authored-by: Samuel Bible --- tests/foreman/api/test_contentview.py | 185 ++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) diff --git a/tests/foreman/api/test_contentview.py b/tests/foreman/api/test_contentview.py index 88e9940cb0b..52c1fd2cb1e 100644 --- a/tests/foreman/api/test_contentview.py +++ b/tests/foreman/api/test_contentview.py @@ -29,6 +29,7 @@ from robottelo.config import user_nailgun_config from robottelo.constants import CONTAINER_REGISTRY_HUB from robottelo.constants import CUSTOM_RPM_SHA_512_FEED_COUNT +from robottelo.constants import DataFile from robottelo.constants import FILTER_ERRATA_TYPE from robottelo.constants import PERMISSIONS from robottelo.constants import PRDS @@ -750,6 +751,103 @@ def test_composite_content_view_with_same_repos(self, module_org, target_sat): comp_content_view_info = comp_content_view.version[0].read() assert comp_content_view_info.package_count == 36 + @pytest.mark.tier2 + def test_ccv_audit_scenarios(self, module_org, target_sat): + """Check for various scenarios where a composite content view or it's component + content views needs_publish flags should be set to true and that they properly + get set and unset + + :id: cdd94ab8-da31-40ac-ab81-02472517e9bf + + :steps: + + 1. Create a ccv + 2. Add some content views to the composite view + 3. Remove a content view from the composite view + 4. Add a CV that has `latest` set to true to the composite view + 5. Publish a new version of that CV + 6. Publish a new version of another CV in the CCV, and update it to latest + + :expectedresults: When appropriate, a ccv and it's cvs needs_publish flags get + set or unset + + :CaseLevel: Integration + + :CaseImportance: High + """ + composite_cv = target_sat.api.ContentView(composite=True).create() + # Needs_publish is set to True on creation + assert composite_cv.read().needs_publish + composite_cv.publish() + assert not composite_cv.read().needs_publish + # Add some content_views to the composite view + self.add_content_views_to_composite(composite_cv, module_org, 2) + # Needs_publish should be set to True when a component CV is added/removed + assert composite_cv.read().needs_publish + composite_cv.publish() + assert not composite_cv.read().needs_publish + component_cvs = composite_cv.read().component + # Remove a component cv, should need publish now + component_cvs.pop(1) + composite_cv.component = component_cvs + composite_cv = composite_cv.update(['component']) + assert composite_cv.read().needs_publish + composite_cv.publish() + assert not composite_cv.read().needs_publish + # add a CV that has `latest` set to true + new_cv = target_sat.api.ContentView().create() + new_cv.publish() + composite_cv.content_view_component[0].add( + data={"components": [{"content_view_id": new_cv.id, "latest": "true"}]} + ) + assert composite_cv.read().needs_publish + composite_cv.publish() + assert not composite_cv.read().needs_publish + # a new version of a component cv that has "latest" - needs publish + new_cv.publish() + assert composite_cv.read().needs_publish + composite_cv.publish() + assert not composite_cv.read().needs_publish + # a component CV was changed to "always update" when ccv has old version - needs publish + # update composite_cv after changes + composite_cv = composite_cv.read() + # get the first CV that was added, which has 1 version + old_component = composite_cv.content_view_component[0].read() + old_component.content_view.publish() + old_component.latest = True + old_component = old_component.update(['latest']) + # set latest to true and see if CV needs publish + assert composite_cv.read().needs_publish + composite_cv.publish() + assert not composite_cv.read().needs_publish + + @pytest.mark.tier2 + def test_check_needs_publish_flag(self, target_sat): + """Check that the publish_only_if_needed option in the API works as intended (defaults to + false, is able to be overriden to true, and if so gives the appropriate message + if the cvs needs_publish flag is set to false) + + :id: 6e4aa845-db08-4cc3-a960-ea64fb20f50c + + :expectedresults: The publish_only_if_needed flag is working as intended, and is defaulted + to false + + :CaseLevel: Integration + + :CaseImportance: High + """ + cv = target_sat.api.ContentView().create() + assert cv.publish() + assert not cv.read().needs_publish + with pytest.raises(HTTPError): + assert ( + cv.publish(data={'publish_only_if_needed': True})['displayMessage'] + == """ + Content view does not need a publish since there are no audited changes since the + last publish. Pass check_needs_publish parameter as false if you don't want to check + if content view needs a publish.""" + ) + class TestContentViewUpdate: """Tests for updating content views.""" @@ -1015,6 +1113,93 @@ def test_positive_promote_rh_custom_spin(self, content_view, module_lce): content_view.read().version[0].promote(data={'environment_ids': module_lce.id}) assert len(content_view.read().version[0].read().environment) == 2 + @pytest.mark.tier2 + def test_cv_audit_scenarios(self, module_product, target_sat): + """Check for various scenarios where a content view's needs_publish flag + should be set to true and that it properly gets set and unset + + :id: 48b0ce35-f76b-447e-a465-d9ce70cbb20e` + + :steps: + + 1. Create a CV + 2. Add a filter to the CV + 3. Add a rule to the filter + 4. Delete that rule + 5. Delete that filter + 6. Add a repo to the CV + 7. Delete content from the repo + 8. Add content to the repo + 9. Sync the repo + + :expectedresults: All of the above steps should results in the CV needing to be + be published + + :CaseLevel: Integration + + :CaseImportance: High + """ + # needs_publish is set to true when created + assert self.yumcv.read().needs_publish + self.yumcv.publish() + assert not self.yumcv.read().needs_publish + # needs_publish is set to true when a filter is added/updated/deleted + cv_filter = target_sat.api.RPMContentViewFilter( + content_view=self.yumcv, inclusion='true', name=gen_string('alphanumeric') + ).create() + assert self.yumcv.read().needs_publish + self.yumcv.publish() + assert not self.yumcv.read().needs_publish + # Adding a rule should set needs_publish to true + cvf_rule = target_sat.api.ContentViewFilterRule( + content_view_filter=cv_filter, name=gen_string('alphanumeric'), version='1.0' + ).create() + assert self.yumcv.read().needs_publish + self.yumcv.publish() + assert not self.yumcv.read().needs_publish + # Deleting a rule should set needs_publish to true + cvf_rule.delete() + assert self.yumcv.read().needs_publish + self.yumcv.publish() + assert not self.yumcv.read().needs_publish + # Deleting a filter should set needs_publish to true + cv_filter.delete() + assert self.yumcv.read().needs_publish + self.yumcv.publish() + assert not self.yumcv.read().needs_publish + # needs_publish is set to true whenever repositories are interacted with on the CV + # add a repo to the CV, needs_publish should be set to true + repo_url = settings.repos.yum_0.url + repo = target_sat.api.Repository( + download_policy='immediate', + mirroring_policy='mirror_complete', + product=module_product, + url=repo_url, + ).create() + repo.sync() + self.yumcv.repository = [repo] + self.yumcv = self.yumcv.update(['repository']) + assert self.yumcv.read().needs_publish + self.yumcv.publish() + assert not self.yumcv.read().needs_publish + # needs_publish is set to true when repository content is removed + packages = target_sat.api.Package(repository=repo).search(query={'per_page': '1000'}) + repo.remove_content(data={'ids': [package.id for package in packages]}) + assert self.yumcv.read().needs_publish + self.yumcv.publish() + assert not self.yumcv.read().needs_publish + # needs_publish is set to true whenever repo content is added + with open(DataFile.RPM_TO_UPLOAD, 'rb') as handle: + repo.upload_content(files={'content': handle}) + assert self.yumcv.read().needs_publish + self.yumcv.publish() + assert not self.yumcv.read().needs_publish + # needs_publish is set to true whenever a repo is synced + repo.sync() + assert self.yumcv.read().needs_publish + self.yumcv.publish() + assert not self.yumcv.read().needs_publish + @pytest.mark.tier2 def test_positive_admin_user_actions( From aeaf3a39afc05d93a1f583dfd522cbe4a5f9196d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 30 Jun 2023 16:37:39 -0400 Subject: [PATCH 054/586] [6.14.z] register function made more flexible (#11788) --- robottelo/hosts.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 14412638e5a..a9b39d56b44 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -588,8 +588,8 @@ def register( using a global registration template. :param target: Satellite or Capusle object to register to, required. - :param org: Organization to register content host for, required. - :param loc: Location to register content host for, required. + :param org: Organization to register content host to. Previously required, pass None to omit + :param loc: Location to register content host for, Previously required, pass None to omit. :param activation_keys: Activation key name to register content host with, required. :param setup_insights: Install and register Insights client, requires OS repo. :param setup_remote_execution: Copy remote execution SSH key. @@ -609,11 +609,25 @@ def register( """ options = { 'activation-keys': activation_keys, - 'organization-id': org.id, - 'location-id': loc.id, 'insecure': str(insecure).lower(), 'update-packages': str(update_packages).lower(), } + if org is not None: + if isinstance(org, entities.Organization): + options['organization-id'] = org.id + elif isinstance(org, dict): + options['organization-id'] = org['id'] + else: + raise ValueError('org must be a dict or an Organization object') + + if loc is not None: + if isinstance(loc, entities.Location): + options['location-id'] = loc.id + elif isinstance(loc, dict): + options['location-id'] = loc['id'] + else: + raise ValueError('loc must be a dict or a Location object') + if target.__class__.__name__ == 'Capsule': options['smart-proxy'] = target.hostname elif target is not None and target.__class__.__name__ not in ['Capsule', 'Satellite']: From 2abbf34ce219a8836b7b15efbe7c595f37431eb4 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 3 Jul 2023 06:44:47 -0400 Subject: [PATCH 055/586] [6.14.z] Bump pytest-reportportal from 5.1.9 to 5.2.0 (#11795) Bump pytest-reportportal from 5.1.9 to 5.2.0 (#11791) (cherry picked from commit bfa98620d1034cbb0f8d090d3a993700a7ca896c) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1ab31c4e242..ea020c9e72a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ python-box==7.0.1 pytest==7.4.0 pytest-services==2.2.1 pytest-mock==3.11.1 -pytest-reportportal==5.1.9 +pytest-reportportal==5.2.0 pytest-xdist==3.3.1 pytest-ibutsu==2.2.4 PyYAML==6.0 From 09cbce1379b768f6282742b85a407a895739fcaf Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 4 Jul 2023 06:11:34 -0400 Subject: [PATCH 056/586] [6.14.z] katello_host_tools_host fixture fix (#11802) --- pytest_fixtures/core/contenthosts.py | 10 +++++++++- tests/foreman/cli/test_host.py | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pytest_fixtures/core/contenthosts.py b/pytest_fixtures/core/contenthosts.py index 2379ca108b2..b76258f1f69 100644 --- a/pytest_fixtures/core/contenthosts.py +++ b/pytest_fixtures/core/contenthosts.py @@ -128,7 +128,15 @@ def registered_hosts(request, target_sat, module_org): def katello_host_tools_host(target_sat, module_org, rhel_contenthost): """Register content host to Satellite and install katello-host-tools on the host.""" repo = settings.repos['SATCLIENT_REPO'][f'RHEL{rhel_contenthost.os_version.major}'] - target_sat.register_host_custom_repo(module_org, rhel_contenthost, [repo]) + ak = target_sat.api.ActivationKey( + content_view=module_org.default_content_view, + max_hosts=100, + organization=module_org, + environment=target_sat.api.LifecycleEnvironment(id=module_org.library.id), + auto_attach=True, + ).create() + + rhel_contenthost.register(module_org, None, ak.name, target_sat, repo=repo) rhel_contenthost.install_katello_host_tools() yield rhel_contenthost diff --git a/tests/foreman/cli/test_host.py b/tests/foreman/cli/test_host.py index 4842966f020..6cbfead3f88 100644 --- a/tests/foreman/cli/test_host.py +++ b/tests/foreman/cli/test_host.py @@ -1624,6 +1624,7 @@ def restore_sca_setting(): url=settings.repos[custom_repo].url, ).create() custom_repo.sync() + subs = target_sat.api.Subscription(organization=module_org, name=prod.name).search() assert len(subs), f'Subscription for sat client product: {prod.name} was not found.' custom_sub = subs[0] @@ -1635,6 +1636,10 @@ def restore_sca_setting(): "subscriptions": [{"id": custom_sub.id, "quantity": 1}], } ) + # make sure repo is enabled + katello_host_tools_host.enable_repo( + f'{module_org.name}_{prod.name}_{custom_repo.name}', force=True + ) # refresh repository metadata katello_host_tools_host.subscription_manager_list_repos() return details From 3d52ef939715a8e4c72697d7a4e464bcec2ec774 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 4 Jul 2023 08:26:07 -0400 Subject: [PATCH 057/586] [6.14.z] Add test for redis type foreman-rails-cache-store (#11805) --- tests/foreman/installer/test_installer.py | 54 +++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/foreman/installer/test_installer.py b/tests/foreman/installer/test_installer.py index 7f66211f96d..1a4797046a1 100644 --- a/tests/foreman/installer/test_installer.py +++ b/tests/foreman/installer/test_installer.py @@ -1815,3 +1815,57 @@ def test_installer_cap_pub_directory_accessibility(capsule_configured): ) command_output = capsule_configured.execute('satellite-installer', timeout='20m') assert 'Success!' in command_output.stdout + + +@pytest.mark.e2e +@pytest.mark.tier1 +@pytest.mark.parametrize("sat_ready_rhel", [settings.server.version.rhel_version], indirect=True) +def test_satellite_installation_with_options(sat_ready_rhel): + """Run Satellite installation with different options + + :id: fa315028-d9fd-42b3-a07b-53166203abdc + + :steps: + 1. Get a RHEL host + 2. Configure satellite repos + 3. Enable satellite module + 4. Install satellite + 5. Run satellite-installer with required options + + :expectedresults: + 1. Correct satellite packaged is installed + 2. satellite-installer runs successfully + 3. satellite-maintain health check runs successfully + + :CaseImportance: High + + :customerscenario: true + + :BZ: 2063717, 2165092 + """ + sat_version = settings.server.version.release + # Register for RHEL8 repos, get Ohsnap repofile, and enable and download satellite + sat_ready_rhel.register_to_cdn() + sat_ready_rhel.download_repofile(product='satellite', release=settings.server.version.release) + sat_ready_rhel.execute('dnf -y module enable satellite:el8 && dnf -y install satellite') + installed_version = sat_ready_rhel.execute('rpm --query satellite').stdout + assert sat_version in installed_version + # Install Satellite + sat_ready_rhel.execute( + InstallerCommand( + installer_args=[ + 'scenario satellite', + f'foreman-initial-admin-password {settings.server.admin_password}', + 'foreman-rails-cache-store type:redis', + ] + ).get_command(), + timeout='30m', + ) + result = sat_ready_rhel.execute( + r'grep "\[ERROR" --after-context=100 /var/log/foreman-installer/satellite.log' + ) + assert len(result.stdout) == 0 + result = sat_ready_rhel.cli.Health.check() + assert 'FAIL' not in result.stdout + assert sat_ready_rhel.execute('rpm -q foreman-redis').status == 0 + assert sat_ready_rhel.execute('grep "type: redis" /etc/foreman/settings.yaml').status == 0 From 7f1f05adf755180a6cce60dd413163ace7bc1525 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 4 Jul 2023 10:09:28 -0400 Subject: [PATCH 058/586] [6.14.z] rex_contenthost now using global registration (#11804) rex_contenthost now using global registration (cherry picked from commit c9ee4d759e9f238b02ae2a914dc97a0b84aa70ae) Co-authored-by: Peter Ondrejka --- pytest_fixtures/core/contenthosts.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pytest_fixtures/core/contenthosts.py b/pytest_fixtures/core/contenthosts.py index b76258f1f69..830070c6b10 100644 --- a/pytest_fixtures/core/contenthosts.py +++ b/pytest_fixtures/core/contenthosts.py @@ -157,14 +157,11 @@ def cockpit_host(class_target_sat, class_org, rhel_contenthost): @pytest.fixture -def rex_contenthost(request, module_org, target_sat): +def rex_contenthost(request, module_org, target_sat, module_ak_with_cv): request.param['no_containers'] = True with Broker(**host_conf(request), host_class=ContentHost) as host: - # Register content host to Satellite repo = settings.repos['SATCLIENT_REPO'][f'RHEL{host.os_version.major}'] - target_sat.register_host_custom_repo(module_org, host, [repo]) - # Enable remote execution on the host - host.add_rex_key(satellite=target_sat) + host.register(module_org, None, module_ak_with_cv.name, target_sat, repo=repo) yield host From 56a57c13ef40439f995a96c9ea480afac5205d12 Mon Sep 17 00:00:00 2001 From: yanpliu Date: Wed, 28 Jun 2023 11:00:51 -0400 Subject: [PATCH 059/586] virtwho UI cases to support for Content Host Legacy UI and parametrize to consolidate multiple cases with duplicated steps into one (#11543) * virtwho UI cases to support for Content Host Legacy UI and parametrize to consolidate multiple cases with duplicated steps into one * Update the legacy ui method name from airgun update * add assert for read_legacy_ui (cherry picked from commit 47c151b939698f884423aada4a07a58ea9cd9b08) --- tests/foreman/virtwho/api/test_esx.py | 75 +++------------- tests/foreman/virtwho/api/test_hyperv.py | 75 +++------------- tests/foreman/virtwho/api/test_kubevirt.py | 26 ++++-- tests/foreman/virtwho/api/test_libvirt.py | 75 +++------------- tests/foreman/virtwho/api/test_nutanix.py | 85 ++++--------------- tests/foreman/virtwho/cli/test_esx.py | 68 +++------------ tests/foreman/virtwho/cli/test_esx_sca.py | 6 +- tests/foreman/virtwho/cli/test_hyperv.py | 68 ++++----------- tests/foreman/virtwho/cli/test_hyperv_sca.py | 6 +- tests/foreman/virtwho/cli/test_kubevirt.py | 68 ++++----------- .../foreman/virtwho/cli/test_kubevirt_sca.py | 6 +- tests/foreman/virtwho/cli/test_libvirt.py | 68 ++++----------- tests/foreman/virtwho/cli/test_libvirt_sca.py | 6 +- tests/foreman/virtwho/cli/test_nutanix.py | 68 ++++----------- tests/foreman/virtwho/cli/test_nutanix_sca.py | 9 +- tests/foreman/virtwho/ui/test_esx.py | 67 ++++++--------- tests/foreman/virtwho/ui/test_hyperv.py | 61 +++++-------- tests/foreman/virtwho/ui/test_kubevirt.py | 61 +++++-------- tests/foreman/virtwho/ui/test_libvirt.py | 61 +++++-------- tests/foreman/virtwho/ui/test_nutanix.py | 59 +++++-------- 20 files changed, 292 insertions(+), 726 deletions(-) diff --git a/tests/foreman/virtwho/api/test_esx.py b/tests/foreman/virtwho/api/test_esx.py index 39e68087f1f..069eefc969a 100644 --- a/tests/foreman/virtwho/api/test_esx.py +++ b/tests/foreman/virtwho/api/test_esx.py @@ -68,8 +68,9 @@ def delete_host(form_data, target_sat): @pytest.mark.usefixtures('delete_host') class TestVirtWhoConfigforEsx: @pytest.mark.tier2 - def test_positive_deploy_configure_by_id( - self, default_org, form_data, virtwho_config, target_sat + @pytest.mark.parametrize('deploy_type', ['id', 'script']) + def test_positive_deploy_configure_by_id_script( + self, default_org, form_data, virtwho_config, target_sat, deploy_type ): """Verify "POST /foreman_virt_who_configure/api/v2/configs" @@ -82,67 +83,19 @@ def test_positive_deploy_configure_by_id( :CaseImportance: High """ assert virtwho_config.status == 'unknown' - command = get_configure_command(virtwho_config.id, default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - virt_who_instance = ( - target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] - .status - ) - assert virt_who_instance == 'ok' - hosts = [ - ( - hypervisor_name, - f'product_id={settings.virtwho.sku.vdc_physical} and type=NORMAL', - ), - ( - guest_name, - f'product_id={settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED', - ), - ] - for hostname, sku in hosts: - host = target_sat.cli.Host.list({'search': hostname})[0] - subscriptions = target_sat.cli.Subscription.list( - {'organization': default_org.name, 'search': sku} + if deploy_type == "id": + command = get_configure_command(virtwho_config.id, default_org.name) + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data['hypervisor_type'], debug=True, org=default_org.label ) - vdc_id = subscriptions[0]['id'] - if 'type=STACK_DERIVED' in sku: - for item in subscriptions: - if hypervisor_name.lower() in item['type']: - vdc_id = item['id'] - break - target_sat.api.HostSubscription(host=host['id']).add_subscriptions( - data={'subscriptions': [{'id': vdc_id, 'quantity': 'Automatic'}]} + elif deploy_type == "script": + script = virtwho_config.deploy_script() + hypervisor_name, guest_name = deploy_configure_by_script( + script['virt_who_config_script'], + form_data['hypervisor_type'], + debug=True, + org=default_org.label, ) - result = target_sat.api.Host().search(query={'search': hostname})[0].read_json() - assert result['subscription_status_label'] == 'Fully entitled' - - @pytest.mark.tier2 - def test_positive_deploy_configure_by_script( - self, default_org, form_data, virtwho_config, target_sat - ): - """Verify "GET /foreman_virt_who_configure/api/ - - v2/configs/:id/deploy_script" - - :id: 166ec4f8-e3fa-4555-9acb-1a5d693a42bb - - :expectedresults: Config can be created and deployed - - :CaseLevel: Integration - - :CaseImportance: High - """ - assert virtwho_config.status == 'unknown' - script = virtwho_config.deploy_script() - hypervisor_name, guest_name = deploy_configure_by_script( - script['virt_who_config_script'], - form_data['hypervisor_type'], - debug=True, - org=default_org.label, - ) virt_who_instance = ( target_sat.api.VirtWhoConfig() .search(query={'search': f'name={virtwho_config.name}'})[0] diff --git a/tests/foreman/virtwho/api/test_hyperv.py b/tests/foreman/virtwho/api/test_hyperv.py index a2af53b39c0..084975d00a3 100644 --- a/tests/foreman/virtwho/api/test_hyperv.py +++ b/tests/foreman/virtwho/api/test_hyperv.py @@ -55,8 +55,9 @@ def virtwho_config(form_data, target_sat): class TestVirtWhoConfigforHyperv: @pytest.mark.tier2 - def test_positive_deploy_configure_by_id( - self, default_org, form_data, virtwho_config, target_sat + @pytest.mark.parametrize('deploy_type', ['id', 'script']) + def test_positive_deploy_configure_by_id_script( + self, default_org, form_data, virtwho_config, target_sat, deploy_type ): """Verify "POST /foreman_virt_who_configure/api/v2/configs" @@ -69,67 +70,19 @@ def test_positive_deploy_configure_by_id( :CaseImportance: High """ assert virtwho_config.status == 'unknown' - command = get_configure_command(virtwho_config.id, default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - virt_who_instance = ( - target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] - .status - ) - assert virt_who_instance == 'ok' - hosts = [ - ( - hypervisor_name, - f'product_id={settings.virtwho.sku.vdc_physical} and type=NORMAL', - ), - ( - guest_name, - f'product_id={settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED', - ), - ] - for hostname, sku in hosts: - host = target_sat.cli.Host.list({'search': hostname})[0] - subscriptions = target_sat.cli.Subscription.list( - {'organization': default_org.name, 'search': sku} + if deploy_type == "id": + command = get_configure_command(virtwho_config.id, default_org.name) + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data['hypervisor_type'], debug=True, org=default_org.label ) - vdc_id = subscriptions[0]['id'] - if 'type=STACK_DERIVED' in sku: - for item in subscriptions: - if hypervisor_name.lower() in item['type']: - vdc_id = item['id'] - break - target_sat.api.HostSubscription(host=host['id']).add_subscriptions( - data={'subscriptions': [{'id': vdc_id, 'quantity': 'Automatic'}]} + elif deploy_type == "script": + script = virtwho_config.deploy_script() + hypervisor_name, guest_name = deploy_configure_by_script( + script['virt_who_config_script'], + form_data['hypervisor_type'], + debug=True, + org=default_org.label, ) - result = target_sat.api.Host().search(query={'search': hostname})[0].read_json() - assert result['subscription_status_label'] == 'Fully entitled' - - @pytest.mark.tier2 - def test_positive_deploy_configure_by_script( - self, default_org, form_data, virtwho_config, target_sat - ): - """Verify "GET /foreman_virt_who_configure/api/ - - v2/configs/:id/deploy_script" - - :id: 2c58b131-5d68-41d2-b804-4548f998ab5f - - :expectedresults: Config can be created and deployed - - :CaseLevel: Integration - - :CaseImportance: High - """ - assert virtwho_config.status == 'unknown' - script = virtwho_config.deploy_script() - hypervisor_name, guest_name = deploy_configure_by_script( - script['virt_who_config_script'], - form_data['hypervisor_type'], - debug=True, - org=default_org.label, - ) virt_who_instance = ( target_sat.api.VirtWhoConfig() .search(query={'search': f'name={virtwho_config.name}'})[0] diff --git a/tests/foreman/virtwho/api/test_kubevirt.py b/tests/foreman/virtwho/api/test_kubevirt.py index 059fdab7f21..0ff6ade433d 100644 --- a/tests/foreman/virtwho/api/test_kubevirt.py +++ b/tests/foreman/virtwho/api/test_kubevirt.py @@ -53,18 +53,19 @@ def virtwho_config(form_data, target_sat): @pytest.fixture(autouse=True) -def clean_host(form_data, target_sat): +def delete_host(form_data, target_sat): guest_name, _ = get_guest_info(form_data['hypervisor_type']) results = target_sat.api.Host().search(query={'search': guest_name}) if results: target_sat.api.Host(id=results[0].read_json()['id']).delete() -@pytest.mark.usefixtures('clean_host') +@pytest.mark.usefixtures('delete_host') class TestVirtWhoConfigforKubevirt: @pytest.mark.tier2 - def test_positive_deploy_configure_by_id( - self, default_org, form_data, virtwho_config, target_sat + @pytest.mark.parametrize('deploy_type', ['id', 'script']) + def test_positive_deploy_configure_by_id_script( + self, default_org, form_data, virtwho_config, target_sat, deploy_type ): """Verify "POST /foreman_virt_who_configure/api/v2/configs" @@ -77,10 +78,19 @@ def test_positive_deploy_configure_by_id( :CaseImportance: High """ assert virtwho_config.status == 'unknown' - command = get_configure_command(virtwho_config.id, default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) + if deploy_type == "id": + command = get_configure_command(virtwho_config.id, default_org.name) + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data['hypervisor_type'], debug=True, org=default_org.label + ) + elif deploy_type == "script": + script = virtwho_config.deploy_script() + hypervisor_name, guest_name = deploy_configure_by_script( + script['virt_who_config_script'], + form_data['hypervisor_type'], + debug=True, + org=default_org.label, + ) virt_who_instance = ( target_sat.api.VirtWhoConfig() .search(query={'search': f'name={virtwho_config.name}'})[0] diff --git a/tests/foreman/virtwho/api/test_libvirt.py b/tests/foreman/virtwho/api/test_libvirt.py index 976a8e424a0..b1f105938e5 100644 --- a/tests/foreman/virtwho/api/test_libvirt.py +++ b/tests/foreman/virtwho/api/test_libvirt.py @@ -54,8 +54,9 @@ def virtwho_config(form_data, target_sat): class TestVirtWhoConfigforLibvirt: @pytest.mark.tier2 - def test_positive_deploy_configure_by_id( - self, default_org, form_data, virtwho_config, target_sat + @pytest.mark.parametrize('deploy_type', ['id', 'script']) + def test_positive_deploy_configure_by_id_script( + self, default_org, form_data, virtwho_config, target_sat, deploy_type ): """Verify "POST /foreman_virt_who_configure/api/v2/configs" @@ -68,67 +69,19 @@ def test_positive_deploy_configure_by_id( :CaseImportance: High """ assert virtwho_config.status == 'unknown' - command = get_configure_command(virtwho_config.id, default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - virt_who_instance = ( - target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] - .status - ) - assert virt_who_instance == 'ok' - hosts = [ - ( - hypervisor_name, - f'product_id={settings.virtwho.sku.vdc_physical} and type=NORMAL', - ), - ( - guest_name, - f'product_id={settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED', - ), - ] - for hostname, sku in hosts: - host = target_sat.cli.Host.list({'search': hostname})[0] - subscriptions = target_sat.cli.Subscription.list( - {'organization': default_org.name, 'search': sku} + if deploy_type == "id": + command = get_configure_command(virtwho_config.id, default_org.name) + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data['hypervisor_type'], debug=True, org=default_org.label ) - vdc_id = subscriptions[0]['id'] - if 'type=STACK_DERIVED' in sku: - for item in subscriptions: - if hypervisor_name.lower() in item['type']: - vdc_id = item['id'] - break - target_sat.api.HostSubscription(host=host['id']).add_subscriptions( - data={'subscriptions': [{'id': vdc_id, 'quantity': 'Automatic'}]} + elif deploy_type == "script": + script = virtwho_config.deploy_script() + hypervisor_name, guest_name = deploy_configure_by_script( + script['virt_who_config_script'], + form_data['hypervisor_type'], + debug=True, + org=default_org.label, ) - result = target_sat.api.Host().search(query={'search': hostname})[0].read_json() - assert result['subscription_status_label'] == 'Fully entitled' - - @pytest.mark.tier2 - def test_positive_deploy_configure_by_script( - self, default_org, form_data, virtwho_config, target_sat - ): - """Verify "GET /foreman_virt_who_configure/api/ - - v2/configs/:id/deploy_script" - - :id: 9668b900-0d2f-42ae-b2f8-523ca292b2bd - - :expectedresults: Config can be created and deployed - - :CaseLevel: Integration - - :CaseImportance: High - """ - assert virtwho_config.status == 'unknown' - script = virtwho_config.deploy_script() - hypervisor_name, guest_name = deploy_configure_by_script( - script['virt_who_config_script'], - form_data['hypervisor_type'], - debug=True, - org=default_org.label, - ) virt_who_instance = ( target_sat.api.VirtWhoConfig() .search(query={'search': f'name={virtwho_config.name}'})[0] diff --git a/tests/foreman/virtwho/api/test_nutanix.py b/tests/foreman/virtwho/api/test_nutanix.py index bdf393c88c2..147c6246d1e 100644 --- a/tests/foreman/virtwho/api/test_nutanix.py +++ b/tests/foreman/virtwho/api/test_nutanix.py @@ -59,18 +59,19 @@ def virtwho_config(form_data, target_sat): @pytest.fixture(autouse=True) -def clean_host(form_data, target_sat): +def delete_host(form_data, target_sat): guest_name, _ = get_guest_info(form_data['hypervisor_type']) results = target_sat.api.Host().search(query={'search': guest_name}) if results: target_sat.api.Host(id=results[0].read_json()['id']).delete() -@pytest.mark.usefixtures('clean_host') +@pytest.mark.usefixtures('delete_host') class TestVirtWhoConfigforNutanix: @pytest.mark.tier2 - def test_positive_deploy_configure_by_id( - self, default_org, form_data, virtwho_config, target_sat + @pytest.mark.parametrize('deploy_type', ['id', 'script']) + def test_positive_deploy_configure_by_id_script( + self, default_org, form_data, virtwho_config, target_sat, deploy_type ): """Verify "POST /foreman_virt_who_configure/api/v2/configs" @@ -83,71 +84,19 @@ def test_positive_deploy_configure_by_id( :CaseImportance: High """ assert virtwho_config.status == 'unknown' - command = get_configure_command(virtwho_config.id, default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - virt_who_instance = ( - target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] - .status - ) - assert virt_who_instance == 'ok' - hosts = [ - ( - hypervisor_name, - settings.virtwho.sku.vdc_physical, - 'NORMAL', - ), - ( - guest_name, - settings.virtwho.sku.vdc_virtual, - 'STACK_DERIVED', - ), - ] - for hostname, sku, type in hosts: - host = target_sat.api.Host().search(query={'search': hostname})[0].read_json() - subscriptions = target_sat.api.Organization(id=default_org.id).subscriptions()[ - 'results' - ] - for item in subscriptions: - if item['type'] == type and item['product_id'] == sku: - vdc_id = item['id'] - if ( - 'hypervisor' in item - and hypervisor_name.lower() in item['hypervisor']['name'] - ): - break - target_sat.api.HostSubscription(host=host['id']).add_subscriptions( - data={'subscriptions': [{'id': vdc_id, 'quantity': 'Automatic'}]} + if deploy_type == "id": + command = get_configure_command(virtwho_config.id, default_org.name) + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data['hypervisor_type'], debug=True, org=default_org.label + ) + elif deploy_type == "script": + script = virtwho_config.deploy_script() + hypervisor_name, guest_name = deploy_configure_by_script( + script['virt_who_config_script'], + form_data['hypervisor_type'], + debug=True, + org=default_org.label, ) - result = target_sat.api.Host().search(query={'search': hostname})[0].read_json() - assert result['subscription_status_label'] == 'Fully entitled' - - @pytest.mark.tier2 - def test_positive_deploy_configure_by_script( - self, default_org, form_data, virtwho_config, target_sat - ): - """Verify "GET /foreman_virt_who_configure/api/ - - v2/configs/:id/deploy_script" - - :id: 7aabfa3e-0ec0-44a3-8b7c-b67476318c2c - - :expectedresults: Config can be created and deployed - - :CaseLevel: Integration - - :CaseImportance: High - """ - assert virtwho_config.status == 'unknown' - script = virtwho_config.deploy_script() - hypervisor_name, guest_name = deploy_configure_by_script( - script['virt_who_config_script'], - form_data['hypervisor_type'], - debug=True, - org=default_org.label, - ) virt_who_instance = ( target_sat.api.VirtWhoConfig() .search(query={'search': f'name={virtwho_config.name}'})[0] diff --git a/tests/foreman/virtwho/cli/test_esx.py b/tests/foreman/virtwho/cli/test_esx.py index dec037c6058..613f69acf0f 100644 --- a/tests/foreman/virtwho/cli/test_esx.py +++ b/tests/foreman/virtwho/cli/test_esx.py @@ -64,8 +64,9 @@ def virtwho_config(form_data, target_sat): class TestVirtWhoConfigforEsx: @pytest.mark.tier2 - def test_positive_deploy_configure_by_id( - self, default_org, form_data, virtwho_config, target_sat + @pytest.mark.parametrize('deploy_type', ['id', 'script']) + def test_positive_deploy_configure_by_id_script( + self, default_org, form_data, virtwho_config, target_sat, deploy_type ): """Verify " hammer virt-who-config deploy" @@ -78,61 +79,18 @@ def test_positive_deploy_configure_by_id( :CaseImportance: High """ assert virtwho_config['status'] == 'No Report Yet' - command = get_configure_command(virtwho_config['id'], default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=default_org.label - ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ - 'general-information' - ]['status'] - assert virt_who_instance == 'OK' - hosts = [ - ( - hypervisor_name, - f'product_id={settings.virtwho.sku.vdc_physical} and type=NORMAL', - ), - ( - guest_name, - f'product_id={settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED', - ), - ] - for hostname, sku in hosts: - host = target_sat.cli.Host.list({'search': hostname})[0] - subscriptions = target_sat.cli.Subscription.list( - {'organization': default_org.name, 'search': sku} + if deploy_type == "id": + command = get_configure_command(virtwho_config['id'], default_org.name) + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data['hypervisor-type'], debug=True, org=default_org.label ) - vdc_id = subscriptions[0]['id'] - if 'type=STACK_DERIVED' in sku: - for item in subscriptions: - if hypervisor_name.lower() in item['type']: - vdc_id = item['id'] - break - result = target_sat.cli.Host.subscription_attach( - {'host-id': host['id'], 'subscription-id': vdc_id} + elif deploy_type == "script": + script = target_sat.cli.VirtWhoConfig.fetch( + {'id': virtwho_config['id']}, output_format='base' + ) + hypervisor_name, guest_name = deploy_configure_by_script( + script, form_data['hypervisor-type'], debug=True, org=default_org.label ) - assert result.strip() == 'Subscription attached to the host successfully.' - - @pytest.mark.tier2 - def test_positive_deploy_configure_by_script( - self, default_org, form_data, virtwho_config, target_sat - ): - """Verify " hammer virt-who-config fetch" - - :id: 6aaffaeb-aaf2-42cf-b0dc-ca41a53d42a6 - - :expectedresults: Config can be created, fetch and deploy - - :CaseLevel: Integration - - :CaseImportance: High - """ - assert virtwho_config['status'] == 'No Report Yet' - script = target_sat.cli.VirtWhoConfig.fetch( - {'id': virtwho_config['id']}, output_format='base' - ) - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor-type'], debug=True, org=default_org.label - ) virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ 'general-information' ]['status'] diff --git a/tests/foreman/virtwho/cli/test_esx_sca.py b/tests/foreman/virtwho/cli/test_esx_sca.py index 4a6625d1659..c82520b30fc 100644 --- a/tests/foreman/virtwho/cli/test_esx_sca.py +++ b/tests/foreman/virtwho/cli/test_esx_sca.py @@ -67,11 +67,13 @@ class TestVirtWhoConfigforEsx: def test_positive_deploy_configure_by_id_script( self, module_sca_manifest_org, form_data, virtwho_config, target_sat, deploy_type ): - """Verify "hammer virt-who-config deploy" + """Verify "hammer virt-who-config deploy & fetch" :id: 04f2cef8-c88e-4a21-9d2f-c17238eea308 - :expectedresults: Config can be created and deployed + :expectedresults: + 1. Config can be created and deployed + 2. Config can be created, fetch and deploy :CaseLevel: Integration diff --git a/tests/foreman/virtwho/cli/test_hyperv.py b/tests/foreman/virtwho/cli/test_hyperv.py index 3696554af50..6ccb39c6bd2 100644 --- a/tests/foreman/virtwho/cli/test_hyperv.py +++ b/tests/foreman/virtwho/cli/test_hyperv.py @@ -55,69 +55,35 @@ def virtwho_config(form_data, target_sat): class TestVirtWhoConfigforHyperv: @pytest.mark.tier2 - def test_positive_deploy_configure_by_id( - self, default_org, form_data, virtwho_config, target_sat + @pytest.mark.parametrize('deploy_type', ['id', 'script']) + def test_positive_deploy_configure_by_id_script( + self, default_org, form_data, virtwho_config, target_sat, deploy_type ): - """Verify " hammer virt-who-config deploy" + """Verify " hammer virt-who-config deploy & fetch" :id: 7cc0ad4f-e185-4d63-a2f5-1cb0245faa6c - :expectedresults: Config can be created and deployed + :expectedresults: + 1. Config can be created and deployed + 2. Config can be created, fetch and deploy :CaseLevel: Integration :CaseImportance: High """ assert virtwho_config['status'] == 'No Report Yet' - command = get_configure_command(virtwho_config['id'], default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=default_org.label - ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ - 'general-information' - ]['status'] - assert virt_who_instance == 'OK' - hosts = [ - (hypervisor_name, f'product_id={settings.virtwho.sku.vdc_physical} and type=NORMAL'), - (guest_name, f'product_id={settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED'), - ] - for hostname, sku in hosts: - host = target_sat.cli.Host.list({'search': hostname})[0] - subscriptions = target_sat.cli.Subscription.list( - {'organization': default_org.name, 'search': sku} + if deploy_type == "id": + command = get_configure_command(virtwho_config['id'], default_org.name) + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data['hypervisor-type'], debug=True, org=default_org.label ) - vdc_id = subscriptions[0]['id'] - if 'type=STACK_DERIVED' in sku: - for item in subscriptions: - if hypervisor_name.lower() in item['type']: - vdc_id = item['id'] - break - result = target_sat.cli.Host.subscription_attach( - {'host-id': host['id'], 'subscription-id': vdc_id} + elif deploy_type == "script": + script = target_sat.cli.VirtWhoConfig.fetch( + {'id': virtwho_config['id']}, output_format='base' + ) + hypervisor_name, guest_name = deploy_configure_by_script( + script, form_data['hypervisor-type'], debug=True, org=default_org.label ) - assert result.strip() == 'Subscription attached to the host successfully.' - - @pytest.mark.tier2 - def test_positive_deploy_configure_by_script( - self, default_org, form_data, virtwho_config, target_sat - ): - """Verify " hammer virt-who-config fetch" - - :id: 22dc8068-c843-4ca0-acbe-0b2aef8ece31 - - :expectedresults: Config can be created, fetch and deploy - - :CaseLevel: Integration - - :CaseImportance: High - """ - assert virtwho_config['status'] == 'No Report Yet' - script = target_sat.cli.VirtWhoConfig.fetch( - {'id': virtwho_config['id']}, output_format='base' - ) - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor-type'], debug=True, org=default_org.label - ) virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ 'general-information' ]['status'] diff --git a/tests/foreman/virtwho/cli/test_hyperv_sca.py b/tests/foreman/virtwho/cli/test_hyperv_sca.py index 4fa1c0d3ee5..715ec1eafa1 100644 --- a/tests/foreman/virtwho/cli/test_hyperv_sca.py +++ b/tests/foreman/virtwho/cli/test_hyperv_sca.py @@ -59,11 +59,13 @@ class TestVirtWhoConfigforHyperv: def test_positive_deploy_configure_by_id_script( self, module_sca_manifest_org, form_data, virtwho_config, target_sat, deploy_type ): - """Verify " hammer virt-who-config deploy" + """Verify " hammer virt-who-config deploy & fetch" :id: ba51dd0e-39da-4afd-b7e1-d470082024ba - :expectedresults: Config can be created and deployed + :expectedresults: + 1. Config can be created and deployed + 2. Config can be created, fetch and deploy :CaseLevel: Integration diff --git a/tests/foreman/virtwho/cli/test_kubevirt.py b/tests/foreman/virtwho/cli/test_kubevirt.py index 8ab1fa93e5c..32d1a75416b 100644 --- a/tests/foreman/virtwho/cli/test_kubevirt.py +++ b/tests/foreman/virtwho/cli/test_kubevirt.py @@ -53,69 +53,35 @@ def virtwho_config(form_data, target_sat): class TestVirtWhoConfigforKubevirt: @pytest.mark.tier2 - def test_positive_deploy_configure_by_id( - self, default_org, form_data, virtwho_config, target_sat + @pytest.mark.parametrize('deploy_type', ['id', 'script']) + def test_positive_deploy_configure_by_id_script( + self, default_org, form_data, virtwho_config, target_sat, deploy_type ): - """Verify " hammer virt-who-config deploy" + """Verify " hammer virt-who-config deploy & fetch" :id: d0b109f5-2699-43e4-a6cd-d682204d97a7 - :expectedresults: Config can be created and deployed + :expectedresults: + 1. Config can be created and deployed + 2. Config can be created, fetch and deploy :CaseLevel: Integration :CaseImportance: High """ assert virtwho_config['status'] == 'No Report Yet' - command = get_configure_command(virtwho_config['id'], default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=default_org.label - ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ - 'general-information' - ]['status'] - assert virt_who_instance == 'OK' - hosts = [ - (hypervisor_name, f'product_id={settings.virtwho.sku.vdc_physical} and type=NORMAL'), - (guest_name, f'product_id={settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED'), - ] - for hostname, sku in hosts: - host = target_sat.cli.Host.list({'search': hostname})[0] - subscriptions = target_sat.cli.Subscription.list( - {'organization': default_org.name, 'search': sku} + if deploy_type == "id": + command = get_configure_command(virtwho_config['id'], default_org.name) + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data['hypervisor-type'], debug=True, org=default_org.label ) - vdc_id = subscriptions[0]['id'] - if 'type=STACK_DERIVED' in sku: - for item in subscriptions: - if hypervisor_name.lower() in item['type']: - vdc_id = item['id'] - break - result = target_sat.cli.Host.subscription_attach( - {'host-id': host['id'], 'subscription-id': vdc_id} + elif deploy_type == "script": + script = target_sat.cli.VirtWhoConfig.fetch( + {'id': virtwho_config['id']}, output_format='base' + ) + hypervisor_name, guest_name = deploy_configure_by_script( + script, form_data['hypervisor-type'], debug=True, org=default_org.label ) - assert result.strip() == 'Subscription attached to the host successfully.' - - @pytest.mark.tier2 - def test_positive_deploy_configure_by_script( - self, default_org, form_data, virtwho_config, target_sat - ): - """Verify " hammer virt-who-config fetch" - - :id: 273df8e0-5ef5-47d9-9567-543157be7dd8 - - :expectedresults: Config can be created, fetch and deploy - - :CaseLevel: Integration - - :CaseImportance: High - """ - assert virtwho_config['status'] == 'No Report Yet' - script = target_sat.cli.VirtWhoConfig.fetch( - {'id': virtwho_config['id']}, output_format='base' - ) - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor-type'], debug=True, org=default_org.label - ) virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ 'general-information' ]['status'] diff --git a/tests/foreman/virtwho/cli/test_kubevirt_sca.py b/tests/foreman/virtwho/cli/test_kubevirt_sca.py index a22d69513e9..4c86aa21f51 100644 --- a/tests/foreman/virtwho/cli/test_kubevirt_sca.py +++ b/tests/foreman/virtwho/cli/test_kubevirt_sca.py @@ -55,11 +55,13 @@ class TestVirtWhoConfigforKubevirt: def test_positive_deploy_configure_by_id_script( self, module_sca_manifest_org, form_data, virtwho_config, target_sat, deploy_type ): - """Verify " hammer virt-who-config deploy" + """Verify " hammer virt-who-config deploy & fetch" :id: e0162dba-a50f-4356-9dc2-c928a1bed15c - :expectedresults: Config can be created and deployed + :expectedresults: + 1. Config can be created and deployed + 2. Config can be created, fetch and deploy :CaseLevel: Integration diff --git a/tests/foreman/virtwho/cli/test_libvirt.py b/tests/foreman/virtwho/cli/test_libvirt.py index d8d2f7b2c19..59508ba74a7 100644 --- a/tests/foreman/virtwho/cli/test_libvirt.py +++ b/tests/foreman/virtwho/cli/test_libvirt.py @@ -54,69 +54,35 @@ def virtwho_config(form_data, target_sat): class TestVirtWhoConfigforLibvirt: @pytest.mark.tier2 - def test_positive_deploy_configure_by_id( - self, default_org, form_data, virtwho_config, target_sat + @pytest.mark.parametrize('deploy_type', ['id', 'script']) + def test_positive_deploy_configure_by_id_script( + self, default_org, form_data, virtwho_config, target_sat, deploy_type ): - """Verify " hammer virt-who-config deploy" + """Verify " hammer virt-who-config deploy & fetch" :id: e66bf88a-bd4e-409a-91a8-bc5e005d95dd - :expectedresults: Config can be created and deployed + :expectedresults: + 1. Config can be created and deployed + 2. Config can be created, fetch and deploy :CaseLevel: Integration :CaseImportance: High """ assert virtwho_config['status'] == 'No Report Yet' - command = get_configure_command(virtwho_config['id'], default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=default_org.label - ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ - 'general-information' - ]['status'] - assert virt_who_instance == 'OK' - hosts = [ - (hypervisor_name, f'product_id={settings.virtwho.sku.vdc_physical} and type=NORMAL'), - (guest_name, f'product_id={settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED'), - ] - for hostname, sku in hosts: - host = target_sat.cli.Host.list({'search': hostname})[0] - subscriptions = target_sat.cli.Subscription.list( - {'organization': default_org.name, 'search': sku} + if deploy_type == "id": + command = get_configure_command(virtwho_config['id'], default_org.name) + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data['hypervisor-type'], debug=True, org=default_org.label ) - vdc_id = subscriptions[0]['id'] - if 'type=STACK_DERIVED' in sku: - for item in subscriptions: - if hypervisor_name.lower() in item['type']: - vdc_id = item['id'] - break - result = target_sat.cli.Host.subscription_attach( - {'host-id': host['id'], 'subscription-id': vdc_id} + elif deploy_type == "script": + script = target_sat.cli.VirtWhoConfig.fetch( + {'id': virtwho_config['id']}, output_format='base' + ) + hypervisor_name, guest_name = deploy_configure_by_script( + script, form_data['hypervisor-type'], debug=True, org=default_org.label ) - assert result.strip() == 'Subscription attached to the host successfully.' - - @pytest.mark.tier2 - def test_positive_deploy_configure_by_script( - self, default_org, form_data, virtwho_config, target_sat - ): - """Verify " hammer virt-who-config fetch" - - :id: bd5c52ab-3dbd-4cf1-9837-b8eb6233f1cd - - :expectedresults: Config can be created, fetch and deploy - - :CaseLevel: Integration - - :CaseImportance: High - """ - assert virtwho_config['status'] == 'No Report Yet' - script = target_sat.cli.VirtWhoConfig.fetch( - {'id': virtwho_config['id']}, output_format='base' - ) - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor-type'], debug=True, org=default_org.label - ) virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ 'general-information' ]['status'] diff --git a/tests/foreman/virtwho/cli/test_libvirt_sca.py b/tests/foreman/virtwho/cli/test_libvirt_sca.py index a4d0c4d287d..02550772ca6 100644 --- a/tests/foreman/virtwho/cli/test_libvirt_sca.py +++ b/tests/foreman/virtwho/cli/test_libvirt_sca.py @@ -56,11 +56,13 @@ class TestVirtWhoConfigforLibvirt: def test_positive_deploy_configure_by_id_script( self, module_sca_manifest_org, form_data, virtwho_config, target_sat, deploy_type ): - """Verify " hammer virt-who-config deploy" + """Verify " hammer virt-who-config deploy & fetch" :id: 53143fc3-97e0-4403-8114-4d7f20fde98e - :expectedresults: Config can be created and deployed + :expectedresults: + 1. Config can be created and deployed + 2. Config can be created, fetch and deploy :CaseLevel: Integration diff --git a/tests/foreman/virtwho/cli/test_nutanix.py b/tests/foreman/virtwho/cli/test_nutanix.py index e312f98fc86..10599c55c00 100644 --- a/tests/foreman/virtwho/cli/test_nutanix.py +++ b/tests/foreman/virtwho/cli/test_nutanix.py @@ -59,69 +59,35 @@ def virtwho_config(form_data, target_sat): class TestVirtWhoConfigforNutanix: @pytest.mark.tier2 - def test_positive_deploy_configure_by_id( - self, default_org, form_data, virtwho_config, target_sat + @pytest.mark.parametrize('deploy_type', ['id', 'script']) + def test_positive_deploy_configure_by_id_script( + self, default_org, form_data, virtwho_config, target_sat, deploy_type ): - """Verify "hammer virt-who-config deploy" + """Verify "hammer virt-who-config deploy & fetch" :id: 129d8e57-b4fc-4d95-ad33-5aa6ec6fb146 - :expectedresults: Config can be created and deployed + :expectedresults: + 1. Config can be created and deployed + 2. Config can be created, fetch and deploy :CaseLevel: Integration :CaseImportance: High """ assert virtwho_config['status'] == 'No Report Yet' - command = get_configure_command(virtwho_config['id'], default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=default_org.label - ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ - 'general-information' - ]['status'] - assert virt_who_instance == 'OK' - hosts = [ - (hypervisor_name, f'product_id={settings.virtwho.sku.vdc_physical} and type=NORMAL'), - (guest_name, f'product_id={settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED'), - ] - for hostname, sku in hosts: - host = target_sat.cli.Host.list({'search': hostname})[0] - subscriptions = target_sat.cli.Subscription.list( - {'organization': default_org.name, 'search': sku} + if deploy_type == "id": + command = get_configure_command(virtwho_config['id'], default_org.name) + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data['hypervisor-type'], debug=True, org=default_org.label ) - vdc_id = subscriptions[0]['id'] - if 'type=STACK_DERIVED' in sku: - for item in subscriptions: - if hypervisor_name.lower() in item['type']: - vdc_id = item['id'] - break - result = target_sat.cli.Host.subscription_attach( - {'host-id': host['id'], 'subscription-id': vdc_id} + elif deploy_type == "script": + script = target_sat.cli.VirtWhoConfig.fetch( + {'id': virtwho_config['id']}, output_format='base' + ) + hypervisor_name, guest_name = deploy_configure_by_script( + script, form_data['hypervisor-type'], debug=True, org=default_org.label ) - assert result.strip() == 'Subscription attached to the host successfully.' - - @pytest.mark.tier2 - def test_positive_deploy_configure_by_script( - self, default_org, form_data, virtwho_config, target_sat - ): - """Verify "hammer virt-who-config fetch" - - :id: d707fac0-f2b1-4493-b083-cf1edc231691 - - :expectedresults: Config can be created, fetch and deploy - - :CaseLevel: Integration - - :CaseImportance: High - """ - assert virtwho_config['status'] == 'No Report Yet' - script = target_sat.cli.VirtWhoConfig.fetch( - {'id': virtwho_config['id']}, output_format='base' - ) - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor-type'], debug=True, org=default_org.label - ) virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ 'general-information' ]['status'] diff --git a/tests/foreman/virtwho/cli/test_nutanix_sca.py b/tests/foreman/virtwho/cli/test_nutanix_sca.py index 84db5ce8733..b18a82cd670 100644 --- a/tests/foreman/virtwho/cli/test_nutanix_sca.py +++ b/tests/foreman/virtwho/cli/test_nutanix_sca.py @@ -60,11 +60,13 @@ class TestVirtWhoConfigforNutanix: def test_positive_deploy_configure_by_id_script( self, module_sca_manifest_org, form_data, virtwho_config, target_sat, deploy_type ): - """Verify "hammer virt-who-config deploy" + """Verify "hammer virt-who-config deploy & fetch" :id: 71750104-b436-4ad4-9b6b-6f0fe8c3ee4c - :expectedresults: Config can be created and deployed + :expectedresults: + 1. Config can be created and deployed + 2. Config can be created, fetch and deploy :CaseLevel: Integration @@ -120,13 +122,14 @@ def test_positive_hypervisor_id_option( def test_positive_prism_central_deploy_configure_by_id_script( self, module_sca_manifest_org, form_data, target_sat, deploy_type ): - """Verify "hammer virt-who-config deploy" on nutanix prism central mode + """Verify "hammer virt-who-config deploy & fetch" on nutanix prism central mode :id: 96fd691f-5b62-469c-adc7-f2739ddf4a62 :expectedresults: 1. Config can be created and deployed 2. The prism_central has been set in /etc/virt-who.d/vir-who.conf file + 3. Config can be created, fetch and deploy :CaseLevel: Integration diff --git a/tests/foreman/virtwho/ui/test_esx.py b/tests/foreman/virtwho/ui/test_esx.py index ae69655e3e2..8a49ba6250f 100644 --- a/tests/foreman/virtwho/ui/test_esx.py +++ b/tests/foreman/virtwho/ui/test_esx.py @@ -67,23 +67,26 @@ def virtwho_config(form_data, target_sat, session): @pytest.fixture(autouse=True) -def clean_host(form_data, target_sat): +def delete_host(form_data, target_sat): guest_name, _ = get_guest_info(form_data['hypervisor_type']) results = target_sat.api.Host().search(query={'search': guest_name}) if results: target_sat.api.Host(id=results[0].read_json()['id']).delete() -@pytest.mark.usefixtures('clean_host') +@pytest.mark.usefixtures('delete_host') class TestVirtwhoConfigforEsx: @pytest.mark.tier2 - def test_positive_deploy_configure_by_id(self, default_org, virtwho_config, session, form_data): - """Verify configure created and deployed with id. + @pytest.mark.parametrize('deploy_type', ['id', 'script']) + def test_positive_deploy_configure_by_id_script( + self, default_org, virtwho_config, session, form_data, deploy_type + ): + """Verify configure created and deployed with id|script. :id: 44f93ec8-a59a-42a4-ab30-edc554b022b2 :expectedresults: - 1. Config can be created and deployed by command + 1. Config can be created and deployed by command|script 2. No error msg in /var/log/rhsm/rhsm.log 3. Report is sent to satellite 4. Virtual sku can be generated and attached @@ -95,50 +98,30 @@ def test_positive_deploy_configure_by_id(self, default_org, virtwho_config, sess """ name = form_data['name'] values = session.virtwho_configure.read(name) - command = values['deploy']['command'] - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) + if deploy_type == "id": + command = values['deploy']['command'] + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data['hypervisor_type'], debug=True, org=default_org.label + ) + elif deploy_type == "script": + script = values['deploy']['script'] + hypervisor_name, guest_name = deploy_configure_by_script( + script, form_data['hypervisor_type'], debug=True, org=default_org.label + ) assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' hypervisor_display_name = session.contenthost.search(hypervisor_name)[0]['Name'] vdc_physical = f'product_id = {settings.virtwho.sku.vdc_physical} and type=NORMAL' vdc_virtual = f'product_id = {settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED' - session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) - assert session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' - session.contenthost.add_subscription(guest_name, vdc_virtual) - assert session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' - - @pytest.mark.tier2 - def test_positive_deploy_configure_by_script( - self, default_org, virtwho_config, session, form_data - ): - """Verify configure created and deployed with script. - - :id: d64332fb-a6e0-4864-9f8b-2406223fcdcc - - :expectedresults: - 1. Config can be created and deployed by script - 2. No error msg in /var/log/rhsm/rhsm.log - 3. Report is sent to satellite - 4. Virtual sku can be generated and attached - 5. Config can be deleted - - :CaseLevel: Integration - - :CaseImportance: High - """ - name = form_data['name'] - values = session.virtwho_configure.read(name) - script = values['deploy']['script'] - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor_type'], debug=True, org=default_org.label + assert ( + session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions']['status'] + == 'Unsubscribed hypervisor' ) - assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' - hypervisor_display_name = session.contenthost.search(hypervisor_name)[0]['Name'] - vdc_physical = f'product_id = {settings.virtwho.sku.vdc_physical} and type=NORMAL' - vdc_virtual = f'product_id = {settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED' session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) assert session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' + assert ( + session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] + == 'Unentitled' + ) session.contenthost.add_subscription(guest_name, vdc_virtual) assert session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' diff --git a/tests/foreman/virtwho/ui/test_hyperv.py b/tests/foreman/virtwho/ui/test_hyperv.py index dad6a6121d0..e6dafffd15a 100644 --- a/tests/foreman/virtwho/ui/test_hyperv.py +++ b/tests/foreman/virtwho/ui/test_hyperv.py @@ -55,13 +55,16 @@ def virtwho_config(form_data, target_sat, session): class TestVirtwhoConfigforHyperv: @pytest.mark.tier2 - def test_positive_deploy_configure_by_id(self, default_org, virtwho_config, session, form_data): + @pytest.mark.parametrize('deploy_type', ['id', 'script']) + def test_positive_deploy_configure_by_id_script( + self, default_org, virtwho_config, session, form_data, deploy_type + ): """Verify configure created and deployed with id. :id: c8913398-c5c6-4f2c-bc53-0bbfb158b762 :expectedresults: - 1. Config can be created and deployed by command + 1. Config can be created and deployed by command/script 2. No error msg in /var/log/rhsm/rhsm.log 3. Report is sent to satellite 4. Virtual sku can be generated and attached @@ -73,50 +76,30 @@ def test_positive_deploy_configure_by_id(self, default_org, virtwho_config, sess """ name = form_data['name'] values = session.virtwho_configure.read(name) - command = values['deploy']['command'] - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) + if deploy_type == "id": + command = values['deploy']['command'] + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data['hypervisor_type'], debug=True, org=default_org.label + ) + elif deploy_type == "script": + script = values['deploy']['script'] + hypervisor_name, guest_name = deploy_configure_by_script( + script, form_data['hypervisor_type'], debug=True, org=default_org.label + ) assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' hypervisor_display_name = session.contenthost.search(hypervisor_name)[0]['Name'] vdc_physical = f'product_id = {settings.virtwho.sku.vdc_physical} and type=NORMAL' vdc_virtual = f'product_id = {settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED' - session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) - assert session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' - session.contenthost.add_subscription(guest_name, vdc_virtual) - assert session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' - - @pytest.mark.tier2 - def test_positive_deploy_configure_by_script( - self, default_org, virtwho_config, session, form_data - ): - """Verify configure created and deployed with script. - - :id: b0401417-3a6e-4a54-b8e8-22d290813da3 - - :expectedresults: - 1. Config can be created and deployed by script - 2. No error msg in /var/log/rhsm/rhsm.log - 3. Report is sent to satellite - 4. Virtual sku can be generated and attached - 5. Config can be deleted - - :CaseLevel: Integration - - :CaseImportance: High - """ - name = form_data['name'] - values = session.virtwho_configure.read(name) - script = values['deploy']['script'] - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor_type'], debug=True, org=default_org.label + assert ( + session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions']['status'] + == 'Unsubscribed hypervisor' ) - assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' - hypervisor_display_name = session.contenthost.search(hypervisor_name)[0]['Name'] - vdc_physical = f'product_id = {settings.virtwho.sku.vdc_physical} and type=NORMAL' - vdc_virtual = f'product_id = {settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED' session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) assert session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' + assert ( + session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] + == 'Unentitled' + ) session.contenthost.add_subscription(guest_name, vdc_virtual) assert session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' diff --git a/tests/foreman/virtwho/ui/test_kubevirt.py b/tests/foreman/virtwho/ui/test_kubevirt.py index 57d7f278e03..20c5e6170e3 100644 --- a/tests/foreman/virtwho/ui/test_kubevirt.py +++ b/tests/foreman/virtwho/ui/test_kubevirt.py @@ -53,13 +53,16 @@ def virtwho_config(form_data, target_sat, session): class TestVirtwhoConfigforKubevirt: @pytest.mark.tier2 - def test_positive_deploy_configure_by_id(self, default_org, virtwho_config, session, form_data): + @pytest.mark.parametrize('deploy_type', ['id', 'script']) + def test_positive_deploy_configure_by_id_script( + self, default_org, virtwho_config, session, form_data, deploy_type + ): """Verify configure created and deployed with id. :id: 7b2a1b08-f33c-44f4-ad2e-317b6c44b938 :expectedresults: - 1. Config can be created and deployed by command + 1. Config can be created and deployed by command/script 2. No error msg in /var/log/rhsm/rhsm.log 3. Report is sent to satellite 4. Virtual sku can be generated and attached @@ -71,50 +74,30 @@ def test_positive_deploy_configure_by_id(self, default_org, virtwho_config, sess """ name = form_data['name'] values = session.virtwho_configure.read(name) - command = values['deploy']['command'] - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) + if deploy_type == "id": + command = values['deploy']['command'] + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data['hypervisor_type'], debug=True, org=default_org.label + ) + elif deploy_type == "script": + script = values['deploy']['script'] + hypervisor_name, guest_name = deploy_configure_by_script( + script, form_data['hypervisor_type'], debug=True, org=default_org.label + ) assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' hypervisor_display_name = session.contenthost.search(hypervisor_name)[0]['Name'] vdc_physical = f'product_id = {settings.virtwho.sku.vdc_physical} and type=NORMAL' vdc_virtual = f'product_id = {settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED' - session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) - assert session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' - session.contenthost.add_subscription(guest_name, vdc_virtual) - assert session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' - - @pytest.mark.tier2 - def test_positive_deploy_configure_by_script( - self, default_org, virtwho_config, session, form_data - ): - """Verify configure created and deployed with script. - - :id: b3903ccb-04cc-4867-b7ed-d5053d2bfe03 - - :expectedresults: - 1. Config can be created and deployed by script - 2. No error msg in /var/log/rhsm/rhsm.log - 3. Report is sent to satellite - 4. Virtual sku can be generated and attached - 5. Config can be deleted - - :CaseLevel: Integration - - :CaseImportance: High - """ - name = form_data['name'] - values = session.virtwho_configure.read(name) - script = values['deploy']['script'] - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor_type'], debug=True, org=default_org.label + assert ( + session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions']['status'] + == 'Unsubscribed hypervisor' ) - assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' - hypervisor_display_name = session.contenthost.search(hypervisor_name)[0]['Name'] - vdc_physical = f'product_id = {settings.virtwho.sku.vdc_physical} and type=NORMAL' - vdc_virtual = f'product_id = {settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED' session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) assert session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' + assert ( + session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] + == 'Unentitled' + ) session.contenthost.add_subscription(guest_name, vdc_virtual) assert session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' diff --git a/tests/foreman/virtwho/ui/test_libvirt.py b/tests/foreman/virtwho/ui/test_libvirt.py index f75db53cc20..cd90836beb4 100644 --- a/tests/foreman/virtwho/ui/test_libvirt.py +++ b/tests/foreman/virtwho/ui/test_libvirt.py @@ -54,13 +54,16 @@ def virtwho_config(form_data, target_sat, session): class TestVirtwhoConfigforLibvirt: @pytest.mark.tier2 - def test_positive_deploy_configure_by_id(self, default_org, virtwho_config, session, form_data): + @pytest.mark.parametrize('deploy_type', ['id', 'script']) + def test_positive_deploy_configure_by_id_script( + self, default_org, virtwho_config, session, form_data, deploy_type + ): """Verify configure created and deployed with id. :id: ae37ea79-f99c-4511-ace9-a7de26d6db40 :expectedresults: - 1. Config can be created and deployed by command + 1. Config can be created and deployed by command/script 2. No error msg in /var/log/rhsm/rhsm.log 3. Report is sent to satellite 4. Virtual sku can be generated and attached @@ -72,50 +75,30 @@ def test_positive_deploy_configure_by_id(self, default_org, virtwho_config, sess """ name = form_data['name'] values = session.virtwho_configure.read(name) - command = values['deploy']['command'] - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) + if deploy_type == "id": + command = values['deploy']['command'] + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data['hypervisor_type'], debug=True, org=default_org.label + ) + elif deploy_type == "script": + script = values['deploy']['script'] + hypervisor_name, guest_name = deploy_configure_by_script( + script, form_data['hypervisor_type'], debug=True, org=default_org.label + ) assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' hypervisor_display_name = session.contenthost.search(hypervisor_name)[0]['Name'] vdc_physical = f'product_id = {settings.virtwho.sku.vdc_physical} and type=NORMAL' vdc_virtual = f'product_id = {settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED' - session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) - assert session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' - session.contenthost.add_subscription(guest_name, vdc_virtual) - assert session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' - - @pytest.mark.tier2 - def test_positive_deploy_configure_by_script( - self, default_org, virtwho_config, session, form_data - ): - """Verify configure created and deployed with script. - - :id: 3655a501-ab05-4724-945a-7f6e6878091d - - :expectedresults: - 1. Config can be created and deployed by script - 2. No error msg in /var/log/rhsm/rhsm.log - 3. Report is sent to satellite - 4. Virtual sku can be generated and attached - 5. Config can be deleted - - :CaseLevel: Integration - - :CaseImportance: High - """ - name = form_data['name'] - values = session.virtwho_configure.read(name) - script = values['deploy']['script'] - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor_type'], debug=True, org=default_org.label + assert ( + session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions']['status'] + == 'Unsubscribed hypervisor' ) - assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' - hypervisor_display_name = session.contenthost.search(hypervisor_name)[0]['Name'] - vdc_physical = f'product_id = {settings.virtwho.sku.vdc_physical} and type=NORMAL' - vdc_virtual = f'product_id = {settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED' session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) assert session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' + assert ( + session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] + == 'Unentitled' + ) session.contenthost.add_subscription(guest_name, vdc_virtual) assert session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' diff --git a/tests/foreman/virtwho/ui/test_nutanix.py b/tests/foreman/virtwho/ui/test_nutanix.py index fca28223f50..35c1db88385 100644 --- a/tests/foreman/virtwho/ui/test_nutanix.py +++ b/tests/foreman/virtwho/ui/test_nutanix.py @@ -59,7 +59,10 @@ def virtwho_config(form_data, target_sat, session): class TestVirtwhoConfigforNutanix: @pytest.mark.tier2 - def test_positive_deploy_configure_by_id(self, default_org, virtwho_config, session, form_data): + @pytest.mark.parametrize('deploy_type', ['id', 'script']) + def test_positive_deploy_configure_by_id_script( + self, default_org, virtwho_config, session, form_data, deploy_type + ): """Verify configure created and deployed with id. :id: becea4d0-db4e-4a85-93d2-d40e86da0e2f @@ -77,50 +80,30 @@ def test_positive_deploy_configure_by_id(self, default_org, virtwho_config, sess """ name = form_data['name'] values = session.virtwho_configure.read(name) - command = values['deploy']['command'] - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) + if deploy_type == "id": + command = values['deploy']['command'] + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data['hypervisor_type'], debug=True, org=default_org.label + ) + elif deploy_type == "script": + script = values['deploy']['script'] + hypervisor_name, guest_name = deploy_configure_by_script( + script, form_data['hypervisor_type'], debug=True, org=default_org.label + ) assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' hypervisor_display_name = session.contenthost.search(hypervisor_name)[0]['Name'] vdc_physical = f'product_id = {settings.virtwho.sku.vdc_physical} and type=NORMAL' vdc_virtual = f'product_id = {settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED' - session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) - assert session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' - session.contenthost.add_subscription(guest_name, vdc_virtual) - assert session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' - - @pytest.mark.tier2 - def test_positive_deploy_configure_by_script( - self, default_org, virtwho_config, session, form_data - ): - """Verify configure created and deployed with script. - - :id: 1c1b19c9-988c-4b86-a2b2-658fded10ccb - - :expectedresults: - 1. Config can be created and deployed by script - 2. No error msg in /var/log/rhsm/rhsm.log - 3. Report is sent to satellite - 4. Virtual sku can be generated and attached - 5. Config can be deleted - - :CaseLevel: Integration - - :CaseImportance: High - """ - name = form_data['name'] - values = session.virtwho_configure.read(name) - script = values['deploy']['script'] - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor_type'], debug=True, org=default_org.label + assert ( + session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions']['status'] + == 'Unsubscribed hypervisor' ) - assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' - hypervisor_display_name = session.contenthost.search(hypervisor_name)[0]['Name'] - vdc_physical = f'product_id = {settings.virtwho.sku.vdc_physical} and type=NORMAL' - vdc_virtual = f'product_id = {settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED' session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) assert session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' + assert ( + session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] + == 'Unentitled' + ) session.contenthost.add_subscription(guest_name, vdc_virtual) assert session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' From 8fe2e89654fbb9d54ae2ea5ea9a9da3c55d8bcd9 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 6 Jul 2023 02:59:05 -0400 Subject: [PATCH 060/586] [6.14.z] update checktime format for contenthost (#11814) update checktime format for contenthost (#11811) (cherry picked from commit 974b3461bb270a902e6b94316eabfcf36efdd7d2) Co-authored-by: yanpliu --- tests/foreman/virtwho/ui/test_esx.py | 2 +- tests/foreman/virtwho/ui/test_esx_sca.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/foreman/virtwho/ui/test_esx.py b/tests/foreman/virtwho/ui/test_esx.py index 8a49ba6250f..c88897294d4 100644 --- a/tests/foreman/virtwho/ui/test_esx.py +++ b/tests/foreman/virtwho/ui/test_esx.py @@ -686,7 +686,7 @@ def test_positive_last_checkin_status(self, default_org, virtwho_config, form_da # 10 mins margin to check the Last Checkin time assert ( abs( - datetime.strptime(checkin_time, "%B %d, %Y, %I:%M %p") + datetime.strptime(checkin_time, "%B %d, %Y at %I:%M %p") .replace(year=datetime.utcnow().year) .timestamp() - time_now.timestamp() diff --git a/tests/foreman/virtwho/ui/test_esx_sca.py b/tests/foreman/virtwho/ui/test_esx_sca.py index 90dc761cc2f..d662bf0b807 100644 --- a/tests/foreman/virtwho/ui/test_esx_sca.py +++ b/tests/foreman/virtwho/ui/test_esx_sca.py @@ -334,7 +334,7 @@ def test_positive_last_checkin_status( # 10 mins margin to check the Last Checkin time assert ( abs( - datetime.strptime(checkin_time, "%B %d, %Y, %I:%M %p") + datetime.strptime(checkin_time, "%B %d, %Y at %I:%M %p") .replace(year=datetime.utcnow().year) .timestamp() - time_now.timestamp() From c2ec5ac16bec059aede7ca9cc7083e6e72074189 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 6 Jul 2023 02:59:47 -0400 Subject: [PATCH 061/586] [6.14.z] virtwho-config:Update whitelist&blacklist (#11816) virtwho-config:Update whitelist&blacklist (#11809) (cherry picked from commit e904c4a9e0ff8dad169b647a1302dd562fe9068f) Co-authored-by: yanpliu --- tests/foreman/virtwho/api/test_esx_sca.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/foreman/virtwho/api/test_esx_sca.py b/tests/foreman/virtwho/api/test_esx_sca.py index ada886ea4ec..2388d0e924e 100644 --- a/tests/foreman/virtwho/api/test_esx_sca.py +++ b/tests/foreman/virtwho/api/test_esx_sca.py @@ -266,7 +266,7 @@ def test_positive_filter_option( assert get_configure_option('filter_hosts', config_file) == regex assert ( get_configure_option('filter_host_parents', config_file) - == whitelist['filter_host_parents'] + == whitelist['whitelist'] ) assert result.whitelist == regex assert result.filter_host_parents == regex @@ -275,7 +275,7 @@ def test_positive_filter_option( assert get_configure_option('exclude_hosts', config_file) == regex assert ( get_configure_option('exclude_host_parents', config_file) - == blacklist['exclude_host_parents'] + == blacklist['blacklist'] ) assert result.blacklist == regex assert result.exclude_host_parents == regex From 9df8854f4d15ba3cd2a012d4554c73f6ae08e238 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 6 Jul 2023 03:00:35 -0400 Subject: [PATCH 062/586] [6.14.z] add Legacy content UI support for nutanix prism central & ahv option modification (#11819) add Legacy content UI support for nutanix prism central & ahv option modification (#11812) (cherry picked from commit c0bd75c9f0a643ecf4a98c56579d17e4d8dcf0fe) Co-authored-by: yanpliu --- tests/foreman/virtwho/ui/test_nutanix.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/foreman/virtwho/ui/test_nutanix.py b/tests/foreman/virtwho/ui/test_nutanix.py index 35c1db88385..db04c6b6eea 100644 --- a/tests/foreman/virtwho/ui/test_nutanix.py +++ b/tests/foreman/virtwho/ui/test_nutanix.py @@ -181,8 +181,18 @@ def test_positive_prism_central_deploy_configure_by_id_script( hypervisor_display_name = session.contenthost.search(hypervisor_name)[0]['Name'] vdc_physical = f'product_id = {settings.virtwho.sku.vdc_physical} and type=NORMAL' vdc_virtual = f'product_id = {settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED' + assert ( + session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions'][ + 'status' + ] + == 'Unsubscribed hypervisor' + ) session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) assert session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' + assert ( + session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] + == 'Unentitled' + ) session.contenthost.add_subscription(guest_name, vdc_virtual) assert session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' session.virtwho_configure.delete(name) @@ -263,7 +273,7 @@ def test_positive_ahv_internal_debug_option( assert check_message_in_rhsm_log(message) == message # Update ahv_internal_debug option to true - session.virtwho_configure.edit(name, {'ahv-internal-debug': True}) + session.virtwho_configure.edit(name, {'ahv_internal_debug': True}) results = session.virtwho_configure.read(name) command = results['deploy']['command'] assert str(results['overview']['ahv_internal_debug']) == 'True' From bdf87c41c1b97f72348131c899368b9655c908f1 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 7 Jul 2023 04:38:42 -0400 Subject: [PATCH 063/586] [6.14.z] [6.14 RFE] Upgrade test for enabled repos in sca (#11821) [6.14 RFE] Upgrade test for enabled repos in sca (#11729) * upgrade testing for enabled repos in sca * updated pytest mark * addressing comments * added class and made small changes * added function fixtures * removed old comments (cherry picked from commit 66c307d504bf565fd1fe3eb60389a3946de943be) Co-authored-by: Cole Higgins --- tests/upgrades/test_repository.py | 102 ++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/tests/upgrades/test_repository.py b/tests/upgrades/test_repository.py index c221336d925..4a54bfbcab5 100644 --- a/tests/upgrades/test_repository.py +++ b/tests/upgrades/test_repository.py @@ -203,3 +203,105 @@ def test_post_scenario_custom_repo_check(self, target_sat, pre_upgrade_data): )[0] result = rhel_client.execute(f'yum install -y {FAKE_4_CUSTOM_PACKAGE_NAME}') assert result.status == 0 + + +class TestScenarioCustomRepoOverrideCheck: + """Scenario test to verify that repositories in a non-sca org set to "Enabled" + should be overridden to "Enabled(Override)" when upgrading to 6.14. + + Test Steps: + + 1. Before Satellite upgrade. + 2. Create new Organization, Location. + 3. Create Product, Custom Repository, Content view. + 4. Create Activation Key and add Subscription. + 5. Create a Content Host, register it, and check Repository Sets for enabled Repository. + 6. Upgrade Satellite. + 7. Search Host to verify Repository Set is set to Enabled(Override). + + BZ: 1265120 + """ + + @pytest.mark.pre_upgrade + def test_pre_scenario_custom_repo_sca_toggle( + self, + target_sat, + function_org, + function_product, + function_lce, + sat_upgrade_chost, + save_test_data, + default_location, + ): + """This is a pre-upgrade scenario test to verify that repositories in a non-sca org + set to "Enabled" should be overridden to "Enabled(Override)" when upgrading to 6.14. + + :id: preupgrade-65e1e312-a743-4605-b226-f580f523377f + + :steps: + 1. Before Satellite upgrade. + 2. Create new Organization, Location. + 3. Create Product, Custom Repository, Content view. + 4. Create Activation Key and add Subscription. + 5. Create a Content Host, register it, and check Repository Sets for enabled Repository. + + :expectedresults: + + 1. Custom Repository is created. + 2. Custom Repository is enabled on Host. + + """ + repo = target_sat.api.Repository( + product=function_product.id, url=settings.repos.yum_1.url + ).create() + repo.sync() + content_view = target_sat.publish_content_view(function_org, repo) + content_view.version[0].promote(data={'environment_ids': function_lce.id}) + ak = target_sat.api.ActivationKey( + content_view=content_view, organization=function_org.id, environment=function_lce + ).create() + if not target_sat.is_sca_mode_enabled(function_org.id): + subscription = target_sat.api.Subscription(organization=function_org).search( + query={'search': f'name={function_product.name}'} + )[0] + ak.add_subscriptions(data={'subscription_id': subscription.id}) + sat_upgrade_chost.register(function_org, default_location, ak.name, target_sat) + product_details = sat_upgrade_chost.execute('subscription-manager repos --list') + assert 'Enabled: 1' in product_details.stdout + + save_test_data( + { + 'rhel_client': sat_upgrade_chost.hostname, + 'org_name': function_org.name, + 'product_name': function_product.name, + 'repo_name': repo.name, + 'product_details': product_details.stdout, + } + ) + + @pytest.mark.post_upgrade(depend_on=test_pre_scenario_custom_repo_sca_toggle) + def test_post_scenario_custom_repo_sca_toggle(self, pre_upgrade_data): + """This is a post-upgrade scenario test to verify that repositories in a non-sca + Organization set to "Enabled" should be overridden to "Enabled(Override)" + when upgrading to 6.14. + + :id: postupgrade-cc392ce3-f3bb-4cf3-afd5-c062e3a5d109 + + :steps: + 1. After upgrade, search Host to verify Repository Set is set to + Enabled(Override). + + + :expectedresults: Repository on Host should be overridden. + + """ + client_hostname = pre_upgrade_data.get('rhel_client') + org_name = pre_upgrade_data.get('org_name') + product_name = pre_upgrade_data.get('product_name') + repo_name = pre_upgrade_data.get('repo_name') + rhel_client = Broker(host_class=ContentHost).from_inventory( + filter=f'@inv.hostname == "{client_hostname}"' + )[0] + result = rhel_client.execute('subscription-manager repo-override --list') + assert 'enabled: 1' in result.stdout + assert f'{org_name}_{product_name}_{repo_name}' in result.stdout From 5d5aca740f4d3bce9408fd610d1c6d4e0f5de671 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 7 Jul 2023 10:53:56 -0400 Subject: [PATCH 064/586] [6.14.z] Tweaks for the Satellite version properties (#11825) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tweaks for the Satellite version properties (#11572) * Revert "Handle stream versions in version cached_property (#11467)" This reverts commit 8ece887e35f79053143bd70dc9cdb552c9e07a8a. * Tweaks for the Satellite version properties * Use RPM query format option to get package tags * Fix usage of Satellite's version (cherry picked from commit 2cd91eea5be35d7d58c021e5b7cf9142f1ac785c) Co-authored-by: Ondřej Gajdušek --- pytest_fixtures/component/provision_pxe.py | 6 +-- robottelo/hosts.py | 44 +++++++++++----------- tests/foreman/cli/test_host.py | 2 +- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/pytest_fixtures/component/provision_pxe.py b/pytest_fixtures/component/provision_pxe.py index 32175b4c8d4..aaa9837b810 100644 --- a/pytest_fixtures/component/provision_pxe.py +++ b/pytest_fixtures/component/provision_pxe.py @@ -152,12 +152,12 @@ def module_provisioning_sat( provisioning_domain_name = f"{gen_string('alpha').lower()}.foo" broker_data_out = Broker().execute( - workflow="configure-install-sat-provisioning-rhv", - artifacts="last", + workflow='configure-install-sat-provisioning-rhv', + artifacts='last', target_vlan_id=settings.provisioning.vlan_id, target_host=sat.name, provisioning_dns_zone=provisioning_domain_name, - sat_version=sat.version, + sat_version='stream' if sat.is_stream else sat.version, ) broker_data_out = Box(**broker_data_out['data_out']) diff --git a/robottelo/hosts.py b/robottelo/hosts.py index a9b39d56b44..bfa02f7f83a 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1360,6 +1360,8 @@ def update_host_location(self, location): class Capsule(ContentHost, CapsuleMixins): rex_key_path = '~foreman-proxy/.ssh/id_rsa_foreman_proxy.pub' + product_rpm_name = 'satellite-capsule' + upstream_rpm_name = 'foreman-proxy' @property def nailgun_capsule(self): @@ -1406,15 +1408,25 @@ def is_upstream(self): :return: True if no downstream satellite RPMS are installed :rtype: bool """ - return self.execute('rpm -q satellite-capsule &>/dev/null').status != 0 + return self.execute(f'rpm -q {self.product_rpm_name}').status != 0 + + @cached_property + def is_stream(self): + """Check if the Capsule is a stream release or not + + :return: True if the Capsule is a stream release + :rtype: bool + """ + if self.is_upstream: + return False + return ( + 'stream' in self.execute(f'rpm -q --qf "%{{RELEASE}}" {self.product_rpm_name}').stdout + ) @cached_property def version(self): - if not self.is_upstream: - version = self.execute('rpm -q satellite-capsule').stdout - return 'stream' if 'stream' in version else version.split('-')[2] - else: - return 'upstream' + rpm_name = self.upstream_rpm_name if self.is_upstream else self.product_rpm_name + return self.execute(f'rpm -q --qf "%{{VERSION}}" {rpm_name}').stdout @cached_property def url(self): @@ -1565,6 +1577,9 @@ def cli(self): class Satellite(Capsule, SatelliteMixins): + product_rpm_name = 'satellite' + upstream_rpm_name = 'foreman' + def __init__(self, hostname=None, **kwargs): hostname = hostname or settings.server.hostname # instance attr set by broker.Host self.omitting_credentials = False @@ -1675,23 +1690,6 @@ def satellite(self): return self return self._satellite - @cached_property - def is_upstream(self): - """Figure out which product distribution is installed on the server. - - :return: True if no downstream satellite RPMS are installed - :rtype: bool - """ - return self.execute('rpm -q satellite &>/dev/null').status != 0 - - @cached_property - def version(self): - if not self.is_upstream: - version = self.execute('rpm -q satellite').stdout - return 'stream' if 'stream' in version else version.split('-')[1] - else: - return 'upstream' - def is_remote_db(self): return ( self.execute(f'grep "db_manage: false" {constants.SATELLITE_ANSWER_FILE}').status == 0 diff --git a/tests/foreman/cli/test_host.py b/tests/foreman/cli/test_host.py index 6cbfead3f88..bb8abb3c2b1 100644 --- a/tests/foreman/cli/test_host.py +++ b/tests/foreman/cli/test_host.py @@ -917,7 +917,7 @@ def test_positive_list_with_nested_hostgroup(target_sat): logger.info(f'Host info: {host}') assert host['operating-system']['medium'] == options.medium.name assert host['operating-system']['partition-table'] == options.ptable.name # inherited - if not is_open('BZ:2215294') or target_sat.version != 'stream': + if not is_open('BZ:2215294') or not target_sat.is_stream: assert 'id' in host['content-information']['lifecycle-environment'] assert int(host['content-information']['lifecycle-environment']['id']) == int(lce.id) assert int(host['content-information']['content-view']['id']) == int( From 378bf0e683902fee65986b2ee3b97c63190bfe16 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 7 Jul 2023 16:43:13 -0400 Subject: [PATCH 065/586] [6.14.z] Test exercising new Virtualization card (#11834) --- conf/vmware.yaml.template | 7 ++ .../foreman/ui/test_computeresource_vmware.py | 111 ++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/conf/vmware.yaml.template b/conf/vmware.yaml.template index b861b1aed80..72b333cded6 100644 --- a/conf/vmware.yaml.template +++ b/conf/vmware.yaml.template @@ -8,8 +8,15 @@ VMWARE: PASSWORD: # vmware datacenter DATACENTER: + # cluster: vmware cluster + CLUSTER: # Name of VM that should be used VM_NAME: + # mac_address: Mac address of the vm + MAC_ADDRESS: + # hypervisor: IP address of the hypervisor + HYPERVISOR: + # Vmware Compute resource image data # Operating system of the image diff --git a/tests/foreman/ui/test_computeresource_vmware.py b/tests/foreman/ui/test_computeresource_vmware.py index dd323bd262c..9a51147201d 100644 --- a/tests/foreman/ui/test_computeresource_vmware.py +++ b/tests/foreman/ui/test_computeresource_vmware.py @@ -29,6 +29,7 @@ from robottelo.config import settings from robottelo.constants import COMPUTE_PROFILE_LARGE +from robottelo.constants import DEFAULT_LOC from robottelo.constants import FOREMAN_PROVIDERS from robottelo.constants import VMWARE_CONSTANTS from robottelo.utils.datafactory import gen_string @@ -103,6 +104,9 @@ def module_vmware_settings(): image_username=settings.vmware.image_username, image_password=settings.vmware.image_password, vm_name=settings.vmware.vm_name, + cluster=settings.vmware.cluster, + mac_address=settings.vmware.mac_address, + hypervisor=settings.vmware.hypervisor, ) if 'INTERFACE' in settings.vmware: ret['interface'] = VMWARE_CONSTANTS['network_interfaces'] % settings.vmware.interface @@ -501,3 +505,110 @@ def test_positive_access_vmware_with_custom_profile(session, module_vmware_setti if key in expected_disk_value } assert provided_disk_value == expected_disk_value + + +@pytest.mark.tier2 +def test_positive_virt_card( + session, target_sat, module_vmware_settings, module_location, module_org +): + """Check to see that the Virtualization card appears for an imported VM + + :id: 0502d5a6-64c1-422f-a9ba-ac7c2ee7bad2 + + :parametrized: no + + :expectedresults: Virtualization card appears in the new Host UI for the VM + + :CaseLevel: Integration + + :CaseImportance: Medium + """ + # create entities for hostgroup + default_loc_id = ( + target_sat.api.Location().search(query={'search': f'name="{DEFAULT_LOC}"'})[0].id + ) + target_sat.api.SmartProxy(id=1, location=[default_loc_id, module_location.id]).update() + domain = target_sat.api.Domain( + organization=[module_org.id], location=[module_location] + ).create() + subnet = target_sat.api.Subnet( + organization=[module_org.id], location=[module_location], domain=[domain] + ).create() + architecture = target_sat.api.Architecture().create() + ptable = target_sat.api.PartitionTable( + organization=[module_org.id], location=[module_location] + ).create() + operatingsystem = target_sat.api.OperatingSystem( + architecture=[architecture], ptable=[ptable] + ).create() + medium = target_sat.api.Media( + organization=[module_org.id], location=[module_location], operatingsystem=[operatingsystem] + ).create() + lce = ( + target_sat.api.LifecycleEnvironment(name="Library", organization=module_org.id) + .search()[0] + .read() + .id + ) + cv = target_sat.api.ContentView(organization=module_org).create() + cv.publish() + + # create hostgroup + hostgroup_name = gen_string('alpha') + target_sat.api.HostGroup( + name=hostgroup_name, + architecture=architecture, + domain=domain, + subnet=subnet, + location=[module_location.id], + medium=medium, + operatingsystem=operatingsystem, + organization=[module_org], + ptable=ptable, + lifecycle_environment=lce, + content_view=cv, + content_source=1, + ).create() + cr_name = gen_string('alpha') + with session: + session.computeresource.create( + { + 'name': cr_name, + 'provider': FOREMAN_PROVIDERS['vmware'], + 'provider_content.vcenter': module_vmware_settings['vcenter'], + 'provider_content.user': module_vmware_settings['user'], + 'provider_content.password': module_vmware_settings['password'], + 'provider_content.datacenter.value': module_vmware_settings['datacenter'], + 'locations.resources.assigned': [module_location.name], + 'organizations.resources.assigned': [module_org.name], + } + ) + session.hostgroup.update(hostgroup_name, {'host_group.deploy': cr_name + " (VMware)"}) + session.computeresource.vm_import( + cr_name, + module_vmware_settings['vm_name'], + hostgroup_name, + module_location.name, + module_org.name, + module_vmware_settings['vm_name'], + ) + host_name = module_vmware_settings['vm_name'] + '.' + domain.name + virt_card = session.host_new.get_details(host_name, widget_names='details.virtualization')[ + 'details' + ]['virtualization'] + assert virt_card['datacenter'] == module_vmware_settings['datacenter'] + assert virt_card['cluster'] == module_vmware_settings['cluster'] + assert virt_card['memory'] == '2 GB' + assert virt_card['public_ip_address'] + assert virt_card['mac_address'] == module_vmware_settings['mac_address'] + assert virt_card['cpus'] == '1' + assert virt_card['cores_per_socket'] == '1' + assert virt_card['firmware'] == 'bios' + assert virt_card['hypervisor'] == module_vmware_settings['hypervisor'] + assert virt_card['connection_state'] == 'connected' + assert virt_card['overall_status'] == 'green' + assert virt_card['annotation_notes'] == '' + assert virt_card['running_on'] == cr_name + target_sat.api.Host( + id=target_sat.api.Host().search(query={'search': f'name={host_name}'})[0].id + ).delete() From bad5e73dc9bc06d787ebe5dc2704b2342270ebef Mon Sep 17 00:00:00 2001 From: Adarsh dubey Date: Mon, 10 Jul 2023 20:43:16 +0530 Subject: [PATCH 066/586] [6.14.Z] - Support the --local-files oscap argument (#11836) [6.14.0-Feature] - Support the --local-files oscap argument Signed-off-by: Adarsh Dubey Using sat object Cleanup for redundant and unused code Signed-off-by: Adarsh Dubey Using sat object for cli fixtures Signed-off-by: Adarsh Dubey Using module_target_sat object Signed-off-by: Adarsh Dubey (cherry picked from commit b7b4d65ccb9e8026e9d58fdb60a9a68af6a2e280) --- robottelo/hosts.py | 1 + tests/foreman/longrun/test_oscap.py | 138 +++++++++++++++++++++++++++- 2 files changed, 134 insertions(+), 5 deletions(-) diff --git a/robottelo/hosts.py b/robottelo/hosts.py index bfa02f7f83a..6754d5eb556 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -925,6 +925,7 @@ def execute_foreman_scap_client(self, policy_id=None): f'stdout: {result.stdout} host_data_stdout: {data.stdout}, ' f'and host_data_stderr: {data.stderr}' ) + return result.stdout def configure_rex(self, satellite, org, subnet_id=None, by_ip=True, register=True): """Setup a VM host for remote execution. diff --git a/tests/foreman/longrun/test_oscap.py b/tests/foreman/longrun/test_oscap.py index 907fca35e69..d7cf3e505ca 100644 --- a/tests/foreman/longrun/test_oscap.py +++ b/tests/foreman/longrun/test_oscap.py @@ -123,13 +123,15 @@ def activation_key(module_org, lifecycle_env, content_view): @pytest.fixture(scope='module', autouse=True) -def update_scap_content(module_org): +def update_scap_content(module_org, module_target_sat): """Update default scap contents""" for content in rhel8_content, rhel7_content, rhel6_content: - content = Scapcontent.info({'title': content}, output_format='json') + content = module_target_sat.cli.Scapcontent.info({'title': content}, output_format='json') organization_ids = [content_org['id'] for content_org in content.get('organizations', [])] organization_ids.append(module_org.id) - Scapcontent.update({'title': content['title'], 'organization-ids': organization_ids}) + module_target_sat.cli.Scapcontent.update( + {'title': content['title'], 'organization-ids': organization_ids} + ) @pytest.mark.skip_if_open('BZ:2211437') @@ -153,7 +155,7 @@ def test_positive_oscap_run_via_ansible( 1. Create a valid scap content 2. Import Ansible role theforeman.foreman_scap_client 3. Import Ansible Variables needed for the role - 4. Create a scap policy with anisble as deploy option + 4. Create a scap policy with ansible as deploy option 5. Associate the policy with a hostgroup 6. Provision a host using the hostgroup 7. Configure REX and associate the Ansible role to created host @@ -275,7 +277,7 @@ def test_positive_oscap_run_via_ansible_bz_1814988( 1. Create a valid scap content 2. Import Ansible role theforeman.foreman_scap_client 3. Import Ansible Variables needed for the role - 4. Create a scap policy with anisble as deploy option + 4. Create a scap policy with ansible as deploy option 5. Associate the policy with a hostgroup 6. Provision a host using the hostgroup 7. Harden the host by remediating it with DISA STIG security policy @@ -493,3 +495,129 @@ def test_positive_reporting_emails_of_oscap_reports(): :CaseLevel: System """ + + +@pytest.mark.parametrize('distro', ['rhel8']) +def test_positive_oscap_run_via_local_files( + module_org, default_proxy, content_view, lifecycle_env, distro, module_target_sat +): + """End-to-End Oscap run via local files deployed with ansible + + :id: 0dde5893-540c-4e03-a206-55fccdb2b9ca + + :parametrized: yes + + :customerscenario: true + + :setup: scap content, scap policy , Remote execution + + :steps: + + 1. Create a valid scap content + 2. Import Ansible role theforeman.foreman_scap_client + 3. Create a scap policy with ansible as deploy option + 4. Associate the policy with a hostgroup + 5. Run the Ansible job and then trigger the Oscap job. + 6. Oscap must Utilize the local files for the client scan. + + :expectedresults: Oscap run should happen using the --localfile argument. + + :BZ: 2081777,2211952 + + :CaseImportance: Critical + """ + SELECTED_ROLE = 'theforeman.foreman_scap_client' + file_name = 'security-data-oval-com.redhat.rhsa-RHEL8.xml.bz2' + download_url = 'https://www.redhat.com/security/data/oval/com.redhat.rhsa-RHEL8.xml.bz2' + profile = OSCAP_PROFILE['ospp8'] + content = OSCAP_DEFAULT_CONTENT[f'{distro}_content'] + hgrp_name = gen_string('alpha') + policy_name = gen_string('alpha') + + module_target_sat.cli_factory.make_hostgroup( + { + 'content-source-id': default_proxy, + 'name': hgrp_name, + 'organizations': module_org.name, + } + ) + # Creates oscap_policy. + scap_id, scap_profile_id = fetch_scap_and_profile_id(content, profile) + with Broker( + nick=distro, + host_class=ContentHost, + deploy_flavor=settings.flavors.default, + ) as vm: + vm.create_custom_repos( + **{ + 'baseos': settings.repos.rhel8_os.baseos, + 'appstream': settings.repos.rhel8_os.appstream, + 'sat_client': settings.repos['SATCLIENT_REPO'][distro.upper()], + } + ) + result = vm.register(module_org, None, ak_name[distro], module_target_sat) + assert result.status == 0, f'Failed to register host: {result.stderr}' + proxy_id = module_target_sat.nailgun_smart_proxy.id + target_host = vm.nailgun_host + module_target_sat.api.AnsibleRoles().sync( + data={'proxy_id': proxy_id, 'role_names': [SELECTED_ROLE]} + ) + role_id = ( + module_target_sat.api.AnsibleRoles() + .search(query={'search': f'name={SELECTED_ROLE}'})[0] + .id + ) + module_target_sat.api.Host(id=target_host.id).add_ansible_role( + data={'ansible_role_id': role_id} + ) + host_roles = target_host.list_ansible_roles() + assert host_roles[0]['name'] == SELECTED_ROLE + module_target_sat.cli_factory.make_scap_policy( + { + 'scap-content-id': scap_id, + 'hostgroups': hgrp_name, + 'deploy-by': 'ansible', + 'name': policy_name, + 'period': OSCAP_PERIOD['weekly'].lower(), + 'scap-content-profile-id': scap_profile_id, + 'weekday': OSCAP_WEEKDAY['friday'].lower(), + 'organizations': module_org.name, + } + ) + # The file here needs to be present on the client in order + # to perform the scan from the local-files. + vm.execute(f'curl -o {file_name} {download_url}') + module_target_sat.cli.Host.update( + { + 'name': vm.hostname, + 'lifecycle-environment': lifecycle_env.name, + 'content-view': content_view.name, + 'hostgroup': hgrp_name, + 'openscap-proxy-id': default_proxy, + 'organization': module_org.name, + } + ) + + template_id = ( + module_target_sat.api.JobTemplate() + .search(query={'search': 'name="Ansible Roles - Ansible Default"'})[0] + .id + ) + job = module_target_sat.api.JobInvocation().run( + synchronous=False, + data={ + 'job_template_id': template_id, + 'targeting_type': 'static_query', + 'search_query': f'name = {vm.hostname}', + }, + ) + module_target_sat.wait_for_tasks( + f'resource_type = JobInvocation and resource_id = {job["id"]}', + poll_timeout=1000, + ) + assert module_target_sat.api.JobInvocation(id=job['id']).read().succeeded == 1 + assert vm.run('cat /etc/foreman_scap_client/config.yaml | grep profile').status == 0 + # Runs the actual oscap scan on the vm/clients + # TODO: instead of running it on the client itself we should invoke a job from satellite + result = vm.execute_foreman_scap_client() + assert f"WARNING: Using local file '/root/{file_name}'" in result From 4875418a1630dce9e6e3e53ab594a93242d959f9 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 11 Jul 2023 03:25:01 -0400 Subject: [PATCH 067/586] [6.14.z] [6.14 RFE] Custom products disabled by default (#11822) [6.14 RFE] Custom products disabled by default (#11380) * applying stash * added UI test for prt job * saving point for new host testing * latest changes * removed api tests and fixed ui * updated host call * updated comments and docstring * updated test to override status * final push for review * updated docstring * addressing comments 1 * address comments for ak and quotes * adressing comments and updating tests * updated case level (cherry picked from commit d9959538feddda7f07c4759e3c8ce14df1eccf48) Co-authored-by: Cole Higgins --- pytest_fixtures/component/repository.py | 24 ++++++++++ tests/foreman/cli/test_repositories.py | 47 +++++++++++++++++++ tests/foreman/ui/test_repositories.py | 60 +++++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 tests/foreman/cli/test_repositories.py create mode 100644 tests/foreman/ui/test_repositories.py diff --git a/pytest_fixtures/component/repository.py b/pytest_fixtures/component/repository.py index 87164fe5f32..43861e07716 100644 --- a/pytest_fixtures/component/repository.py +++ b/pytest_fixtures/component/repository.py @@ -91,6 +91,30 @@ def repo_setup(): yield details +@pytest.fixture(scope='module') +def setup_content(module_org): + """This fixture is used to setup an activation key with a custom product attached. Used for + registering a host + """ + org = module_org + custom_repo = entities.Repository( + product=entities.Product(organization=org).create(), + ).create() + custom_repo.sync() + lce = entities.LifecycleEnvironment(organization=org).create() + cv = entities.ContentView( + organization=org, + repository=[custom_repo.id], + ).create() + cv.publish() + cvv = cv.read().version[0].read() + cvv.promote(data={'environment_ids': lce.id, 'force': False}) + ak = entities.ActivationKey( + content_view=cv, max_hosts=100, organization=org, environment=lce, auto_attach=True + ).create() + return ak, org, custom_repo + + @pytest.fixture(scope='module') def module_repository(os_path, module_product, module_target_sat): repo = module_target_sat.api.Repository(product=module_product, url=os_path).create() diff --git a/tests/foreman/cli/test_repositories.py b/tests/foreman/cli/test_repositories.py new file mode 100644 index 00000000000..c6bd95c302b --- /dev/null +++ b/tests/foreman/cli/test_repositories.py @@ -0,0 +1,47 @@ +"""Test module for Repositories CLI. + +:Requirement: Repository + +:CaseAutomation: Automated + +:CaseLevel: Component + +:CaseComponent: Repositories + +:team: Phoenix-content + +:TestType: Functional + +:CaseImportance: Critical + +:Upstream: No +""" +import pytest + + +@pytest.mark.rhel_ver_match('[^6]') +def test_positive_custom_products_disabled_by_default( + setup_content, + default_location, + rhel_contenthost, + target_sat, +): + """Verify that custom products should be disabled by default for content hosts + + :id: ba237e11-3b41-49e3-94b3-63e1f404d9e5 + + :steps: + 1. Create custom product and upload repository + 2. Attach to activation key + 3. Register Host + 4. Assert that custom proudcts are disabled by default + + :expectedresults: Custom products should be disabled by default. "Enabled: 0" + + :BZ: 1265120 + """ + ak, org, _ = setup_content + rhel_contenthost.register(org, default_location, ak.name, target_sat) + assert rhel_contenthost.subscribed + product_details = rhel_contenthost.run('subscription-manager repos --list') + assert 'Enabled: 0' in product_details.stdout diff --git a/tests/foreman/ui/test_repositories.py b/tests/foreman/ui/test_repositories.py new file mode 100644 index 00000000000..e5561e7a8a1 --- /dev/null +++ b/tests/foreman/ui/test_repositories.py @@ -0,0 +1,60 @@ +"""Test module for Repositories UI. + +:Requirement: Repository + +:CaseAutomation: Automated + +:CaseLevel: Integration + +:CaseComponent: Repositories + +:team: Phoenix-content + +:TestType: Functional + +:CaseImportance: Critical + +:Upstream: No +""" +import pytest + + +@pytest.mark.rhel_ver_match('[^6]') +def test_positive_custom_products_disabled_by_default( + session, + default_location, + setup_content, + rhel_contenthost, + target_sat, +): + """Verify that custom products should be disabled by default for content hosts + and activation keys + + :id: 05bdf790-a7a1-48b1-bbae-dc25b6ee7d58 + + :steps: + 1. Create custom product and upload repository + 2. Attach to activation key + 3. Register Host + 4. Assert that custom proudcts are disabled by default + + :expectedresults: Custom products should be disabled by default. + + :BZ: 1265120 + + :customerscenario: true + """ + ak, org, custom_repo = setup_content + rhel_contenthost.register(org, default_location, ak.name, target_sat) + assert rhel_contenthost.subscribed + with session: + session.organization.select(org.name) + session.location.select(default_location.name) + ak_details = session.activationkey.read(ak.name, widget_names='repository sets')[ + 'repository sets' + ]['table'][0] + assert 'Disabled' in ak_details['Status'] + repos = session.host_new.get_repo_sets(rhel_contenthost.hostname, custom_repo.name) + assert repos[0]['Repository'] == custom_repo.name + assert repos[0]['Status'] == 'Disabled' + assert repos[0]['Repository type'] == 'Custom' From 36a24a970e7d8a529cf2d3d03e4402844791100d Mon Sep 17 00:00:00 2001 From: Cole Higgins Date: Tue, 11 Jul 2023 04:11:45 -0400 Subject: [PATCH 068/586] [RFE 6.14] [6.14.z] Custom product disabled by default and search on activation keys (#11832) * custom product disabled by default and search (cherry picked from commit b7d7d3a9d9150f9e08954ddfcf878e88a0ff76ca) * addressing comments 1 (cherry picked from commit e59d3487137bb2c2fe8cea743d1e06a30d6600df) * added customer scenario (cherry picked from commit d27842e0dff4ba7e17874a154501adb8057286e2) * addressig comments from airgun and updating test (cherry picked from commit 283f2c90a9cd31e68d118c78b70f8ec642d2de08) --- tests/foreman/ui/test_activationkey.py | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/foreman/ui/test_activationkey.py b/tests/foreman/ui/test_activationkey.py index f6421c8c0b1..bf6d39f5870 100644 --- a/tests/foreman/ui/test_activationkey.py +++ b/tests/foreman/ui/test_activationkey.py @@ -1227,3 +1227,38 @@ def test_positive_ak_with_custom_product_on_rhel6(session, rhel6_contenthost, ta ak = session.activationkey.read(ak.name, widget_names='content_hosts') assert len(ak['content_hosts']['table']) == 1 assert ak['content_hosts']['table'][0]['Name'] == rhel6_contenthost.hostname + + +def test_positive_custom_products_disabled_by_default_ak( + session, + default_location, + setup_content, +): + """Verify that repositories can be filtered by type and that repositories + are Disabled by default on activation keys + + :id: 3e6793ff-3501-47f6-8cd6-e24a60bdcb73 + + :steps: + 1. Create custom product and upload repository + 2. Attach to activation key + 3. Filter Repositories by "Custom" type + 4. Assert that custom product is visible and disabled by default + 5. Filter Repositories by "Red Hat" type + 6. Assert custom product is not visible + + :expectedresults: Custom products should be filtered and Disabled by default. + + :BZ: 1265120 + + :customerscenario: true + """ + ak, org, custom_repo = setup_content + with session: + session.organization.select(org.name) + session.location.select(default_location.name) + repo1 = session.activationkey.get_repos(ak.name, repo_type="Custom") + assert repo1[0]['Repository Name'] == custom_repo.name + assert repo1[0]['Status'] == 'Disabled' + repo2 = session.activationkey.get_repos(ak.name, repo_type="Red Hat") + assert repo2[0]['Repository Name'] != custom_repo.name From 25e624f3d2d354598aad2e80f9a40ae1c49d77af Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 11 Jul 2023 11:11:07 -0400 Subject: [PATCH 069/586] [6.14.z] Test fixes for Ansible (#11845) --- tests/foreman/ui/test_ansible.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/foreman/ui/test_ansible.py b/tests/foreman/ui/test_ansible.py index 591a5b7480a..162c2e107c5 100644 --- a/tests/foreman/ui/test_ansible.py +++ b/tests/foreman/ui/test_ansible.py @@ -43,8 +43,7 @@ def test_positive_create_and_delete_variable(target_sat): key = gen_string('alpha') role = 'redhat.satellite.activation_keys' with target_sat.ui_session() as session: - if not session.ansibleroles.imported_roles_count: - session.ansibleroles.import_all_roles() + session.ansibleroles.import_all_roles() session.ansiblevariables.create( { 'key': key, @@ -72,8 +71,7 @@ def test_positive_create_variable_with_overrides(target_sat): key = gen_string('alpha') role = 'redhat.satellite.activation_keys' with target_sat.ui_session() as session: - if not session.ansibleroles.imported_roles_count: - session.ansibleroles.import_all_roles() + session.ansibleroles.import_all_roles() session.ansiblevariables.create_with_overrides( { 'key': key, From fc4cd16db1307d7dae1e29f48890996a5ffa8158 Mon Sep 17 00:00:00 2001 From: Peter Ondrejka Date: Fri, 7 Jul 2023 14:35:03 +0200 Subject: [PATCH 070/586] fixes in rex packgage job tests (#11798) * fixes in rex packgage job tests * Update tests/foreman/cli/test_remoteexecution.py Co-authored-by: Gaurav Talreja * Update tests/foreman/cli/test_remoteexecution.py Co-authored-by: Gaurav Talreja --------- Co-authored-by: Gaurav Talreja (cherry picked from commit f3060e0f423924653ed68fbad8a10931c3922aeb) --- tests/foreman/cli/test_remoteexecution.py | 67 ++++++++--------------- 1 file changed, 22 insertions(+), 45 deletions(-) diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index 5119b9fa013..cd3b64654c2 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -278,11 +278,14 @@ def test_positive_run_default_job_template_multiple_hosts_by_ip( assert result['success'] == '2', output_msgs @pytest.mark.tier3 + @pytest.mark.no_containers @pytest.mark.rhel_ver_list([8]) @pytest.mark.skipif( (not settings.robottelo.repos_hosting_url), reason='Missing repos_hosting_url' ) - def test_positive_install_multiple_packages_with_a_job_by_ip(self, rex_contenthost, module_org): + def test_positive_install_multiple_packages_with_a_job_by_ip( + self, rhel_contenthost, module_org, module_ak_with_cv, target_sat + ): """Run job to install several packages on host by ip :id: 8b73033f-83c9-4024-83c3-5e442a79d320 @@ -292,30 +295,15 @@ def test_positive_install_multiple_packages_with_a_job_by_ip(self, rex_contentho :parametrized: yes """ - self.org = module_org - client = rex_contenthost + client = rhel_contenthost packages = ['monkey', 'panda', 'seal'] - # Create a custom repo - repo = entities.Repository( - content_type='yum', - product=entities.Product(organization=self.org).create(), - url=settings.repos.yum_3.url, - ).create() - repo.sync() - prod = repo.product.read() - subs = entities.Subscription(organization=self.org).search( - query={'search': f'name={prod.name}'} + client.register( + module_org, + None, + module_ak_with_cv.name, + target_sat, + repo=settings.repos.yum_3.url, ) - assert len(subs) > 0, 'No subscriptions matching the product returned' - - ak = entities.ActivationKey( - organization=self.org, - content_view=self.org.default_content_view, - environment=self.org.library, - ).create() - ak.add_subscriptions(data={'subscriptions': [{'id': subs[0].id}]}) - client.register_contenthost(org=self.org.label, activation_key=ak.name) - invocation_command = make_job_invocation( { 'job-template': 'Install Package - Katello Script Default', @@ -720,12 +708,15 @@ def test_positive_run_concurrent_jobs(self, registered_hosts, module_org): @pytest.mark.tier3 @pytest.mark.upgrade @pytest.mark.e2e + @pytest.mark.no_containers @pytest.mark.pit_server @pytest.mark.rhel_ver_match('[^6].*') @pytest.mark.skipif( (not settings.robottelo.repos_hosting_url), reason='Missing repos_hosting_url' ) - def test_positive_run_packages_and_services_job(self, rex_contenthost, module_org): + def test_positive_run_packages_and_services_job( + self, rhel_contenthost, module_org, module_ak_with_cv, target_sat + ): """Tests Ansible REX job can install packages and start services :id: 47ed82fb-77ca-43d6-a52e-f62bae5d3a42 @@ -756,29 +747,15 @@ def test_positive_run_packages_and_services_job(self, rex_contenthost, module_or :parametrized: yes """ - self.org = module_org - client = rex_contenthost + client = rhel_contenthost packages = ['tapir'] - # Create a custom repo - repo = entities.Repository( - content_type='yum', - product=entities.Product(organization=self.org).create(), - url=settings.repos.yum_3.url, - ).create() - repo.sync() - prod = repo.product.read() - subs = entities.Subscription(organization=self.org).search( - query={'search': f'name={prod.name}'} + client.register( + module_org, + None, + module_ak_with_cv.name, + target_sat, + repo=settings.repos.yum_3.url, ) - assert len(subs), 'No subscriptions matching the product returned' - ak = entities.ActivationKey( - organization=self.org, - content_view=self.org.default_content_view, - environment=self.org.library, - ).create() - ak.add_subscriptions(data={'subscriptions': [{'id': subs[0].id}]}) - client.register_contenthost(org=self.org.label, activation_key=ak.name) - # install package invocation_command = make_job_invocation( { From a5a61bf523f30d083defa5fc6977c2419136ea14 Mon Sep 17 00:00:00 2001 From: Cole Higgins Date: Tue, 11 Jul 2023 18:15:45 -0400 Subject: [PATCH 071/586] [6.14.z] [RFE 6.14.z]Added test for overriding custom products 6.14.z (#11850) * rebase for merge (cherry picked from commit dc80e2998526e62df0a0d09a35fd4b23ee8811b4) * addressing comments and rebase (cherry picked from commit 115031c3c194f1398ba92598d2047376696ce3a1) --- tests/foreman/ui/test_repositories.py | 43 +++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/foreman/ui/test_repositories.py b/tests/foreman/ui/test_repositories.py index e5561e7a8a1..7ad4ac8efc5 100644 --- a/tests/foreman/ui/test_repositories.py +++ b/tests/foreman/ui/test_repositories.py @@ -58,3 +58,46 @@ def test_positive_custom_products_disabled_by_default( assert repos[0]['Repository'] == custom_repo.name assert repos[0]['Status'] == 'Disabled' assert repos[0]['Repository type'] == 'Custom' + + +@pytest.mark.rhel_ver_match('[^6]') +def test_positive_override_custom_products_on_existing_host( + session, + default_location, + setup_content, + rhel_contenthost, + target_sat, +): + """Verify that custom products can be easily enabled/disabled on existing host + using "select all" method + + :id: a3c9a8c8-a40f-448b-961a-7eb888883ba9 + + :steps: + 1. Create custom product and upload repository + 2. Attach to activation key + 3. Register Host + 4. Assert that custom proudcts are disabled by default + 5. Override custom products to enabled using new functionality + 6. Assert custom products are now enabled + + :expectedresults: Custom products should be easily enabled. + + :parametrized: yes + """ + ak, org, custom_repo = setup_content + client = rhel_contenthost + client.register(org, default_location, ak.name, target_sat) + assert client.subscribed + with session: + session.organization.select(org.name) + session.location.select(default_location.name) + repo = session.host_new.get_repo_sets(rhel_contenthost.hostname, custom_repo.name) + assert repo[0]['Repository'] == custom_repo.name + assert repo[0]['Status'] == 'Disabled' + session.host_new.bulk_override_repo_sets( + rhel_contenthost.hostname, 'Custom', "Override to enabled" + ) + repo = session.host_new.get_repo_sets(rhel_contenthost.hostname, custom_repo.name) + assert repo[0]['Repository'] == custom_repo.name + assert repo[0]['Status'] == 'Enabled' From 2f73912fb9a112022a66e8bb17f35e7ca48da883 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 12 Jul 2023 01:47:14 -0400 Subject: [PATCH 072/586] [6.14.z] [Component Audit][Part1]Modify/Delete deprecated registration method (#11848) --- tests/foreman/cli/test_host.py | 53 +------------------------- tests/foreman/cli/test_registration.py | 23 +++++++++++ 2 files changed, 24 insertions(+), 52 deletions(-) diff --git a/tests/foreman/cli/test_host.py b/tests/foreman/cli/test_host.py index bb8abb3c2b1..a9d5f688b5b 100644 --- a/tests/foreman/cli/test_host.py +++ b/tests/foreman/cli/test_host.py @@ -590,56 +590,6 @@ def test_positive_katello_and_openscap_loaded(): ), f'--{arg} not supported by update subcommand' -@pytest.mark.cli_host_create -@pytest.mark.tier3 -@pytest.mark.upgrade -def test_positive_register_with_no_ak( - module_lce, module_org, module_promoted_cv, rhel7_contenthost, target_sat -): - """Register host to satellite without activation key - - :id: 6a7cedd2-aa9c-4113-a83b-3f0eea43ecb4 - - :expectedresults: Host successfully registered to appropriate org - - :parametrized: yes - - :CaseLevel: System - """ - rhel7_contenthost.install_katello_ca(target_sat) - rhel7_contenthost.register_contenthost( - module_org.label, - lce=f'{module_lce.label}/{module_promoted_cv.label}', - ) - assert rhel7_contenthost.subscribed - - -@pytest.mark.cli_host_create -@pytest.mark.tier3 -def test_negative_register_twice(module_ak_with_cv, module_org, rhel7_contenthost, target_sat): - """Attempt to register a host twice to Satellite - - :id: 0af81129-cd69-4fa7-a128-9e8fcf2d03b1 - - :expectedresults: host cannot be registered twice - - :parametrized: yes - - :CaseLevel: System - """ - rhel7_contenthost.install_katello_ca(target_sat) - rhel7_contenthost.register_contenthost(module_org.label, module_ak_with_cv.name) - assert rhel7_contenthost.subscribed - result = rhel7_contenthost.register_contenthost( - module_org.label, module_ak_with_cv.name, force=False - ) - # Depending on distro version, successful status may be 0 or - # 1, so we can't verify host wasn't registered by status != 0 - # check. Verifying status == 64 here, which stands for content - # host being already registered. - assert result.status == 64 - - @pytest.mark.cli_host_create @pytest.mark.tier3 def test_positive_list_and_unregister( @@ -656,8 +606,7 @@ def test_positive_list_and_unregister( :CaseLevel: System """ - rhel7_contenthost.install_katello_ca(target_sat) - rhel7_contenthost.register_contenthost(module_org.label, module_ak_with_cv.name) + rhel7_contenthost.register(module_org, None, module_ak_with_cv.name, target_sat) assert rhel7_contenthost.subscribed hosts = Host.list({'organization-id': module_org.id}) assert rhel7_contenthost.hostname in [host['name'] for host in hosts] diff --git a/tests/foreman/cli/test_registration.py b/tests/foreman/cli/test_registration.py index dfd26184d35..50102ae2c0e 100644 --- a/tests/foreman/cli/test_registration.py +++ b/tests/foreman/cli/test_registration.py @@ -147,3 +147,26 @@ def test_upgrade_katello_ca_consumer_rpm( result = vm.execute('subscription-manager identity') # Result will be 0 if registered assert result.status == 0 + + +@pytest.mark.rhel_ver_match('[^6]') +@pytest.mark.tier3 +def test_negative_register_twice(module_ak_with_cv, module_org, rhel_contenthost, target_sat): + """Attempt to register a host twice to Satellite + + :id: 0af81129-cd69-4fa7-a128-9e8fcf2d03b1 + + :expectedresults: host cannot be registered twice + + :parametrized: yes + + :CaseLevel: System + """ + rhel_contenthost.register(module_org, None, module_ak_with_cv.name, target_sat) + assert rhel_contenthost.subscribed + result = rhel_contenthost.register( + module_org, None, module_ak_with_cv.name, target_sat, force=False + ) + # host being already registered. + assert result.status == 1 + assert 'This system is already registered' in str(result.stderr) From 393ac1985c783421524f238ae0b1ff963ae1a9fa Mon Sep 17 00:00:00 2001 From: Jameer Pathan Date: Tue, 11 Jul 2023 22:26:00 +0200 Subject: [PATCH 073/586] Fix remote db tests (cherry picked from commit 9e60b913256ce9ecb2ec379012335eb1b2a275dc) --- pytest_fixtures/component/maintain.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/pytest_fixtures/component/maintain.py b/pytest_fixtures/component/maintain.py index 089e95c5023..7add0d13aea 100644 --- a/pytest_fixtures/component/maintain.py +++ b/pytest_fixtures/component/maintain.py @@ -30,34 +30,32 @@ def _finalize(): @pytest.fixture(scope='module') -def module_synced_repos(session_target_sat, session_capsule_configured, module_sca_manifest): - org = session_target_sat.api.Organization().create() - session_target_sat.upload_manifest(org.id, module_sca_manifest.content) +def module_synced_repos(sat_maintain, session_capsule_configured, module_sca_manifest): + org = sat_maintain.api.Organization().create() + sat_maintain.upload_manifest(org.id, module_sca_manifest.content) # sync custom repo - cust_prod = session_target_sat.api.Product(organization=org).create() - cust_repo = session_target_sat.api.Repository( + cust_prod = sat_maintain.api.Product(organization=org).create() + cust_repo = sat_maintain.api.Repository( url=settings.repos.yum_1.url, product=cust_prod ).create() cust_repo.sync() # sync RH repo - product = session_target_sat.api.Product( - name=constants.PRDS['rhae'], organization=org.id - ).search()[0] - r_set = session_target_sat.api.RepositorySet( + product = sat_maintain.api.Product(name=constants.PRDS['rhae'], organization=org.id).search()[0] + r_set = sat_maintain.api.RepositorySet( name=constants.REPOSET['rhae2'], product=product ).search()[0] payload = {'basearch': constants.DEFAULT_ARCHITECTURE, 'product_id': product.id} r_set.enable(data=payload) - result = session_target_sat.api.Repository(name=constants.REPOS['rhae2']['name']).search( + result = sat_maintain.api.Repository(name=constants.REPOS['rhae2']['name']).search( query={'organization_id': org.id} ) rh_repo_id = result[0].id - rh_repo = session_target_sat.api.Repository(id=rh_repo_id).read() + rh_repo = sat_maintain.api.Repository(id=rh_repo_id).read() rh_repo.sync() # assign the Library LCE to the Capsule - lce = session_target_sat.api.LifecycleEnvironment(organization=org).search( + lce = sat_maintain.api.LifecycleEnvironment(organization=org).search( query={'search': f'name={constants.ENVIRONMENT}'} )[0] session_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( From b2c197deb61fb5669d5dce51601267918aeb1f88 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 12 Jul 2023 06:41:59 -0400 Subject: [PATCH 074/586] [6.14.z] Component audit - Modify/Delete CLI OS tests (#11855) Component audit - Modify/Delete CLI OS tests (#11831) component audit- Modify/Delete cli OS tests (cherry picked from commit 8a41633ec845cbd39a245997a614c611b8175ad5) Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> --- robottelo/cli/factory.py | 1 - robottelo/host_helpers/cli_factory.py | 1 + tests/foreman/cli/test_operatingsystem.py | 335 +++++++++------------- 3 files changed, 141 insertions(+), 196 deletions(-) diff --git a/robottelo/cli/factory.py b/robottelo/cli/factory.py index 3eeca63b3a2..cbd2e855145 100644 --- a/robottelo/cli/factory.py +++ b/robottelo/cli/factory.py @@ -1281,7 +1281,6 @@ def make_os(options=None): 'password-hash': None, 'release-name': None, } - return create_object(OperatingSys, args, options) diff --git a/robottelo/host_helpers/cli_factory.py b/robottelo/host_helpers/cli_factory.py index beae91759c4..c79f0d047ee 100644 --- a/robottelo/host_helpers/cli_factory.py +++ b/robottelo/host_helpers/cli_factory.py @@ -195,6 +195,7 @@ def create_object(cli_object, options, values=None, credentials=None): 'name': gen_alphanumeric, }, 'os': { + '_entity_cls': 'OperatingSys', 'major': partial(random.randint, 0, 10), 'minor': partial(random.randint, 0, 10), 'name': gen_alphanumeric, diff --git a/tests/foreman/cli/test_operatingsystem.py b/tests/foreman/cli/test_operatingsystem.py index 5bcf6ec535c..eeb5394cba2 100644 --- a/tests/foreman/cli/test_operatingsystem.py +++ b/tests/foreman/cli/test_operatingsystem.py @@ -21,13 +21,10 @@ from fauxfactory import gen_string from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import CLIFactoryError from robottelo.cli.factory import make_architecture from robottelo.cli.factory import make_medium -from robottelo.cli.factory import make_os from robottelo.cli.factory import make_partition_table from robottelo.cli.factory import make_template -from robottelo.cli.operatingsys import OperatingSys from robottelo.constants import DEFAULT_ORG from robottelo.utils.datafactory import filtered_datapoint from robottelo.utils.datafactory import invalid_values_list @@ -44,84 +41,113 @@ def negative_delete_data(): class TestOperatingSystem: """Test class for Operating System CLI.""" - @pytest.mark.tier1 - def test_positive_search_by_name(self): - """Search for newly created OS by name - - :id: ff9f667c-97ca-49cd-902b-a9b18b5aa021 - - :expectedresults: Operating System is created and listed - - :CaseImportance: Critical - """ - os_list_before = OperatingSys.list() - os = make_os() - os_list = OperatingSys.list({'search': 'name=%s' % os['name']}) - os_info = OperatingSys.info({'id': os_list[0]['id']}) - assert os['id'] == os_info['id'] - os_list_after = OperatingSys.list() - assert len(os_list_after) > len(os_list_before) - - @pytest.mark.tier1 - def test_positive_search_by_title(self): - """Search for newly created OS by title - - :id: a555e848-f1f2-4326-aac6-9de8ff45abee - - :expectedresults: Operating System is created and listed - - :CaseImportance: Critical - """ - os_list_before = OperatingSys.list() - os = make_os() - os_list = OperatingSys.list({'search': 'title=\\"%s\\"' % os['title']}) - os_info = OperatingSys.info({'id': os_list[0]['id']}) - assert os['id'] == os_info['id'] - os_list_after = OperatingSys.list() - assert len(os_list_after) > len(os_list_before) - - @pytest.mark.tier1 - def test_positive_list(self): - """Displays list for operating system + @pytest.mark.e2e + @pytest.mark.upgrade + def test_positive_end_to_end_os(self, target_sat): + """End-to-end test for Operating system - :id: fca309c5-edff-4296-a800-55470669935a + :id: 531ab5c0-ccba-45ec-bd52-85b4df250d79 - :expectedresults: Operating System is created and listed + :steps: + 1. Create OS + 2. Check OS is created with all given fields + 3. Read OS + 4. Update OS + 5. Check OS is updated with all given fields + 6. Delete OS + 7. Check if OS is deleted - :CaseImportance: Critical + :expectedresults: All CRUD operations are performed successfully. """ - os_list_before = OperatingSys.list() name = gen_string('alpha') - os = make_os({'name': name}) - os_list = OperatingSys.list({'search': 'name=%s' % name}) - os_info = OperatingSys.info({'id': os_list[0]['id']}) - assert os['id'] == os_info['id'] - os_list_after = OperatingSys.list() - assert len(os_list_after) > len(os_list_before) - - @pytest.mark.tier1 - def test_positive_info_by_id(self): - """Displays info for operating system by its ID - - :id: b8f23b53-439a-4726-9757-164d99d5ed05 - - :expectedresults: Operating System is created and can be looked up by - its ID - - :CaseImportance: Critical - """ - os = make_os() - os_info = OperatingSys.info({'id': os['id']}) - # Info does not return major or minor but a concat of name, - # major and minor - assert os['id'] == os_info['id'] - assert os['name'] == os_info['name'] - assert str(os['major-version']) == os_info['major-version'] - assert str(os['minor-version']) == os_info['minor-version'] + desc = gen_string('alpha') + os_family = 'Redhat' + pass_hash = 'SHA256' + minor_version = gen_string('numeric', 1) + major_version = gen_string('numeric', 1) + architecture = target_sat.cli_factory.make_architecture() + medium = target_sat.cli_factory.make_medium() + ptable = target_sat.cli_factory.make_partition_table() + template = target_sat.cli_factory.make_template() + # Create OS + os = target_sat.cli.OperatingSys.create( + { + 'name': name, + 'description': desc, + 'family': os_family, + 'password-hash': pass_hash, + 'major': major_version, + 'minor': minor_version, + 'architecture-ids': architecture['id'], + 'medium-ids': medium['id'], + 'partition-table-ids': ptable['id'], + 'provisioning-template-ids': template['id'], + } + ) + assert os['name'] == name + assert os['title'] == desc + assert os['family'] == os_family + assert str(os['major-version']) == major_version + assert str(os['minor-version']) == minor_version + assert os['architectures'][0] == architecture['name'] + assert os['installation-media'][0] == medium['name'] + assert ptable['name'] in os['partition-tables'] + assert template['name'] in str(os['templates']) + # Read OS + os = target_sat.cli.OperatingSys.list({'search': f'name={name}'}) + assert os[0]['title'] == desc + os = target_sat.cli.OperatingSys.info({'id': os[0]['id']}) + assert os['name'] == name + assert os['title'] == desc + assert os['family'] == os_family + assert str(os['major-version']) == major_version + assert str(os['minor-version']) == minor_version + assert os['architectures'][0] == architecture['name'] + assert ptable['name'] in os['partition-tables'] + assert template['name'] in str(os['templates']) + new_name = gen_string('alpha') + new_desc = gen_string('alpha') + new_os_family = 'Redhat' + new_pass_hash = 'SHA256' + new_minor_version = gen_string('numeric', 1) + new_major_version = gen_string('numeric', 1) + new_architecture = make_architecture() + new_medium = make_medium() + new_ptable = make_partition_table() + new_template = make_template() + os = target_sat.cli.OperatingSys.update( + { + 'id': os['id'], + 'name': new_name, + 'description': new_desc, + 'family': new_os_family, + 'password-hash': new_pass_hash, + 'major': new_major_version, + 'minor': new_minor_version, + 'architecture-ids': new_architecture['id'], + 'medium-ids': new_medium['id'], + 'partition-table-ids': new_ptable['id'], + 'provisioning-template-ids': new_template['id'], + } + ) + os = target_sat.cli.OperatingSys.list({'search': f'title={new_desc}'}) + os = target_sat.cli.OperatingSys.info({'id': os[0]['id']}) + assert os['name'] == new_name + assert os['title'] == new_desc + assert os['family'] == new_os_family + assert str(os['major-version']) == new_major_version + assert str(os['minor-version']) == new_minor_version + assert os['architectures'][0] == new_architecture['name'] + assert os['installation-media'][0] == new_medium['name'] + assert new_ptable['name'] in os['partition-tables'] + assert new_template['name'] in str(os['templates']) + target_sat.cli.OperatingSys.delete({'id': os['id']}) + with pytest.raises(CLIReturnCodeError): + target_sat.cli.OperatingSys.info({'id': os['id']}) @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(valid_data_list())) - def test_positive_create_with_name(self, name): + def test_positive_create_with_name(self, name, target_sat): """Create Operating System for all variations of name :id: d36eba9b-ccf6-4c9d-a07f-c74eebada89b @@ -132,39 +158,12 @@ def test_positive_create_with_name(self, name): :CaseImportance: Critical """ - os = make_os({'name': name}) + os = target_sat.cli_factory.make_os({'name': name}) assert os['name'] == name - @pytest.mark.tier1 - def test_positive_create_with_arch_medium_ptable(self): - """Create an OS pointing to an arch, medium and partition table. - - :id: 05bdb2c6-0d2e-4141-9e07-3ada3933b577 - - :expectedresults: An operating system is created. - - :CaseImportance: Critical - """ - architecture = make_architecture() - medium = make_medium() - ptable = make_partition_table() - operating_system = make_os( - { - 'architecture-ids': architecture['id'], - 'medium-ids': medium['id'], - 'partition-table-ids': ptable['id'], - } - ) - - for attr in ('architectures', 'installation-media', 'partition-tables'): - assert len(operating_system[attr]) == 1 - assert operating_system['architectures'][0] == architecture['name'] - assert operating_system['installation-media'][0] == medium['name'] - assert operating_system['partition-tables'][0] == ptable['name'] - @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) - def test_negative_create_with_name(self, name): + def test_negative_create_with_name(self, name, target_sat): """Create Operating System using invalid names :id: 848a20ce-292a-47d8-beea-da5916c43f11 @@ -175,48 +174,12 @@ def test_negative_create_with_name(self, name): :CaseImportance: Critical """ - with pytest.raises(CLIFactoryError): - make_os({'name': name}) - - @pytest.mark.tier1 - @pytest.mark.parametrize('new_name', **parametrized(valid_data_list())) - def test_positive_update_name(self, new_name): - """Positive update of operating system name - - :id: 49b655f7-ba9b-4bb9-b09d-0f7140969a40 - - :parametrized: yes - - :expectedresults: Operating System name is updated - - :CaseImportance: Critical - """ - os = make_os({'name': gen_alphanumeric()}) - OperatingSys.update({'id': os['id'], 'name': new_name}) - result = OperatingSys.info({'id': os['id']}) - assert result['id'] == os['id'] - assert result['name'] != os['name'] - - @pytest.mark.tier1 - def test_positive_update_major_version(self): - """Update an Operating System's major version. - - :id: 38a89dbe-6d1c-4602-a4c1-664425668de8 - - :expectedresults: Operating System major version is updated - - :CaseImportance: Critical - """ - os = make_os() - # New value for major - major = int(os['major-version']) + 1 - OperatingSys.update({'id': os['id'], 'major': major}) - os = OperatingSys.info({'id': os['id']}) - assert int(os['major-version']) == major + with pytest.raises(CLIReturnCodeError): + target_sat.cli.OperatingSys.create({'name': name}) @pytest.mark.tier1 @pytest.mark.parametrize('new_name', **parametrized(invalid_values_list())) - def test_negative_update_name(self, new_name): + def test_negative_update_name(self, new_name, target_sat): """Negative update of system name :id: 4b18ff6d-7728-4245-a1ce-38e62c05f454 @@ -227,34 +190,15 @@ def test_negative_update_name(self, new_name): :CaseImportance: Critical """ - os = make_os({'name': gen_alphanumeric()}) + os = target_sat.cli_factory.make_os({'name': gen_alphanumeric()}) with pytest.raises(CLIReturnCodeError): - OperatingSys.update({'id': os['id'], 'name': new_name}) - result = OperatingSys.info({'id': os['id']}) + target_sat.cli.OperatingSys.update({'id': os['id'], 'name': new_name}) + result = target_sat.cli.OperatingSys.info({'id': os['id']}) assert result['name'] == os['name'] - @pytest.mark.tier1 - @pytest.mark.upgrade - @pytest.mark.parametrize('name', **parametrized(valid_data_list())) - def test_positive_delete_by_id(self, name): - """Successfully deletes Operating System by its ID - - :id: a67a7b01-081b-42f8-a9ab-1f41166d649e - - :parametrized: yes - - :expectedresults: Operating System is deleted - - :CaseImportance: Critical - """ - os = make_os({'name': name}) - OperatingSys.delete({'id': os['id']}) - with pytest.raises(CLIReturnCodeError): - OperatingSys.info({'id': os['id']}) - @pytest.mark.tier1 @pytest.mark.parametrize('test_data', **parametrized(negative_delete_data())) - def test_negative_delete_by_id(self, test_data): + def test_negative_delete_by_id(self, test_data, target_sat): """Delete Operating System using invalid data :id: d29a9c95-1fe3-4a7a-9f7b-127be065856d @@ -265,17 +209,17 @@ def test_negative_delete_by_id(self, test_data): :CaseImportance: Critical """ - os = make_os() + os = target_sat.cli_factory.make_os() # The delete method requires the ID which we will not pass with pytest.raises(CLIReturnCodeError): - OperatingSys.delete(test_data) + target_sat.cli.OperatingSys.delete(test_data) # Now make sure that it still exists - result = OperatingSys.info({'id': os['id']}) + result = target_sat.cli.OperatingSys.info({'id': os['id']}) assert os['id'] == result['id'] assert os['name'] == result['name'] @pytest.mark.tier2 - def test_positive_add_arch(self): + def test_positive_add_arch(self, target_sat): """Add Architecture to operating system :id: 99add22d-d936-4232-9441-beff85867040 @@ -284,16 +228,18 @@ def test_positive_add_arch(self): :CaseLevel: Integration """ - architecture = make_architecture() - os = make_os() - OperatingSys.add_architecture({'architecture-id': architecture['id'], 'id': os['id']}) - os = OperatingSys.info({'id': os['id']}) + architecture = target_sat.cli_factory.make_architecture() + os = target_sat.cli_factory.make_os() + target_sat.cli.OperatingSys.add_architecture( + {'architecture-id': architecture['id'], 'id': os['id']} + ) + os = target_sat.cli.OperatingSys.info({'id': os['id']}) assert len(os['architectures']) == 1 assert architecture['name'] == os['architectures'][0] @pytest.mark.tier2 @pytest.mark.upgrade - def test_positive_add_template(self): + def test_positive_add_template(self, target_sat): """Add provisioning template to operating system :id: 0ea9eb88-2d27-423d-a9d3-fdd788b4e28a @@ -302,20 +248,20 @@ def test_positive_add_template(self): :CaseLevel: Integration """ - template = make_template() - os = make_os() + template = target_sat.cli_factory.make_template() + os = target_sat.cli_factory.make_os() default_template_name = os['default-templates'][0] - OperatingSys.add_provisioning_template( + target_sat.cli.OperatingSys.add_provisioning_template( {'provisioning-template': template['name'], 'id': os['id']} ) - os = OperatingSys.info({'id': os['id']}) + os = target_sat.cli.OperatingSys.info({'id': os['id']}) assert len(os['templates']) == 2 provision_template_name = f"{template['name']} ({template['type']})" assert default_template_name in os['templates'] assert provision_template_name in os['templates'] @pytest.mark.tier2 - def test_positive_add_ptable(self): + def test_positive_add_ptable(self, target_sat): """Add partition table to operating system :id: beba676f-b4e4-48e1-bb0c-18ad91847566 @@ -325,18 +271,18 @@ def test_positive_add_ptable(self): :CaseLevel: Integration """ # Create a partition table. - ptable_name = make_partition_table()['name'] + ptable_name = target_sat.cli_factory.make_partition_table()['name'] # Create an operating system. - os_id = make_os()['id'] + os_id = target_sat.cli_factory.make_os()['id'] # Add the partition table to the operating system. - OperatingSys.add_ptable({'id': os_id, 'partition-table': ptable_name}) + target_sat.cli.OperatingSys.add_ptable({'id': os_id, 'partition-table': ptable_name}) # Verify that the operating system has a partition table. - os = OperatingSys.info({'id': os_id}) + os = target_sat.cli.OperatingSys.info({'id': os_id}) assert len(os['partition-tables']) == 1 assert os['partition-tables'][0] == ptable_name @pytest.mark.tier2 - def test_positive_update_parameters_attributes(self): + def test_positive_update_parameters_attributes(self, target_sat): """Update os-parameters-attributes to operating system :id: 5d566eea-b323-4128-9356-3bf39943e4d4 @@ -347,8 +293,8 @@ def test_positive_update_parameters_attributes(self): """ param_name = gen_string('alpha') param_value = gen_string('alpha') - os_id = make_os()['id'] - OperatingSys.update( + os_id = target_sat.cli_factory.make_os()['id'] + target_sat.cli.OperatingSys.update( { 'id': os_id, 'os-parameters-attributes': 'name={}, value={}'.format( @@ -356,14 +302,13 @@ def test_positive_update_parameters_attributes(self): ), } ) - os = OperatingSys.info({'id': os_id}, output_format='json') + os = target_sat.cli.OperatingSys.info({'id': os_id}, output_format='json') assert param_name == os['parameters'][0]['name'] assert param_value == os['parameters'][0]['value'] @pytest.mark.tier2 -@pytest.mark.skip_if_open("BZ:1649011") -def test_positive_os_list_with_default_organization_set(satellite_host): +def test_positive_os_list_with_default_organization_set(target_sat): """list operating systems when the default organization is set :id: 2c1ba416-a5d5-4031-b154-54794569a85b @@ -375,19 +320,19 @@ def test_positive_os_list_with_default_organization_set(satellite_host): :expectedresults: os list should list operating systems when the default organization is set """ - satellite_host.api.OperatingSystem().create() - os_list_before_default = satellite_host.cli.OperatingSys.list() + target_sat.api.OperatingSystem().create() + os_list_before_default = target_sat.cli.OperatingSys.list() assert len(os_list_before_default) > 0 try: - satellite_host.cli.Defaults.add({'param-name': 'organization', 'param-value': DEFAULT_ORG}) - result = satellite_host.execute('hammer defaults list') + target_sat.cli.Defaults.add({'param-name': 'organization', 'param-value': DEFAULT_ORG}) + result = target_sat.execute('hammer defaults list') assert result.status == 0 assert DEFAULT_ORG in result.stdout - os_list_after_default = satellite_host.cli.OperatingSys.list() + os_list_after_default = target_sat.cli.OperatingSys.list() assert len(os_list_after_default) > 0 finally: - satellite_host.cli.Defaults.delete({'param-name': 'organization'}) - result = satellite_host.execute('hammer defaults list') + target_sat.cli.Defaults.delete({'param-name': 'organization'}) + result = target_sat.execute('hammer defaults list') assert result.status == 0 assert DEFAULT_ORG not in result.stdout From b8c94f25b21b22cc963e8767adb70bafae86622b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 12 Jul 2023 07:15:15 -0400 Subject: [PATCH 075/586] [6.14.z] cs tag added for repos test (#11863) cs tag added for repos test (#11858) (cherry picked from commit 6c7fdc33d818cdcc3ddff583691d4e30debd0c16) Co-authored-by: Peter Ondrejka --- tests/foreman/cli/test_repositories.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/foreman/cli/test_repositories.py b/tests/foreman/cli/test_repositories.py index c6bd95c302b..f35a93603ab 100644 --- a/tests/foreman/cli/test_repositories.py +++ b/tests/foreman/cli/test_repositories.py @@ -38,6 +38,8 @@ def test_positive_custom_products_disabled_by_default( :expectedresults: Custom products should be disabled by default. "Enabled: 0" + :customerscenario: true + :BZ: 1265120 """ ak, org, _ = setup_content From 7de9332cc2d4dab8f9475776f21fccde51f9a111 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 13 Jul 2023 03:52:39 -0400 Subject: [PATCH 076/586] [6.14.z] [Component Audit] Provisioning templates component refactor (#11873) --- .../foreman/api/test_provisioningtemplate.py | 119 +++++----- .../foreman/api/test_template_combination.py | 87 +++----- .../foreman/cli/test_provisioningtemplate.py | 206 +++++------------- tests/foreman/ui/test_provisioningtemplate.py | 99 +++------ 4 files changed, 159 insertions(+), 352 deletions(-) diff --git a/tests/foreman/api/test_provisioningtemplate.py b/tests/foreman/api/test_provisioningtemplate.py index 5db24fa37bf..58f3b0bae2e 100644 --- a/tests/foreman/api/test_provisioningtemplate.py +++ b/tests/foreman/api/test_provisioningtemplate.py @@ -26,46 +26,33 @@ from fauxfactory import gen_mac from fauxfactory import gen_string from nailgun import client -from nailgun import entities from requests.exceptions import HTTPError -from robottelo.config import get_credentials from robottelo.config import settings from robottelo.config import user_nailgun_config from robottelo.utils.datafactory import invalid_names_list from robottelo.utils.datafactory import valid_data_list -@pytest.fixture(scope="module") -def module_location(module_location): - yield module_location - module_location.delete() - - -@pytest.fixture(scope="module") -def module_org(module_org): - yield module_org - module_org.delete() - - -@pytest.fixture(scope="module") -def module_user(module_org, module_location): +@pytest.fixture(scope='module') +def module_user(module_target_sat, module_org, module_location): """Creates an org admin role and user""" user_login = gen_string('alpha') user_password = gen_string('alpha') - # Create user with Manager role - orig_role = entities.Role().search(query={'search': 'name="Organization admin"'})[0] - new_role_dict = entities.Role(id=orig_role.id).clone( + orig_role = module_target_sat.api.Role().search(query={'search': 'name="Organization admin"'})[ + 0 + ] + new_role_dict = module_target_sat.api.Role(id=orig_role.id).clone( data={ 'role': { - 'name': f"test_template_admin_{gen_string('alphanumeric', 3)}", + 'name': f'test_template_admin_{gen_string("alphanumeric", 3)}', 'organization_ids': [module_org.id], 'location_ids': [module_location.id], } } ) - new_role = entities.Role(id=new_role_dict['id']).read() - user = entities.User( + new_role = module_target_sat.api.Role(id=new_role_dict['id']).read() + user = module_target_sat.api.User( role=[new_role], admin=False, login=user_login, @@ -73,13 +60,14 @@ def module_user(module_org, module_location): organization=[module_org], location=[module_location], ).create() - yield (user, user_login, user_password) + user.password = user_password + yield user user.delete() new_role.delete() -@pytest.fixture(scope="function") -def tftpboot(module_org, target_sat): +@pytest.fixture +def tftpboot(module_org, module_target_sat): """This fixture removes the current deployed templates from TFTP, and sets up new ones. It manipulates the global defaults, so it shouldn't be used in concurrent environment @@ -106,36 +94,37 @@ def tftpboot(module_org, target_sat): }, 'ipxe': { 'setting': 'global_iPXE', - 'path': f'{target_sat.url}/unattended/iPXE?bootstrap=1', + 'path': f'{module_target_sat.url}/unattended/iPXE?bootstrap=1', 'kind': 'iPXE', }, } # we keep the value of these for the teardown - default_settings = entities.Setting().search(query={"search": "name ~ global_"}) - kinds = entities.TemplateKind().search(query={"search": "name ~ PXE"}) + default_settings = module_target_sat.api.Setting().search(query={'search': 'name ~ global_'}) + kinds = module_target_sat.api.TemplateKind().search(query={"search": "name ~ PXE"}) # clean the already-deployed default pxe configs - target_sat.execute('rm {}'.format(' '.join([i['path'] for i in default_templates.values()]))) + module_target_sat.execute(f'rm -f {" ".join([i["path"] for i in default_templates.values()])}') # create custom Templates per kind for template in default_templates.values(): - template['entity'] = entities.ProvisioningTemplate( + template['entity'] = module_target_sat.api.ProvisioningTemplate( name=gen_string('alpha'), organization=[module_org], snippet=False, template_kind=[i.id for i in kinds if i.name == template['kind']][0], - template=f"<%= foreman_server_url %> {template['kind']}", + template=f'<%= foreman_server_url %> {template["kind"]}', ).create(create_missing=False) # Update the global settings to use newly created template - template['setting_id'] = entities.Setting( + module_target_sat.api.Setting( id=[i.id for i in default_settings if i.name == template['setting']][0], value=template['entity'].name, ).update(fields=['value']) yield default_templates - # delete the deployed tftp files - target_sat.execute('rm {}'.format(' '.join([i['path'] for i in default_templates.values()]))) + # clean the already-deployed default pxe configs + module_target_sat.execute(f'rm -f {" ".join([i["path"] for i in default_templates.values()])}') + # set the settings back to defaults for setting in default_settings: if setting.value is None: @@ -152,7 +141,9 @@ class TestProvisioningTemplate: @pytest.mark.tier1 @pytest.mark.e2e @pytest.mark.upgrade - def test_positive_end_to_end_crud(self, module_org, module_location, module_user, target_sat): + def test_positive_end_to_end_crud( + self, module_org, module_location, module_user, module_target_sat + ): """Create a new provisioning template with several attributes, update them, clone the provisioning template and then delete it @@ -163,12 +154,12 @@ def test_positive_end_to_end_crud(self, module_org, module_location, module_user :CaseImportance: Critical """ - cfg = user_nailgun_config(module_user[1], module_user[2]) + cfg = user_nailgun_config(module_user.login, module_user.password) name = gen_string('alpha') new_name = gen_string('alpha') - template_kind = choice(target_sat.api.TemplateKind().search()) + template_kind = choice(module_target_sat.api.TemplateKind().search()) - template = target_sat.api.ProvisioningTemplate( + template = module_target_sat.api.ProvisioningTemplate( name=name, organization=[module_org], location=[module_location], @@ -176,19 +167,21 @@ def test_positive_end_to_end_crud(self, module_org, module_location, module_user template_kind=template_kind, ).create() assert template.name == name - assert len(template.organization) == 1, "Template should be assigned to a single org here" + assert len(template.organization) == 1, 'Template should be assigned to a single org here' assert template.organization[0].id == module_org.id - assert len(template.location) == 1, "Template should be assigned to a single location here" + assert len(template.location) == 1, 'Template should be assigned to a single location here' assert template.location[0].id == module_location.id - assert template.snippet is False, "Template snippet attribute is True instead of False" + assert template.snippet is False, 'Template snippet attribute is True instead of False' assert template.template_kind.id == template_kind.id # negative create with pytest.raises(HTTPError) as e1: - target_sat.api.ProvisioningTemplate(name=gen_choice(invalid_names_list())).create() + module_target_sat.api.ProvisioningTemplate( + name=gen_choice(invalid_names_list()) + ).create() assert e1.value.response.status_code == 422 - invalid = target_sat.api.ProvisioningTemplate(snippet=False) + invalid = module_target_sat.api.ProvisioningTemplate(snippet=False) invalid.create_missing() invalid.template_kind = None invalid.template_kind_name = gen_string('alpha') @@ -197,13 +190,12 @@ def test_positive_end_to_end_crud(self, module_org, module_location, module_user assert e2.value.response.status_code == 422 # update - assert template.template_kind.id == template_kind.id, "Template kind id doesn't match" - updated = target_sat.api.ProvisioningTemplate( + assert template.template_kind.id == template_kind.id, 'Template kind id does not match' + updated = module_target_sat.api.ProvisioningTemplate( server_config=cfg, id=template.id, name=new_name ).update(['name']) - assert updated.name == new_name, "The Provisioning template wasn't properly renamed" + assert updated.name == new_name, 'The Provisioning template was not properly renamed' # clone - template_origin = template.read_json() # remove unique keys unique_keys = ('updated_at', 'created_at', 'id', 'name') @@ -212,10 +204,10 @@ def test_positive_end_to_end_crud(self, module_org, module_location, module_user } dupe_name = gen_choice(list(valid_data_list().values())) - dupe_json = target_sat.api.ProvisioningTemplate( + dupe_json = module_target_sat.api.ProvisioningTemplate( id=template.clone(data={'name': dupe_name})['id'] ).read_json() - dupe_template = target_sat.api.ProvisioningTemplate(id=dupe_json['id']) + dupe_template = module_target_sat.api.ProvisioningTemplate(id=dupe_json['id']) dupe_json = {key: value for key, value in dupe_json.items() if key not in unique_keys} assert template_origin == dupe_json @@ -229,8 +221,7 @@ def test_positive_end_to_end_crud(self, module_org, module_location, module_user @pytest.mark.e2e @pytest.mark.tier2 @pytest.mark.upgrade - @pytest.mark.run_in_one_thread - def test_positive_build_pxe_default(self, tftpboot, target_sat): + def test_positive_build_pxe_default(self, tftpboot, module_target_sat): """Call the "build_pxe_default" path. :id: ca19d9da-1049-4b39-823b-933fc1a0cebd @@ -244,27 +235,25 @@ def test_positive_build_pxe_default(self, tftpboot, target_sat): :BZ: 1202564 """ - response = client.post( - entities.ProvisioningTemplate().path('build_pxe_default'), - auth=get_credentials(), - verify=False, - ) - response.raise_for_status() - assert type(response.json()) == dict + # Build PXE default template to get default PXE file + module_target_sat.api.ProvisioningTemplate().build_pxe_default() + for template in tftpboot.values(): if template['path'].startswith('http'): r = client.get(template['path'], verify=False) r.raise_for_status() rendered = r.text else: - rendered = target_sat.execute(f'cat {template["path"]}').stdout.splitlines()[0] + rendered = module_target_sat.execute(f'cat {template["path"]}').stdout.splitlines()[ + 0 + ] if template['kind'] == 'iPXE': - assert f'{target_sat.hostname}/unattended/iPXE' in r.text + assert f'{module_target_sat.hostname}/unattended/iPXE' in r.text else: assert ( - rendered == f"{settings.server.scheme}://" - f"{target_sat.hostname} {template['kind']}" + rendered == f'{settings.server.scheme}://' + f'{module_target_sat.hostname} {template["kind"]}' ) @pytest.mark.parametrize('module_sync_kickstart_content', [7, 8, 9], indirect=True) @@ -354,10 +343,8 @@ def test_positive_template_check_ipxe( }, ).create() ipxe_template = host.read_template(data={'template_kind': 'iPXE'}) - if module_sync_kickstart_content.rhel_ver <= 8: - assert str(ipxe_template).count('ks=') == 1 - else: - assert str(ipxe_template).count('inst.ks=') == 1 + ks_param = 'ks=' if module_sync_kickstart_content.rhel_ver <= 8 else 'inst.ks=' + assert str(ipxe_template).count(ks_param) == 1 @pytest.mark.parametrize('module_sync_kickstart_content', [7, 8, 9], indirect=True) def test_positive_template_check_vlan_parameter( diff --git a/tests/foreman/api/test_template_combination.py b/tests/foreman/api/test_template_combination.py index c9f8c1360ad..3eb5d59c125 100644 --- a/tests/foreman/api/test_template_combination.py +++ b/tests/foreman/api/test_template_combination.py @@ -15,86 +15,49 @@ :Upstream: No """ import pytest -from nailgun import entities from requests.exceptions import HTTPError -@pytest.fixture(scope='module') -def module_hostgroup_template(module_hostgroup): - """Create hostgroup to be used on TemplateCombination creation""" - yield {'hostgroup': module_hostgroup} - # Delete hostgroup used on TemplateCombination creation - module_hostgroup.delete() +@pytest.mark.tier1 +@pytest.mark.upgrade +def test_positive_end_to_end_template_combination(request, module_target_sat, module_hostgroup): + """Assert API template combination get/delete method works. + :id: 3a5cb370-c5f6-11e6-bb2f-68f72889dc7g + + :Setup: save a template combination -@pytest.fixture(scope='function') -def function_template_combination(module_hostgroup_template): - """Create ProvisioningTemplate and TemplateConfiguration for each test - and at the end delete ProvisioningTemplate used on tests + :expectedresults: TemplateCombination can be created, retrieved and deleted through API + + :CaseImportance: Critical + + :BZ: 1369737 """ - template = entities.ProvisioningTemplate( + template = module_target_sat.api.ProvisioningTemplate( snippet=False, template_combinations=[ { - 'hostgroup_id': module_hostgroup_template['hostgroup'].id, + 'hostgroup_id': module_hostgroup.id, } ], ) template = template.create() template_combination_dct = template.template_combinations[0] - template_combination = entities.TemplateCombination( + template_combination = module_target_sat.api.TemplateCombination( id=template_combination_dct['id'], provisioning_template=template, - hostgroup=module_hostgroup_template['hostgroup'], + hostgroup=module_hostgroup, ) - yield {'template': template, 'template_combination': template_combination} - # Clean combination if it is not already deleted - try: - template_combination.delete() - except HTTPError: - pass - template.delete() - - -@pytest.mark.tier1 -def test_positive_get_combination(function_template_combination, module_hostgroup_template): - """Assert API template combination get method works. - - :id: 2447674e-c37e-11e6-93cb-68f72889dc7f - - :Setup: save a template combination - - :expectedresults: TemplateCombination can be retrieved through API - - :CaseImportance: Critical - - :BZ: 1369737 - """ - combination = function_template_combination['template_combination'].read() - assert isinstance(combination, entities.TemplateCombination) - assert function_template_combination['template'].id == combination.provisioning_template.id - assert module_hostgroup_template['hostgroup'].id == combination.hostgroup.id - - -@pytest.mark.tier1 -@pytest.mark.upgrade -def test_positive_delete_combination(function_template_combination): - """Assert API template combination delete method works. - - :id: 3a5cb370-c5f6-11e6-bb2f-68f72889dc7f + # GET + combination = template_combination.read() + assert template.id == combination.provisioning_template.id + assert module_hostgroup.id == combination.hostgroup.id - :Setup: save a template combination - - :expectedresults: TemplateCombination can be deleted through API - - :CaseImportance: Critical - - :BZ: 1369737 - """ - combination = function_template_combination['template_combination'].read() - assert isinstance(combination, entities.TemplateCombination) - assert 1 == len(function_template_combination['template'].read().template_combinations) + # DELETE + assert 1 == len(template.read().template_combinations) combination.delete() with pytest.raises(HTTPError): combination.read() - assert 0 == len(function_template_combination['template'].read().template_combinations) + assert 0 == len(template.read().template_combinations) + template.delete() + module_hostgroup.delete() diff --git a/tests/foreman/cli/test_provisioningtemplate.py b/tests/foreman/cli/test_provisioningtemplate.py index e379ecfaa9c..db2cca0dcda 100644 --- a/tests/foreman/cli/test_provisioningtemplate.py +++ b/tests/foreman/cli/test_provisioningtemplate.py @@ -21,22 +21,21 @@ import pytest from fauxfactory import gen_string -from nailgun import entities from robottelo import constants from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_template -from robottelo.cli.template import Template -from robottelo.cli.user import User @pytest.fixture(scope='module') -def module_os_with_minor(): - return entities.OperatingSystem(minor=randint(0, 10)).create() +def module_os_with_minor(module_target_sat): + return module_target_sat.api.OperatingSystem(minor=randint(0, 10)).create() @pytest.mark.e2e -def test_positive_end_to_end_crud(module_org, module_location, module_os, target_sat): +@pytest.mark.upgrade +def test_positive_end_to_end_crud( + module_org, module_location, module_os_with_minor, module_target_sat +): """Create a new provisioning template with several attributes, list, update them, clone the provisioning template and then delete it @@ -59,14 +58,14 @@ def test_positive_end_to_end_crud(module_org, module_location, module_os, target cloned_template_name = gen_string('alpha') template_type = random.choice(constants.TEMPLATE_TYPES) # create - template = target_sat.cli_factory.make_template( + template = module_target_sat.cli_factory.make_template( { 'name': name, 'audit-comment': gen_string('alpha'), 'description': gen_string('alpha'), 'locked': 'no', 'type': template_type, - 'operatingsystem-ids': module_os.id, + 'operatingsystem-ids': module_os_with_minor.id, 'organization-ids': module_org.id, 'location-ids': module_location.id, } @@ -74,22 +73,22 @@ def test_positive_end_to_end_crud(module_org, module_location, module_os, target assert template['name'] == name assert template['locked'] == 'no' assert template['type'] == template_type - assert module_os.title in template['operating-systems'] + assert module_os_with_minor.title in template['operating-systems'] assert module_org.name in template['organizations'] assert module_location.name in template['locations'] # list - template_list = target_sat.cli.Template.list({'search': f'name={name}'}) + template_list = module_target_sat.cli.Template.list({'search': f'name={name}'}) assert template_list[0]['name'] == name assert template_list[0]['type'] == template_type # update - updated_pt = target_sat.cli.Template.update({'id': template['id'], 'name': new_name}) - template = target_sat.cli.Template.info({'id': updated_pt[0]['id']}) + updated_pt = module_target_sat.cli.Template.update({'id': template['id'], 'name': new_name}) + template = module_target_sat.cli.Template.info({'id': updated_pt[0]['id']}) assert new_name == template['name'], "The Provisioning template wasn't properly renamed" # clone - template_clone = target_sat.cli.Template.clone( + template_clone = module_target_sat.cli.Template.clone( {'id': template['id'], 'new-name': cloned_template_name} ) - new_template = target_sat.cli.Template.info({'id': template_clone[0]['id']}) + new_template = module_target_sat.cli.Template.info({'id': template_clone[0]['id']}) assert new_template['name'] == cloned_template_name assert new_template['locked'] == template['locked'] assert new_template['type'] == template['type'] @@ -97,47 +96,16 @@ def test_positive_end_to_end_crud(module_org, module_location, module_os, target assert new_template['organizations'] == template['organizations'] assert new_template['locations'] == template['locations'] # delete - target_sat.cli.Template.delete({'id': template['id']}) + module_target_sat.cli.Template.delete({'id': template['id']}) with pytest.raises(CLIReturnCodeError): - target_sat.cli.Template.info({'id': template['id']}) - - -@pytest.mark.tier1 -def test_positive_create_with_name(): - """Check if Template can be created - - :id: 77deaae8-447b-47cc-8af3-8b17476c905f - - :expectedresults: Template is created - - :CaseImportance: Critical - """ - name = gen_string('alpha') - template = make_template({'name': name}) - assert template['name'] == name - - -@pytest.mark.tier1 -def test_positive_update_name(): - """Check if Template can be updated - - :id: 99bdab7b-1279-4349-a655-4294395ecbe1 - - :expectedresults: Template is updated - - :CaseImportance: Critical - """ - template = make_template() - updated_name = gen_string('alpha') - Template.update({'id': template['id'], 'name': updated_name}) - template = Template.info({'id': template['id']}) - assert updated_name == template['name'] + module_target_sat.cli.Template.info({'id': template['id']}) @pytest.mark.tier1 -def test_positive_update_with_manager_role(module_location, module_org): +@pytest.mark.parametrize('role_name', ['Manager', 'Organization admin']) +def test_positive_update_with_role(module_target_sat, module_location, module_org, role_name): """Create template providing the initial name, then update its name - with manager user role. + with manager/organization admin user roles. :id: 28c4357a-93cb-4b01-a445-5db50435bcc0 @@ -147,47 +115,35 @@ def test_positive_update_with_manager_role(module_location, module_org): :CaseImportance: Medium :BZ: 1277308 + + :parametrized: yes """ new_name = gen_string('alpha') username = gen_string('alpha') password = gen_string('alpha') - template = make_template( + template = module_target_sat.cli_factory.make_template( {'organization-ids': module_org.id, 'location-ids': module_location.id} ) - # Create user with Manager role - user = entities.User( + # Create user with Manager/Organization admin role + user = module_target_sat.api.User( login=username, password=password, admin=False, organization=[module_org.id], location=[module_location.id], ).create() - User.add_role({'id': user.id, 'role': "Manager"}) + module_target_sat.cli.User.add_role({'id': user.id, 'role': role_name}) # Update template name with that user - Template.with_user(username=username, password=password).update( + module_target_sat.cli.Template.with_user(username=username, password=password).update( {'id': template['id'], 'name': new_name} ) - template = Template.info({'id': template['id']}) + template = module_target_sat.cli.Template.info({'id': template['id']}) assert new_name == template['name'] @pytest.mark.tier1 -def test_positive_create_with_loc(module_location): - """Check if Template with Location can be created - - :id: 263aba0e-4f54-4227-af97-f4bc8f5c0788 - - :expectedresults: Template is created and new Location has been - assigned - - :CaseImportance: Medium - """ - new_template = make_template({'location-ids': module_location.id}) - assert module_location.name in new_template['locations'] - - -@pytest.mark.tier1 -def test_positive_create_locked(): +@pytest.mark.upgrade +def test_positive_create_locked(module_target_sat): """Check that locked Template can be created :id: ff10e369-85c6-45f3-9cda-7e1c17a6632d @@ -197,80 +153,41 @@ def test_positive_create_locked(): :CaseImportance: Medium """ - new_template = make_template({'locked': 'true', 'name': gen_string('alpha')}) + new_template = module_target_sat.cli_factory.make_template( + {'locked': 'true', 'name': gen_string('alpha')} + ) assert new_template['locked'] == 'yes' -@pytest.mark.tier2 -def test_positive_create_with_org(module_org): - """Check if Template with Organization can be created - - :id: 5de5ca76-1a39-46ac-8dd4-5d41b4b49076 - - :expectedresults: Template is created and new Organization has been - assigned - - :CaseImportance: Medium - """ - new_template = make_template({'name': gen_string('alpha'), 'organization-ids': module_org.id}) - assert module_org.name in new_template['organizations'] - - @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_add_os_by_id(module_os_with_minor): - """Check if operating system can be added to a template +def test_positive_add_remove_os_by_id(module_target_sat, module_os_with_minor): + """Check if operating system can be added and removed to a template :id: d9f481b3-9757-4208-b451-baf4792d4d70 - :expectedresults: Operating system is added to the template + :expectedresults: Operating system is added/removed from the template :CaseLevel: Integration """ - new_template = make_template() - Template.add_operatingsystem( - {'id': new_template['id'], 'operatingsystem-id': module_os_with_minor.id} - ) - new_template = Template.info({'id': new_template['id']}) - os_string = ( - f'{module_os_with_minor.name} {module_os_with_minor.major}.{module_os_with_minor.minor}' + os = module_os_with_minor + os_string = f'{os.name} {os.major}.{os.minor}' + new_template = module_target_sat.cli_factory.make_template() + module_target_sat.cli.Template.add_operatingsystem( + {'id': new_template['id'], 'operatingsystem-id': os.id} ) + new_template = module_target_sat.cli.Template.info({'id': new_template['id']}) assert os_string in new_template['operating-systems'] - - -@pytest.mark.tier2 -def test_positive_remove_os_by_id(module_os_with_minor): - """Check if operating system can be removed from a template - - :id: b5362565-6dce-4770-81e1-4fe3ec6f6cee - - :expectedresults: Operating system is removed from template - - :CaseLevel: Integration - - :CaseImportance: Medium - - :BZ: 1395229 - """ - template = make_template() - Template.add_operatingsystem( - {'id': template['id'], 'operatingsystem-id': module_os_with_minor.id} - ) - template = Template.info({'id': template['id']}) - os_string = ( - f'{module_os_with_minor.name} {module_os_with_minor.major}.{module_os_with_minor.minor}' - ) - assert os_string in template['operating-systems'] - Template.remove_operatingsystem( - {'id': template['id'], 'operatingsystem-id': module_os_with_minor.id} + module_target_sat.cli.Template.remove_operatingsystem( + {'id': new_template['id'], 'operatingsystem-id': os.id} ) - template = Template.info({'id': template['id']}) - assert os_string not in template['operating-systems'] + new_template = module_target_sat.cli.Template.info({'id': new_template['id']}) + assert os_string not in new_template['operating-systems'] @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_create_with_content(): +def test_positive_create_with_content(module_target_sat): """Check if Template can be created with specific content :id: 0fcfc46d-5e97-4451-936a-e8684acac275 @@ -281,32 +198,15 @@ def test_positive_create_with_content(): """ content = gen_string('alpha') name = gen_string('alpha') - template = make_template({'content': content, 'name': name}) + template = module_target_sat.cli_factory.make_template({'content': content, 'name': name}) assert template['name'] == name - template_content = Template.dump({'id': template['id']}) + template_content = module_target_sat.cli.Template.dump({'id': template['id']}) assert content in template_content -@pytest.mark.tier1 -@pytest.mark.upgrade -def test_positive_delete_by_id(): - """Check if Template can be deleted - - :id: 8e5245ee-13dd-44d4-8111-d4382cacf005 - - :expectedresults: Template is deleted - - :CaseImportance: Critical - """ - template = make_template() - Template.delete({'id': template['id']}) - with pytest.raises(CLIReturnCodeError): - Template.info({'id': template['id']}) - - @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_clone(): +def test_positive_clone(module_target_sat): """Assure ability to clone a provisioning template :id: 27d69c1e-0d83-4b99-8a3c-4f1bdec3d261 @@ -316,7 +216,9 @@ def test_positive_clone(): :CaseLevel: Integration """ cloned_template_name = gen_string('alpha') - template = make_template() - result = Template.clone({'id': template['id'], 'new-name': cloned_template_name}) - new_template = Template.info({'id': result[0]['id']}) + template = module_target_sat.cli_factory.make_template() + result = module_target_sat.cli.Template.clone( + {'id': template['id'], 'new-name': cloned_template_name} + ) + new_template = module_target_sat.cli.Template.info({'id': result[0]['id']}) assert new_template['name'] == cloned_template_name diff --git a/tests/foreman/ui/test_provisioningtemplate.py b/tests/foreman/ui/test_provisioningtemplate.py index e14ba4a83ef..579ca711f7b 100644 --- a/tests/foreman/ui/test_provisioningtemplate.py +++ b/tests/foreman/ui/test_provisioningtemplate.py @@ -17,10 +17,7 @@ :Upstream: No """ import pytest -from airgun.session import Session -from nailgun import entities -from robottelo.config import settings from robottelo.constants import DataFile from robottelo.utils.datafactory import gen_string @@ -31,12 +28,12 @@ def template_data(): @pytest.fixture() -def clone_setup(module_org, module_location): +def clone_setup(target_sat, module_org, module_location): name = gen_string('alpha') content = gen_string('alpha') - os_list = [entities.OperatingSystem().create().title for _ in range(2)] + os_list = [target_sat.api.OperatingSystem().create().title for _ in range(2)] return { - 'pt': entities.ProvisioningTemplate( + 'pt': target_sat.api.ProvisioningTemplate( name=name, organization=[module_org], location=[module_location], @@ -48,7 +45,7 @@ def clone_setup(module_org, module_location): @pytest.mark.tier2 -def test_positive_clone(session, clone_setup): +def test_positive_clone(module_org, module_location, target_sat, clone_setup): """Assure ability to clone a provisioning template :id: 912f1619-4bb0-4e0f-88ce-88b5726fdbe0 @@ -62,7 +59,9 @@ def test_positive_clone(session, clone_setup): :CaseLevel: Integration """ clone_name = gen_string('alpha') - with session: + with target_sat.ui_session() as session: + session.organization.select(org_name=module_org.name) + session.location.select(loc_name=module_location.name) session.provisioningtemplate.clone( clone_setup['pt'].name, { @@ -70,18 +69,14 @@ def test_positive_clone(session, clone_setup): 'association.applicable_os.assigned': clone_setup['os_list'], }, ) - pt = entities.ProvisioningTemplate().search(query={'search': f'name=={clone_name}'}) + pt = target_sat.api.ProvisioningTemplate().search(query={'search': f'name=={clone_name}'}) assigned_oses = [os.read() for os in pt[0].read().operatingsystem] - assert ( - pt - ), 'Template {} expected to exist but is not included in the search' 'results'.format( - clone_name - ) + assert pt, f'Template {clone_name} expected to exist but is not included in the search' assert set(clone_setup['os_list']) == {f'{os.name} {os.major}' for os in assigned_oses} @pytest.mark.tier2 -def test_positive_clone_locked(session, target_sat): +def test_positive_clone_locked(target_sat): """Assure ability to clone a locked provisioning template :id: 2df8550a-fe7d-405f-ab48-2896554cda12 @@ -95,22 +90,22 @@ def test_positive_clone_locked(session, target_sat): :CaseLevel: Integration """ clone_name = gen_string('alpha') - with session: + with target_sat.ui_session() as session: session.provisioningtemplate.clone( 'Kickstart default', { 'template.name': clone_name, }, ) - pt = target_sat.api.ProvisioningTemplate().search(query={'search': f'name=={clone_name}'}) - assert pt, ( - f'Template {clone_name} expected to exist but is not included in the search' 'results' - ) + assert target_sat.api.ProvisioningTemplate().search( + query={'search': f'name=={clone_name}'} + ), f'Template {clone_name} expected to exist but is not included in the search' @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_end_to_end(session, module_org, module_location, template_data, target_sat): +@pytest.mark.e2e +def test_positive_end_to_end(module_org, module_location, template_data, target_sat): """Perform end to end testing for provisioning template component :id: b44d4cc8-b78e-47cf-9993-0bb871ac2c96 @@ -138,7 +133,9 @@ def test_positive_end_to_end(session, module_org, module_location, template_data 'input_content.description': gen_string('alpha'), } ] - with session: + with target_sat.ui_session() as session: + session.organization.select(org_name=module_org.name) + session.location.select(loc_name=module_location.name) session.provisioningtemplate.create( { 'template.name': name, @@ -154,10 +151,9 @@ def test_positive_end_to_end(session, module_org, module_location, template_data 'locations.resources.assigned': [module_location.name], } ) - assert target_sat.api.ProvisioningTemplate().search(query={'search': f'name=={name}'}), ( - 'Provisioning template {} expected to exist but is not included in the search' - 'results'.format(name) - ) + assert target_sat.api.ProvisioningTemplate().search( + query={'search': f'name=={name}'} + ), f'Provisioning template {name} expected to exist but is not included in the search' pt = session.provisioningtemplate.read(name) assert pt['template']['name'] == name assert pt['template']['default'] is True @@ -176,54 +172,13 @@ def test_positive_end_to_end(session, module_org, module_location, template_data updated_pt = target_sat.api.ProvisioningTemplate().search( query={'search': f'name=={new_name}'} ) - assert updated_pt, ( - 'Provisioning template {} expected to exist but is not included in the search' - 'results'.format(new_name) - ) + assert ( + updated_pt + ), f'Provisioning template {new_name} expected to exist but is not included in the search' updated_pt = updated_pt[0].read() assert updated_pt.snippet is True, 'Snippet attribute not updated for Provisioning Template' - assert not updated_pt.template_kind, 'Snippet template is {}'.format( - updated_pt.template_kind - ) + assert not updated_pt.template_kind, f'Snippet template is {updated_pt.template_kind}' session.provisioningtemplate.delete(new_name) assert not target_sat.api.ProvisioningTemplate().search( query={'search': f'name=={new_name}'} - ), ( - 'Provisioning template {} expected to be removed but is included in the search ' - 'results'.format(new_name) - ) - - -@pytest.mark.skip_if_open("BZ:1767040") -@pytest.mark.tier3 -def test_negative_template_search_using_url(): - """Satellite should not show full trace on web_browser after invalid search in url - - :id: aeb365dc-49de-11eb-bf99-d46d6dd3b5b2 - - :customerscenario: true - - :expectedresults: Satellite should not show full trace and show correct error message - - :CaseLevel: Integration - - :CaseImportance: High - - :BZ: 1767040 - """ - with Session( - url='/templates/provisioning_templates?search=&page=1"sadfasf', login=False - ) as session: - login_details = { - 'username': settings.server.admin_username, - 'password': settings.server.admin_password, - } - session.login.login(login_details) - error_page = session.browser.selenium.page_source - error_helper_message = ( - "Please include in your report the full error log that can be acquired by running" - ) - trace_link_word = "Full trace" - assert error_helper_message in error_page - assert trace_link_word not in error_page - assert "foreman-rake errors:fetch_log request_id=" in error_page + ), f'Provisioning template {new_name} expected to be removed but is included in the search' From 1f3e75b8c04d5690087b12356717a3959427c322 Mon Sep 17 00:00:00 2001 From: jyejare Date: Thu, 13 Jul 2023 15:08:20 +0530 Subject: [PATCH 077/586] Image Push event for 6.14.z branch --- .github/workflows/merge_to_master.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/merge_to_master.yml b/.github/workflows/merge_to_master.yml index 915e70aef0b..66dba30485b 100644 --- a/.github/workflows/merge_to_master.yml +++ b/.github/workflows/merge_to_master.yml @@ -4,7 +4,7 @@ name: update_robottelo_image on: push: branches: - - master + - 6.14.z env: PYCURL_SSL_LIBRARY: openssl From 6bd1f11ab155ba03ecf9b91f5609bd3f135ec365 Mon Sep 17 00:00:00 2001 From: Matyas Strelec Date: Wed, 12 Jul 2023 14:36:16 +0200 Subject: [PATCH 078/586] Add api/test_rhc.py to test RHCloud Cloud Connector Nailgun support --- tests/foreman/api/test_rhc.py | 107 ++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 tests/foreman/api/test_rhc.py diff --git a/tests/foreman/api/test_rhc.py b/tests/foreman/api/test_rhc.py new file mode 100644 index 00000000000..d3279ebc7cb --- /dev/null +++ b/tests/foreman/api/test_rhc.py @@ -0,0 +1,107 @@ +"""Test class for RH Cloud connector - rhc + +:Requirement: Remoteexecution + +:CaseAutomation: Automated + +:CaseLevel: Acceptance + +:CaseComponent: RHCloud-CloudConnector + +:Team: Platform + +:TestType: Functional + +:CaseImportance: High + +:Upstream: No +""" +import pytest +from fauxfactory import gen_string + +from robottelo.utils.issue_handlers import is_open + + +@pytest.fixture(scope='module') +def fixture_enable_rhc_repos(module_target_sat): + """Enable repos required for configuring RHC.""" + # subscribe rhc satellite to cdn. + module_target_sat.register_to_cdn() + if module_target_sat.os_version.major > 7: + module_target_sat.enable_repo(module_target_sat.REPOS['rhel_bos']['id']) + module_target_sat.enable_repo(module_target_sat.REPOS['rhel_aps']['id']) + else: + module_target_sat.enable_repo(module_target_sat.REPOS['rhscl']['id']) + module_target_sat.enable_repo(module_target_sat.REPOS['rhel']['id']) + + +@pytest.mark.e2e +@pytest.mark.tier3 +def test_positive_configure_cloud_connector( + module_target_sat, module_org, fixture_enable_rhc_repos +): + """ + Enable RH Cloud Connector through API + + :id: 1338dc6a-12e0-4378-9a51-a33f4679ba30 + + :Steps: + + 1. Enable RH Cloud Connector + 2. Check if the task is completed successfully + + :expectedresults: The Cloud Connector has been installed and the service is running + + :CaseImportance: Critical + """ + + # Delete old satellite hostname if BZ#2130173 is open + if is_open('BZ:2130173'): + host = module_target_sat.api.Host().search( + query={'search': f"! {module_target_sat.hostname}"} + )[0] + host.delete() + + # Copy foreman-proxy user's key to root@localhost user's authorized_keys + module_target_sat.add_rex_key(satellite=module_target_sat) + + # Set Host parameter source_display_name to something random. + # To avoid 'name has already been taken' error when run multiple times + # on a machine with the same hostname. + host = module_target_sat.api.Host().search(query={'search': module_target_sat.hostname})[0] + parameters = [{'name': 'source_display_name', 'value': gen_string('alpha')}] + host.host_parameters_attributes = parameters + host.update(['host_parameters_attributes']) + + enable_connector = module_target_sat.api.RHCloud(organization=module_org).enable_connector() + + template_name = 'Configure Cloud Connector' + invocation_id = ( + module_target_sat.api.JobInvocation() + .search(query={'search': f'description="{template_name}"'})[0] + .id + ) + task_id = enable_connector['task_id'] + module_target_sat.wait_for_tasks( + search_query=(f'label = Actions::RemoteExecution::RunHostsJob and id = {task_id}'), + search_rate=15, + ) + + job_output = module_target_sat.cli.JobInvocation.get_output( + {'id': invocation_id, 'host': module_target_sat.hostname} + ) + # get rhc status + rhc_status = module_target_sat.execute('rhc status') + # get rhcd log + rhcd_log = module_target_sat.execute('journalctl --unit=rhcd') + + assert module_target_sat.api.JobInvocation(id=invocation_id).read().status == 0 + assert "Install yggdrasil-worker-forwarder and rhc" in job_output + assert "Restart rhcd" in job_output + assert 'Exit status: 0' in job_output + + assert rhc_status.status == 0 + assert "Connected to Red Hat Subscription Management" in rhc_status.stdout + assert "The Remote Host Configuration daemon is active" in rhc_status.stdout + + assert "error" not in rhcd_log.stdout From 3eadf3c88e9e648afdaf5c911576dc00c6f5e5c5 Mon Sep 17 00:00:00 2001 From: vijay sawant Date: Thu, 13 Jul 2023 17:49:28 +0530 Subject: [PATCH 079/586] delete policies which are no longer needed (#11872) --- tests/foreman/cli/test_oscap.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/foreman/cli/test_oscap.py b/tests/foreman/cli/test_oscap.py index 25307107b25..e2738b25be1 100644 --- a/tests/foreman/cli/test_oscap.py +++ b/tests/foreman/cli/test_oscap.py @@ -475,6 +475,10 @@ def test_postive_create_scap_policy_with_valid_name(self, name, scap_content): } ) assert scap_policy['name'] == name + # Deleting policy which created for all valid input (ex- latin1, cjk, utf-8, etc.) + Scappolicy.delete({'name': scap_policy['name']}) + with pytest.raises(CLIReturnCodeError): + Scappolicy.info({'name': scap_policy['name']}) @pytest.mark.parametrize('name', **parametrized(invalid_names_list())) @pytest.mark.tier2 From a95dd5e34767729208900e60592f75f96109006f Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 13 Jul 2023 13:43:41 -0400 Subject: [PATCH 080/586] [6.14.z] Fix the indent of `test_positive_allow_reregistration_when_dmi_uuid_changed` (#11882) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix the indent of `test_positive_allow_reregistration_when_dmi_uuid_changed` (#11880) Fix the indent of test_positive_allow_reregistration_when_dmi_uuid_changed (cherry picked from commit dcb46d3fabbb5d86ec8f0e6d9e6d09be5e95d736) Co-authored-by: Ondřej Gajdušek --- tests/foreman/api/test_registration.py | 61 ++++++++++++-------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/tests/foreman/api/test_registration.py b/tests/foreman/api/test_registration.py index fca8c7ff8ca..8b23f651ac3 100644 --- a/tests/foreman/api/test_registration.py +++ b/tests/foreman/api/test_registration.py @@ -87,35 +87,32 @@ def test_host_registration_end_to_end( ) assert CLIENT_PORT == rhel_contenthost.subscription_config['server']['port'] - @pytest.mark.tier3 - def test_positive_allow_reregistration_when_dmi_uuid_changed( - self, module_org, rhel_contenthost, target_sat - ): - """Register a content host with a custom DMI UUID, unregistering it, change - the DMI UUID, and re-registering it again - - :id: 7f431cb2-5a63-41f7-a27f-62b86328b50d - - :expectedresults: The content host registers successfully - - :customerscenario: true - - :BZ: 1747177 - - :CaseLevel: Integration - """ - uuid_1 = str(uuid.uuid1()) - uuid_2 = str(uuid.uuid4()) - rhel_contenthost.install_katello_ca(target_sat) - target_sat.execute( - f'echo \'{{"dmi.system.uuid": "{uuid_1}"}}\' > /etc/rhsm/facts/uuid.facts' - ) - result = rhel_contenthost.register_contenthost(module_org.label, lce=ENVIRONMENT) - assert result.status == 0 - result = rhel_contenthost.execute('subscription-manager clean') - assert result.status == 0 - target_sat.execute( - f'echo \'{{"dmi.system.uuid": "{uuid_2}"}}\' > /etc/rhsm/facts/uuid.facts' - ) - result = rhel_contenthost.register_contenthost(module_org.label, lce=ENVIRONMENT) - assert result.status == 0 + +@pytest.mark.tier3 +def test_positive_allow_reregistration_when_dmi_uuid_changed( + module_org, rhel_contenthost, target_sat +): + """Register a content host with a custom DMI UUID, unregistering it, change + the DMI UUID, and re-registering it again + + :id: 7f431cb2-5a63-41f7-a27f-62b86328b50d + + :expectedresults: The content host registers successfully + + :customerscenario: true + + :BZ: 1747177 + + :CaseLevel: Integration + """ + uuid_1 = str(uuid.uuid1()) + uuid_2 = str(uuid.uuid4()) + rhel_contenthost.install_katello_ca(target_sat) + target_sat.execute(f'echo \'{{"dmi.system.uuid": "{uuid_1}"}}\' > /etc/rhsm/facts/uuid.facts') + result = rhel_contenthost.register_contenthost(module_org.label, lce=ENVIRONMENT) + assert result.status == 0 + result = rhel_contenthost.execute('subscription-manager clean') + assert result.status == 0 + target_sat.execute(f'echo \'{{"dmi.system.uuid": "{uuid_2}"}}\' > /etc/rhsm/facts/uuid.facts') + result = rhel_contenthost.register_contenthost(module_org.label, lce=ENVIRONMENT) + assert result.status == 0 From 60fc79252ccd22355ee55b36ce2a05d3a624985e Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 14 Jul 2023 07:52:57 -0400 Subject: [PATCH 081/586] [6.14.z] skip-if removed from e2e tests (#11898) --- tests/foreman/endtoend/test_api_endtoend.py | 37 +++++++++++--------- tests/foreman/endtoend/test_cli_endtoend.py | 38 +++++++++------------ 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/tests/foreman/endtoend/test_api_endtoend.py b/tests/foreman/endtoend/test_api_endtoend.py index 83816d2e61a..93e1f494559 100644 --- a/tests/foreman/endtoend/test_api_endtoend.py +++ b/tests/foreman/endtoend/test_api_endtoend.py @@ -1180,22 +1180,27 @@ def test_positive_end_to_end(self, function_entitlement_manifest, target_sat, rh # BONUS: Create a content host and associate it with promoted # content view and last lifecycle where it exists - if not is_open('BZ:2216461'): - content_host = target_sat.api.Host( - content_facet_attributes={ - 'content_view_id': content_view.id, - 'lifecycle_environment_id': le1.id, - }, - organization=org, - ).create() - # check that content view matches what we passed - assert ( - content_host.content_facet_attributes['content_views'][0]['id'] == content_view.id - ) - # check that lifecycle environment matches - assert ( - content_host.content_facet_attributes['lifecycle_environments'][0]['id'] == le1.id - ) + content_host = target_sat.api.Host( + content_facet_attributes={ + 'content_view_id': content_view.id, + 'lifecycle_environment_id': le1.id, + }, + organization=org, + ).create() + # check that content view matches what we passed + assert ( + content_host.content_facet_attributes['content_view_environments'][0]['content_view'][ + 'id' + ] + == content_view.id + ) + # check that lifecycle environment matches + assert ( + content_host.content_facet_attributes['content_view_environments'][0][ + 'lifecycle_environment' + ]['id'] + == le1.id + ) # step 2.14: Create a new libvirt compute resource target_sat.api.LibvirtComputeResource( diff --git a/tests/foreman/endtoend/test_cli_endtoend.py b/tests/foreman/endtoend/test_cli_endtoend.py index 98aeaafab64..0fb77f62b26 100644 --- a/tests/foreman/endtoend/test_cli_endtoend.py +++ b/tests/foreman/endtoend/test_cli_endtoend.py @@ -40,7 +40,6 @@ from robottelo.config import setting_is_set from robottelo.config import settings from robottelo.constants.repos import CUSTOM_RPM_REPO -from robottelo.utils.issue_handlers import is_open @pytest.fixture(scope='module') @@ -269,28 +268,25 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel # BONUS: Create a content host and associate it with promoted # content view and last lifecycle where it exists - if not is_open('BZ:2216461'): - content_host_name = gen_alphanumeric() - content_host = Host.with_user(user['login'], user['password']).subscription_register( - { - 'content-view-id': content_view['id'], - 'lifecycle-environment-id': lifecycle_environment['id'], - 'name': content_host_name, - 'organization-id': org['id'], - } - ) + content_host_name = gen_alphanumeric() + content_host = Host.with_user(user['login'], user['password']).subscription_register( + { + 'content-view-id': content_view['id'], + 'lifecycle-environment-id': lifecycle_environment['id'], + 'name': content_host_name, + 'organization-id': org['id'], + } + ) - content_host = Host.with_user(user['login'], user['password']).info( - {'id': content_host['id']} - ) - # check that content view matches what we passed - assert content_host['content-information']['content-view']['name'] == content_view['name'] + content_host = Host.with_user(user['login'], user['password']).info({'id': content_host['id']}) + # check that content view matches what we passed + assert content_host['content-information']['content-view']['name'] == content_view['name'] - # check that lifecycle environment matches - assert ( - content_host['content-information']['lifecycle-environment']['name'] - == lifecycle_environment['name'] - ) + # check that lifecycle environment matches + assert ( + content_host['content-information']['lifecycle-environment']['name'] + == lifecycle_environment['name'] + ) # step 2.14: Create a new libvirt compute resource _create( From 37d0b6e82729be16f6062a5823a7ca1532cefea4 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 14 Jul 2023 09:25:38 -0400 Subject: [PATCH 082/586] [6.14.z] audit_log update event, skip on bz (#11899) audit_log update event, skip on bz (#11897) (cherry picked from commit 29992c8a88907273d000f7becb7fefa64f5ae7e7) Co-authored-by: Peter Ondrejka --- tests/foreman/ui/test_audit.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/foreman/ui/test_audit.py b/tests/foreman/ui/test_audit.py index c9262320ab5..f2df8e5ef3d 100644 --- a/tests/foreman/ui/test_audit.py +++ b/tests/foreman/ui/test_audit.py @@ -112,6 +112,7 @@ def test_positive_audit_comment(session, module_org): assert values['comment'] == audit_comment +@pytest.mark.skip_if_open("BZ:2222890") @pytest.mark.tier2 def test_positive_update_event(session, module_org): """When existing content view is updated, corresponding audit entry appear @@ -126,6 +127,8 @@ def test_positive_update_event(session, module_org): :CaseLevel: Integration :CaseImportance: Medium + + :bz: 2222890 """ name = gen_string('alpha') new_name = gen_string('alpha') From 036ef5348755ad49de161795c047544fd422743b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 14 Jul 2023 09:26:32 -0400 Subject: [PATCH 083/586] [6.14.z] Remove e2e flag from failing UI test, and apply it to 2 api tests (#11895) Remove e2e flag from failing UI test, and apply it to 2 api tests (#11886) (cherry picked from commit 930e696180b710af15385c6abff424a5a4d70c91) Co-authored-by: Samuel Bible --- tests/foreman/api/test_contentview.py | 2 ++ tests/foreman/ui/test_contentview.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/foreman/api/test_contentview.py b/tests/foreman/api/test_contentview.py index 52c1fd2cb1e..7bacc6b7b6b 100644 --- a/tests/foreman/api/test_contentview.py +++ b/tests/foreman/api/test_contentview.py @@ -751,6 +751,7 @@ def test_composite_content_view_with_same_repos(self, module_org, target_sat): comp_content_view_info = comp_content_view.version[0].read() assert comp_content_view_info.package_count == 36 + @pytest.mark.e2e @pytest.mark.tier2 def test_ccv_audit_scenarios(self, module_org, target_sat): """Check for various scenarios where a composite content view or it's component @@ -1113,6 +1114,7 @@ def test_positive_promote_rh_custom_spin(self, content_view, module_lce): content_view.read().version[0].promote(data={'environment_ids': module_lce.id}) assert len(content_view.read().version[0].read().environment) == 2 + @pytest.mark.e2e @pytest.mark.tier2 def test_cv_audit_scenarios(self, module_product, target_sat): """Check for various scenarios where a content view's needs_publish flag diff --git a/tests/foreman/ui/test_contentview.py b/tests/foreman/ui/test_contentview.py index 340dea318f6..21d900a582b 100644 --- a/tests/foreman/ui/test_contentview.py +++ b/tests/foreman/ui/test_contentview.py @@ -95,7 +95,6 @@ def test_positive_add_custom_content(session): assert cv['repositories']['resources']['assigned'][0]['Name'] == repo_name -@pytest.mark.e2e @pytest.mark.tier2 @pytest.mark.upgrade def test_positive_end_to_end(session, module_org, target_sat): From b80647e04b3a0451fab80e753caa840a3aeb6a1a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 17 Jul 2023 04:19:13 -0400 Subject: [PATCH 084/586] [6.14.z] Ansible test fixes for selecting only the used role in the test case (#11901) --- tests/foreman/ui/test_ansible.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/foreman/ui/test_ansible.py b/tests/foreman/ui/test_ansible.py index 162c2e107c5..866ad4da049 100644 --- a/tests/foreman/ui/test_ansible.py +++ b/tests/foreman/ui/test_ansible.py @@ -41,13 +41,15 @@ def test_positive_create_and_delete_variable(target_sat): :expectedresults: The variable is successfully created and deleted. """ key = gen_string('alpha') - role = 'redhat.satellite.activation_keys' + + SELECTED_ROLE = 'redhat.satellite.activation_keys' + proxy_id = target_sat.nailgun_smart_proxy.id + target_sat.api.AnsibleRoles().sync(data={'proxy_id': proxy_id, 'role_names': [SELECTED_ROLE]}) with target_sat.ui_session() as session: - session.ansibleroles.import_all_roles() session.ansiblevariables.create( { 'key': key, - 'ansible_role': role, + 'ansible_role': SELECTED_ROLE, } ) assert session.ansiblevariables.search(key)[0]['Name'] == key @@ -69,14 +71,16 @@ def test_positive_create_variable_with_overrides(target_sat): :expectedresults: The variable is successfully created. """ key = gen_string('alpha') - role = 'redhat.satellite.activation_keys' + + SELECTED_ROLE = 'redhat.satellite.activation_keys' + proxy_id = target_sat.nailgun_smart_proxy.id + target_sat.api.AnsibleRoles().sync(data={'proxy_id': proxy_id, 'role_names': [SELECTED_ROLE]}) with target_sat.ui_session() as session: - session.ansibleroles.import_all_roles() session.ansiblevariables.create_with_overrides( { 'key': key, 'description': 'this is a description', - 'ansible_role': role, + 'ansible_role': SELECTED_ROLE, 'parameter_type': 'integer', 'default_value': '11', 'validator_type': 'list', From 08e32f07640d3c218b2a1eb3f68bd0cd3bd33ccb Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 17 Jul 2023 12:24:57 -0400 Subject: [PATCH 085/586] [6.14.z] Fix remote db tests failing because of session_capsule_configured (#11912) Fix remote db tests failing because of session_capsule_configured (cherry picked from commit f48b5f73d8e57280dc15a42352097f91cea8b4e5) Co-authored-by: Jameer Pathan --- pytest_fixtures/component/maintain.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pytest_fixtures/component/maintain.py b/pytest_fixtures/component/maintain.py index 7add0d13aea..b69f76025db 100644 --- a/pytest_fixtures/component/maintain.py +++ b/pytest_fixtures/component/maintain.py @@ -54,19 +54,19 @@ def module_synced_repos(sat_maintain, session_capsule_configured, module_sca_man rh_repo = sat_maintain.api.Repository(id=rh_repo_id).read() rh_repo.sync() - # assign the Library LCE to the Capsule - lce = sat_maintain.api.LifecycleEnvironment(organization=org).search( - query={'search': f'name={constants.ENVIRONMENT}'} - )[0] - session_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( - data={'environment_id': lce.id} - ) - result = session_capsule_configured.nailgun_capsule.content_lifecycle_environments() - assert lce.id in [capsule_lce['id'] for capsule_lce in result['results']] - - # sync the Capsule - sync_status = session_capsule_configured.nailgun_capsule.content_sync() - assert sync_status['result'] == 'success' + if not settings.remotedb.server: + # assign the Library LCE to the Capsule + lce = sat_maintain.api.LifecycleEnvironment(organization=org).search( + query={'search': f'name={constants.ENVIRONMENT}'} + )[0] + session_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( + data={'environment_id': lce.id} + ) + result = session_capsule_configured.nailgun_capsule.content_lifecycle_environments() + assert lce.id in [capsule_lce['id'] for capsule_lce in result['results']] + # sync the Capsule + sync_status = session_capsule_configured.nailgun_capsule.content_sync() + assert sync_status['result'] == 'success' yield {'custom': cust_repo, 'rh': rh_repo} From 387fb3a1eb876ce2877c5df36a1946f257ca2f6a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 17 Jul 2023 12:44:44 -0400 Subject: [PATCH 086/586] [6.14.z] Component audit - Modify/Delete API OS tests (#11904) Component audit - Modify/Delete API OS tests (#11826) * follow ohsnap repofile redirects * component audit cli operating system --------- Co-authored-by: Radek Mynar (cherry picked from commit 531f31b11836cfda6a2f8dda99863675880bd2fa) Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> --- tests/foreman/api/test_operatingsystem.py | 442 +++++++++++++++ tests/foreman/api/test_provisioning.py | 655 ---------------------- 2 files changed, 442 insertions(+), 655 deletions(-) create mode 100644 tests/foreman/api/test_operatingsystem.py diff --git a/tests/foreman/api/test_operatingsystem.py b/tests/foreman/api/test_operatingsystem.py new file mode 100644 index 00000000000..722362063a3 --- /dev/null +++ b/tests/foreman/api/test_operatingsystem.py @@ -0,0 +1,442 @@ +"""Provisioning tests + +:Requirement: Provisioning + +:CaseAutomation: NotAutomated + +:CaseLevel: System + +:CaseComponent: Provisioning + +:Team: Rocket + +:TestType: Functional + +:CaseImportance: Critical + +:Upstream: No +""" +import random +from http.client import NOT_FOUND + +import pytest +from fauxfactory import gen_string +from requests.exceptions import HTTPError + +from robottelo.constants import OPERATING_SYSTEMS +from robottelo.utils.datafactory import invalid_values_list +from robottelo.utils.datafactory import parametrized +from robottelo.utils.datafactory import valid_data_list + + +class TestOperatingSystemParameter: + """Tests for operating system parameters.""" + + @pytest.mark.tier1 + @pytest.mark.upgrade + def test_verify_bugzilla_1114640(self, target_sat): + """Create a parameter for operating system 1. + + :id: e817ae43-226c-44e3-b559-62b8d394047b + + :expectedresults: A parameter is created and can be read afterwards. + + :CaseImportance: Critical + """ + # Check whether OS 1 exists. + os1 = target_sat.api.OperatingSystem(id=1).read_raw() + if os1.status_code == NOT_FOUND and target_sat.api.OperatingSystem().create().id != 1: + pytest.skip('Cannot execute test, as operating system 1 is not available.') + + # Create and read a parameter for operating system 1. The purpose of + # this test is to make sure an HTTP 422 is not returned, but we're also + # going to verify the name and value of the parameter, just for good + # measure. + name = gen_string('utf8') + value = gen_string('utf8') + os_param = target_sat.api.OperatingSystemParameter( + name=name, operatingsystem=1, value=value + ).create() + assert os_param.name == name + assert os_param.value == value + + +class TestOperatingSystem: + """Tests for operating systems.""" + + @pytest.mark.e2e + @pytest.mark.upgrade + def test_positive_end_to_end_os(self, module_target_sat, module_org, module_architecture): + """End-to-end test for Operating system + + :id: ecf2196d-96dd-4ed2-abfa-a5ffade75d91 + + :steps: + 1. Create OS + 2. Check OS is created with all given fields + 3. Read OS + 4. Update OS + 5. Check OS is updated with all given fields + 6. Delete OS + 7. Check if OS is deleted + + :expectedresults: All CRUD operations are performed successfully. + + :BZ: 2101435, 1230902 + """ + name = gen_string('alpha') + desc = gen_string('alpha') + os_family = 'Redhat' + minor_version = gen_string('numeric') + major_version = gen_string('numeric', 5) + pass_hash = 'SHA256' + ptable = module_target_sat.api.PartitionTable().create() + medium = module_target_sat.api.Media(organization=[module_org]).create() + template = module_target_sat.api.ProvisioningTemplate( + organization=[module_org], + ).create() + # Create OS + os = module_target_sat.api.OperatingSystem( + name=name, + description=desc, + minor=minor_version, + major=major_version, + family=os_family, + architecture=[module_architecture], + password_hash=pass_hash, + provisioning_template=[template], + ptable=[ptable], + medium=[medium], + ).create() + assert os.name == name + assert os.family == os_family + assert os.minor == str(minor_version) + assert os.major == major_version + assert os.description == desc + assert os.architecture[0].id == module_architecture.id + assert os.password_hash == pass_hash + assert os.ptable[0].id == ptable.id + assert os.provisioning_template[0].id == template.id + assert os.medium[0].id == medium.id + # Read OS + os = module_target_sat.api.OperatingSystem(id=os.id).read() + assert os.name == name + assert os.family == os_family + assert os.minor == minor_version + assert os.major == major_version + assert os.description == desc + assert os.architecture[0].id == module_architecture.id + assert os.password_hash == pass_hash + assert str(ptable.id) in str(os.ptable) + assert str(template.id) in str(os.provisioning_template) + assert os.medium[0].id == medium.id + new_name = gen_string('alpha') + new_desc = gen_string('alpha') + new_os_family = 'Rhcos' + new_minor_version = gen_string('numeric') + new_major_version = gen_string('numeric', 5) + new_pass_hash = 'SHA512' + new_arch = module_target_sat.api.Architecture().create() + new_ptable = module_target_sat.api.PartitionTable().create() + new_medium = module_target_sat.api.Media(organization=[module_org]).create() + new_template = module_target_sat.api.ProvisioningTemplate( + organization=[module_org] + ).create() + # Update OS + os = module_target_sat.api.OperatingSystem( + id=os.id, + name=new_name, + description=new_desc, + minor=new_minor_version, + major=new_major_version, + family=new_os_family, + architecture=[new_arch], + password_hash=new_pass_hash, + provisioning_template=[new_template], + ptable=[new_ptable], + medium=[new_medium], + ).update( + [ + 'name', + 'description', + 'minor', + 'major', + 'family', + 'architecture', + 'password_hash', + 'provisioning_template', + 'ptable', + 'medium', + ] + ) + assert os.name == new_name + assert os.family == new_os_family + assert os.minor == new_minor_version + assert os.major == new_major_version + assert os.description == new_desc + assert os.architecture[0].id == new_arch.id + assert os.password_hash == new_pass_hash + assert os.ptable[0].id == new_ptable.id + assert os.provisioning_template[0].id == new_template.id + assert os.medium[0].id == new_medium.id + # Delete OS + module_target_sat.api.OperatingSystem(id=os.id).delete() + with pytest.raises(HTTPError): + module_target_sat.api.OperatingSystem(id=os.id).read() + + @pytest.mark.tier1 + @pytest.mark.parametrize('name', **parametrized(valid_data_list())) + def test_positive_create_with_name(self, name, target_sat): + """Create operating system with valid name only + + :id: e95707bf-3344-4d85-866f-4642a8f66cff + + :parametrized: yes + + :expectedresults: Operating system entity is created and has proper + name + + :CaseImportance: Critical + """ + os = target_sat.api.OperatingSystem(name=name).create() + assert os.name == name + + @pytest.mark.tier2 + def test_positive_create_with_archs(self, target_sat): + """Create an operating system that points at multiple different + architectures. + + :id: afd26c6a-bf54-4883-baa5-95f263e6fb36 + + :expectedresults: The operating system is created and points at the + expected architectures. + + :CaseLevel: Integration + """ + amount = range(random.randint(3, 5)) + archs = [target_sat.api.Architecture().create() for _ in amount] + operating_sys = target_sat.api.OperatingSystem(architecture=archs).create() + assert len(operating_sys.architecture) == len(amount) + assert {arch.id for arch in operating_sys.architecture} == {arch.id for arch in archs} + + @pytest.mark.tier2 + def test_positive_create_with_ptables(self, target_sat): + """Create an operating system that points at multiple different + partition tables. + + :id: ed48a279-a222-45ce-81e4-72ae9422482a + + :expectedresults: The operating system is created and points at the + expected partition tables. + + :CaseLevel: Integration + """ + amount = range(random.randint(3, 5)) + ptables = [target_sat.api.PartitionTable().create() for _ in amount] + operating_sys = target_sat.api.OperatingSystem(ptable=ptables).create() + assert len(operating_sys.ptable) == len(amount) + assert {ptable.id for ptable in operating_sys.ptable} == {ptable.id for ptable in ptables} + + @pytest.mark.tier1 + @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) + def test_negative_create_with_invalid_name(self, name, target_sat): + """Try to create operating system entity providing an invalid + name + + :id: cd4286fd-7128-4385-9c8d-ef979c22ee38 + + :parametrized: yes + + :expectedresults: Operating system entity is not created + + :CaseImportance: Critical + """ + with pytest.raises(HTTPError): + target_sat.api.OperatingSystem(name=name).create() + + @pytest.mark.tier1 + def test_negative_create_with_invalid_os_family(self, target_sat): + """Try to create operating system entity providing an invalid + operating system family + + :id: 205a433d-750b-4b06-9fd4-274303780d6d + + :expectedresults: Operating system entity is not created + + :CaseImportance: Critical + """ + with pytest.raises(HTTPError): + target_sat.api.OperatingSystem(family='NON_EXISTENT_OS').create() + + @pytest.mark.tier1 + def test_negative_create_with_too_long_description(self, target_sat): + """Try to create operating system entity providing too long + description value + + :id: fe5fc36a-5994-4d8a-91f6-0425765b8c39 + + :expectedresults: Operating system entity is not created + + :BZ: 1328935 + + :CaseImportance: Critical + """ + with pytest.raises(HTTPError): + target_sat.api.OperatingSystem(description=gen_string('alphanumeric', 256)).create() + + @pytest.mark.skip_if_open('BZ:2101435') + @pytest.mark.tier1 + @pytest.mark.parametrize('major_version', **parametrized((gen_string('numeric', 6), '', '-6'))) + def test_negative_create_with_invalid_major_version(self, major_version, target_sat): + """Try to create operating system entity providing incorrect + major version value (More than 5 characters, empty value, negative + number) + + :id: f2646bc2-d639-4079-bdcb-ff76679f1457 + + :parametrized: yes + + :expectedresults: Operating system entity is not created + + :BZ: 2101435 + + :CaseImportance: Critical + """ + with pytest.raises(HTTPError): + target_sat.api.OperatingSystem(major=major_version).create() + + @pytest.mark.tier1 + @pytest.mark.parametrize('minor_version', **parametrized((gen_string('numeric', 17), '-5'))) + def test_negative_create_with_invalid_minor_version(self, minor_version, target_sat): + """Try to create operating system entity providing incorrect + minor version value (More than 16 characters and negative number) + + :id: dec4b456-153c-4a66-8b8e-b12ac7800e51 + + :parametrized: yes + + :expectedresults: Operating system entity is not created + + :CaseImportance: Critical + """ + with pytest.raises(HTTPError): + target_sat.api.OperatingSystem(minor=minor_version).create() + + @pytest.mark.tier1 + def test_negative_create_with_invalid_password_hash(self, target_sat): + """Try to create operating system entity providing invalid + password hash value + + :id: 9cfcb6d4-0601-4fc7-bd1e-8b8327129a69 + + :expectedresults: Operating system entity is not created + + :CaseImportance: Critical + """ + with pytest.raises(HTTPError): + target_sat.api.OperatingSystem(password_hash='INVALID_HASH').create() + + @pytest.mark.tier1 + def test_negative_create_with_same_name_and_version(self, target_sat): + """Create operating system providing valid name and major + version. Then try to create operating system using the same name and + version + + :id: 3f2ca323-7789-4d2b-bf21-2454317147ff + + :expectedresults: Second operating system entity is not created + + :CaseImportance: Critical + """ + os = target_sat.api.OperatingSystem().create() + with pytest.raises(HTTPError): + target_sat.api.OperatingSystem(name=os.name, major=os.major).create() + + @pytest.mark.tier2 + @pytest.mark.upgrade + def test_positive_update_medias(self, module_org, module_target_sat): + """Create an operating system that points at media entity and + then update it to point to another multiple different medias. + + :id: 756c4aa8-278d-488e-b48f-a8d2ace4526e + + :expectedresults: The operating system is updated and points at the + expected medias. + + :CaseLevel: Integration + """ + initial_media = module_target_sat.api.Media(organization=[module_org]).create() + os = module_target_sat.api.OperatingSystem(medium=[initial_media]).create() + assert len(os.medium) == 1 + assert os.medium[0].id == initial_media.id + amount = range(random.randint(3, 5)) + medias = [module_target_sat.api.Media().create() for _ in amount] + os = module_target_sat.api.OperatingSystem(id=os.id, medium=medias).update(['medium']) + assert len(os.medium) == len(amount) + assert {medium.id for medium in os.medium} == {medium.id for medium in medias} + + @pytest.mark.tier1 + @pytest.mark.parametrize('new_name', **parametrized(invalid_values_list())) + def test_negative_update_name(self, new_name, target_sat): + """Create operating system entity providing the initial name, + then update its name to invalid one. + + :id: 3ba55d6e-99cb-4878-b41b-a59476d1db58 + + :parametrized: yes + + :expectedresults: Operating system entity is not updated + + :CaseImportance: Critical + """ + os = target_sat.api.OperatingSystem().create() + with pytest.raises(HTTPError): + os = target_sat.api.OperatingSystem(id=os.id, name=new_name).update(['name']) + + @pytest.mark.skip_if_open('BZ:2101435') + @pytest.mark.tier1 + def test_negative_update_major_version(self, target_sat): + """Create operating entity providing the initial major version, + then update that version to invalid one. + + :id: de07c2f7-0896-493d-976c-e9f3a8a57025 + + :expectedresults: Operating system entity is not updated + + :BZ: 2101435 + + :CaseImportance: Critical + """ + os = target_sat.api.OperatingSystem().create() + with pytest.raises(HTTPError): + target_sat.api.OperatingSystem(id=os.id, major='-20').update(['major']) + + @pytest.mark.tier1 + def test_negative_update_minor_version(self, target_sat): + """Create operating entity providing the initial minor version, + then update that version to invalid one. + + :id: 130d028f-302d-4c20-b35c-c7f024f3897b + + :expectedresults: Operating system entity is not updated + + :CaseImportance: Critical + """ + os = target_sat.api.OperatingSystem(minor=gen_string('numeric')).create() + with pytest.raises(HTTPError): + target_sat.api.OperatingSystem(id=os.id, minor='INVALID_VERSION').update(['minor']) + + @pytest.mark.tier1 + def test_negative_update_os_family(self, target_sat): + """Create operating entity providing the initial os family, then + update that family to invalid one. + + :id: fc11506e-8a46-470b-bde0-6fc5db98463f + + :expectedresults: Operating system entity is not updated + + :CaseImportance: Critical + """ + os = target_sat.api.OperatingSystem(family=OPERATING_SYSTEMS[0]).create() + with pytest.raises(HTTPError): + target_sat.api.OperatingSystem(id=os.id, family='NON_EXISTENT_OS').update(['family']) diff --git a/tests/foreman/api/test_provisioning.py b/tests/foreman/api/test_provisioning.py index bc6b80e6291..e129fac7bd6 100644 --- a/tests/foreman/api/test_provisioning.py +++ b/tests/foreman/api/test_provisioning.py @@ -16,21 +16,11 @@ :Upstream: No """ -import random -from http.client import NOT_FOUND - import pytest from fauxfactory import gen_string from packaging.version import Version -from requests.exceptions import HTTPError from wait_for import wait_for -from robottelo.constants import OPERATING_SYSTEMS -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list -from robottelo.utils.issue_handlers import is_open - @pytest.mark.e2e @pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) @@ -150,648 +140,3 @@ def test_rhel_pxe_provisioning( # assert that the host is subscribed and consumes # subsctiption provided by the activation key assert provisioning_host.subscribed, 'Host is not subscribed' - - -class TestOperatingSystemParameter: - """Tests for operating system parameters.""" - - @pytest.mark.tier1 - @pytest.mark.upgrade - def test_verify_bugzilla_1114640(self, target_sat): - """Create a parameter for operating system 1. - - :id: e817ae43-226c-44e3-b559-62b8d394047b - - :expectedresults: A parameter is created and can be read afterwards. - - :CaseImportance: Critical - """ - # Check whether OS 1 exists. - os1 = target_sat.api.OperatingSystem(id=1).read_raw() - if os1.status_code == NOT_FOUND and target_sat.api.OperatingSystem().create().id != 1: - pytest.skip('Cannot execute test, as operating system 1 is not available.') - - # Create and read a parameter for operating system 1. The purpose of - # this test is to make sure an HTTP 422 is not returned, but we're also - # going to verify the name and value of the parameter, just for good - # measure. - name = gen_string('utf8') - value = gen_string('utf8') - os_param = target_sat.api.OperatingSystemParameter( - name=name, operatingsystem=1, value=value - ).create() - assert os_param.name == name - assert os_param.value == value - - -class TestOperatingSystem: - """Tests for operating systems.""" - - @pytest.mark.tier1 - @pytest.mark.parametrize('name', **parametrized(valid_data_list())) - def test_positive_create_with_name(self, name, target_sat): - """Create operating system with valid name only - - :id: e95707bf-3344-4d85-866f-4642a8f66cff - - :parametrized: yes - - :expectedresults: Operating system entity is created and has proper - name - - :CaseImportance: Critical - """ - os = target_sat.api.OperatingSystem(name=name).create() - assert os.name == name - - @pytest.mark.tier1 - @pytest.mark.parametrize('os_family', **parametrized(OPERATING_SYSTEMS)) - def test_positive_create_with_os_family(self, os_family, target_sat): - """Create operating system with every OS family possible - - :id: 6ad32d22-53cc-4bab-ac10-f466f75d7cc6 - - :parametrized: yes - - :expectedresults: Operating system entity is created and has proper OS - family assigned - - :CaseAutomation: Automated - - :CaseImportance: Critical - """ - if is_open('BZ:1709683') and os_family == 'Debian': - pytest.skip("BZ 1709683") - os = target_sat.api.OperatingSystem(family=os_family).create() - assert os.family == os_family - - @pytest.mark.tier1 - def test_positive_create_with_minor_version(self, target_sat): - """Create operating system with minor version - - :id: fc2e36ca-eb5c-440b-957e-390cd9820945 - - :expectedresults: Operating system entity is created and has proper - minor version - - :CaseImportance: Critical - """ - minor_version = gen_string('numeric') - os = target_sat.api.OperatingSystem(minor=minor_version).create() - assert os.minor == minor_version - - @pytest.mark.tier1 - def test_positive_read_minor_version_as_string(self, target_sat): - """Create an operating system with an integer minor version. - - :id: b45e0b94-62f7-45ff-a19e-83c7a0f51339 - - :expectedresults: The minor version can be read back as a string. - - :CaseImportance: Critical - - :BZ: 1230902 - """ - minor = int(gen_string('numeric', random.randint(1, 16))) - operating_sys = target_sat.api.OperatingSystem(minor=minor).create() - assert operating_sys.minor == str(minor) - - @pytest.mark.tier1 - @pytest.mark.parametrize('desc', **parametrized(valid_data_list())) - def test_positive_create_with_description(self, desc, target_sat): - """Create operating system with description - - :id: 980e6411-da11-4fec-ae46-47722367ae40 - - :parametrized: yes - - :expectedresults: Operating system entity is created and has proper - description - - :CaseImportance: Critical - """ - name = gen_string('utf8') - os = target_sat.api.OperatingSystem(name=name, description=desc).create() - assert os.name == name - assert os.description == desc - - @pytest.mark.tier1 - @pytest.mark.parametrize('pass_hash', **parametrized(('SHA256', 'SHA512'))) - def test_positive_create_with_password_hash(self, pass_hash, target_sat): - """Create operating system with valid password hash option - - :id: 00830e71-b414-41ab-bc8f-03fd2fbd5a84 - - :parametrized: yes - - :expectedresults: Operating system entity is created and has proper - password hash type - - :CaseImportance: Critical - """ - os = target_sat.api.OperatingSystem(password_hash=pass_hash).create() - assert os.password_hash == pass_hash - - @pytest.mark.tier2 - def test_positive_create_with_arch(self, target_sat): - """Create an operating system that points at an architecture. - - :id: 6a3f7183-b0bf-4834-8c69-a49fe8d7ee5a - - :expectedresults: The operating system is created and points at the - given architecture. - - :CaseLevel: Integration - """ - arch = target_sat.api.Architecture().create() - operating_sys = target_sat.api.OperatingSystem(architecture=[arch]).create() - assert len(operating_sys.architecture) == 1 - assert operating_sys.architecture[0].id == arch.id - - @pytest.mark.tier2 - def test_positive_create_with_archs(self, target_sat): - """Create an operating system that points at multiple different - architectures. - - :id: afd26c6a-bf54-4883-baa5-95f263e6fb36 - - :expectedresults: The operating system is created and points at the - expected architectures. - - :CaseLevel: Integration - """ - amount = range(random.randint(3, 5)) - archs = [target_sat.api.Architecture().create() for _ in amount] - operating_sys = target_sat.api.OperatingSystem(architecture=archs).create() - assert len(operating_sys.architecture) == len(amount) - assert {arch.id for arch in operating_sys.architecture} == {arch.id for arch in archs} - - @pytest.mark.tier2 - def test_positive_create_with_ptable(self, target_sat): - """Create an operating system that points at a partition table. - - :id: bef37ff9-d8fa-4518-9073-0518aa9f9a42 - - :expectedresults: The operating system is created and points at the - given partition table. - - :CaseLevel: Integration - """ - ptable = target_sat.api.PartitionTable().create() - operating_sys = target_sat.api.OperatingSystem(ptable=[ptable]).create() - assert len(operating_sys.ptable) == 1 - assert operating_sys.ptable[0].id == ptable.id - - @pytest.mark.tier2 - def test_positive_create_with_ptables(self, target_sat): - """Create an operating system that points at multiple different - partition tables. - - :id: ed48a279-a222-45ce-81e4-72ae9422482a - - :expectedresults: The operating system is created and points at the - expected partition tables. - - :CaseLevel: Integration - """ - amount = range(random.randint(3, 5)) - ptables = [target_sat.api.PartitionTable().create() for _ in amount] - operating_sys = target_sat.api.OperatingSystem(ptable=ptables).create() - assert len(operating_sys.ptable) == len(amount) - assert {ptable.id for ptable in operating_sys.ptable} == {ptable.id for ptable in ptables} - - @pytest.mark.tier2 - def test_positive_create_with_media(self, module_org, module_target_sat): - """Create an operating system that points at a media. - - :id: 56fadee4-c676-48b6-a2db-e6fef9d2a575 - - :expectedresults: The operating system is created and points at the - given media. - - :CaseLevel: Integration - """ - medium = module_target_sat.api.Media(organization=[module_org]).create() - operating_sys = module_target_sat.api.OperatingSystem(medium=[medium]).create() - assert len(operating_sys.medium) == 1 - assert operating_sys.medium[0].id == medium.id - - @pytest.mark.tier2 - def test_positive_create_with_template(self, module_org, module_target_sat): - """Create an operating system that points at a provisioning template. - - :id: df73ecba-5a1c-4201-9c2f-b2e03e8fec25 - - :expectedresults: The operating system is created and points at the - expected provisioning template. - - :CaseLevel: Integration - """ - template = module_target_sat.api.ProvisioningTemplate(organization=[module_org]).create() - operating_sys = module_target_sat.api.OperatingSystem( - provisioning_template=[template] - ).create() - assert len(operating_sys.provisioning_template) == 1 - assert operating_sys.provisioning_template[0].id == template.id - - @pytest.mark.tier1 - @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) - def test_negative_create_with_invalid_name(self, name, target_sat): - """Try to create operating system entity providing an invalid - name - - :id: cd4286fd-7128-4385-9c8d-ef979c22ee38 - - :parametrized: yes - - :expectedresults: Operating system entity is not created - - :CaseImportance: Critical - """ - with pytest.raises(HTTPError): - target_sat.api.OperatingSystem(name=name).create() - - @pytest.mark.tier1 - def test_negative_create_with_invalid_os_family(self, target_sat): - """Try to create operating system entity providing an invalid - operating system family - - :id: 205a433d-750b-4b06-9fd4-274303780d6d - - :expectedresults: Operating system entity is not created - - :CaseImportance: Critical - """ - with pytest.raises(HTTPError): - target_sat.api.OperatingSystem(family='NON_EXISTENT_OS').create() - - @pytest.mark.tier1 - def test_negative_create_with_too_long_description(self, target_sat): - """Try to create operating system entity providing too long - description value - - :id: fe5fc36a-5994-4d8a-91f6-0425765b8c39 - - :expectedresults: Operating system entity is not created - - :BZ: 1328935 - - :CaseImportance: Critical - """ - with pytest.raises(HTTPError): - target_sat.api.OperatingSystem(description=gen_string('alphanumeric', 256)).create() - - @pytest.mark.skip_if_open('BZ:2101435') - @pytest.mark.tier1 - @pytest.mark.parametrize('major_version', **parametrized((gen_string('numeric', 6), '', '-6'))) - def test_negative_create_with_invalid_major_version(self, major_version, target_sat): - """Try to create operating system entity providing incorrect - major version value (More than 5 characters, empty value, negative - number) - - :id: f2646bc2-d639-4079-bdcb-ff76679f1457 - - :parametrized: yes - - :expectedresults: Operating system entity is not created - - :BZ: 2101435 - - :CaseImportance: Critical - """ - with pytest.raises(HTTPError): - target_sat.api.OperatingSystem(major=major_version).create() - - @pytest.mark.tier1 - @pytest.mark.parametrize('minor_version', **parametrized((gen_string('numeric', 17), '-5'))) - def test_negative_create_with_invalid_minor_version(self, minor_version, target_sat): - """Try to create operating system entity providing incorrect - minor version value (More than 16 characters and negative number) - - :id: dec4b456-153c-4a66-8b8e-b12ac7800e51 - - :parametrized: yes - - :expectedresults: Operating system entity is not created - - :CaseImportance: Critical - """ - with pytest.raises(HTTPError): - target_sat.api.OperatingSystem(minor=minor_version).create() - - @pytest.mark.tier1 - def test_negative_create_with_invalid_password_hash(self, target_sat): - """Try to create operating system entity providing invalid - password hash value - - :id: 9cfcb6d4-0601-4fc7-bd1e-8b8327129a69 - - :expectedresults: Operating system entity is not created - - :CaseImportance: Critical - """ - with pytest.raises(HTTPError): - target_sat.api.OperatingSystem(password_hash='INVALID_HASH').create() - - @pytest.mark.tier1 - def test_negative_create_with_same_name_and_version(self, target_sat): - """Create operating system providing valid name and major - version. Then try to create operating system using the same name and - version - - :id: 3f2ca323-7789-4d2b-bf21-2454317147ff - - :expectedresults: Second operating system entity is not created - - :CaseImportance: Critical - """ - os = target_sat.api.OperatingSystem().create() - with pytest.raises(HTTPError): - target_sat.api.OperatingSystem(name=os.name, major=os.major).create() - - @pytest.mark.tier1 - @pytest.mark.parametrize('new_name', **parametrized(valid_data_list())) - def test_positive_update_name(self, new_name, target_sat): - """Create operating system entity providing the initial name, - then update its name to another valid name. - - :id: 2898e16a-865a-4de6-b2a5-bb0934fc2b76 - - :parametrized: yes - - :expectedresults: Operating system entity is created and updated - properly - - :CaseImportance: Critical - """ - os = target_sat.api.OperatingSystem().create() - os = target_sat.api.OperatingSystem(id=os.id, name=new_name).update(['name']) - assert os.name == new_name - - @pytest.mark.tier1 - @pytest.mark.parametrize('new_desc', **parametrized(valid_data_list())) - def test_positive_update_description(self, new_desc, target_sat): - """Create operating entity providing the initial description, - then update that description to another valid one. - - :id: c809700a-b6ab-4651-9bd0-d0d9bd6a47dd - - :parametrized: yes - - :expectedresults: Operating system entity is created and updated - properly - - :CaseImportance: Critical - """ - os = target_sat.api.OperatingSystem(description=gen_string('utf8')).create() - os = target_sat.api.OperatingSystem(id=os.id, description=new_desc).update(['description']) - assert os.description == new_desc - - @pytest.mark.tier1 - def test_positive_update_major_version(self, target_sat): - """Create operating entity providing the initial major version, - then update that version to another valid one. - - :id: e57fd4a3-f0ae-49fb-bd84-9a6ec606a2a2 - - :expectedresults: Operating system entity is created and updated - properly - - :CaseImportance: Critical - """ - os = target_sat.api.OperatingSystem().create() - new_major_version = gen_string('numeric', 5) - os = target_sat.api.OperatingSystem(id=os.id, major=new_major_version).update(['major']) - assert os.major == new_major_version - - @pytest.mark.tier1 - def test_positive_update_minor_version(self, target_sat): - """Create operating entity providing the initial minor version, - then update that version to another valid one. - - :id: ca36f7cf-4487-4743-be06-52c5f47ffe71 - - :expectedresults: Operating system entity is created and updated - properly - - :CaseImportance: Critical - """ - os = target_sat.api.OperatingSystem(minor=gen_string('numeric')).create() - new_minor_version = gen_string('numeric') - os = target_sat.api.OperatingSystem(id=os.id, minor=new_minor_version).update(['minor']) - assert os.minor == new_minor_version - - @pytest.mark.tier1 - def test_positive_update_os_family(self, target_sat): - """Create operating entity providing the initial os family, then - update that family to another valid one from the list. - - :id: 3d1f8fdc-d2de-4277-a0ba-07228a2fae82 - - :expectedresults: Operating system entity is created and updated - properly - - :CaseImportance: Critical - """ - os = target_sat.api.OperatingSystem(family=OPERATING_SYSTEMS[0]).create() - new_os_family = OPERATING_SYSTEMS[random.randint(1, len(OPERATING_SYSTEMS) - 1)] - os = target_sat.api.OperatingSystem(id=os.id, family=new_os_family).update(['family']) - assert os.family == new_os_family - - @pytest.mark.tier2 - @pytest.mark.upgrade - def test_positive_update_arch(self, target_sat): - """Create an operating system that points at an architecture and - then update it to point to another architecture - - :id: ad69b4a3-6371-4516-b5ce-f6298edf35b3 - - :expectedresults: The operating system is updated and points at the - expected architecture. - - :CaseLevel: Integration - """ - arch_1 = target_sat.api.Architecture().create() - arch_2 = target_sat.api.Architecture().create() - os = target_sat.api.OperatingSystem(architecture=[arch_1]).create() - assert len(os.architecture) == 1 - assert os.architecture[0].id == arch_1.id - os = target_sat.api.OperatingSystem(id=os.id, architecture=[arch_2]).update( - ['architecture'] - ) - assert len(os.architecture) == 1 - assert os.architecture[0].id == arch_2.id - - @pytest.mark.tier2 - def test_positive_update_ptable(self, target_sat): - """Create an operating system that points at partition table and - then update it to point to another partition table - - :id: 0dde5372-4b90-4c83-b497-31e94065adab - - :expectedresults: The operating system is updated and points at the - expected partition table. - - :CaseLevel: Integration - """ - ptable_1 = target_sat.api.PartitionTable().create() - ptable_2 = target_sat.api.PartitionTable().create() - os = target_sat.api.OperatingSystem(ptable=[ptable_1]).create() - assert len(os.ptable) == 1 - assert os.ptable[0].id == ptable_1.id - os = target_sat.api.OperatingSystem(id=os.id, ptable=[ptable_2]).update(['ptable']) - assert len(os.ptable) == 1 - assert os.ptable[0].id == ptable_2.id - - @pytest.mark.tier2 - def test_positive_update_media(self, module_org, module_target_sat): - """Create an operating system that points at media entity and - then update it to point to another media - - :id: 18b5f6b5-52ab-4722-8412-f0de85ad20fe - - :expectedresults: The operating system is updated and points at the - expected media. - - :CaseLevel: Integration - """ - media_1 = module_target_sat.api.Media(organization=[module_org]).create() - media_2 = module_target_sat.api.Media(organization=[module_org]).create() - os = module_target_sat.api.OperatingSystem(medium=[media_1]).create() - assert len(os.medium) == 1 - assert os.medium[0].id == media_1.id - os = module_target_sat.api.OperatingSystem(id=os.id, medium=[media_2]).update(['medium']) - assert len(os.medium) == 1 - assert os.medium[0].id == media_2.id - - @pytest.mark.tier2 - @pytest.mark.upgrade - def test_positive_update_medias(self, module_org, module_target_sat): - """Create an operating system that points at media entity and - then update it to point to another multiple different medias. - - :id: 756c4aa8-278d-488e-b48f-a8d2ace4526e - - :expectedresults: The operating system is updated and points at the - expected medias. - - :CaseLevel: Integration - """ - initial_media = module_target_sat.api.Media(organization=[module_org]).create() - os = module_target_sat.api.OperatingSystem(medium=[initial_media]).create() - assert len(os.medium) == 1 - assert os.medium[0].id == initial_media.id - amount = range(random.randint(3, 5)) - medias = [module_target_sat.api.Media().create() for _ in amount] - os = module_target_sat.api.OperatingSystem(id=os.id, medium=medias).update(['medium']) - assert len(os.medium) == len(amount) - assert {medium.id for medium in os.medium} == {medium.id for medium in medias} - - @pytest.mark.tier2 - @pytest.mark.upgrade - def test_positive_update_template(self, module_org, module_target_sat): - """Create an operating system that points at provisioning template and - then update it to point to another template - - :id: 02125a7a-905a-492a-a49b-768adf4ac00c - - :expectedresults: The operating system is updated and points at the - expected provisioning template. - - :CaseLevel: Integration - """ - template_1 = module_target_sat.api.ProvisioningTemplate(organization=[module_org]).create() - template_2 = module_target_sat.api.ProvisioningTemplate(organization=[module_org]).create() - os = module_target_sat.api.OperatingSystem(provisioning_template=[template_1]).create() - assert len(os.provisioning_template) == 1 - assert os.provisioning_template[0].id == template_1.id - os = module_target_sat.api.OperatingSystem( - id=os.id, provisioning_template=[template_2] - ).update(['provisioning_template']) - assert len(os.provisioning_template) == 1 - assert os.provisioning_template[0].id == template_2.id - - @pytest.mark.tier1 - @pytest.mark.parametrize('new_name', **parametrized(invalid_values_list())) - def test_negative_update_name(self, new_name, target_sat): - """Create operating system entity providing the initial name, - then update its name to invalid one. - - :id: 3ba55d6e-99cb-4878-b41b-a59476d1db58 - - :parametrized: yes - - :expectedresults: Operating system entity is not updated - - :CaseImportance: Critical - """ - os = target_sat.api.OperatingSystem().create() - with pytest.raises(HTTPError): - os = target_sat.api.OperatingSystem(id=os.id, name=new_name).update(['name']) - - @pytest.mark.skip_if_open('BZ:2101435') - @pytest.mark.tier1 - def test_negative_update_major_version(self, target_sat): - """Create operating entity providing the initial major version, - then update that version to invalid one. - - :id: de07c2f7-0896-493d-976c-e9f3a8a57025 - - :expectedresults: Operating system entity is not updated - - :BZ: 2101435 - - :CaseImportance: Critical - """ - os = target_sat.api.OperatingSystem().create() - with pytest.raises(HTTPError): - target_sat.api.OperatingSystem(id=os.id, major='-20').update(['major']) - - @pytest.mark.tier1 - def test_negative_update_minor_version(self, target_sat): - """Create operating entity providing the initial minor version, - then update that version to invalid one. - - :id: 130d028f-302d-4c20-b35c-c7f024f3897b - - :expectedresults: Operating system entity is not updated - - :CaseImportance: Critical - """ - os = target_sat.api.OperatingSystem(minor=gen_string('numeric')).create() - with pytest.raises(HTTPError): - target_sat.api.OperatingSystem(id=os.id, minor='INVALID_VERSION').update(['minor']) - - @pytest.mark.tier1 - def test_negative_update_os_family(self, target_sat): - """Create operating entity providing the initial os family, then - update that family to invalid one. - - :id: fc11506e-8a46-470b-bde0-6fc5db98463f - - :expectedresults: Operating system entity is not updated - - :CaseImportance: Critical - """ - os = target_sat.api.OperatingSystem(family=OPERATING_SYSTEMS[0]).create() - with pytest.raises(HTTPError): - target_sat.api.OperatingSystem(id=os.id, family='NON_EXISTENT_OS').update(['family']) - - @pytest.mark.tier1 - @pytest.mark.upgrade - @pytest.mark.parametrize('name', **parametrized(valid_data_list())) - def test_positive_delete_os(self, name, target_sat): - """Create new operating system entity and then delete it. - - :id: 3dbffb56-ad99-441d-921c-0fad6504d257 - - :parametrized: yes - - :expectedresults: Operating System entity is deleted successfully - - :CaseImportance: Critical - """ - os = target_sat.api.OperatingSystem(name=name).create() - os.delete() - with pytest.raises(HTTPError): - os.read() From 94decfa86ce41e3ea0bd95d1bd2e2de0fc6ab28a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 17 Jul 2023 13:09:30 -0400 Subject: [PATCH 087/586] [6.14.z] fix in fixture for multiple hosts (#11915) --- pytest_fixtures/core/contenthosts.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pytest_fixtures/core/contenthosts.py b/pytest_fixtures/core/contenthosts.py index 830070c6b10..9a5b6490a34 100644 --- a/pytest_fixtures/core/contenthosts.py +++ b/pytest_fixtures/core/contenthosts.py @@ -113,14 +113,12 @@ def mod_content_hosts(request): @pytest.fixture() -def registered_hosts(request, target_sat, module_org): +def registered_hosts(request, target_sat, module_org, module_ak_with_cv): """Fixture that registers content hosts to Satellite, based on rh_cloud setup""" with Broker(**host_conf(request), host_class=ContentHost, _count=2) as hosts: for vm in hosts: repo = settings.repos['SATCLIENT_REPO'][f'RHEL{vm.os_version.major}'] - target_sat.register_host_custom_repo(module_org, vm, [repo]) - vm.install_katello_host_tools() - vm.add_rex_key(target_sat) + vm.register(module_org, None, module_ak_with_cv.name, target_sat, repo=repo) yield hosts From 0da4acfacf3c706e3550efec5b1e6b241260b402 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 17 Jul 2023 13:48:26 -0400 Subject: [PATCH 088/586] [6.14.z] Add --f-p-c-pulcpore-hide-guarded-distributions option (#11769) Add --f-p-c-pulcpore-hide-guarded-distributions option A new option was added[1] to expose Pulpcore's HIDE_GUARDED_DISTRIBUTIONS option. [1]: https://github.com/theforeman/puppet-foreman_proxy_content/commit/fdc6a78dbc894f6c68e52d52f2d01632b04d70f4 (cherry picked from commit 301ffbb36c98fb16856b4f24ff6b5fd837c26ff4) Co-authored-by: Ewoud Kohl van Wijngaarden --- tests/foreman/installer/test_installer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/foreman/installer/test_installer.py b/tests/foreman/installer/test_installer.py index 1a4797046a1..18e53788bd2 100644 --- a/tests/foreman/installer/test_installer.py +++ b/tests/foreman/installer/test_installer.py @@ -222,6 +222,7 @@ '--foreman-proxy-content-pulpcore-cache-expires-ttl', '--foreman-proxy-content-pulpcore-content-service-worker-timeout', '--foreman-proxy-content-pulpcore-django-secret-key', + '--foreman-proxy-content-pulpcore-hide-guarded-distributions', '--foreman-proxy-content-pulpcore-manage-postgresql', '--foreman-proxy-content-pulpcore-mirror', '--foreman-proxy-content-pulpcore-postgresql-db-name', @@ -805,6 +806,7 @@ '--reset-foreman-proxy-content-pulpcore-cache-expires-ttl', '--reset-foreman-proxy-content-pulpcore-content-service-worker-timeout', '--reset-foreman-proxy-content-pulpcore-django-secret-key', + '--reset-foreman-proxy-content-pulpcore-hide-guarded-distributions', '--reset-foreman-proxy-content-pulpcore-manage-postgresql', '--reset-foreman-proxy-content-pulpcore-mirror', '--reset-foreman-proxy-content-pulpcore-postgresql-db-name', From 2b9424daf15c0e81a70130d09e763515cac5a8f9 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 18 Jul 2023 03:12:42 -0400 Subject: [PATCH 089/586] [6.14.z] Bump pyyaml from 6.0 to 6.0.1 (#11918) Bump pyyaml from 6.0 to 6.0.1 (#11917) (cherry picked from commit a0ea07daa2825da915538e2f3ab6b646084a71dd) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ea020c9e72a..10ee1c779ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ pytest-mock==3.11.1 pytest-reportportal==5.2.0 pytest-xdist==3.3.1 pytest-ibutsu==2.2.4 -PyYAML==6.0 +PyYAML==6.0.1 requests==2.31.0 tenacity==8.2.2 testimony==2.2.0 From 6a366bc30fa9560f66c9ba6520bec76babc124cf Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 18 Jul 2023 05:27:25 -0400 Subject: [PATCH 090/586] [6.14.z] Bump wrapanapi from 3.5.15 to 3.5.17 (#11924) Bump wrapanapi from 3.5.15 to 3.5.17 (#11916) Bumps [wrapanapi](https://github.com/RedHatQE/wrapanapi) from 3.5.15 to 3.5.17. - [Release notes](https://github.com/RedHatQE/wrapanapi/releases) - [Commits](https://github.com/RedHatQE/wrapanapi/compare/3.5.15...3.5.17) --- updated-dependencies: - dependency-name: wrapanapi dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit f0149e8048b814abdabdcab226c93a9b7aefb1b9) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 10ee1c779ca..6265e6032b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ requests==2.31.0 tenacity==8.2.2 testimony==2.2.0 wait-for==1.2.0 -wrapanapi==3.5.15 +wrapanapi==3.5.17 # Get airgun, nailgun and upgrade from master git+https://github.com/SatelliteQE/airgun.git@6.14.z#egg=airgun From 3af4ab08a07c95e4b109bf9dd672eba43896c38d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 19 Jul 2023 04:16:48 -0400 Subject: [PATCH 091/586] [6.14.z] Bump pytest-reportportal from 5.2.0 to 5.2.1 (#11930) Bump pytest-reportportal from 5.2.0 to 5.2.1 (#11928) (cherry picked from commit ea28168634529104eef73251831d5e9fd3f0a6bb) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6265e6032b1..0f66ec3751a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ python-box==7.0.1 pytest==7.4.0 pytest-services==2.2.1 pytest-mock==3.11.1 -pytest-reportportal==5.2.0 +pytest-reportportal==5.2.1 pytest-xdist==3.3.1 pytest-ibutsu==2.2.4 PyYAML==6.0.1 From 044ca641b0862bb3ea28692454cbb4a4eb956f53 Mon Sep 17 00:00:00 2001 From: Jameer Pathan Date: Thu, 20 Jul 2023 21:13:51 +0200 Subject: [PATCH 092/586] Update robottelo image when there's change in Airgun/Nailgun repo (#11428) (#11947) * Add event listener for airgun or nailgun changes * Address pr comment * Use workflow_dispatch --- .github/workflows/merge_to_master.yml | 102 ------------------- .github/workflows/update_robottelo_image.yml | 44 ++++++++ 2 files changed, 44 insertions(+), 102 deletions(-) delete mode 100644 .github/workflows/merge_to_master.yml create mode 100644 .github/workflows/update_robottelo_image.yml diff --git a/.github/workflows/merge_to_master.yml b/.github/workflows/merge_to_master.yml deleted file mode 100644 index 66dba30485b..00000000000 --- a/.github/workflows/merge_to_master.yml +++ /dev/null @@ -1,102 +0,0 @@ -# CI stages to execute against master branch on PR merge -name: update_robottelo_image - -on: - push: - branches: - - 6.14.z - -env: - PYCURL_SSL_LIBRARY: openssl - ROBOTTELO_BUGZILLA__API_KEY: ${{ secrets.BUGZILLA_KEY }} - -jobs: - codechecks: - name: Code Quality - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ['3.9', '3.10', '3.11'] - steps: - - name: Checkout Robottelo - uses: actions/checkout@v3 - - - name: Set Up Python-${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Install Dependencies - run: | - sudo apt-get update -y - sudo apt-get install -y libgnutls28-dev libcurl4-openssl-dev libssl-dev - wget https://raw.githubusercontent.com/SatelliteQE/broker/master/broker_settings.yaml.example - # link vs compile time ssl implementations can break the environment when installing requirements - # Uninstall pycurl - its likely not installed, but in case the ubuntu-latest packages change - # Then compile and install it with PYCURL_SSL_LIBRARY set to openssl - pip install -U pip - pip uninstall -y pycurl - pip install --compile --no-cache-dir pycurl - pip install -U --no-cache-dir -r requirements.txt -r requirements-optional.txt - for conffile in conf/*.yaml.template; do mv -- "$conffile" "${conffile%.yaml.template}.yaml"; done - cp broker_settings.yaml.example broker_settings.yaml - cp .env.example .env - - - name: Pre Commit Checks - uses: pre-commit/action@v3.0.0 - - - name: Collect Tests - run: | - pytest --collect-only --disable-pytest-warnings tests/foreman/ tests/robottelo/ - pytest --collect-only --disable-pytest-warnings -m pre_upgrade tests/upgrades/ - pytest --collect-only --disable-pytest-warnings -m post_upgrade tests/upgrades/ - - - name: Make Docs - run: | - make test-docstrings - make docs - - - name: Analysis (git diff) - if: failure() - run: git diff - - - robottelo_container: - needs: codechecks - name: Update Robottelo container image on Quay. - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Get image tag - id: image_tag - run: | - echo -n ::set-output name=IMAGE_TAG:: - TAG="${GITHUB_REF##*/}" - if [ "${TAG}" == "master" ]; then - TAG="latest" - fi - echo "${TAG}" - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to Quay Container Registry - uses: docker/login-action@v2 - with: - registry: ${{ secrets.QUAY_SERVER }} - username: ${{ secrets.QUAY_USERNAME }} - password: ${{ secrets.QUAY_PASSWORD }} - - - name: Build and push image to Quay - uses: docker/build-push-action@v4 - with: - context: . - file: ./Dockerfile - push: true - tags: ${{ secrets.QUAY_SERVER }}/${{ secrets.QUAY_NAMESPACE }}/robottelo:${{ steps.image_tag.outputs.IMAGE_TAG }} diff --git a/.github/workflows/update_robottelo_image.yml b/.github/workflows/update_robottelo_image.yml new file mode 100644 index 00000000000..2b2e4654bdc --- /dev/null +++ b/.github/workflows/update_robottelo_image.yml @@ -0,0 +1,44 @@ +# Update robottelo image on quay. +name: update_robottelo_image + +on: + push: + branches: + - master + - 6.*.z + workflow_dispatch: + +jobs: + robottelo_container: + name: Update Robottelo container image on Quay. + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Get image tag + id: image_tag + run: | + TAG="${GITHUB_REF##*/}" + TAG=${TAG/master/latest} + echo "IMAGE_TAG=$TAG" >> $GITHUB_OUTPUT + + - name: Build Robottelo image + id: build-image + uses: redhat-actions/buildah-build@v2 + with: + image: robottelo + tags: ${{ steps.image_tag.outputs.IMAGE_TAG }} + containerfiles: | + ./Dockerfile + + - name: Push Robottelo image to quay.io + id: push-to-quay + uses: redhat-actions/push-to-registry@v2 + with: + image: robottelo + tags: ${{ steps.image_tag.outputs.IMAGE_TAG }} + registry: ${{ secrets.QUAY_SERVER }}/${{ secrets.QUAY_NAMESPACE }} + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} From ed51dd586f5ee354a972a1a324c928804fb19df8 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 24 Jul 2023 04:08:34 -0400 Subject: [PATCH 093/586] [6.14.z] Remove Python 3.9 support from robottelo (#11954) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove Python 3.9 support from robottelo (#11953) * Remove 3.9 support from robottelo * Update setup.py (cherry picked from commit 786da02bce4ff5178a92fafad7368760ddad271b) Co-authored-by: Ladislav Vašina --- .github/workflows/pull_request.yml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 972c1960983..cc58fb9177a 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11'] + python-version: ['3.10', '3.11'] steps: - name: Checkout Robottelo uses: actions/checkout@v3 diff --git a/setup.py b/setup.py index f3a682641fc..e173ae35c1e 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ include_package_data=True, license='GNU GPL v3.0', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers - python_requires="~=3.9", + python_requires="~=3.10", classifiers=( 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', From 47f5f2644711cc1e61606c9484d07a69beacd685 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 25 Jul 2023 04:21:00 -0400 Subject: [PATCH 094/586] [6.14.z] Bump sphinx from 7.0.1 to 7.1.0 (#11969) Bump sphinx from 7.0.1 to 7.1.0 (#11966) (cherry picked from commit 975373201120b9ba3559b7f7c69064ce9be6251e) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 674c469fa95..36dba852e8c 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -5,7 +5,7 @@ redis==4.5.5 pre-commit==3.3.3 # For generating documentation. -sphinx==7.0.1 +sphinx==7.1.0 sphinx-autoapi==2.1.1 # For 'manage' interactive shell From 00c0f203570ba8e946f141ee6301f622b830a34e Mon Sep 17 00:00:00 2001 From: Jitendra Yejare Date: Mon, 24 Jul 2023 19:59:40 +0530 Subject: [PATCH 095/586] Sanity Plugin with new sanity tests runs on installer tests satellite (#11645) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * New Sanity Tests, BVT Tests and the process * Resolving comments * Update pytest_fixtures/component/puppet.py Co-authored-by: Ondřej Gajdušek --------- Co-authored-by: Ondřej Gajdušek --- conftest.py | 1 + pytest_fixtures/component/puppet.py | 5 +- pytest_fixtures/core/broker.py | 65 +---- pytest_fixtures/core/contenthosts.py | 28 --- pytest_fixtures/core/sat_cap_factory.py | 229 ++++++++++++++---- pytest_fixtures/core/sys.py | 4 +- pytest_fixtures/core/xdist.py | 81 ++++--- pytest_plugins/sanity_plugin.py | 60 +++++ robottelo/hosts.py | 25 ++ robottelo/utils/ohsnap.py | 22 ++ testimony.yaml | 2 + tests/foreman/api/test_computeresource_gce.py | 1 + tests/foreman/api/test_host.py | 1 - tests/foreman/api/test_organization.py | 1 + tests/foreman/api/test_repository.py | 1 + tests/foreman/api/test_syncplan.py | 1 - tests/foreman/api/test_user.py | 1 + tests/foreman/cli/test_contentcredentials.py | 1 - tests/foreman/cli/test_contentview.py | 1 + tests/foreman/cli/test_hostcollection.py | 1 - tests/foreman/cli/test_ping.py | 1 - tests/foreman/cli/test_role.py | 1 - tests/foreman/cli/test_user.py | 1 - tests/foreman/endtoend/test_api_endtoend.py | 3 - tests/foreman/installer/test_installer.py | 77 +++--- tests/foreman/sanity/test_bvt.py | 118 +++++++++ 26 files changed, 526 insertions(+), 206 deletions(-) create mode 100644 pytest_plugins/sanity_plugin.py create mode 100644 tests/foreman/sanity/test_bvt.py diff --git a/conftest.py b/conftest.py index 0ade0cb33de..91eb0d54ae9 100644 --- a/conftest.py +++ b/conftest.py @@ -19,6 +19,7 @@ 'pytest_plugins.fixture_collection', 'pytest_plugins.factory_collection', 'pytest_plugins.requirements.update_requirements', + 'pytest_plugins.sanity_plugin', # Fixtures 'pytest_fixtures.core.broker', 'pytest_fixtures.core.sat_cap_factory', diff --git a/pytest_fixtures/component/puppet.py b/pytest_fixtures/component/puppet.py index d0f9b267263..f088e05d0a8 100644 --- a/pytest_fixtures/component/puppet.py +++ b/pytest_fixtures/component/puppet.py @@ -8,7 +8,10 @@ @pytest.fixture(scope='session') def session_puppet_enabled_sat(session_satellite_host): """Satellite with enabled puppet plugin""" - yield session_satellite_host.enable_puppet_satellite() + if session_satellite_host: + yield session_satellite_host.enable_puppet_satellite() + else: + yield @pytest.fixture(scope='session') diff --git a/pytest_fixtures/core/broker.py b/pytest_fixtures/core/broker.py index 0fb863c23cb..36822ad2461 100644 --- a/pytest_fixtures/core/broker.py +++ b/pytest_fixtures/core/broker.py @@ -3,21 +3,10 @@ import pytest from box import Box from broker import Broker -from wait_for import wait_for from robottelo.config import settings -from robottelo.hosts import Capsule +from robottelo.hosts import lru_sat_ready_rhel from robottelo.hosts import Satellite -from robottelo.logging import logger - - -def _resolve_deploy_args(args_dict): - # TODO: https://github.com/rochacbruno/dynaconf/issues/690 - for key, val in args_dict.copy().to_dict().items(): - if isinstance(val, str) and val.startswith('this.'): - # Args transformed into small letters and existing capital args removed - args_dict[key.lower()] = settings.get(args_dict.pop(key).replace('this.', '')) - return args_dict @pytest.fixture(scope='session') @@ -41,6 +30,10 @@ def _target_sat_imp(request, _default_sat, satellite_factory): yield new_sat new_sat.teardown() Broker(hosts=[new_sat]).checkin() + elif 'sanity' in request.config.option.markexpr: + installer_sat = lru_sat_ready_rhel(settings.server.version.rhel_version) + settings.set('server.hostname', installer_sat.hostname) + yield installer_sat else: yield _default_sat @@ -69,54 +62,6 @@ def class_target_sat(request, _default_sat, satellite_factory): yield sat -@pytest.fixture(scope='session') -def satellite_factory(): - if settings.server.get('deploy_arguments'): - logger.debug(f'Original deploy arguments for sat: {settings.server.deploy_arguments}') - resolved = _resolve_deploy_args(settings.server.deploy_arguments) - settings.set('server.deploy_arguments', resolved) - logger.debug(f'Resolved deploy arguments for sat: {settings.server.deploy_arguments}') - - def factory(retry_limit=3, delay=300, workflow=None, **broker_args): - if settings.server.deploy_arguments: - broker_args.update(settings.server.deploy_arguments) - logger.debug(f'Updated broker args for sat: {broker_args}') - - vmb = Broker( - host_class=Satellite, - workflow=workflow or settings.server.deploy_workflow, - **broker_args, - ) - timeout = (1200 + delay) * retry_limit - sat = wait_for(vmb.checkout, timeout=timeout, delay=delay, fail_condition=[]) - return sat.out - - return factory - - -@pytest.fixture(scope='session') -def capsule_factory(): - if settings.capsule.get('deploy_arguments'): - logger.debug(f'Original deploy arguments for cap: {settings.capsule.deploy_arguments}') - resolved = _resolve_deploy_args(settings.capsule.deploy_arguments) - settings.set('capsule.deploy_arguments', resolved) - logger.debug(f'Resolved deploy arguments for cap: {settings.capsule.deploy_arguments}') - - def factory(retry_limit=3, delay=300, workflow=None, **broker_args): - if settings.capsule.deploy_arguments: - broker_args.update(settings.capsule.deploy_arguments) - vmb = Broker( - host_class=Capsule, - workflow=workflow or settings.capsule.deploy_workflow, - **broker_args, - ) - timeout = (1200 + delay) * retry_limit - cap = wait_for(vmb.checkout, timeout=timeout, delay=delay, fail_condition=[]) - return cap.out - - return factory - - @pytest.fixture(scope='module') def module_discovery_sat( module_provisioning_sat, diff --git a/pytest_fixtures/core/contenthosts.py b/pytest_fixtures/core/contenthosts.py index 9a5b6490a34..131beda131d 100644 --- a/pytest_fixtures/core/contenthosts.py +++ b/pytest_fixtures/core/contenthosts.py @@ -9,7 +9,6 @@ from robottelo import constants from robottelo.config import settings -from robottelo.hosts import Capsule from robottelo.hosts import ContentHost from robottelo.hosts import Satellite @@ -223,33 +222,6 @@ def oracle_host(request, version): yield host -@pytest.fixture -def sat_ready_rhel(request): - deploy_args = { - 'deploy_rhel_version': request.param, - 'deploy_flavor': settings.flavors.default, - 'promtail_config_template_file': 'config_sat.j2', - 'workflow': 'deploy-rhel', - } - # if 'deploy_rhel_version' is not set, let's default to RHEL 8 - deploy_args['deploy_rhel_version'] = deploy_args.get('deploy_rhel_version', '8') - deploy_args['workflow'] = 'deploy-rhel' - with Broker(**deploy_args, host_class=Satellite) as host: - yield host - - -@pytest.fixture -def cap_ready_rhel(): - rhel8 = settings.content_host.rhel8.vm - deploy_args = { - 'deploy_rhel_version': rhel8.deploy_rhel_version, - 'deploy_flavor': 'satqe-ssd.standard.std', - 'workflow': rhel8.workflow, - } - with Broker(**deploy_args, host_class=Capsule) as host: - yield host - - @pytest.fixture(scope='module', params=[{'rhel_version': 8, 'no_containers': True}]) def external_puppet_server(request): deploy_args = host_conf(request) diff --git a/pytest_fixtures/core/sat_cap_factory.py b/pytest_fixtures/core/sat_cap_factory.py index d4808eeeaa5..89287967c19 100644 --- a/pytest_fixtures/core/sat_cap_factory.py +++ b/pytest_fixtures/core/sat_cap_factory.py @@ -1,74 +1,148 @@ +from contextlib import contextmanager + import pytest from broker import Broker from wait_for import wait_for -from pytest_fixtures.core.broker import _resolve_deploy_args +from robottelo.config import configure_airgun +from robottelo.config import configure_nailgun from robottelo.config import settings from robottelo.hosts import Capsule from robottelo.hosts import IPAHost +from robottelo.hosts import lru_sat_ready_rhel +from robottelo.hosts import Satellite +from robottelo.logging import logger +from robottelo.utils.installer import InstallerCommand -@pytest.fixture -def satellite_host(satellite_factory): - """A fixture that provides a Satellite based on config settings""" - new_sat = satellite_factory() - yield new_sat - new_sat.teardown() - Broker(hosts=[new_sat]).checkin() +def resolve_deploy_args(args_dict): + # TODO: https://github.com/rochacbruno/dynaconf/issues/690 + for key, val in args_dict.copy().to_dict().items(): + if isinstance(val, str) and val.startswith('this.'): + # Args transformed into small letters and existing capital args removed + args_dict[key.lower()] = settings.get(args_dict.pop(key).replace('this.', '')) + return args_dict -@pytest.fixture(scope='module') -def module_satellite_host(satellite_factory): - """A fixture that provides a Satellite based on config settings""" - new_sat = satellite_factory() - yield new_sat - new_sat.teardown() - Broker(hosts=[new_sat]).checkin() +@contextmanager +def _target_satellite_host(request, satellite_factory): + if 'sanity' not in request.config.option.markexpr: + new_sat = satellite_factory() + yield new_sat + new_sat.teardown() + Broker(hosts=[new_sat]).checkin() + else: + yield + + +@contextmanager +def _target_capsule_host(request, capsule_factory): + if 'sanity' not in request.config.option.markexpr: + new_cap = capsule_factory() + yield new_cap + new_cap.teardown() + Broker(hosts=[new_cap]).checkin() + else: + yield @pytest.fixture(scope='session') -def session_satellite_host(satellite_factory): - """A fixture that provides a Satellite based on config settings""" - new_sat = satellite_factory() - yield new_sat - new_sat.teardown() - Broker(hosts=[new_sat]).checkin() +def satellite_factory(): + if settings.server.get('deploy_arguments'): + logger.debug(f'Original deploy arguments for sat: {settings.server.deploy_arguments}') + resolved = resolve_deploy_args(settings.server.deploy_arguments) + settings.set('server.deploy_arguments', resolved) + logger.debug(f'Resolved deploy arguments for sat: {settings.server.deploy_arguments}') + + def factory(retry_limit=3, delay=300, workflow=None, **broker_args): + if settings.server.deploy_arguments: + broker_args.update(settings.server.deploy_arguments) + logger.debug(f'Updated broker args for sat: {broker_args}') + + vmb = Broker( + host_class=Satellite, + workflow=workflow or settings.server.deploy_workflow, + **broker_args, + ) + timeout = (1200 + delay) * retry_limit + sat = wait_for(vmb.checkout, timeout=timeout, delay=delay, fail_condition=[]) + return sat.out + + return factory @pytest.fixture -def capsule_host(capsule_factory): +def large_capsule_host(capsule_factory): """A fixture that provides a Capsule based on config settings""" - new_cap = capsule_factory() + new_cap = capsule_factory(deploy_flavor=settings.flavors.custom_db) yield new_cap new_cap.teardown() Broker(hosts=[new_cap]).checkin() +@pytest.fixture(scope='session') +def capsule_factory(): + if settings.capsule.get('deploy_arguments'): + logger.debug(f'Original deploy arguments for cap: {settings.capsule.deploy_arguments}') + resolved = resolve_deploy_args(settings.capsule.deploy_arguments) + settings.set('capsule.deploy_arguments', resolved) + logger.debug(f'Resolved deploy arguments for cap: {settings.capsule.deploy_arguments}') + + def factory(retry_limit=3, delay=300, workflow=None, **broker_args): + if settings.capsule.deploy_arguments: + broker_args.update(settings.capsule.deploy_arguments) + vmb = Broker( + host_class=Capsule, + workflow=workflow or settings.capsule.deploy_workflow, + **broker_args, + ) + timeout = (1200 + delay) * retry_limit + cap = wait_for(vmb.checkout, timeout=timeout, delay=delay, fail_condition=[]) + return cap.out + + return factory + + @pytest.fixture -def large_capsule_host(capsule_factory): +def satellite_host(request, satellite_factory): + """A fixture that provides a Satellite based on config settings""" + with _target_satellite_host(request, satellite_factory) as sat: + yield sat + + +@pytest.fixture(scope='module') +def module_satellite_host(request, satellite_factory): + """A fixture that provides a Satellite based on config settings""" + with _target_satellite_host(request, satellite_factory) as sat: + yield sat + + +@pytest.fixture(scope='session') +def session_satellite_host(request, satellite_factory): + """A fixture that provides a Satellite based on config settings""" + with _target_satellite_host(request, satellite_factory) as sat: + yield sat + + +@pytest.fixture +def capsule_host(request, capsule_factory): """A fixture that provides a Capsule based on config settings""" - new_cap = capsule_factory(deploy_flavor=settings.flavors.custom_db) - yield new_cap - new_cap.teardown() - Broker(hosts=[new_cap]).checkin() + with _target_capsule_host(request, capsule_factory) as cap: + yield cap @pytest.fixture(scope='module') -def module_capsule_host(capsule_factory): +def module_capsule_host(request, capsule_factory): """A fixture that provides a Capsule based on config settings""" - new_cap = capsule_factory() - yield new_cap - new_cap.teardown() - Broker(hosts=[new_cap]).checkin() + with _target_capsule_host(request, capsule_factory) as cap: + yield cap @pytest.fixture(scope='session') -def session_capsule_host(capsule_factory): +def session_capsule_host(request, capsule_factory): """A fixture that provides a Capsule based on config settings""" - new_cap = capsule_factory() - yield new_cap - new_cap.teardown() - Broker(hosts=[new_cap]).checkin() + with _target_capsule_host(request, capsule_factory) as cap: + yield cap @pytest.fixture @@ -95,8 +169,10 @@ def module_capsule_configured(module_capsule_host, module_target_sat): @pytest.fixture(scope='session') def session_capsule_configured(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) - yield session_capsule_host + if session_capsule_host: + session_capsule_host.capsule_setup(sat_host=session_target_sat) + yield session_capsule_host + yield @pytest.fixture(scope='module') @@ -120,7 +196,7 @@ def module_lb_capsule(retry_limit=3, delay=300, **broker_args): :return: List of capsules """ if settings.capsule.get('deploy_arguments'): - resolved = _resolve_deploy_args(settings.capsule.deploy_arguments) + resolved = resolve_deploy_args(settings.capsule.deploy_arguments) settings.set('capsule.deploy_arguments', resolved) broker_args.update(settings.capsule.deploy_arguments) timeout = (1200 + delay) * retry_limit @@ -175,3 +251,74 @@ def parametrized_enrolled_sat( new_sat.unregister() new_sat.teardown() Broker(hosts=[new_sat]).checkin() + + +@pytest.fixture +def sat_ready_rhel(request): + deploy_args = { + 'deploy_rhel_version': request.param, + 'deploy_flavor': settings.flavors.default, + 'promtail_config_template_file': 'config_sat.j2', + 'workflow': 'deploy-rhel', + } + # if 'deploy_rhel_version' is not set, let's default to RHEL 8 + deploy_args['deploy_rhel_version'] = deploy_args.get('deploy_rhel_version', '8') + deploy_args['workflow'] = 'deploy-rhel' + with Broker(**deploy_args, host_class=Satellite) as host: + yield host + + +@pytest.fixture +def cap_ready_rhel(): + rhel8 = settings.content_host.rhel8.vm + deploy_args = { + 'deploy_rhel_version': rhel8.deploy_rhel_version, + 'deploy_flavor': 'satqe-ssd.standard.std', + 'workflow': rhel8.workflow, + } + with Broker(**deploy_args, host_class=Capsule) as host: + yield host + + +@pytest.fixture(scope='session') +def installer_satellite(request): + """A fixture to freshly install the satellite using installer on RHEL machine + + This is a pure / virgin / nontemplate based satellite + + :params request: A pytest request object and this fixture is looking for + broker object of class satellite + """ + sat_version = settings.server.version.release + if 'sanity' in request.config.option.markexpr: + sat = Satellite(settings.server.hostname) + else: + sat = lru_sat_ready_rhel(getattr(request, 'param', None)) + sat.setup_firewall() + # # Register for RHEL8 repos, get Ohsnap repofile, and enable and download satellite + sat.register_to_cdn() + sat.download_repofile(product='satellite', release=settings.server.version.release) + sat.execute('dnf -y module enable satellite:el8 && dnf -y install satellite') + installed_version = sat.execute('rpm --query satellite').stdout + assert sat_version in installed_version + # Install Satellite + sat.execute( + InstallerCommand( + installer_args=[ + 'scenario satellite', + f'foreman-initial-admin-password {settings.server.admin_password}', + ] + ).get_command(), + timeout='30m', + ) + if 'sanity' in request.config.option.markexpr: + configure_nailgun() + configure_airgun() + yield sat + if 'sanity' not in request.config.option.markexpr: + sanity_sat = Satellite(sat.hostname) + sanity_sat.unregister() + broker_sat = Broker(host_class=Satellite).from_inventory( + filter=f'@inv.hostname == "{sanity_sat.hostname}"' + )[0] + Broker(hosts=[broker_sat]).checkin() diff --git a/pytest_fixtures/core/sys.py b/pytest_fixtures/core/sys.py index 1684c0bb251..b9106eac7ad 100644 --- a/pytest_fixtures/core/sys.py +++ b/pytest_fixtures/core/sys.py @@ -22,9 +22,9 @@ def allow_repo_discovery(target_sat): @pytest.fixture(autouse=True, scope="session") -def relax_bfa(session_target_sat): +def relax_bfa(request, session_target_sat): """Relax BFA protection against failed login attempts""" - if session_target_sat: + if session_target_sat and 'sanity' not in request.config.option.markexpr: session_target_sat.cli.Settings.set({'name': 'failed_login_attempts_limit', 'value': '0'}) diff --git a/pytest_fixtures/core/xdist.py b/pytest_fixtures/core/xdist.py index 982f3554749..7c4917725be 100644 --- a/pytest_fixtures/core/xdist.py +++ b/pytest_fixtures/core/xdist.py @@ -12,41 +12,56 @@ @pytest.fixture(scope="session", autouse=True) -def align_to_satellite(worker_id, satellite_factory): +def align_to_satellite(request, worker_id, satellite_factory): """Attempt to align a Satellite to the current xdist worker""" - # clear any hostname that may have been previously set - settings.set("server.hostname", None) - on_demand_sat = None - - if worker_id in ['master', 'local']: - worker_pos = 0 + if 'build_sanity' in request.config.option.markexpr: + yield + if settings.server.hostname: + sanity_sat = Satellite(settings.server.hostname) + sanity_sat.unregister() + broker_sat = Broker(host_class=Satellite).from_inventory( + filter=f'@inv.hostname == "{sanity_sat.hostname}"' + )[0] + Broker(hosts=[broker_sat]).checkin() else: - worker_pos = int(worker_id.replace('gw', '')) + # clear any hostname that may have been previously set + settings.set("server.hostname", None) + on_demand_sat = None + + if worker_id in ['master', 'local']: + worker_pos = 0 + else: + worker_pos = int(worker_id.replace('gw', '')) - # attempt to add potential satellites from the broker inventory file - if settings.server.inventory_filter: - hosts = Broker(host_class=Satellite).from_inventory(filter=settings.server.inventory_filter) - settings.server.hostnames += [host.hostname for host in hosts] + # attempt to add potential satellites from the broker inventory file + if settings.server.inventory_filter: + hosts = Broker(host_class=Satellite).from_inventory( + filter=settings.server.inventory_filter + ) + settings.server.hostnames += [host.hostname for host in hosts] - # attempt to align a worker to a satellite - if settings.server.xdist_behavior == 'run-on-one' and settings.server.hostnames: - settings.set("server.hostname", settings.server.hostnames[0]) - elif settings.server.hostnames and worker_pos < len(settings.server.hostnames): - settings.set("server.hostname", settings.server.hostnames[worker_pos]) - elif settings.server.xdist_behavior == 'balance' and settings.server.hostnames: - settings.set("server.hostname", random.choice(settings.server.hostnames)) - # get current satellite information - elif settings.server.xdist_behavior == 'on-demand': - on_demand_sat = satellite_factory() - if on_demand_sat.hostname: - settings.set("server.hostname", on_demand_sat.hostname) - # if no satellite was received, fallback to balance - if not settings.server.hostname: + # attempt to align a worker to a satellite + if settings.server.xdist_behavior == 'run-on-one' and settings.server.hostnames: + settings.set("server.hostname", settings.server.hostnames[0]) + elif settings.server.hostnames and worker_pos < len(settings.server.hostnames): + settings.set("server.hostname", settings.server.hostnames[worker_pos]) + elif settings.server.xdist_behavior == 'balance' and settings.server.hostnames: settings.set("server.hostname", random.choice(settings.server.hostnames)) - logger.info(f'xdist worker {worker_id} was assigned hostname {settings.server.hostname}') - configure_airgun() - configure_nailgun() - yield - if on_demand_sat and settings.server.auto_checkin: - on_demand_sat.teardown() - Broker(hosts=[on_demand_sat]).checkin() + # get current satellite information + elif settings.server.xdist_behavior == 'on-demand': + on_demand_sat = satellite_factory() + if on_demand_sat.hostname: + settings.set("server.hostname", on_demand_sat.hostname) + # if no satellite was received, fallback to balance + if not settings.server.hostname: + settings.set("server.hostname", random.choice(settings.server.hostnames)) + if settings.server.hostname: + logger.info( + f'xdist worker {worker_id} was assigned hostname {settings.server.hostname}' + ) + configure_airgun() + configure_nailgun() + yield + if on_demand_sat and settings.server.auto_checkin: + on_demand_sat.teardown() + Broker(hosts=[on_demand_sat]).checkin() diff --git a/pytest_plugins/sanity_plugin.py b/pytest_plugins/sanity_plugin.py new file mode 100644 index 00000000000..f955d9cf4ca --- /dev/null +++ b/pytest_plugins/sanity_plugin.py @@ -0,0 +1,60 @@ +""" A sanity testing plugin to assist in executing robottelo tests as sanity tests smartly + +1. Make installer test to run first which should set the hostname and all other tests then +should run after that +""" + + +class ConfigurationException(Exception): + """Raised when pytest configuration is missed""" + + pass + + +def pytest_configure(config): + """Register markers related to testimony tokens""" + config.addinivalue_line( + "markers", "first_sanity: An installer test to run first in sanity testing" + ) + + +def pytest_collection_modifyitems(session, items, config): + if 'sanity' not in config.option.markexpr: + return + + selected = [] + deselected = [] + installer_test = None + + for item in items: + if item.get_closest_marker('build_sanity'): + # Identify the installer sanity test to run first + if not installer_test and item.get_closest_marker('first_sanity'): + installer_test = item + deselected.append(item) + continue + # Test parameterization disablement for sanity + # Remove Puppet based tests + if 'session_puppet_enabled_sat' in item.fixturenames and 'puppet' in item.name: + deselected.append(item) + continue + # Remove capsule tests + if 'sat_maintain' in item.fixturenames and 'capsule' in item.name: + deselected.append(item) + continue + # Remove parametrization from organization test + if 'test_positive_create_with_name_and_description' in item.name: + if 'alphanumeric' not in item.name: + deselected.append(item) + continue + # Else select + selected.append(item) + + # Append the installer test first to run + if not installer_test: + raise ConfigurationException( + 'The installer test is not configured to base the sanity testing on!' + ) + selected.insert(0, installer_test) + config.hook.pytest_deselected(items=deselected) + items[:] = selected diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 6754d5eb556..06283794a71 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -71,6 +71,18 @@ } +@lru_cache +def lru_sat_ready_rhel(rhel_ver): + deploy_args = { + 'deploy_rhel_version': rhel_ver or settings.server.version.rhel_version, + 'deploy_flavor': settings.flavors.default, + 'promtail_config_template_file': 'config_sat.j2', + 'workflow': 'deploy-rhel', + } + sat_ready_rhel = Broker(**deploy_args, host_class=Satellite).checkout() + return sat_ready_rhel + + def get_sat_version(): """Try to read sat_version from envvar SATELLITE_VERSION if not available fallback to ssh connection to get it.""" @@ -1696,6 +1708,19 @@ def is_remote_db(self): self.execute(f'grep "db_manage: false" {constants.SATELLITE_ANSWER_FILE}').status == 0 ) + def setup_firewall(self): + # Setups firewall on Satellite + assert ( + self.execute( + command='firewall-cmd --add-port="53/udp" --add-port="53/tcp" --add-port="67/udp" ' + '--add-port="69/udp" --add-port="80/tcp" --add-port="443/tcp" ' + '--add-port="5647/tcp" --add-port="8000/tcp" --add-port="9090/tcp" ' + '--add-port="8140/tcp"' + ).status + == 0 + ) + assert self.execute(command='firewall-cmd --runtime-to-permanent').status == 0 + def capsule_certs_generate(self, capsule, cert_path=None, **extra_kwargs): """Generate capsule certs, returning the cert path, installer command stdout and args""" cert_file_path = cert_path or f'/root/{capsule.hostname}-certs.tar' diff --git a/robottelo/utils/ohsnap.py b/robottelo/utils/ohsnap.py index cc6d551c70c..202d40f81f1 100644 --- a/robottelo/utils/ohsnap.py +++ b/robottelo/utils/ohsnap.py @@ -103,3 +103,25 @@ def dogfood_repository( f'{product=}, {release=}, {os_release=}, {snap=}' ) return Box(**repository) + + +def ohsnap_snap_rpms(ohsnap, sat_version, snap_version, os_major, is_all=True): + sat_xy = '.'.join(sat_version.split('.')[:2]) + url = f'{ohsnap.host}/api/releases/{sat_version}/snaps/{snap_version}/rpms' + if is_all: + url += '?all=true' + res, _ = wait_for( + lambda: requests.get(url, hooks={'response': ohsnap_response_hook}), + handle_exception=True, + raise_original=True, + timeout=ohsnap.request_retry.timeout, + delay=ohsnap.request_retry.delay, + ) + rpms = [] + rpm_repos = [f'satellite {sat_xy}', f'maintenance {sat_xy}'] + if res.status_code == 200: + for repo_data in res.json(): + if repo_data['rhel'] == os_major: + if any(repo in repo_data['repository'].lower() for repo in rpm_repos): + rpms += repo_data['rpms'] + return rpms diff --git a/testimony.yaml b/testimony.yaml index e698eed5979..e197b61388c 100644 --- a/testimony.yaml +++ b/testimony.yaml @@ -23,6 +23,7 @@ CaseComponent: - BootdiskPlugin - Bootstrap - Branding + - BVT - Candlepin - Capsule - Capsule-Content @@ -104,6 +105,7 @@ CaseComponent: - SyncPlans - TasksPlugin - TemplatesPlugin + - TestEnvironment - TFTP - Upgrades - UsersRoles diff --git a/tests/foreman/api/test_computeresource_gce.py b/tests/foreman/api/test_computeresource_gce.py index 74a532ccb47..bf904dc0560 100644 --- a/tests/foreman/api/test_computeresource_gce.py +++ b/tests/foreman/api/test_computeresource_gce.py @@ -238,6 +238,7 @@ def google_host(self, googleclient): @pytest.mark.e2e @pytest.mark.tier1 + @pytest.mark.build_sanity @pytest.mark.parametrize('sat_gce', ['sat', 'puppet_sat'], indirect=True) def test_positive_gce_host_provisioned(self, class_host, google_host): """Host can be provisioned on Google Cloud diff --git a/tests/foreman/api/test_host.py b/tests/foreman/api/test_host.py index 5b9d0ef7c5f..11897ec1590 100644 --- a/tests/foreman/api/test_host.py +++ b/tests/foreman/api/test_host.py @@ -157,7 +157,6 @@ def test_positive_update_owner_type(owner_type, module_org, module_location, mod assert host.owner.read() == owners[owner_type] -@pytest.mark.build_sanity @pytest.mark.tier1 def test_positive_create_and_update_with_name(): """Create and update a host with different names and minimal input parameters diff --git a/tests/foreman/api/test_organization.py b/tests/foreman/api/test_organization.py index b581cbb5e5e..4fa1c5f6397 100644 --- a/tests/foreman/api/test_organization.py +++ b/tests/foreman/api/test_organization.py @@ -80,6 +80,7 @@ def test_positive_create(self): assert http.client.UNSUPPORTED_MEDIA_TYPE == response.status_code @pytest.mark.tier1 + @pytest.mark.build_sanity @pytest.mark.parametrize('name', **parametrized(valid_org_data_list())) def test_positive_create_with_name_and_description(self, name): """Create an organization and provide a name and description. diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index bf52acb803c..7a9efeb88af 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -1327,6 +1327,7 @@ def test_positive_sync_repos_with_lots_files(self): assert response, f"Repository {repo} failed to sync." @pytest.mark.tier2 + @pytest.mark.build_sanity def test_positive_sync_rh(self, module_entitlement_manifest_org, target_sat): """Sync RedHat Repository. diff --git a/tests/foreman/api/test_syncplan.py b/tests/foreman/api/test_syncplan.py index 30d16893bbb..8719c0629b6 100644 --- a/tests/foreman/api/test_syncplan.py +++ b/tests/foreman/api/test_syncplan.py @@ -136,7 +136,6 @@ def test_positive_get_routes(): assert response1.json()['results'] == response2.json()['results'] -@pytest.mark.build_sanity @pytest.mark.parametrize("enabled", [False, True]) @pytest.mark.tier1 def test_positive_create_enabled_disabled(module_org, enabled, request, target_sat): diff --git a/tests/foreman/api/test_user.py b/tests/foreman/api/test_user.py index 2932e3b1ae8..bc2dfe83c14 100644 --- a/tests/foreman/api/test_user.py +++ b/tests/foreman/api/test_user.py @@ -440,6 +440,7 @@ def make_roles(self): return [entities.Role().create() for _ in range(2)] @pytest.mark.tier1 + @pytest.mark.build_sanity @pytest.mark.parametrize('number_of_roles', range(1, 3)) def test_positive_create_with_role(self, make_roles, number_of_roles): """Create a user with the ``role`` attribute. diff --git a/tests/foreman/cli/test_contentcredentials.py b/tests/foreman/cli/test_contentcredentials.py index 25324aa2d3d..750ec746bbf 100644 --- a/tests/foreman/cli/test_contentcredentials.py +++ b/tests/foreman/cli/test_contentcredentials.py @@ -62,7 +62,6 @@ def create_gpg_key_file(content=None): search_key = 'name' -@pytest.mark.build_sanity @pytest.mark.tier1 def test_verify_gpg_key_content_displayed(module_org): """content-credential info should display key content diff --git a/tests/foreman/cli/test_contentview.py b/tests/foreman/cli/test_contentview.py index 78217af34a5..0a9a764eeda 100644 --- a/tests/foreman/cli/test_contentview.py +++ b/tests/foreman/cli/test_contentview.py @@ -1242,6 +1242,7 @@ def test_positive_promote_rh_and_custom_content( environment = {'id': env1['id'], 'name': env1['name']} assert environment in new_cv['lifecycle-environments'] + @pytest.mark.build_sanity @pytest.mark.tier2 def test_positive_promote_custom_content(self, module_org, module_product): """attempt to promote a content view containing custom content diff --git a/tests/foreman/cli/test_hostcollection.py b/tests/foreman/cli/test_hostcollection.py index 41b980d498d..80b11a56350 100644 --- a/tests/foreman/cli/test_hostcollection.py +++ b/tests/foreman/cli/test_hostcollection.py @@ -118,7 +118,6 @@ def test_positive_end_to_end(module_org): HostCollection.info({'id': new_host_col['id']}) -@pytest.mark.build_sanity @pytest.mark.tier1 def test_positive_create_with_limit(module_org): """Check if host collection can be created with correct limits diff --git a/tests/foreman/cli/test_ping.py b/tests/foreman/cli/test_ping.py index 1993ad01049..3a59698fb82 100644 --- a/tests/foreman/cli/test_ping.py +++ b/tests/foreman/cli/test_ping.py @@ -21,7 +21,6 @@ pytestmark = [pytest.mark.tier1, pytest.mark.upgrade] -@pytest.mark.build_sanity @pytest.mark.parametrize('switch_user', [False, True], ids=['root', 'non-root']) def test_positive_ping(target_sat, switch_user): """hammer ping return code diff --git a/tests/foreman/cli/test_role.py b/tests/foreman/cli/test_role.py index b01f703d338..67c96671ef5 100644 --- a/tests/foreman/cli/test_role.py +++ b/tests/foreman/cli/test_role.py @@ -75,7 +75,6 @@ def test_positive_crud_with_name(self, name, new_name): @pytest.mark.tier1 @pytest.mark.upgrade - @pytest.mark.build_sanity def test_positive_create_with_permission(self): """Create new role with a set of permission diff --git a/tests/foreman/cli/test_user.py b/tests/foreman/cli/test_user.py index e8fef60401d..3d681f67f95 100644 --- a/tests/foreman/cli/test_user.py +++ b/tests/foreman/cli/test_user.py @@ -141,7 +141,6 @@ def test_positive_CRUD(self, email): @pytest.mark.tier1 @pytest.mark.upgrade - @pytest.mark.build_sanity def test_positive_CRUD_admin(self): """Create an Admin user diff --git a/tests/foreman/endtoend/test_api_endtoend.py b/tests/foreman/endtoend/test_api_endtoend.py index 93e1f494559..c152bd0c4a9 100644 --- a/tests/foreman/endtoend/test_api_endtoend.py +++ b/tests/foreman/endtoend/test_api_endtoend.py @@ -953,7 +953,6 @@ def api_url(self): """We want to delay referencing get_url() until test execution""" return f'{get_url()}/api/v2' - @pytest.mark.build_sanity def test_positive_get_status_code(self, api_url): """GET ``api/v2`` and examine the response. @@ -1015,7 +1014,6 @@ class TestEndToEnd: def fake_manifest_is_set(self): return setting_is_set('fake_manifest') - @pytest.mark.build_sanity def test_positive_find_default_org(self): """Check if 'Default Organization' is present @@ -1029,7 +1027,6 @@ def test_positive_find_default_org(self): assert len(results) == 1 assert results[0].name == constants.DEFAULT_ORG - @pytest.mark.build_sanity def test_positive_find_default_loc(self): """Check if 'Default Location' is present diff --git a/tests/foreman/installer/test_installer.py b/tests/foreman/installer/test_installer.py index 18e53788bd2..2af8eda6baa 100644 --- a/tests/foreman/installer/test_installer.py +++ b/tests/foreman/installer/test_installer.py @@ -1429,12 +1429,48 @@ def test_installer_options_and_sections(filter): assert previous == current, msg -@pytest.mark.e2e @pytest.mark.tier1 -@pytest.mark.parametrize("sat_ready_rhel", [settings.server.version.rhel_version], indirect=True) -def test_satellite_and_capsule_installation(sat_ready_rhel, cap_ready_rhel): +@pytest.mark.build_sanity +@pytest.mark.first_sanity +@pytest.mark.parametrize( + "installer_satellite", [settings.server.version.rhel_version], indirect=True +) +def test_satellite_installation(installer_satellite): """Run a basic Satellite installation + :id: 661206f3-2eec-403c-af26-3c5cadcd5766 + + :steps: + 1. Get RHEL Host + 2. Configure satellite repos + 3. Enable satellite module + 4. Install satellite + 5. Run satellite-installer + + :expectedresults: + 1. Correct satellite packaged is installed + 2. satellite-installer runs successfully + 3. satellite-maintain health check runs successfully + + :CaseImportance: Critical + + """ + result = installer_satellite.execute( + r'grep "\[ERROR" --after-context=100 /var/log/foreman-installer/satellite.log' + ) + assert len(result.stdout) == 0 + result = installer_satellite.cli.Health.check() + assert 'FAIL' not in result.stdout + + +@pytest.mark.e2e +@pytest.mark.tier1 +@pytest.mark.parametrize( + "installer_satellite", [settings.server.version.rhel_version], indirect=True +) +def test_satellite_and_capsule_installation(installer_satellite, cap_ready_rhel): + """Run a basic Satellite and Capsule installation + :id: bbab30a6-6861-494f-96dd-23b883c2c906 :steps: @@ -1453,31 +1489,8 @@ def test_satellite_and_capsule_installation(sat_ready_rhel, cap_ready_rhel): 3. satellite-maintain health check runs successfully 4. Capsule is installed and setup correctly - :CaseImportance: High + :CaseImportance: Critical """ - sat_version = settings.server.version.release - # Register for RHEL8 repos, get Ohsnap repofile, and enable and download satellite - sat_ready_rhel.register_to_cdn() - sat_ready_rhel.download_repofile(product='satellite', release=settings.server.version.release) - sat_ready_rhel.execute('dnf -y module enable satellite:el8 && dnf -y install satellite') - installed_version = sat_ready_rhel.execute('rpm --query satellite').stdout - assert sat_version in installed_version - # Install Satellite - sat_ready_rhel.execute( - InstallerCommand( - installer_args=[ - 'scenario satellite', - f'foreman-initial-admin-password {settings.server.admin_password}', - ] - ).get_command(), - timeout='30m', - ) - result = sat_ready_rhel.execute( - r'grep "\[ERROR" --after-context=100 /var/log/foreman-installer/satellite.log' - ) - assert len(result.stdout) == 0 - result = sat_ready_rhel.cli.Health.check() - assert 'FAIL' not in result.stdout # Get Capsule repofile, and enable and download satellite-capsule cap_ready_rhel.register_to_cdn() cap_ready_rhel.download_repofile(product='capsule', release=settings.server.version.release) @@ -1485,16 +1498,18 @@ def test_satellite_and_capsule_installation(sat_ready_rhel, cap_ready_rhel): 'dnf -y module enable satellite-capsule:el8 && dnf -y install satellite-capsule' ) # Configure Satellite firewall to open communication - sat_ready_rhel.execute( + installer_satellite.execute( 'firewall-cmd --permanent --add-service RH-Satellite-6 && ' 'firewall-cmd --add-service RH-Satellite-6' ) # Setup Capsule - org = sat_ready_rhel.api.Organization().search(query={'search': f'name="{DEFAULT_ORG}"'})[0] - setup_capsule(sat_ready_rhel, cap_ready_rhel, org) - assert sat_ready_rhel.api.Capsule().search(query={'search': f'name={cap_ready_rhel.hostname}'})[ + org = installer_satellite.api.Organization().search(query={'search': f'name="{DEFAULT_ORG}"'})[ 0 ] + setup_capsule(installer_satellite, cap_ready_rhel, org) + assert installer_satellite.api.Capsule().search( + query={'search': f'name={cap_ready_rhel.hostname}'} + )[0] result = cap_ready_rhel.execute( r'grep "\[ERROR" --after-context=100 /var/log/foreman-installer/satellite.log' ) diff --git a/tests/foreman/sanity/test_bvt.py b/tests/foreman/sanity/test_bvt.py new file mode 100644 index 00000000000..7fe5dc77f02 --- /dev/null +++ b/tests/foreman/sanity/test_bvt.py @@ -0,0 +1,118 @@ +"""Build Validation tests + +:Requirement: Sanity + +:CaseAutomation: Automated + +:CaseLevel: Acceptance + +:CaseComponent: BVT + +:Team: JPL + +:TestType: Functional + +:CaseImportance: Critical + +:Upstream: No +""" +import re + +import pytest + +from robottelo.config import settings +from robottelo.utils.ohsnap import ohsnap_snap_rpms + +pytestmark = [pytest.mark.build_sanity] + + +def test_installed_packages_with_versions(target_sat): + """Compare the packages that suppose to be installed from repo vs installed packages + + :id: 08913caa-5084-444f-a37d-63da205acc74 + + :expectedresults: All the satellite packages and their installed versions should be + identical to ohsnap as SOT + """ + # Raw Data from Ohsnap and Satellite Server + ohsnap_rpms = ohsnap_snap_rpms( + ohsnap=settings.ohsnap, + sat_version=settings.server.version.release, + snap_version=settings.server.version.snap, + os_major=settings.server.version.rhel_version, + ) + # Suggested Comment: + # rpm -qa --queryformat "" + installed_rpms = target_sat.execute('rpm -qa').stdout + installed_rpms = installed_rpms.splitlines() + + # Changing both the datas to comparable formats + + # Formatting Regex Patters + split_by_version = r'-\d+[-._]*\d*' + namever_pattern = r'.*-\d+[-._]*\d*' + + # Formatted Ohsnap Data + ohsnp_rpm_names = [re.split(split_by_version, rpm)[0] for rpm in ohsnap_rpms] + ohsnap_rpm_name_vers = [re.findall(namever_pattern, rpm)[0] for rpm in ohsnap_rpms] + + # Comparing installed rpms with ohsnap data + mismatch_versions = [] + for rpm in installed_rpms: + installed_rpm_name = re.split(split_by_version, rpm)[0] + if installed_rpm_name in ohsnp_rpm_names: + installed_rpm_name_ver = re.findall(namever_pattern, rpm)[0] + if installed_rpm_name_ver not in ohsnap_rpm_name_vers: + mismatch_versions.append(rpm) + + if mismatch_versions: + pytest.fail(f'Some RPMs are found with mismatch versions. They are {mismatch_versions}') + assert not mismatch_versions + + +def test_all_interfaces_are_accessible(target_sat): + """API, CLI and UI interfaces are accessible + + :id: 0a212120-8e49-4489-a1a4-4272004e16dc + + :expectedresults: All three satellite interfaces are accessible + + + """ + errors = {} + # API Interface + try: + api_org = target_sat.api.Organization(id=1).read() + assert api_org + assert api_org.name == 'Default Organization' + except Exception as api_exc: + errors['api'] = api_exc + + # CLI Interface + try: + cli_org = target_sat.cli.Org.info({'id': 1}) + assert cli_org + assert cli_org['name'] == 'Default Organization' + except Exception as cli_exc: + errors['cli'] = cli_exc + + # UI Interface + try: + with target_sat.ui_session() as session: + ui_org = session.organization.read('Default Organization', widget_names='primary') + assert ui_org + assert ui_org['primary']['name'] == 'Default Organization' + except Exception as ui_exc: + errors['ui'] = ui_exc + + # Final Exception + if errors: + pytest.fail( + '\n'.join( + [ + f'Interface {interface} interaction failed with error {err}' + for interface, err in errors.items() + ] + ) + ) + assert True From de5f89b3d49ec315824f72035cbdffe0def21f8d Mon Sep 17 00:00:00 2001 From: Jameer Pathan Date: Mon, 24 Jul 2023 22:35:25 +0200 Subject: [PATCH 096/586] Add test for foreman-proxy-content-pulpcore-hide-guarded-distributions installer setting --- pytest_fixtures/core/sat_cap_factory.py | 27 +- robottelo/constants/__init__.py | 1 + robottelo/hosts.py | 5 + tests/foreman/destructive/test_clone.py | 1 - tests/foreman/installer/test_installer.py | 319 +++++++++++++--------- 5 files changed, 220 insertions(+), 133 deletions(-) diff --git a/pytest_fixtures/core/sat_cap_factory.py b/pytest_fixtures/core/sat_cap_factory.py index 89287967c19..7331ac36a06 100644 --- a/pytest_fixtures/core/sat_cap_factory.py +++ b/pytest_fixtures/core/sat_cap_factory.py @@ -8,6 +8,7 @@ from robottelo.config import configure_nailgun from robottelo.config import settings from robottelo.hosts import Capsule +from robottelo.hosts import get_sat_rhel_version from robottelo.hosts import IPAHost from robottelo.hosts import lru_sat_ready_rhel from robottelo.hosts import Satellite @@ -253,21 +254,35 @@ def parametrized_enrolled_sat( Broker(hosts=[new_sat]).checkin() -@pytest.fixture -def sat_ready_rhel(request): +def get_deploy_args(request): deploy_args = { - 'deploy_rhel_version': request.param, + 'deploy_rhel_version': get_sat_rhel_version().base_version, 'deploy_flavor': settings.flavors.default, 'promtail_config_template_file': 'config_sat.j2', 'workflow': 'deploy-rhel', } - # if 'deploy_rhel_version' is not set, let's default to RHEL 8 - deploy_args['deploy_rhel_version'] = deploy_args.get('deploy_rhel_version', '8') - deploy_args['workflow'] = 'deploy-rhel' + if hasattr(request, 'param'): + if isinstance(request.param, dict): + deploy_args.update(request.param) + else: + deploy_args['deploy_rhel_version'] = request.param + return deploy_args + + +@pytest.fixture +def sat_ready_rhel(request): + deploy_args = get_deploy_args(request) with Broker(**deploy_args, host_class=Satellite) as host: yield host +@pytest.fixture(scope='module') +def module_sat_ready_rhels(request): + deploy_args = get_deploy_args(request) + with Broker(**deploy_args, host_class=Satellite, _count=2) as hosts: + yield hosts + + @pytest.fixture def cap_ready_rhel(): rhel8 = settings.content_host.rhel8.vm diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index f146c899c41..2a804bca699 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -1742,6 +1742,7 @@ class Colored(Box): CAPSULE_ANSWER_FILE = "/etc/foreman-installer/scenarios.d/capsule-answers.yaml" MAINTAIN_HAMMER_YML = "/etc/foreman-maintain/foreman-maintain-hammer.yml" SATELLITE_MAINTAIN_YML = "/etc/foreman-maintain/foreman_maintain.yml" +FOREMAN_SETTINGS_YML = '/etc/foreman/settings.yaml' FOREMAN_TEMPLATE_IMPORT_URL = 'https://github.com/SatelliteQE/foreman_templates.git' FOREMAN_TEMPLATE_IMPORT_API_URL = 'http://api.github.com/repos/SatelliteQE/foreman_templates' diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 06283794a71..28eb79c5227 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1736,6 +1736,11 @@ def capsule_certs_generate(self, capsule, cert_path=None, **extra_kwargs): install_cmd = InstallerCommand.from_cmd_str(cmd_str=result.stdout) return cert_file_path, result, install_cmd + def load_remote_yaml_file(self, file_path): + """Load a remote yaml file and return a Box object""" + data = self.session.sftp_read(file_path, return_data=True) + return Box(yaml.load(data, yaml.FullLoader)) + def __enter__(self): """Satellite objects can be used as a context manager to temporarily force everything to use the Satellite object's hostname. diff --git a/tests/foreman/destructive/test_clone.py b/tests/foreman/destructive/test_clone.py index 861ae2dc311..44fbfd8c13a 100644 --- a/tests/foreman/destructive/test_clone.py +++ b/tests/foreman/destructive/test_clone.py @@ -27,7 +27,6 @@ @pytest.mark.e2e -@pytest.mark.parametrize("sat_ready_rhel", [8], indirect=True) @pytest.mark.parametrize('backup_type', ['online', 'offline']) @pytest.mark.parametrize('skip_pulp', [False, True], ids=['include_pulp', 'skip_pulp']) def test_positive_clone_backup(target_sat, sat_ready_rhel, backup_type, skip_pulp): diff --git a/tests/foreman/installer/test_installer.py b/tests/foreman/installer/test_installer.py index 2af8eda6baa..66b9c0de11b 100644 --- a/tests/foreman/installer/test_installer.py +++ b/tests/foreman/installer/test_installer.py @@ -17,10 +17,15 @@ :Upstream: No """ import pytest +import requests from robottelo import ssh from robottelo.config import settings from robottelo.constants import DEFAULT_ORG +from robottelo.constants import FOREMAN_SETTINGS_YML +from robottelo.constants import PRDS +from robottelo.constants import REPOS +from robottelo.constants import REPOSET from robottelo.hosts import setup_capsule from robottelo.utils.installer import InstallerCommand @@ -1321,6 +1326,185 @@ def extract_help(filter='params'): yield token.replace(',', '') +def common_sat_install_assertions(satellite): + sat_version = 'stream' if satellite.is_stream else satellite.version + assert settings.server.version.release == sat_version + result = satellite.execute( + r'grep "\[ERROR" --after-context=100 /var/log/foreman-installer/satellite.log' + ) + assert len(result.stdout) == 0 + result = satellite.cli.Health.check() + assert 'FAIL' not in result.stdout + + +def install_satellite(satellite, installer_args): + # Register for RHEL8 repos, get Ohsnap repofile, and enable and download satellite + satellite.register_to_cdn() + satellite.download_repofile(product='satellite', release=settings.server.version.release) + satellite.execute('dnf -y module enable satellite:el8 && dnf -y install satellite') + # Configure Satellite firewall to open communication + satellite.execute( + 'firewall-cmd --permanent --add-service RH-Satellite-6 && firewall-cmd --reload' + ) + # Install Satellite + satellite.execute( + InstallerCommand(installer_args=installer_args).get_command(), + timeout='30m', + ) + common_sat_install_assertions(satellite) + + +@pytest.fixture(scope='module') +def sat_default_install(module_sat_ready_rhels): + """Install Satellite with default options""" + installer_args = [ + 'scenario satellite', + f'foreman-initial-admin-password {settings.server.admin_password}', + ] + install_satellite(module_sat_ready_rhels[0], installer_args) + return module_sat_ready_rhels[0] + + +@pytest.fixture(scope='module') +def sat_non_default_install(module_sat_ready_rhels): + """Install Satellite with various options""" + installer_args = [ + 'scenario satellite', + f'foreman-initial-admin-password {settings.server.admin_password}', + 'foreman-rails-cache-store type:redis', + 'foreman-proxy-content-pulpcore-hide-guarded-distributions false', + ] + install_satellite(module_sat_ready_rhels[1], installer_args) + return module_sat_ready_rhels[1] + + +@pytest.mark.e2e +@pytest.mark.tier1 +def test_capsule_installation(sat_default_install, cap_ready_rhel, default_org): + """Run a basic Capsule installation + + :id: 64fa85b6-96e6-4fea-bea4-a30539d59e65 + + :steps: + 1. Get a Satellite + 2. Configure capsule repos + 3. Enable capsule module + 4. Install and setup capsule + + :expectedresults: + 1. Capsule is installed and setup correctly + + :CaseImportance: Critical + """ + # Get Capsule repofile, and enable and download satellite-capsule + cap_ready_rhel.register_to_cdn() + cap_ready_rhel.download_repofile(product='capsule', release=settings.server.version.release) + cap_ready_rhel.execute( + 'dnf -y module enable satellite-capsule:el8 && dnf -y install satellite-capsule' + ) + # Setup Capsule + org = sat_default_install.api.Organization().search(query={'search': f'name="{DEFAULT_ORG}"'})[ + 0 + ] + setup_capsule(sat_default_install, cap_ready_rhel, org) + assert sat_default_install.api.Capsule().search( + query={'search': f'name={cap_ready_rhel.hostname}'} + )[0] + result = cap_ready_rhel.execute( + r'grep "\[ERROR" --after-context=100 /var/log/foreman-installer/satellite.log' + ) + assert len(result.stdout) == 0 + result = cap_ready_rhel.execute( + r'grep "\[ERROR" --after-context=100 /var/log/foreman-installer/capsule.log' + ) + assert len(result.stdout) == 0 + result = cap_ready_rhel.cli.Health.check() + assert 'FAIL' not in result.stdout + + +@pytest.mark.e2e +@pytest.mark.tier1 +def test_foreman_rails_cache_store(sat_non_default_install): + """Test foreman-rails-cache-store option + + :id: 379a2fe8-1085-4a7f-8ac3-24c421412f12 + + :steps: + 1. Install Satellite. + 2. Verify that foreman-redis package is installed. + 3. Check /etc/foreman/settings.yaml + + :CaseImportance: Medium + + :customerscenario: true + + :BZ: 2063717, 2165092 + """ + # Verify foreman-rails-cache-store option works + assert sat_non_default_install.execute('rpm -q foreman-redis').status == 0 + settings_file = sat_non_default_install.load_remote_yaml_file(FOREMAN_SETTINGS_YML) + assert settings_file.rails_cache_store.type == 'redis' + + +@pytest.mark.e2e +@pytest.mark.tier1 +def test_content_guarded_distributions_option( + sat_default_install, sat_non_default_install, module_sca_manifest +): + """Verify foreman-proxy-content-pulpcore-hide-guarded-distributions option works + + :id: a9ceefbc-fc2d-415e-9461-1811fabc63dc + + :steps: + 1. Install Satellite. + 2. Verify that no content is listed on https://sat-fqdn/pulp/content/ + with default Satellite installation. + 3. Verify that content is not downloadable when content guard setting is disabled. + + :expectedresults: + 1. no content is listed on https://sat-fqdn/pulp/content/ + + :CaseImportance: Medium + + :customerscenario: true + + :BZ: 2063717, 2088559 + """ + # Verify that no content is listed on https://sat-fqdn/pulp/content/. + result = requests.get(f'https://{sat_default_install.hostname}/pulp/content/', verify=False) + assert 'Default_Organization' not in result.text + # Verify that content is not downloadable when content guard setting is disabled. + org = sat_non_default_install.api.Organization().create() + sat_non_default_install.upload_manifest(org.id, module_sca_manifest.content) + # sync ansible repo + product = sat_non_default_install.api.Product(name=PRDS['rhae'], organization=org.id).search()[ + 0 + ] + r_set = sat_non_default_install.api.RepositorySet( + name=REPOSET['rhae2.9_el8'], product=product + ).search()[0] + r_set.enable( + data={ + 'basearch': 'x86_64', + 'name': REPOSET['rhae2.9_el8'], + 'organization-id': org.id, + 'product_id': product.id, + 'releasever': '8', + } + ) + rh_repo = sat_non_default_install.api.Repository(name=REPOS['rhae2.9_el8']['name']).search( + query={'organization_id': org.id} + )[0] + rh_repo.sync() + assert ( + "403: [('PEM routines', 'get_name', 'no start line')]" + in sat_non_default_install.execute( + f'curl https://{sat_non_default_install.hostname}/pulp/content/{org.label}' + f'/Library/content/dist/layered/rhel8/x86_64/ansible/2.9/os/' + ).stdout + ) + + @pytest.mark.upgrade @pytest.mark.tier1 def test_positive_selinux_foreman_module(target_sat): @@ -1429,95 +1613,6 @@ def test_installer_options_and_sections(filter): assert previous == current, msg -@pytest.mark.tier1 -@pytest.mark.build_sanity -@pytest.mark.first_sanity -@pytest.mark.parametrize( - "installer_satellite", [settings.server.version.rhel_version], indirect=True -) -def test_satellite_installation(installer_satellite): - """Run a basic Satellite installation - - :id: 661206f3-2eec-403c-af26-3c5cadcd5766 - - :steps: - 1. Get RHEL Host - 2. Configure satellite repos - 3. Enable satellite module - 4. Install satellite - 5. Run satellite-installer - - :expectedresults: - 1. Correct satellite packaged is installed - 2. satellite-installer runs successfully - 3. satellite-maintain health check runs successfully - - :CaseImportance: Critical - - """ - result = installer_satellite.execute( - r'grep "\[ERROR" --after-context=100 /var/log/foreman-installer/satellite.log' - ) - assert len(result.stdout) == 0 - result = installer_satellite.cli.Health.check() - assert 'FAIL' not in result.stdout - - -@pytest.mark.e2e -@pytest.mark.tier1 -@pytest.mark.parametrize( - "installer_satellite", [settings.server.version.rhel_version], indirect=True -) -def test_satellite_and_capsule_installation(installer_satellite, cap_ready_rhel): - """Run a basic Satellite and Capsule installation - - :id: bbab30a6-6861-494f-96dd-23b883c2c906 - - :steps: - 1. Get 2 RHEL hosts - 2. Configure satellite repos - 3. Enable satellite module - 4. Install satellite - 5. Run satellite-installer - 6. Configure capsule repos - 7. Enable capsule module - 8. Install and setup capsule - - :expectedresults: - 1. Correct satellite packaged is installed - 2. satellite-installer runs successfully - 3. satellite-maintain health check runs successfully - 4. Capsule is installed and setup correctly - - :CaseImportance: Critical - """ - # Get Capsule repofile, and enable and download satellite-capsule - cap_ready_rhel.register_to_cdn() - cap_ready_rhel.download_repofile(product='capsule', release=settings.server.version.release) - cap_ready_rhel.execute( - 'dnf -y module enable satellite-capsule:el8 && dnf -y install satellite-capsule' - ) - # Configure Satellite firewall to open communication - installer_satellite.execute( - 'firewall-cmd --permanent --add-service RH-Satellite-6 && ' - 'firewall-cmd --add-service RH-Satellite-6' - ) - # Setup Capsule - org = installer_satellite.api.Organization().search(query={'search': f'name="{DEFAULT_ORG}"'})[ - 0 - ] - setup_capsule(installer_satellite, cap_ready_rhel, org) - assert installer_satellite.api.Capsule().search( - query={'search': f'name={cap_ready_rhel.hostname}'} - )[0] - result = cap_ready_rhel.execute( - r'grep "\[ERROR" --after-context=100 /var/log/foreman-installer/satellite.log' - ) - assert len(result.stdout) == 0 - result = cap_ready_rhel.cli.Health.check() - assert 'FAIL' not in result.stdout - - @pytest.mark.stubbed @pytest.mark.tier3 def test_satellite_installation_on_ipv6(): @@ -1834,55 +1929,27 @@ def test_installer_cap_pub_directory_accessibility(capsule_configured): assert 'Success!' in command_output.stdout -@pytest.mark.e2e @pytest.mark.tier1 -@pytest.mark.parametrize("sat_ready_rhel", [settings.server.version.rhel_version], indirect=True) -def test_satellite_installation_with_options(sat_ready_rhel): - """Run Satellite installation with different options +@pytest.mark.build_sanity +@pytest.mark.first_sanity +def test_satellite_installation(installer_satellite): + """Run a basic Satellite installation - :id: fa315028-d9fd-42b3-a07b-53166203abdc + :id: 661206f3-2eec-403c-af26-3c5cadcd5766 :steps: - 1. Get a RHEL host + 1. Get RHEL Host 2. Configure satellite repos 3. Enable satellite module 4. Install satellite - 5. Run satellite-installer with required options + 5. Run satellite-installer :expectedresults: 1. Correct satellite packaged is installed 2. satellite-installer runs successfully 3. satellite-maintain health check runs successfully - :CaseImportance: High - - :customerscenario: true + :CaseImportance: Critical - :BZ: 2063717, 2165092 """ - sat_version = settings.server.version.release - # Register for RHEL8 repos, get Ohsnap repofile, and enable and download satellite - sat_ready_rhel.register_to_cdn() - sat_ready_rhel.download_repofile(product='satellite', release=settings.server.version.release) - sat_ready_rhel.execute('dnf -y module enable satellite:el8 && dnf -y install satellite') - installed_version = sat_ready_rhel.execute('rpm --query satellite').stdout - assert sat_version in installed_version - # Install Satellite - sat_ready_rhel.execute( - InstallerCommand( - installer_args=[ - 'scenario satellite', - f'foreman-initial-admin-password {settings.server.admin_password}', - 'foreman-rails-cache-store type:redis', - ] - ).get_command(), - timeout='30m', - ) - result = sat_ready_rhel.execute( - r'grep "\[ERROR" --after-context=100 /var/log/foreman-installer/satellite.log' - ) - assert len(result.stdout) == 0 - result = sat_ready_rhel.cli.Health.check() - assert 'FAIL' not in result.stdout - assert sat_ready_rhel.execute('rpm -q foreman-redis').status == 0 - assert sat_ready_rhel.execute('grep "type: redis" /etc/foreman/settings.yaml').status == 0 + common_sat_install_assertions(installer_satellite) From 2a06017f00dbea464ba5bddb4565cfe0d71beb86 Mon Sep 17 00:00:00 2001 From: Griffin Sullivan Date: Fri, 21 Jul 2023 17:07:50 -0400 Subject: [PATCH 097/586] Adding wait for to service enable test (cherry picked from commit a3061f1a358615abdce8e334390e59e48d59e8e9) --- tests/foreman/maintain/test_service.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/foreman/maintain/test_service.py b/tests/foreman/maintain/test_service.py index 78ca8129fc7..9886d368761 100644 --- a/tests/foreman/maintain/test_service.py +++ b/tests/foreman/maintain/test_service.py @@ -18,11 +18,13 @@ """ import pytest from fauxfactory import gen_string +from wait_for import wait_for from robottelo.config import settings from robottelo.constants import HAMMER_CONFIG from robottelo.constants import MAINTAIN_HAMMER_YML from robottelo.constants import SATELLITE_ANSWER_FILE +from robottelo.hosts import Satellite pytestmark = pytest.mark.destructive @@ -158,7 +160,24 @@ def test_positive_service_enable_disable(sat_maintain): assert 'FAIL' not in result.stdout assert result.status == 0 sat_maintain.power_control(state='reboot') - result = sat_maintain.cli.Service.status(options={'brief': True, 'only': 'foreman.service'}) + if type(sat_maintain) is Satellite: + result, _ = wait_for( + sat_maintain.cli.Service.status, + func_kwargs={'options': {'brief': True, 'only': 'foreman.service'}}, + fail_condition=lambda res: "FAIL" in res.stdout, + handle_exception=True, + delay=30, + timeout=300, + ) + else: + result, _ = wait_for( + sat_maintain.cli.Service.status, + func_kwargs={'options': {'brief': True}}, + fail_condition=lambda res: "FAIL" in res.stdout, + handle_exception=True, + delay=30, + timeout=300, + ) assert 'FAIL' not in result.stdout assert result.status == 0 From bd53a84d7dcddf5e9096973de747ef3cfde83abe Mon Sep 17 00:00:00 2001 From: Omkar Khatavkar Date: Wed, 26 Jul 2023 13:48:51 +0530 Subject: [PATCH 098/586] [6.14.z]- ignore the create issue in non satellite version label (#12010) ignore the create issue in non satellite version label --- .github/workflows/auto_cherry_pick.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto_cherry_pick.yml b/.github/workflows/auto_cherry_pick.yml index c8f087bf3a0..3ef661b252f 100644 --- a/.github/workflows/auto_cherry_pick.yml +++ b/.github/workflows/auto_cherry_pick.yml @@ -97,7 +97,7 @@ jobs: ## Failure Logging to issues and GChat Group - name: Create Github issue on cherrypick failure id: create-issue - if: ${{ always() && steps.cherrypick.outcome == 'failure' }} + if: ${{ always() && steps.cherrypick.outcome != 'success' && startsWith(matrix.label, '6.') && matrix.label != github.base_ref }} uses: dacbd/create-issue-action@main with: token: ${{ secrets.CHERRYPICK_PAT }} From ce545ceacafdd2f627ac3f333d4076158ed49caf Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 26 Jul 2023 08:46:56 -0400 Subject: [PATCH 099/586] [6.14.z] Fix test_content_access_after_stopped_foreman (#12015) --- tests/foreman/destructive/test_contenthost.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/foreman/destructive/test_contenthost.py b/tests/foreman/destructive/test_contenthost.py index 451e8ca6b35..1f624475026 100644 --- a/tests/foreman/destructive/test_contenthost.py +++ b/tests/foreman/destructive/test_contenthost.py @@ -46,13 +46,6 @@ def test_content_access_after_stopped_foreman(target_sat, rhel7_contenthost): """ org = target_sat.api.Organization().create() org.sca_disable() - # adding remote_execution_connect_by_ip=Yes at org level - target_sat.api.Parameter( - name='remote_execution_connect_by_ip', - value='Yes', - parameter_type='boolean', - organization=org.id, - ).create() lce = target_sat.api.LifecycleEnvironment(organization=org).create() repos_collection = target_sat.cli_factory.RepositoryCollection( distro='rhel7', @@ -60,7 +53,7 @@ def test_content_access_after_stopped_foreman(target_sat, rhel7_contenthost): target_sat.cli_factory.YumRepository(url=settings.repos.yum_1.url), ], ) - repos_collection.setup_content(org.id, lce.id, upload_manifest=False) + repos_collection.setup_content(org.id, lce.id, upload_manifest=False, override=True) repos_collection.setup_virtual_machine(rhel7_contenthost, install_katello_agent=False) result = rhel7_contenthost.execute(f'yum -y install {FAKE_1_CUSTOM_PACKAGE}') assert result.status == 0 From b1cb6d226b134da4a2536a9973a114f812ed8a2b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 26 Jul 2023 11:56:30 -0400 Subject: [PATCH 100/586] [6.14.z] Fix double yielding for capsule conf fixture (#12004) Fix double yielding for capsule conf fixture (#11977) (cherry picked from commit 271d7658466feadc3dee137fddfacd92c0852f0f) Co-authored-by: Jitendra Yejare --- pytest_fixtures/core/sat_cap_factory.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pytest_fixtures/core/sat_cap_factory.py b/pytest_fixtures/core/sat_cap_factory.py index 7331ac36a06..0ca8431ff9a 100644 --- a/pytest_fixtures/core/sat_cap_factory.py +++ b/pytest_fixtures/core/sat_cap_factory.py @@ -170,10 +170,8 @@ def module_capsule_configured(module_capsule_host, module_target_sat): @pytest.fixture(scope='session') def session_capsule_configured(session_capsule_host, session_target_sat): """Configure the capsule instance with the satellite from settings.server.hostname""" - if session_capsule_host: - session_capsule_host.capsule_setup(sat_host=session_target_sat) - yield session_capsule_host - yield + session_capsule_host.capsule_setup(sat_host=session_target_sat) + yield session_capsule_host @pytest.fixture(scope='module') From 84fb1f666bec3ef4e87e612371adae02dba3d89f Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 28 Jul 2023 02:47:57 -0400 Subject: [PATCH 101/586] [6.14.z] Bump sphinx from 7.1.0 to 7.1.1 (#12038) Bump sphinx from 7.1.0 to 7.1.1 (#12028) (cherry picked from commit e66fe20a60ceecf3329f7f0d07ce2321bcfe8c8b) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 36dba852e8c..c21b550732a 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -5,7 +5,7 @@ redis==4.5.5 pre-commit==3.3.3 # For generating documentation. -sphinx==7.1.0 +sphinx==7.1.1 sphinx-autoapi==2.1.1 # For 'manage' interactive shell From fb41604ef6f2854d5b7401c6455708878735f5aa Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 28 Jul 2023 02:52:13 -0400 Subject: [PATCH 102/586] [6.14.z] Bump productmd from 1.35 to 1.36 (#12031) Bump productmd from 1.35 to 1.36 (#12027) (cherry picked from commit a7b227e6e6fa74e327c8ba4168a530fba303fa64) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0f66ec3751a..24d5e33941d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ fauxfactory==3.1.0 jinja2==3.1.2 manifester==0.0.13 navmazing==1.1.6 -productmd==1.35 +productmd==1.36 pyotp==2.8.0 python-box==7.0.1 pytest==7.4.0 From 0846cb1ffa362ea2ca46bd7eabc06cece38456fa Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 28 Jul 2023 02:54:11 -0400 Subject: [PATCH 103/586] [6.14.z] api endpoints updated (#12026) api endpoints updated (#11893) (cherry picked from commit 1801a1cd60f7ee31e3af78643d65bd49bbc204f1) Co-authored-by: Peter Ondrejka --- tests/foreman/endtoend/test_api_endtoend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/foreman/endtoend/test_api_endtoend.py b/tests/foreman/endtoend/test_api_endtoend.py index c152bd0c4a9..2ffdb772993 100644 --- a/tests/foreman/endtoend/test_api_endtoend.py +++ b/tests/foreman/endtoend/test_api_endtoend.py @@ -849,7 +849,7 @@ '/katello/api/sync_plans', '/katello/api/sync_plans/:id/sync', ), - 'sync': ('/katello/api/organizations/:organization_id/products/:product_id/sync',), + 'sync': ('/katello/api/repositories/:repository_id/sync',), 'tailoring_files': ( '/api/compliance/tailoring_files', '/api/compliance/tailoring_files', @@ -912,6 +912,7 @@ '/api/webhooks', '/api/webhooks/:id', '/api/webhooks/:id', + '/api/webhooks/:id/test', '/api/webhooks/events', ), 'webhook_templates': ( From fdbd021007d0c0b0390f8d1b78bad202f0262e79 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 28 Jul 2023 03:16:26 -0400 Subject: [PATCH 104/586] [6.14.z] Don't just take random webhook, search by name (#12045) --- tests/foreman/ui/test_webhook.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/foreman/ui/test_webhook.py b/tests/foreman/ui/test_webhook.py index 731bb6053f0..32cebf1a4b2 100644 --- a/tests/foreman/ui/test_webhook.py +++ b/tests/foreman/ui/test_webhook.py @@ -74,7 +74,9 @@ def test_positive_end_to_end(session, target_sat): assert values['credentials']['capsule_auth'] is True assert values['credentials']['verify_ssl'] is False assert values['credentials']['user'] == username - result = target_sat.execute('echo "Webhook.last.password" | foreman-rake console') + result = target_sat.execute( + f'echo "Webhook.find_by_name(\\"{hook_name}\\").password" | foreman-rake console' + ) assert password in result.stdout session.webhook.update( hook_name, @@ -94,7 +96,9 @@ def test_positive_end_to_end(session, target_sat): assert values['general']['template'] == new_template assert values['general']['http_method'] == new_http_method assert values['general']['enabled'] is True - result = target_sat.execute('echo "Webhook.last.password" | foreman-rake console') + result = target_sat.execute( + f'echo "Webhook.find_by_name(\\"{new_hook_name}\\").password" | foreman-rake console' + ) assert password in result.stdout session.webhook.delete(new_hook_name) assert not session.webhook.search(new_hook_name) From d6adf6ccee690605d7f88638ea3964a6317dd139 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 28 Jul 2023 13:09:23 -0400 Subject: [PATCH 105/586] [6.14.z] new hammer command and log level fix (#12047) new hammer command and log level fix (#11900) new hammer commands (cherry picked from commit b361ffcba45eaed02df59b968b557e368dd23936) Co-authored-by: Peter Ondrejka --- tests/foreman/cli/test_hammer.py | 4 ++-- tests/foreman/data/hammer_commands.json | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/foreman/cli/test_hammer.py b/tests/foreman/cli/test_hammer.py index 8fe12f17627..966c53b2117 100644 --- a/tests/foreman/cli/test_hammer.py +++ b/tests/foreman/cli/test_hammer.py @@ -181,9 +181,9 @@ def test_positive_check_debug_log_levels(target_sat): """ target_sat.cli.Admin.logging({'all': True, 'level-debug': True}) # Verify value of `log4j.logger.org.candlepin` as `DEBUG` - result = target_sat.execute('grep DEBUG /etc/candlepin/candlepin.conf') + result = target_sat.execute('grep log4j.logger.org.candlepin /etc/candlepin/candlepin.conf') assert result.status == 0 - assert 'log4j.logger.org.candlepin = DEBUG' in result.stdout + assert 'DEBUG' in result.stdout target_sat.cli.Admin.logging({"all": True, "level-production": True}) # Verify value of `log4j.logger.org.candlepin` as `WARN` diff --git a/tests/foreman/data/hammer_commands.json b/tests/foreman/data/hammer_commands.json index eb1c144e14b..90b88f4536f 100644 --- a/tests/foreman/data/hammer_commands.json +++ b/tests/foreman/data/hammer_commands.json @@ -33308,6 +33308,12 @@ "description": "Update the CDN configuration", "name": "configure-cdn", "options": [ + { + "help": "If product certificates should be used to authenticate to a custom CDN.", + "name": "custom-cdn-auth-enabled", + "shortname": null, + "value": "VALUE" + }, { "help": "Id of the Organization", "name": "id", From 3f2d25079fbeb8c9695e4c0363dc632406742a78 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 1 Aug 2023 03:48:09 -0400 Subject: [PATCH 106/586] [6.14.z] Add automation for BZ 2175005, BZ 1955861/1784012 (#12065) --- tests/foreman/api/test_provisioning.py | 8 +- .../foreman/api/test_provisioningtemplate.py | 90 ++++++++++++++++--- 2 files changed, 84 insertions(+), 14 deletions(-) diff --git a/tests/foreman/api/test_provisioning.py b/tests/foreman/api/test_provisioning.py index e129fac7bd6..4d54a403672 100644 --- a/tests/foreman/api/test_provisioning.py +++ b/tests/foreman/api/test_provisioning.py @@ -52,9 +52,11 @@ def test_rhel_pxe_provisioning( 2. Satellite is able to run REX job on the host 3. Host is registered to Satellite and subscription status is 'Success' - :BZ: 2105441 + :BZ: 2105441, 1955861, 1784012 :customerscenario: true + + :parametrized: yes """ host_mac_addr = provisioning_host._broker_args['provisioning_nic_mac_addr'] sat = module_provisioning_sat.sat @@ -121,6 +123,10 @@ def test_rhel_pxe_provisioning( provisioning_host.os_version == expected_rhel_version ), 'Different than the expected OS version was installed' + # Verify provisioning log exists on host at correct path + assert provisioning_host.execute('test -s /root/install.post.log').status == 0 + assert provisioning_host.execute('test -s /mnt/sysimage/root/install.post.log').status == 1 + # 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 diff --git a/tests/foreman/api/test_provisioningtemplate.py b/tests/foreman/api/test_provisioningtemplate.py index 58f3b0bae2e..cf8225bb933 100644 --- a/tests/foreman/api/test_provisioningtemplate.py +++ b/tests/foreman/api/test_provisioningtemplate.py @@ -244,16 +244,14 @@ def test_positive_build_pxe_default(self, tftpboot, module_target_sat): r.raise_for_status() rendered = r.text else: - rendered = module_target_sat.execute(f'cat {template["path"]}').stdout.splitlines()[ - 0 - ] + rendered = module_target_sat.execute(f'cat {template["path"]}').stdout if template['kind'] == 'iPXE': assert f'{module_target_sat.hostname}/unattended/iPXE' in r.text else: assert ( - rendered == f'{settings.server.scheme}://' - f'{module_target_sat.hostname} {template["kind"]}' + f'{settings.server.scheme}://{module_target_sat.hostname} {template["kind"]}' + in rendered ) @pytest.mark.parametrize('module_sync_kickstart_content', [7, 8, 9], indirect=True) @@ -277,6 +275,8 @@ def test_positive_provision_template_check_net_interface( :BZ: 2148433 :customerscenario: true + + :parametrized: yes """ macaddress = gen_mac(multicast=False) capsule = module_target_sat.nailgun_smart_proxy @@ -296,8 +296,8 @@ def test_positive_provision_template_check_net_interface( 'lifecycle_environment_id': module_lce_library.id, }, ).create() - provision_template = host.read_template(data={'template_kind': 'provision'}) - assert 'ifcfg-$sanitized_real' in str(provision_template) + provision_template = host.read_template(data={'template_kind': 'provision'})['template'] + assert 'ifcfg-$sanitized_real' in provision_template @pytest.mark.e2e @pytest.mark.parametrize('module_sync_kickstart_content', [7, 8, 9], indirect=True) @@ -323,6 +323,8 @@ def test_positive_template_check_ipxe( :BZ: 2149030 :customerscenario: true + + :parametrized: yes """ macaddress = gen_mac(multicast=False) capsule = module_target_sat.nailgun_smart_proxy @@ -342,9 +344,9 @@ def test_positive_template_check_ipxe( 'lifecycle_environment_id': module_lce_library.id, }, ).create() - ipxe_template = host.read_template(data={'template_kind': 'iPXE'}) + ipxe_template = host.read_template(data={'template_kind': 'iPXE'})['template'] ks_param = 'ks=' if module_sync_kickstart_content.rhel_ver <= 8 else 'inst.ks=' - assert str(ipxe_template).count(ks_param) == 1 + assert ipxe_template.count(ks_param) == 1 @pytest.mark.parametrize('module_sync_kickstart_content', [7, 8, 9], indirect=True) def test_positive_template_check_vlan_parameter( @@ -368,6 +370,8 @@ def test_positive_template_check_vlan_parameter( :BZ: 1607706 :customerscenario: true + + :parametrized: yes """ macaddress = gen_mac(multicast=False) capsule = module_target_sat.nailgun_smart_proxy @@ -406,7 +410,67 @@ def test_positive_template_check_vlan_parameter( }, ], ).create() - provision_template = host.read_template(data={'template_kind': 'provision'}) - assert f'interfacename=vlan{tag}' in str(provision_template) - ipxe_template = host.read_template(data={'template_kind': 'iPXE'}) - assert f'vlan=vlan{tag}:{identifier}' in str(ipxe_template) + provision_template = host.read_template(data={'template_kind': 'provision'})['template'] + assert f'interfacename=vlan{tag}' in provision_template + ipxe_template = host.read_template(data={'template_kind': 'iPXE'})['template'] + assert f'vlan=vlan{tag}:{identifier}' in ipxe_template + + @pytest.mark.parametrize('module_sync_kickstart_content', [7, 8, 9], indirect=True) + @pytest.mark.parametrize('pxe_loader', ['uefi'], indirect=True) + @pytest.mark.parametrize('boot_mode', ['Static', 'DHCP']) + def test_positive_template_subnet_with_boot_mode( + self, + module_sync_kickstart_content, + module_target_sat, + module_sca_manifest_org, + module_location, + default_architecture, + default_partitiontable, + pxe_loader, + boot_mode, + ): + """Check whether boot mode paremeter in subnet respected when PXELoader UEFI is used, + and properly rendered in the provisioning templates + + :id: 2decc787-59b0-41e6-96be-5dd9371c8966 + + :expectedresults: templates should get render and contains boot mode used as set in subnet + + :BZ: 2168967, 1955861, 1784012 + + :customerscenario: true + + :parametrized: yes + """ + subnet = module_target_sat.api.Subnet( + name=gen_string('alpha'), + organization=[module_sca_manifest_org], + location=[module_location], + network='192.168.0.1', + mask='255.255.255.240', + boot_mode=boot_mode, + ).create() + host = module_target_sat.api.Host( + name=gen_string('alpha'), + organization=module_sca_manifest_org, + location=module_location, + subnet=subnet, + pxe_loader=pxe_loader.pxe_loader, + root_pass=settings.provisioning.host_root_password, + ptable=default_partitiontable, + architecture=default_architecture, + operatingsystem=module_sync_kickstart_content.os, + ).create() + # Verify provision templates for boot_mode in subnet, and check provision logs exists + rendered = host.read_template(data={'template_kind': 'provision'})['template'] + assert f'--bootproto {boot_mode.lower()}' in rendered + assert '/root/install.post.log' in rendered + assert '/mnt/sysimage/root/install.post.log' not in rendered + + # Verify PXE templates for boot_mode in subnet + pxe_templates = ['PXEGrub', 'PXEGrub2', 'PXELinux', 'iPXE'] + provision_url = f'http://{module_target_sat.hostname}/unattended/provision' + ks_param = provision_url + '?static=1' if boot_mode == 'Static' else provision_url + for template in pxe_templates: + rendered = host.read_template(data={'template_kind': f'{template}'})['template'] + assert f'ks={ks_param}' in rendered From 9483420475d172da141b2b36f1d00e6a11ec23b3 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 1 Aug 2023 05:20:31 -0400 Subject: [PATCH 107/586] [6.14.z] Adding test for check-update when system is up to date (#12025) Adding test for check-update when system is up to date (#12009) (cherry picked from commit 6d15063eb0d9adfe11b0461296dbccd21c2e2320) Co-authored-by: Griffin Sullivan <48397354+Griffin-Sullivan@users.noreply.github.com> --- tests/foreman/destructive/test_packages.py | 52 ++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/foreman/destructive/test_packages.py diff --git a/tests/foreman/destructive/test_packages.py b/tests/foreman/destructive/test_packages.py new file mode 100644 index 00000000000..3a31e354e46 --- /dev/null +++ b/tests/foreman/destructive/test_packages.py @@ -0,0 +1,52 @@ +"""Test class for satellite-maintain packages functionality + +:Requirement: foreman-maintain + +:CaseAutomation: Automated + +:CaseLevel: Component + +:CaseComponent: ForemanMaintain + +:Team: Platform + +:TestType: Functional + +:CaseImportance: Critical + +:Upstream: No +""" +import pytest + +pytestmark = pytest.mark.destructive + + +def test_positive_all_packages_update(target_sat): + """Verify update and check-update work as expected. + + :id: eb8a5611-b1a8-4a18-b80e-56b045c0d2f6 + + :steps: + 1. Run yum update + 2. Reboot + 3. Run satellite-maintain packages check-update + + :expectedresults: update should update the packages, + and check-update should list no packages at the end. + + :BZ: 2218656 + + :customerscenario: true + """ + # Register to CDN for package updates + target_sat.register_to_cdn() + # Update packages with yum + result = target_sat.execute('yum update -y --disableplugin=foreman-protector') + assert result.status == 0 + # Reboot + if target_sat.execute('needs-restarting -r').status == 1: + target_sat.power_control(state='reboot') + # Run check-update again to verify there are no more packages available to update + result = target_sat.cli.Packages.check_update() + assert 'FAIL' not in result.stdout + assert result.status == 0 From c40fb74652bd6c275966b28fe36c3833a5efcd62 Mon Sep 17 00:00:00 2001 From: Lukas Pramuk Date: Mon, 31 Jul 2023 14:37:01 +0200 Subject: [PATCH 108/586] Fix 4-digit logic in download_repofile and ohsnap_repo_url (cherry picked from commit d4ba7194825b85ea5a31ee8f6b7c046b6e102482) --- robottelo/host_helpers/contenthost_mixins.py | 3 ++- robottelo/utils/ohsnap.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/robottelo/host_helpers/contenthost_mixins.py b/robottelo/host_helpers/contenthost_mixins.py index 63570b13d92..a4ab9457762 100644 --- a/robottelo/host_helpers/contenthost_mixins.py +++ b/robottelo/host_helpers/contenthost_mixins.py @@ -67,10 +67,11 @@ def _dogfood_helper(self, product, release, snap, repo=None): product = self.__class__.__name__.lower() repo = repo or product # if repo is not specified, set it to the same as the product is release = self.satellite.version if not release else str(release) + # issue warning if requesting repofile of different version than the product is settings_release = settings.server.version.release.split('.') if len(settings_release) == 2: settings_release.append('0') - settings_release = '.'.join(settings_release[:3]) # keep only major.minor.patch + settings_release = '.'.join(settings_release) if product != 'client' and release != settings_release: logger.warning( 'Satellite release in settings differs from the one passed to the function ' diff --git a/robottelo/utils/ohsnap.py b/robottelo/utils/ohsnap.py index 202d40f81f1..ac9bf6b81c1 100644 --- a/robottelo/utils/ohsnap.py +++ b/robottelo/utils/ohsnap.py @@ -62,7 +62,6 @@ def ohsnap_repo_url(ohsnap, request_type, product, release, os_release, snap='') else: logger.warning(f'Ohsnap returned no releases for the given stream: {release}') - release = '.'.join(release.split('.')[:3]) # keep only major.minor.patch logger.debug(f'Release string after processing: {release}') return ( f'{ohsnap.host}/api/releases/' From 0a874795a58b00cb2434ab699118363251bb15d2 Mon Sep 17 00:00:00 2001 From: Peter Ondrejka Date: Thu, 23 Mar 2023 13:00:00 +0100 Subject: [PATCH 109/586] dashboard test fix (cherry picked from commit 09439ffaa90fa38bf003668743338ce3f55ad1c9) --- robottelo/host_helpers/repository_mixins.py | 20 ++++++++------ tests/foreman/ui/test_dashboard.py | 29 ++++++++++++--------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/robottelo/host_helpers/repository_mixins.py b/robottelo/host_helpers/repository_mixins.py index e10db0b50f8..a60affdd75d 100644 --- a/robottelo/host_helpers/repository_mixins.py +++ b/robottelo/host_helpers/repository_mixins.py @@ -691,13 +691,18 @@ def setup_content( subscription_names = list(rh_subscriptions) if custom_product_name: subscription_names.append(custom_product_name) - activation_key = self.setup_activation_key( - org_id, - content_view['id'], - lce_id, - subscription_names=subscription_names, - override=override, - ) + if not self.satellite.is_sca_mode_enabled(org_id): + activation_key = self.setup_activation_key( + org_id, + content_view['id'], + lce_id, + subscription_names=subscription_names, + override=override, + ) + else: + activation_key = self.setup_activation_key( + org_id, content_view['id'], lce_id, override=override + ) setup_content_data = dict( activation_key=activation_key, content_view=content_view, @@ -746,7 +751,6 @@ def setup_virtual_machine( repo_labels = [ repo['label'] for repo in self.custom_repos_info if repo['content-type'] == 'yum' ] - vm.contenthost_setup( self.satellite, self.organization['label'], diff --git a/tests/foreman/ui/test_dashboard.py b/tests/foreman/ui/test_dashboard.py index f08465319fb..c1dc9158967 100644 --- a/tests/foreman/ui/test_dashboard.py +++ b/tests/foreman/ui/test_dashboard.py @@ -22,7 +22,7 @@ from nailgun.entity_mixins import TaskFailedError from robottelo.config import settings -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE +from robottelo.constants import FAKE_7_CUSTOM_PACKAGE from robottelo.utils.datafactory import gen_string from robottelo.utils.issue_handlers import is_open @@ -186,23 +186,24 @@ def test_positive_task_status(session): @pytest.mark.upgrade +@pytest.mark.no_containers @pytest.mark.run_in_one_thread @pytest.mark.skip_if_not_set('clients') @pytest.mark.tier3 +@pytest.mark.rhel_ver_match('8') @pytest.mark.skipif((not settings.robottelo.repos_hosting_url), reason='Missing repos_hosting_url') @pytest.mark.parametrize( 'repos_collection', [ { - 'distro': 'rhel7', - 'SatelliteToolsRepository': {}, - 'YumRepository': {'url': settings.repos.yum_6.url}, - }, + 'distro': 'rhel8', + 'YumRepository': {'url': settings.repos.yum_3.url}, + } ], indirect=True, ) def test_positive_user_access_with_host_filter( - test_name, module_location, rhel7_contenthost, target_sat, repos_collection + test_name, module_location, rhel_contenthost, target_sat, repos_collection ): """Check if user with necessary host permissions can access dashboard and required widgets are rendered with proper values @@ -252,21 +253,23 @@ def test_positive_user_access_with_host_filter( with Session(test_name, user=user_login, password=user_password) as session: assert session.dashboard.read('HostConfigurationStatus')['total_count'] == 0 assert len(session.dashboard.read('LatestErrata')['erratas']) == 0 - repos_collection.setup_content(org.id, lce.id, upload_manifest=True) + rhel_contenthost.add_rex_key(target_sat) + repos_collection.setup_content(org.id, lce.id, upload_manifest=False) repos_collection.setup_virtual_machine( - rhel7_contenthost, location_title=module_location.name + rhel_contenthost, location_title=module_location.name, install_katello_agent=False ) - result = rhel7_contenthost.run(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}') + rhel_contenthost.run('subscription-manager repos --enable "*"') + result = rhel_contenthost.run(f'yum install -y {FAKE_7_CUSTOM_PACKAGE}') assert result.status == 0 - hostname = rhel7_contenthost.hostname + hostname = rhel_contenthost.hostname # Check UI for values assert session.host.search(hostname)[0]['Name'] == hostname hosts_values = session.dashboard.read('HostConfigurationStatus') assert hosts_values['total_count'] == 1 errata_values = session.dashboard.read('LatestErrata')['erratas'] - assert len(errata_values) == 1 - assert errata_values[0]['Type'] == 'security' - assert settings.repos.yum_6.errata[2] in errata_values[0]['Errata'] + assert len(errata_values) == 2 + assert 'security' in [e['Type'] for e in errata_values] + assert settings.repos.yum_3.errata[25] in [e['Errata'].split()[0] for e in errata_values] @pytest.mark.tier2 From 404356de493143261616ac626fe38cfb8cc0c24b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 1 Aug 2023 12:24:53 -0400 Subject: [PATCH 110/586] [6.14.z] Fix failing foreman/maintain/health tests (#12073) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix failing foreman/maintain/health tests (#11676) Add a whitelist option and update assertion (cherry picked from commit ce7c3b7f855db4c2e7eedb3da893c1a4ea06aeec) Co-authored-by: Matyáš Strelec --- tests/foreman/maintain/test_health.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/foreman/maintain/test_health.py b/tests/foreman/maintain/test_health.py index 72f6656b0f9..f13286e9e4e 100644 --- a/tests/foreman/maintain/test_health.py +++ b/tests/foreman/maintain/test_health.py @@ -118,7 +118,12 @@ def test_positive_health_check_by_tags(sat_maintain): result = sat_maintain.cli.Health.list_tags().stdout output = [i.split("]\x1b[0m")[0] for i in result.split("\x1b[36m[") if i] for tag in output: - assert sat_maintain.cli.Health.check(options={'tags': tag, 'assumeyes': True}).status == 0 + assert ( + sat_maintain.cli.Health.check( + options={'tags': tag, 'assumeyes': True, 'whitelist': 'non-rh-packages'} + ).status + == 0 + ) @pytest.mark.include_capsule @@ -134,7 +139,9 @@ def test_positive_health_check_pre_upgrade(sat_maintain): :expectedresults: Pre-upgrade health checks should pass. """ - result = sat_maintain.cli.Health.check(options={'tags': 'pre-upgrade'}) + result = sat_maintain.cli.Health.check( + options={'tags': 'pre-upgrade', 'whitelist': 'non-rh-packages'} + ) assert result.status == 0 assert 'FAIL' not in result.stdout @@ -760,7 +767,7 @@ def test_positive_health_check_non_rh_packages(sat_maintain, request): == 0 ) result = sat_maintain.cli.Health.check({'label': 'non-rh-packages'}) - assert 'Found 1 unexpected non Red Hat Package(s) installed!' in result.stdout + assert 'unexpected non Red Hat Package(s) installed!' in result.stdout assert 'walrus-5.21-1.noarch' in result.stdout assert result.status == 78 assert 'WARNING' in result.stdout From e40b4fa27b4ce5724dfda62584576b3e8623b79c Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 2 Aug 2023 02:30:57 -0400 Subject: [PATCH 111/586] [6.14.z] Bump sphinx from 7.1.1 to 7.1.2 (#12086) Bump sphinx from 7.1.1 to 7.1.2 (#12083) (cherry picked from commit 1f2fd547f4e3d19b8094533c37a786144634a2e4) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index c21b550732a..0c3be0b6be7 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -5,7 +5,7 @@ redis==4.5.5 pre-commit==3.3.3 # For generating documentation. -sphinx==7.1.1 +sphinx==7.1.2 sphinx-autoapi==2.1.1 # For 'manage' interactive shell From 7755f2fc0437ca729ee767daf7406ecf6a50715d Mon Sep 17 00:00:00 2001 From: Cole Higgins Date: Thu, 13 Apr 2023 15:34:39 -0400 Subject: [PATCH 112/586] rebase --- tests/foreman/api/test_repositories.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/foreman/api/test_repositories.py b/tests/foreman/api/test_repositories.py index 12b43cb880b..29675dfa2eb 100644 --- a/tests/foreman/api/test_repositories.py +++ b/tests/foreman/api/test_repositories.py @@ -217,3 +217,29 @@ def test_negative_upload_expired_manifest(module_org, target_sat): "The manifest doesn't exist on console.redhat.com. " "Please create and import a new manifest." in error.value.stderr ) + + +@pytest.mark.tier1 +def test_positive_multiple_orgs_with_same_repo(target_sat): + """Test that multiple organizations with the same repository synced have matching metadata + + :id: 39cff8ea-969d-4b8f-9fb4-33b1ba768ff2 + + :Steps: + 1. Create multiple organizations + 2. Sync the same repository to each organization + 3. Assert that each repository from each organization contain the same content counts + + :expectedresults: Each repository in each organziation should have the same content counts + """ + repos = [] + orgs = [target_sat.api.Organization().create() for _ in range(3)] + for org in orgs: + product = target_sat.api.Product(organization=org).create() + repo = target_sat.api.Repository( + content_type='yum', product=product, url=settings.repos.module_stream_1.url + ).create() + repo.sync() + repo_counts = target_sat.api.Repository(id=repo.id).read().content_counts + repos.append(repo_counts) + assert repos[0] == repos[1] == repos[2] From 5edb2bbbe28d8750439b13195d7a1470eec7d10e Mon Sep 17 00:00:00 2001 From: Cole Higgins Date: Thu, 13 Apr 2023 15:36:48 -0400 Subject: [PATCH 113/586] removed incorrect merge --- tests/foreman/api/test_subscription.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/foreman/api/test_subscription.py b/tests/foreman/api/test_subscription.py index 8b68a2a6b45..85b19bd8a85 100644 --- a/tests/foreman/api/test_subscription.py +++ b/tests/foreman/api/test_subscription.py @@ -448,9 +448,7 @@ def test_positive_os_restriction_on_repos(): """ -def test_positive_async_endpoint_for_manifest_refresh( - target_sat, function_entitlement_manifest_org -): +def test_positive_async_endpoint_for_manifest_refresh(target_sat, module_entitlement_manifest_org): """Verify that manifest refresh is using an async endpoint. Previously this was a single, synchronous endpoint. The endpoint to retrieve manifests is now split into two: an async endpoint to start "exporting" the manifest, and a second endpoint to download the @@ -469,12 +467,12 @@ def test_positive_async_endpoint_for_manifest_refresh( :BZ: 2066323 """ - sub = target_sat.api.Subscription(organization=function_entitlement_manifest_org) + sub = target_sat.api.Subscription(organization=module_entitlement_manifest_org) # set log level to 'debug' and restart services target_sat.cli.Admin.logging({'all': True, 'level-debug': True}) target_sat.cli.Service.restart() # refresh manifest and assert new log message to confirm async endpoint - sub.refresh_manifest(data={'organization_id': function_entitlement_manifest_org.id}) + sub.refresh_manifest(data={'organization_id': module_entitlement_manifest_org.id}) results = target_sat.execute( 'grep "Sending GET request to upstream Candlepin" /var/log/foreman/production.log' ) From a0aee970e980a4445813c259d71dacec25121852 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 2 Aug 2023 04:24:59 -0400 Subject: [PATCH 114/586] [6.14.z] Fix TestRepository test cases (#12090) --- tests/foreman/cli/test_repository.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/foreman/cli/test_repository.py b/tests/foreman/cli/test_repository.py index 3f2d0f93d5c..46e0e849a47 100644 --- a/tests/foreman/cli/test_repository.py +++ b/tests/foreman/cli/test_repository.py @@ -1560,7 +1560,7 @@ def test_positive_upload_content(self, repo, target_sat): local_path=DataFile.RPM_TO_UPLOAD, remote_path=f"/tmp/{RPM_TO_UPLOAD}", ) - result = Repository.upload_content( + result = target_sat.cli.Repository.upload_content( { 'name': repo['name'], 'organization': repo['organization'], @@ -1568,8 +1568,11 @@ def test_positive_upload_content(self, repo, target_sat): 'product-id': repo['product']['id'], } ) - assert f"Successfully uploaded file '{RPM_TO_UPLOAD}'" in result[0]['message'] - assert int(Repository.info({'id': repo['id']})['content-counts']['packages']) == 1 + assert f"Successfully uploaded file {RPM_TO_UPLOAD}" == result[0]['message'] + assert ( + int(target_sat.cli.Repository.info({'id': repo['id']})['content-counts']['packages']) + == 1 + ) @pytest.mark.tier1 @pytest.mark.parametrize( @@ -1592,15 +1595,15 @@ def test_positive_upload_content_to_file_repo(self, repo, target_sat): :CaseImportance: Critical """ - Repository.synchronize({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) # Verify it has finished - new_repo = Repository.info({'id': repo['id']}) + new_repo = target_sat.cli.Repository.info({'id': repo['id']}) assert int(new_repo['content-counts']['files']) == CUSTOM_FILE_REPO_FILES_COUNT target_sat.put( local_path=DataFile.OS_TEMPLATE_DATA_FILE, remote_path=f"/tmp/{OS_TEMPLATE_DATA_FILE}", ) - result = Repository.upload_content( + result = target_sat.cli.Repository.upload_content( { 'name': new_repo['name'], 'organization': new_repo['organization'], @@ -1608,8 +1611,8 @@ def test_positive_upload_content_to_file_repo(self, repo, target_sat): 'product-id': new_repo['product']['id'], } ) - assert f"Successfully uploaded file '{OS_TEMPLATE_DATA_FILE}'" in result[0]['message'] - new_repo = Repository.info({'id': new_repo['id']}) + assert f"Successfully uploaded file {OS_TEMPLATE_DATA_FILE}" == result[0]['message'] + new_repo = target_sat.cli.Repository.info({'id': new_repo['id']}) assert int(new_repo['content-counts']['files']) == CUSTOM_FILE_REPO_FILES_COUNT + 1 @pytest.mark.skip_if_open("BZ:1410916") From 349e878408036d43c464e985a85dad89fe8144c2 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 3 Aug 2023 09:58:18 -0400 Subject: [PATCH 115/586] [6.14.z] Test fixes for correctly fetching scap_profile_ids (#12109) Test fixes for correctly fetching scap_profile_ids (#12106) Signed-off-by: Adarsh Dubey (cherry picked from commit 42a058ae13a80a461306a867412e37acae14197f) Co-authored-by: Adarsh dubey --- tests/foreman/cli/test_oscap.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/foreman/cli/test_oscap.py b/tests/foreman/cli/test_oscap.py index e2738b25be1..f5e442476aa 100644 --- a/tests/foreman/cli/test_oscap.py +++ b/tests/foreman/cli/test_oscap.py @@ -32,7 +32,6 @@ from robottelo.config import settings from robottelo.constants import OSCAP_DEFAULT_CONTENT from robottelo.constants import OSCAP_PERIOD -from robottelo.constants import OSCAP_PROFILE from robottelo.constants import OSCAP_WEEKDAY from robottelo.utils.datafactory import invalid_names_list from robottelo.utils.datafactory import parametrized @@ -43,21 +42,16 @@ class TestOpenScap: """Tests related to the oscap cli hammer plugin""" @classmethod - def fetch_scap_and_profile_id(cls, scap_name, scap_profile): + def fetch_scap_and_profile_id(cls, scap_name): """Extracts the scap ID and scap profile id :param scap_name: Scap title - :param scap_profile: Scap profile you want to select :returns: scap_id and scap_profile_id """ default_content = Scapcontent.info({'title': scap_name}, output_format='json') scap_id = default_content['id'] - scap_profile_ids = [ - profile['id'] - for profile in default_content['scap-content-profiles'] - if scap_profile in profile['title'] - ] + scap_profile_ids = default_content['scap-content-profiles'][0]['id'] return scap_id, scap_profile_ids @pytest.mark.tier1 @@ -901,14 +895,14 @@ def test_positive_update_scap_policy_with_content(self, scap_content): ) assert scap_policy['scap-content-id'] == scap_content["scap_id"] scap_id, scap_profile_id = self.fetch_scap_and_profile_id( - OSCAP_DEFAULT_CONTENT['rhel_firefox'], OSCAP_PROFILE['firefox'] + OSCAP_DEFAULT_CONTENT['rhel_firefox'] ) Scappolicy.update( {'name': name, 'scap-content-id': scap_id, 'scap-content-profile-id': scap_profile_id} ) scap_info = Scappolicy.info({'name': name}) assert scap_info['scap-content-id'] == scap_id - assert scap_info['scap-content-profile-id'] == scap_profile_id[0] + assert scap_info['scap-content-profile-id'] == scap_profile_id @pytest.mark.tier2 def test_positive_associate_scap_policy_with_single_server(self, scap_content): From dec87514439c84a09bdb17d95b0a1ef79268b3a2 Mon Sep 17 00:00:00 2001 From: David Moore <109112035+damoore044@users.noreply.github.com> Date: Fri, 4 Aug 2023 03:17:46 -0400 Subject: [PATCH 116/586] [6.14.z] Cherrypick RFE Installable Errata (#12120) * * test Installable Errata ReportTemplate * Minor refinements * Full send * Addressing comments Matching repos --- tests/foreman/api/test_reporttemplates.py | 96 +++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/tests/foreman/api/test_reporttemplates.py b/tests/foreman/api/test_reporttemplates.py index 173e32fbd2c..c3f88b952f7 100644 --- a/tests/foreman/api/test_reporttemplates.py +++ b/tests/foreman/api/test_reporttemplates.py @@ -16,6 +16,8 @@ :Upstream: No """ +from datetime import datetime + import pytest from broker import Broker from fauxfactory import gen_string @@ -26,6 +28,8 @@ from robottelo.config import settings from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME from robottelo.constants import FAKE_1_CUSTOM_PACKAGE +from robottelo.constants import FAKE_1_CUSTOM_PACKAGE_NAME +from robottelo.constants import FAKE_2_CUSTOM_PACKAGE from robottelo.constants import PRDS from robottelo.constants import REPOS from robottelo.constants import REPOSET @@ -707,3 +711,95 @@ def test_positive_generate_job_report(setup_content, target_sat, rhel7_contentho ) assert res[0]['Host'] == rhel7_contenthost.hostname assert '/root' in res[0]['stdout'] + + +@pytest.mark.tier2 +@pytest.mark.no_containers +@pytest.mark.rhel_ver_match(r'^(?!6$)\d+$') +def test_positive_installable_errata( + module_org, module_target_sat, module_location, module_cv, module_lce, rhel_contenthost +): + """Generate an Installable Errata report + + :id: 6263a0fa-5021-4553-939b-84fb71c81d59 + + :setup: A Host with some applied errata + + :steps: + 1. Downgrade a package contained within the applied errata + 2. Perform a search for any applicable Erratum + 3. Generate an Installable Errata report + + :expectedresults: A report is generated with the installable errata listed + + :CaseImportance: Medium + + :customerscenario: true + + :BZ: 1726504 + """ + activation_key = module_target_sat.api.ActivationKey( + environment=module_lce, organization=module_org + ).create() + ERRATUM_ID = str(settings.repos.yum_9.errata[0]) + module_target_sat.cli_factory.setup_org_for_a_custom_repo( + { + 'url': settings.repos.yum_9.url, + 'organization-id': module_org.id, + 'content-view-id': module_cv.id, + 'lifecycle-environment-id': module_lce.id, + 'activationkey-id': activation_key.id, + } + ) + result = rhel_contenthost.register( + module_org, module_location, activation_key.name, module_target_sat + ) + assert f'The registered system name is: {rhel_contenthost.hostname}' in result.stdout + assert rhel_contenthost.subscribed + result = rhel_contenthost.run(f'yum install -y {FAKE_2_CUSTOM_PACKAGE}') + assert result.status == 0 + # Install/Apply the errata + task_id = module_target_sat.api.JobInvocation().run( + data={ + 'feature': 'katello_errata_install', + 'inputs': {'errata': ERRATUM_ID}, + 'targeting_type': 'static_query', + 'search_query': f'name = {rhel_contenthost.hostname}', + 'organization_id': module_org.id, + }, + )['id'] + module_target_sat.wait_for_tasks( + search_query=(f'label = Actions::RemoteExecution::RunHostsJob and id = {task_id}'), + search_rate=15, + max_tries=10, + ) + # Downgrade package impacted by the erratum + result = rhel_contenthost.run(f'yum downgrade -y {FAKE_1_CUSTOM_PACKAGE}') + assert result.status == 0 + + _data_for_generate = { + 'organization_id': module_org.id, + 'report_format': "json", + 'input_values': { + 'Filter Errata Type': 'all', + 'Include Last Reboot': 'no', + 'Status': 'all', + }, + } + search_query = {'search': 'name="Host - Applicable Errata"'} + template = module_target_sat.api.ReportTemplate() + # Allow search to collect results, it may take some time + # Wait until a generated report is populated + wait_for( + lambda: ( + [] != template.search(query=search_query)[0].read().generate(data=_data_for_generate) + ), + timeout=120, + delay=10, + ) + # Now that a populated report is ready, generate a final time + report = template.search(query=search_query)[0].read().generate(data=_data_for_generate) + assert report != [] + assert datetime.now().strftime("%Y-%m-%d") in report[0]['Available since'] + assert FAKE_1_CUSTOM_PACKAGE_NAME in report[0]['Packages'] + assert report[0]['Erratum'] == ERRATUM_ID From 3bafe922df49f9d6f05817d8b974e910b1859232 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 4 Aug 2023 04:21:45 -0400 Subject: [PATCH 117/586] [6.14.z] Notifications: test for new RFE for long-running tasks (#12127) --- tests/foreman/api/test_notifications.py | 229 ++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 tests/foreman/api/test_notifications.py diff --git a/tests/foreman/api/test_notifications.py b/tests/foreman/api/test_notifications.py new file mode 100644 index 00000000000..c832bfd0990 --- /dev/null +++ b/tests/foreman/api/test_notifications.py @@ -0,0 +1,229 @@ +"""Test class for Notifications API + +:Requirement: Notifications + +:CaseAutomation: Automated + +:CaseLevel: Acceptance + +:CaseComponent: Notifications + +:Team: Endeavour + +:TestType: Functional + +:CaseImportance: High + +:Upstream: No +""" +from mailbox import mbox +from re import findall +from tempfile import mkstemp + +import pytest +from fauxfactory import gen_string +from wait_for import TimedOutError +from wait_for import wait_for + +from robottelo.config import settings +from robottelo.constants import DEFAULT_LOC +from robottelo.constants import DEFAULT_ORG +from robottelo.utils.issue_handlers import is_open + + +@pytest.fixture +def admin_user_with_localhost_email(target_sat): + """Admin user with e-mail set to `root@localhost`.""" + user = target_sat.api.User( + admin=True, + default_organization=DEFAULT_ORG, + default_location=DEFAULT_LOC, + description='created by nailgun', + login=gen_string("alphanumeric"), + password=gen_string("alphanumeric"), + mail='root@localhost', + ).create() + user.mail_enabled = True + user.update() + + yield user + + user.delete() + + +@pytest.fixture +def reschedule_long_running_tasks_notification(target_sat): + """Reschedule long-running tasks checker from midnight (default) to every minute. + Reset it back after the test. + """ + default_cron_schedule = '0 0 * * *' + every_minute_cron_schedule = '* * * * *' + + assert ( + target_sat.execute( + f"FOREMAN_TASKS_CHECK_LONG_RUNNING_TASKS_CRONLINE='{every_minute_cron_schedule}' " + "foreman-rake foreman_tasks:reschedule_long_running_tasks_checker" + ).status + == 0 + ) + + yield + + assert ( + target_sat.execute( + f"FOREMAN_TASKS_CHECK_LONG_RUNNING_TASKS_CRONLINE='{default_cron_schedule}' " + "foreman-rake foreman_tasks:reschedule_long_running_tasks_checker" + ).status + == 0 + ) + + +@pytest.fixture +def start_postfix_service(target_sat): + """Start postfix service (disabled by default).""" + assert target_sat.execute('systemctl start postfix').status == 0 + + +@pytest.fixture +def clean_root_mailbox(target_sat): + """Backup & purge local mailbox of the Satellite's root@localhost user. + Restore it afterwards. + """ + root_mailbox = '/var/spool/mail/root' + root_mailbox_backup = f'{root_mailbox}-{gen_string("alphanumeric")}.bak' + target_sat.execute(f'cp -f {root_mailbox} {root_mailbox_backup}') + target_sat.execute(f'truncate -s 0 {root_mailbox}') + + yield root_mailbox + + target_sat.execute(f'mv -f {root_mailbox_backup} {root_mailbox}') + + +@pytest.fixture +def wait_for_long_running_task_mail(target_sat, clean_root_mailbox, long_running_task): + """Wait until the long-running task ID is found in the Satellite's mbox file.""" + timeout = 300 + try: + wait_for( + func=target_sat.execute, + func_args=[f'grep --quiet {long_running_task["task"]["id"]} {clean_root_mailbox}'], + fail_condition=lambda res: res.status == 0, + timeout=timeout, + delay=5, + ) + except TimedOutError: + raise AssertionError( + f'No notification e-mail with long-running task ID {long_running_task["task"]["id"]} ' + f'has arrived to {clean_root_mailbox} after {timeout} seconds.' + ) + return True + + +@pytest.fixture +def root_mailbox_copy(target_sat, clean_root_mailbox, wait_for_long_running_task_mail): + """Parsed local system copy of the Satellite's root user mailbox. + + :returns: :class:`mailbox.mbox` instance + """ + assert wait_for_long_running_task_mail + result = target_sat.execute(f'cat {clean_root_mailbox}') + assert result.status == 0, f'Could not read mailbox {clean_root_mailbox} on Satellite host.' + mbox_content = result.stdout + _, local_mbox_file = mkstemp() + with open(local_mbox_file, 'w') as fh: + fh.writelines(mbox_content) + return mbox(path=local_mbox_file) + + +@pytest.fixture +def long_running_task(target_sat): + """Create an async task and set its start time and last report time to two days ago. + After the test finishes, the task is cancelled. + """ + template_id = ( + target_sat.api.JobTemplate() + .search(query={'search': 'name="Run Command - Script Default"'})[0] + .id + ) + job = target_sat.api.JobInvocation().run( + synchronous=False, + data={ + 'job_template_id': template_id, + 'organization': DEFAULT_ORG, + 'location': DEFAULT_LOC, + 'inputs': { + 'command': 'sleep 300', + }, + 'targeting_type': 'static_query', + 'search_query': f'name = {target_sat.hostname}', + 'password': settings.server.ssh_password, + }, + ) + sql_date_2_days_ago = "now() - INTERVAL \'2 days\'" + result = target_sat.execute( + "su - postgres -c \"psql foreman postgres < Date: Fri, 4 Aug 2023 15:13:09 +0530 Subject: [PATCH 118/586] Component audit - Modify/Delete UI OS tests (#12046) Component Audit part2 UI OS tests --- tests/foreman/ui/test_operatingsystem.py | 148 ++++++++++------------- 1 file changed, 67 insertions(+), 81 deletions(-) diff --git a/tests/foreman/ui/test_operatingsystem.py b/tests/foreman/ui/test_operatingsystem.py index 7d7272a948b..3a172369115 100644 --- a/tests/foreman/ui/test_operatingsystem.py +++ b/tests/foreman/ui/test_operatingsystem.py @@ -17,49 +17,13 @@ :Upstream: No """ import pytest -from nailgun import entities -from robottelo.constants import DEFAULT_TEMPLATE from robottelo.constants import HASH_TYPE from robottelo.utils.datafactory import gen_string -@pytest.fixture(scope='module') -def module_org(): - return entities.Organization().create() - - -@pytest.mark.tier2 -def test_positive_update_with_params(session): - """Set Operating System parameter - - :id: 05b504d8-2518-4359-a53a-f577339f1ebe - - :expectedresults: OS is updated with new parameter - - :CaseLevel: Integration - """ - name = gen_string('alpha') - major_version = gen_string('numeric', 2) - param_name = gen_string('alpha') - param_value = gen_string('alpha') - with session: - session.operatingsystem.create( - {'operating_system.name': name, 'operating_system.major': major_version} - ) - os_full_name = f'{name} {major_version}' - assert session.operatingsystem.search(name)[0]['Title'] == os_full_name - session.operatingsystem.update( - os_full_name, {'parameters.os_params': {'name': param_name, 'value': param_value}} - ) - values = session.operatingsystem.read(os_full_name) - assert len(values['parameters']) == 1 - assert values['parameters']['os_params'][0]['name'] == param_name - assert values['parameters']['os_params'][0]['value'] == param_value - - @pytest.mark.tier2 -def test_positive_end_to_end(session): +def test_positive_end_to_end(session, module_org, module_location, target_sat): """Create all possible entities that required for operating system and then test all scenarios like create/read/update/delete for it @@ -77,18 +41,17 @@ def test_positive_end_to_end(session): description = gen_string('alpha') family = 'Red Hat' hash = HASH_TYPE['md5'] - architecture = entities.Architecture().create() - org = entities.Organization().create() - loc = entities.Location().create() - ptable = entities.PartitionTable( - organization=[org], location=[loc], os_family='Redhat' + architecture = target_sat.api.Architecture().create() + ptable = target_sat.api.PartitionTable( + organization=[module_org], location=[module_location], os_family='Redhat' ).create() - medium = entities.Media(organization=[org], location=[loc]).create() + medium = target_sat.api.Media(organization=[module_org], location=[module_location]).create() param_name = gen_string('alpha') param_value = gen_string('alpha') with session: - session.organization.select(org_name=org.name) - session.location.select(loc_name=loc.name) + session.organization.select(org_name=module_org.name) + session.location.select(loc_name=module_location.name) + # Create session.operatingsystem.create( { 'operating_system.name': name, @@ -113,54 +76,77 @@ def test_positive_end_to_end(session): assert os['operating_system']['password_hash'] == hash assert len(os['operating_system']['architectures']['assigned']) == 1 assert os['operating_system']['architectures']['assigned'][0] == architecture.name + assert os['templates']['resources']['Provisioning template'] == 'Kickstart default' assert ptable.name in os['partition_table']['resources']['assigned'] assert os['installation_media']['resources']['assigned'][0] == medium.name assert len(os['parameters']['os_params']) == 1 assert os['parameters']['os_params'][0]['name'] == param_name assert os['parameters']['os_params'][0]['value'] == param_value + template_name = gen_string('alpha') new_description = gen_string('alpha') + new_name = gen_string('alpha') + new_major_version = gen_string('numeric', 2) + new_minor_version = gen_string('numeric', 2) + new_family = 'Red Hat CoreOS' + new_hash = HASH_TYPE['sha512'] + new_architecture = target_sat.api.Architecture().create() + new_ptable = target_sat.api.PartitionTable( + organization=[module_org], location=[module_location], os_family='Redhat' + ).create() + new_medium = target_sat.api.Media( + organization=[module_org], location=[module_location] + ).create() + new_param_value = gen_string('alpha') + session.provisioningtemplate.create( + { + 'template.name': template_name, + 'template.default': True, + 'type.snippet': False, + 'template.template_editor.editor': gen_string('alpha'), + 'type.template_type': 'Provisioning template', + 'association.applicable_os.assigned': [os['operating_system']['description']], + 'organizations.resources.assigned': [module_org.name], + 'locations.resources.assigned': [module_location.name], + } + ) + # Update session.operatingsystem.update( - description, {'operating_system.description': new_description} + description, + { + 'operating_system.name': new_name, + 'templates.resources': {'Provisioning template': template_name}, + 'operating_system.description': new_description, + 'operating_system.major': new_major_version, + 'operating_system.minor': new_minor_version, + 'operating_system.family': new_family, + 'operating_system.password_hash': new_hash, + 'operating_system.architectures.assigned': [new_architecture.name], + 'partition_table.resources.assigned': [new_ptable.name], + 'installation_media.resources.assigned': [new_medium.name], + 'parameters.os_params': {'name': param_name, 'value': new_param_value}, + }, ) - assert not session.operatingsystem.search(description) - assert session.operatingsystem.search(new_description)[0]['Title'] == new_description - assert session.partitiontable.search(ptable.name)[0]['Operating Systems'] == new_description + os = session.operatingsystem.read(new_description) + assert os['operating_system']['name'] == new_name + assert os['operating_system']['major'] == new_major_version + assert os['operating_system']['minor'] == new_minor_version + assert os['operating_system']['description'] == new_description + assert os['operating_system']['family'] == new_family + assert os['operating_system']['password_hash'] == new_hash + assert new_architecture.name in os['operating_system']['architectures']['assigned'] + assert new_ptable.name in os['partition_table']['resources']['assigned'] + assert new_medium.name in os['installation_media']['resources']['assigned'] + assert os['templates']['resources']['Provisioning template'] == template_name + assert len(os['parameters']['os_params']) == 1 + assert os['parameters']['os_params'][0]['name'] == param_name + assert os['parameters']['os_params'][0]['value'] == new_param_value + # Delete session.operatingsystem.delete(new_description) assert not session.operatingsystem.search(new_description) @pytest.mark.tier2 -def test_positive_update_template(session, module_org): - """Update operating system with new provisioning template value - - :id: 0b90eb24-8fc9-4e42-8709-6eee8ffbbdb5 - - :expectedresults: OS is updated with new template - - :CaseLevel: Integration - """ - name = gen_string('alpha') - major_version = gen_string('numeric', 2) - os = entities.OperatingSystem(name=name, major=major_version, family='Redhat').create() - template = entities.ProvisioningTemplate( - organization=[module_org], - snippet=False, - template_kind=entities.TemplateKind().search(query={'search': 'name="provision"'})[0], - operatingsystem=[os], - ).create() - with session: - os_full_name = f'{name} {major_version}' - values = session.operatingsystem.read(os_full_name) - assert values['templates']['resources']['Provisioning template'] == DEFAULT_TEMPLATE - session.operatingsystem.update( - os_full_name, {'templates.resources': {'Provisioning template': template.name}} - ) - values = session.operatingsystem.read(os_full_name) - assert values['templates']['resources']['Provisioning template'] == template.name - - -@pytest.mark.tier2 -def test_positive_verify_os_name(session): +def test_positive_verify_os_name(session, target_sat): """Check that the Operating System name is displayed correctly :id: 2cb9cdcf-1723-4317-ab0a-971e7b2dd161 @@ -176,7 +162,7 @@ def test_positive_verify_os_name(session): name = gen_string('alpha') major_version = gen_string('numeric', 2) os_full_name = f"{name} {major_version}" - entities.OperatingSystem(name=name, major=major_version).create() + target_sat.api.OperatingSystem(name=name, major=major_version).create() with session: values = session.operatingsystem.search(os_full_name) assert values[0]['Title'] == os_full_name From 7592b2e4c0091efda9761a01355adb59e4018693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Gajdu=C5=A1ek?= Date: Fri, 4 Aug 2023 15:53:56 +0200 Subject: [PATCH 119/586] [6.14.z] Sync requirements with master branch (#12114) Sync requirements with master branch Sync with master commit 42a058ae13a80a461306a867412e37acae14197f --- requirements-optional.txt | 6 +++--- requirements.txt | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 0c3be0b6be7..fecd9ed8f91 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -1,7 +1,7 @@ # For running tests and checking code quality using these modules. -flake8==6.0.0 -pytest-cov==3.0.0 -redis==4.5.5 +flake8==6.1.0 +pytest-cov==4.1.0 +redis==4.6.0 pre-commit==3.3.3 # For generating documentation. diff --git a/requirements.txt b/requirements.txt index 24d5e33941d..4a99dd6ce50 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,16 @@ # Version updates managed by dependabot betelgeuse==1.10.0 -broker[docker]==0.3.2 -cryptography==41.0.0 -deepdiff==6.3.0 -dynaconf[vault]==3.1.12 +broker[docker]==0.3.3 +cryptography==41.0.3 +deepdiff==6.3.1 +dynaconf[vault]==3.2.0 fauxfactory==3.1.0 jinja2==3.1.2 manifester==0.0.13 navmazing==1.1.6 productmd==1.36 -pyotp==2.8.0 +pyotp==2.9.0 python-box==7.0.1 pytest==7.4.0 pytest-services==2.2.1 @@ -21,7 +21,7 @@ pytest-ibutsu==2.2.4 PyYAML==6.0.1 requests==2.31.0 tenacity==8.2.2 -testimony==2.2.0 +testimony==2.3.0 wait-for==1.2.0 wrapanapi==3.5.17 From edd37d1dfa57dd40d148e811baf55fc25fbff6b8 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 4 Aug 2023 11:12:17 -0400 Subject: [PATCH 120/586] [6.14.z] Improve doc build speed ~10 times (#12138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improve doc build speed ~10 times (#12131) (cherry picked from commit 4338e28b5307f8f019b9f85e0fbcdfcff6973b75) Co-authored-by: Ondřej Gajdušek --- docs/conf.py | 4 ++++ robottelo/config/__init__.py | 39 +++++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index c30e6c820b2..b143f7ac6fe 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,9 +4,13 @@ http://sphinx-doc.org/config.html """ +import builtins import os import sys +# Set the __sphinx_build__ variable to True. This is used to skip config generation +builtins.__sphinx_build__ = True + def skip_data(app, what, name, obj, skip, options): """Skip double generating docs for robottelo.utils.decorators.func_shared.shared""" diff --git a/robottelo/config/__init__.py b/robottelo/config/__init__.py index 197cc36f60f..0062ce17b6f 100644 --- a/robottelo/config/__init__.py +++ b/robottelo/config/__init__.py @@ -1,3 +1,4 @@ +import builtins import logging import os from pathlib import Path @@ -21,24 +22,30 @@ def get_settings(): :return: A validated Lazy settings object """ - settings = LazySettings( - envvar_prefix="ROBOTTELO", - core_loaders=["YAML"], - settings_file="settings.yaml", - preload=["conf/*.yaml"], - includes=["settings.local.yaml", ".secrets.yaml", ".secrets_*.yaml"], - envless_mode=True, - lowercase_read=True, - load_dotenv=True, - ) - settings.validators.register(**VALIDATORS) - try: - settings.validators.validate() - except ValidationError as err: - logger.warning(f'Dynaconf validation failed, continuing for the sake of unit tests\n{err}') + if builtins.__sphinx_build__: + settings = None + except AttributeError: + settings = LazySettings( + envvar_prefix="ROBOTTELO", + core_loaders=["YAML"], + settings_file="settings.yaml", + preload=["conf/*.yaml"], + includes=["settings.local.yaml", ".secrets.yaml", ".secrets_*.yaml"], + envless_mode=True, + lowercase_read=True, + load_dotenv=True, + ) + settings.validators.register(**VALIDATORS) + + try: + settings.validators.validate() + except ValidationError as err: + logger.warning( + f'Dynaconf validation failed, continuing for the sake of unit tests\n{err}' + ) - return settings + return settings settings = get_settings() From d070238e91b215875f81bc63b9de668cd0e12c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Gajdu=C5=A1ek?= Date: Fri, 4 Aug 2023 19:55:06 +0200 Subject: [PATCH 121/586] [6.14.z] Memory optimization for GCE tests (#12126) * Remove newline (cherry picked from commit 9f2f236745281601c6b25e853127a0b4e7f11023) * Memory optimization for GCE tests (cherry picked from commit 50a9dc839a9a0942067ab25676054173b1969c5c) * Remove duplicate constants --- pytest_fixtures/component/provision_gce.py | 38 ++++++++----------- robottelo/constants/__init__.py | 2 + tests/foreman/api/test_computeresource_gce.py | 5 +-- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/pytest_fixtures/component/provision_gce.py b/pytest_fixtures/component/provision_gce.py index 7006313ea51..fbdb2a886e9 100644 --- a/pytest_fixtures/component/provision_gce.py +++ b/pytest_fixtures/component/provision_gce.py @@ -10,6 +10,8 @@ from robottelo.constants import DEFAULT_ARCHITECTURE from robottelo.constants import DEFAULT_PTABLE from robottelo.constants import FOREMAN_PROVIDERS +from robottelo.constants import GCE_RHEL_CLOUD_PROJECTS +from robottelo.constants import GCE_TARGET_RHEL_IMAGE_NAME from robottelo.exceptions import GCECertNotFoundError @@ -86,16 +88,19 @@ def googleclient(gce_cert): @pytest.fixture(scope='session') def gce_latest_rhel_uuid(googleclient): - template_names = [img.name for img in googleclient.list_templates(True)] - latest_rhel7_template = max(name for name in template_names if name.startswith('rhel-7')) - latest_rhel7_uuid = googleclient.get_template(latest_rhel7_template, project='rhel-cloud').uuid - return latest_rhel7_uuid + templates = googleclient.find_templates( + include_public=True, + public_projects=GCE_RHEL_CLOUD_PROJECTS, + filter_expr=f'name:{GCE_TARGET_RHEL_IMAGE_NAME}*', + ) + latest_template_name = max(tpl.name for tpl in templates) + latest_template_uuid = next(tpl for tpl in templates if tpl.name == latest_template_name).uuid + return latest_template_uuid @pytest.fixture(scope='session') def gce_custom_cloudinit_uuid(googleclient, gce_cert): - cloudinit_uuid = googleclient.get_template('customcinit', project=gce_cert['project_id']).uuid - return cloudinit_uuid + return googleclient.get_template('customcinit', project=gce_cert['project_id']).uuid @pytest.fixture(scope='session') @@ -120,19 +125,6 @@ def module_gce_compute(sat_gce, sat_gce_org, sat_gce_loc, gce_cert): return gce_cr -@pytest.fixture(scope='module') -def gce_template(googleclient): - max_rhel7_template = max( - img.name for img in googleclient.list_templates(True) if str(img.name).startswith('rhel-7') - ) - return googleclient.get_template(max_rhel7_template, project='rhel-cloud').uuid - - -@pytest.fixture(scope='module') -def gce_cloudinit_template(googleclient, gce_cert): - return googleclient.get_template('customcinit', project=gce_cert['project_id']).uuid - - @pytest.fixture(scope='module') def gce_domain(sat_gce_org, sat_gce_loc, gce_cert, sat_gce): domain_name = f'{settings.gce.zone}.c.{gce_cert["project_id"]}.internal' @@ -151,8 +143,8 @@ def gce_domain(sat_gce_org, sat_gce_loc, gce_cert, sat_gce): @pytest.fixture(scope='module') def gce_resource_with_image( - gce_template, - gce_cloudinit_template, + gce_latest_rhel_uuid, + gce_custom_cloudinit_uuid, gce_cert, sat_gce_default_architecture, sat_gce_default_os, @@ -184,7 +176,7 @@ def gce_resource_with_image( name='autogce_img', operatingsystem=sat_gce_default_os, username=vm_user, - uuid=gce_template, + uuid=gce_latest_rhel_uuid, ).create() # Cloud-Init Image sat_gce.api.Image( @@ -193,7 +185,7 @@ def gce_resource_with_image( name='autogce_img_cinit', operatingsystem=sat_gce_default_os, username=vm_user, - uuid=gce_cloudinit_template, + uuid=gce_custom_cloudinit_uuid, user_data=True, ).create() return gce_cr diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index 2a804bca699..e0ab7024fdb 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -116,6 +116,8 @@ class Colored(Box): GCE_MACHINE_TYPE_DEFAULT = 'f1-micro' GCE_NETWORK_DEFAULT = 'default' GCE_EXTERNAL_IP_DEFAULT = True +GCE_RHEL_CLOUD_PROJECTS = ['rhel-cloud', 'rhel-sap-cloud'] +GCE_TARGET_RHEL_IMAGE_NAME = 'rhel-7' # AzureRM specific constants AZURERM_VALID_REGIONS = [ diff --git a/tests/foreman/api/test_computeresource_gce.py b/tests/foreman/api/test_computeresource_gce.py index bf904dc0560..eb841508176 100644 --- a/tests/foreman/api/test_computeresource_gce.py +++ b/tests/foreman/api/test_computeresource_gce.py @@ -25,10 +25,9 @@ from fauxfactory import gen_string from robottelo.config import settings +from robottelo.constants import GCE_RHEL_CLOUD_PROJECTS from robottelo.constants import VALID_GCE_ZONES -RHEL_CLOUD_PROJECTS = ['rhel-cloud', 'rhel-sap-cloud'] - @pytest.mark.skip_if_not_set('gce') class TestGCEComputeResourceTestCases: @@ -89,7 +88,7 @@ def test_positive_check_available_images(self, module_gce_compute, googleclient) """ satgce_images = module_gce_compute.available_images() googleclient_images = googleclient.list_templates( - include_public=True, public_projects=RHEL_CLOUD_PROJECTS + include_public=True, public_projects=GCE_RHEL_CLOUD_PROJECTS ) googleclient_image_names = [img.name for img in googleclient_images] # Validating GCE_CR images in Google CR From 1a2c7d40c06d88b7f100136586c2550e6eb901b1 Mon Sep 17 00:00:00 2001 From: Gaurav Talreja Date: Thu, 3 Aug 2023 16:07:06 +0530 Subject: [PATCH 122/586] Update scap tests to use deploy_flavor and Global Registration (#12098) Signed-off-by: Gaurav Talreja (cherry picked from commit eee703e516449c3bfab0386e749627b53838c780) --- robottelo/constants/__init__.py | 2 -- tests/foreman/longrun/test_oscap.py | 48 ++++------------------------- 2 files changed, 6 insertions(+), 44 deletions(-) diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index e0ab7024fdb..7c6537aaa8d 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -1539,8 +1539,6 @@ class Colored(Box): 'mail': 'mail', } -OSCAP_TARGET_CORES = 4 -OSCAP_TARGET_MEMORY = '16GiB' OSCAP_PERIOD = {'weekly': 'Weekly', 'monthly': 'Monthly', 'custom': 'Custom'} OSCAP_TAILORING_FILE = 'ssg-rhel7-ds-tailoring.xml' diff --git a/tests/foreman/longrun/test_oscap.py b/tests/foreman/longrun/test_oscap.py index d7cf3e505ca..9d4ea15e7ed 100644 --- a/tests/foreman/longrun/test_oscap.py +++ b/tests/foreman/longrun/test_oscap.py @@ -34,8 +34,6 @@ from robottelo.constants import OSCAP_DEFAULT_CONTENT from robottelo.constants import OSCAP_PERIOD from robottelo.constants import OSCAP_PROFILE -from robottelo.constants import OSCAP_TARGET_CORES -from robottelo.constants import OSCAP_TARGET_MEMORY from robottelo.constants import OSCAP_WEEKDAY from robottelo.exceptions import ProxyError from robottelo.hosts import ContentHost @@ -134,7 +132,6 @@ def update_scap_content(module_org, module_target_sat): ) -@pytest.mark.skip_if_open('BZ:2211437') @pytest.mark.e2e @pytest.mark.upgrade @pytest.mark.tier4 @@ -201,29 +198,13 @@ def test_positive_oscap_run_via_ansible( 'organizations': module_org.name, } ) - with Broker( - nick=distro, - host_class=ContentHost, - target_cores=OSCAP_TARGET_CORES, - target_memory=OSCAP_TARGET_MEMORY, - ) as vm: - host_name, _, host_domain = vm.hostname.partition('.') - vm.install_katello_ca(target_sat) - vm.register_contenthost(module_org.name, ak_name[distro]) - assert vm.subscribed - Host.set_parameter( - { - 'host': vm.hostname.lower(), - 'name': 'remote_execution_connect_by_ip', - 'value': 'True', - 'parameter-type': 'boolean', - } - ) + with Broker(nick=distro, host_class=ContentHost, deploy_flavor=settings.flavors.default) as vm: + result = vm.register(module_org, None, ak_name[distro], target_sat) + assert result.status == 0, f'Failed to register host: {result.stderr}' if distro not in ('rhel7'): vm.create_custom_repos(**rhel_repo) else: vm.create_custom_repos(**{distro: rhel_repo}) - vm.add_rex_key(satellite=target_sat) Host.update( { 'name': vm.hostname.lower(), @@ -257,7 +238,6 @@ def test_positive_oscap_run_via_ansible( assert result is not None -@pytest.mark.skip_if_open('BZ:2211437') @pytest.mark.tier4 def test_positive_oscap_run_via_ansible_bz_1814988( module_org, default_proxy, content_view, lifecycle_env, target_sat @@ -317,24 +297,9 @@ def test_positive_oscap_run_via_ansible_bz_1814988( 'organizations': module_org.name, } ) - with Broker( - nick='rhel7', - host_class=ContentHost, - target_cores=OSCAP_TARGET_CORES, - target_memory=OSCAP_TARGET_MEMORY, - ) as vm: - host_name, _, host_domain = vm.hostname.partition('.') - vm.install_katello_ca(target_sat) - vm.register_contenthost(module_org.name, ak_name['rhel7']) - assert vm.subscribed - Host.set_parameter( - { - 'host': vm.hostname.lower(), - 'name': 'remote_execution_connect_by_ip', - 'value': 'True', - 'parameter-type': 'boolean', - } - ) + with Broker(nick='rhel7', host_class=ContentHost, deploy_flavor=settings.flavors.default) as vm: + result = vm.register(module_org, None, ak_name['rhel7'], target_sat) + assert result.status == 0, f'Failed to register host: {result.stderr}' vm.create_custom_repos(rhel7=settings.repos.rhel7_os) # Harden the rhel7 client with DISA STIG security policy vm.run('yum install -y scap-security-guide') @@ -343,7 +308,6 @@ def test_positive_oscap_run_via_ansible_bz_1814988( '--fetch-remote-resources --results-arf results.xml ' '/usr/share/xml/scap/ssg/content/ssg-rhel7-ds.xml', ) - vm.add_rex_key(satellite=target_sat) Host.update( { 'name': vm.hostname.lower(), From a18400fbfa5f1ef38e1ec287b36c58c10a479a32 Mon Sep 17 00:00:00 2001 From: Gaurav Talreja Date: Thu, 3 Aug 2023 21:46:06 +0530 Subject: [PATCH 123/586] Update download_url for scap content to point to DS v2 Signed-off-by: Gaurav Talreja --- tests/foreman/longrun/test_oscap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/longrun/test_oscap.py b/tests/foreman/longrun/test_oscap.py index 9d4ea15e7ed..496d6a6c6e4 100644 --- a/tests/foreman/longrun/test_oscap.py +++ b/tests/foreman/longrun/test_oscap.py @@ -492,7 +492,7 @@ def test_positive_oscap_run_via_local_files( """ SELECTED_ROLE = 'theforeman.foreman_scap_client' file_name = 'security-data-oval-com.redhat.rhsa-RHEL8.xml.bz2' - download_url = 'https://www.redhat.com/security/data/oval/com.redhat.rhsa-RHEL8.xml.bz2' + download_url = 'https://www.redhat.com/security/data/oval/v2/RHEL8/rhel-8.oval.xml.bz2' profile = OSCAP_PROFILE['ospp8'] content = OSCAP_DEFAULT_CONTENT[f'{distro}_content'] hgrp_name = gen_string('alpha') From 2badfa77004ecef01114b1c6b8ceceadd102e5e4 Mon Sep 17 00:00:00 2001 From: David Moore <109112035+damoore044@users.noreply.github.com> Date: Mon, 7 Aug 2023 10:58:11 -0400 Subject: [PATCH 124/586] [6.14.z] cherrypick of ContentCredentials Updates (#12140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Gajdušek Fix TestRepository test cases (#12088) --- tests/foreman/cli/test_contentcredentials.py | 523 +++++++------------ tests/foreman/ui/test_contentcredentials.py | 391 +++----------- 2 files changed, 273 insertions(+), 641 deletions(-) diff --git a/tests/foreman/cli/test_contentcredentials.py b/tests/foreman/cli/test_contentcredentials.py index 750ec746bbf..935d41e3e90 100644 --- a/tests/foreman/cli/test_contentcredentials.py +++ b/tests/foreman/cli/test_contentcredentials.py @@ -27,16 +27,8 @@ from fauxfactory import gen_string from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.content_credentials import ContentCredential -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_content_credential -from robottelo.cli.factory import make_product -from robottelo.cli.factory import make_repository -from robottelo.cli.org import Org -from robottelo.cli.product import Product -from robottelo.cli.repository import Repository from robottelo.constants import DataFile -from robottelo.constants import DEFAULT_ORG +from robottelo.host_helpers.cli_factory import CLIFactoryError from robottelo.utils.datafactory import invalid_values_list from robottelo.utils.datafactory import parametrized from robottelo.utils.datafactory import valid_data_list @@ -63,7 +55,7 @@ def create_gpg_key_file(content=None): @pytest.mark.tier1 -def test_verify_gpg_key_content_displayed(module_org): +def test_verify_gpg_key_content_displayed(target_sat, module_org): """content-credential info should display key content :id: 0ee87ee0-8bf1-4d15-b5f9-0ac364e61155 @@ -76,14 +68,14 @@ def test_verify_gpg_key_content_displayed(module_org): content = gen_alphanumeric() key_path = create_gpg_key_file(content=content) assert key_path, 'GPG Key file must be created' - gpg_key = make_content_credential( + gpg_key = target_sat.cli_factory.make_content_credential( {'path': key_path, 'name': gen_string('alpha'), 'organization-id': module_org.id} ) assert gpg_key['content'] == content @pytest.mark.tier1 -def test_positive_get_info_by_name(module_org): +def test_positive_get_info_by_name(target_sat, module_org): """Create single gpg key and get its info by name :id: 890456ea-0b31-4386-9231-f47572f26d08 @@ -94,15 +86,17 @@ def test_positive_get_info_by_name(module_org): :CaseImportance: Critical """ name = gen_string('utf8') - gpg_key = make_content_credential( - {'key': VALID_GPG_KEY_FILE_PATH, 'name': name, 'organization-id': module_org.id} + gpg_key = target_sat.cli_factory.make_content_credential( + {'path': VALID_GPG_KEY_FILE_PATH, 'name': name, 'organization-id': module_org.id} + ) + gpg_key = target_sat.cli.ContentCredential.info( + {'name': gpg_key['name'], 'organization-id': module_org.id} ) - gpg_key = ContentCredential.info({'name': gpg_key['name'], 'organization-id': module_org.id}) assert gpg_key['name'] == name @pytest.mark.tier1 -def test_positive_block_delete_key_in_use(module_org, target_sat): +def test_positive_block_delete_key_in_use(target_sat, module_org): """Create a product and single associated repository. Create a new gpg key and associate it with the product and repository. Attempt to delete the gpg key in use @@ -118,10 +112,12 @@ def test_positive_block_delete_key_in_use(module_org, target_sat): :CaseImportance: Critical """ name = gen_string('utf8') - product = make_product({'organization-id': module_org.id}) - repo = make_repository({'product-id': product['id']}) - gpg_key = make_content_credential( - {'key': VALID_GPG_KEY_FILE_PATH, 'name': name, 'organization-id': module_org.id} + product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) + repo = target_sat.cli_factory.make_repository( + {'product-id': product['id'], 'content-type': 'yum'} + ) + gpg_key = target_sat.cli_factory.make_content_credential( + {'path': VALID_GPG_KEY_FILE_PATH, 'name': name, 'organization-id': module_org.id} ) # Associate repo with the product, gpg key with product and repo @@ -149,7 +145,7 @@ def test_positive_block_delete_key_in_use(module_org, target_sat): @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_create_with_default_org(name, module_org, default_org): +def test_positive_create_with_default_org(target_sat, name, default_org): """Create gpg key with valid name and valid gpg key via file import using the default created organization @@ -161,20 +157,21 @@ def test_positive_create_with_default_org(name, module_org, default_org): :CaseImportance: Critical """ - org = Org.info({'name': DEFAULT_ORG}) - gpg_key = make_content_credential( - {'key': VALID_GPG_KEY_FILE_PATH, 'name': name, 'organization-id': org['id']} + + gpg_key = target_sat.cli_factory.make_content_credential( + {'path': VALID_GPG_KEY_FILE_PATH, 'name': name, 'organization-id': default_org.id} ) # Can we find the new object? - result = ContentCredential.exists( - {'organization-id': org['id']}, (search_key, gpg_key[search_key]) + result = target_sat.cli.ContentCredential.exists( + {'organization-id': default_org.id}, (search_key, gpg_key[search_key]) ) + target_sat.cli.ContentCredential.delete({'id': gpg_key.id}) assert gpg_key[search_key] == result[search_key] @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_create_with_custom_org(name, module_org): +def test_positive_create_with_custom_org(target_sat, name, module_org): """Create gpg key with valid name and valid gpg key via file import using a new organization @@ -186,15 +183,15 @@ def test_positive_create_with_custom_org(name, module_org): :CaseImportance: Critical """ - gpg_key = make_content_credential( + gpg_key = target_sat.cli_factory.make_content_credential( { - 'key': VALID_GPG_KEY_FILE_PATH, + 'path': VALID_GPG_KEY_FILE_PATH, 'name': name, 'organization-id': module_org.id, } ) # Can we find the new object? - result = ContentCredential.exists( + result = target_sat.cli.ContentCredential.exists( {'organization-id': module_org.id}, (search_key, gpg_key[search_key]), ) @@ -202,7 +199,7 @@ def test_positive_create_with_custom_org(name, module_org): @pytest.mark.tier1 -def test_negative_create_with_same_name(module_org): +def test_negative_create_with_same_name(target_sat, module_org): """Create gpg key with valid name and valid gpg key via file import then try to create new one with same name @@ -213,19 +210,23 @@ def test_negative_create_with_same_name(module_org): :CaseImportance: Critical """ name = gen_string('alphanumeric') - gpg_key = make_content_credential({'name': name, 'organization-id': module_org.id}) + gpg_key = target_sat.cli_factory.make_content_credential( + {'name': name, 'organization-id': module_org.id} + ) # Can we find the new object? - result = ContentCredential.exists( + result = target_sat.cli.ContentCredential.exists( {'organization-id': module_org.id}, (search_key, gpg_key[search_key]) ) assert gpg_key[search_key] == result[search_key] with pytest.raises(CLIFactoryError): - make_content_credential({'name': name, 'organization-id': module_org.id}) + target_sat.cli_factory.make_content_credential( + {'name': name, 'organization-id': module_org.id} + ) @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_negative_create_with_no_gpg_key(name, module_org): +def test_negative_create_with_no_gpg_key(name, target_sat, module_org): """Create gpg key with valid name and no gpg key :id: bbfd5306-cfe7-40c1-a3a2-35834108163c @@ -237,12 +238,12 @@ def test_negative_create_with_no_gpg_key(name, module_org): :CaseImportance: Critical """ with pytest.raises(CLIReturnCodeError): - ContentCredential.create({'name': name, 'organization-id': module_org.id}) + target_sat.cli.ContentCredential.create({'name': name, 'organization-id': module_org.id}) @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) @pytest.mark.tier1 -def test_negative_create_with_invalid_name(name, module_org): +def test_negative_create_with_invalid_name(target_sat, name, module_org): """Create gpg key with invalid name and valid gpg key via file import @@ -256,13 +257,15 @@ def test_negative_create_with_invalid_name(name, module_org): """ with pytest.raises(CLIFactoryError): # factory will provide a valid key - make_content_credential({'name': name, 'organization-id': module_org.id}) + target_sat.cli_factory.make_content_credential( + {'name': name, 'organization-id': module_org.id} + ) @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_delete(name, module_org): +def test_positive_delete(target_sat, name, module_org): """Create gpg key with valid name and valid gpg key via file import then delete it @@ -274,14 +277,16 @@ def test_positive_delete(name, module_org): :CaseImportance: Critical """ - gpg_key = make_content_credential({'name': name, 'organization-id': module_org.id}) - result = ContentCredential.exists( + gpg_key = target_sat.cli_factory.make_content_credential( + {'name': name, 'organization-id': module_org.id} + ) + result = target_sat.cli.ContentCredential.exists( {'organization-id': module_org.id}, (search_key, gpg_key[search_key]), ) assert gpg_key[search_key] == result[search_key] - ContentCredential.delete({'name': name, 'organization-id': module_org.id}) - result = ContentCredential.exists( + target_sat.cli.ContentCredential.delete({'name': name, 'organization-id': module_org.id}) + result = target_sat.cli.ContentCredential.exists( {'organization-id': module_org.id}, (search_key, gpg_key[search_key]), ) @@ -290,7 +295,7 @@ def test_positive_delete(name, module_org): @pytest.mark.parametrize('new_name', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_update_name(new_name, module_org): +def test_positive_update_name(target_sat, new_name, module_org): """Create gpg key with valid name and valid gpg key via file import then update its name @@ -302,15 +307,17 @@ def test_positive_update_name(new_name, module_org): :CaseImportance: Critical """ - gpg_key = make_content_credential({'organization-id': module_org.id}) - ContentCredential.update( + gpg_key = target_sat.cli_factory.make_content_credential({'organization-id': module_org.id}) + target_sat.cli.ContentCredential.update( { 'name': gpg_key['name'], 'new-name': new_name, 'organization-id': module_org.id, } ) - gpg_key = ContentCredential.info({'name': new_name, 'organization-id': module_org.id}) + gpg_key = target_sat.cli.ContentCredential.info( + {'name': new_name, 'organization-id': module_org.id} + ) @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @@ -327,23 +334,25 @@ def test_positive_update_key(name, module_org, target_sat): :CaseImportance: Critical """ - gpg_key = make_content_credential({'organization-id': module_org.id}) + gpg_key = target_sat.cli_factory.make_content_credential({'organization-id': module_org.id}) content = gen_alphanumeric(gen_integer(20, 50)) assert gpg_key['content'] != content local_key = create_gpg_key_file(content) assert gpg_key, 'GPG Key file must be created' key = f'/tmp/{gen_alphanumeric()}' target_sat.put(local_key, key) - ContentCredential.update( + target_sat.cli.ContentCredential.update( {'path': key, 'name': gpg_key['name'], 'organization-id': module_org.id} ) - gpg_key = ContentCredential.info({'name': gpg_key['name'], 'organization-id': module_org.id}) + gpg_key = target_sat.cli.ContentCredential.info( + {'name': gpg_key['name'], 'organization-id': module_org.id} + ) assert gpg_key['content'] == content @pytest.mark.parametrize('new_name', **parametrized(invalid_values_list())) @pytest.mark.tier1 -def test_negative_update_name(new_name, module_org): +def test_negative_update_name(target_sat, new_name, module_org): """Create gpg key with valid name and valid gpg key via file import then fail to update its name @@ -355,9 +364,9 @@ def test_negative_update_name(new_name, module_org): :CaseImportance: Critical """ - gpg_key = make_content_credential({'organization-id': module_org.id}) + gpg_key = target_sat.cli_factory.make_content_credential({'organization-id': module_org.id}) with pytest.raises(CLIReturnCodeError): - ContentCredential.update( + target_sat.cli.ContentCredential.update( { 'name': gpg_key['name'], 'new-name': new_name, @@ -367,7 +376,7 @@ def test_negative_update_name(new_name, module_org): @pytest.mark.tier2 -def test_positive_add_empty_product(module_org): +def test_positive_add_empty_product(target_sat, module_org): """Create gpg key with valid name and valid gpg key via file import then associate it with empty (no repos) custom product @@ -377,13 +386,15 @@ def test_positive_add_empty_product(module_org): :CaseLevel: Integration """ - gpg_key = make_content_credential({'organization-id': module_org.id}) - product = make_product({'gpg-key-id': gpg_key['id'], 'organization-id': module_org.id}) + gpg_key = target_sat.cli_factory.make_content_credential({'organization-id': module_org.id}) + product = target_sat.cli_factory.make_product( + {'gpg-key-id': gpg_key['id'], 'organization-id': module_org.id} + ) assert product['gpg']['gpg-key'] == gpg_key['name'] @pytest.mark.tier2 -def test_positive_add_product_with_repo(module_org): +def test_positive_add_product_with_repo(target_sat, module_org): """Create gpg key with valid name and valid gpg key via file import then associate it with custom product that has one repository @@ -394,20 +405,22 @@ def test_positive_add_product_with_repo(module_org): :CaseLevel: Integration """ - product = make_product({'organization-id': module_org.id}) - repo = make_repository({'product-id': product['id']}) - gpg_key = make_content_credential({'organization-id': module_org.id}) - Product.update( + product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) + repo = target_sat.cli_factory.make_repository( + {'product-id': product['id'], 'content-type': 'yum'} + ) + gpg_key = target_sat.cli_factory.make_content_credential({'organization-id': module_org.id}) + target_sat.cli.Product.update( {'gpg-key-id': gpg_key['id'], 'id': product['id'], 'organization-id': module_org.id} ) - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) - repo = Repository.info({'id': repo['id']}) + product = target_sat.cli.Product.info({'id': product['id'], 'organization-id': module_org.id}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert product['gpg']['gpg-key-id'] == gpg_key['id'] assert repo['gpg-key']['id'] == gpg_key['id'] @pytest.mark.tier2 -def test_positive_add_product_with_repos(module_org): +def test_positive_add_product_with_repos(target_sat, module_org): """Create gpg key with valid name and valid gpg key via file import then associate it with custom product that has more than one repository @@ -419,21 +432,24 @@ def test_positive_add_product_with_repos(module_org): :CaseLevel: Integration """ - product = make_product({'organization-id': module_org.id}) - repos = [make_repository({'product-id': product['id']}) for _ in range(gen_integer(2, 5))] - gpg_key = make_content_credential({'organization-id': module_org.id}) - Product.update( + product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) + repos = [ + target_sat.cli_factory.make_repository({'product-id': product['id'], 'content-type': 'yum'}) + for _ in range(gen_integer(2, 5)) + ] + gpg_key = target_sat.cli_factory.make_content_credential({'organization-id': module_org.id}) + target_sat.cli.Product.update( {'gpg-key-id': gpg_key['id'], 'id': product['id'], 'organization-id': module_org.id} ) - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) + product = target_sat.cli.Product.info({'id': product['id'], 'organization-id': module_org.id}) assert product['gpg']['gpg-key-id'] == gpg_key['id'] for repo in repos: - repo = Repository.info({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert repo['gpg-key']['id'] == gpg_key['id'] @pytest.mark.tier2 -def test_positive_add_repo_from_product_with_repo(module_org): +def test_positive_add_repo_from_product_with_repo(target_sat, module_org): """Create gpg key with valid name and valid gpg key via file import then associate it to repository from custom product that has one repository @@ -445,20 +461,22 @@ def test_positive_add_repo_from_product_with_repo(module_org): :CaseLevel: Integration """ - product = make_product({'organization-id': module_org.id}) - repo = make_repository({'product-id': product['id']}) - gpg_key = make_content_credential({'organization-id': module_org.id}) - Repository.update( + product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) + repo = target_sat.cli_factory.make_repository( + {'product-id': product['id'], 'content-type': 'yum'} + ) + gpg_key = target_sat.cli_factory.make_content_credential({'organization-id': module_org.id}) + target_sat.cli.Repository.update( {'gpg-key-id': gpg_key['id'], 'id': repo['id'], 'organization-id': module_org.id} ) - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) - repo = Repository.info({'id': repo['id']}) + product = target_sat.cli.Product.info({'id': product['id'], 'organization-id': module_org.id}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert repo['gpg-key']['id'] == gpg_key['id'] assert product['gpg'].get('gpg-key-id') != gpg_key['id'] @pytest.mark.tier2 -def test_positive_add_repo_from_product_with_repos(module_org): +def test_positive_add_repo_from_product_with_repos(target_sat, module_org): """Create gpg key via file import and associate with custom repo GPGKey should contain valid name and valid key and should be associated @@ -471,25 +489,28 @@ def test_positive_add_repo_from_product_with_repos(module_org): :CaseLevel: Integration """ - product = make_product({'organization-id': module_org.id}) - repos = [make_repository({'product-id': product['id']}) for _ in range(gen_integer(2, 5))] - gpg_key = make_content_credential({'organization-id': module_org.id}) - Repository.update( + product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) + repos = [ + target_sat.cli_factory.make_repository({'product-id': product['id'], 'content-type': 'yum'}) + for _ in range(gen_integer(2, 5)) + ] + gpg_key = target_sat.cli_factory.make_content_credential({'organization-id': module_org.id}) + target_sat.cli.Repository.update( {'gpg-key-id': gpg_key['id'], 'id': repos[0]['id'], 'organization-id': module_org.id} ) - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) + product = target_sat.cli.Product.info({'id': product['id'], 'organization-id': module_org.id}) assert product['gpg'].get('gpg-key-id') != gpg_key['id'] # First repo should have a valid gpg key assigned - repo = Repository.info({'id': repos.pop(0)['id']}) + repo = target_sat.cli.Repository.info({'id': repos.pop(0)['id']}) assert repo['gpg-key']['id'] == gpg_key['id'] # The rest of repos should not for repo in repos: - repo = Repository.info({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert repo['gpg-key'].get('id') != gpg_key['id'] @pytest.mark.tier2 -def test_positive_update_key_for_empty_product(module_org): +def test_positive_update_key_for_empty_product(target_sat, module_org): """Create gpg key with valid name and valid gpg key via file import then associate it with empty (no repos) custom product then update the key @@ -502,30 +523,32 @@ def test_positive_update_key_for_empty_product(module_org): :CaseLevel: Integration """ # Create a product and a gpg key - product = make_product({'organization-id': module_org.id}) - gpg_key = make_content_credential({'organization-id': module_org.id}) + product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) + gpg_key = target_sat.cli_factory.make_content_credential({'organization-id': module_org.id}) # Associate gpg key with a product - Product.update( + target_sat.cli.Product.update( {'gpg-key-id': gpg_key['id'], 'id': product['id'], 'organization-id': module_org.id} ) # Verify gpg key was associated - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) + product = target_sat.cli.Product.info({'id': product['id'], 'organization-id': module_org.id}) assert product['gpg']['gpg-key'] == gpg_key['name'] # Update the gpg key new_name = gen_choice(list(valid_data_list().values())) - ContentCredential.update( + target_sat.cli.ContentCredential.update( {'name': gpg_key['name'], 'new-name': new_name, 'organization-id': module_org.id} ) # Verify changes are reflected in the gpg key - gpg_key = ContentCredential.info({'id': gpg_key['id'], 'organization-id': module_org.id}) + gpg_key = target_sat.cli.ContentCredential.info( + {'id': gpg_key['id'], 'organization-id': module_org.id} + ) assert gpg_key['name'] == new_name # Verify changes are reflected in the product - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) + product = target_sat.cli.Product.info({'id': product['id'], 'organization-id': module_org.id}) assert product['gpg']['gpg-key'] == new_name @pytest.mark.tier2 -def test_positive_update_key_for_product_with_repo(module_org): +def test_positive_update_key_for_product_with_repo(target_sat, module_org): """Create gpg key with valid name and valid gpg key via file import then associate it with custom product that has one repository then update the key @@ -538,37 +561,41 @@ def test_positive_update_key_for_product_with_repo(module_org): :CaseLevel: Integration """ # Create a product and a gpg key - product = make_product({'organization-id': module_org.id}) - gpg_key = make_content_credential({'organization-id': module_org.id}) + product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) + gpg_key = target_sat.cli_factory.make_content_credential({'organization-id': module_org.id}) # Create a repository and assign it to the product - repo = make_repository({'product-id': product['id']}) + repo = target_sat.cli_factory.make_repository( + {'product-id': product['id'], 'content-type': 'yum'} + ) # Associate gpg key with a product - Product.update( + target_sat.cli.Product.update( {'gpg-key-id': gpg_key['id'], 'id': product['id'], 'organization-id': module_org.id} ) # Verify gpg key was associated - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) - repo = Repository.info({'id': repo['id']}) + product = target_sat.cli.Product.info({'id': product['id'], 'organization-id': module_org.id}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert product['gpg']['gpg-key'] == gpg_key['name'] assert repo['gpg-key'].get('name') == gpg_key['name'] # Update the gpg key new_name = gen_choice(list(valid_data_list().values())) - ContentCredential.update( + target_sat.cli.ContentCredential.update( {'name': gpg_key['name'], 'new-name': new_name, 'organization-id': module_org.id} ) # Verify changes are reflected in the gpg key - gpg_key = ContentCredential.info({'id': gpg_key['id'], 'organization-id': module_org.id}) + gpg_key = target_sat.cli.ContentCredential.info( + {'id': gpg_key['id'], 'organization-id': module_org.id} + ) assert gpg_key['name'] == new_name # Verify changes are reflected in the product - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) + product = target_sat.cli.Product.info({'id': product['id'], 'organization-id': module_org.id}) assert product['gpg']['gpg-key'] == new_name # Verify changes are reflected in the repository - repo = Repository.info({'id': repo['id']}) - assert repo['gpg-key'].get('id') == gpg_key['id'] + repo = target_sat.cli.Repository.info({'id': repo['id']}) + assert repo['gpg-key'].get('name') == new_name @pytest.mark.tier2 -def test_positive_update_key_for_product_with_repos(module_org): +def test_positive_update_key_for_product_with_repos(target_sat, module_org): """Create gpg key with valid name and valid gpg key via file import then associate it with custom product that has more than one repository then update the key @@ -581,39 +608,44 @@ def test_positive_update_key_for_product_with_repos(module_org): :CaseLevel: Integration """ # Create a product and a gpg key - product = make_product({'organization-id': module_org.id}) - gpg_key = make_content_credential({'organization-id': module_org.id}) + product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) + gpg_key = target_sat.cli_factory.make_content_credential({'organization-id': module_org.id}) # Create repositories and assign them to the product - repos = [make_repository({'product-id': product['id']}) for _ in range(gen_integer(2, 5))] + repos = [ + target_sat.cli_factory.make_repository({'product-id': product['id'], 'content-type': 'yum'}) + for _ in range(gen_integer(2, 5)) + ] # Associate gpg key with a product - Product.update( + target_sat.cli.Product.update( {'gpg-key-id': gpg_key['id'], 'id': product['id'], 'organization-id': module_org.id} ) # Verify gpg key was associated - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) + product = target_sat.cli.Product.info({'id': product['id'], 'organization-id': module_org.id}) assert product['gpg']['gpg-key'] == gpg_key['name'] for repo in repos: - repo = Repository.info({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert repo['gpg-key'].get('name') == gpg_key['name'] # Update the gpg key new_name = gen_choice(list(valid_data_list().values())) - ContentCredential.update( + target_sat.cli.ContentCredential.update( {'name': gpg_key['name'], 'new-name': new_name, 'organization-id': module_org.id} ) # Verify changes are reflected in the gpg key - gpg_key = ContentCredential.info({'id': gpg_key['id'], 'organization-id': module_org.id}) + gpg_key = target_sat.cli.ContentCredential.info( + {'id': gpg_key['id'], 'organization-id': module_org.id} + ) assert gpg_key['name'] == new_name # Verify changes are reflected in the product - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) + product = target_sat.cli.Product.info({'id': product['id'], 'organization-id': module_org.id}) assert product['gpg']['gpg-key'] == new_name # Verify changes are reflected in the repositories for repo in repos: - repo = Repository.info({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert repo['gpg-key'].get('name') == new_name @pytest.mark.tier2 -def test_positive_update_key_for_repo_from_product_with_repo(module_org): +def test_positive_update_key_for_repo_from_product_with_repo(target_sat, module_org): """Create gpg key with valid name and valid gpg key via file import then associate it to repository from custom product that has one repository then update the key @@ -626,30 +658,34 @@ def test_positive_update_key_for_repo_from_product_with_repo(module_org): :CaseLevel: Integration """ # Create a product and a gpg key - product = make_product({'organization-id': module_org.id}) - gpg_key = make_content_credential({'organization-id': module_org.id}) + product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) + gpg_key = target_sat.cli_factory.make_content_credential({'organization-id': module_org.id}) # Create repository, assign product and gpg-key - repo = make_repository({'gpg-key-id': gpg_key['id'], 'product-id': product['id']}) + repo = target_sat.cli_factory.make_repository( + {'gpg-key-id': gpg_key['id'], 'product-id': product['id'], 'content-type': 'yum'} + ) # Verify gpg key was associated assert repo['gpg-key'].get('name') == gpg_key['name'] # Update the gpg key new_name = gen_choice(list(valid_data_list().values())) - ContentCredential.update( + target_sat.cli.ContentCredential.update( {'name': gpg_key['name'], 'new-name': new_name, 'organization-id': module_org.id} ) # Verify changes are reflected in the gpg key - gpg_key = ContentCredential.info({'id': gpg_key['id'], 'organization-id': module_org.id}) + gpg_key = target_sat.cli.ContentCredential.info( + {'id': gpg_key['id'], 'organization-id': module_org.id} + ) assert gpg_key['name'] == new_name # Verify changes are reflected in the repositories - repo = Repository.info({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert repo['gpg-key'].get('name') == new_name # Verify gpg key wasn't added to the product - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) + product = target_sat.cli.Product.info({'id': product['id'], 'organization-id': module_org.id}) assert product['gpg']['gpg-key'] != new_name @pytest.mark.tier2 -def test_positive_update_key_for_repo_from_product_with_repos(module_org): +def test_positive_update_key_for_repo_from_product_with_repos(target_sat, module_org): """Create gpg key with valid name and valid gpg key via file import then associate it to repository from custom product that has more than one repository then update the key @@ -663,228 +699,44 @@ def test_positive_update_key_for_repo_from_product_with_repos(module_org): :CaseLevel: Integration """ # Create a product and a gpg key - product = make_product({'organization-id': module_org.id}) - gpg_key = make_content_credential({'organization-id': module_org.id}) + product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) + gpg_key = target_sat.cli_factory.make_content_credential({'organization-id': module_org.id}) # Create repositories and assign them to the product - repos = [make_repository({'product-id': product['id']}) for _ in range(gen_integer(2, 5))] + repos = [ + target_sat.cli_factory.make_repository({'product-id': product['id'], 'content-type': 'yum'}) + for _ in range(gen_integer(2, 5)) + ] # Associate gpg key with a single repository - Repository.update( + target_sat.cli.Repository.update( {'gpg-key-id': gpg_key['id'], 'id': repos[0]['id'], 'organization-id': module_org.id} ) # Verify gpg key was associated - repos[0] = Repository.info({'id': repos[0]['id']}) + repos[0] = target_sat.cli.Repository.info({'id': repos[0]['id']}) assert repos[0]['gpg-key']['name'] == gpg_key['name'] # Update the gpg key new_name = gen_choice(list(valid_data_list().values())) - ContentCredential.update( + target_sat.cli.ContentCredential.update( {'name': gpg_key['name'], 'new-name': new_name, 'organization-id': module_org.id} ) # Verify changes are reflected in the gpg key - gpg_key = ContentCredential.info({'id': gpg_key['id'], 'organization-id': module_org.id}) + gpg_key = target_sat.cli.ContentCredential.info( + {'id': gpg_key['id'], 'organization-id': module_org.id} + ) assert gpg_key['name'] == new_name # Verify changes are reflected in the associated repository - repos[0] = Repository.info({'id': repos[0]['id']}) + repos[0] = target_sat.cli.Repository.info({'id': repos[0]['id']}) assert repos[0]['gpg-key'].get('name') == new_name # Verify changes are not reflected in the product - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) + product = target_sat.cli.Product.info({'id': product['id'], 'organization-id': module_org.id}) assert product['gpg']['gpg-key'] != new_name # Verify changes are not reflected in the rest of repositories for repo in repos[1:]: - repo = Repository.info({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert repo['gpg-key'].get('name') != new_name -@pytest.mark.tier2 -def test_positive_delete_key_for_empty_product(module_org): - """Create gpg key with valid name and valid gpg key via file - import then associate it with empty (no repos) custom product - then delete it - - :id: 238a80f8-983a-4fd5-a168-6ef9442e2b1c - - :expectedresults: gpg key is associated with product during creation - but removed from product after deletion - - :CaseLevel: Integration - """ - # Create a product and a gpg key - gpg_key = make_content_credential({'organization-id': module_org.id}) - product = make_product({'gpg-key-id': gpg_key['id'], 'organization-id': module_org.id}) - # Verify gpg key was associated - assert product['gpg']['gpg-key'] == gpg_key['name'] - # Delete the gpg key - ContentCredential.delete({'name': gpg_key['name'], 'organization-id': module_org.id}) - # Verify gpg key was actually deleted - with pytest.raises(CLIReturnCodeError): - ContentCredential.info({'id': gpg_key['id'], 'organization-id': module_org.id}) - # Verify gpg key was disassociated from the product - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) - assert product['gpg']['gpg-key'] != gpg_key['name'] - - -@pytest.mark.tier2 -@pytest.mark.upgrade -def test_positive_delete_key_for_product_with_repo(module_org): - """Create gpg key with valid name and valid gpg key via file - import then associate it with custom product that has one repository - then delete it - - :id: 1e98e588-8b5d-475c-ad84-5d566df5619c - - :expectedresults: gpg key is associated with product but and its - repository during creation but removed from product and repository - after deletion - - :CaseLevel: Integration - """ - # Create product, repository and gpg key - product = make_product({'organization-id': module_org.id}) - repo = make_repository({'product-id': product['id']}) - gpg_key = make_content_credential({'organization-id': module_org.id}) - # Associate gpg key with a product - Product.update( - {'gpg-key-id': gpg_key['id'], 'id': product['id'], 'organization-id': module_org.id} - ) - # Verify gpg key was associated both with product and its repository - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) - repo = Repository.info({'id': repo['id']}) - assert product['gpg']['gpg-key'] == gpg_key['name'] - assert repo['gpg-key'].get('name') == gpg_key['name'] - # Delete the gpg key - ContentCredential.delete({'name': gpg_key['name'], 'organization-id': module_org.id}) - # Verify gpg key was actually deleted - with pytest.raises(CLIReturnCodeError): - ContentCredential.info({'id': gpg_key['id'], 'organization-id': module_org.id}) - # Verify gpg key was disassociated from the product and its repository - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) - repo = Repository.info({'id': repo['id']}) - assert product['gpg']['gpg-key'] != gpg_key['name'] - assert repo['gpg-key'].get('name') != gpg_key['name'] - - -@pytest.mark.tier2 -def test_positive_delete_key_for_product_with_repos(module_org): - """Create gpg key with valid name and valid gpg key via file - import then associate it with custom product that has more than one - repository then delete it - - :id: 3848441f-746a-424c-afc3-4d5a15888af8 - - :expectedresults: gpg key is associated with product and its - repositories during creation but removed from the product and the - repositories after deletion - - :CaseLevel: Integration - """ - # Create product, repositories and gpg key - product = make_product({'organization-id': module_org.id}) - repos = [make_repository({'product-id': product['id']}) for _ in range(gen_integer(2, 5))] - gpg_key = make_content_credential({'organization-id': module_org.id}) - # Associate gpg key with a product - Product.update( - {'gpg-key-id': gpg_key['id'], 'id': product['id'], 'organization-id': module_org.id} - ) - # Verify gpg key was associated with product and its repositories - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) - assert product['gpg']['gpg-key'] == gpg_key['name'] - for repo in repos: - repo = Repository.info({'id': repo['id']}) - assert repo['gpg-key'].get('name') == gpg_key['name'] - # Delete the gpg key - ContentCredential.delete({'name': gpg_key['name'], 'organization-id': module_org.id}) - # Verify gpg key was actually deleted - with pytest.raises(CLIReturnCodeError): - ContentCredential.info({'id': gpg_key['id'], 'organization-id': module_org.id}) - # Verify gpg key was disassociated from the product and its - # repositories - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) - assert product['gpg']['gpg-key'] != gpg_key['name'] - for repo in repos: - repo = Repository.info({'id': repo['id']}) - assert repo['gpg-key'].get('name') != gpg_key['name'] - - -@pytest.mark.tier2 -def test_positive_delete_key_for_repo_from_product_with_repo(module_org): - """Create gpg key with valid name and valid gpg key via file - import then associate it to repository from custom product that has - one repository then delete the key - - :id: 2555b08f-8cee-4e84-8f4d-9b46743f5758 - - :expectedresults: gpg key is associated with the single repository but - not the product during creation and was removed from repository - after deletion - - :CaseLevel: Integration - """ - # Create product, repository and gpg key - product = make_product({'organization-id': module_org.id}) - repo = make_repository({'product-id': product['id']}) - gpg_key = make_content_credential({'organization-id': module_org.id}) - # Associate gpg key with a repository - Repository.update( - {'gpg-key-id': gpg_key['id'], 'id': repo['id'], 'organization-id': module_org.id} - ) - # Verify gpg key was associated with the repository but not with the - # product - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) - repo = Repository.info({'id': repo['id']}) - assert product['gpg']['gpg-key'] != gpg_key['name'] - assert repo['gpg-key'].get('name') == gpg_key['name'] - # Delete the gpg key - ContentCredential.delete({'name': gpg_key['name'], 'organization-id': module_org.id}) - # Verify gpg key was actually deleted - with pytest.raises(CLIReturnCodeError): - ContentCredential.info({'id': gpg_key['id'], 'organization-id': module_org.id}) - # Verify gpg key was disassociated from the repository - repo = Repository.info({'id': repo['id']}) - assert repo['gpg-key'].get('name') != gpg_key['name'] - - -@pytest.mark.tier2 -def test_positive_delete_key_for_repo_from_product_with_repos(module_org): - """Create gpg key with valid name and valid gpg key via file - import then associate it to repository from custom product that has - more than one repository then delete the key - - :id: 7d6a278b-1063-4e72-bc32-ca60bd17bb84 - - :expectedresults: gpg key is associated with a single repository but - not the product during creation and removed from repository after - deletion - - :CaseLevel: Integration - """ - # Create product, repositories and gpg key - product = make_product({'organization-id': module_org.id}) - repos = [] - for _ in range(gen_integer(2, 5)): - repos.append(make_repository({'product-id': product['id']})) - gpg_key = make_content_credential({'organization-id': module_org.id}) - # Associate gpg key with a repository - Repository.update( - {'gpg-key-id': gpg_key['id'], 'id': repos[0]['id'], 'organization-id': module_org.id} - ) - # Verify gpg key was associated with the repository - repos[0] = Repository.info({'id': repos[0]['id']}) - assert repos[0]['gpg-key']['name'] == gpg_key['name'] - # Delete the gpg key - ContentCredential.delete({'name': gpg_key['name'], 'organization-id': module_org.id}) - # Verify gpg key was actually deleted - with pytest.raises(CLIReturnCodeError): - ContentCredential.info({'id': gpg_key['id'], 'organization-id': module_org.id}) - # Verify gpg key is not associated with any repository or the product - # itself - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) - assert product['gpg']['gpg-key'] != gpg_key['name'] - for repo in repos: - repo = Repository.info({'id': repo['id']}) - assert repo['gpg-key'].get('name') != gpg_key['name'] - - @pytest.mark.tier1 -def test_positive_list(module_org): +def test_positive_list(module_target_sat, module_org): """Create gpg key and list it :id: ca69e23b-ca96-43dd-89a6-55b0e4ea322d @@ -893,16 +745,21 @@ def test_positive_list(module_org): :CaseImportance: Critical """ - gpg_key = make_content_credential( - {'key': VALID_GPG_KEY_FILE_PATH, 'organization-id': module_org.id} + + gpg_key = module_target_sat.cli_factory.make_content_credential( + {'path': VALID_GPG_KEY_FILE_PATH, 'organization-id': module_org.id} ) - gpg_keys_list = ContentCredential.list({'organization-id': module_org.id}) - assert gpg_key['id'] in [gpg['id'] for gpg in gpg_keys_list] + + gpg_key_list = module_target_sat.cli.ContentCredential.list( + {'organization-id': module_org.id, 'name': gpg_key['name']} + ) + + assert gpg_key['id'] in [gpg['id'] for gpg in gpg_key_list] @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_search(name, module_org): +def test_positive_search(target_sat, name, module_org): """Create gpg key and search for it :id: f72648f1-b468-4662-9653-3464e7d0c349 @@ -913,15 +770,15 @@ def test_positive_search(name, module_org): :CaseImportance: Critical """ - gpg_key = make_content_credential( + gpg_key = target_sat.cli_factory.make_content_credential( { - 'key': VALID_GPG_KEY_FILE_PATH, + 'path': VALID_GPG_KEY_FILE_PATH, 'name': name, 'organization-id': module_org.id, } ) # Can we find the new object? - result = ContentCredential.exists( + result = target_sat.cli.ContentCredential.exists( {'organization-id': module_org.id}, search=('name', gpg_key['name']) ) assert gpg_key['name'] == result['name'] diff --git a/tests/foreman/ui/test_contentcredentials.py b/tests/foreman/ui/test_contentcredentials.py index 3a2ce7c9296..36d1575c796 100644 --- a/tests/foreman/ui/test_contentcredentials.py +++ b/tests/foreman/ui/test_contentcredentials.py @@ -17,7 +17,6 @@ :Upstream: No """ import pytest -from nailgun import entities from robottelo.config import settings from robottelo.constants import CONTENT_CREDENTIALS_TYPES @@ -27,14 +26,9 @@ empty_message = "You currently don't have any Products associated with this Content Credential." -@pytest.fixture(scope='module') -def module_org(): - return entities.Organization().create() - - @pytest.fixture(scope='module') def gpg_content(): - return DataFile.VALID_GPG_KEY_FILE.read_bytes() + return DataFile.VALID_GPG_KEY_FILE.read_text() @pytest.fixture(scope='module') @@ -45,7 +39,7 @@ def gpg_path(): @pytest.mark.e2e @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_end_to_end(session, module_org, gpg_content): +def test_positive_end_to_end(session, target_sat, module_org, gpg_content): """Perform end to end testing for gpg key component :id: d1a8cc1b-a072-465b-887d-5bca0acd21c3 @@ -66,11 +60,11 @@ def test_positive_end_to_end(session, module_org, gpg_content): } ) assert session.contentcredential.search(name)[0]['Name'] == name - gpg_key = entities.ContentCredential(organization=module_org).search( + gpg_key = target_sat.api.ContentCredential(organization=module_org).search( query={'search': f'name="{name}"'} )[0] - product = entities.Product(gpg_key=gpg_key, organization=module_org).create() - repo = entities.Repository(product=product).create() + product = target_sat.api.Product(gpg_key=gpg_key, organization=module_org).create() + repo = target_sat.api.Repository(product=product).create() values = session.contentcredential.read(name) assert values['details']['name'] == name assert values['details']['content_type'] == CONTENT_CREDENTIALS_TYPES['gpg'] @@ -86,13 +80,16 @@ def test_positive_end_to_end(session, module_org, gpg_content): # Update gpg key with new name session.contentcredential.update(name, {'details.name': new_name}) assert session.contentcredential.search(new_name)[0]['Name'] == new_name + # Delete repo and product dependent on the gpg key + repo.delete() + product.delete() # Delete gpg key session.contentcredential.delete(new_name) assert session.contentcredential.search(new_name)[0]['Name'] != new_name @pytest.mark.tier2 -def test_positive_search_scoped(session, gpg_content): +def test_positive_search_scoped(session, target_sat, gpg_content, module_org): """Search for gpgkey by organization id parameter :id: e1e04f68-5d4f-43f6-a9c1-b9f566fcbc92 @@ -104,9 +101,8 @@ def test_positive_search_scoped(session, gpg_content): :BZ: 1259374 """ name = gen_string('alpha') - org = entities.Organization().create() with session: - session.organization.select(org.name) + session.organization.select(module_org.name) session.contentcredential.create( { 'name': name, @@ -114,11 +110,14 @@ def test_positive_search_scoped(session, gpg_content): 'content': gpg_content, } ) - assert session.contentcredential.search(f'organization_id = {org.id}')[0]['Name'] == name + assert ( + session.contentcredential.search(f'organization_id = {module_org.id}')[0]['Name'] + == name + ) @pytest.mark.tier2 -def test_positive_add_empty_product(session, module_org, gpg_content): +def test_positive_add_empty_product(session, target_sat, module_org, gpg_content): """Create gpg key with valid name and valid gpg key then associate it with empty (no repos) custom product @@ -129,7 +128,7 @@ def test_positive_add_empty_product(session, module_org, gpg_content): :CaseLevel: Integration """ prod_name = gen_string('alpha') - gpg_key = entities.GPGKey(content=gpg_content, organization=module_org).create() + gpg_key = target_sat.api.GPGKey(content=gpg_content, organization=module_org).create() with session: session.product.create({'name': prod_name, 'gpg_key': gpg_key.name}) values = session.contentcredential.read(gpg_key.name) @@ -140,7 +139,7 @@ def test_positive_add_empty_product(session, module_org, gpg_content): @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_add_product_with_repo(session, module_org, gpg_content): +def test_positive_add_product_with_repo(session, target_sat, module_org, gpg_content): """Create gpg key with valid name and valid gpg key then associate it with custom product that has one repository @@ -152,11 +151,13 @@ def test_positive_add_product_with_repo(session, module_org, gpg_content): :CaseLevel: Integration """ name = gen_string('alpha') - gpg_key = entities.GPGKey(content=gpg_content, name=name, organization=module_org).create() + gpg_key = target_sat.api.GPGKey( + content=gpg_content, name=name, organization=module_org + ).create() # Creates new product - product = entities.Product(organization=module_org).create() + product = target_sat.api.Product(organization=module_org).create() # Creates new repository without GPGKey - repo = entities.Repository(url=settings.repos.yum_1.url, product=product).create() + repo = target_sat.api.Repository(url=settings.repos.yum_1.url, product=product).create() with session: values = session.contentcredential.read(name) assert values['products']['table'][0]['Name'] == empty_message @@ -174,7 +175,7 @@ def test_positive_add_product_with_repo(session, module_org, gpg_content): @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_add_product_with_repos(session, module_org, gpg_content): +def test_positive_add_product_with_repos(session, target_sat, module_org, gpg_content): """Create gpg key with valid name and valid gpg key then associate it with custom product that has more than one repository @@ -185,13 +186,15 @@ def test_positive_add_product_with_repos(session, module_org, gpg_content): :CaseLevel: Integration """ name = gen_string('alpha') - gpg_key = entities.GPGKey(content=gpg_content, name=name, organization=module_org).create() + gpg_key = target_sat.api.GPGKey( + content=gpg_content, name=name, organization=module_org + ).create() # Creates new product and associate GPGKey with it - product = entities.Product(gpg_key=gpg_key, organization=module_org).create() + product = target_sat.api.Product(gpg_key=gpg_key, organization=module_org).create() # Creates new repository_1 without GPGKey - repo1 = entities.Repository(product=product, url=settings.repos.yum_1.url).create() + repo1 = target_sat.api.Repository(product=product, url=settings.repos.yum_1.url).create() # Creates new repository_2 without GPGKey - repo2 = entities.Repository(product=product, url=settings.repos.yum_2.url).create() + repo2 = target_sat.api.Repository(product=product, url=settings.repos.yum_2.url).create() with session: values = session.contentcredential.read(name) assert len(values['repositories']['table']) == 2 @@ -201,7 +204,7 @@ def test_positive_add_product_with_repos(session, module_org, gpg_content): @pytest.mark.tier2 -def test_positive_add_repo_from_product_with_repo(session, module_org, gpg_content): +def test_positive_add_repo_from_product_with_repo(session, target_sat, module_org, gpg_content): """Create gpg key with valid name and valid gpg key then associate it to repository from custom product that has one repository @@ -213,11 +216,13 @@ def test_positive_add_repo_from_product_with_repo(session, module_org, gpg_conte :CaseLevel: Integration """ name = gen_string('alpha') - gpg_key = entities.GPGKey(content=gpg_content, name=name, organization=module_org).create() + gpg_key = target_sat.api.GPGKey( + content=gpg_content, name=name, organization=module_org + ).create() # Creates new product - product = entities.Product(organization=module_org).create() + product = target_sat.api.Product(organization=module_org).create() # Creates new repository - repo = entities.Repository(url=settings.repos.yum_1.url, product=product).create() + repo = target_sat.api.Repository(url=settings.repos.yum_1.url, product=product).create() with session: values = session.contentcredential.read(name) assert values['products']['table'][0]['Name'] == empty_message @@ -232,7 +237,7 @@ def test_positive_add_repo_from_product_with_repo(session, module_org, gpg_conte @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_add_repo_from_product_with_repos(session, module_org, gpg_content): +def test_positive_add_repo_from_product_with_repos(session, target_sat, module_org, gpg_content): """Create gpg key with valid name and valid gpg key then associate it to repository from custom product that has more than one repository @@ -244,15 +249,17 @@ def test_positive_add_repo_from_product_with_repos(session, module_org, gpg_cont :CaseLevel: Integration """ name = gen_string('alpha') - gpg_key = entities.GPGKey(content=gpg_content, name=name, organization=module_org).create() + gpg_key = target_sat.api.GPGKey( + content=gpg_content, name=name, organization=module_org + ).create() # Creates new product without selecting GPGkey - product = entities.Product(organization=module_org).create() + product = target_sat.api.Product(organization=module_org).create() # Creates new repository with GPGKey - repo1 = entities.Repository( + repo1 = target_sat.api.Repository( url=settings.repos.yum_1.url, product=product, gpg_key=gpg_key ).create() # Creates new repository without GPGKey - entities.Repository(url=settings.repos.yum_2.url, product=product).create() + target_sat.api.Repository(url=settings.repos.yum_2.url, product=product).create() with session: values = session.contentcredential.read(name) assert values['products']['table'][0]['Name'] == empty_message @@ -308,7 +315,7 @@ def test_positive_add_product_using_repo_discovery(session, gpg_path): @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_add_product_and_search(session, module_org, gpg_content): +def test_positive_add_product_and_search(session, target_sat, module_org, gpg_content): """Create gpg key with valid name and valid gpg key then associate it with custom product that has one repository After search and select product through gpg key interface @@ -325,11 +332,13 @@ def test_positive_add_product_and_search(session, module_org, gpg_content): :CaseLevel: Integration """ name = gen_string('alpha') - gpg_key = entities.GPGKey(content=gpg_content, name=name, organization=module_org).create() + gpg_key = target_sat.api.GPGKey( + content=gpg_content, name=name, organization=module_org + ).create() # Creates new product and associate GPGKey with it - product = entities.Product(gpg_key=gpg_key, organization=module_org).create() + product = target_sat.api.Product(gpg_key=gpg_key, organization=module_org).create() # Creates new repository without GPGKey - repo = entities.Repository(url=settings.repos.yum_1.url, product=product).create() + repo = target_sat.api.Repository(url=settings.repos.yum_1.url, product=product).create() with session: values = session.contentcredential.read(gpg_key.name) assert len(values['products']['table']) == 1 @@ -400,54 +409,7 @@ def test_positive_update_key_for_product_using_repo_discovery(session, gpg_path) @pytest.mark.tier2 -@pytest.mark.upgrade -@pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -@pytest.mark.usefixtures('allow_repo_discovery') -def test_positive_delete_key_for_product_using_repo_discovery(session, gpg_path): - """Create gpg key with valid name and valid gpg then associate - it with custom product using Repo discovery method then delete it - - :id: 513ae138-84d9-4c43-8d4e-7b9fb797208d - - :expectedresults: gpg key is associated with product as well as with - the repositories during creation but removed from product after - deletion - - :BZ: 1210180, 1461804 - - :CaseLevel: Integration - """ - name = gen_string('alpha') - product_name = gen_string('alpha') - repo_name = 'fakerepo01' - with session: - session.contentcredential.create( - { - 'name': name, - 'content_type': CONTENT_CREDENTIALS_TYPES['gpg'], - 'upload_file': gpg_path, - } - ) - assert session.contentcredential.search(name)[0]['Name'] == name - session.product.discover_repo( - { - 'repo_type': 'Yum Repositories', - 'url': settings.repos.repo_discovery.url, - 'discovered_repos.repos': repo_name, - 'create_repo.product_type': 'New Product', - 'create_repo.product_content.product_name': product_name, - 'create_repo.product_content.gpg_key': name, - } - ) - product_values = session.product.read(product_name) - assert product_values['details']['gpg_key'] == name - session.contentcredential.delete(name) - product_values = session.product.read(product_name) - assert product_values['details']['gpg_key'] == '' - - -@pytest.mark.tier2 -def test_positive_update_key_for_empty_product(session, module_org, gpg_content): +def test_positive_update_key_for_empty_product(session, target_sat, module_org, gpg_content): """Create gpg key with valid name and valid gpg key then associate it with empty (no repos) custom product then update the key @@ -460,9 +422,11 @@ def test_positive_update_key_for_empty_product(session, module_org, gpg_content) """ name = gen_string('alpha') new_name = gen_string('alpha') - gpg_key = entities.GPGKey(content=gpg_content, name=name, organization=module_org).create() + gpg_key = target_sat.api.GPGKey( + content=gpg_content, name=name, organization=module_org + ).create() # Creates new product and associate GPGKey with it - product = entities.Product(gpg_key=gpg_key, organization=module_org).create() + product = target_sat.api.Product(gpg_key=gpg_key, organization=module_org).create() with session: values = session.contentcredential.read(name) # Assert that GPGKey is associated with product @@ -477,7 +441,7 @@ def test_positive_update_key_for_empty_product(session, module_org, gpg_content) @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_update_key_for_product_with_repo(session, module_org, gpg_content): +def test_positive_update_key_for_product_with_repo(session, target_sat, module_org, gpg_content): """Create gpg key with valid name and valid gpg key then associate it with custom product that has one repository then update the key @@ -490,11 +454,13 @@ def test_positive_update_key_for_product_with_repo(session, module_org, gpg_cont """ name = gen_string('alpha') new_name = gen_string('alpha') - gpg_key = entities.GPGKey(content=gpg_content, name=name, organization=module_org).create() + gpg_key = target_sat.api.GPGKey( + content=gpg_content, name=name, organization=module_org + ).create() # Creates new product and associate GPGKey with it - product = entities.Product(gpg_key=gpg_key, organization=module_org).create() + product = target_sat.api.Product(gpg_key=gpg_key, organization=module_org).create() # Creates new repository without GPGKey - repo = entities.Repository(product=product, url=settings.repos.yum_1.url).create() + repo = target_sat.api.Repository(product=product, url=settings.repos.yum_1.url).create() with session: session.contentcredential.update(name, {'details.name': new_name}) values = session.contentcredential.read(new_name) @@ -508,7 +474,7 @@ def test_positive_update_key_for_product_with_repo(session, module_org, gpg_cont @pytest.mark.tier2 @pytest.mark.upgrade @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_update_key_for_product_with_repos(session, module_org, gpg_content): +def test_positive_update_key_for_product_with_repos(session, target_sat, module_org, gpg_content): """Create gpg key with valid name and valid gpg key then associate it with custom product that has more than one repository then update the key @@ -522,13 +488,15 @@ def test_positive_update_key_for_product_with_repos(session, module_org, gpg_con """ name = gen_string('alpha') new_name = gen_string('alpha') - gpg_key = entities.GPGKey(content=gpg_content, name=name, organization=module_org).create() + gpg_key = target_sat.api.GPGKey( + content=gpg_content, name=name, organization=module_org + ).create() # Creates new product and associate GPGKey with it - product = entities.Product(gpg_key=gpg_key, organization=module_org).create() + product = target_sat.api.Product(gpg_key=gpg_key, organization=module_org).create() # Creates new repository_1 without GPGKey - repo1 = entities.Repository(product=product, url=settings.repos.yum_1.url).create() + repo1 = target_sat.api.Repository(product=product, url=settings.repos.yum_1.url).create() # Creates new repository_2 without GPGKey - repo2 = entities.Repository(product=product, url=settings.repos.yum_2.url).create() + repo2 = target_sat.api.Repository(product=product, url=settings.repos.yum_2.url).create() with session: session.contentcredential.update(name, {'details.name': new_name}) values = session.contentcredential.read(new_name) @@ -540,7 +508,9 @@ def test_positive_update_key_for_product_with_repos(session, module_org, gpg_con @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_update_key_for_repo_from_product_with_repo(session, module_org, gpg_content): +def test_positive_update_key_for_repo_from_product_with_repo( + session, target_sat, module_org, gpg_content +): """Create gpg key with valid name and valid gpg key then associate it to repository from custom product that has one repository then update the key @@ -554,11 +524,13 @@ def test_positive_update_key_for_repo_from_product_with_repo(session, module_org """ name = gen_string('alpha') new_name = gen_string('alpha') - gpg_key = entities.GPGKey(content=gpg_content, name=name, organization=module_org).create() + gpg_key = target_sat.api.GPGKey( + content=gpg_content, name=name, organization=module_org + ).create() # Creates new product without selecting GPGkey - product = entities.Product(organization=module_org).create() + product = target_sat.api.Product(organization=module_org).create() # Creates new repository with GPGKey - repo = entities.Repository( + repo = target_sat.api.Repository( gpg_key=gpg_key, product=product, url=settings.repos.yum_1.url ).create() with session: @@ -575,7 +547,9 @@ def test_positive_update_key_for_repo_from_product_with_repo(session, module_org @pytest.mark.tier2 @pytest.mark.upgrade @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_update_key_for_repo_from_product_with_repos(session, module_org, gpg_content): +def test_positive_update_key_for_repo_from_product_with_repos( + session, target_sat, module_org, gpg_content +): """Create gpg key with valid name and valid gpg key then associate it to repository from custom product that has more than one repository then update the key @@ -589,219 +563,20 @@ def test_positive_update_key_for_repo_from_product_with_repos(session, module_or """ name = gen_string('alpha') new_name = gen_string('alpha') - gpg_key = entities.GPGKey(content=gpg_content, name=name, organization=module_org).create() + gpg_key = target_sat.api.GPGKey( + content=gpg_content, name=name, organization=module_org + ).create() # Creates new product without selecting GPGkey - product = entities.Product(organization=module_org).create() + product = target_sat.api.Product(organization=module_org).create() # Creates new repository_1 with GPGKey - repo1 = entities.Repository( + repo1 = target_sat.api.Repository( url=settings.repos.yum_1.url, product=product, gpg_key=gpg_key ).create() # Creates new repository_2 without GPGKey - entities.Repository(product=product, url=settings.repos.yum_2.url).create() + target_sat.api.Repository(product=product, url=settings.repos.yum_2.url).create() with session: session.contentcredential.update(name, {'details.name': new_name}) values = session.contentcredential.read(new_name) assert values['products']['table'][0]['Name'] == empty_message assert len(values['repositories']['table']) == 1 assert values['repositories']['table'][0]['Name'] == repo1.name - - -@pytest.mark.tier2 -def test_positive_delete_key_for_empty_product(session, module_org, gpg_content): - """Create gpg key with valid name and valid gpg key then - associate it with empty (no repos) custom product then delete it - - :id: b9766403-61b2-4a88-a744-a25d53d577fb - - :expectedresults: gpg key is associated with product during creation - but removed from product after deletion - - :CaseLevel: Integration - """ - gpg_key = entities.GPGKey(content=gpg_content, organization=module_org).create() - # Creates new product and associate GPGKey with it - product = entities.Product( - gpg_key=gpg_key, name=gen_string('alpha'), organization=module_org - ).create() - with session: - # Assert that GPGKey is associated with product - gpg_values = session.contentcredential.read(gpg_key.name) - assert len(gpg_values['products']['table']) == 1 - assert gpg_values['products']['table'][0]['Name'] == product.name - product_values = session.product.read(product.name) - assert product_values['details']['gpg_key'] == gpg_key.name - session.contentcredential.delete(gpg_key.name) - # Assert GPGKey isn't associated with product - product_values = session.product.read(product.name) - assert not product_values['details']['gpg_key'] - - -@pytest.mark.tier2 -@pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_delete_key_for_product_with_repo(session, module_org, gpg_content): - """Create gpg key with valid name and valid gpg key then - associate it with custom product that has one repository then delete it - - :id: 75057dd2-9083-47a8-bea7-4f073bdb667e - - :expectedresults: gpg key is associated with product as well as with - the repository during creation but removed from product after - deletion - - :CaseLevel: Integration - """ - gpg_key = entities.GPGKey(content=gpg_content, organization=module_org).create() - # Creates new product and associate GPGKey with it - product = entities.Product( - gpg_key=gpg_key, name=gen_string('alpha'), organization=module_org - ).create() - # Creates new repository without GPGKey - repo = entities.Repository( - name=gen_string('alpha'), url=settings.repos.yum_1.url, product=product - ).create() - with session: - # Assert that GPGKey is associated with product - values = session.contentcredential.read(gpg_key.name) - assert len(values['products']['table']) == 1 - assert values['products']['table'][0]['Name'] == product.name - assert len(values['repositories']['table']) == 1 - assert values['repositories']['table'][0]['Name'] == repo.name - repo_values = session.repository.read(product.name, repo.name) - assert repo_values['repo_content']['gpg_key'] == gpg_key.name - session.contentcredential.delete(gpg_key.name) - # Assert GPGKey isn't associated with product and repository - product_values = session.product.read(product.name) - assert not product_values['details']['gpg_key'] - repo_values = session.repository.read(product.name, repo.name) - assert not repo_values['repo_content']['gpg_key'] - - -@pytest.mark.tier2 -@pytest.mark.upgrade -@pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_delete_key_for_product_with_repos(session, module_org, gpg_content): - """Create gpg key with valid name and valid gpg key then - associate it with custom product that has more than one repository then - delete it - - :id: cb5d4efd-863a-4b8e-b1f8-a0771e90ff5e - - :expectedresults: gpg key is associated with product as well as with - repositories during creation but removed from product after - deletion - - :CaseLevel: Integration - """ - gpg_key = entities.GPGKey(content=gpg_content, organization=module_org).create() - # Creates new product and associate GPGKey with it - product = entities.Product( - gpg_key=gpg_key, name=gen_string('alpha'), organization=module_org - ).create() - # Creates new repository_1 without GPGKey - repo1 = entities.Repository( - name=gen_string('alpha'), product=product, url=settings.repos.yum_1.url - ).create() - # Creates new repository_2 without GPGKey - repo2 = entities.Repository( - name=gen_string('alpha'), product=product, url=settings.repos.yum_2.url - ).create() - with session: - # Assert that GPGKey is associated with product - values = session.contentcredential.read(gpg_key.name) - assert len(values['products']['table']) == 1 - assert values['products']['table'][0]['Name'] == product.name - assert len(values['repositories']['table']) == 2 - assert {repo1.name, repo2.name} == { - repo['Name'] for repo in values['repositories']['table'] - } - session.contentcredential.delete(gpg_key.name) - # Assert GPGKey isn't associated with product and repositories - product_values = session.product.read(product.name) - assert not product_values['details']['gpg_key'] - for repo in [repo1, repo2]: - repo_values = session.repository.read(product.name, repo.name) - assert not repo_values['repo_content']['gpg_key'] - - -@pytest.mark.tier2 -@pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_delete_key_for_repo_from_product_with_repo(session, module_org, gpg_content): - """Create gpg key with valid name and valid gpg key then - associate it to repository from custom product that has one repository - then delete the key - - :id: 92ba492e-79af-48fe-84cb-763102b42fa7 - - :expectedresults: gpg key is associated with single repository during - creation but removed from repository after deletion - - :CaseLevel: Integration - """ - gpg_key = entities.GPGKey(content=gpg_content, organization=module_org).create() - # Creates new product without selecting GPGkey - product = entities.Product(name=gen_string('alpha'), organization=module_org).create() - # Creates new repository with GPGKey - repo = entities.Repository( - name=gen_string('alpha'), url=settings.repos.yum_1.url, product=product, gpg_key=gpg_key - ).create() - with session: - # Assert that GPGKey is associated with product - values = session.contentcredential.read(gpg_key.name) - assert values['products']['table'][0]['Name'] == empty_message - assert len(values['repositories']['table']) == 1 - assert values['repositories']['table'][0]['Name'] == repo.name - repo_values = session.repository.read(product.name, repo.name) - assert repo_values['repo_content']['gpg_key'] == gpg_key.name - session.contentcredential.delete(gpg_key.name) - # Assert GPGKey isn't associated with repository - repo_values = session.repository.read(product.name, repo.name) - assert not repo_values['repo_content']['gpg_key'] - - -@pytest.mark.tier2 -@pytest.mark.upgrade -@pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_delete_key_for_repo_from_product_with_repos(session, module_org, gpg_content): - """Create gpg key with valid name and valid gpg key then - associate it to repository from custom product that has more than - one repository then delete the key - - :id: 5f204a44-bf7b-4a9c-9974-b701e0d38860 - - :expectedresults: gpg key is associated with single repository but not - with product during creation but removed from repository after - deletion - - :BZ: 1461804 - - :CaseLevel: Integration - """ - # Creates New GPGKey - gpg_key = entities.GPGKey(content=gpg_content, organization=module_org).create() - # Creates new product without GPGKey association - product = entities.Product(name=gen_string('alpha'), organization=module_org).create() - # Creates new repository_1 with GPGKey association - repo1 = entities.Repository( - gpg_key=gpg_key, name=gen_string('alpha'), product=product, url=settings.repos.yum_1.url - ).create() - repo2 = entities.Repository( - name=gen_string('alpha'), - product=product, - url=settings.repos.yum_2.url, - # notice that we're not making this repo point to the GPG key - ).create() - with session: - # Assert that GPGKey is associated with product - values = session.contentcredential.read(gpg_key.name) - assert values['products']['table'][0]['Name'] == empty_message - assert len(values['repositories']['table']) == 1 - assert values['repositories']['table'][0]['Name'] == repo1.name - repo_values = session.repository.read(product.name, repo1.name) - assert repo_values['repo_content']['gpg_key'] == gpg_key.name - repo_values = session.repository.read(product.name, repo2.name) - assert not repo_values['repo_content']['gpg_key'] - session.contentcredential.delete(gpg_key.name) - # Assert GPGKey isn't associated with repositories - for repo in [repo1, repo2]: - repo_values = session.repository.read(product.name, repo.name) - assert not repo_values['repo_content']['gpg_key'] From 10eec8d73ae04a5072f87f79a77a1b1d9f06cae1 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 7 Aug 2023 13:20:00 -0400 Subject: [PATCH 125/586] [6.14.z] Change certs fixture to use sat_ready_rhel fixture (#12152) --- .../component/katello_certs_check.py | 21 ++----- robottelo/constants/__init__.py | 3 - .../destructive/test_katello_certs_check.py | 60 ------------------- tests/foreman/sys/test_katello_certs_check.py | 45 ++++++++++++++ 4 files changed, 50 insertions(+), 79 deletions(-) diff --git a/pytest_fixtures/component/katello_certs_check.py b/pytest_fixtures/component/katello_certs_check.py index 3dd8d71df83..d9150fa539b 100644 --- a/pytest_fixtures/component/katello_certs_check.py +++ b/pytest_fixtures/component/katello_certs_check.py @@ -2,29 +2,18 @@ from pathlib import Path import pytest -from broker import Broker from fauxfactory import gen_string from robottelo.constants import CERT_DATA as cert_data from robottelo.hosts import Capsule -from robottelo.hosts import ContentHost @pytest.fixture -def certs_vm_setup(request): - """Create VM and register content host""" - target_cores = request.param.get('target_cores', 1) - target_memory = request.param.get('target_memory', '1GiB') - with Broker( - nick=request.param['nick'], - host_class=ContentHost, - target_cores=target_cores, - target_memory=target_memory, - ) as host: - cert_data['key_file_name'] = f'{host.hostname}/{host.hostname}.key' - cert_data['cert_file_name'] = f'{host.hostname}/{host.hostname}.crt' - host.custom_cert_generate(cert_data['capsule_hostname']) - yield cert_data, host +def certs_data(sat_ready_rhel): + cert_data['key_file_name'] = f'{sat_ready_rhel.hostname}/{sat_ready_rhel.hostname}.key' + cert_data['cert_file_name'] = f'{sat_ready_rhel.hostname}/{sat_ready_rhel.hostname}.crt' + sat_ready_rhel.custom_cert_generate(cert_data['capsule_hostname']) + yield cert_data @pytest.fixture diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index 7c6537aaa8d..b6b29c613e0 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -261,9 +261,6 @@ class Colored(Box): 'rhel-8-for-x86_64-appstream-rpms', ) -INSTALL_RHEL7_STEPS = 'yum -y install satellite' -INSTALL_RHEL8_STEPS = 'dnf -y module enable satellite:el8 && dnf -y install satellite' - # On importing manifests, Red Hat repositories are listed like this: # Product -> RepositorySet -> Repository # We need to first select the Product, then the reposet and then the repos diff --git a/tests/foreman/destructive/test_katello_certs_check.py b/tests/foreman/destructive/test_katello_certs_check.py index 5e9100aa2e3..0d0f9fd5a99 100644 --- a/tests/foreman/destructive/test_katello_certs_check.py +++ b/tests/foreman/destructive/test_katello_certs_check.py @@ -21,8 +21,6 @@ import pytest -import robottelo.constants as constants -from robottelo.config import settings from robottelo.utils.issue_handlers import is_open pytestmark = pytest.mark.destructive @@ -79,64 +77,6 @@ def test_positive_update_katello_certs(cert_setup_destructive_teardown): assert result.status == 0, 'Not all services are running' -@pytest.mark.e2e -@pytest.mark.parametrize( - 'certs_vm_setup', - [ - {'nick': 'rhel8', 'target_memory': '20GiB', 'target_cores': 4}, - ], - ids=['rhel8'], - indirect=True, -) -def test_positive_install_sat_with_katello_certs(certs_vm_setup): - """Update certificates on a currently running satellite instance. - - :id: 47e3a57f-d7a2-40d2-bbc7-d1bb3d79a7e1 - - :steps: - - 1. Generate the custom certs on RHEL 7 machine - 2. Install satellite with custom certs - 3. Assert output does not report SSL certificate error - 4. Assert all services are running - - - :expectedresults: Satellite should be installed using the custom certs. - - :CaseAutomation: Automated - """ - cert_data, rhel_vm = certs_vm_setup - version = rhel_vm.os_version.major - rhel_vm.download_repofile(product='satellite', release=settings.server.version.release) - rhel_vm.register_contenthost( - org=None, - lce=None, - username=settings.subscription.rhn_username, - password=settings.subscription.rhn_password, - ) - result = rhel_vm.subscription_manager_attach_pool([settings.subscription.rhn_poolid])[0] - for repo in getattr(constants, f"OHSNAP_RHEL{version}_REPOS"): - rhel_vm.enable_repo(repo, force=True) - rhel_vm.execute('yum -y update') - result = rhel_vm.execute(getattr(constants, f"INSTALL_RHEL{version}_STEPS")) - assert result.status == 0 - command = ( - 'satellite-installer --scenario satellite ' - f'--certs-server-cert "/root/{cert_data["cert_file_name"]}" ' - f'--certs-server-key "/root/{cert_data["key_file_name"]}" ' - f'--certs-server-ca-cert "/root/{cert_data["ca_bundle_file_name"]}" ' - ) - result = rhel_vm.execute(command, timeout=2200000) - assert result.status == 0 - # assert no hammer ping SSL cert error - result = rhel_vm.execute('hammer ping') - assert 'SSL certificate verification failed' not in result.stdout - assert result.stdout.count('ok') == 8 - # assert all services are running - result = rhel_vm.execute('satellite-maintain health check --label services-up -y') - assert result.status == 0, 'Not all services are running' - - def test_regeneration_ssl_build_certs(target_sat): """delete the ssl-build folder and cross check that ssl-build folder is recovered/regenerated after running the installer diff --git a/tests/foreman/sys/test_katello_certs_check.py b/tests/foreman/sys/test_katello_certs_check.py index a0c6eabe8cf..073f7b22fb1 100644 --- a/tests/foreman/sys/test_katello_certs_check.py +++ b/tests/foreman/sys/test_katello_certs_check.py @@ -21,6 +21,51 @@ import pytest +from robottelo.config import settings +from robottelo.utils.installer import InstallerCommand + + +@pytest.mark.e2e +def test_positive_install_sat_with_katello_certs(certs_data, sat_ready_rhel): + """Install Satellite with custom certs. + + :id: 47e3a57f-d7a2-40d2-bbc7-d1bb3d79a7e1 + + :steps: + + 1. Generate the custom certs on RHEL machine + 2. Install satellite with custom certs + 3. Assert output does not report SSL certificate error + 4. Assert all services are running + + + :expectedresults: Satellite should be installed using the custom certs. + + :CaseAutomation: Automated + """ + sat_ready_rhel.download_repofile(product='satellite', release=settings.server.version.release) + sat_ready_rhel.register_to_cdn() + sat_ready_rhel.execute('dnf -y update') + result = sat_ready_rhel.execute( + 'dnf -y module enable satellite:el8 && dnf -y install satellite' + ) + assert result.status == 0 + command = InstallerCommand( + scenario='satellite', + certs_server_cert=f'/root/{certs_data["cert_file_name"]}', + certs_server_key=f'/root/{certs_data["key_file_name"]}', + certs_server_ca_cert=f'/root/{certs_data["ca_bundle_file_name"]}', + ).get_command() + result = sat_ready_rhel.execute(command, timeout='30m') + assert result.status == 0 + # assert no hammer ping SSL cert error + result = sat_ready_rhel.execute('hammer ping') + assert 'SSL certificate verification failed' not in result.stdout + assert result.stdout.count('Status:') == result.stdout.count(' ok') + # assert all services are running + result = sat_ready_rhel.execute('satellite-maintain health check --label services-up -y') + assert result.status == 0, 'Not all services are running' + @pytest.mark.run_in_one_thread class TestKatelloCertsCheck: From 86771265be575af2643936de0331d36b678916e6 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 8 Aug 2023 11:04:43 -0400 Subject: [PATCH 126/586] [6.14.z] delete tailoring files which are no longer needed (#12165) --- tests/foreman/cli/test_oscap_tailoringfiles.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/foreman/cli/test_oscap_tailoringfiles.py b/tests/foreman/cli/test_oscap_tailoringfiles.py index 97c9675f053..cb3c4f697bb 100644 --- a/tests/foreman/cli/test_oscap_tailoringfiles.py +++ b/tests/foreman/cli/test_oscap_tailoringfiles.py @@ -52,6 +52,10 @@ def test_positive_create(self, tailoring_file_path, name): {'name': name, 'scap-file': tailoring_file_path['satellite']} ) assert tailoring_file['name'] == name + # Delete tailoring files which created for all valid input (ex- latin1, cjk, utf-8, etc.) + TailoringFiles.delete({'id': tailoring_file['id']}) + with pytest.raises(CLIReturnCodeError): + TailoringFiles.info({'id': tailoring_file['id']}) @pytest.mark.tier1 def test_positive_create_with_space(self, tailoring_file_path): From 8f898aabf513f3a0d315c1a3dd1f55706a34ef31 Mon Sep 17 00:00:00 2001 From: Jameer Pathan Date: Tue, 8 Aug 2023 14:55:17 +0200 Subject: [PATCH 127/586] Mark test_positive_configure_cloud_connector as destructive (cherry picked from commit 433e2c045b2b2f41db7bccb21b9433acaf8ff666) --- tests/foreman/api/test_rhc.py | 1 + tests/foreman/cli/test_remoteexecution.py | 83 ----------------------- 2 files changed, 1 insertion(+), 83 deletions(-) diff --git a/tests/foreman/api/test_rhc.py b/tests/foreman/api/test_rhc.py index d3279ebc7cb..08b7d89cc38 100644 --- a/tests/foreman/api/test_rhc.py +++ b/tests/foreman/api/test_rhc.py @@ -37,6 +37,7 @@ def fixture_enable_rhc_repos(module_target_sat): @pytest.mark.e2e @pytest.mark.tier3 +@pytest.mark.destructive def test_positive_configure_cloud_connector( module_target_sat, module_org, fixture_enable_rhc_repos ): diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index cd3b64654c2..9de5569c81b 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -26,10 +26,7 @@ from dateutil.relativedelta import FR from dateutil.relativedelta import relativedelta from fauxfactory import gen_string -from nailgun import entities -from wait_for import wait_for -from robottelo import constants from robottelo.cli.factory import make_filter from robottelo.cli.factory import make_job_invocation from robottelo.cli.factory import make_job_invocation_with_credentials @@ -50,7 +47,6 @@ from robottelo.constants import REPOS from robottelo.constants import REPOSET from robottelo.hosts import ContentHost -from robottelo.logging import logger from robottelo.utils import ohsnap @@ -72,14 +68,6 @@ def fixture_sca_vmsetup(request, module_sca_manifest_org, target_sat): yield client -@pytest.fixture() -def fixture_enable_receptor_repos(request, target_sat): - """Enable RHSCL repo required by receptor installer""" - target_sat.enable_repo(constants.REPOS['rhscl7']['id']) - target_sat.enable_repo(constants.REPOS['rhae2']['id']) - target_sat.enable_repo(constants.REPOS['rhs7']['id']) - - @pytest.fixture() def infra_host(request, target_sat, module_capsule_configured): infra_hosts = {'target_sat': target_sat, 'module_capsule_configured': module_capsule_configured} @@ -467,77 +455,6 @@ def test_positive_run_scheduled_job_template_by_ip(self, rex_contenthost, target sleep(30) assert_job_invocation_result(invocation_command['id'], client.hostname) - @pytest.mark.tier3 - @pytest.mark.upgrade - @pytest.mark.skip("Receptor plugin is deprecated/removed for Satellite >= 6.11") - def test_positive_run_receptor_installer( - self, target_sat, subscribe_satellite, fixture_enable_receptor_repos - ): - """Run Receptor installer ("Configure Cloud Connector") - - :CaseComponent: RHCloud-CloudConnector - - :Team: Platform - - :id: 811c7747-bec6-1a2d-8e5c-b5045d3fbc0d - - :expectedresults: The job passes, installs Receptor that peers with c.r.c - - :BZ: 1818076 - """ - result = target_sat.execute('stat /etc/receptor/*/receptor.conf') - if result.status == 0: - pytest.skip( - 'Cloud Connector has already been configured on this system. ' - 'It is possible to reconfigure it but then the test would not really ' - 'check if everything is correctly configured from scratch. Skipping.' - ) - # Copy foreman-proxy user's key to root@localhost user's authorized_keys - target_sat.add_rex_key(satellite=target_sat) - - # Set Host parameter source_display_name to something random. - # To avoid 'name has already been taken' error when run multiple times - # on a machine with the same hostname. - host_id = Host.info({'name': target_sat.hostname})['id'] - Host.set_parameter( - {'host-id': host_id, 'name': 'source_display_name', 'value': gen_string('alpha')} - ) - - template_name = 'Configure Cloud Connector' - invocation = make_job_invocation( - { - 'async': True, - 'job-template': template_name, - 'inputs': f'satellite_user="{settings.server.admin_username}",\ - satellite_password="{settings.server.admin_password}"', - 'search-query': f'name ~ {target_sat.hostname}', - } - ) - invocation_id = invocation['id'] - wait_for( - lambda: entities.JobInvocation(id=invocation_id).read().status_label - in ['succeeded', 'failed'], - timeout='1500s', - ) - - result = JobInvocation.get_output({'id': invocation_id, 'host': target_sat.hostname}) - logger.debug(f'Invocation output>>\n{result}\n< print enabled repos - repolist = target_sat.execute('yum repolist') - logger.debug(f'Repolist>>\n{repolist}\n<= 1 - class TestAnsibleREX: """Test class for remote execution via Ansible""" From 605ce94f0f2b1ecf94be84f95f0af45b044b1b4c Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 8 Aug 2023 13:04:12 -0400 Subject: [PATCH 128/586] [6.14.z] Registration test dmi_uuid fix (#12177) --- tests/foreman/api/test_registration.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/foreman/api/test_registration.py b/tests/foreman/api/test_registration.py index 8b23f651ac3..9d184f029f5 100644 --- a/tests/foreman/api/test_registration.py +++ b/tests/foreman/api/test_registration.py @@ -21,7 +21,6 @@ import pytest from robottelo.constants import CLIENT_PORT -from robottelo.constants import ENVIRONMENT pytestmark = pytest.mark.tier1 @@ -89,8 +88,10 @@ def test_host_registration_end_to_end( @pytest.mark.tier3 +@pytest.mark.rhel_ver_match('[^6]') +@pytest.mark.skip_if_open("BZ:2229112") def test_positive_allow_reregistration_when_dmi_uuid_changed( - module_org, rhel_contenthost, target_sat + module_org, rhel_contenthost, target_sat, module_ak_with_synced_repo, module_location ): """Register a content host with a custom DMI UUID, unregistering it, change the DMI UUID, and re-registering it again @@ -101,18 +102,27 @@ def test_positive_allow_reregistration_when_dmi_uuid_changed( :customerscenario: true - :BZ: 1747177 + :BZ: 1747177,2229112 :CaseLevel: Integration """ uuid_1 = str(uuid.uuid1()) uuid_2 = str(uuid.uuid4()) - rhel_contenthost.install_katello_ca(target_sat) target_sat.execute(f'echo \'{{"dmi.system.uuid": "{uuid_1}"}}\' > /etc/rhsm/facts/uuid.facts') - result = rhel_contenthost.register_contenthost(module_org.label, lce=ENVIRONMENT) + command = target_sat.api.RegistrationCommand( + organization=module_org, + activation_keys=[module_ak_with_synced_repo.name], + location=module_location, + ).create() + result = rhel_contenthost.execute(command) assert result.status == 0 result = rhel_contenthost.execute('subscription-manager clean') assert result.status == 0 target_sat.execute(f'echo \'{{"dmi.system.uuid": "{uuid_2}"}}\' > /etc/rhsm/facts/uuid.facts') - result = rhel_contenthost.register_contenthost(module_org.label, lce=ENVIRONMENT) + command = target_sat.api.RegistrationCommand( + organization=module_org, + activation_keys=[module_ak_with_synced_repo.name], + location=module_location, + ).create() + result = rhel_contenthost.execute(command) assert result.status == 0 From 29d9b57412d271a6c3d56de1627b9a217667b767 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 9 Aug 2023 03:10:27 -0400 Subject: [PATCH 129/586] [6.14.z] Bump wrapanapi from 3.5.17 to 3.5.18 (#12185) Bump wrapanapi from 3.5.17 to 3.5.18 (#12181) (cherry picked from commit b6947fcd63e418bbb0deafd3034ee5e2af5a2045) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4a99dd6ce50..ecb994a8106 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ requests==2.31.0 tenacity==8.2.2 testimony==2.3.0 wait-for==1.2.0 -wrapanapi==3.5.17 +wrapanapi==3.5.18 # Get airgun, nailgun and upgrade from master git+https://github.com/SatelliteQE/airgun.git@6.14.z#egg=airgun From 5c0fbfd52e2fef4e627779716269cd5d2c791965 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 9 Aug 2023 03:12:27 -0400 Subject: [PATCH 130/586] [6.14.z] Add another test for capsule sync deadlock issue (#12174) Add another test for capsule sync deadlock issue Signed-off-by: Gaurav Talreja (cherry picked from commit 3109797073641373f5031ddbc88643d88d2d5dfa) Co-authored-by: Gaurav Talreja --- robottelo/cli/contentview.py | 6 + .../destructive/test_capsulecontent.py | 106 ++++++++++++++++-- 2 files changed, 105 insertions(+), 7 deletions(-) diff --git a/robottelo/cli/contentview.py b/robottelo/cli/contentview.py index 8677d7de655..bd1a96fe4c5 100644 --- a/robottelo/cli/contentview.py +++ b/robottelo/cli/contentview.py @@ -166,6 +166,12 @@ def version_delete(cls, options): cls.command_sub = 'version delete' return cls.execute(cls._construct_command(options), ignore_stderr=True) + @classmethod + def version_republish_repositories(cls, options): + """Removes content-view version.""" + cls.command_sub = 'version republish-repositories' + return cls.execute(cls._construct_command(options), ignore_stderr=True) + @classmethod def remove_from_environment(cls, options=None): """Remove content-view from an environment""" diff --git a/tests/foreman/destructive/test_capsulecontent.py b/tests/foreman/destructive/test_capsulecontent.py index 6efd1c641b6..c5a74334721 100644 --- a/tests/foreman/destructive/test_capsulecontent.py +++ b/tests/foreman/destructive/test_capsulecontent.py @@ -17,6 +17,7 @@ :Upstream: No """ import pytest +from box import Box from fauxfactory import gen_alpha from robottelo import constants @@ -58,7 +59,7 @@ def test_positive_sync_without_deadlock( # smaller RHSCL repo instead, which was also capable to hit the deadlock issue, regardless # the lower rpms count. When the BZ is fixed, reconsider upscale to RHEL7 repo or similar. repo_id = target_sat.api_factory.enable_rhrepo_and_fetchid( - basearch='x86_64', + basearch=constants.DEFAULT_ARCHITECTURE, org_id=function_entitlement_manifest_org.id, product=constants.REPOS['rhscl7']['product'], repo=constants.REPOS['rhscl7']['name'], @@ -79,15 +80,106 @@ def test_positive_sync_without_deadlock( proxy.download_policy = 'immediate' proxy.update(['download_policy']) + nailgun_capsule = large_capsule_configured.nailgun_capsule lce = target_sat.api.LifecycleEnvironment( organization=function_entitlement_manifest_org ).search(query={'search': f'name={constants.ENVIRONMENT}'})[0] - large_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( - data={'environment_id': lce.id} - ) - result = large_capsule_configured.nailgun_capsule.content_lifecycle_environments() + nailgun_capsule.content_add_lifecycle_environment(data={'environment_id': lce.id}) + result = nailgun_capsule.content_lifecycle_environments() assert len(result['results']) == 1 assert result['results'][0]['id'] == lce.id - - sync_status = large_capsule_configured.nailgun_capsule.content_sync(timeout='90m') + sync_status = nailgun_capsule.content_sync(timeout='90m') assert sync_status['result'] == 'success', 'Capsule sync task failed.' + + +@pytest.mark.tier4 +@pytest.mark.skip_if_not_set('capsule') +def test_positive_sync_without_deadlock_after_rpm_trim_changelog( + target_sat, capsule_configured, function_entitlement_manifest_org +): + """Promote a CV published with larger repos into multiple LCEs, assign LCEs to blank Capsule. + Assert that the sync task succeeds and no deadlock happens. + + :id: 91c6eec9-a582-46ea-9898-bdcaebcea2f1 + + :setup: + 1. Blank external capsule which is not synced yet, running with 4 & more pulpcore-workers + + :steps: + 1. Sync a few large repositories to the Satellite. + 2. Create a Content View, add the repository and publish it. + 3. Promote it to 10+ LCEs and assign them to Capsule + 4. Sync the Capsule + 5. pulpcore-manager rpm-trim-changelogs --changelog-limit x + 6. Sync Capsule again and verify no deadlocks occurs. + 7. Repeat step 5 with a lesser changelog limit and step 6 again + + :expectedresults: + 1. Sync passes without deadlock. + + :customerscenario: true + + :BZ: 2170535, 2218661 + """ + org = function_entitlement_manifest_org + rh_repos = [] + tasks = [] + LCE_COUNT = 10 + for name in ['rhel8_bos', 'rhel8_aps']: + rh_repo_id = target_sat.api_factory.enable_rhrepo_and_fetchid( + basearch=constants.DEFAULT_ARCHITECTURE, + org_id=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 + rh_repo = target_sat.api.Repository(id=rh_repo_id).read() + task = rh_repo.sync(synchronous=False) + tasks.append(task) + rh_repos.append(rh_repo) + for task in tasks: + target_sat.wait_for_tasks( + search_query=(f'id = {task["id"]}'), + poll_timeout=2500, + ) + task_status = target_sat.api.ForemanTask(id=task['id']).poll() + assert task_status['result'] == 'success' + + cv = target_sat.publish_content_view(org, rh_repos) + cvv = cv.version[0].read() + nailgun_capsule = capsule_configured.nailgun_capsule + + for i in range(LCE_COUNT): + lce = target_sat.api.LifecycleEnvironment(name=f'lce{i}', organization=org).create() + cvv.promote(data={'environment_ids': lce.id, 'force': False}) + nailgun_capsule.content_add_lifecycle_environment(data={'environment_id': lce.id}) + + result = Box(nailgun_capsule.content_lifecycle_environments()) + assert len(result.results) == LCE_COUNT + # Verify all LCE have a CV promoted + assert [env.content_views[0].name for env in result.results].count(cv.name) == LCE_COUNT + + sync_status = nailgun_capsule.content_sync(timeout='60m') + assert sync_status['result'] == 'success', 'Capsule sync task failed' + + # Run rpm-trim-changelogs with changelog-limit below 10 then again sync capsule + # and repeat again with changelog-limit below it to make sure deadlock + pulp_cmd = ( + 'sudo -u pulp PULP_SETTINGS="/etc/pulp/settings.py" ' + '/usr/bin/pulpcore-manager rpm-trim-changelogs --changelog-limit ' + ) + for limit in range(9, 0, -1): + result = target_sat.execute(f'{pulp_cmd}{limit}') + assert f'Trimmed changelogs for {cvv.package_count} packages' in result.stdout + target_sat.cli.ContentView.version_republish_repositories({'id': cvv.id, 'force': 'true'}) + + sync_status = nailgun_capsule.content_sync(timeout='60m') + assert sync_status['result'] == 'success', 'Capsule sync task failed' + + # Verify no deadlock detected in the postgres logs and pulp/syslogs + day = target_sat.execute('date').stdout[:3] + check_log_files = [f'/var/lib/pgsql/data/log/postgresql-{day}.log', '/var/log/messages'] + for file in check_log_files: + assert capsule_configured.execute(f'grep -i "deadlock detected" {file}').status From 7892efc9f3c02d31b3337d7ba9f88fc81f5ba853 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 9 Aug 2023 04:01:28 -0400 Subject: [PATCH 131/586] [6.14.z] remove delete org and create a new location for virt-who upgrade scenarios (#12187) remove delete org and create a new location for virt-who upgrade scenarios (#12092) (cherry picked from commit c23589edad8a1e77ba7d5fff7829a5d4e4ee05f0) Co-authored-by: yanpliu --- tests/upgrades/test_virtwho.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/tests/upgrades/test_virtwho.py b/tests/upgrades/test_virtwho.py index f950d45e520..4b2c30b2934 100644 --- a/tests/upgrades/test_virtwho.py +++ b/tests/upgrades/test_virtwho.py @@ -23,7 +23,6 @@ from robottelo.cli.subscription import Subscription from robottelo.cli.virt_who_config import VirtWhoConfig from robottelo.config import settings -from robottelo.constants import DEFAULT_LOC from robottelo.utils.issue_handlers import is_open from robottelo.utils.virtwho import deploy_configure_by_command from robottelo.utils.virtwho import get_configure_command @@ -78,16 +77,8 @@ def test_pre_create_virt_who_configuration( 3. Report is sent to satellite. 4. Virtual sku can be generated and attached. """ - org = target_sat.api.Organization().search(query={'search': f'name={ORG_DATA["name"]}'}) - if org: - target_sat.api.Organization(id=org[0].id).delete() - default_loc_id = ( - target_sat.api.Location().search(query={'search': f'name="{DEFAULT_LOC}"'})[0].id - ) - default_loc = target_sat.api.Location(id=default_loc_id).read() org = target_sat.api.Organization(name=ORG_DATA['name']).create() - default_loc.organization.append(target_sat.api.Organization(id=org.id)) - default_loc.update(['organization']) + target_sat.api.Location(organization=[org]).create() org.sca_disable() target_sat.upload_manifest(org.id, function_entitlement_manifest.content) form_data.update({'organization_id': org.id}) From e6327a054116b94517a62504369a94e3fb53b1d0 Mon Sep 17 00:00:00 2001 From: Jameer Pathan Date: Wed, 9 Aug 2023 14:30:11 +0200 Subject: [PATCH 132/586] Use target_sat for test_positive_configure_cloud_connector api test (cherry picked from commit 7c4be222f5670966532c0bc3833e1ccc85e5d54c) --- tests/foreman/api/test_rhc.py | 44 ++++++++++++++++------------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/tests/foreman/api/test_rhc.py b/tests/foreman/api/test_rhc.py index 08b7d89cc38..abc35f87744 100644 --- a/tests/foreman/api/test_rhc.py +++ b/tests/foreman/api/test_rhc.py @@ -22,25 +22,23 @@ from robottelo.utils.issue_handlers import is_open -@pytest.fixture(scope='module') -def fixture_enable_rhc_repos(module_target_sat): +@pytest.fixture +def fixture_enable_rhc_repos(target_sat): """Enable repos required for configuring RHC.""" # subscribe rhc satellite to cdn. - module_target_sat.register_to_cdn() - if module_target_sat.os_version.major > 7: - module_target_sat.enable_repo(module_target_sat.REPOS['rhel_bos']['id']) - module_target_sat.enable_repo(module_target_sat.REPOS['rhel_aps']['id']) + target_sat.register_to_cdn() + if target_sat.os_version.major > 7: + target_sat.enable_repo(target_sat.REPOS['rhel_bos']['id']) + target_sat.enable_repo(target_sat.REPOS['rhel_aps']['id']) else: - module_target_sat.enable_repo(module_target_sat.REPOS['rhscl']['id']) - module_target_sat.enable_repo(module_target_sat.REPOS['rhel']['id']) + target_sat.enable_repo(target_sat.REPOS['rhscl']['id']) + target_sat.enable_repo(target_sat.REPOS['rhel']['id']) @pytest.mark.e2e @pytest.mark.tier3 @pytest.mark.destructive -def test_positive_configure_cloud_connector( - module_target_sat, module_org, fixture_enable_rhc_repos -): +def test_positive_configure_cloud_connector(target_sat, default_org, fixture_enable_rhc_repos): """ Enable RH Cloud Connector through API @@ -58,45 +56,43 @@ def test_positive_configure_cloud_connector( # Delete old satellite hostname if BZ#2130173 is open if is_open('BZ:2130173'): - host = module_target_sat.api.Host().search( - query={'search': f"! {module_target_sat.hostname}"} - )[0] + host = target_sat.api.Host().search(query={'search': f"! {target_sat.hostname}"})[0] host.delete() # Copy foreman-proxy user's key to root@localhost user's authorized_keys - module_target_sat.add_rex_key(satellite=module_target_sat) + target_sat.add_rex_key(satellite=target_sat) # Set Host parameter source_display_name to something random. # To avoid 'name has already been taken' error when run multiple times # on a machine with the same hostname. - host = module_target_sat.api.Host().search(query={'search': module_target_sat.hostname})[0] + host = target_sat.api.Host().search(query={'search': target_sat.hostname})[0] parameters = [{'name': 'source_display_name', 'value': gen_string('alpha')}] host.host_parameters_attributes = parameters host.update(['host_parameters_attributes']) - enable_connector = module_target_sat.api.RHCloud(organization=module_org).enable_connector() + enable_connector = target_sat.api.RHCloud(organization=default_org).enable_connector() template_name = 'Configure Cloud Connector' invocation_id = ( - module_target_sat.api.JobInvocation() + target_sat.api.JobInvocation() .search(query={'search': f'description="{template_name}"'})[0] .id ) task_id = enable_connector['task_id'] - module_target_sat.wait_for_tasks( + target_sat.wait_for_tasks( search_query=(f'label = Actions::RemoteExecution::RunHostsJob and id = {task_id}'), search_rate=15, ) - job_output = module_target_sat.cli.JobInvocation.get_output( - {'id': invocation_id, 'host': module_target_sat.hostname} + job_output = target_sat.cli.JobInvocation.get_output( + {'id': invocation_id, 'host': target_sat.hostname} ) # get rhc status - rhc_status = module_target_sat.execute('rhc status') + rhc_status = target_sat.execute('rhc status') # get rhcd log - rhcd_log = module_target_sat.execute('journalctl --unit=rhcd') + rhcd_log = target_sat.execute('journalctl --unit=rhcd') - assert module_target_sat.api.JobInvocation(id=invocation_id).read().status == 0 + assert target_sat.api.JobInvocation(id=invocation_id).read().status == 0 assert "Install yggdrasil-worker-forwarder and rhc" in job_output assert "Restart rhcd" in job_output assert 'Exit status: 0' in job_output From b5b9448069ea4697e409ca0421767549a50eda64 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 10 Aug 2023 07:21:45 -0400 Subject: [PATCH 133/586] [6.14.z] Update osp.yaml and osp tests (#12197) --- conf/osp.yaml.template | 29 +++++-- tests/foreman/cli/test_computeresource_osp.py | 79 ++++++++++--------- 2 files changed, 66 insertions(+), 42 deletions(-) diff --git a/conf/osp.yaml.template b/conf/osp.yaml.template index 836e6fe9657..be8adc427d2 100644 --- a/conf/osp.yaml.template +++ b/conf/osp.yaml.template @@ -1,16 +1,33 @@ OSP: # Openstack to be added as a compute resource + HOSTNAME: + OSP16: hostname.example.com + OSP17: hostname.example.com + # Openstack authentication url + AUTH_URL: + OSP16: '@format http://{this.osp.hostname.osp16}:5000' + OSP17: '@format http://{this.osp.hostname.osp17}:5000' # Openstack api url For eg: https://hostname:port/v3/auth/tokens - HOSTNAME: https://hostname:port/v3/auth/tokens + API_URL: + OSP16: '@format {this.osp.auth_url.osp16}/v3/auth/tokens' + OSP17: '@format {this.osp.auth_url.osp17}/v3/auth/tokens' # Login for Openstack - USERNAME: psi-satellite-jenkins + USERNAME: # Password for Openstack PASSWORD: + # Name of Security group + SECURITY_GROUP: # Openstack tenant to be used - TENANT: satellite-jenkins - # Name of group for provisioning - SECURITY_GROUP: satellite5 + TENANT: # Name of VM to power On/Off & delete - VM_NAME: automation-robottelo-vm + VM_NAME: # ID of the domain for the project PROJECT_DOMAIN_ID: + # Openstack network name + NETWORK_NAME: + # Openstack rhel8 image name + RHEL8_IMAGE: + # Openstack template's configuration flavour name + FLAVOR_NAME: + # Openstack ssh-key configuration + SSHKEY: diff --git a/tests/foreman/cli/test_computeresource_osp.py b/tests/foreman/cli/test_computeresource_osp.py index 36fe06c90b1..170a7410ea9 100644 --- a/tests/foreman/cli/test_computeresource_osp.py +++ b/tests/foreman/cli/test_computeresource_osp.py @@ -16,19 +16,16 @@ :Upstream: No """ import pytest +from box import Box from fauxfactory import gen_string -from robottelo.cli.computeresource import ComputeResource -from robottelo.cli.factory import CLIFactoryError from robottelo.cli.factory import CLIReturnCodeError -from robottelo.cli.factory import make_compute_resource from robottelo.config import settings -OSP_SETTINGS = dict( +OSP_SETTINGS = Box( username=settings.osp.username, password=settings.osp.password, tenant=settings.osp.tenant, - hostname=settings.osp.hostname, project_domain_id=settings.osp.project_domain_id, ) @@ -36,19 +33,29 @@ class TestOSPComputeResourceTestCase: """OSPComputeResource CLI tests.""" - def cr_cleanup(self, cr_id): + def cr_cleanup(self, cr_id, id_type, target_sat): """Finalizer for removing CR from Satellite. This should remove ssh key pairs from OSP in case of test fail. """ try: - ComputeResource.delete({'id': cr_id}) + target_sat.cli.ComputeResource.delete({id_type: cr_id}) + assert not target_sat.cli.ComputeResource.exists(search=(id_type, cr_id)) except CLIReturnCodeError: pass + @pytest.fixture + def osp_version(request): + versions = {'osp16': settings.osp.api_url.osp16, 'osp17': settings.osp.api_url.osp17} + yield versions[getattr(request, 'param', 'osp16')] + @pytest.mark.upgrade @pytest.mark.tier3 - @pytest.mark.parametrize('id_type', ['id', 'name']) - def test_crud_and_duplicate_name(self, request, id_type): + @pytest.mark.parametrize('osp_version', ['osp16', 'osp17'], indirect=True) + @pytest.mark.parametrize( + 'id_type', + ['id', 'name'], + ) + def test_crud_and_duplicate_name(self, request, id_type, osp_version, target_sat): """Create, list, update and delete Openstack compute resource :id: caf60bad-999d-483e-807f-95f52f35085d @@ -63,47 +70,47 @@ def test_crud_and_duplicate_name(self, request, id_type): """ # create name = gen_string('alpha') - compute_resource = make_compute_resource( + compute_resource = target_sat.cli.ComputeResource.create( { 'name': name, 'provider': 'Openstack', - 'user': OSP_SETTINGS['username'], - 'password': OSP_SETTINGS['password'], - 'tenant': OSP_SETTINGS['tenant'], - 'url': OSP_SETTINGS['hostname'], - 'project-domain-id': OSP_SETTINGS['project_domain_id'], + 'user': OSP_SETTINGS.username, + 'password': OSP_SETTINGS.password, + 'tenant': OSP_SETTINGS.tenant, + 'project-domain-id': OSP_SETTINGS.project_domain_id, + 'url': osp_version, } ) - request.addfinalizer(lambda: self.cr_cleanup(compute_resource['id'])) assert compute_resource['name'] == name - assert ComputeResource.exists(search=(id_type, compute_resource[id_type])) + assert target_sat.cli.ComputeResource.exists(search=(id_type, compute_resource[id_type])) # negative create with same name - with pytest.raises(CLIFactoryError): - make_compute_resource( + with pytest.raises(CLIReturnCodeError): + target_sat.cli.ComputeResource.create( { 'name': name, 'provider': 'Openstack', - 'user': OSP_SETTINGS['username'], - 'password': OSP_SETTINGS['password'], - 'tenant': OSP_SETTINGS['tenant'], - 'url': OSP_SETTINGS['hostname'], - 'project-domain-id': OSP_SETTINGS['project_domain_id'], + 'user': OSP_SETTINGS.username, + 'password': OSP_SETTINGS.password, + 'tenant': OSP_SETTINGS.tenant, + 'project-domain-id': OSP_SETTINGS.project_domain_id, + 'url': osp_version, } ) # update new_name = gen_string('alpha') - ComputeResource.update({id_type: compute_resource[id_type], 'new-name': new_name}) + target_sat.cli.ComputeResource.update( + {id_type: compute_resource[id_type], 'new-name': new_name} + ) if id_type == 'name': - compute_resource = ComputeResource.info({'name': new_name}) + compute_resource = target_sat.cli.ComputeResource.info({'name': new_name}) else: - compute_resource = ComputeResource.info({'id': compute_resource['id']}) + compute_resource = target_sat.cli.ComputeResource.info({'id': compute_resource['id']}) assert new_name == compute_resource['name'] - ComputeResource.delete({id_type: compute_resource[id_type]}) - assert not ComputeResource.exists(search=(id_type, compute_resource[id_type])) + request.addfinalizer(lambda: self.cr_cleanup(compute_resource['id'], id_type, target_sat)) @pytest.mark.tier3 - def test_negative_create_osp_with_url(self): + def test_negative_create_osp_with_url(self, target_sat): """Attempt to create Openstack compute resource with invalid URL :id: a6be8233-2641-4c87-8563-f48d6efbb6ac @@ -116,15 +123,15 @@ def test_negative_create_osp_with_url(self): """ name = gen_string('alpha') with pytest.raises(CLIReturnCodeError): - ComputeResource.create( + target_sat.cli.ComputeResource.create( { 'name': name, 'provider': 'Openstack', - 'user': OSP_SETTINGS['username'], - 'password': OSP_SETTINGS['password'], - 'tenant': OSP_SETTINGS['tenant'], - 'url': 'invalid url', - 'project-domain-id': OSP_SETTINGS['project_domain_id'], + 'user': OSP_SETTINGS.username, + 'password': OSP_SETTINGS.password, + 'tenant': OSP_SETTINGS.tenant, + 'project-domain-id': OSP_SETTINGS.project_domain_id, + 'url': 'invalid_url', } ) From ee60f8e32a6a3f8e8559b772918bb9815ee7ef3a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 10 Aug 2023 07:54:03 -0400 Subject: [PATCH 134/586] [6.14.z] password is now redacted in the template output (#12201) --- tests/foreman/ui/test_templatesync.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/foreman/ui/test_templatesync.py b/tests/foreman/ui/test_templatesync.py index 86b4a2b8c9a..cb91a50df4e 100644 --- a/tests/foreman/ui/test_templatesync.py +++ b/tests/foreman/ui/test_templatesync.py @@ -170,7 +170,7 @@ def test_positive_export_filtered_templates_to_git(session, git_repository, git_ :expectedresults: 1. Assert matching templates are exported to git repo. - :BZ: 1785613 + :BZ: 1785613, 2013759 :parametrized: yes @@ -189,7 +189,10 @@ def test_positive_export_filtered_templates_to_git(session, git_repository, git_ 'template.dirname': dirname, } ) - assert export_title == f'Export to {url} and branch {git_branch} as user {session._user}' + assert ( + export_title == f'Export to {url.replace(git.password, "*****")} ' + f'and branch {git_branch} as user {session._user}' + ) path = f"{dirname}/provisioning_templates/provision" auth = (git.username, git.password) api_url = f"http://{git.hostname}:{git.http_port}/api/v1/repos/{git.username}" From 94bf6d4a5a425e4df2f7705d82bc2882cfcbe4b0 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 10 Aug 2023 09:35:47 -0400 Subject: [PATCH 135/586] [6.14.z] Fix test_positive_inventory_generate_upload_cli (#12204) --- tests/foreman/cli/test_rhcloud_inventory.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/foreman/cli/test_rhcloud_inventory.py b/tests/foreman/cli/test_rhcloud_inventory.py index 0668df0f941..4d67f6c6c0e 100644 --- a/tests/foreman/cli/test_rhcloud_inventory.py +++ b/tests/foreman/cli/test_rhcloud_inventory.py @@ -80,7 +80,9 @@ def test_positive_inventory_generate_upload_cli( f'/var/lib/foreman/red_hat_inventory/uploads/done/report_for_{org.id}.tar.xz' ) wait_for( - lambda: rhcloud_sat_host.get(remote_path=remote_report_path, local_path=local_report_path), + lambda: rhcloud_sat_host.get( + remote_path=str(remote_report_path), local_path=str(local_report_path) + ), timeout=60, delay=15, silent_failure=True, From 94d4128b73a70498c5d20e8fa1e644013ba29d02 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 11 Aug 2023 04:13:34 -0400 Subject: [PATCH 136/586] [6.14.z] added new api path (#12207) added new api path (#12206) (cherry picked from commit 8710344a75266eca824f5357c03b095f20eea9bd) Co-authored-by: Peter Ondrejka --- tests/foreman/endtoend/test_api_endtoend.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/foreman/endtoend/test_api_endtoend.py b/tests/foreman/endtoend/test_api_endtoend.py index 2ffdb772993..e29032e918f 100644 --- a/tests/foreman/endtoend/test_api_endtoend.py +++ b/tests/foreman/endtoend/test_api_endtoend.py @@ -520,6 +520,7 @@ '/api/organizations/:organization_id/rh_cloud/report', '/api/organizations/:organization_id/rh_cloud/report', '/api/organizations/:organization_id/rh_cloud/inventory_sync', + '/api/organizations/:organization_id/rh_cloud/missing_hosts', '/api/rh_cloud/enable_connector', ), 'interfaces': ( From e05d0ae4bb4014d65e90bcffa14e1218b6bbfd7a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:59:48 -0400 Subject: [PATCH 137/586] [6.14.z] fix for per_page key issue (#12217) --- tests/foreman/ui/test_settings.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/foreman/ui/test_settings.py b/tests/foreman/ui/test_settings.py index 3101226057b..de752f1c449 100644 --- a/tests/foreman/ui/test_settings.py +++ b/tests/foreman/ui/test_settings.py @@ -546,6 +546,7 @@ def test_positive_entries_per_page(session, setting_update): with session: session.settings.update(f"name={property_name}", property_value) page_content = session.task.read_all(widget_names="Pagination") - assert str(property_value) in page_content["Pagination"]["per_page"] - total_pages = math.ceil(int(page_content["Pagination"]["total_items"]) / property_value) - assert str(total_pages) == page_content["Pagination"]["pages"] + assert str(property_value) in page_content["Pagination"]["_items"] + total_pages_str = page_content["Pagination"]['_items'].split()[-2] + total_pages = math.ceil(int(total_pages_str.split()[-1]) / property_value) + assert str(total_pages) == page_content["Pagination"]['_total_pages'].split()[-1] From ac9f238295de0b8f4deaf0ab156ec618abef1a4d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 14 Aug 2023 04:35:44 -0400 Subject: [PATCH 138/586] [6.14.z] Bump dynaconf[vault] from 3.2.0 to 3.2.1 (#12222) Bump dynaconf[vault] from 3.2.0 to 3.2.1 (#12220) (cherry picked from commit eb014ad539f77b863a1ad7da5241164216bb89dd) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ecb994a8106..a1178350f05 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ betelgeuse==1.10.0 broker[docker]==0.3.3 cryptography==41.0.3 deepdiff==6.3.1 -dynaconf[vault]==3.2.0 +dynaconf[vault]==3.2.1 fauxfactory==3.1.0 jinja2==3.1.2 manifester==0.0.13 From db7e54a42a44cc7466b2a8cf5024a974ba36da1f Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 14 Aug 2023 11:50:02 -0400 Subject: [PATCH 139/586] [6.14.z] ISS refactor (#12227) --- tests/foreman/cli/test_contentview.py | 31 + tests/foreman/cli/test_satellitesync.py | 794 ++++++------------------ 2 files changed, 227 insertions(+), 598 deletions(-) diff --git a/tests/foreman/cli/test_contentview.py b/tests/foreman/cli/test_contentview.py index 0a9a764eeda..1b71e7e42cb 100644 --- a/tests/foreman/cli/test_contentview.py +++ b/tests/foreman/cli/test_contentview.py @@ -1506,6 +1506,37 @@ def test_positive_publish_custom_content(self, module_org, module_product): assert new_cv['yum-repositories'][0]['name'] == new_repo['name'] assert new_cv['versions'][0]['version'] == '1.0' + @pytest.mark.tier2 + def test_positive_publish_custom_major_minor_cv_version(self): + """CV can published with custom major and minor versions + + :id: 6697cd22-253a-4bdc-a108-7e0af22caaf4 + + :steps: + + 1. Create product and repository with custom contents + 2. Sync the repository + 3. Create CV with above repository + 4. Publish the CV with custom major and minor versions + + :expectedresults: + + 1. CV version with custom major and minor versions is created + + :CaseLevel: System + """ + org = cli_factory.make_org() + major = random.randint(1, 1000) + minor = random.randint(1, 1000) + content_view = cli_factory.make_content_view( + {'name': gen_string('alpha'), 'organization-id': org['id']} + ) + ContentView.publish({'id': content_view['id'], 'major': major, 'minor': minor}) + content_view = ContentView.info({'id': content_view['id']}) + cvv = content_view['versions'][0]['version'] + assert cvv.split('.')[0] == str(major) + assert cvv.split('.')[1] == str(minor) + @pytest.mark.upgrade @pytest.mark.tier3 @pytest.mark.skipif( diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index 17f131c1656..4d47712da50 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -16,8 +16,6 @@ :Upstream: No """ -from random import randint - import pytest from fauxfactory import gen_string @@ -34,17 +32,15 @@ from robottelo.cli.package import Package from robottelo.cli.product import Product from robottelo.cli.repository import Repository -from robottelo.cli.repository_set import RepositorySet from robottelo.cli.settings import Settings from robottelo.config import settings from robottelo.constants import CONTAINER_REGISTRY_HUB +from robottelo.constants import DEFAULT_ARCHITECTURE from robottelo.constants import DEFAULT_CV -from robottelo.constants import PRDS from robottelo.constants import PULP_EXPORT_DIR from robottelo.constants import PULP_IMPORT_DIR from robottelo.constants import REPO_TYPE from robottelo.constants import REPOS -from robottelo.constants import REPOSET from robottelo.constants.repos import ANSIBLE_GALAXY @@ -98,64 +94,58 @@ def docker_repo(module_target_sat, module_org): yield repo -@pytest.mark.run_in_one_thread -class TestRepositoryExport: - """Tests for exporting a repository via CLI""" +@pytest.fixture(scope='module') +def module_synced_repo(module_target_sat, module_org, module_product): + repo = module_target_sat.cli_factory.make_repository( + { + 'content-type': 'yum', + 'download-policy': 'immediate', + 'organization-id': module_org.id, + 'product-id': module_product.id, + } + ) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + yield repo - @pytest.mark.tier3 - def test_positive_export_complete_version_custom_repo( - self, target_sat, export_import_cleanup_module, module_org - ): - """Export a custom repository via complete version - :id: 9c855866-b9b1-4e32-b3eb-7342fdaa7116 +@pytest.fixture(scope='function') +def function_synced_repo(target_sat, function_org, function_product): + repo = target_sat.cli_factory.make_repository( + { + 'content-type': 'yum', + 'download-policy': 'immediate', + 'organization-id': function_org.id, + 'product-id': function_product.id, + } + ) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + yield repo - :expectedresults: Repository was successfully exported, exported files are - present on satellite machine - :CaseLevel: System - """ - # setup custom repo - cv_name = gen_string('alpha') - product = make_product({'organization-id': module_org.id}) - repo = make_repository( - { - 'download-policy': 'immediate', - 'organization-id': module_org.id, - 'product-id': product['id'], - } - ) - Repository.synchronize({'id': repo['id']}) - # create cv and publish - cv = make_content_view({'name': cv_name, 'organization-id': module_org.id}) - ContentView.add_repository( - { - 'id': cv['id'], - 'organization-id': module_org.id, - 'repository-id': repo['id'], - } - ) - ContentView.publish({'id': cv['id']}) - cv = ContentView.info({'id': cv['id']}) - assert len(cv['versions']) == 1 - cvv = cv['versions'][0] - # Verify export directory is empty - assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) == '' - # Export content view - ContentExport.completeVersion({'id': cvv['id'], 'organization-id': module_org.id}) - # Verify export directory is not empty - assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) != '' +@pytest.mark.run_in_one_thread +class TestRepositoryExport: + """Tests for exporting a repository via CLI""" @pytest.mark.tier3 - def test_positive_export_incremental_version_custom_repo( - self, target_sat, export_import_cleanup_module, module_org + def test_positive_export_version_custom_repo( + self, target_sat, export_import_cleanup_module, module_org, module_synced_repo ): - """Export custom repo via incremental export + """Export custom repo via complete and incremental CV version export. :id: 1b58dca7-c8bb-4893-a306-5882826da559 - :expectedresults: Repository was successfully exported, exported files are - present on satellite machine + :setup: + 1. Product with synced custom repository. + + :steps: + 1. Create a CV, add the product and publish it. + 2. Export complete CV version. + 3. Publish new CV version. + 4. Export incremental CV version. + + :expectedresults: + 1. Complete export succeeds, exported files are present on satellite machine. + 2. Incremental export succeeds, exported files are present on satellite machine. :CaseLevel: System @@ -163,117 +153,82 @@ def test_positive_export_incremental_version_custom_repo( :customerscenario: true """ - # Create custom product and repository + # Create cv and publish cv_name = gen_string('alpha') - product = make_product({'organization-id': module_org.id}) - repo = make_repository( - { - 'download-policy': 'immediate', - 'organization-id': module_org.id, - 'product-id': product['id'], - } + cv = target_sat.cli_factory.make_content_view( + {'name': cv_name, 'organization-id': module_org.id} ) - Repository.synchronize({'id': repo['id']}) - # Create cv and publish - cv = make_content_view({'name': cv_name, 'organization-id': module_org.id}) - ContentView.add_repository( + target_sat.cli.ContentView.add_repository( { 'id': cv['id'], 'organization-id': module_org.id, - 'repository-id': repo['id'], + 'repository-id': module_synced_repo['id'], } ) - ContentView.publish({'id': cv['id']}) - cv = ContentView.info({'id': cv['id']}) + target_sat.cli.ContentView.publish({'id': cv['id']}) + cv = target_sat.cli.ContentView.info({'id': cv['id']}) assert len(cv['versions']) == 1 cvv = cv['versions'][0] # Verify export directory is empty assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) == '' - # Export complete first, then incremental - ContentExport.completeVersion({'id': cvv['id'], 'organization-id': module_org.id}) - ContentExport.incrementalVersion({'id': cvv['id'], 'organization-id': module_org.id}) - # Verify export directory is not empty - assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) != '' - - @pytest.mark.tier3 - def test_positive_export_complete_library_custom_repo( - self, function_org, export_import_cleanup_function, target_sat - ): - """Export custom repo via complete library export - - :id: 5f35654b-fc46-48f0-b064-595e04e2bd7e - - :expectedresults: Repository was successfully exported, exported files are - present on satellite machine - - :CaseLevel: System - """ - # Create custom product and repository - cv_name = gen_string('alpha') - product = make_product({'organization-id': function_org.id}) - repo = make_repository( - { - 'download-policy': 'immediate', - 'organization-id': function_org.id, - 'product-id': product['id'], - } + # Export complete and check the export directory + target_sat.cli.ContentExport.completeVersion( + {'id': cvv['id'], 'organization-id': module_org.id} ) - cv = make_content_view({'name': cv_name, 'organization-id': function_org.id}) - ContentView.add_repository( - { - 'id': cv['id'], - 'organization-id': function_org.id, - 'repository-id': repo['id'], - } + assert '1.0' in target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) + # Publish new CV version, export incremental and check the export directory + target_sat.cli.ContentView.publish({'id': cv['id']}) + cv = target_sat.cli.ContentView.info({'id': cv['id']}) + assert len(cv['versions']) == 2 + cvv = cv['versions'][1] + target_sat.cli.ContentExport.incrementalVersion( + {'id': cvv['id'], 'organization-id': module_org.id} ) - ContentView.publish({'id': cv['id']}) - # Verify export directory is empty - assert target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) == '' - # Export content view - ContentExport.completeLibrary({'organization-id': function_org.id}) - # Verify export directory is not empty - assert target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) != '' + assert '2.0' in target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) @pytest.mark.tier3 - def test_positive_export_incremental_library_custom_repo( - self, export_import_cleanup_function, function_org, target_sat + def test_positive_export_library_custom_repo( + self, target_sat, export_import_cleanup_function, function_org, function_synced_repo ): - """Export custom repo via incremental library export + """Export custom repo via complete and incremental library export. :id: ba8dc7f3-55c2-4120-ac76-cc825ef0abb8 - :expectedresults: Repository was successfully exported, exported files are - present on satellite machine + :setup: + 1. Product with synced custom repository. + + :steps: + 1. Create a CV, add the product and publish it. + 2. Export complete library. + 3. Export incremental library. + + :expectedresults: + 1. Complete export succeeds, exported files are present on satellite machine. + 2. Incremental export succeeds, exported files are present on satellite machine. :CaseLevel: System """ - # Create custom product and repository + # Create cv and publish cv_name = gen_string('alpha') - product = make_product({'organization-id': function_org.id}) - repo = make_repository( - { - 'download-policy': 'immediate', - 'organization-id': function_org.id, - 'product-id': product['id'], - } + cv = target_sat.cli_factory.make_content_view( + {'name': cv_name, 'organization-id': function_org.id} ) - # Create cv and publish - cv = make_content_view({'name': cv_name, 'organization-id': function_org.id}) - ContentView.add_repository( + target_sat.cli.ContentView.add_repository( { 'id': cv['id'], 'organization-id': function_org.id, - 'repository-id': repo['id'], + 'repository-id': function_synced_repo['id'], } ) - ContentView.publish({'id': cv['id']}) + target_sat.cli.ContentView.publish({'id': cv['id']}) # Verify export directory is empty assert target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) == '' - # Export complete library, then export incremental - ContentExport.completeLibrary({'organization-id': function_org.id}) - ContentExport.incrementalLibrary({'organization-id': function_org.id}) - # Verify export directory is not empty - assert target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) != '' + # Export complete and check the export directory + target_sat.cli.ContentExport.completeLibrary({'organization-id': function_org.id}) + assert '1.0' in target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) + # Export incremental and check the export directory + target_sat.cli.ContentExport.incrementalLibrary({'organization-id': function_org.id}) + assert '2.0' in target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) @pytest.mark.tier3 @pytest.mark.upgrade @@ -289,28 +244,14 @@ def test_positive_export_complete_version_rh_repo( :CaseLevel: System """ - # Enable RH repository - cv_name = gen_string('alpha') - RepositorySet.enable( - { - 'basearch': 'x86_64', - 'name': REPOSET['rhva6'], - 'organization-id': module_entitlement_manifest_org.id, - 'product': PRDS['rhel'], - 'releasever': '6Server', - } - ) - repo = Repository.info( - { - 'name': REPOS['rhva6']['name'], - 'organization-id': module_entitlement_manifest_org.id, - 'product': PRDS['rhel'], - } + # Enable and sync RH repository + repo = _enable_rhel_content( + sat=target_sat, + org=module_entitlement_manifest_org, + repo_dict=REPOS['rhae2'], ) - # Update the download policy to 'immediate' and sync - Repository.update({'download-policy': 'immediate', 'id': repo['id']}) - Repository.synchronize({'id': repo['id']}) # Create cv and publish + cv_name = gen_string('alpha') cv = make_content_view( {'name': cv_name, 'organization-id': module_entitlement_manifest_org.id} ) @@ -349,33 +290,19 @@ def test_positive_export_complete_library_rh_repo( :id: ffae18bf-6536-4f11-8002-7bf1568bf7f1 - :expectedresults: Repository was successfully exported, exported files are - present on satellite machine + :expectedresults: + 1. Repository was successfully exported, exported files are present on satellite machine :CaseLevel: System """ - # Enable RH repository - cv_name = gen_string('alpha') - RepositorySet.enable( - { - 'basearch': 'x86_64', - 'name': REPOSET['rhva6'], - 'organization-id': function_entitlement_manifest_org.id, - 'product': PRDS['rhel'], - 'releasever': '6Server', - } - ) - repo = Repository.info( - { - 'name': REPOS['rhva6']['name'], - 'organization-id': function_entitlement_manifest_org.id, - 'product': PRDS['rhel'], - } + # Enable and sync RH repository + repo = _enable_rhel_content( + sat=target_sat, + org=function_entitlement_manifest_org, + repo_dict=REPOS['rhae2'], ) - # Update the download policy to 'immediate' and sync - Repository.update({'download-policy': 'immediate', 'id': repo['id']}) - Repository.synchronize({'id': repo['id']}, timeout=7200000) # Create cv and publish + cv_name = gen_string('alpha') cv = make_content_view( {'name': cv_name, 'organization-id': function_entitlement_manifest_org.id} ) @@ -548,42 +475,40 @@ def _create_cv(cv_name, repo, module_org, publish=True): return content_view, cvv_id -def _enable_rhel_content( - sat_obj, module_entitlement_manifest_org, repo_name, releasever=None, product=None, sync=True -): - """Enable and/or Synchronize rhel content +def _enable_rhel_content(sat, org, repo_dict, ver=None, sync=True): + """Enable (and synchronize) rhel content - :param organization: The organization directory into which the rhel - contents will be enabled - :param bool sync: Syncs contents to repository if true else doesnt + :param sat: Satellite instance to work with + :param org: The organization directory into which the rhel contents will be enabled + :param repo_dict: The repository dict as defined in consts REPOS + :param bool sync: Syncs contents to repository if true else doesn't :return: Repository cli object """ - RepositorySet.enable( + sat.cli.RepositorySet.enable( { - 'basearch': 'x86_64', - 'name': REPOSET[repo_name], - 'organization-id': module_entitlement_manifest_org.id, - 'product': PRDS[product], - 'releasever': releasever, + 'organization-id': org.id, + 'name': repo_dict['reposet'], + 'product': repo_dict['product'], + 'releasever': ver or repo_dict.get('releasever', None), + 'basearch': DEFAULT_ARCHITECTURE, } ) - repo = Repository.info( + repo = sat.cli.Repository.info( { - 'name': REPOS[repo_name]['name'], - 'organization-id': module_entitlement_manifest_org.id, - 'product': PRDS[product], + 'organization-id': org.id, + 'name': repo_dict['name'], + 'product': repo_dict['product'], } ) - # Update the download policy to 'immediate' - Repository.update({'download-policy': 'immediate', 'mirror-on-sync': 'no', 'id': repo['id']}) + # Update the download policy to 'immediate' and sync if required + sat.cli.Repository.update({'download-policy': 'immediate', 'id': repo['id']}) if sync: - # Synchronize the repository - Repository.synchronize({'id': repo['id']}, timeout=7200000) - repo = Repository.info( + sat.cli.Repository.synchronize({'id': repo['id']}, timeout=7200000) + repo = sat.cli.Repository.info( { - 'name': REPOS[repo_name]['name'], - 'organization-id': module_entitlement_manifest_org.id, - 'product': PRDS[product], + 'organization-id': org.id, + 'name': repo_dict['name'], + 'product': repo_dict['product'], } ) return repo @@ -1018,28 +943,15 @@ def test_positive_export_import_redhat_cv( :CaseLevel: System """ - # Setup rhel repo - cv_name = gen_string('alpha') - RepositorySet.enable( - { - 'basearch': 'x86_64', - 'name': REPOSET['kickstart']['rhel7'], - 'organization-id': function_entitlement_manifest_org.id, - 'product': PRDS['rhel'], - 'releasever': '7.9', - } + # Enable and sync RH repository + repo = _enable_rhel_content( + sat=target_sat, + org=function_entitlement_manifest_org, + repo_dict=REPOS['kickstart']['rhel7'], + ver=REPOS['kickstart']['rhel7']['version'], ) - repo = Repository.info( - { - 'name': REPOS['kickstart']['rhel7']['name'], - 'organization-id': function_entitlement_manifest_org.id, - 'product': PRDS['rhel'], - } - ) - # Update the download policy to 'immediate' and sync - Repository.update({'download-policy': 'immediate', 'id': repo['id']}) - Repository.synchronize({'id': repo['id']}, timeout=7200000) # Create cv and publish + cv_name = gen_string('alpha') cv = make_content_view( {'name': cv_name, 'organization-id': function_entitlement_manifest_org.id} ) @@ -1144,28 +1056,14 @@ def test_positive_export_import_redhat_cv_with_huge_contents( :CaseLevel: Acceptance """ - cv_name = gen_string('alpha') - - RepositorySet.enable( - { - 'basearch': 'x86_64', - 'name': REPOSET['rhscl7'], - 'organization-id': function_entitlement_manifest_org.id, - 'product': PRDS['rhscl'], - 'releasever': '7Server', - } + # Enable and sync RH repository + repo = _enable_rhel_content( + sat=target_sat, + org=function_entitlement_manifest_org, + repo_dict=REPOS['rhscl7'], ) - repo = Repository.info( - { - 'name': REPOS['rhscl7']['name'], - 'organization-id': function_entitlement_manifest_org.id, - 'product': PRDS['rhscl'], - } - ) - # Update the download policy to 'immediate' and sync - Repository.update({'download-policy': 'immediate', 'id': repo['id']}) - Repository.synchronize({'id': repo['id']}, timeout=7200000) # Create cv and publish + cv_name = gen_string('alpha') cv = make_content_view( {'name': cv_name, 'organization-id': function_entitlement_manifest_org.id} ) @@ -1241,66 +1139,87 @@ def test_positive_export_cv_with_on_demand_repo( :id: c366ace5-1fde-4ae7-9e84-afe58c06c0ca :steps: - 1. Create product 2. Create repos with immediate and on_demand download policy 3. Sync the repositories 4. Create CV with above product and publish - 5. Attempt to export CV version contents + 5. Attempt to export CV version with 'fail-on-missing' option + 6. Attempt to export CV version without 'fail-on-missing' option :expectedresults: - - 1. Export passes with warning and skips the on_demand repo - - :BZ: 1998626 + 1. Export fails when 'fail-on-missing' option is used + 2. Export passes otherwise with warning and skips the on_demand repo """ # Create custom product - product = make_product({'organization-id': module_org.id, 'name': gen_string('alpha')}) + product = target_sat.cli_factory.make_product( + {'organization-id': module_org.id, 'name': gen_string('alpha')} + ) - # Create repositories - repo_ondemand = make_repository( + # Create repositories and sync them + repo_ondemand = target_sat.cli_factory.make_repository( { + 'content-type': 'yum', 'download-policy': 'on_demand', 'organization-id': module_org.id, 'product-id': product['id'], } ) - repo_immediate = make_repository( + repo_immediate = target_sat.cli_factory.make_repository( { + 'content-type': 'yum', 'download-policy': 'immediate', 'organization-id': module_org.id, 'product-id': product['id'], } ) - - # Sync repositories - Repository.synchronize({'id': repo_ondemand['id']}) - Repository.synchronize({'id': repo_immediate['id']}) + target_sat.cli.Repository.synchronize({'id': repo_ondemand['id']}) + target_sat.cli.Repository.synchronize({'id': repo_immediate['id']}) # Create cv and publish - cv = make_content_view({'name': gen_string('alpha'), 'organization-id': module_org.id}) - ContentView.add_repository( + cv = target_sat.cli_factory.make_content_view( + {'name': gen_string('alpha'), 'organization-id': module_org.id} + ) + target_sat.cli.ContentView.add_repository( { 'id': cv['id'], 'organization-id': module_org.id, 'repository-id': repo_ondemand['id'], } ) - ContentView.add_repository( + target_sat.cli.ContentView.add_repository( { 'id': cv['id'], 'organization-id': module_org.id, 'repository-id': repo_immediate['id'], } ) - ContentView.publish({'id': cv['id']}) + target_sat.cli.ContentView.publish({'id': cv['id']}) # Verify export directory is empty assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) == '' - # Export Content View version - result = ContentExport.completeVersion( - {'id': cv['id'], 'organization-id': module_org.id}, + # Attempt to export CV version with 'fail-on-missing' option + with pytest.raises(CLIReturnCodeError): + target_sat.cli.ContentExport.completeVersion( + { + 'organization-id': module_org.id, + 'content-view-id': cv['id'], + 'version': '1.0', + 'fail-on-missing-content': True, + }, + output_format='base', # json output can't be parsed - BZ#1998626 + ) + + # Export is not generated + assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) == '' + + # Attempt to export CV version without 'fail-on-missing' option + result = target_sat.cli.ContentExport.completeVersion( + { + 'organization-id': module_org.id, + 'content-view-id': cv['id'], + 'version': '1.0', + }, output_format='base', # json output can't be parsed - BZ#1998626 ) @@ -1398,117 +1317,6 @@ def test_negative_import_invalid_path(self, module_org): '--metadata-file option' ) in error.value.message - @pytest.mark.tier2 - def test_negative_export_cv_with_on_demand_repo( - self, export_import_cleanup_module, target_sat, module_org - ): - """Exporting CV version having on_demand repo throws error - with "fail-on-missing-content" option set - - :id: f8b86d0e-e1a7-4e19-bb82-6de7d16c6676 - - :steps: - - 1. Create product - 2. Create repos with immediate and on_demand download policy - 3. Sync the repositories - 4. Create CV with above product and publish - 5. Attempt to export CV version contents - - :expectedresults: - - 1. Export fails with error and no export is created - - :BZ: 1998626, 2067275 - """ - - # Create custom product - product = make_product({'organization-id': module_org.id, 'name': gen_string('alpha')}) - - # Create repositories - repo_ondemand = make_repository( - { - 'download-policy': 'on_demand', - 'organization-id': module_org.id, - 'product-id': product['id'], - } - ) - repo_immediate = make_repository( - { - 'download-policy': 'immediate', - 'organization-id': module_org.id, - 'product-id': product['id'], - } - ) - - # Sync repositories - Repository.synchronize({'id': repo_ondemand['id']}) - Repository.synchronize({'id': repo_immediate['id']}) - - # Create cv and publish - cv = make_content_view({'name': gen_string('alpha'), 'organization-id': module_org.id}) - ContentView.add_repository( - { - 'id': cv['id'], - 'organization-id': module_org.id, - 'repository-id': repo_ondemand['id'], - } - ) - ContentView.add_repository( - { - 'id': cv['id'], - 'organization-id': module_org.id, - 'repository-id': repo_immediate['id'], - } - ) - ContentView.publish({'id': cv['id']}) - - # Verify export directory is empty - assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) == '' - - # Export Content View version - with pytest.raises(CLIReturnCodeError) as error: - ContentExport.completeVersion( - {'id': cv['id'], 'organization-id': module_org.id, 'fail-on-missing-content': True}, - output_format='base', # json output can't be parsed - BZ#1998626 - ) - # Error is raised - assert error.status != 0 - - # Export is not generated - assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) == '' - - @pytest.mark.tier2 - def test_positive_create_custom_major_minor_cv_version(self): - """CV can published with custom major and minor versions - - :id: 6697cd22-253a-4bdc-a108-7e0af22caaf4 - - :steps: - - 1. Create product and repository with custom contents - 2. Sync the repository - 3. Create CV with above repository - 4. Publish the CV with custom major and minor versions - - :expectedresults: - - 1. CV version with custom major and minor versions is created - - :CaseLevel: System - """ - org = make_org() - major = randint(1, 1000) - minor = randint(1, 1000) - content_view = make_content_view( - {'name': gen_string('alpha'), 'organization-id': org['id']} - ) - ContentView.publish({'id': content_view['id'], 'major': major, 'minor': minor}) - content_view = ContentView.info({'id': content_view['id']}) - cvv = content_view['versions'][0]['version'] - assert cvv.split('.')[0] == str(major) - assert cvv.split('.')[1] == str(minor) - @pytest.mark.tier3 def test_postive_export_cv_with_mixed_content_repos( self, class_export_entities, export_import_cleanup_module, target_sat, module_org @@ -1754,28 +1562,14 @@ def test_negative_import_redhat_cv_without_manifest( importing content." """ - # Setup rhel repo - cv_name = gen_string('alpha') - RepositorySet.enable( - { - 'basearch': 'x86_64', - 'name': REPOSET['rhva6'], - 'organization-id': function_entitlement_manifest_org.id, - 'product': PRDS['rhel'], - 'releasever': '6Server', - } + # Enable and sync RH repository + repo = _enable_rhel_content( + sat=target_sat, + org=function_entitlement_manifest_org, + repo_dict=REPOS['rhae2'], ) - repo = Repository.info( - { - 'name': REPOS['rhva6']['name'], - 'organization-id': function_entitlement_manifest_org.id, - 'product': PRDS['rhel'], - } - ) - # Update the download policy to 'immediate' and sync - Repository.update({'download-policy': 'immediate', 'id': repo['id']}) - Repository.synchronize({'id': repo['id']}, timeout=7200000) # Create cv and publish + cv_name = gen_string('alpha') cv = make_content_view( {'name': cv_name, 'organization-id': function_entitlement_manifest_org.id} ) @@ -1875,104 +1669,6 @@ def test_positive_import_content_for_disconnected_sat_with_existing_content( class TestInterSatelliteSync: """Implements InterSatellite Sync tests in CLI""" - @pytest.mark.stubbed - @pytest.mark.tier3 - def test_negative_export_cv(self): - """Export whole CV version contents is aborted due to insufficient - memory. - - :id: 4fa58c0c-95d2-45f5-a7fc-c5f3312a989c - - :steps: Attempt to Export whole CV version contents to a directory - which has less memory available than contents size. - - :expectedresults: The export CV version contents has been aborted due - to insufficient memory. - - :CaseAutomation: NotAutomated - - :CaseLevel: System - """ - - @pytest.mark.stubbed - @pytest.mark.tier3 - @pytest.mark.upgrade - def test_positive_export_import_cv_iso(self): - """Export CV version contents in directory as iso and Import it. - - :id: 5c39afd4-09d6-43c5-8d50-edc98105b7db - - :steps: - - 1. Export whole CV version contents as ISO - 2. Import these copied ISO to some other org/satellite. - - :expectedresults: - - 1. CV version has been exported to directory as ISO in specified in - settings. - 2. The exported ISO has been imported in org/satellite. - - :CaseAutomation: NotAutomated - - :CaseLevel: System - """ - - @pytest.mark.stubbed - @pytest.mark.tier3 - def test_negative_export_cv_iso(self): - """Export whole CV version to iso is aborted due to insufficient - memory. - - :id: ef84ffbd-c7cf-4d9a-9944-3c3b06a18872 - - :steps: Attempt to Export whole CV version as iso to a directory which - has less memory available than contents size. - - :expectedresults: The export CV version to iso has been aborted due to - insufficient memory. - - :CaseAutomation: NotAutomated - - :CaseLevel: System - """ - - @pytest.mark.stubbed - @pytest.mark.tier3 - def test_negative_export_cv_iso_max_size(self): - """Export whole CV version to iso is aborted due to inadequate maximum - iso size. - - :id: 93fe1cef-254b-484d-a628-bec56b356234 - - :steps: Attempt to Export whole CV version as iso with mb size less - than required. - - :expectedresults: The export CV version to iso has been aborted due to - maximum size is not enough to contain the CV version contents. - - :CaseAutomation: NotAutomated - - :CaseLevel: System - """ - - @pytest.mark.stubbed - @pytest.mark.tier3 - def test_positive_export_cv_iso_max_size(self): - """CV version exported to iso in maximum iso size. - - :id: 7ec91557-bafc-490d-b760-573a07389be5 - - :steps: Attempt to Export whole CV version as iso with mb size more - than required. - - :expectedresults: CV version has been exported to iso successfully. - - :CaseAutomation: NotAutomated - - :CaseLevel: System - """ - @pytest.mark.stubbed @pytest.mark.tier3 @pytest.mark.upgrade @@ -1982,44 +1678,20 @@ def test_positive_export_import_cv_incremental(self): :id: 3c4dfafb-fabf-406e-bca8-7af1ab551135 :steps: - 1. In upstream, Export CV version contents to a directory specified - in settings. - 2. In downstream, Import these copied contents from some other - org/satellite. - 3. In upstream, Add new packages to the CV. - 4. Export the CV incrementally from the last date time. + 1. In upstream, Export CV version contents to a directory specified in settings. + 2. In downstream, Import these copied contents from some other org/satellite. + 3. In upstream, don't add any new packages to the CV. + 4. Export the CV incrementally. 5. In downstream, Import the CV incrementally. + 6. In upstream, add new packages to the CV. + 7. Export the CV incrementally. + 8. In downstream, Import the CV incrementally. :expectedresults: 1. On incremental export, only the new packages are exported. - 2. New directory of incremental export with new packages is - created. - 3. On incremental import, only the new packages are imported. - - :CaseAutomation: NotAutomated - - :CaseLevel: System - """ - - @pytest.mark.stubbed - @pytest.mark.tier3 - def test_negative_export_import_cv_incremental(self): - """No new incremental packages exported or imported. - - :id: 90692d59-788c-4e18-add1-33db04204a4b - - :steps: - 1. In upstream, Export CV version contents to a directory specified - in settings. - 2. In downstream, Import these copied contents from some other - org/satellite. - 3. In upstream, Don't add any new packages to the CV. - 4. Export the CV incrementally from the last date time. - 5. In downstream, Import the CV incrementally. - - :expectedresults: - 1. An Empty packages directory created on incremental export. - 2. On incremental import, no new packages are imported. + 2. New directory of incremental export with new packages is created. + 3. On first incremental import, no new packages are imported. + 4. On second incremental import, only the new packages are imported. :CaseAutomation: NotAutomated @@ -2092,54 +1764,6 @@ def test_positive_export_import_kickstart_tree(self): :CaseLevel: System """ - @pytest.mark.stubbed - @pytest.mark.tier3 - def test_negative_import_kickstart_tree(self): - """Export whole kickstart tree in directory and Import nothing. - - :id: 55ddf6a6-b99a-4986-bdd3-7a5384f06915 - - :steps: - - 1. Export whole kickstart tree contents to a directory specified in - settings. - 2. Dont copy exported contents to /var/www/html/pub/export - directory. - 3. Attempt to import these not copied contents from some other - org/satellite. - - :expectedresults: - - 1. Whole kickstart tree has been exported to directory specified in - settings. - 2. The exported contents are not imported due to non availability. - 3. Error is thrown for non availability of kickstart tree to - import. - - :CaseAutomation: NotAutomated - - :CaseLevel: System - """ - - @pytest.mark.stubbed - @pytest.mark.tier3 - def test_negative_export_kickstart_tree(self): - """Export whole kickstart tree contents is aborted due to insufficient - memory. - - :id: 5f681f43-bac8-4196-9b3c-8b66b9c149f9 - - :steps: Attempt to Export whole kickstart tree contents to a directory - which has less memory available than contents size. - - :expectedresults: The export kickstart tree has been aborted due to - insufficient memory. - - :CaseAutomation: NotAutomated - - :CaseLevel: System - """ - @pytest.mark.stubbed @pytest.mark.tier3 def test_positive_export_redhat_incremental_yum_repo(self): @@ -2184,32 +1808,6 @@ def test_positive_export_import_redhat_incremental_yum_repo(self): :CaseLevel: System """ - @pytest.mark.stubbed - @pytest.mark.tier3 - @pytest.mark.upgrade - def test_positive_export_import_redhat_mix_cv(self): - """Export CV version having Red Hat and custom repo in directory - and Import them. - - :id: a38cf67d-563c-46f0-a263-4825b26faf2b - - :steps: - - 1. Export whole CV version having mixed repos to a path accessible - over HTTP. - 2. Import the Red Hat repository by defining the CDN URL from the - exported HTTP URL. - 3. Import custom repo by creating new repo and setting yum repo url - to exported HTTP url. - - :expectedresults: Both custom and Red Hat repos are imported - successfully. - - :CaseAutomation: NotAutomated - - :CaseLevel: System - """ - @pytest.mark.stubbed @pytest.mark.tier3 @pytest.mark.upgrade From 1142a60745206d6c1c4820184a2ee38eacbb3c3b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 15 Aug 2023 11:06:03 -0400 Subject: [PATCH 140/586] [6.14.z] Modify and delete foreman-maintain tests (#12237) --- tests/foreman/maintain/test_advanced.py | 59 +------- tests/foreman/maintain/test_backup_restore.py | 34 ++--- tests/foreman/maintain/test_health.py | 142 ++---------------- tests/foreman/maintain/test_packages.py | 3 + tests/foreman/maintain/test_service.py | 54 ------- 5 files changed, 29 insertions(+), 263 deletions(-) diff --git a/tests/foreman/maintain/test_advanced.py b/tests/foreman/maintain/test_advanced.py index a95e6b6adb9..49faf4e4730 100644 --- a/tests/foreman/maintain/test_advanced.py +++ b/tests/foreman/maintain/test_advanced.py @@ -165,6 +165,8 @@ def _finalize(): assert default_admin_pass in result.stdout +@pytest.mark.e2e +@pytest.mark.upgrade def test_positive_advanced_run_packages(request, sat_maintain): """Packages install/downgrade/check-update/update using advanced procedure run @@ -248,20 +250,6 @@ def test_positive_advanced_run_foreman_task_resume(sat_maintain): assert 'FAIL' not in result.stdout -def test_positive_advanced_run_foreman_tasks_ui_investigate(sat_maintain): - """Run foreman-tasks-ui-investigate using advanced procedure run - - :id: 3b4f69c6-c0a1-42e3-a099-8a6e26280d17 - - :steps: - 1. Run satellite-maintain advanced procedure run foreman-tasks-ui-investigate - - :expectedresults: procedure foreman-tasks-ui-investigate should work. - """ - result = sat_maintain.cli.Advanced.run_foreman_tasks_ui_investigate(env_var='echo " " | ') - assert result.status == 0 - - @pytest.mark.e2e def test_positive_advanced_run_sync_plan(setup_sync_plan, sat_maintain): """Run sync-plans-enable and sync-plans-disable using advanced procedure run @@ -295,49 +283,6 @@ def test_positive_advanced_run_sync_plan(setup_sync_plan, sat_maintain): assert len(enable_sync_ids) == len(data_yml[':default'][':sync_plans'][':enabled']) -@pytest.mark.include_capsule -def test_positive_advanced_by_tag_check_migrations(sat_maintain): - """Run pre-migrations and post-migrations using advanced procedure by-tag - - :id: 65cacca0-f142-4a63-a644-01f76120f16c - - :parametrized: yes - - :steps: - 1. Run satellite-maintain advanced procedure by-tag pre-migrations - 2. Run satellite-maintain advanced procedure by-tag post-migrations - - :expectedresults: procedures of pre-migrations tag and post-migrations tag should work. - """ - result = sat_maintain.cli.AdvancedByTag.pre_migrations() - assert 'FAIL' not in result.stdout - rules = sat_maintain.execute('nft list tables') - assert 'FOREMAN_MAINTAIN_TABLE' in rules.stdout - - result = sat_maintain.cli.AdvancedByTag.post_migrations() - assert 'FAIL' not in result.stdout - rules = sat_maintain.execute('nft list tables') - assert 'FOREMAN_MAINTAIN_TABLE' not in rules.stdout - - -@pytest.mark.include_capsule -def test_positive_advanced_by_tag_restore_confirmation(sat_maintain): - """Run restore_confirmation using advanced procedure by-tag - - :id: f9e10352-04fb-49ba-8346-5b02e64fd028 - - :parametrized: yes - - :steps: - 1. Run satellite-maintain advanced procedure by-tag restore - - :expectedresults: procedure restore_confirmation should work. - """ - result = sat_maintain.cli.AdvancedByTag.restore(options={'assumeyes': True}) - assert 'FAIL' not in result.stdout - assert result.status == 0 - - def test_positive_sync_plan_with_hammer_defaults(request, sat_maintain, module_org): """Verify that sync plan is disabled and enabled with hammer defaults set. diff --git a/tests/foreman/maintain/test_backup_restore.py b/tests/foreman/maintain/test_backup_restore.py index 29a48da2361..90e4a2c3cf3 100644 --- a/tests/foreman/maintain/test_backup_restore.py +++ b/tests/foreman/maintain/test_backup_restore.py @@ -159,7 +159,7 @@ def test_positive_backup_split_pulp_tar( @pytest.mark.include_capsule @pytest.mark.parametrize('backup_type', ['online', 'offline']) -def test_positive_backup_caspule_features( +def test_positive_backup_capsule_features( sat_maintain, setup_backup_tests, module_synced_repos, backup_type ): """Take a backup with capsule features as dns, tftp, dhcp, openscap @@ -368,29 +368,7 @@ def test_negative_backup_maintenance_mode(sat_maintain, setup_backup_tests): @pytest.mark.include_capsule -def test_negative_restore_nodir(sat_maintain, setup_backup_tests): - """Try to run restore with no source dir provided - - :id: dadc4e32-c0b8-427f-a449-4ae66fe09268 - - :parametrized: yes - - :steps: - 1. try to run restore with no path argument provided - - :expectedresults: - 1. should fail with appropriate error message - """ - result = sat_maintain.cli.Restore.run( - backup_dir='', - options={'assumeyes': True, 'plaintext': True}, - ) - assert result.status != 0 - assert NODIR_MSG in str(result.stderr) - - -@pytest.mark.include_capsule -def test_negative_restore_baddir(sat_maintain, setup_backup_tests): +def test_negative_restore_baddir_nodir(sat_maintain, setup_backup_tests): """Try to run restore with non-existing source dir provided :id: 65ccc0d0-ca43-4877-9b29-50037e378ca5 @@ -399,6 +377,7 @@ def test_negative_restore_baddir(sat_maintain, setup_backup_tests): :steps: 1. try to run restore with non-existing path provided + 2. try to run restore without a backup dir :expectedresults: 1. should fail with appropriate error message @@ -410,6 +389,12 @@ def test_negative_restore_baddir(sat_maintain, setup_backup_tests): ) assert result.status != 0 assert BADDIR_MSG in str(result.stdout) + result = sat_maintain.cli.Restore.run( + backup_dir='', + options={'assumeyes': True, 'plaintext': True}, + ) + assert result.status != 0 + assert NODIR_MSG in str(result.stderr) @pytest.mark.parametrize('backup_type', ['online', 'offline']) @@ -495,6 +480,7 @@ def test_positive_puppet_backup_restore( @pytest.mark.e2e +@pytest.mark.upgrade @pytest.mark.include_capsule @pytest.mark.parametrize('skip_pulp', [False, True], ids=['include_pulp', 'skip_pulp']) @pytest.mark.parametrize('backup_type', ['online', 'offline']) diff --git a/tests/foreman/maintain/test_health.py b/tests/foreman/maintain/test_health.py index f13286e9e4e..fe58b8e7c7e 100644 --- a/tests/foreman/maintain/test_health.py +++ b/tests/foreman/maintain/test_health.py @@ -83,6 +83,7 @@ def test_positive_list_health_check_by_tags(sat_maintain): @pytest.mark.e2e +@pytest.mark.upgrade @pytest.mark.include_capsule def test_positive_health_check(sat_maintain): """Verify satellite-maintain health check @@ -164,18 +165,21 @@ def test_positive_health_check_server_ping(sat_maintain): assert 'FAIL' not in result.stdout -def test_negative_health_check_server_ping(sat_maintain, request): - """Verify hammer ping check +def test_health_check_server_ping(sat_maintain, request): + """Verify health check server-ping :id: ecdc5bfb-2adf-49f6-948d-995dae34bcd3 :steps: - 1. Run satellite maintain service stop - 2. Run satellite-maintain health check --label server-ping - 3. Run satellite maintain service start + 1. Run satellite-maintain health check --label server-ping + 2. Run satellite-maintain service stop + 3. Run satellite-maintain health check --label server-ping :expectedresults: server-ping health check should pass """ + result = sat_maintain.cli.Health.check(options={'label': 'server-ping', 'assumeyes': True}) + assert result.status == 0 + assert 'FAIL' not in result.stdout assert sat_maintain.cli.Service.stop().status == 0 result = sat_maintain.cli.Health.check(options={'label': 'server-ping', 'assumeyes': True}) assert result.status == 0 @@ -187,7 +191,7 @@ def _finalize(): @pytest.mark.include_capsule -def test_positive_health_check_upstream_repository(sat_maintain, request): +def test_negative_health_check_upstream_repository(sat_maintain, request): """Verify upstream repository check :id: 349fcf33-2d25-4628-a6af-cff53e624b25 @@ -197,7 +201,7 @@ def test_positive_health_check_upstream_repository(sat_maintain, request): :steps: 1. Run satellite-maintain health check --label check-upstream-repository - :expectedresults: check-upstream-repository health check should pass. + :expectedresults: check-upstream-repository health check should fail. """ for name, url in upstream_url.items(): sat_maintain.create_custom_repos(**{name: url}) @@ -231,24 +235,13 @@ def test_positive_health_check_available_space(sat_maintain): :steps: 1. Run satellite-maintain health check --label available-space + 2. Run satellite-maintain health check --label available-space-cp :expectedresults: available-space health check should pass. """ result = sat_maintain.cli.Health.check(options={'label': 'available-space'}) assert 'FAIL' not in result.stdout assert result.status == 0 - - -def test_positive_health_check_available_space_candlepin(sat_maintain): - """Verify available-space-cp check - - :id: 382a2bf3-a3da-4e46-b370-a443450f93b7 - - :steps: - 1. Run satellite-maintain health check --label available-space-cp - - :expectedresults: available-space-cp health check should pass. - """ result = sat_maintain.cli.Health.check(options={'label': 'available-space-cp'}) assert 'FAIL' not in result.stdout assert result.status == 0 @@ -359,7 +352,7 @@ def test_positive_health_check_validate_yum_config(sat_maintain): @pytest.mark.include_capsule -def test_positive_health_check_epel_repository(request, sat_maintain): +def test_negative_health_check_epel_repository(request, sat_maintain): """Verify check-non-redhat-repository. :id: ce2d7278-d7b7-4f76-9923-79be831c0368 @@ -373,7 +366,7 @@ def test_positive_health_check_epel_repository(request, sat_maintain): :BZ: 1755755 - :expectedresults: check-non-redhat-repository health check should pass. + :expectedresults: check-non-redhat-repository health check should fail. """ epel_repo = 'https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm' sat_maintain.execute(f'dnf install -y {epel_repo}') @@ -520,81 +513,6 @@ def _finalize(): ) -def test_positive_health_check_postgresql_checkpoint_segments(sat_maintain): - """Verify check-postgresql-checkpoint-segments - - :id: 963a5b47-168a-4443-9fdf-bba59c9b0e97 - - :steps: - 1. Have an invalid /etc/foreman-installer/custom-hiera.yaml file - 2. Run satellite-maintain health check --label check-postgresql-checkpoint-segments. - 3. Assert that check-postgresql-checkpoint-segments gives proper - error message saying an invalid yaml file - 4. Make /etc/foreman-installer/custom-hiera.yaml file valid - 5. Add config_entries section in /etc/foreman-installer/custom-hiera.yaml - 6. Run satellite-maintain health check --label check-postgresql-checkpoint-segments. - 7. Assert that check-postgresql-checkpoint-segments fails. - 8. Add checkpoint_segments parameter in /etc/foreman-installer/custom-hiera.yaml - 9. Run satellite-maintain health check --label check-postgresql-checkpoint-segments. - 10. Assert that check-postgresql-checkpoint-segments fails. - 11. Remove config_entries section from /etc/foreman-installer/custom-hiera.yaml - 12. Run satellite-maintain health check --label check-postgresql-checkpoint-segments. - 13. Assert that check-postgresql-checkpoint-segments pass. - - :BZ: 1894149, 1899322 - - :customerscenario: true - - :expectedresults: check-postgresql-checkpoint-segments health check should pass. - - :CaseImportance: High - """ - custom_hiera = '/etc/foreman-installer/custom-hiera.yaml' - # Create invalid yaml file - sat_maintain.execute(f'sed -i "s/---/----/g" {custom_hiera}') - result = sat_maintain.cli.Health.check( - options={'label': 'check-postgresql-checkpoint-segments'} - ) - - assert f'File {custom_hiera} is not a yaml file.' in result.stdout - assert 'FAIL' in result.stdout - assert result.status == 1 - # Make yaml file valid - sat_maintain.execute(f'sed -i "s/----/---/g" {custom_hiera}') - # Add config_entries section - sat_maintain.execute(f'sed -i "$ a postgresql::server::config_entries:" {custom_hiera}') - # Run check-postgresql-checkpoint-segments check. - result = sat_maintain.cli.Health.check( - options={'label': 'check-postgresql-checkpoint-segments'} - ) - - assert "ERROR: 'postgresql::server::config_entries' cannot be null." in result.stdout - assert 'Please remove it from following file and re-run the command.' in result.stdout - assert result.status == 1 - assert 'FAIL' in result.stdout - # Add checkpoint_segments - sat_maintain.execute(fr'sed -i "$ a\ checkpoint_segments: 32" {custom_hiera}') - # Run check-postgresql-checkpoint-segments check. - result = sat_maintain.cli.Health.check( - options={'label': 'check-postgresql-checkpoint-segments'} - ) - - assert "ERROR: Tuning option 'checkpoint_segments' found." in result.stdout - assert 'Please remove it from following file and re-run the command.' in result.stdout - assert result.status == 1 - assert 'FAIL' in result.stdout - # Remove config_entries section - sat_maintain.execute( - fr'sed -i "/postgresql::server::config_entries\|checkpoint_segments: 32/d" {custom_hiera}' - ) - # Run check-postgresql-checkpoint-segments check. - result = sat_maintain.cli.Health.check( - options={'label': 'check-postgresql-checkpoint-segments'} - ) - assert result.status == 0 - assert 'FAIL' not in result.stdout - - @pytest.mark.include_capsule def test_positive_health_check_env_proxy(sat_maintain): """Verify env-proxy health check. @@ -778,38 +696,6 @@ def _finalize(): assert sat_maintain.execute('rm -fr /etc/yum.repos.d/custom_repo.repo').status == 0 -@pytest.mark.stubbed -def test_positive_health_check_available_space_postgresql12(): - """Verify warnings when available space in /var/opt/rh/rh-postgresql12/ - is less than consumed space of /var/lib/pgsql/ - - :id: 283e627d-6afc-49cb-afdb-5b77a91bbd1e - - :parametrized: yes - - :setup: - 1. Have some data under /var/lib/pgsql (upgrade templates have ~565Mib data) - 2. Create dir /var/opt/rh/rh-postgresql12/ and mount a partition of ~300Mib - to this dir (less than /var/lib/pgsql). - - :steps: - 1. satellite-maintain health check --label available-space-for-postgresql12 - 2. Verify Warning or Error is displayed when enough space is not - available under /var/opt/rh/rh-postgresql12/ - - :BZ: 1898108, 1973363 - - :expectedresults: Verify warnings when available space in /var/opt/rh/rh-postgresql12/ - is less than consumed space of /var/lib/pgsql/ - - :CaseImportance: High - - :customerscenario: true - - :CaseAutomation: ManualOnly - """ - - def test_positive_health_check_duplicate_permissions(sat_maintain): """Verify duplicate-permissions check diff --git a/tests/foreman/maintain/test_packages.py b/tests/foreman/maintain/test_packages.py index 74adba1bf33..a27fb510697 100644 --- a/tests/foreman/maintain/test_packages.py +++ b/tests/foreman/maintain/test_packages.py @@ -141,6 +141,9 @@ def test_positive_lock_package_versions_with_installer(sat_maintain): @pytest.mark.e2e +@pytest.mark.upgrade +@pytest.mark.pit_client +@pytest.mark.pit_server @pytest.mark.include_capsule def test_positive_fm_packages_install(request, sat_maintain): """Verify whether packages install/update work as expected. diff --git a/tests/foreman/maintain/test_service.py b/tests/foreman/maintain/test_service.py index 9886d368761..ed6f9262ff7 100644 --- a/tests/foreman/maintain/test_service.py +++ b/tests/foreman/maintain/test_service.py @@ -18,13 +18,11 @@ """ import pytest from fauxfactory import gen_string -from wait_for import wait_for from robottelo.config import settings from robottelo.constants import HAMMER_CONFIG from robottelo.constants import MAINTAIN_HAMMER_YML from robottelo.constants import SATELLITE_ANSWER_FILE -from robottelo.hosts import Satellite pytestmark = pytest.mark.destructive @@ -68,7 +66,6 @@ def test_positive_service_list(sat_maintain): assert result.status == 0 -@pytest.mark.e2e @pytest.mark.include_capsule def test_positive_service_stop_start(sat_maintain): """Start/Stop services using satellite-maintain service subcommand @@ -99,7 +96,6 @@ def test_positive_service_stop_start(sat_maintain): assert result.status == 0 -@pytest.mark.e2e @pytest.mark.include_capsule def test_positive_service_stop_restart(sat_maintain): """Disable services using satellite-maintain service @@ -145,10 +141,6 @@ def test_positive_service_enable_disable(sat_maintain): 2. Run satellite-maintain service enable :expectedresults: Services should enable/disable - - :BZ: 1995783 - - :customerscenario: true """ result = sat_maintain.cli.Service.stop() assert 'FAIL' not in result.stdout @@ -159,27 +151,6 @@ def test_positive_service_enable_disable(sat_maintain): result = sat_maintain.cli.Service.enable() assert 'FAIL' not in result.stdout assert result.status == 0 - sat_maintain.power_control(state='reboot') - if type(sat_maintain) is Satellite: - result, _ = wait_for( - sat_maintain.cli.Service.status, - func_kwargs={'options': {'brief': True, 'only': 'foreman.service'}}, - fail_condition=lambda res: "FAIL" in res.stdout, - handle_exception=True, - delay=30, - timeout=300, - ) - else: - result, _ = wait_for( - sat_maintain.cli.Service.status, - func_kwargs={'options': {'brief': True}}, - fail_condition=lambda res: "FAIL" in res.stdout, - handle_exception=True, - delay=30, - timeout=300, - ) - assert 'FAIL' not in result.stdout - assert result.status == 0 def test_positive_foreman_service(request, sat_maintain): @@ -244,31 +215,6 @@ def test_positive_service_restart_without_hammer_config(missing_hammer_config, s assert result.status == 0 -def test_positive_satellite_maintain_service_list_sidekiq(sat_maintain): - """List sidekiq services with service list - - :id: 5acb68a9-c430-485d-bb45-b499adc90927 - - :steps: - 1. Run satellite-maintain service list - 2. Run satellite-maintain service restart - - :expectedresults: Sidekiq services should list and should restart. - - :CaseImportance: Medium - """ - result = sat_maintain.cli.Service.list() - assert 'FAIL' not in result.stdout - assert result.status == 0 - assert 'dynflow-sidekiq@.service' in result.stdout - - result = sat_maintain.cli.Service.restart() - assert 'FAIL' not in result.stdout - assert result.status == 0 - for service in ['orchestrator', 'worker', 'worker-hosts-queue']: - assert f'dynflow-sidekiq@{service}' in result.stdout - - def test_positive_status_rpmsave(request, sat_maintain): """Verify satellite-maintain service status doesn't contain any backup files like .rpmsave, or any file with .yml which don't exist as services. From 2524179d3807ed2218e23913dc02fcf6be3cd006 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 15 Aug 2023 11:28:04 -0400 Subject: [PATCH 141/586] [6.14.z] Switch CLIFactoryError import for cli/test_capsule.py (#12240) --- tests/foreman/cli/test_capsule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/cli/test_capsule.py b/tests/foreman/cli/test_capsule.py index 30c3efcba7a..67f4dac627f 100644 --- a/tests/foreman/cli/test_capsule.py +++ b/tests/foreman/cli/test_capsule.py @@ -21,8 +21,8 @@ from fauxfactory import gen_string from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import CLIFactoryError from robottelo.cli.proxy import Proxy +from robottelo.host_helpers.cli_factory import CLIFactoryError from robottelo.utils.datafactory import parametrized from robottelo.utils.datafactory import valid_data_list from robottelo.utils.issue_handlers import is_open From 8556c92d1b156fa4bad93aeacd97961f49a51ad5 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 16 Aug 2023 02:58:46 -0400 Subject: [PATCH 142/586] [6.14.z] Bump redis from 4.6.0 to 5.0.0 (#12249) Bump redis from 4.6.0 to 5.0.0 (#12247) (cherry picked from commit ecdc1e9ac5a737129ec213f0c089581dff55667f) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index fecd9ed8f91..16f9e33653d 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -1,7 +1,7 @@ # For running tests and checking code quality using these modules. flake8==6.1.0 pytest-cov==4.1.0 -redis==4.6.0 +redis==5.0.0 pre-commit==3.3.3 # For generating documentation. From a971f3696a42451a757449725c0eeea56dda4997 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 16 Aug 2023 03:03:45 -0400 Subject: [PATCH 143/586] [6.14.z] Bump tenacity from 8.2.2 to 8.2.3 (#12232) Bump tenacity from 8.2.2 to 8.2.3 (#12231) (cherry picked from commit 7e60918315a729fbe12d2774cff838b3e5bb138d) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a1178350f05..2888594b554 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ pytest-xdist==3.3.1 pytest-ibutsu==2.2.4 PyYAML==6.0.1 requests==2.31.0 -tenacity==8.2.2 +tenacity==8.2.3 testimony==2.3.0 wait-for==1.2.0 wrapanapi==3.5.18 From d2e44d2a17d1cd0ec51067314541b8a5ac4bf948 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 16 Aug 2023 03:23:40 -0400 Subject: [PATCH 144/586] [6.14.z] Fix conditional in capsule update_name test (#12256) --- tests/foreman/cli/test_capsule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/cli/test_capsule.py b/tests/foreman/cli/test_capsule.py index 67f4dac627f..bc47379c515 100644 --- a/tests/foreman/cli/test_capsule.py +++ b/tests/foreman/cli/test_capsule.py @@ -124,7 +124,7 @@ def test_positive_update_name(request, target_sat): """ proxy = _make_proxy(request, target_sat, options={'name': gen_alphanumeric()}) valid_data = valid_data_list() - if is_open('BZ:2084661') and 'html' in request.node.name: + if is_open('BZ:2084661') and 'html' in valid_data: del valid_data['html'] for new_name in valid_data.values(): newport = target_sat.available_capsule_port From 0b0dcf83bc6040c10ffd841712ef1cdb8fd0256a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 16 Aug 2023 04:17:46 -0400 Subject: [PATCH 145/586] [6.14.z] Component Audit for Discovery Plugin - 1 (#12257) --- tests/foreman/cli/test_discoveryrule.py | 496 ++++++++---------------- 1 file changed, 166 insertions(+), 330 deletions(-) diff --git a/tests/foreman/cli/test_discoveryrule.py b/tests/foreman/cli/test_discoveryrule.py index 66ffc5a49e6..e51468fbbf5 100644 --- a/tests/foreman/cli/test_discoveryrule.py +++ b/tests/foreman/cli/test_discoveryrule.py @@ -29,12 +29,8 @@ from requests import HTTPError from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.discoveryrule import DiscoveryRule from robottelo.cli.factory import CLIFactoryError from robottelo.cli.factory import make_discoveryrule -from robottelo.cli.factory import make_hostgroup -from robottelo.cli.factory import make_location -from robottelo.cli.factory import make_org from robottelo.logging import logger from robottelo.utils.datafactory import filtered_datapoint from robottelo.utils.datafactory import invalid_values_list @@ -46,7 +42,7 @@ def invalid_hostnames_list(): """Generates a list of invalid host names. - :return: Returns the invalid host names list + :return: Returns the invalid host names list. """ return { 'cjk': gen_string('cjk'), @@ -103,20 +99,22 @@ def _create_discoveryrule(org, loc, hostgroup, options=None): @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(valid_data_list())) - def test_positive_create_with_name(self, name, discoveryrule_factory, request, target_sat): + def test_positive_create_with_name(self, name, discoveryrule_factory, target_sat): """Create Discovery Rule using different names :id: 066e66bc-c572-4ae9-b458-90daf83bab54 - :expectedresults: Rule should be successfully created + :expectedresults: Rule should be successfully created. :CaseImportance: Critical :parametrized: yes """ rule = discoveryrule_factory(options={'name': name, 'priority': gen_int32()}) - request.addfinalizer(target_sat.api.DiscoveryRule(id=rule.id).delete) assert rule.name == name + target_sat.cli.DiscoveryRule.delete({'id': rule.id}) + with pytest.raises(CLIReturnCodeError): + target_sat.cli.DiscoveryRule.info({'id': rule.id}) @pytest.mark.tier1 def test_positive_create_with_search(self, discoveryrule_factory): @@ -132,6 +130,9 @@ def test_positive_create_with_search(self, discoveryrule_factory): search_query = 'cpu_count = 2' rule = discoveryrule_factory(options={'search': search_query}) assert rule.search == search_query + custom_query = 'processor = x86' + rule = discoveryrule_factory(options={'search': custom_query}) + assert rule.search == custom_query @pytest.mark.tier2 def test_positive_create_with_hostname(self, discoveryrule_factory): @@ -149,14 +150,14 @@ def test_positive_create_with_hostname(self, discoveryrule_factory): assert rule['hostname-template'] == host_name @pytest.mark.tier1 - def test_positive_create_with_org_loc_id( - self, discoveryrule_factory, class_org, class_location, class_hostgroup + def test_positive_create_and_update_with_org_loc_id( + self, discoveryrule_factory, class_org, class_location, class_hostgroup, target_sat ): - """Create discovery rule by associating org and location ids + """Create discovery rule by associating org and location ids and update :id: bdb4c581-d27a-4d1a-920b-89689e68a57f - :expectedresults: Rule was created and with given org & location names. + :expectedresults: Rule was created with given org & location ids and updated. :BZ: 1377990, 1523221 @@ -172,15 +173,33 @@ def test_positive_create_with_org_loc_id( assert class_org.name in rule.organizations assert class_location.name in rule.locations + new_org = target_sat.cli_factory.make_org() + new_loc = target_sat.cli_factory.make_location() + new_hostgroup = target_sat.cli_factory.make_hostgroup( + {'organization-ids': new_org.id, 'location-ids': new_loc.id} + ) + target_sat.cli.DiscoveryRule.update( + { + 'id': rule.id, + 'organization-ids': new_org.id, + 'location-ids': new_loc.id, + 'hostgroup-id': new_hostgroup.id, + } + ) + rule = target_sat.cli.DiscoveryRule.info({'id': rule.id}, output_format='json') + assert new_org.name == rule['organizations'][0]['name'] + assert new_loc.name == rule['locations'][0]['name'] + assert new_hostgroup.name == rule['host-group']['name'] + @pytest.mark.tier2 - def test_positive_create_with_org_loc_name( - self, discoveryrule_factory, class_org, class_location, class_hostgroup + def test_positive_create_and_update_with_org_loc_name( + self, discoveryrule_factory, class_org, class_location, class_hostgroup, target_sat ): - """Create discovery rule by associating org and location names + """Create discovery rule by associating org and location names and update :id: f0d550ae-16d8-48ec-817e-d2e5b7405b46 - :expectedresults: Rule was created and with given org & location names. + :expectedresults: Rule was created and with given org & location names and updated :BZ: 1377990 """ @@ -194,6 +213,25 @@ def test_positive_create_with_org_loc_name( assert class_org.name in rule.organizations assert class_location.name in rule.locations + new_org = target_sat.cli_factory.make_org() + new_loc = target_sat.cli_factory.make_location() + new_hostgroup = target_sat.cli_factory.make_hostgroup( + {'organization-ids': new_org.id, 'location-ids': new_loc.id} + ) + + target_sat.cli.DiscoveryRule.update( + { + 'id': rule.id, + 'organizations': new_org.name, + 'locations': new_loc.name, + 'hostgroup-id': new_hostgroup.id, + } + ) + rule = target_sat.cli.DiscoveryRule.info({'id': rule.id}, output_format='json') + assert new_org.name == rule['organizations'][0]['name'] + assert new_loc.name == rule['locations'][0]['name'] + assert new_hostgroup.name == rule['host-group']['name'] + @pytest.mark.tier2 def test_positive_create_with_hosts_limit(self, discoveryrule_factory): """Create Discovery Rule providing any number from range 1..100 for @@ -211,21 +249,27 @@ def test_positive_create_with_hosts_limit(self, discoveryrule_factory): assert rule['hosts-limit'] == hosts_limit @pytest.mark.tier1 - def test_positive_create_with_priority(self, discoveryrule_factory): + def test_positive_create_and_update_with_priority(self, discoveryrule_factory, target_sat): """Create Discovery Rule providing any number from range 1..100 for - priority option + priority option and update :id: 8ef58279-0ad3-41a4-b8dd-65594afdb655 - :expectedresults: Rule should be successfully created and has expected + :expectedresults: Rule should be successfully created/updated and has expected priority value :CaseImportance: Critical """ - available = set(range(1, 1000)) - {int(Box(r).priority) for r in DiscoveryRule.list()} + available = set(range(1, 1000)) - { + int(Box(r).priority) for r in target_sat.cli.DiscoveryRule.list() + } rule_priority = random.sample(sorted(available), 1) rule = discoveryrule_factory(options={'priority': rule_priority[0]}) assert rule.priority == str(rule_priority[0]) + # Update + target_sat.cli.DiscoveryRule.update({'id': rule.id, 'priority': rule_priority[0]}) + rule = Box(target_sat.cli.DiscoveryRule.info({'id': rule.id})) + assert rule.priority == str(rule_priority[0]) @pytest.mark.tier2 def test_positive_create_disabled_rule(self, discoveryrule_factory): @@ -307,189 +351,45 @@ def test_negative_create_with_same_name(self, discoveryrule_factory): with pytest.raises(CLIFactoryError): discoveryrule_factory(options={'name': name}) - @pytest.mark.tier1 - def test_positive_delete(self, discoveryrule_factory): - """Delete existing Discovery Rule - - :id: c9b88a94-13c4-496f-a5c1-c088187250dc - - :expectedresults: Rule should be successfully deleted - - :CaseImportance: Critical - """ - rule = discoveryrule_factory() - DiscoveryRule.delete({'id': rule.id}) - with pytest.raises(CLIReturnCodeError): - DiscoveryRule.info({'id': rule.id}) - @pytest.mark.tier3 - def test_positive_update_name(self, discoveryrule_factory): - """Update discovery rule name + def test_positive_update_discovery_params(self, discoveryrule_factory, class_org, target_sat): + """Update discovery rule parameters :id: 1045e2c4-e1f7-42c9-95f7-488fc79bf70b - :expectedresults: Rule name is updated + :expectedresults: Rule params are updated :CaseLevel: Component :CaseImportance: Medium """ - rule = discoveryrule_factory() - new_name = gen_string('numeric') - DiscoveryRule.update({'id': rule.id, 'name': new_name}) - rule = Box(DiscoveryRule.info({'id': rule.id})) - assert rule.name == new_name - - @pytest.mark.tier2 - def test_positive_update_org_loc_by_id(self, discoveryrule_factory): - """Update org and location of selected discovery rule using org/loc ids - - :id: 26da79aa-30e5-4052-98ae-141de071a68a - - :expectedresults: Rule was updated and with given org & location. - - :BZ: 1377990 - - :CaseLevel: Component - """ - new_org = Box(make_org()) - new_loc = Box(make_location()) - new_hostgroup = Box( - make_hostgroup({'organization-ids': new_org.id, 'location-ids': new_loc.id}) - ) - rule = discoveryrule_factory() - DiscoveryRule.update( - { - 'id': rule.id, - 'organization-ids': new_org.id, - 'location-ids': new_loc.id, - 'hostgroup-id': new_hostgroup.id, - } - ) - rule = Box(DiscoveryRule.info({'id': rule.id})) - assert new_org.name in rule.organizations - assert new_loc.name in rule.locations - - @pytest.mark.tier3 - def test_positive_update_org_loc_by_name(self, discoveryrule_factory): - """Update org and location of selected discovery rule using org/loc - names - - :id: 7a5d61ac-6a2d-48f6-a00d-df437a7dc3c4 - - :expectedresults: Rule was updated and with given org & location. - - :BZ: 1377990 - - :CaseLevel: Component + rule = discoveryrule_factory(options={'hosts-limit': '5'}) + new_name = gen_string('alpha') + new_query = 'model = KVM' + new_hostname = gen_string('alpha') + new_limit = '10' + new_hostgroup = target_sat.cli_factory.make_hostgroup({'organization-ids': class_org.id}) - :CaseImportance: Medium - """ - new_org = Box(make_org()) - new_loc = Box(make_location()) - new_hostgroup = Box( - make_hostgroup({'organization-ids': new_org.id, 'location-ids': new_loc.id}) - ) - rule = discoveryrule_factory() - DiscoveryRule.update( + target_sat.cli.DiscoveryRule.update( { 'id': rule.id, - 'organizations': new_org.name, - 'locations': new_loc.name, - 'hostgroup-id': new_hostgroup.id, + 'name': new_name, + 'search': new_query, + 'hostgroup': new_hostgroup.name, + 'hostname': new_hostname, + 'hosts-limit': new_limit, } ) - rule = Box(DiscoveryRule.info({'id': rule.id})) - assert new_org.name in rule.organizations - assert new_loc.name in rule.locations - - @pytest.mark.tier2 - def test_positive_update_query(self, discoveryrule_factory): - """Update discovery rule search query - - :id: 86943095-acc5-40ff-8e3c-88c76b36333d - - :expectedresults: Rule search field is updated - :CaseLevel: Component - """ - rule = discoveryrule_factory() - new_query = 'model = KVM' - DiscoveryRule.update({'id': rule.id, 'search': new_query}) - rule = Box(DiscoveryRule.info({'id': rule.id})) + rule = Box(target_sat.cli.DiscoveryRule.info({'id': rule.id})) + assert rule.name == new_name assert rule.search == new_query - - @pytest.mark.tier2 - def test_positive_update_hostgroup(self, discoveryrule_factory, class_org): - """Update discovery rule host group - - :id: 07992a3f-2aa9-4e45-b2e8-ef3d2f255292 - - :expectedresults: Rule host group is updated - - :CaseLevel: Component - """ - new_hostgroup = Box(make_hostgroup({'organization-ids': class_org.id})) - rule = discoveryrule_factory() - DiscoveryRule.update({'id': rule.id, 'hostgroup': new_hostgroup.name}) - rule = DiscoveryRule.info({'id': rule.id}) assert rule['host-group'] == new_hostgroup.name - - @pytest.mark.tier2 - def test_positive_update_hostname(self, discoveryrule_factory): - """Update discovery rule hostname value - - - :id: 4c123488-92df-42f6-afe3-8a88cd90ffc2 - - :expectedresults: Rule host name is updated - - :CaseLevel: Component - """ - new_hostname = gen_string('alpha') - rule = discoveryrule_factory() - DiscoveryRule.update({'id': rule.id, 'hostname': new_hostname}) - rule = Box(DiscoveryRule.info({'id': rule.id})) assert rule['hostname-template'] == new_hostname - - @pytest.mark.tier2 - def test_positive_update_limit(self, discoveryrule_factory): - """Update discovery rule limit value - - :id: efa6f5bc-4d56-4449-90f5-330affbcfb09 - - :expectedresults: Rule host limit field is updated - - :CaseLevel: Component - """ - rule = discoveryrule_factory(options={'hosts-limit': '5'}) - new_limit = '10' - DiscoveryRule.update({'id': rule.id, 'hosts-limit': new_limit}) - rule = Box(DiscoveryRule.info({'id': rule.id})) assert rule['hosts-limit'] == new_limit @pytest.mark.tier1 - def test_positive_update_priority(self, discoveryrule_factory): - """Update discovery rule priority value - - :id: 0543cc73-c692-4bbf-818b-37353ec98986 - - :expectedresults: Rule priority is updated - - :CaseImportance: Critical - """ - available = set(range(1, 1000)) - {int(Box(r).priority) for r in DiscoveryRule.list()} - rule_priority = random.sample(sorted(available), 1) - rule = discoveryrule_factory(options={'priority': rule_priority[0]}) - assert rule.priority == str(rule_priority[0]) - available = set(range(1, 1000)) - {int(Box(r).priority) for r in DiscoveryRule.list()} - rule_priority = random.sample(sorted(available), 1) - DiscoveryRule.update({'id': rule.id, 'priority': rule_priority[0]}) - rule = Box(DiscoveryRule.info({'id': rule.id})) - assert rule.priority == str(rule_priority[0]) - - @pytest.mark.tier1 - def test_positive_update_disable_enable(self, discoveryrule_factory): + def test_positive_update_disable_enable(self, discoveryrule_factory, target_sat): """Update discovery rule enabled state. (Disabled->Enabled) :id: 64e8b21b-2ab0-49c3-a12d-02dbdb36647a @@ -500,18 +400,18 @@ def test_positive_update_disable_enable(self, discoveryrule_factory): """ rule = discoveryrule_factory(options={'enabled': 'false'}) assert rule.enabled == 'false' - DiscoveryRule.update({'id': rule.id, 'enabled': 'true'}) - rule = Box(DiscoveryRule.info({'id': rule.id})) + target_sat.cli.DiscoveryRule.update({'id': rule.id, 'enabled': 'true'}) + rule = Box(target_sat.cli.DiscoveryRule.info({'id': rule.id})) assert rule.enabled == 'true' @pytest.mark.tier3 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) - def test_negative_update_name(self, name, discoveryrule_factory): - """Update discovery rule name using invalid names only + def test_negative_update_discovery_params(self, name, discoveryrule_factory, target_sat): + """Update discovery rule name using invalid parameters :id: 8293cc6a-d983-460a-b76e-221ad02b54b7 - :expectedresults: Rule name is not updated + :expectedresults: Rule params are not updated :CaseLevel: Component @@ -520,58 +420,37 @@ def test_negative_update_name(self, name, discoveryrule_factory): :parametrized: yes """ rule = discoveryrule_factory() - with pytest.raises(CLIReturnCodeError): - DiscoveryRule.update({'id': rule.id, 'name': name}) - - @pytest.mark.tier3 - def test_negative_update_hostname(self, discoveryrule_factory): - """Update discovery rule host name using number as a value - - :id: c382dbc7-9509-4060-9038-1617f7fef038 - - :expectedresults: Rule host name is not updated - - :CaseImportance: Medium - - :CaseLevel: Component - """ - rule = discoveryrule_factory() - with pytest.raises(CLIReturnCodeError): - DiscoveryRule.update({'id': rule.id, 'hostname': '$#@!*'}) - - @pytest.mark.tier3 - def test_negative_update_limit(self, discoveryrule_factory): - """Update discovery rule host limit using invalid values - - :id: e3257d8a-91b9-406f-bd74-0fd1fb05bb77 - - :expectedresults: Rule host limit is not updated - - :CaseLevel: Component - - :CaseImportance: Medium - """ - rule = discoveryrule_factory() + priority = gen_string('alpha') host_limit = gen_string('alpha') + params = { + 'name': name, + 'hostname': '$#@!*', + 'hosts-limit': host_limit, + 'priority': priority, + } + key = random.choice(list(params.keys())) with pytest.raises(CLIReturnCodeError): - DiscoveryRule.update({'id': rule.id, 'hosts-limit': host_limit}) - - @pytest.mark.tier3 - def test_negative_update_priority(self, discoveryrule_factory): - """Update discovery rule priority using invalid values + target_sat.cli.DiscoveryRule.update( + { + 'id': rule.id, + key: params[key], + } + ) - :id: 0778dd00-aa19-4062-bdf3-752e1b546ec2 + @pytest.mark.tier1 + def test_positive_delete(self, discoveryrule_factory, target_sat): + """Delete existing Discovery Rule - :expectedresults: Rule priority is not updated + :id: c9b88a94-13c4-496f-a5c1-c088187250dc - :CaseLevel: Component + :expectedresults: Rule should be successfully deleted - :CaseImportance: Medium + :CaseImportance: Critical """ rule = discoveryrule_factory() - priority = gen_string('alpha') + target_sat.cli.DiscoveryRule.delete({'id': rule.id}) with pytest.raises(CLIReturnCodeError): - DiscoveryRule.update({'id': rule.id, 'priority': priority}) + target_sat.cli.DiscoveryRule.info({'id': rule.id}) class TestDiscoveryRuleRole: @@ -592,8 +471,9 @@ def class_user_manager(self, class_user_password, class_org, class_location): yield user try: user.delete() - except HTTPError: - logger.exception('Exception while deleting class scope user entity in teardown') + except HTTPError as err: + logger.exception(err) + logger.error('Exception while deleting class scope user entity in teardown') @pytest.fixture(scope='class') def class_user_reader(self, class_user_password, class_org, class_location): @@ -610,24 +490,34 @@ def class_user_reader(self, class_user_password, class_org, class_location): yield user try: user.delete() - except HTTPError: - logger.exception('Exception while deleting class scope user entity in teardown') + except HTTPError as err: + logger.exception(err) + logger.error('Exception while deleting class scope user entity in teardown') @pytest.mark.tier2 - def test_positive_create_rule_with_non_admin_user( - self, class_org, class_location, class_user_password, class_user_manager, class_hostgroup + def test_positive_crud_with_non_admin_user( + self, + class_org, + class_location, + class_user_password, + class_user_manager, + class_hostgroup, + target_sat, ): - """Create rule with non-admin user by associating discovery_manager role + """Create, update and delete rule with non-admin user by associating discovery_manager role :id: 056535aa-3338-4c1e-8a4b-ebfc8bd6e456 - :expectedresults: Rule should be created successfully. + :expectedresults: Rule should be created and deleted successfully. :CaseLevel: Integration """ rule_name = gen_string('alpha') + new_name = gen_string('alpha') rule = Box( - DiscoveryRule.with_user(class_user_manager.login, class_user_password).create( + target_sat.cli.DiscoveryRule.with_user( + class_user_manager.login, class_user_password + ).create( { 'name': rule_name, 'search': 'cpu_count = 5', @@ -638,90 +528,37 @@ def test_positive_create_rule_with_non_admin_user( ) ) rule = Box( - DiscoveryRule.with_user(class_user_manager.login, class_user_password).info( - {'id': rule.id} - ) + target_sat.cli.DiscoveryRule.with_user( + class_user_manager.login, class_user_password + ).info({'id': rule.id}) ) assert rule.name == rule_name - @pytest.mark.tier2 - def test_positive_delete_rule_with_non_admin_user( - self, class_org, class_location, class_user_manager, class_hostgroup, class_user_password - ): - """Delete rule with non-admin user by associating discovery_manager role - - :id: 87ab969b-7d92-478d-a5c0-1c0d50e9bdd6 - - :expectedresults: Rule should be deleted successfully. - - :CaseLevel: Integration - """ - rule_name = gen_string('alpha') - rule = Box( - DiscoveryRule.with_user(class_user_manager.login, class_user_password).create( - { - 'name': rule_name, - 'search': 'cpu_count = 5', - 'organizations': class_org.name, - 'locations': class_location.name, - 'hostgroup-id': class_hostgroup.id, - } - ) - ) - rule = Box( - DiscoveryRule.with_user(class_user_manager.login, class_user_password).info( - {'id': rule.id} - ) - ) - - DiscoveryRule.with_user(class_user_manager.login, class_user_password).delete( - {'id': rule.id} + target_sat.cli.DiscoveryRule.update( + { + 'id': rule.id, + 'name': new_name, + } ) - with pytest.raises(CLIReturnCodeError): - DiscoveryRule.info({'id': rule.id}) - - @pytest.mark.tier2 - def test_positive_view_existing_rule_with_non_admin_user( - self, class_org, class_location, class_user_password, class_user_reader, class_hostgroup - ): - """Existing rule should be viewed to non-admin user by associating - discovery_reader role. - - :id: 7b1d90b9-fc2d-4ccb-93d3-605c2da876f7 - - :Steps: - 1. create a rule with admin user - 2. create a non-admin user and assign 'Discovery Reader' role - 3. Login with non-admin user - - :expectedresults: Rule should be visible to non-admin user. + rule = Box(target_sat.cli.DiscoveryRule.info({'id': rule.id})) + assert rule.name == new_name - :CaseLevel: Integration - """ - rule_name = gen_string('alpha') - rule = Box( - make_discoveryrule( - { - 'name': rule_name, - 'enabled': 'false', - 'search': "last_report = Today", - 'organizations': class_org.name, - 'locations': class_location.name, - 'hostgroup-id': class_hostgroup.id, - } - ) - ) - rule = Box( - DiscoveryRule.with_user(class_user_reader.login, class_user_password).info( - {'id': rule.id} - ) - ) - assert rule.name == rule_name + target_sat.cli.DiscoveryRule.with_user( + class_user_manager.login, class_user_password + ).delete({'id': rule.id}) + with pytest.raises(CLIReturnCodeError): + target_sat.cli.DiscoveryRule.info({'id': rule.id}) @pytest.mark.tier2 def test_negative_delete_rule_with_non_admin_user( - self, class_org, class_location, class_user_password, class_user_reader, class_hostgroup + self, + class_org, + class_location, + class_user_password, + class_user_reader, + class_hostgroup, + target_sat, ): """Delete rule with non-admin user by associating discovery_reader role @@ -732,23 +569,22 @@ def test_negative_delete_rule_with_non_admin_user( :CaseLevel: Integration """ - rule = Box( - make_discoveryrule( - { - 'enabled': 'false', - 'search': "last_report = Today", - 'organizations': class_org.name, - 'locations': class_location.name, - 'hostgroup-id': class_hostgroup.id, - } - ) + rule = target_sat.cli_factory.make_discoveryrule( + { + 'enabled': 'false', + 'search': "last_report = Today", + 'organizations': class_org.name, + 'locations': class_location.name, + 'hostgroup-id': class_hostgroup.id, + } ) + rule = Box( - DiscoveryRule.with_user(class_user_reader.login, class_user_password).info( - {'id': rule.id} - ) + target_sat.cli.DiscoveryRule.with_user( + class_user_reader.login, class_user_password + ).info({'id': rule.id}) ) with pytest.raises(CLIReturnCodeError): - DiscoveryRule.with_user(class_user_reader.login, class_user_password).delete( - {'id': rule.id} - ) + target_sat.cli.DiscoveryRule.with_user( + class_user_reader.login, class_user_password + ).delete({'id': rule.id}) From 795bfa0c1161bda8227b311c6fa298d91d9df2d9 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 16 Aug 2023 10:52:04 -0400 Subject: [PATCH 146/586] [6.14.z] Fixed failing test for recreating repositories (#12261) Fixed failing test for recreating repositories (#12246) Fixed failing test for recreating repos (cherry picked from commit 915ab05b3a07a8e22321d3a94180976f8c74aab0) Co-authored-by: Cole Higgins --- tests/foreman/api/test_repository.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index 7a9efeb88af..b12c7389cc3 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -1188,8 +1188,8 @@ def test_positive_recreate_pulp_repositories(self, module_entitlement_manifest_o identifier = results.stdout.split('version_href\n"', 1)[1].split('version')[0] target_sat.execute( f'curl -X DELETE {target_sat.url}/{identifier}' - f' --cert /etc/pki/katello/certs/pulp-client.crt' - f' --key /etc/pki/katello/private/pulp-client.key' + f' --cert /etc/foreman/client_cert.pem' + f' --key /etc/foreman/client_key.pem' ) command_output = target_sat.execute('foreman-rake katello:correct_repositories COMMIT=true') assert 'Recreating' in command_output.stdout and 'TaskError' not in command_output.stdout From 09d80ab2084c16265ec4680e6750a188b39b58b8 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 17 Aug 2023 05:05:37 -0400 Subject: [PATCH 147/586] [6.14.z] Bump sphinx from 7.1.2 to 7.2.0 (#12273) Bump sphinx from 7.1.2 to 7.2.0 (#12271) (cherry picked from commit ed14b13045617d4a32174043af1f1bc7e09d90b2) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 16f9e33653d..b2239ac5e21 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -5,7 +5,7 @@ redis==5.0.0 pre-commit==3.3.3 # For generating documentation. -sphinx==7.1.2 +sphinx==7.2.0 sphinx-autoapi==2.1.1 # For 'manage' interactive shell From ae7b96f4e51ec452337921ced5f8aa93821e3371 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 17 Aug 2023 09:00:20 -0400 Subject: [PATCH 148/586] [6.14.z] Fix infoblox test (#12278) --- tests/foreman/destructive/test_infoblox.py | 52 ++++++++++++---------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/tests/foreman/destructive/test_infoblox.py b/tests/foreman/destructive/test_infoblox.py index c54b3fad31e..11cef9636e3 100644 --- a/tests/foreman/destructive/test_infoblox.py +++ b/tests/foreman/destructive/test_infoblox.py @@ -22,7 +22,6 @@ from robottelo.config import settings from robottelo.utils.installer import InstallerCommand -from robottelo.utils.issue_handlers import is_open pytestmark = pytest.mark.destructive @@ -64,7 +63,13 @@ infoblox_plugin_disable = [ 'no-enable-foreman-proxy-plugin-dhcp-infoblox', 'no-enable-foreman-proxy-plugin-dns-infoblox', + 'reset-foreman-proxy-dhcp-provider', + 'reset-foreman-proxy-dns-provider', ] +infoblox_plugin_disable_opts = { + 'foreman-proxy-dhcp': 'false', + 'foreman-proxy-dns': 'false', +} infoblox_plugin_opts = { 'foreman-proxy-dhcp': 'true', @@ -231,27 +236,26 @@ def test_infoblox_end_to_end( # check hostname and ip is present in A record assert host.name in result.text assert host.ip in result.text + # delete host + module_target_sat.api.Subnet(id=subnet.id, domain=[]).update() + module_target_sat.api.Host(id=host.id).delete() + module_target_sat.api.Subnet(id=subnet.id).delete() + module_target_sat.api.Domain(id=domain.id).delete() + with pytest.raises(HTTPError): + host.read() # disable dhcp and dns plugin - if not is_open('BZ:2210256'): - disable_infoblox_plugin = InstallerCommand(installer_args=infoblox_plugin_disable) - result = module_target_sat.install(disable_infoblox_plugin) - assert result.status == 0 - assert 'Success!' in result.stdout - installer = module_target_sat.install( - InstallerCommand(help='| grep enable-foreman-proxy-plugin-dhcp-infoblox') - ) - assert 'default: false' in installer.stdout - - installer = module_target_sat.install( - InstallerCommand(help='| grep enable-foreman-proxy-plugin-dns-infoblox') - ) - assert 'default: false' in installer.stdout - - @request.addfinalizer - def _finalize(): - module_target_sat.api.Subnet(id=subnet.id, domain=[]).update() - module_target_sat.api.Host(id=host.id).delete() - module_target_sat.api.Subnet(id=subnet.id).delete() - module_target_sat.api.Domain(id=domain.id).delete() - with pytest.raises(HTTPError): - host.read() + disable_infoblox_plugin = InstallerCommand( + installer_args=infoblox_plugin_disable, installer_opts=infoblox_plugin_disable_opts + ) + result = module_target_sat.install(disable_infoblox_plugin) + assert result.status == 0 + assert 'Success!' in result.stdout + installer = module_target_sat.install( + InstallerCommand(help='| grep enable-foreman-proxy-plugin-dhcp-infoblox') + ) + assert 'default: false' in installer.stdout + + installer = module_target_sat.install( + InstallerCommand(help='| grep enable-foreman-proxy-plugin-dns-infoblox') + ) + assert 'default: false' in installer.stdout From c0453188ed0aa5e42d19d64a84de90ecc9cbd288 Mon Sep 17 00:00:00 2001 From: synkd <48261305+synkd@users.noreply.github.com> Date: Wed, 16 Aug 2023 16:05:55 -0400 Subject: [PATCH 149/586] Convert UI manifest tests to manifester (#10394) * Convert UI manifest tests to manifester This PR will convert all UI tests that use manifests to obtain those manifests via manifester. I will aim to push one commit for each module that I'm converting. The PR will remain in draft state until I have converted all affected modules. The list of UI modules that I will be converting is as follows: ``` test_repository.py test_activationkey.py test_contenthost.py test_dashboard.py test_errata.py test_hostcollection.py test_host.py test_organization.py test_package.py test_reporttemplates.py test_rhcloud_insights.py test_rhcloud_inventory.py test_rhc.py test_subscription.py test_sync.py ``` * Remove fake manifest skip flag from activation key tests * Convert UI content host tests to manifester * Convert UI dashboard test to manifester * Convert UI errata tests to manifester Previously, the UI errata tests used an internal `_org` method to create an org, enable the `remote_execution_connection_by_ip` parameter, and upload a manifest. This method was then returned by locally-defined `org` and `module_org` fixtures to create a function-scoped and a module-scoped org fixture with the parameter enabled on the org. This approach was not compatible with the fixture scopes of the manifester fixtures, so this commit removes the internal `_org` method and enables the parameter separately in each locally-defined org fixture. * Convert UI host collection tests to manifester * Convert UI organization tests to manifester * Convert UI package test to manifester * Convert UI report templates tests to manifester * Convert UI cloud tests to manifester * Import epdb and add breakpoint * Remove merge conflict artifacts * Convert test_rhc fixture to manifester * Convert UI subscription tests to manifester * Convert UI sync tests to manifester * Additional cleanup due to inactivity on PR * Remove breakpoint * Remove breakpoints and convert fixtures to SCA This commit removes breakpoints and converts the `rhcloud_manifest_org` fixture to use SCA. The latter change also requires switching the activation key to not use auto-attach. * Refactor local fixture stacks and update reqs This commit modifies the local fixture stacks in ui/test_errata.py and ui/test_hostcollection.py and removes a package requirement per reviewer feedback. * Address remaining reviewer comments * Adding debugger temporarily * Removing debugger (cherry picked from commit 27bc835c277bede8f39eeef6e37f1685b4f22f49) --- pytest_fixtures/component/repository.py | 8 +- pytest_fixtures/component/rh_cloud.py | 2 +- robottelo/cli/factory.py | 1 - robottelo/hosts.py | 4 +- tests/foreman/ui/test_activationkey.py | 9 +- tests/foreman/ui/test_dashboard.py | 9 +- tests/foreman/ui/test_errata.py | 101 +++++++++++------ tests/foreman/ui/test_hostcollection.py | 135 ++++++++++++++--------- tests/foreman/ui/test_organization.py | 25 ++--- tests/foreman/ui/test_package.py | 5 +- tests/foreman/ui/test_reporttemplates.py | 6 +- tests/foreman/ui/test_repository.py | 22 ++-- tests/foreman/ui/test_rhc.py | 12 +- tests/foreman/ui/test_subscription.py | 43 ++++---- tests/foreman/ui/test_sync.py | 19 +--- 15 files changed, 231 insertions(+), 170 deletions(-) diff --git a/pytest_fixtures/component/repository.py b/pytest_fixtures/component/repository.py index 43861e07716..1cc17092768 100644 --- a/pytest_fixtures/component/repository.py +++ b/pytest_fixtures/component/repository.py @@ -235,7 +235,9 @@ def module_repos_collection_with_setup(request, module_target_sat, module_org, m @pytest.fixture(scope='module') -def module_repos_collection_with_manifest(request, module_target_sat, module_org, module_lce): +def module_repos_collection_with_manifest( + request, module_target_sat, module_entitlement_manifest_org, module_lce +): """This fixture and its usage is very similar to repos_collection fixture above with extra setup_content and uploaded manifest capabilities using module_org and module_lce fixtures @@ -253,7 +255,5 @@ def module_repos_collection_with_manifest(request, module_target_sat, module_org for repo_name, repo_params in repo.items() ], ) - _repos_collection.setup_content( - module_org.id, module_lce.id, upload_manifest=True, override=True - ) + _repos_collection.setup_content(module_entitlement_manifest_org.id, module_lce.id) return _repos_collection diff --git a/pytest_fixtures/component/rh_cloud.py b/pytest_fixtures/component/rh_cloud.py index 8efc0044ae0..e788f8e006e 100644 --- a/pytest_fixtures/component/rh_cloud.py +++ b/pytest_fixtures/component/rh_cloud.py @@ -31,7 +31,7 @@ def organization_ak_setup(rhcloud_sat_host, rhcloud_manifest_org): service_level='Self-Support', purpose_usage='test-usage', purpose_role='test-role', - auto_attach=True, + auto_attach=False, ).create() yield rhcloud_manifest_org, ak ak.delete() diff --git a/robottelo/cli/factory.py b/robottelo/cli/factory.py index cbd2e855145..397af1fb612 100644 --- a/robottelo/cli/factory.py +++ b/robottelo/cli/factory.py @@ -205,7 +205,6 @@ def make_content_view_with_credentials(options=None, credentials=None): # Organization ID is a required field. if not options or not options.get('organization-id'): raise CLIFactoryError('Please provide a valid ORG ID.') - args = { 'component-ids': None, 'composite': False, diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 28eb79c5227..71cbd1e40c3 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1161,8 +1161,8 @@ def virt_who_hypervisor_config( ] repos.extend(extra_repos) content_setup_data = cli_factory.setup_cdn_and_custom_repos_content( - org['id'], - lce['id'], + org[id], + lce[id], repos, upload_manifest=upload_manifest, rh_subscriptions=[constants.DEFAULT_SUBSCRIPTION_NAME], diff --git a/tests/foreman/ui/test_activationkey.py b/tests/foreman/ui/test_activationkey.py index bf6d39f5870..91671bb1d1b 100644 --- a/tests/foreman/ui/test_activationkey.py +++ b/tests/foreman/ui/test_activationkey.py @@ -422,7 +422,6 @@ def test_positive_update_cv(session, module_org, cv2_name, target_sat): @pytest.mark.run_in_one_thread -@pytest.mark.skip_if_not_set('fake_manifest') @pytest.mark.tier2 def test_positive_update_rh_product(function_entitlement_manifest_org, session, target_sat): """Update Content View in an Activation key @@ -479,7 +478,6 @@ def test_positive_update_rh_product(function_entitlement_manifest_org, session, @pytest.mark.run_in_one_thread -@pytest.mark.skip_if_not_set('fake_manifest') @pytest.mark.tier2 def test_positive_add_rh_product(function_entitlement_manifest_org, session, target_sat): """Test that RH product can be associated to Activation Keys @@ -548,7 +546,6 @@ def test_positive_add_custom_product(session, module_org, target_sat): @pytest.mark.run_in_one_thread -@pytest.mark.skip_if_not_set('fake_manifest') @pytest.mark.tier2 @pytest.mark.upgrade def test_positive_add_rh_and_custom_products( @@ -612,7 +609,6 @@ def test_positive_add_rh_and_custom_products( @pytest.mark.run_in_one_thread -@pytest.mark.skip_if_not_set('fake_manifest') @pytest.mark.tier2 @pytest.mark.upgrade def test_positive_fetch_product_content(target_sat, function_entitlement_manifest_org, session): @@ -1076,7 +1072,7 @@ def test_positive_host_associations(session, target_sat): assert ak2['content_hosts']['table'][0]['Name'] == vm2.hostname -@pytest.mark.skip_if_not_set('clients', 'fake_manifest') +@pytest.mark.skip_if_not_set('clients') @pytest.mark.tier3 @pytest.mark.skipif((not settings.robottelo.repos_hosting_url), reason='Missing repos_hosting_url') def test_positive_service_level_subscription_with_custom_product( @@ -1147,7 +1143,6 @@ def test_positive_service_level_subscription_with_custom_product( @pytest.mark.run_in_one_thread -@pytest.mark.skip_if_not_set('fake_manifest') @pytest.mark.tier2 def test_positive_delete_manifest(session, function_entitlement_manifest_org): """Check if deleting a manifest removes it from Activation key @@ -1191,7 +1186,7 @@ def test_positive_delete_manifest(session, function_entitlement_manifest_org): assert not ak['subscriptions']['resources']['assigned'] -@pytest.mark.skip_if_not_set('clients', 'fake_manifest') +@pytest.mark.skip_if_not_set('clients') @pytest.mark.tier3 @pytest.mark.skipif((not settings.robottelo.repos_hosting_url), reason='Missing repos_hosting_url') def test_positive_ak_with_custom_product_on_rhel6(session, rhel6_contenthost, target_sat): diff --git a/tests/foreman/ui/test_dashboard.py b/tests/foreman/ui/test_dashboard.py index c1dc9158967..439f80c48e7 100644 --- a/tests/foreman/ui/test_dashboard.py +++ b/tests/foreman/ui/test_dashboard.py @@ -203,7 +203,12 @@ def test_positive_task_status(session): indirect=True, ) def test_positive_user_access_with_host_filter( - test_name, module_location, rhel_contenthost, target_sat, repos_collection + test_name, + function_entitlement_manifest_org, + module_location, + rhel_contenthost, + target_sat, + repos_collection, ): """Check if user with necessary host permissions can access dashboard and required widgets are rendered with proper values @@ -229,7 +234,7 @@ def test_positive_user_access_with_host_filter( """ user_login = gen_string('alpha') user_password = gen_string('alphanumeric') - org = entities.Organization().create() + org = function_entitlement_manifest_org lce = entities.LifecycleEnvironment(organization=org).create() # create a role with necessary permissions role = entities.Role().create() diff --git a/tests/foreman/ui/test_errata.py b/tests/foreman/ui/test_errata.py index 4747930357f..36e86537e0c 100644 --- a/tests/foreman/ui/test_errata.py +++ b/tests/foreman/ui/test_errata.py @@ -20,6 +20,7 @@ from airgun.session import Session from broker import Broker from fauxfactory import gen_string +from manifester import Manifester from nailgun import entities from robottelo.config import settings @@ -83,37 +84,55 @@ def _set_setting_value(setting_entity, value): setting_entity.update(['value']) -def _org(module_target_sat): - org = entities.Organization().create() - # adding remote_execution_connect_by_ip=Yes at org level - entities.Parameter( +@pytest.fixture(scope='module') +def module_manifest(): + with Manifester(manifest_category=settings.manifest.entitlement) as manifest: + yield manifest + + +@pytest.fixture +def function_manifest(): + with Manifester(manifest_category=settings.manifest.entitlement) as manifest: + yield manifest + + +@pytest.fixture(scope='module') +def module_org_with_parameter(module_target_sat, module_manifest): + org = module_target_sat.api.Organization(simple_content_access=False).create() + # org.sca_disable() + module_target_sat.api.Parameter( name='remote_execution_connect_by_ip', parameter_type='boolean', value='Yes', organization=org.id, ).create() - module_target_sat.upload_manifest(org.id) + module_target_sat.upload_manifest(org.id, module_manifest.content) return org -@pytest.fixture(scope='module') -def module_org(): - return _org() - - @pytest.fixture -def org(): - return _org() +def function_org_with_parameter(target_sat, function_manifest): + org = target_sat.api.Organization(simple_content_access=False).create() + target_sat.api.Parameter( + name='remote_execution_connect_by_ip', + parameter_type='boolean', + value='Yes', + organization=org.id, + ).create() + target_sat.upload_manifest(org.id, function_manifest.content) + return org @pytest.fixture(scope='module') -def module_lce(module_org): - return entities.LifecycleEnvironment(organization=module_org).create() +def module_lce(module_target_sat, module_org_with_parameter): + return module_target_sat.api.LifecycleEnvironment( + organization=module_org_with_parameter + ).create() @pytest.fixture -def lce(org): - return entities.LifecycleEnvironment(organization=org).create() +def lce(target_sat, function_org_with_parameter): + return target_sat.api.LifecycleEnvironment(organization=function_org_with_parameter).create() @pytest.fixture @@ -160,7 +179,12 @@ def vm(module_repos_collection_with_setup, rhel7_contenthost, target_sat): ) @pytest.mark.no_containers def test_end_to_end( - session, module_org, module_repos_collection_with_setup, vm, target_sat, setting_update + session, + module_org_with_parameter, + module_repos_collection_with_setup, + vm, + target_sat, + setting_update, ): """Create all entities required for errata, set up applicable host, read errata details and apply it to host @@ -240,7 +264,7 @@ def test_end_to_end( @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_content_host_errata_page_pagination(session, org, lce, target_sat): +def test_content_host_errata_page_pagination(session, function_org_with_parameter, lce, target_sat): """ # Test per-page pagination for BZ#1662254 # Test apply by REX using Select All for BZ#1846670 @@ -273,6 +297,7 @@ def test_content_host_errata_page_pagination(session, org, lce, target_sat): :CaseLevel: System """ + org = function_org_with_parameter pkgs = ' '.join(FAKE_3_YUM_OUTDATED_PACKAGES) repos_collection = target_sat.cli_factory.RepositoryCollection( distro='rhel7', @@ -281,7 +306,7 @@ def test_content_host_errata_page_pagination(session, org, lce, target_sat): target_sat.cli_factory.YumRepository(url=settings.repos.yum_3.url), ], ) - repos_collection.setup_content(org.id, lce.id, upload_manifest=True) + repos_collection.setup_content(org.id, lce.id) with Broker(nick=repos_collection.distro, host_class=ContentHost) as client: client.add_rex_key(satellite=target_sat) # Add repo and install packages that need errata @@ -326,7 +351,7 @@ def test_content_host_errata_page_pagination(session, org, lce, target_sat): @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_list(session, org, lce, target_sat): +def test_positive_list(session, function_org_with_parameter, lce, target_sat): """View all errata in an Org :id: 71c7a054-a644-4c1e-b304-6bc34ea143f4 @@ -343,6 +368,7 @@ def test_positive_list(session, org, lce, target_sat): :CaseLevel: Integration """ + org = function_org_with_parameter rc = target_sat.cli_factory.RepositoryCollection( repositories=[target_sat.cli_factory.YumRepository(settings.repos.yum_3.url)] ) @@ -373,7 +399,9 @@ def test_positive_list(session, org, lce, target_sat): ], indirect=True, ) -def test_positive_list_permission(test_name, module_org, module_repos_collection_with_setup): +def test_positive_list_permission( + test_name, module_org_with_parameter, module_repos_collection_with_setup +): """Show errata only if the User has permissions to view them :id: cdb28f6a-23df-47a2-88ab-cd3b492126b2 @@ -391,6 +419,7 @@ def test_positive_list_permission(test_name, module_org, module_repos_collection :CaseLevel: Integration """ + module_org = module_org_with_parameter role = entities.Role().create() entities.Filter( organization=[module_org], @@ -430,7 +459,7 @@ def test_positive_list_permission(test_name, module_org, module_repos_collection indirect=True, ) def test_positive_apply_for_all_hosts( - session, module_org, module_repos_collection_with_setup, target_sat + session, module_org_with_parameter, module_repos_collection_with_setup, target_sat ): """Apply an erratum for all content hosts @@ -528,7 +557,7 @@ def test_positive_view_cve(session, module_repos_collection_with_setup): indirect=True, ) def test_positive_filter_by_environment( - session, module_org, module_repos_collection_with_setup, target_sat + session, module_org_with_parameter, module_repos_collection_with_setup, target_sat ): """Filter Content hosts by environment @@ -547,6 +576,7 @@ def test_positive_filter_by_environment( :CaseLevel: System """ + module_org = module_org_with_parameter with Broker( nick=module_repos_collection_with_setup.distro, host_class=ContentHost, _count=2 ) as clients: @@ -604,7 +634,7 @@ def test_positive_filter_by_environment( indirect=True, ) def test_positive_content_host_previous_env( - session, module_org, module_repos_collection_with_setup, vm + session, module_org_with_parameter, module_repos_collection_with_setup, vm ): """Check if the applicable errata are available from the content host's previous environment @@ -625,6 +655,7 @@ def test_positive_content_host_previous_env( :CaseLevel: System """ + module_org = module_org_with_parameter hostname = vm.hostname assert _install_client_package(vm, FAKE_1_CUSTOM_PACKAGE, errata_applicability=True) # Promote the latest content view version to a new lifecycle environment @@ -663,7 +694,7 @@ def test_positive_content_host_previous_env( ], indirect=True, ) -def test_positive_content_host_library(session, module_org, vm): +def test_positive_content_host_library(session, module_org_with_parameter, vm): """Check if the applicable errata are available from the content host's Library @@ -777,7 +808,9 @@ def test_positive_content_host_search_type(session, erratatype_vm): ], indirect=True, ) -def test_positive_show_count_on_content_host_page(session, module_org, erratatype_vm): +def test_positive_show_count_on_content_host_page( + session, module_org_with_parameter, erratatype_vm +): """Available errata count displayed in Content hosts page :id: 8575e282-d56e-41dc-80dd-f5f6224417cb @@ -833,7 +866,9 @@ def test_positive_show_count_on_content_host_page(session, module_org, erratatyp ], indirect=True, ) -def test_positive_show_count_on_content_host_details_page(session, module_org, erratatype_vm): +def test_positive_show_count_on_content_host_details_page( + session, module_org_with_parameter, erratatype_vm +): """Errata count on Content host Details page :id: 388229da-2b0b-41aa-a457-9b5ecbf3df4b @@ -876,7 +911,11 @@ def test_positive_show_count_on_content_host_details_page(session, module_org, e @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') @pytest.mark.parametrize('setting_update', ['errata_status_installable'], indirect=True) def test_positive_filtered_errata_status_installable_param( - session, errata_status_installable, target_sat, setting_update + session, + function_entitlement_manifest_org, + errata_status_installable, + target_sat, + setting_update, ): """Filter errata for specific content view and verify that host that was registered using that content view has different states in @@ -904,7 +943,7 @@ def test_positive_filtered_errata_status_installable_param( :CaseLevel: System """ - org = entities.Organization().create() + org = function_entitlement_manifest_org lce = entities.LifecycleEnvironment(organization=org).create() repos_collection = target_sat.cli_factory.RepositoryCollection( distro='rhel7', @@ -916,7 +955,7 @@ def test_positive_filtered_errata_status_installable_param( target_sat.cli_factory.YumRepository(url=CUSTOM_REPO_URL), ], ) - repos_collection.setup_content(org.id, lce.id, upload_manifest=True) + repos_collection.setup_content(org.id, lce.id) with Broker(nick=repos_collection.distro, host_class=ContentHost) as client: repos_collection.setup_virtual_machine(client) assert _install_client_package(client, FAKE_1_CUSTOM_PACKAGE, errata_applicability=True) @@ -990,7 +1029,7 @@ def test_positive_filtered_errata_status_installable_param( indirect=True, ) def test_content_host_errata_search_commands( - session, module_org, module_repos_collection_with_setup, target_sat + session, module_org_with_parameter, module_repos_collection_with_setup, target_sat ): """View a list of affected content hosts for security (RHSA) and bugfix (RHBA) errata, filtered with errata status and applicable flags. Applicability is calculated using the diff --git a/tests/foreman/ui/test_hostcollection.py b/tests/foreman/ui/test_hostcollection.py index b9b75313e3c..689a93afae4 100644 --- a/tests/foreman/ui/test_hostcollection.py +++ b/tests/foreman/ui/test_hostcollection.py @@ -20,7 +20,7 @@ import pytest from broker import Broker -from nailgun import entities +from manifester import Manifester from robottelo import constants from robottelo.config import settings @@ -29,25 +29,34 @@ @pytest.fixture(scope='module') -def module_org(): - org = entities.Organization().create() +def module_manifest(): + with Manifester(manifest_category=settings.manifest.entitlement) as manifest: + yield manifest + + +@pytest.fixture(scope='module') +def module_org_with_parameter(module_target_sat, module_manifest): # adding remote_execution_connect_by_ip=Yes at org level - entities.Parameter( + org = module_target_sat.api.Organization(simple_content_access=False).create() + module_target_sat.api.Parameter( name='remote_execution_connect_by_ip', parameter_type='boolean', value='Yes', organization=org.id, ).create() + module_target_sat.upload_manifest(org.id, module_manifest.content) return org @pytest.fixture(scope='module') -def module_lce(module_org): - return entities.LifecycleEnvironment(organization=module_org).create() +def module_lce(module_target_sat, module_org_with_parameter): + return module_target_sat.api.LifecycleEnvironment( + organization=module_org_with_parameter + ).create() @pytest.fixture(scope='module') -def module_repos_collection(module_org, module_lce, module_target_sat): +def module_repos_collection(module_org_with_parameter, module_lce, module_target_sat): repos_collection = module_target_sat.cli_factory.RepositoryCollection( distro=constants.DISTRO_DEFAULT, repositories=[ @@ -56,7 +65,7 @@ def module_repos_collection(module_org, module_lce, module_target_sat): module_target_sat.cli_factory.YumRepository(url=settings.repos.yum_6.url), ], ) - repos_collection.setup_content(module_org.id, module_lce.id, upload_manifest=True) + repos_collection.setup_content(module_org_with_parameter.id, module_lce.id) return repos_collection @@ -86,22 +95,28 @@ def vm_content_hosts_module_stream( @pytest.fixture -def vm_host_collection(module_org, vm_content_hosts): +def vm_host_collection(module_target_sat, module_org_with_parameter, vm_content_hosts): host_ids = [ - entities.Host().search(query={'search': f'name={host.hostname}'})[0].id + module_target_sat.api.Host().search(query={'search': f'name={host.hostname}'})[0].id for host in vm_content_hosts ] - host_collection = entities.HostCollection(host=host_ids, organization=module_org).create() + host_collection = module_target_sat.api.HostCollection( + host=host_ids, organization=module_org_with_parameter + ).create() return host_collection @pytest.fixture -def vm_host_collection_module_stream(module_org, vm_content_hosts_module_stream): +def vm_host_collection_module_stream( + module_target_sat, module_org_with_parameter, vm_content_hosts_module_stream +): host_ids = [ - entities.Host().search(query={'search': f'name={host.hostname}'})[0].id + module_target_sat.api.Host().search(query={'search': f'name={host.hostname}'})[0].id for host in vm_content_hosts_module_stream ] - host_collection = entities.HostCollection(host=host_ids, organization=module_org).create() + host_collection = module_target_sat.api.HostCollection( + host=host_ids, organization=module_org_with_parameter + ).create() return host_collection @@ -199,7 +214,9 @@ def _get_content_repository_urls(repos_collection, lce, content_view, module_tar @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_end_to_end(session, module_org, smart_proxy_location): +def test_positive_end_to_end( + session, module_target_sat, module_org_with_parameter, smart_proxy_location +): """Perform end to end testing for host collection component :id: 1d40bc74-8e05-42fa-b6e3-2999dc3b730d @@ -213,7 +230,9 @@ def test_positive_end_to_end(session, module_org, smart_proxy_location): hc_name = gen_string('alpha') new_name = gen_string('alpha') description = gen_string('alpha') - host = entities.Host(organization=module_org, location=smart_proxy_location).create() + host = module_target_sat.api.Host( + organization=module_org_with_parameter, location=smart_proxy_location + ).create() with session: session.location.select(smart_proxy_location.name) # Create new host collection @@ -244,7 +263,9 @@ def test_positive_end_to_end(session, module_org, smart_proxy_location): @pytest.mark.tier2 -def test_negative_install_via_remote_execution(session, module_org, smart_proxy_location): +def test_negative_install_via_remote_execution( + session, module_target_sat, module_org_with_parameter, smart_proxy_location +): """Test basic functionality of the Hosts collection UI install package via remote execution. @@ -259,9 +280,13 @@ def test_negative_install_via_remote_execution(session, module_org, smart_proxy_ """ hosts = [] for _ in range(2): - hosts.append(entities.Host(organization=module_org, location=smart_proxy_location).create()) - host_collection = entities.HostCollection( - host=[host.id for host in hosts], organization=module_org + hosts.append( + module_target_sat.api.Host( + organization=module_org_with_parameter, location=smart_proxy_location + ).create() + ) + host_collection = module_target_sat.api.HostCollection( + host=[host.id for host in hosts], organization=module_org_with_parameter ).create() with session: session.location.select(smart_proxy_location.name) @@ -278,7 +303,9 @@ def test_negative_install_via_remote_execution(session, module_org, smart_proxy_ @pytest.mark.tier2 -def test_negative_install_via_custom_remote_execution(session, module_org, smart_proxy_location): +def test_negative_install_via_custom_remote_execution( + session, module_target_sat, module_org_with_parameter, smart_proxy_location +): """Test basic functionality of the Hosts collection UI install package via remote execution - customize first. @@ -293,9 +320,13 @@ def test_negative_install_via_custom_remote_execution(session, module_org, smart """ hosts = [] for _ in range(2): - hosts.append(entities.Host(organization=module_org, location=smart_proxy_location).create()) - host_collection = entities.HostCollection( - host=[host.id for host in hosts], organization=module_org + hosts.append( + module_target_sat.api.Host( + organization=module_org_with_parameter, location=smart_proxy_location + ).create() + ) + host_collection = module_target_sat.api.HostCollection( + host=[host.id for host in hosts], organization=module_org_with_parameter ).create() with session: session.location.select(smart_proxy_location.name) @@ -313,7 +344,7 @@ def test_negative_install_via_custom_remote_execution(session, module_org, smart @pytest.mark.upgrade @pytest.mark.tier3 -def test_positive_add_host(session): +def test_positive_add_host(session, module_target_sat): """Check if host can be added to Host Collection :id: 80824c9f-15a1-4f76-b7ac-7d9ca9f6ed9e @@ -323,13 +354,13 @@ def test_positive_add_host(session): :CaseLevel: System """ hc_name = gen_string('alpha') - org = entities.Organization().create() - loc = entities.Location().create() - cv = entities.ContentView(organization=org).create() - lce = entities.LifecycleEnvironment(organization=org).create() + org = module_target_sat.api.Organization().create() + loc = module_target_sat.api.Location().create() + cv = module_target_sat.api.ContentView(organization=org).create() + lce = module_target_sat.api.LifecycleEnvironment(organization=org).create() cv.publish() cv.read().version[0].promote(data={'environment_ids': lce.id}) - host = entities.Host( + host = module_target_sat.api.Host( organization=org, location=loc, content_facet_attributes={'content_view_id': cv.id, 'lifecycle_environment_id': lce.id}, @@ -347,7 +378,7 @@ def test_positive_add_host(session): @pytest.mark.tier3 @pytest.mark.upgrade def test_positive_install_package( - session, module_org, smart_proxy_location, vm_content_hosts, vm_host_collection + session, module_org_with_parameter, smart_proxy_location, vm_content_hosts, vm_host_collection ): """Install a package to hosts inside host collection remotely @@ -359,7 +390,7 @@ def test_positive_install_package( :CaseLevel: System """ with session: - session.organization.select(org_name=module_org.name) + session.organization.select(org_name=module_org_with_parameter.name) session.location.select(smart_proxy_location.name) session.hostcollection.manage_packages( vm_host_collection.name, @@ -373,7 +404,7 @@ def test_positive_install_package( @pytest.mark.tier3 @pytest.mark.upgrade def test_positive_remove_package( - session, module_org, smart_proxy_location, vm_content_hosts, vm_host_collection + session, module_org_with_parameter, smart_proxy_location, vm_content_hosts, vm_host_collection ): """Remove a package from hosts inside host collection remotely @@ -386,7 +417,7 @@ def test_positive_remove_package( """ _install_package_with_assertion(vm_content_hosts, constants.FAKE_0_CUSTOM_PACKAGE) with session: - session.organization.select(org_name=module_org.name) + session.organization.select(org_name=module_org_with_parameter.name) session.location.select(smart_proxy_location.name) session.hostcollection.manage_packages( vm_host_collection.name, @@ -401,7 +432,7 @@ def test_positive_remove_package( @pytest.mark.tier3 def test_positive_upgrade_package( - session, module_org, smart_proxy_location, vm_content_hosts, vm_host_collection + session, module_org_with_parameter, smart_proxy_location, vm_content_hosts, vm_host_collection ): """Upgrade a package on hosts inside host collection remotely @@ -414,7 +445,7 @@ def test_positive_upgrade_package( """ _install_package_with_assertion(vm_content_hosts, constants.FAKE_1_CUSTOM_PACKAGE) with session: - session.organization.select(org_name=module_org.name) + session.organization.select(org_name=module_org_with_parameter.name) session.location.select(smart_proxy_location.name) session.hostcollection.manage_packages( vm_host_collection.name, @@ -428,7 +459,7 @@ def test_positive_upgrade_package( @pytest.mark.tier3 @pytest.mark.upgrade def test_positive_install_package_group( - session, module_org, smart_proxy_location, vm_content_hosts, vm_host_collection + session, module_org_with_parameter, smart_proxy_location, vm_content_hosts, vm_host_collection ): """Install a package group to hosts inside host collection remotely @@ -440,7 +471,7 @@ def test_positive_install_package_group( :CaseLevel: System """ with session: - session.organization.select(org_name=module_org.name) + session.organization.select(org_name=module_org_with_parameter.name) session.location.select(smart_proxy_location.name) session.hostcollection.manage_packages( vm_host_collection.name, @@ -455,7 +486,7 @@ def test_positive_install_package_group( @pytest.mark.tier3 def test_positive_remove_package_group( - session, module_org, smart_proxy_location, vm_content_hosts, vm_host_collection + session, module_org_with_parameter, smart_proxy_location, vm_content_hosts, vm_host_collection ): """Remove a package group from hosts inside host collection remotely @@ -472,7 +503,7 @@ def test_positive_remove_package_group( for package in constants.FAKE_0_CUSTOM_PACKAGE_GROUP: assert _is_package_installed(vm_content_hosts, package) with session: - session.organization.select(org_name=module_org.name) + session.organization.select(org_name=module_org_with_parameter.name) session.location.select(smart_proxy_location.name) session.hostcollection.manage_packages( vm_host_collection.name, @@ -488,7 +519,7 @@ def test_positive_remove_package_group( @pytest.mark.tier3 @pytest.mark.upgrade def test_positive_install_errata( - session, module_org, smart_proxy_location, vm_content_hosts, vm_host_collection + session, module_org_with_parameter, smart_proxy_location, vm_content_hosts, vm_host_collection ): """Install an errata to the hosts inside host collection remotely @@ -501,7 +532,7 @@ def test_positive_install_errata( """ _install_package_with_assertion(vm_content_hosts, constants.FAKE_1_CUSTOM_PACKAGE) with session: - session.organization.select(org_name=module_org.name) + session.organization.select(org_name=module_org_with_parameter.name) session.location.select(smart_proxy_location.name) result = session.hostcollection.install_errata( vm_host_collection.name, @@ -518,7 +549,7 @@ def test_positive_install_errata( @pytest.mark.tier3 def test_positive_change_assigned_content( session, - module_org, + module_org_with_parameter, smart_proxy_location, module_lce, vm_content_hosts, @@ -572,7 +603,7 @@ def test_positive_change_assigned_content( new_lce_name = gen_string('alpha') new_cv_name = gen_string('alpha') new_lce = module_target_sat.api.LifecycleEnvironment( - name=new_lce_name, organization=module_org + name=new_lce_name, organization=module_org_with_parameter ).create() content_view = module_target_sat.api.ContentView( id=module_repos_collection.setup_content_data['content_view']['id'] @@ -603,7 +634,7 @@ def test_positive_change_assigned_content( assert len(client_repo_urls) assert set(expected_repo_urls) == set(client_repo_urls) with session: - session.organization.select(org_name=module_org.name) + session.organization.select(org_name=module_org_with_parameter.name) session.location.select(smart_proxy_location.name) task_values = session.hostcollection.change_assigned_content( vm_host_collection.name, new_lce.name, new_content_view.name @@ -628,7 +659,9 @@ def test_positive_change_assigned_content( @pytest.mark.tier3 -def test_negative_hosts_limit(session, module_org, smart_proxy_location): +def test_negative_hosts_limit( + session, module_target_sat, module_org_with_parameter, smart_proxy_location +): """Check that Host limit actually limits usage :id: 57b70977-2110-47d9-be3b-461ad15c70c7 @@ -647,16 +680,16 @@ def test_negative_hosts_limit(session, module_org, smart_proxy_location): :CaseLevel: System """ hc_name = gen_string('alpha') - org = entities.Organization().create() - cv = entities.ContentView(organization=org).create() - lce = entities.LifecycleEnvironment(organization=org).create() + org = module_target_sat.api.Organization().create() + cv = module_target_sat.api.ContentView(organization=org).create() + lce = module_target_sat.api.LifecycleEnvironment(organization=org).create() cv.publish() cv.read().version[0].promote(data={'environment_ids': lce.id}) hosts = [] for _ in range(2): hosts.append( - entities.Host( - organization=module_org, + module_target_sat.api.Host( + organization=module_org_with_parameter, location=smart_proxy_location, content_facet_attributes={ 'content_view_id': cv.id, diff --git a/tests/foreman/ui/test_organization.py b/tests/foreman/ui/test_organization.py index b9b370f86c8..45025939870 100644 --- a/tests/foreman/ui/test_organization.py +++ b/tests/foreman/ui/test_organization.py @@ -30,8 +30,7 @@ @pytest.fixture(scope='module') -def module_repos_col(module_org, module_lce, module_target_sat, request): - module_target_sat.upload_manifest(org_id=module_org.id) +def module_repos_col(request, module_entitlement_manifest_org, module_lce, module_target_sat): repos_collection = module_target_sat.cli_factory.RepositoryCollection( repositories=[ # As Satellite Tools may be added as custom repo and to have a "Fully entitled" host, @@ -39,15 +38,15 @@ def module_repos_col(module_org, module_lce, module_target_sat, request): module_target_sat.cli_factory.YumRepository(url=settings.repos.yum_0.url), ], ) - repos_collection.setup_content(module_org.id, module_lce.id) + repos_collection.setup_content(module_entitlement_manifest_org.id, module_lce.id) yield repos_collection @request.addfinalizer def _cleanup(): try: - module_target_sat.api.Subscription(organization=module_org).delete_manifest( - data={'organization_id': module_org.id} - ) + module_target_sat.api.Subscription( + organization=module_entitlement_manifest_org + ).delete_manifest(data={'organization_id': module_entitlement_manifest_org.id}) except Exception: logger.exception('Exception cleaning manifest:') @@ -256,7 +255,7 @@ def test_positive_update_compresource(session): @pytest.mark.skip_if_not_set('fake_manifest') @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_delete_with_manifest_lces(session, target_sat): +def test_positive_delete_with_manifest_lces(session, target_sat, function_entitlement_manifest_org): """Create Organization with valid values and upload manifest. Then try to delete that organization. @@ -268,8 +267,7 @@ def test_positive_delete_with_manifest_lces(session, target_sat): :CaseImportance: Critical """ - org = entities.Organization().create() - target_sat.upload_manifest(org.id) + org = function_entitlement_manifest_org with session: session.organization.select(org.name) session.lifecycleenvironment.create({'name': 'DEV'}) @@ -281,11 +279,11 @@ def test_positive_delete_with_manifest_lces(session, target_sat): assert not session.organization.search(org.name) -@pytest.mark.skip('Skipping due to manifest refresh issues') -@pytest.mark.skip_if_not_set('fake_manifest') @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_download_debug_cert_after_refresh(session, target_sat): +def test_positive_download_debug_cert_after_refresh( + session, target_sat, function_entitlement_manifest_org +): """Create organization with valid manifest. Download debug certificate for that organization and refresh added manifest for few times in a row @@ -298,9 +296,8 @@ def test_positive_download_debug_cert_after_refresh(session, target_sat): :CaseImportance: High """ - org = entities.Organization().create() + org = function_entitlement_manifest_org try: - target_sat.upload_manifest(org.id) with session: session.organization.select(org.name) for _ in range(3): diff --git a/tests/foreman/ui/test_package.py b/tests/foreman/ui/test_package.py index 7d32d67e319..c5896e9ca24 100644 --- a/tests/foreman/ui/test_package.py +++ b/tests/foreman/ui/test_package.py @@ -60,12 +60,11 @@ def module_yum_repo2(module_product): @pytest.fixture(scope='module') -def module_rh_repo(module_org, module_target_sat): - module_target_sat.upload_manifest(module_org.id) +def module_rh_repo(module_entitlement_manifest_org, module_target_sat): rhst = module_target_sat.cli_factory.SatelliteToolsRepository(cdn=True) repo_id = module_target_sat.api_factory.enable_rhrepo_and_fetchid( basearch=rhst.data['arch'], - org_id=module_org.id, + org_id=module_entitlement_manifest_org.id, product=rhst.data['product'], repo=rhst.data['repository'], reposet=rhst.data['repository-set'], diff --git a/tests/foreman/ui/test_reporttemplates.py b/tests/foreman/ui/test_reporttemplates.py index ae8f6bae77b..f8eed10e58f 100644 --- a/tests/foreman/ui/test_reporttemplates.py +++ b/tests/foreman/ui/test_reporttemplates.py @@ -384,7 +384,7 @@ def test_positive_autocomplete(session): @pytest.mark.tier2 def test_positive_schedule_generation_and_get_mail( - session, module_manifest_org, module_location, target_sat + session, module_entitlement_manifest_org, module_location, target_sat ): """Schedule generating a report. Request the result be sent via e-mail. @@ -439,7 +439,9 @@ def test_positive_schedule_generation_and_get_mail( target_sat.get(remote_path=str(gzip_path), local_path=str(local_gzip_file)) assert os.system(f'gunzip {local_gzip_file}') == 0 data = json.loads(local_file.read_text()) - subscription_search = target_sat.api.Subscription(organization=module_manifest_org).search() + subscription_search = target_sat.api.Subscription( + organization=module_entitlement_manifest_org + ).search() assert len(data) >= len(subscription_search) > 0 keys_expected = [ 'Account number', diff --git a/tests/foreman/ui/test_repository.py b/tests/foreman/ui/test_repository.py index a141a32df4b..37468fa4189 100644 --- a/tests/foreman/ui/test_repository.py +++ b/tests/foreman/ui/test_repository.py @@ -741,7 +741,7 @@ def test_positive_no_errors_on_repo_scan(target_sat, function_sca_manifest_org): @pytest.mark.tier2 -def test_positive_reposet_disable(session, target_sat): +def test_positive_reposet_disable(session, target_sat, function_entitlement_manifest_org): """Enable RH repo, sync it and then disable :id: de596c56-1327-49e8-86d5-a1ab907f26aa @@ -750,8 +750,7 @@ def test_positive_reposet_disable(session, target_sat): :CaseLevel: Integration """ - org = entities.Organization().create() - target_sat.upload_manifest(org.id) + org = function_entitlement_manifest_org sat_tools_repo = target_sat.cli_factory.SatelliteToolsRepository(distro='rhel7', cdn=True) repository_name = sat_tools_repo.data['repository'] with session: @@ -782,7 +781,9 @@ def test_positive_reposet_disable(session, target_sat): @pytest.mark.run_in_one_thread @pytest.mark.tier2 -def test_positive_reposet_disable_after_manifest_deleted(session, target_sat): +def test_positive_reposet_disable_after_manifest_deleted( + session, function_entitlement_manifest_org, target_sat +): """Enable RH repo and sync it. Remove manifest and then disable repository @@ -796,8 +797,7 @@ def test_positive_reposet_disable_after_manifest_deleted(session, target_sat): :CaseLevel: Integration """ - org = entities.Organization().create() - target_sat.upload_manifest(org.id) + org = function_entitlement_manifest_org sub = entities.Subscription(organization=org) sat_tools_repo = target_sat.cli_factory.SatelliteToolsRepository(distro='rhel7', cdn=True) repository_name = sat_tools_repo.data['repository'] @@ -866,7 +866,7 @@ def test_positive_delete_random_docker_repo(session, module_org): @pytest.mark.tier2 -def test_positive_delete_rhel_repo(session, module_org, target_sat): +def test_positive_delete_rhel_repo(session, module_entitlement_manifest_org, target_sat): """Enable and sync a Red Hat Repository, and then delete it :id: e96f369d-3e58-4824-802e-0b7e99d6d207 @@ -880,12 +880,11 @@ def test_positive_delete_rhel_repo(session, module_org, target_sat): :BZ: 1152672 """ - target_sat.upload_manifest(module_org.id) sat_tools_repo = target_sat.cli_factory.SatelliteToolsRepository(distro='rhel7', cdn=True) repository_name = sat_tools_repo.data['repository'] product_name = sat_tools_repo.data['product'] with session: - session.organization.select(module_org.name) + session.organization.select(module_entitlement_manifest_org.name) session.redhatrepository.enable( sat_tools_repo.data['repository-set'], sat_tools_repo.data['arch'], @@ -914,7 +913,7 @@ def test_positive_delete_rhel_repo(session, module_org, target_sat): @pytest.mark.tier2 -def test_positive_recommended_repos(session, module_org, target_sat): +def test_positive_recommended_repos(session, module_entitlement_manifest_org): """list recommended repositories using On/Off 'Recommended Repositories' toggle. @@ -929,9 +928,8 @@ def test_positive_recommended_repos(session, module_org, target_sat): :BZ: 1776108 """ - target_sat.upload_manifest(module_org.id) with session: - session.organization.select(module_org.name) + session.organization.select(module_entitlement_manifest_org.name) rrepos_on = session.redhatrepository.read(recommended_repo='on') assert REPOSET['rhel7'] in [repo['name'] for repo in rrepos_on] v = get_sat_version() diff --git a/tests/foreman/ui/test_rhc.py b/tests/foreman/ui/test_rhc.py index 23aa43f07f5..c1d6f658d22 100644 --- a/tests/foreman/ui/test_rhc.py +++ b/tests/foreman/ui/test_rhc.py @@ -65,16 +65,18 @@ def module_rhc_org(module_target_sat): @pytest.fixture() -def fixture_setup_rhc_satellite(request, module_target_sat, module_rhc_org): +def fixture_setup_rhc_satellite( + request, + module_target_sat, + module_rhc_org, + module_entitlement_manifest, +): """Create Organization and activation key after successful test execution""" yield if request.node.rep_call.passed: if settings.rh_cloud.crc_env == 'prod': - manifests_path = module_target_sat.download_file( - file_url=settings.fake_manifest.url['default'] - )[0] module_target_sat.cli.Subscription.upload( - {'file': manifests_path, 'organization-id': module_rhc_org.id} + {'file': module_entitlement_manifest.filename, 'organization-id': module_rhc_org.id} ) # Enable and sync required repos repo1_id = module_target_sat.api_factory.enable_sync_redhat_repo( diff --git a/tests/foreman/ui/test_subscription.py b/tests/foreman/ui/test_subscription.py index e548c1d2535..a56c5796f87 100644 --- a/tests/foreman/ui/test_subscription.py +++ b/tests/foreman/ui/test_subscription.py @@ -231,7 +231,9 @@ def test_positive_access_with_non_admin_user_with_manifest( @pytest.mark.tier2 -def test_positive_access_manifest_as_another_admin_user(test_name, target_sat): +def test_positive_access_manifest_as_another_admin_user( + test_name, target_sat, function_entitlement_manifest +): """Other admin users should be able to access and manage a manifest uploaded by a different admin. @@ -258,7 +260,7 @@ def test_positive_access_manifest_as_another_admin_user(test_name, target_sat): ).create() # use the first admin to upload a manifest with Session(test_name, user=user1.login, password=user1_password) as session: - target_sat.upload_manifest(org.id) + target_sat.upload_manifest(org.id, function_entitlement_manifest.content) assert session.subscription.has_manifest # store subscriptions that have "Red Hat" in the name for later rh_subs = session.subscription.search("Red Hat") @@ -273,7 +275,9 @@ def test_positive_access_manifest_as_another_admin_user(test_name, target_sat): @pytest.mark.tier3 -def test_positive_view_vdc_subscription_products(session, rhel7_contenthost, target_sat): +def test_positive_view_vdc_subscription_products( + session, rhel7_contenthost, target_sat, function_entitlement_manifest_org +): """Ensure that Virtual Datacenters subscription provided products is not empty and that a consumed product exist in content products. @@ -306,16 +310,14 @@ def test_positive_view_vdc_subscription_products(session, rhel7_contenthost, tar :CaseLevel: System """ - org = entities.Organization().create() + org = function_entitlement_manifest_org lce = entities.LifecycleEnvironment(organization=org).create() repos_collection = target_sat.cli_factory.RepositoryCollection( distro='rhel7', repositories=[target_sat.cli_factory.RHELAnsibleEngineRepository(cdn=True)], ) product_name = repos_collection.rh_repos[0].data['product'] - repos_collection.setup_content( - org.id, lce.id, upload_manifest=True, rh_subscriptions=[DEFAULT_SUBSCRIPTION_NAME] - ) + repos_collection.setup_content(org.id, lce.id, rh_subscriptions=[DEFAULT_SUBSCRIPTION_NAME]) rhel7_contenthost.contenthost_setup( target_sat, org.label, @@ -337,7 +339,9 @@ def test_positive_view_vdc_subscription_products(session, rhel7_contenthost, tar @pytest.mark.skip_if_not_set('libvirt') @pytest.mark.tier3 -def test_positive_view_vdc_guest_subscription_products(session, rhel7_contenthost, target_sat): +def test_positive_view_vdc_guest_subscription_products( + session, rhel7_contenthost, target_sat, function_entitlement_manifest_org +): """Ensure that Virtual Data Centers guest subscription Provided Products and Content Products are not empty. @@ -367,7 +371,7 @@ def test_positive_view_vdc_guest_subscription_products(session, rhel7_contenthos :CaseLevel: System """ - org = entities.Organization().create() + org = function_entitlement_manifest_org lce = entities.LifecycleEnvironment(organization=org).create() provisioning_server = settings.libvirt.libvirt_hostname rh_product_repository = target_sat.cli_factory.RHELAnsibleEngineRepository(cdn=True) @@ -390,6 +394,7 @@ def test_positive_view_vdc_guest_subscription_products(session, rhel7_contenthos hypervisor_hostname=provisioning_server, configure_ssh=True, subscription_name=VDC_SUBSCRIPTION_NAME, + upload_manifest=False, extra_repos=[rh_product_repository.data], ) virt_who_hypervisor_host = virt_who_data['virt_who_hypervisor_host'] @@ -416,7 +421,9 @@ def test_positive_view_vdc_guest_subscription_products(session, rhel7_contenthos @pytest.mark.tier3 -def test_select_customizable_columns_uncheck_and_checks_all_checkboxes(session): +def test_select_customizable_columns_uncheck_and_checks_all_checkboxes( + session, function_org, function_entitlement_manifest +): """Ensures that no column headers from checkboxes show up in the table after unticking everything from selectable customizable column @@ -450,28 +457,20 @@ def test_select_customizable_columns_uncheck_and_checks_all_checkboxes(session): 'Consumed': False, 'Entitlements': False, } - org = entities.Organization().create() - _, temporary_local_manifest_path = mkstemp(prefix='manifest-', suffix='.zip') - with clone() as manifest: - with open(temporary_local_manifest_path, 'wb') as file_handler: - file_handler.write(manifest.content.read()) - + org = function_org with session: session.organization.select(org.name) - # Ignore "Danger alert: Katello::Errors::UpstreamConsumerNotFound" as server will connect - # to upstream subscription service to verify - # the consumer uuid, that will be displayed in flash error messages - # Note: this happen only when using clone manifest. session.subscription.add_manifest( - temporary_local_manifest_path, + function_entitlement_manifest.path, ignore_error_messages=['Danger alert: Katello::Errors::UpstreamConsumerNotFound'], ) headers = session.subscription.filter_columns(checkbox_dict) - assert not headers + assert headers == ('Select all rows',) assert len(checkbox_dict) == 9 time.sleep(3) checkbox_dict.update((k, True) for k in checkbox_dict) col = session.subscription.filter_columns(checkbox_dict) + checkbox_dict.update({'Select all rows': ''}) assert set(col) == set(checkbox_dict) diff --git a/tests/foreman/ui/test_sync.py b/tests/foreman/ui/test_sync.py index 65622c73873..ae5e0cc58bc 100644 --- a/tests/foreman/ui/test_sync.py +++ b/tests/foreman/ui/test_sync.py @@ -40,13 +40,6 @@ def module_custom_product(module_org): return entities.Product(organization=module_org).create() -@pytest.fixture(scope='module') -def module_org_with_manifest(module_target_sat): - org = entities.Organization().create() - module_target_sat.upload_manifest(org.id) - return org - - @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') def test_positive_sync_custom_repo(session, module_custom_product): @@ -69,7 +62,7 @@ def test_positive_sync_custom_repo(session, module_custom_product): @pytest.mark.skip_if_not_set('fake_manifest') @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_sync_rh_repos(session, target_sat, module_org_with_manifest): +def test_positive_sync_rh_repos(session, target_sat, module_entitlement_manifest_org): """Create Content RedHat Sync with two repos. :id: e30f6509-0b65-4bcc-a522-b4f3089d3911 @@ -88,7 +81,7 @@ def test_positive_sync_rh_repos(session, target_sat, module_org_with_manifest): for distro, repo in zip(distros, repos) ] for repo_collection in repo_collections: - repo_collection.setup(module_org_with_manifest.id, synchronize=False) + repo_collection.setup(module_entitlement_manifest_org.id, synchronize=False) repo_paths = [ ( repo.repo_data['product'], @@ -99,7 +92,7 @@ def test_positive_sync_rh_repos(session, target_sat, module_org_with_manifest): for repo in repos ] with session: - session.organization.select(org_name=module_org_with_manifest.name) + session.organization.select(org_name=module_entitlement_manifest_org.name) results = session.sync_status.synchronize(repo_paths) assert len(results) == len(repo_paths) assert all([result == 'Syncing Complete.' for result in results]) @@ -139,7 +132,7 @@ def test_positive_sync_custom_ostree_repo(session, module_custom_product): @pytest.mark.skip_if_not_set('fake_manifest') @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_sync_rh_ostree_repo(session, module_org_with_manifest, target_sat): +def test_positive_sync_rh_ostree_repo(session, target_sat, module_entitlement_manifest_org): """Sync CDN based ostree repository. :id: 4d28fff0-5fda-4eee-aa0c-c5af02c31de5 @@ -158,14 +151,14 @@ def test_positive_sync_rh_ostree_repo(session, module_org_with_manifest, target_ """ target_sat.api_factory.enable_rhrepo_and_fetchid( basearch=None, - org_id=module_org_with_manifest.id, + org_id=module_entitlement_manifest_org.id, product=PRDS['rhah'], repo=REPOS['rhaht']['name'], reposet=REPOSET['rhaht'], releasever=None, ) with session: - session.organization.select(org_name=module_org_with_manifest.name) + session.organization.select(org_name=module_entitlement_manifest_org.name) results = session.sync_status.synchronize([(PRDS['rhah'], REPOS['rhaht']['name'])]) assert len(results) == 1 assert results[0] == 'Syncing Complete.' From 796942fe70fade16c0889a44cc1422b2e5030a6d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 17 Aug 2023 12:36:14 -0400 Subject: [PATCH 150/586] [6.14.z] reset global ttp value (#12284) --- tests/foreman/api/test_remoteexecution.py | 80 +++++++++++++++++------ 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/tests/foreman/api/test_remoteexecution.py b/tests/foreman/api/test_remoteexecution.py index 05535ae591a..4cf5174238c 100644 --- a/tests/foreman/api/test_remoteexecution.py +++ b/tests/foreman/api/test_remoteexecution.py @@ -77,7 +77,7 @@ def test_positive_run_capsule_upgrade_playbook(module_capsule_configured, target @pytest.mark.tier3 @pytest.mark.no_containers -@pytest.mark.rhel_ver_match('[^6].*') +@pytest.mark.rhel_ver_list('8') def test_negative_time_to_pickup( module_org, module_target_sat, @@ -95,7 +95,7 @@ def test_negative_time_to_pickup( :CaseImportance: High - :bz: 2158738, 2118651 + :bz: 2158738 :parametrized: yes """ @@ -132,24 +132,6 @@ def test_negative_time_to_pickup( # check mqtt client is running result = rhel_contenthost.execute('systemctl status yggdrasild') assert result.status == 0, f'Failed to start yggdrasil on client: {result.stderr}' - # check that longrunning command is not affected by time_to_pickup BZ#2158738 - job = module_target_sat.api.JobInvocation().run( - synchronous=False, - data={ - 'job_template_id': template_id, - 'organization': module_org.name, - 'location': smart_proxy_location.name, - 'inputs': { - 'command': 'echo start; sleep 10; echo done', - }, - 'targeting_type': 'static_query', - 'search_query': f'name = {rhel_contenthost.hostname}', - 'time_to_pickup': '5', - }, - ) - module_target_sat.wait_for_tasks(f'resource_type = JobInvocation and resource_id = {job["id"]}') - result = module_target_sat.api.JobInvocation(id=job['id']).read() - assert result.succeeded == 1 # stop yggdrasil client on host result = rhel_contenthost.execute('systemctl stop yggdrasild') assert result.status == 0, f'Failed to stop yggdrasil on client: {result.stderr}' @@ -190,6 +172,7 @@ def test_negative_time_to_pickup( global_ttp = module_target_sat.api.Setting().search( query={'search': 'name="remote_execution_time_to_pickup"'} )[0] + default_global_ttp = global_ttp.value global_ttp.value = '10' global_ttp.update(['value']) job = module_target_sat.api.JobInvocation().run( @@ -214,3 +197,60 @@ def test_negative_time_to_pickup( query={'search': f'resource_type = JobInvocation and resource_id = {job["id"]}'} ) assert 'The job was not picked up in time' in result[0].humanized['output'] + global_ttp.value = default_global_ttp + global_ttp.update(['value']) + # start yggdrasil client on host + result = rhel_contenthost.execute('systemctl start yggdrasild') + assert result.status == 0, f'Failed to start on client: {result.stderr}' + result = rhel_contenthost.execute('systemctl status yggdrasild') + assert result.status == 0, f'Failed to start yggdrasil on client: {result.stderr}' + rhel_contenthost.execute('yggdrasil status') + + +@pytest.mark.tier3 +@pytest.mark.no_containers +@pytest.mark.rhel_ver_list('8') +def test_positive_check_longrunning_job( + module_org, + module_target_sat, + smart_proxy_location, + module_ak_with_cv, + module_capsule_configured_mqtt, + rhel_contenthost, +): + """Time to pickup setting doesn't disrupt longrunning jobs + + :id: e6eeb948-faa5-4b76-9a86-5e934f7bee77 + + :expectedresults: Time to pickup doesn't aborts the long running job if + it already started + + :CaseImportance: Medium + + :bz: 2118651 + + :parametrized: yes + """ + template_id = ( + module_target_sat.api.JobTemplate() + .search(query={'search': 'name="Run Command - Script Default"'})[0] + .id + ) + # check that longrunning command is not affected by time_to_pickup BZ#2158738 + job = module_target_sat.api.JobInvocation().run( + synchronous=False, + data={ + 'job_template_id': template_id, + 'organization': module_org.name, + 'location': smart_proxy_location.name, + 'inputs': { + 'command': 'echo start; sleep 25; echo done', + }, + 'targeting_type': 'static_query', + 'search_query': f'name = {rhel_contenthost.hostname}', + 'time_to_pickup': '20', + }, + ) + module_target_sat.wait_for_tasks(f'resource_type = JobInvocation and resource_id = {job["id"]}') + result = module_target_sat.api.JobInvocation(id=job['id']).read() + assert result.succeeded == 1 From c7dc08185839e4990d065619c8b3345db9bfb62e Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Sun, 20 Aug 2023 14:15:56 -0400 Subject: [PATCH 151/586] [6.14.z] Bump sphinx from 7.2.0 to 7.2.2 (#12294) Bump sphinx from 7.2.0 to 7.2.2 (#12290) (cherry picked from commit d3896bce80d9134bbe386c28253f7c2acb4c7c7e) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index b2239ac5e21..0f7055ed133 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -5,7 +5,7 @@ redis==5.0.0 pre-commit==3.3.3 # For generating documentation. -sphinx==7.2.0 +sphinx==7.2.2 sphinx-autoapi==2.1.1 # For 'manage' interactive shell From f79e3f17d6f698175a9e9ca7be702089048e1712 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 22 Aug 2023 03:31:33 -0400 Subject: [PATCH 152/586] [6.14.z] removed wotfix-skipped test (#12296) removed wotfix-skipped test (#12280) (cherry picked from commit eef7524ddeb8bf76677379b8d661c07a2f351c1f) Co-authored-by: Peter Ondrejka --- tests/foreman/ui/test_audit.py | 46 ---------------------------------- 1 file changed, 46 deletions(-) diff --git a/tests/foreman/ui/test_audit.py b/tests/foreman/ui/test_audit.py index f2df8e5ef3d..ecb7c87d7ce 100644 --- a/tests/foreman/ui/test_audit.py +++ b/tests/foreman/ui/test_audit.py @@ -18,7 +18,6 @@ from fauxfactory import gen_string from nailgun import entities -from robottelo.constants import ANY_CONTEXT from robottelo.constants import ENVIRONMENT @@ -112,7 +111,6 @@ def test_positive_audit_comment(session, module_org): assert values['comment'] == audit_comment -@pytest.mark.skip_if_open("BZ:2222890") @pytest.mark.tier2 def test_positive_update_event(session, module_org): """When existing content view is updated, corresponding audit entry appear @@ -202,47 +200,3 @@ def test_positive_add_event(session, module_org): assert values['action_summary'][0]['column0'] == 'Added {}/{} to {}'.format( ENVIRONMENT, cv.name, cv.name ) - - -@pytest.mark.skip_if_open("BZ:1701118") -@pytest.mark.skip_if_open("BZ:1701132") -@pytest.mark.tier2 -def test_positive_create_role_filter(session, module_org, target_sat): - """Update a role with new filter and check that corresponding event - appeared in the audit log - - :id: 74679c0d-7ef1-4ab1-8282-9377c6cabb9f - - :customerscenario: true - - :expectedresults: audit log has an entry for a new filter that was - added to the role - - :BZ: 1425977, 1701118, 1701132 - - :CaseAutomation: Automated - - :CaseLevel: Integration - - :CaseImportance: Medium - """ - role = entities.Role(organization=[module_org]).create() - with session: - session.organization.select(org_name=ANY_CONTEXT['org']) - values = session.audit.search(f'type=role and organization={module_org.name}') - assert values['action_type'] == 'create' - assert values['resource_type'] == 'ROLE' - assert values['resource_name'] == role.name - target_sat.api_factory.create_role_permissions( - role, {'Architecture': ['view_architectures', 'edit_architectures']} - ) - values = session.audit.search('type=filter') - assert values['action_type'] == 'added' - assert values['resource_type'] == 'Filter' - assert values['resource_name'] == '{} and {} / {}'.format( - 'view_architectures', 'edit_architectures', role.name - ) - assert len(values['action_summary']) == 1 - assert values['action_summary'][0]['column0'] == 'Added {} and {} to {}'.format( - 'view_architectures', 'edit_architectures', role.name - ) From 5009b1e67ff207ee4b530bed94d9876c33b00d33 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 22 Aug 2023 03:32:46 -0400 Subject: [PATCH 153/586] [6.14.z] removed wontfix locations test (#12287) removed wontfix locations test (cherry picked from commit b65bcfee9b130612ae05a040fe7475fa23e3dcc9) Co-authored-by: Peter Ondrejka --- tests/foreman/cli/test_location.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/tests/foreman/cli/test_location.py b/tests/foreman/cli/test_location.py index 6f358baa166..7baebfb0d1d 100644 --- a/tests/foreman/cli/test_location.py +++ b/tests/foreman/cli/test_location.py @@ -395,27 +395,3 @@ def test_negative_update_parent_with_child(self, request): Location.update({'id': location['id'], 'parent-id': location['id']}) location = Location.info({'id': location['id']}) assert location['parent'] == parent_location['name'] - - @pytest.mark.skip_if_open("BZ:1937009") - @pytest.mark.tier1 - def test_positive_nested_location(self, request, module_org): - """View nested location in an organization - - :id: cce4a33d-7743-47ef-b437-2539fdc73c44 - - :customerscenario: true - - :BZ: 1937009 - - :expectedresults: Nested location can be viewed - - :CaseImportance: Medium - """ - parent_location = _location(request, {'organization-id': module_org.id}) - location = _location( - request, {'parent-id': parent_location['id'], 'organization-id': module_org.id} - ) - loc_list = Location.list( - {'search': f'title={location["title"]}', 'organization-id': module_org.id} - ) - assert loc_list[0]['title'] == location['title'] From 6372606a49801b546a1bb48ec5801888402443b4 Mon Sep 17 00:00:00 2001 From: Peter Ondrejka Date: Tue, 22 Aug 2023 16:22:48 +0200 Subject: [PATCH 154/586] removed skipped wontfix jt (#12283) (cherry picked from commit f3bc09d2804869b7232a76d64b3f7498c5475eea) --- tests/foreman/ui/test_jobtemplate.py | 29 ---------------------------- 1 file changed, 29 deletions(-) diff --git a/tests/foreman/ui/test_jobtemplate.py b/tests/foreman/ui/test_jobtemplate.py index 494e9ce5d54..1041878f753 100644 --- a/tests/foreman/ui/test_jobtemplate.py +++ b/tests/foreman/ui/test_jobtemplate.py @@ -208,32 +208,3 @@ def test_positive_end_to_end(session, module_org, module_location, target_sat): for name in (template_new_name, template_clone_name): session.jobtemplate.delete(name) assert not session.jobtemplate.search(name) - - -@pytest.mark.skip_if_open('BZ:1705866') -@pytest.mark.tier2 -def test_positive_clone_job_template_with_foreign_input_sets(session): - """Clone job template with foreign input sets - - :id: 7f502750-b8a2-4223-8d3c-47be95781e34 - - :expectedresults: Job template can be cloned with foreign input sets and - new template contain foreign input sets from parent - - :BZ: 1705866 - """ - child_name = gen_string('alpha') - parent_name = 'Install Group - Katello Script Default' - with session: - parent = session.jobtemplate.read(parent_name, widget_names='job')['job'][ - 'foreign_input_sets' - ] - session.jobtemplate.clone(parent_name, {'template.name': child_name}) - child = session.jobtemplate.read(child_name, widget_names='job')['job'][ - 'foreign_input_sets' - ] - assert len(parent) == len(child) - assert parent[0]['target_template'] == child[0]['target_template'] - assert parent[0]['include_all'] == child[0]['include_all'] - assert parent[0]['include'] == child[0]['include'] - assert parent[0]['exclude'] == child[0]['exclude'] From 61968cdc17a2dfd96b95b6297754a91acefaf354 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 24 Aug 2023 03:12:53 -0400 Subject: [PATCH 155/586] [6.14.z] Component Audit for Discovery part-2 (#12315) Component Audit for Discovery part-2 (#12056) Signed-off-by: Adarsh Dubey (cherry picked from commit cc4a1f0fa7250fd5ab0d2ae46d58dfda4b05ebab) Co-authored-by: Adarsh dubey --- tests/foreman/ui/test_discoveryrule.py | 200 +++++++++++-------------- 1 file changed, 89 insertions(+), 111 deletions(-) diff --git a/tests/foreman/ui/test_discoveryrule.py b/tests/foreman/ui/test_discoveryrule.py index 1e7f21285d4..6efd92c5c17 100644 --- a/tests/foreman/ui/test_discoveryrule.py +++ b/tests/foreman/ui/test_discoveryrule.py @@ -21,58 +21,42 @@ from fauxfactory import gen_integer from fauxfactory import gen_ipaddr from fauxfactory import gen_string -from nailgun import entities - - -@pytest.fixture(scope='module') -def manager_loc(): - return entities.Location().create() - - -@pytest.fixture(scope='module') -def module_org(): - return entities.Organization().create() @pytest.fixture -def module_discovery_env(module_org, module_location): - discovery_loc = entities.Setting().search(query={'search': 'name="discovery_location"'})[0] - default_discovery_loc = discovery_loc.value - discovery_loc.value = module_location.name - discovery_loc.update(['value']) - discovery_org = entities.Setting().search(query={'search': 'name="discovery_organization"'})[0] - default_discovery_org = discovery_org.value - discovery_org.value = module_org.name - discovery_org.update(['value']) +def module_discovery_env(module_org, module_location, module_target_sat): + discovery_loc = module_target_sat.update_setting('discovery_location', module_location.name) + discovery_org = module_target_sat.update_setting('discovery_organization', module_org.name) yield - discovery_loc.value = default_discovery_loc - discovery_loc.update(['value']) - discovery_org.value = default_discovery_org - discovery_org.update(['value']) + module_target_sat.update_setting('discovery_location', discovery_loc) + module_target_sat.update_setting('discovery_organization', discovery_org) -@pytest.fixture -def manager_user(manager_loc, module_location, module_org): - manager_role = entities.Role().search(query={'search': 'name="Discovery Manager"'})[0] +@pytest.fixture(scope='module') +def manager_user(module_location, module_org, module_target_sat): + manager_role = module_target_sat.api.Role().search( + query={'search': 'name="Discovery Manager"'} + )[0] password = gen_string('alphanumeric') - manager_user = entities.User( + manager_user = module_target_sat.api.User( login=gen_string('alpha'), role=[manager_role], password=password, - location=[module_location, manager_loc], + location=[module_location], organization=[module_org], ).create() manager_user.password = password return manager_user -@pytest.fixture -def reader_user(module_location, module_org): +@pytest.fixture(scope='module') +def reader_user(module_location, module_org, module_target_sat): password = gen_string('alphanumeric') - reader_role = entities.Role().search(query={'search': 'name="Discovery Reader"'})[0] - reader_user = entities.User( + # Applying Discovery reader_role to the user + role = module_target_sat.api.Role().search(query={'search': 'name="Discovery Reader"'})[0] + reader_user = module_target_sat.api.User( login=gen_string('alpha'), - role=[reader_role], + role=[role], password=password, organization=[module_org], location=[module_location], @@ -87,86 +71,68 @@ def gen_int32(min_value=1): @pytest.mark.tier2 -def test_positive_create_rule_with_non_admin_user(manager_loc, manager_user, module_org, test_name): - """Create rule with non-admin user by associating discovery_manager role +def test_positive_crud_with_non_admin_user( + module_location, manager_user, module_org, module_target_sat +): + """CRUD with non-admin user by associating discovery_manager role :id: 6a03983b-363d-4646-b277-34af5f5abc55 - :expectedresults: Rule should be created successfully. + :expectedresults: All crud operations should work with non_admin user. :CaseLevel: Integration """ - name = gen_string('alpha') + rule_name = gen_string('alpha') search = gen_string('alpha') - hg = entities.HostGroup(organization=[module_org]).create() - with Session(test_name, user=manager_user.login, password=manager_user.password) as session: - session.location.select(loc_name=manager_loc.name) + priority = str(gen_integer(1, 20)) + new_rule_name = gen_string('alpha') + new_search = gen_string('alpha') + new_hg_name = gen_string('alpha') + new_priority = str(gen_integer(101, 200)) + hg = module_target_sat.api.HostGroup(organization=[module_org]).create() + new_hg_name = module_target_sat.api.HostGroup(organization=[module_org]).create() + with Session(user=manager_user.login, password=manager_user.password) as session: + session.location.select(loc_name=module_location.name) session.discoveryrule.create( { - 'primary.name': name, + 'primary.name': rule_name, 'primary.search': search, 'primary.host_group': hg.name, - 'primary.priority': gen_int32(), + 'primary.priority': priority, } ) - dr_val = session.discoveryrule.read(name, widget_names='primary') - assert dr_val['primary']['name'] == name - assert dr_val['primary']['host_group'] == hg.name - - -@pytest.mark.tier2 -def test_positive_delete_rule_with_non_admin_user(manager_loc, manager_user, module_org, test_name): - """Delete rule with non-admin user by associating discovery_manager role - - :id: 7fa56bab-82d7-46c9-a4fa-c44ef173c703 - - :expectedresults: Rule should be deleted successfully. - - :CaseLevel: Integration - """ - hg = entities.HostGroup(organization=[module_org]).create() - dr = entities.DiscoveryRule( - hostgroup=hg, organization=[module_org], location=[manager_loc] - ).create() - with Session(test_name, user=manager_user.login, password=manager_user.password) as session: - dr_val = session.discoveryrule.read_all() - assert dr.name in [rule['Name'] for rule in dr_val] - session.discoveryrule.delete(dr.name) - dr_val = session.discoveryrule.read_all() - assert dr.name not in [rule['Name'] for rule in dr_val] - - -@pytest.mark.tier2 -def test_positive_view_existing_rule_with_non_admin_user( - module_location, module_org, reader_user, test_name -): - """Existing rule should be viewed to non-admin user by associating - discovery_reader role. - :id: 0f5b0221-43be-47bc-8619-749824c4e54f - - :Steps: - - 1. create a rule with admin user - 2. create a non-admin user and assign 'Discovery Reader' role - 3. Login with non-admin user - - :expectedresults: Rule should be visible to non-admin user. + values = session.discoveryrule.read(rule_name, widget_names='primary') + assert values['primary']['name'] == rule_name + assert values['primary']['search'] == search + assert values['primary']['host_group'] == hg.name + assert values['primary']['priority'] == priority + session.discoveryrule.update( + rule_name, + { + 'primary.name': new_rule_name, + 'primary.search': new_search, + 'primary.host_group': new_hg_name.name, + 'primary.priority': new_priority, + }, + ) + values = session.discoveryrule.read( + new_rule_name, + widget_names='primary', + ) + assert values['primary']['name'] == new_rule_name + assert values['primary']['search'] == new_search + assert values['primary']['host_group'] == new_hg_name.name + assert values['primary']['priority'] == new_priority - :CaseLevel: Integration - """ - hg = entities.HostGroup(organization=[module_org]).create() - dr = entities.DiscoveryRule( - hostgroup=hg, organization=[module_org], location=[module_location] - ).create() - with Session(test_name, user=reader_user.login, password=reader_user.password) as session: + session.discoveryrule.delete(new_rule_name) dr_val = session.discoveryrule.read_all() - assert dr.name in [rule['Name'] for rule in dr_val] + assert new_rule_name not in [rule['Name'] for rule in dr_val] @pytest.mark.tier2 def test_negative_delete_rule_with_non_admin_user( - module_location, module_org, reader_user, test_name + module_location, module_org, module_target_sat, reader_user ): """Delete rule with non-admin user by associating discovery_reader role @@ -177,11 +143,20 @@ def test_negative_delete_rule_with_non_admin_user( :CaseLevel: Integration """ - hg = entities.HostGroup(organization=[module_org]).create() - dr = entities.DiscoveryRule( - hostgroup=hg, organization=[module_org], location=[module_location] + hg_name = gen_string('alpha') + rule_name = gen_string('alpha') + search = gen_string('alpha') + hg = module_target_sat.api.HostGroup( + name=hg_name, organization=[module_org], location=[module_location] + ).create() + dr = module_target_sat.api.DiscoveryRule( + name=rule_name, + search_=search, + hostgroup=hg.id, + organization=[module_org], + location=[module_location], ).create() - with Session(test_name, user=reader_user.login, password=reader_user.password) as session: + with Session(user=reader_user.login, password=reader_user.password) as session: with pytest.raises(ValueError): session.discoveryrule.delete(dr.name) dr_val = session.discoveryrule.read_all() @@ -218,8 +193,8 @@ def test_positive_list_host_based_on_rule_search_query( cpu_count = gen_integer(2, 10) rule_search = f'cpu_count = {cpu_count}' # any way create a host to be sure that this org has more than one host - host = entities.Host(organization=module_org, location=module_location).create() - host_group = entities.HostGroup( + host = target_sat.api.Host(organization=module_org, location=module_location).create() + host_group = target_sat.api.HostGroup( organization=[module_org], location=[module_location], medium=host.medium, @@ -229,7 +204,7 @@ def test_positive_list_host_based_on_rule_search_query( domain=host.domain, architecture=host.architecture, ).create() - discovery_rule = entities.DiscoveryRule( + discovery_rule = target_sat.api.DiscoveryRule( hostgroup=host_group, search_=rule_search, organization=[module_org], @@ -241,7 +216,7 @@ def test_positive_list_host_based_on_rule_search_query( ) # create an other discovered host with an other cpu count target_sat.api_factory.create_discovered_host(options={'physicalprocessorcount': cpu_count + 1}) - provisioned_host_name = '{}.{}'.format(discovered_host['name'], host.domain.read().name) + provisioned_host_name = f'{host.domain.read().name}' with session: values = session.discoveryrule.read_all() assert discovery_rule.name in [rule['Name'] for rule in values] @@ -254,16 +229,17 @@ def test_positive_list_host_based_on_rule_search_query( session.discoveredhosts.apply_action('Auto Provision', [discovered_host['name']]) assert not session.discoveredhosts.search('name = "{}"'.format(discovered_host['name'])) values = session.discoveryrule.read_associated_hosts(discovery_rule.name) + host_name = values['table'][0]['Name'] assert values['searchbox'] == f'discovery_rule = "{discovery_rule.name}"' assert len(values['table']) == 1 - assert values['table'][0]['Name'] == provisioned_host_name - values = session.host.get_details(provisioned_host_name) + assert provisioned_host_name in host_name + values = session.host.get_details(host_name) assert values['properties']['properties_table']['IP Address'] == ip_address @pytest.mark.tier3 @pytest.mark.upgrade -def test_positive_end_to_end(session, module_org, module_location): +def test_positive_end_to_end(session, module_org, module_location, module_target_sat): """Perform end to end testing for discovery rule component. :id: dd35e566-dc3a-43d3-939c-a33ae528740f @@ -273,23 +249,25 @@ def test_positive_end_to_end(session, module_org, module_location): :CaseImportance: Critical """ rule_name = gen_string('alpha') - search = f'cpu_count = {gen_integer(1, 5)}' + search = gen_string('alpha') hg_name = gen_string('alpha') hostname = gen_string('alpha') hosts_limit = str(gen_integer(0, 100)) priority = str(gen_integer(1, 100)) new_rule_name = gen_string('alpha') - new_search = f'cpu_count = {gen_integer(6, 10)}' + new_search = gen_string('alpha') new_hg_name = gen_string('alpha') new_hostname = gen_string('alpha') new_hosts_limit = str(gen_integer(101, 200)) new_priority = str(gen_integer(101, 200)) - entities.HostGroup(name=hg_name, organization=[module_org], location=[module_location]).create() - entities.HostGroup( + module_target_sat.api.HostGroup( + name=hg_name, organization=[module_org], location=[module_location] + ).create() + module_target_sat.api.HostGroup( name=new_hg_name, organization=[module_org], location=[module_location] ).create() - new_org = entities.Organization().create() - new_loc = entities.Location().create() + new_org = module_target_sat.api.Organization().create() + new_loc = module_target_sat.api.Location().create() with session: session.discoveryrule.create( { From a2540bdc7fd6659a7bfa71bd8befe48432bdd3e3 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 24 Aug 2023 03:17:30 -0400 Subject: [PATCH 156/586] [6.14.z] Bump sphinx from 7.2.2 to 7.2.3 (#12331) Bump sphinx from 7.2.2 to 7.2.3 (#12329) (cherry picked from commit 9b1bf611c085bab5f1be61c61aa6ca0b1e7f1092) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 0f7055ed133..13b53f9163d 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -5,7 +5,7 @@ redis==5.0.0 pre-commit==3.3.3 # For generating documentation. -sphinx==7.2.2 +sphinx==7.2.3 sphinx-autoapi==2.1.1 # For 'manage' interactive shell From 65b27848a4b03b6ad260944571418c361f3c06d5 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 24 Aug 2023 05:51:02 -0400 Subject: [PATCH 157/586] [6.14.z] bypass cvv promote to lce if already promoted (#12339) --- robottelo/cli/factory.py | 13 ++++++++----- robottelo/host_helpers/cli_factory.py | 15 +++++++++++---- tests/foreman/longrun/test_oscap.py | 5 ++--- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/robottelo/cli/factory.py b/robottelo/cli/factory.py index 397af1fb612..78deaf5ff29 100644 --- a/robottelo/cli/factory.py +++ b/robottelo/cli/factory.py @@ -1687,13 +1687,16 @@ def setup_org_for_a_custom_repo(options=None): ContentView.publish({'id': cv_id}) except CLIReturnCodeError as err: raise CLIFactoryError(f'Failed to publish new version of content view\n{err.msg}') - # Get the version id - cvv = ContentView.info({'id': cv_id})['versions'][-1] + # Get the content view info + cv_info = ContentView.info({'id': cv_id}) + lce_promoted = cv_info['lifecycle-environments'] + cvv = cv_info['versions'][-1] # Promote version to next env try: - ContentView.version_promote( - {'id': cvv['id'], 'organization-id': org_id, 'to-lifecycle-environment-id': env_id} - ) + if env_id not in [int(lce['id']) for lce in lce_promoted]: + ContentView.version_promote( + {'id': cvv['id'], 'organization-id': org_id, 'to-lifecycle-environment-id': env_id} + ) except CLIReturnCodeError as err: raise CLIFactoryError(f'Failed to promote version to next environment\n{err.msg}') # Create activation key if needed and associate content view with it diff --git a/robottelo/host_helpers/cli_factory.py b/robottelo/host_helpers/cli_factory.py index c79f0d047ee..b78370bd9e8 100644 --- a/robottelo/host_helpers/cli_factory.py +++ b/robottelo/host_helpers/cli_factory.py @@ -628,12 +628,19 @@ def setup_org_for_a_custom_repo(self, options=None): except CLIReturnCodeError as err: raise CLIFactoryError(f'Failed to publish new version of content view\n{err.msg}') # Get the version id - cvv = self._satellite.cli.ContentView.info({'id': cv_id})['versions'][-1] + cv_info = self._satellite.cli.ContentView.info({'id': cv_id}) + lce_promoted = cv_info['lifecycle-environments'] + cvv = cv_info['versions'][-1] # Promote version to next env try: - self._satellite.cli.ContentView.version_promote( - {'id': cvv['id'], 'organization-id': org_id, 'to-lifecycle-environment-id': env_id} - ) + if env_id not in [int(lce['id']) for lce in lce_promoted]: + self._satellite.cli.ContentView.version_promote( + { + 'id': cvv['id'], + 'organization-id': org_id, + 'to-lifecycle-environment-id': env_id, + } + ) except CLIReturnCodeError as err: raise CLIFactoryError(f'Failed to promote version to next environment\n{err.msg}') # Create activation key if needed and associate content view with it diff --git a/tests/foreman/longrun/test_oscap.py b/tests/foreman/longrun/test_oscap.py index 496d6a6c6e4..a7f163c2829 100644 --- a/tests/foreman/longrun/test_oscap.py +++ b/tests/foreman/longrun/test_oscap.py @@ -25,7 +25,6 @@ from robottelo.cli.arfreport import Arfreport from robottelo.cli.factory import make_hostgroup from robottelo.cli.factory import make_scap_policy -from robottelo.cli.factory import setup_org_for_a_custom_repo from robottelo.cli.host import Host from robottelo.cli.job_invocation import JobInvocation from robottelo.cli.proxy import Proxy @@ -96,7 +95,7 @@ def content_view(module_org): @pytest.fixture(scope='module', autouse=True) -def activation_key(module_org, lifecycle_env, content_view): +def activation_key(module_target_sat, module_org, lifecycle_env, content_view): """Create activation keys""" repo_values = [ {'repo': settings.repos.satclient_repo.rhel8, 'akname': ak_name['rhel8']}, @@ -109,7 +108,7 @@ def activation_key(module_org, lifecycle_env, content_view): name=repo.get('akname'), environment=lifecycle_env, organization=module_org ).create() # Setup org for a custom repo for RHEL6, RHEL7 and RHEL8. - setup_org_for_a_custom_repo( + module_target_sat.cli_factory.setup_org_for_a_custom_repo( { 'url': repo.get('repo'), 'organization-id': module_org.id, From b7330566b7eefa471ff256bec7992ee0ffeb0c8c Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 24 Aug 2023 08:07:35 -0400 Subject: [PATCH 158/586] [6.14.z] Deprecating api/utils.py module (#12325) Deprecating api/utils.py module (cherry picked from commit 8096996483d72af95f4bc4cd09e1315737360298) Co-authored-by: jyejare --- robottelo/api/__init__.py | 0 robottelo/api/utils.py | 799 -------------------------- robottelo/host_helpers/api_factory.py | 128 +++++ tests/robottelo/test_api_utils.py | 1 - 4 files changed, 128 insertions(+), 800 deletions(-) delete mode 100644 robottelo/api/__init__.py delete mode 100644 robottelo/api/utils.py delete mode 100644 tests/robottelo/test_api_utils.py diff --git a/robottelo/api/__init__.py b/robottelo/api/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/robottelo/api/utils.py b/robottelo/api/utils.py deleted file mode 100644 index 42597a9c6e0..00000000000 --- a/robottelo/api/utils.py +++ /dev/null @@ -1,799 +0,0 @@ -"""Module containing convenience functions for working with the API.""" -import time - -from fauxfactory import gen_ipaddr -from fauxfactory import gen_mac -from fauxfactory import gen_string -from nailgun import entities -from nailgun import entity_mixins -from nailgun.client import request -from nailgun.entity_mixins import call_entity_method_with_timeout -from requests import HTTPError - -from robottelo import ssh -from robottelo.config import get_url -from robottelo.config import settings -from robottelo.constants import DEFAULT_ARCHITECTURE -from robottelo.constants import DEFAULT_PTABLE -from robottelo.constants import DEFAULT_PXE_TEMPLATE -from robottelo.constants import DEFAULT_TEMPLATE -from robottelo.constants import REPO_TYPE -from robottelo.exceptions import ImproperlyConfigured - - -def enable_rhrepo_and_fetchid( - basearch, org_id, product, repo, reposet, releasever=None, strict=False -): - """Enable a RedHat Repository and fetches it's Id. - - :param str org_id: The organization Id. - :param str product: The product name in which repository exists. - :param str reposet: The reposet name in which repository exists. - :param str repo: The repository name who's Id is to be fetched. - :param str basearch: The architecture of the repository. - :param str optional releasever: The releasever of the repository. - :param bool optional strict: Raise exception if the reposet was already enabled. - :return: Returns the repository Id. - :rtype: str - - """ - product = entities.Product(name=product, organization=org_id).search()[0] - r_set = entities.RepositorySet(name=reposet, product=product).search()[0] - payload = {} - if basearch is not None: - payload['basearch'] = basearch - if releasever is not None: - payload['releasever'] = releasever - payload['product_id'] = product.id - try: - r_set.enable(data=payload) - except HTTPError as e: - if ( - not strict - and e.response.status_code == 409 - and 'repository is already enabled' in e.response.json()['displayMessage'] - ): - pass - else: - raise - result = entities.Repository(name=repo).search(query={'organization_id': org_id}) - return result[0].id - - -def create_sync_custom_repo( - org_id=None, - product_name=None, - repo_name=None, - repo_url=None, - repo_type=None, - repo_unprotected=True, - docker_upstream_name=None, -): - """Create product/repo, sync it and returns repo_id""" - if org_id is None: - org_id = entities.Organization().create().id - product_name = product_name or gen_string('alpha') - repo_name = repo_name or gen_string('alpha') - # Creates new product and repository via API's - product = entities.Product(name=product_name, organization=org_id).create() - repo = entities.Repository( - name=repo_name, - url=repo_url or settings.repos.yum_1.url, - content_type=repo_type or REPO_TYPE['yum'], - product=product, - unprotected=repo_unprotected, - docker_upstream_name=docker_upstream_name, - ).create() - # Sync repository - entities.Repository(id=repo.id).sync() - return repo.id - - -def enable_sync_redhat_repo(rh_repo, org_id, timeout=1500): - """Enable the RedHat repo, sync it and returns repo_id""" - # Enable RH repo and fetch repository_id - repo_id = enable_rhrepo_and_fetchid( - basearch=rh_repo['basearch'], - org_id=org_id, - product=rh_repo['product'], - repo=rh_repo['name'], - reposet=rh_repo['reposet'], - releasever=rh_repo['releasever'], - ) - # Sync repository - call_entity_method_with_timeout(entities.Repository(id=repo_id).sync, timeout=timeout) - return repo_id - - -def cv_publish_promote(name=None, env_name=None, repo_id=None, org_id=None): - """Create, publish and promote CV to selected environment""" - if org_id is None: - org_id = entities.Organization().create().id - # Create Life-Cycle content environment - kwargs = {'name': env_name} if env_name is not None else {} - lce = entities.LifecycleEnvironment(organization=org_id, **kwargs).create() - # Create content view(CV) - kwargs = {'name': name} if name is not None else {} - content_view = entities.ContentView(organization=org_id, **kwargs).create() - # Associate YUM repo to created CV - if repo_id is not None: - content_view.repository = [entities.Repository(id=repo_id)] - content_view = content_view.update(['repository']) - # Publish content view - content_view.publish() - # Promote the content view version. - content_view.read().version[0].promote(data={'environment_ids': lce.id}) - return content_view.read() - - -def one_to_one_names(name): - """Generate the names Satellite might use for a one to one field. - - Example of usage:: - - >>> one_to_one_names('person') == {'person_name', 'person_id'} - True - - :param name: A field name. - :returns: A set including both ``name`` and variations on ``name``. - - """ - return {name + '_name', name + '_id'} - - -def configure_provisioning(org=None, loc=None, compute=False, os=None): - """Create and configure org, loc, product, repo, cv, env. Update proxy, - domain, subnet, compute resource, provision templates and medium with - previously created entities and create a hostgroup using all mentioned - entities. - - :param str org: Default Organization that should be used in both host - discovering and host provisioning procedures - :param str loc: Default Location that should be used in both host - discovering and host provisioning procedures - :param bool compute: If False creates a default Libvirt compute resource - :param str os: Specify the os to be used while provisioning and to - associate related entities to the specified os. - :return: List of created entities that can be re-used further in - provisioning or validation procedure (e.g. hostgroup or domain) - """ - # Create new organization and location in case they were not passed - if org is None: - org = entities.Organization().create() - if loc is None: - loc = entities.Location(organization=[org]).create() - if settings.repos.rhel7_os is None: - raise ImproperlyConfigured('settings file is not configured for rhel os') - # Create a new Life-Cycle environment - lc_env = entities.LifecycleEnvironment(organization=org).create() - # Create a Product, Repository for custom RHEL7 contents - product = entities.Product(organization=org).create() - repo = entities.Repository( - product=product, url=settings.repos.rhel7_os, download_policy='immediate' - ).create() - - # Increased timeout value for repo sync and CV publishing and promotion - try: - old_task_timeout = entity_mixins.TASK_TIMEOUT - entity_mixins.TASK_TIMEOUT = 3600 - repo.sync() - # Create, Publish and promote CV - content_view = entities.ContentView(organization=org).create() - content_view.repository = [repo] - content_view = content_view.update(['repository']) - content_view.publish() - content_view = content_view.read() - content_view.read().version[0].promote(data={'environment_ids': lc_env.id}) - finally: - entity_mixins.TASK_TIMEOUT = old_task_timeout - # Search for existing organization puppet environment, otherwise create a - # new one, associate organization and location where it is appropriate. - environments = entities.Environment().search(query=dict(search=f'organization_id={org.id}')) - if len(environments) > 0: - environment = environments[0].read() - environment.location.append(loc) - environment = environment.update(['location']) - else: - environment = entities.Environment(organization=[org], location=[loc]).create() - - # Search for SmartProxy, and associate location - proxy = entities.SmartProxy().search(query={'search': f'name={settings.server.hostname}'}) - proxy = proxy[0].read() - if loc.id not in [location.id for location in proxy.location]: - proxy.location.append(loc) - if org.id not in [organization.id for organization in proxy.organization]: - proxy.organization.append(org) - proxy = proxy.update(['location', 'organization']) - - # Search for existing domain or create new otherwise. Associate org, - # location and dns to it - _, _, domain = settings.server.hostname.partition('.') - domain = entities.Domain().search(query={'search': f'name="{domain}"'}) - if len(domain) == 1: - domain = domain[0].read() - domain.location.append(loc) - domain.organization.append(org) - domain.dns = proxy - domain = domain.update(['dns', 'location', 'organization']) - else: - domain = entities.Domain(dns=proxy, location=[loc], organization=[org]).create() - - # Search if subnet is defined with given network. - # If so, just update its relevant fields otherwise, - # Create new subnet - network = settings.vlan_networking.subnet - subnet = entities.Subnet().search(query={'search': f'network={network}'}) - if len(subnet) == 1: - subnet = subnet[0].read() - subnet.domain = [domain] - subnet.location.append(loc) - subnet.organization.append(org) - subnet.dns = proxy - subnet.dhcp = proxy - subnet.tftp = proxy - subnet.discovery = proxy - subnet.ipam = 'DHCP' - subnet = subnet.update( - ['domain', 'discovery', 'dhcp', 'dns', 'location', 'organization', 'tftp', 'ipam'] - ) - else: - # Create new subnet - subnet = entities.Subnet( - network=network, - mask=settings.vlan_networking.netmask, - domain=[domain], - location=[loc], - organization=[org], - dns=proxy, - dhcp=proxy, - tftp=proxy, - discovery=proxy, - ipam='DHCP', - ).create() - - # Search if Libvirt compute-resource already exists - # If so, just update its relevant fields otherwise, - # Create new compute-resource with 'libvirt' provider. - # compute boolean is added to not block existing test's that depend on - # Libvirt resource and use this same functionality to all CR's. - if compute is False: - resource_url = f'qemu+ssh://root@{settings.libvirt.libvirt_hostname}/system' - comp_res = [ - res - for res in entities.LibvirtComputeResource().search() - if res.provider == 'Libvirt' and res.url == resource_url - ] - if len(comp_res) > 0: - computeresource = entities.LibvirtComputeResource(id=comp_res[0].id).read() - computeresource.location.append(loc) - computeresource.organization.append(org) - computeresource.update(['location', 'organization']) - else: - # Create Libvirt compute-resource - entities.LibvirtComputeResource( - provider='libvirt', - url=resource_url, - set_console_password=False, - display_type='VNC', - location=[loc.id], - organization=[org.id], - ).create() - - # Get the Partition table ID - ptable = ( - entities.PartitionTable().search(query={'search': f'name="{DEFAULT_PTABLE}"'})[0].read() - ) - if loc.id not in [location.id for location in ptable.location]: - ptable.location.append(loc) - if org.id not in [organization.id for organization in ptable.organization]: - ptable.organization.append(org) - ptable = ptable.update(['location', 'organization']) - - # Get the OS ID - if os is None: - os = ( - entities.OperatingSystem() - .search(query={'search': 'name="RedHat" AND (major="6" OR major="7")'})[0] - .read() - ) - else: - os_ver = os.split(' ')[1].split('.') - os = ( - entities.OperatingSystem() - .search( - query={ - 'search': f'family="Redhat" AND major="{os_ver[0]}" AND minor="{os_ver[1]}")' - } - )[0] - .read() - ) - - # Get the Provisioning template_ID and update with OS, Org, Location - provisioning_template = entities.ProvisioningTemplate().search( - query={'search': f'name="{DEFAULT_TEMPLATE}"'} - ) - provisioning_template = provisioning_template[0].read() - provisioning_template.operatingsystem.append(os) - if org.id not in [organization.id for organization in provisioning_template.organization]: - provisioning_template.organization.append(org) - if loc.id not in [location.id for location in provisioning_template.location]: - provisioning_template.location.append(loc) - provisioning_template = provisioning_template.update( - ['location', 'operatingsystem', 'organization'] - ) - - # Get the PXE template ID and update with OS, Org, location - pxe_template = entities.ProvisioningTemplate().search( - query={'search': f'name="{DEFAULT_PXE_TEMPLATE}"'} - ) - pxe_template = pxe_template[0].read() - pxe_template.operatingsystem.append(os) - if org.id not in [organization.id for organization in pxe_template.organization]: - pxe_template.organization.append(org) - if loc.id not in [location.id for location in pxe_template.location]: - pxe_template.location.append(loc) - pxe_template = pxe_template.update(['location', 'operatingsystem', 'organization']) - - # Get the arch ID - arch = ( - entities.Architecture().search(query={'search': f'name="{DEFAULT_ARCHITECTURE}"'})[0].read() - ) - - # Update the OS to associate arch, ptable, templates - os.architecture.append(arch) - os.ptable.append(ptable) - os.provisioning_template.append(provisioning_template) - os.provisioning_template.append(pxe_template) - os = os.update(['architecture', 'provisioning_template', 'ptable']) - # kickstart_repository is the content view and lce bind repo - kickstart_repository = entities.Repository().search( - query=dict(content_view_id=content_view.id, environment_id=lc_env.id, name=repo.name) - )[0] - # Create Hostgroup - host_group = entities.HostGroup( - architecture=arch, - domain=domain.id, - subnet=subnet.id, - lifecycle_environment=lc_env.id, - content_view=content_view.id, - location=[loc.id], - environment=environment.id, - puppet_proxy=proxy, - puppet_ca_proxy=proxy, - content_source=proxy, - kickstart_repository=kickstart_repository, - root_pass=gen_string('alphanumeric'), - operatingsystem=os.id, - organization=[org.id], - ptable=ptable.id, - ).create() - - return { - 'host_group': host_group.name, - 'domain': domain.name, - 'environment': environment.name, - 'ptable': ptable.name, - 'subnet': subnet.name, - 'os': os.title, - } - - -def create_role_permissions(role, permissions_types_names, search=None): # pragma: no cover - """Create role permissions found in dict permissions_types_names. - - :param role: nailgun.entities.Role - :param permissions_types_names: a dict containing resource types - and permission names to add to the role. - :param search: string that contains search criteria that should be applied - to the filter - - example usage:: - - permissions_types_names = { - None: ['access_dashboard'], - 'Organization': ['view_organizations'], - 'Location': ['view_locations'], - 'Katello::KTEnvironment': [ - 'view_lifecycle_environments', - 'edit_lifecycle_environments', - 'promote_or_remove_content_views_to_environments' - ] - } - role = entities.Role(name='example_role_name').create() - create_role_permissions( - role, - permissions_types_names, - 'name = {0}'.format(lce.name) - ) - """ - for resource_type, permissions_name in permissions_types_names.items(): - if resource_type is None: - permissions_entities = [] - for name in permissions_name: - result = entities.Permission().search(query={'search': f'name="{name}"'}) - if not result: - raise entities.APIResponseError(f'permission "{name}" not found') - if len(result) > 1: - raise entities.APIResponseError( - f'found more than one entity for permission "{name}"' - ) - entity_permission = result[0] - if entity_permission.name != name: - raise entities.APIResponseError( - 'the returned permission is different from the' - ' requested one "{} != {}"'.format(entity_permission.name, name) - ) - permissions_entities.append(entity_permission) - else: - if not permissions_name: - raise ValueError( - 'resource type "{}" empty. You must select at' - ' least one permission'.format(resource_type) - ) - - resource_type_permissions_entities = entities.Permission().search( - query={'per_page': '350', 'search': f'resource_type="{resource_type}"'} - ) - if not resource_type_permissions_entities: - raise entities.APIResponseError( - f'resource type "{resource_type}" permissions not found' - ) - - permissions_entities = [ - entity - for entity in resource_type_permissions_entities - if entity.name in permissions_name - ] - # ensure that all the requested permissions entities where - # retrieved - permissions_entities_names = {entity.name for entity in permissions_entities} - not_found_names = set(permissions_name).difference(permissions_entities_names) - if not_found_names: - raise entities.APIResponseError( - f'permissions names entities not found "{not_found_names}"' - ) - entities.Filter(permission=permissions_entities, role=role, search=search).create() - - -def wait_for_tasks( - search_query, search_rate=1, max_tries=10, poll_rate=None, poll_timeout=None, must_succeed=True -): - """Search for tasks by specified search query and poll them to ensure that - task has finished. - - :param search_query: Search query that will be passed to API call. - :param search_rate: Delay between searches. - :param max_tries: How many times search should be executed. - :param poll_rate: Delay between the end of one task check-up and - the start of the next check-up. Parameter for - ``nailgun.entities.ForemanTask.poll()`` method. - :param poll_timeout: Maximum number of seconds to wait until timing out. - Parameter for ``nailgun.entities.ForemanTask.poll()`` method. - :param must_succeed: Assert success result on finished task. - :return: List of ``nailgun.entities.ForemanTasks`` entities. - :raises: ``AssertionError``. If not tasks were found until timeout. - """ - for _ in range(max_tries): - tasks = entities.ForemanTask().search(query={'search': search_query}) - if len(tasks) > 0: - for task in tasks: - task.poll(poll_rate=poll_rate, timeout=poll_timeout, must_succeed=must_succeed) - break - else: - time.sleep(search_rate) - else: - raise AssertionError(f"No task was found using query '{search_query}'") - return tasks - - -def wait_for_syncplan_tasks(repo_backend_id=None, timeout=10, repo_name=None): - """Search the pulp tasks and identify repositories sync tasks with - specified name or backend_identifier - - :param repo_backend_id: The Backend ID for the repository to identify the - repo in Pulp environment - :param timeout: Value to decided how long to check for the Sync task - :param repo_name: If repo_backend_id can not be passed, pass the repo_name - """ - if repo_name: - repo_backend_id = ( - entities.Repository() - .search(query={'search': f'name="{repo_name}"', 'per_page': '1000'})[0] - .backend_identifier - ) - # Fetch the Pulp password - pulp_pass = ssh.command( - 'grep "^default_password" /etc/pulp/server.conf | awk \'{print $2}\'' - ).stdout.splitlines()[0] - # Set the Timeout value - timeup = time.time() + int(timeout) * 60 - # Search Filter to filter out the task based on backend-id and sync action - filtered_req = { - 'criteria': { - 'filters': { - 'tags': {'$in': [f"pulp:repository:{repo_backend_id}"]}, - 'task_type': {'$in': ["pulp.server.managers.repo.sync.sync"]}, - } - } - } - while True: - if time.time() > timeup: - raise entities.APIResponseError(f'Pulp task with repo_id {repo_backend_id} not found') - # Send request to pulp API to get the task info - req = request( - 'POST', - f'{get_url()}/pulp/api/v2/tasks/search/', - verify=False, - auth=('admin', f'{pulp_pass}'), - headers={'content-type': 'application/json'}, - data=filtered_req, - ) - # Check Status code of response - if req.status_code != 200: - raise entities.APIResponseError(f'Pulp task with repo_id {repo_backend_id} not found') - # Check content of response - # It is '[]' string for empty content when backend_identifier is wrong - if len(req.content) > 2: - if req.json()[0].get('state') in ['finished']: - return True - elif req.json()[0].get('error'): - raise AssertionError( - f"Pulp task with repo_id {repo_backend_id} error or not found: " - f"'{req.json().get('error')}'" - ) - time.sleep(2) - - -def wait_for_errata_applicability_task( - host_id, from_when, search_rate=1, max_tries=10, poll_rate=None, poll_timeout=15 -): - """Search the generate applicability task for given host and make sure it finishes - - :param int host_id: Content host ID of the host where we are regenerating applicability. - :param int from_when: Timestamp (in UTC) to limit number of returned tasks to investigate. - :param int search_rate: Delay between searches. - :param int max_tries: How many times search should be executed. - :param int poll_rate: Delay between the end of one task check-up and - the start of the next check-up. Parameter for - ``nailgun.entities.ForemanTask.poll()`` method. - :param int poll_timeout: Maximum number of seconds to wait until timing out. - Parameter for ``nailgun.entities.ForemanTask.poll()`` method. - :return: Relevant errata applicability task. - :raises: ``AssertionError``. If not tasks were found for given host until timeout. - """ - assert isinstance(host_id, int), 'Param host_id have to be int' - assert isinstance(from_when, int), 'Param from_when have to be int' - now = int(time.time()) - assert from_when <= now, 'Param from_when have to be timestamp in the past' - for _ in range(max_tries): - now = int(time.time()) - max_age = now - from_when + 1 - search_query = ( - '( label = Actions::Katello::Host::GenerateApplicability OR label = ' - 'Actions::Katello::Host::UploadPackageProfile ) AND started_at > "%s seconds ago"' - % max_age - ) - tasks = entities.ForemanTask().search(query={'search': search_query}) - tasks_finished = 0 - for task in tasks: - if ( - task.label == 'Actions::Katello::Host::GenerateApplicability' - and host_id in task.input['host_ids'] - ): - task.poll(poll_rate=poll_rate, timeout=poll_timeout) - tasks_finished += 1 - elif ( - task.label == 'Actions::Katello::Host::UploadPackageProfile' - and host_id == task.input['host']['id'] - ): - task.poll(poll_rate=poll_rate, timeout=poll_timeout) - tasks_finished += 1 - if tasks_finished > 0: - break - time.sleep(search_rate) - else: - raise AssertionError(f"No task was found using query '{search_query}' for host '{host_id}'") - - -def create_discovered_host(name=None, ip_address=None, mac_address=None, options=None): - """Creates a discovered host. - - :param str name: Name of discovered host. - :param str ip_address: A valid ip address. - :param str mac_address: A valid mac address. - :param dict options: additional facts to add to discovered host - :return: dict of ``entities.DiscoveredHost`` facts. - """ - if name is None: - name = gen_string('alpha') - if ip_address is None: - ip_address = gen_ipaddr() - if mac_address is None: - mac_address = gen_mac(multicast=False) - if options is None: - options = {} - facts = { - 'name': name, - 'discovery_bootip': ip_address, - 'discovery_bootif': mac_address, - 'interfaces': 'eth0', - 'ipaddress': ip_address, - 'ipaddress_eth0': ip_address, - 'macaddress': mac_address, - 'macaddress_eth0': mac_address, - } - facts.update(options) - return entities.DiscoveredHost().facts(json={'facts': facts}) - - -def update_vm_host_location(vm_client, location_id): - """Update vm client host location. - - :param vm_client: A subscribed Virtual Machine client instance. - :param location_id: The location id to update the vm_client host with. - """ - host = entities.Host().search(query={'search': f'name={vm_client.hostname}'})[0] - host.location = entities.Location(id=location_id) - host.update(['location']) - - -def check_create_os_with_title(os_title): - """Check if the OS is present, if not create the required OS - - :param os_title: OS title to check, and create (like: RedHat 7.5) - :return: Created or found OS - """ - # Check if OS that image needs is present or no, If not create the OS - result = entities.OperatingSystem().search(query={'search': f'title="{os_title}"'}) - if result: - os = result[0] - else: - os_name, _, os_version = os_title.partition(' ') - os_version_major, os_version_minor = os_version.split('.') - os = entities.OperatingSystem( - name=os_name, major=os_version_major, minor=os_version_minor - ).create() - return os - - -def attach_custom_product_subscription(prod_name=None, host_name=None): - """Attach custom product subscription to client host - :param str prod_name: custom product name - :param str host_name: client host name - """ - host = entities.Host().search(query={'search': f'{host_name}'})[0] - product_subscription = entities.Subscription().search(query={'search': f'name={prod_name}'})[0] - entities.HostSubscription(host=host.id).add_subscriptions( - data={'subscriptions': [{'id': product_subscription.id, 'quantity': 1}]} - ) - - -class templateupdate: - """Context Manager to unlock lock template for updating""" - - def __init__(self, temp): - """Context manager that takes entities.ProvisioningTemplate's object - - :param entities.ProvisioningTemplate temp: entities.ProvisioningTemplate's object - """ - self.temp = temp - if not isinstance(self.temp, entities.ProvisioningTemplate): - raise TypeError( - 'The template should be of type entities.ProvisioningTemplate, {} given'.format( - type(temp) - ) - ) - - def __enter__(self): - """Unlocks template for update""" - if self.temp.locked: - self.temp.locked = False - self.temp.update(['locked']) - - def __exit__(self, exc_type, exc_val, exc_tb): - """Locks template after update""" - if not self.temp.locked: - self.temp.locked = True - self.temp.update(['locked']) - - -def update_provisioning_template(name=None, old=None, new=None): - """Update provisioning template content - - :param str name: template provisioning name - :param str old: current content - :param str new: replace content - - :return bool: True/False - """ - temp = ( - entities.ProvisioningTemplate() - .search(query={'per_page': '1000', 'search': f'name="{name}"'})[0] - .read() - ) - if old in temp.template: - with templateupdate(temp): - temp.template = temp.template.replace(old, new, 1) - update = temp.update(['template']) - return new in update.template - elif new in temp.template: - return True - else: - raise ValueError(f'{old} does not exists in template {name}') - - -def apply_package_filter(content_view, repo, package, inclusion=True): - """Apply package filter on content view - - :param content_view: entity content view - :param repo: entity repository - :param str package: package name to filter - :param bool inclusion: True/False based on include or exclude filter - - :return list : list of content view versions - """ - cv_filter = entities.RPMContentViewFilter( - content_view=content_view, inclusion=inclusion, repository=[repo] - ).create() - cv_filter_rule = entities.ContentViewFilterRule( - content_view_filter=cv_filter, name=package - ).create() - assert cv_filter.id == cv_filter_rule.content_view_filter.id - content_view.publish() - content_view = content_view.read() - content_view_version_info = content_view.version[0].read() - return content_view_version_info - - -def create_org_admin_role(orgs, locs, name=None): - """Helper function to create org admin role for particular - organizations and locations by cloning 'Organization admin' role. - - :param list orgs: The list of organizations for which the org admin is - being created - :param list locs: The list of locations for which the org admin is - being created - :param str name: The name of cloned Org Admin role, autogenerates if None provided - :return dict: The object of ```nailgun.Role``` of Org Admin role. - """ - name = gen_string('alpha') if not name else name - default_org_admin = entities.Role().search(query={'search': 'name="Organization admin"'}) - org_admin = entities.Role(id=default_org_admin[0].id).clone( - data={'role': {'name': name, 'organization_ids': orgs or [], 'location_ids': locs or []}} - ) - return entities.Role(id=org_admin['id']).read() - - -def create_org_admin_user(orgs, locs): - """Helper function to create an Org Admin user by assigning org admin role and assign - taxonomies to Role and User - - The taxonomies for role and user will be assigned based on parameters of this function - - :return User: Returns the ```nailgun.entities.User``` object with passwd attr - """ - # Create Org Admin Role - org_admin = create_org_admin_role(orgs=orgs, locs=locs) - # Create Org Admin User - user_login = gen_string('alpha') - user_passwd = gen_string('alphanumeric') - user = entities.User( - login=user_login, - password=user_passwd, - organization=orgs, - location=locs, - role=[org_admin.id], - ).create() - user.passwd = user_passwd - return user - - -def disable_syncplan(sync_plan): - """ - Disable sync plans after a test to reduce distracting task events, logs, and load on Satellite. - Note that only a Sync Plan with a repo would create a noticeable load. - You can also create sync plans in a disabled state where it is unlikely to impact the test. - """ - sync_plan.enabled = False - sync_plan = sync_plan.update(['enabled']) - assert sync_plan.enabled is False diff --git a/robottelo/host_helpers/api_factory.py b/robottelo/host_helpers/api_factory.py index d74290934b5..23219148a53 100644 --- a/robottelo/host_helpers/api_factory.py +++ b/robottelo/host_helpers/api_factory.py @@ -2,12 +2,14 @@ It is not meant to be used directly, but as part of a robottelo.hosts.Satellite instance example: my_satellite.api_factory.api_method() """ +import time from contextlib import contextmanager from fauxfactory import gen_ipaddr from fauxfactory import gen_mac from fauxfactory import gen_string from nailgun import entity_mixins +from nailgun.client import request from nailgun.entity_mixins import call_entity_method_with_timeout from requests import HTTPError @@ -630,3 +632,129 @@ def template_update(self, temp): if not template.locked: template.locked = True template.update(['locked']) + + def attach_custom_product_subscription(self, prod_name=None, host_name=None): + """Attach custom product subscription to client host + :param str prod_name: custom product name + :param str host_name: client host name + """ + host = self._satellite.api.Host().search(query={'search': f'{host_name}'})[0] + product_subscription = self._satellite.api.Subscription().search( + query={'search': f'name={prod_name}'} + )[0] + self._satellite.api.HostSubscription(host=host.id).add_subscriptions( + data={'subscriptions': [{'id': product_subscription.id, 'quantity': 1}]} + ) + + def wait_for_errata_applicability_task( + self, host_id, from_when, search_rate=1, max_tries=10, poll_rate=None, poll_timeout=15 + ): + """Search the generate applicability task for given host and make sure it finishes + + :param int host_id: Content host ID of the host where we are regenerating applicability. + :param int from_when: Timestamp (in UTC) to limit number of returned tasks to investigate. + :param int search_rate: Delay between searches. + :param int max_tries: How many times search should be executed. + :param int poll_rate: Delay between the end of one task check-up and + the start of the next check-up. Parameter for + ``nailgun.entities.ForemanTask.poll()`` method. + :param int poll_timeout: Maximum number of seconds to wait until timing out. + Parameter for ``nailgun.entities.ForemanTask.poll()`` method. + :return: Relevant errata applicability task. + :raises: ``AssertionError``. If not tasks were found for given host until timeout. + """ + assert isinstance(host_id, int), 'Param host_id have to be int' + assert isinstance(from_when, int), 'Param from_when have to be int' + now = int(time.time()) + assert from_when <= now, 'Param from_when have to be timestamp in the past' + for _ in range(max_tries): + now = int(time.time()) + max_age = now - from_when + 1 + search_query = ( + '( label = Actions::Katello::Host::GenerateApplicability OR label = ' + 'Actions::Katello::Host::UploadPackageProfile ) AND started_at > "%s seconds ago"' + % max_age + ) + tasks = self._satellite.api.ForemanTask().search(query={'search': search_query}) + tasks_finished = 0 + for task in tasks: + if ( + task.label == 'Actions::Katello::Host::GenerateApplicability' + and host_id in task.input['host_ids'] + ): + task.poll(poll_rate=poll_rate, timeout=poll_timeout) + tasks_finished += 1 + elif ( + task.label == 'Actions::Katello::Host::UploadPackageProfile' + and host_id == task.input['host']['id'] + ): + task.poll(poll_rate=poll_rate, timeout=poll_timeout) + tasks_finished += 1 + if tasks_finished > 0: + break + time.sleep(search_rate) + else: + raise AssertionError( + f"No task was found using query '{search_query}' for host '{host_id}'" + ) + + 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 + + :param repo_backend_id: The Backend ID for the repository to identify the + repo in Pulp environment + :param timeout: Value to decided how long to check for the Sync task + :param repo_name: If repo_backend_id can not be passed, pass the repo_name + """ + if repo_name: + repo_backend_id = ( + self._satellite.api.Repository() + .search(query={'search': f'name="{repo_name}"', 'per_page': '1000'})[0] + .backend_identifier + ) + # Fetch the Pulp password + pulp_pass = self._satellite.execute( + 'grep "^default_password" /etc/pulp/server.conf | awk \'{print $2}\'' + ).stdout.splitlines()[0] + # Set the Timeout value + timeup = time.time() + int(timeout) * 60 + # Search Filter to filter out the task based on backend-id and sync action + filtered_req = { + 'criteria': { + 'filters': { + 'tags': {'$in': [f"pulp:repository:{repo_backend_id}"]}, + 'task_type': {'$in': ["pulp.server.managers.repo.sync.sync"]}, + } + } + } + while True: + if time.time() > timeup: + raise self._satellite.api.APIResponseError( + f'Pulp task with repo_id {repo_backend_id} not found' + ) + # Send request to pulp API to get the task info + req = request( + 'POST', + f'{self._satellite.url}/pulp/api/v2/tasks/search/', + verify=False, + auth=('admin', f'{pulp_pass}'), + headers={'content-type': 'application/json'}, + data=filtered_req, + ) + # Check Status code of response + if req.status_code != 200: + raise self._satellite.api.APIResponseError( + f'Pulp task with repo_id {repo_backend_id} not found' + ) + # Check content of response + # It is '[]' string for empty content when backend_identifier is wrong + if len(req.content) > 2: + if req.json()[0].get('state') in ['finished']: + return True + elif req.json()[0].get('error'): + raise AssertionError( + f"Pulp task with repo_id {repo_backend_id} error or not found: " + f"'{req.json().get('error')}'" + ) + time.sleep(2) diff --git a/tests/robottelo/test_api_utils.py b/tests/robottelo/test_api_utils.py deleted file mode 100644 index 3a78416366c..00000000000 --- a/tests/robottelo/test_api_utils.py +++ /dev/null @@ -1 +0,0 @@ -"""Unit tests for :mod:`robottelo.api.utils`.""" From 2e1e6a9b9aa5ddf62e231025f08c78df0db981f4 Mon Sep 17 00:00:00 2001 From: Jameer Pathan Date: Wed, 23 Aug 2023 20:05:02 +0200 Subject: [PATCH 159/586] Fix fixture_setup_rhc_satellite (#12317) (cherry picked from commit eade9fe5c50a97cc6e390e1d96d9572f17f91ed6) --- tests/foreman/ui/test_rhc.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/foreman/ui/test_rhc.py b/tests/foreman/ui/test_rhc.py index c1d6f658d22..d704b50094b 100644 --- a/tests/foreman/ui/test_rhc.py +++ b/tests/foreman/ui/test_rhc.py @@ -72,12 +72,10 @@ def fixture_setup_rhc_satellite( module_entitlement_manifest, ): """Create Organization and activation key after successful test execution""" + if settings.rh_cloud.crc_env == 'prod': + module_target_sat.upload_manifest(module_rhc_org.id, module_entitlement_manifest.content) yield if request.node.rep_call.passed: - if settings.rh_cloud.crc_env == 'prod': - module_target_sat.cli.Subscription.upload( - {'file': module_entitlement_manifest.filename, 'organization-id': module_rhc_org.id} - ) # Enable and sync required repos repo1_id = module_target_sat.api_factory.enable_sync_redhat_repo( constants.REPOS['rhel8_aps'], module_rhc_org.id From 7be1872574c7eab5655ffd2adbe81c5d6a34bb89 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 25 Aug 2023 06:08:48 -0400 Subject: [PATCH 160/586] [6.14.z] Add Libvirt CR API e2e test and some refactor (#12355) --- .../component/provision_libvirt.py | 13 +- robottelo/constants/__init__.py | 1 - .../api/test_computeresource_libvirt.py | 461 +++++------------- tests/foreman/api/test_host.py | 3 - tests/foreman/ui/test_organization.py | 1 - 5 files changed, 119 insertions(+), 360 deletions(-) diff --git a/pytest_fixtures/component/provision_libvirt.py b/pytest_fixtures/component/provision_libvirt.py index 130f837454b..16f2d962e27 100644 --- a/pytest_fixtures/component/provision_libvirt.py +++ b/pytest_fixtures/component/provision_libvirt.py @@ -1,15 +1,14 @@ # Compute resource - Libvirt entities import pytest -from nailgun import entities -@pytest.fixture(scope="module") -def module_cr_libvirt(module_org, module_location): - return entities.LibvirtComputeResource( +@pytest.fixture(scope='module') +def module_cr_libvirt(module_target_sat, module_org, module_location): + return module_target_sat.api.LibvirtComputeResource( organization=[module_org], location=[module_location] ).create() -@pytest.fixture(scope="module") -def module_libvirt_image(module_cr_libvirt): - return entities.Image(compute_resource=module_cr_libvirt).create() +@pytest.fixture(scope='module') +def module_libvirt_image(module_target_sat, module_cr_libvirt): + return module_target_sat.api.Image(compute_resource=module_cr_libvirt).create() diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index b6b29c613e0..067bd813420 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -58,7 +58,6 @@ class Colored(Box): 'ec2': 'EC2', 'vmware': 'VMware', 'openstack': 'RHEL OpenStack Platform', - 'rackspace': 'Rackspace', 'google': 'Google', 'azurerm': 'Azure Resource Manager', } diff --git a/tests/foreman/api/test_computeresource_libvirt.py b/tests/foreman/api/test_computeresource_libvirt.py index 97fb2d4a88c..a76e4d84ee5 100644 --- a/tests/foreman/api/test_computeresource_libvirt.py +++ b/tests/foreman/api/test_computeresource_libvirt.py @@ -20,67 +20,93 @@ :Upstream: No """ -from random import randint - import pytest from fauxfactory import gen_string -from nailgun import entities from requests.exceptions import HTTPError from robottelo.config import settings +from robottelo.constants import FOREMAN_PROVIDERS from robottelo.constants import LIBVIRT_RESOURCE_URL from robottelo.utils.datafactory import invalid_values_list from robottelo.utils.datafactory import parametrized from robottelo.utils.datafactory import valid_data_list -pytestmark = [ - pytest.mark.skip_if_not_set('libvirt'), -] - +pytestmark = [pytest.mark.skip_if_not_set('libvirt')] -@pytest.fixture(scope="module") -def setup(): - """Set up organization and location for tests.""" - setupEntities = type("", (), {})() - setupEntities.org = entities.Organization().create() - setupEntities.loc = entities.Location(organization=[setupEntities.org]).create() - setupEntities.current_libvirt_url = LIBVIRT_RESOURCE_URL % settings.libvirt.libvirt_hostname - return setupEntities +LIBVIRT_URL = LIBVIRT_RESOURCE_URL % settings.libvirt.libvirt_hostname -@pytest.mark.tier1 -@pytest.mark.parametrize('name', **parametrized(valid_data_list())) -def test_positive_create_with_name(setup, name): - """Create compute resources with different names +@pytest.mark.e2e +def test_positive_crud_libvirt_cr(module_target_sat, module_org, module_location): + """CRUD compute resource libvirt - :id: 1e545c56-2f53-44c1-a17e-38c83f8fe0c1 + :id: 1e545c56-2f53-44c1-a17e-38c83f8fe0c2 :expectedresults: Compute resources are created with expected names :CaseImportance: Critical :CaseLevel: Component - - :parametrized: yes """ - compresource = entities.LibvirtComputeResource( - location=[setup.loc], + name = gen_string('alphanumeric') + description = gen_string('alphanumeric') + display_type = 'spice' + libvirt_url = 'qemu+tcp://libvirt.example.com:16509/system' + + cr = module_target_sat.api.LibvirtComputeResource( name=name, - organization=[setup.org], - url=setup.current_libvirt_url, + description=description, + provider=FOREMAN_PROVIDERS['libvirt'], + display_type=display_type, + organization=[module_org], + location=[module_location], + url=libvirt_url, ).create() - assert compresource.name == name + assert cr.name == name + assert cr.description == description + assert cr.provider == FOREMAN_PROVIDERS['libvirt'] + assert cr.display_type == display_type + assert cr.url == libvirt_url + + # Update + new_name = gen_string('alphanumeric') + new_description = gen_string('alphanumeric') + new_display_type = 'vnc' + new_org = module_target_sat.api.Organization().create() + new_loc = module_target_sat.api.Location(organization=[new_org]).create() + cr.name = new_name + cr.description = new_description + cr.display_type = new_display_type + cr.url = LIBVIRT_URL + cr.organization = [new_org] + cr.location = [new_loc] + cr.update(['name', 'description', 'display_type', 'url', 'organization', 'location']) + + # READ + updated_cr = module_target_sat.api.LibvirtComputeResource(id=cr.id).read() + assert updated_cr.name == new_name + assert updated_cr.description == new_description + assert updated_cr.display_type == new_display_type + assert updated_cr.url == LIBVIRT_URL + assert updated_cr.organization[0].id == new_org.id + assert updated_cr.location[0].id == new_loc.id + # DELETE + updated_cr.delete() + assert not module_target_sat.api.LibvirtComputeResource().search( + query={'search': f'name={new_name}'} + ) @pytest.mark.tier1 -@pytest.mark.parametrize('description', **parametrized(valid_data_list())) -def test_positive_create_with_description(setup, description): - """Create compute resources with different descriptions +@pytest.mark.parametrize('name', **parametrized(valid_data_list())) +def test_positive_create_with_name_description( + name, request, module_target_sat, module_org, module_location +): + """Create compute resources with different names and descriptions - :id: 1fa5b35d-ee47-452b-bb5f-4a4ca321f992 + :id: 1e545c56-2f53-44c1-a17e-38c83f8fe0c1 - :expectedresults: Compute resources are created with expected - descriptions + :expectedresults: Compute resources are created with expected names and descriptions :CaseImportance: Critical @@ -88,64 +114,21 @@ def test_positive_create_with_description(setup, description): :parametrized: yes """ - compresource = entities.LibvirtComputeResource( - description=description, - location=[setup.loc], - organization=[setup.org], - url=setup.current_libvirt_url, - ).create() - assert compresource.description == description - - -@pytest.mark.tier2 -@pytest.mark.parametrize('display_type', ['spice', 'vnc']) -def test_positive_create_libvirt_with_display_type(setup, display_type): - """Create a libvirt compute resources with different values of - 'display_type' parameter - - :id: 76380f31-e217-4ff1-ac6b-20f41e59f133 - - :expectedresults: Compute resources are created with expected - display_type value - - :CaseImportance: High - - :CaseLevel: Component - - :parametrized: yes - """ - compresource = entities.LibvirtComputeResource( - display_type=display_type, - location=[setup.loc], - organization=[setup.org], - url=setup.current_libvirt_url, + compresource = module_target_sat.api.LibvirtComputeResource( + name=name, + description=name, + organization=[module_org], + location=[module_location], + url=LIBVIRT_URL, ).create() - assert compresource.display_type == display_type - - -@pytest.mark.tier1 -def test_positive_create_with_provider(setup): - """Create compute resources with different providers. Testing only - Libvirt and Docker as other providers require valid credentials - - :id: f61c66c9-15f8-4b00-9e53-7ebfb09397cc - - :expectedresults: Compute resources are created with expected providers - - :CaseImportance: Critical - - :CaseLevel: Component - """ - entity = entities.LibvirtComputeResource() - entity.location = [setup.loc] - entity.organization = [setup.org] - result = entity.create() - assert result.provider == entity.provider + assert compresource.name == name + assert compresource.description == name + request.addfinalizer(compresource.delete) @pytest.mark.tier2 -def test_positive_create_with_locs(setup): - """Create a compute resource with multiple locations +def test_positive_create_with_orgs_and_locs(request, module_target_sat): + """Create a compute resource with multiple organizations and locations :id: c6c6c6f7-50ca-4f38-8126-eb95359d7cbb @@ -156,248 +139,19 @@ def test_positive_create_with_locs(setup): :CaseLevel: Integration """ - locs = [entities.Location(organization=[setup.org]).create() for _ in range(randint(3, 5))] - compresource = entities.LibvirtComputeResource( - location=locs, organization=[setup.org], url=setup.current_libvirt_url - ).create() - assert {loc.name for loc in locs} == {loc.read().name for loc in compresource.location} - - -@pytest.mark.tier2 -def test_positive_create_with_orgs(setup): - """Create a compute resource with multiple organizations - - :id: 2f6e5019-6353-477e-a81f-2a551afc7556 - - :expectedresults: A compute resource is created with expected multiple - organizations assigned - - :CaseImportance: High - - :CaseLevel: Integration - """ - orgs = [entities.Organization().create() for _ in range(randint(3, 5))] - compresource = entities.LibvirtComputeResource( - organization=orgs, url=setup.current_libvirt_url + orgs = [module_target_sat.api.Organization().create() for _ in range(2)] + locs = [module_target_sat.api.Location(organization=[org]).create() for org in orgs] + compresource = module_target_sat.api.LibvirtComputeResource( + location=locs, organization=orgs, url=LIBVIRT_URL ).create() assert {org.name for org in orgs} == {org.read().name for org in compresource.organization} - - -@pytest.mark.tier1 -@pytest.mark.parametrize('new_name', **parametrized(valid_data_list())) -def test_positive_update_name(setup, new_name): - """Update a compute resource with different names - - :id: 60f08418-b1a2-445e-9cd6-dbc92a33b57a - - :expectedresults: Compute resource is updated with expected names - - :CaseImportance: Critical - - :CaseLevel: Component - - :parametrized: yes - """ - compresource = entities.LibvirtComputeResource( - location=[setup.loc], organization=[setup.org], url=setup.current_libvirt_url - ).create() - compresource.name = new_name - compresource = compresource.update(['name']) - assert compresource.name == new_name - - -@pytest.mark.tier2 -@pytest.mark.parametrize('new_description', **parametrized(valid_data_list())) -def test_positive_update_description(setup, new_description): - """Update a compute resource with different descriptions - - :id: aac5dc53-8709-441b-b360-28b8efd3f63f - - :expectedresults: Compute resource is updated with expected - descriptions - - :CaseImportance: High - - :CaseLevel: Component - - :parametrized: yes - """ - compresource = entities.LibvirtComputeResource( - description=gen_string('alpha'), - location=[setup.loc], - organization=[setup.org], - url=setup.current_libvirt_url, - ).create() - compresource.description = new_description - compresource = compresource.update(['description']) - assert compresource.description == new_description - - -@pytest.mark.tier2 -@pytest.mark.parametrize('display_type', ['spice', 'vnc']) -def test_positive_update_libvirt_display_type(setup, display_type): - """Update a libvirt compute resource with different values of - 'display_type' parameter - - :id: 0cbf08ac-acc4-476a-b389-271cea2b6cda - - :expectedresults: Compute resource is updated with expected - display_type value - - :CaseImportance: High - - :CaseLevel: Component - - :parametrized: yes - """ - compresource = entities.LibvirtComputeResource( - display_type='VNC', - location=[setup.loc], - organization=[setup.org], - url=setup.current_libvirt_url, - ).create() - compresource.display_type = display_type - compresource = compresource.update(['display_type']) - assert compresource.display_type == display_type - - -@pytest.mark.tier2 -def test_positive_update_url(setup): - """Update a compute resource's url field - - :id: 259aa060-ed9e-4ed5-91e1-7fb0a3592879 - - :expectedresults: Compute resource is updated with expected url - - :CaseImportance: High - - :CaseLevel: Component - """ - new_url = 'qemu+tcp://localhost:16509/system' - - compresource = entities.LibvirtComputeResource( - location=[setup.loc], organization=[setup.org], url=setup.current_libvirt_url - ).create() - compresource.url = new_url - compresource = compresource.update(['url']) - assert compresource.url == new_url - - -@pytest.mark.tier2 -def test_positive_update_loc(setup): - """Update a compute resource's location - - :id: 57e96c7c-da9e-4400-af80-c374cd6b3d4a - - :expectedresults: Compute resource is updated with expected location - - :CaseImportance: High - - :CaseLevel: Integration - """ - compresource = entities.LibvirtComputeResource( - location=[setup.loc], organization=[setup.org], url=setup.current_libvirt_url - ).create() - new_loc = entities.Location(organization=[setup.org]).create() - compresource.location = [new_loc] - compresource = compresource.update(['location']) - assert len(compresource.location) == 1 - assert compresource.location[0].id == new_loc.id - - -@pytest.mark.tier2 -def test_positive_update_locs(setup): - """Update a compute resource with new multiple locations - - :id: cda9f501-2879-4cb0-a017-51ee795232f1 - - :expectedresults: Compute resource is updated with expected locations - - :CaseImportance: High - - :CaseLevel: Integration - """ - compresource = entities.LibvirtComputeResource( - location=[setup.loc], organization=[setup.org], url=setup.current_libvirt_url - ).create() - new_locs = [entities.Location(organization=[setup.org]).create() for _ in range(randint(3, 5))] - compresource.location = new_locs - compresource = compresource.update(['location']) - assert {location.id for location in compresource.location} == { - location.id for location in new_locs - } - - -@pytest.mark.tier2 -def test_positive_update_org(setup): - """Update a compute resource's organization - - :id: 430b64a2-7f64-4344-a73b-1b47d8dfa6cb - - :expectedresults: Compute resource is updated with expected - organization - - :CaseImportance: High - - :CaseLevel: Integration - """ - compresource = entities.LibvirtComputeResource( - organization=[setup.org], url=setup.current_libvirt_url - ).create() - new_org = entities.Organization().create() - compresource.organization = [new_org] - compresource = compresource.update(['organization']) - assert len(compresource.organization) == 1 - assert compresource.organization[0].id == new_org.id - - -@pytest.mark.tier2 -def test_positive_update_orgs(setup): - """Update a compute resource with new multiple organizations - - :id: 2c759ad5-d115-46d9-8365-712c0bb39a1d - - :expectedresults: Compute resource is updated with expected - organizations - - :CaseImportance: High - - :CaseLevel: Integration - """ - compresource = entities.LibvirtComputeResource( - organization=[setup.org], url=setup.current_libvirt_url - ).create() - new_orgs = [entities.Organization().create() for _ in range(randint(3, 5))] - compresource.organization = new_orgs - compresource = compresource.update(['organization']) - assert {organization.id for organization in compresource.organization} == { - organization.id for organization in new_orgs - } - - -@pytest.mark.tier1 -def test_positive_delete(setup): - """Delete a compute resource - - :id: 0117a4f1-e2c2-44aa-8919-453166aeebbc - - :expectedresults: Compute resources is successfully deleted - - :CaseImportance: Critical - - :CaseLevel: Component - """ - compresource = entities.LibvirtComputeResource( - location=[setup.loc], organization=[setup.org], url=setup.current_libvirt_url - ).create() - compresource.delete() - with pytest.raises(HTTPError): - compresource.read() + assert {loc.name for loc in locs} == {loc.read().name for loc in compresource.location} + request.addfinalizer(compresource.delete) @pytest.mark.tier2 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) -def test_negative_create_with_invalid_name(setup, name): +def test_negative_create_with_invalid_name(name, module_target_sat, module_org, module_location): """Attempt to create compute resources with invalid names :id: f73bf838-3ffd-46d3-869c-81b334b47b13 @@ -411,16 +165,16 @@ def test_negative_create_with_invalid_name(setup, name): :parametrized: yes """ with pytest.raises(HTTPError): - entities.LibvirtComputeResource( - location=[setup.loc], + module_target_sat.api.LibvirtComputeResource( name=name, - organization=[setup.org], - url=setup.current_libvirt_url, + organization=[module_org], + location=[module_location], + url=LIBVIRT_URL, ).create() @pytest.mark.tier2 -def test_negative_create_with_same_name(setup): +def test_negative_create_with_same_name(request, module_target_sat, module_org, module_location): """Attempt to create a compute resource with already existing name :id: 9376e25c-2aa8-4d99-83aa-2eec160c030e @@ -432,21 +186,23 @@ def test_negative_create_with_same_name(setup): :CaseLevel: Component """ name = gen_string('alphanumeric') - entities.LibvirtComputeResource( - location=[setup.loc], name=name, organization=[setup.org], url=setup.current_libvirt_url + cr = module_target_sat.api.LibvirtComputeResource( + location=[module_location], name=name, organization=[module_org], url=LIBVIRT_URL ).create() + assert cr.name == name + request.addfinalizer(cr.delete) with pytest.raises(HTTPError): - entities.LibvirtComputeResource( - location=[setup.loc], + module_target_sat.api.LibvirtComputeResource( name=name, - organization=[setup.org], - url=setup.current_libvirt_url, + organization=[module_org], + location=[module_location], + url=LIBVIRT_URL, ).create() @pytest.mark.tier2 @pytest.mark.parametrize('url', **parametrized({'random': gen_string('alpha'), 'empty': ''})) -def test_negative_create_with_url(setup, url): +def test_negative_create_with_url(module_target_sat, module_org, module_location, url): """Attempt to create compute resources with invalid url :id: 37e9bf39-382e-4f02-af54-d3a17e285c2a @@ -460,14 +216,16 @@ def test_negative_create_with_url(setup, url): :parametrized: yes """ with pytest.raises(HTTPError): - entities.LibvirtComputeResource( - location=[setup.loc], organization=[setup.org], url=url + module_target_sat.api.LibvirtComputeResource( + location=[module_location], organization=[module_org], url=url ).create() @pytest.mark.tier2 @pytest.mark.parametrize('new_name', **parametrized(invalid_values_list())) -def test_negative_update_invalid_name(setup, new_name): +def test_negative_update_invalid_name( + request, module_target_sat, module_org, module_location, new_name +): """Attempt to update compute resource with invalid names :id: a6554c1f-e52f-4614-9fc3-2127ced31470 @@ -481,9 +239,10 @@ def test_negative_update_invalid_name(setup, new_name): :parametrized: yes """ name = gen_string('alphanumeric') - compresource = entities.LibvirtComputeResource( - location=[setup.loc], name=name, organization=[setup.org], url=setup.current_libvirt_url + compresource = module_target_sat.api.LibvirtComputeResource( + location=[module_location], name=name, organization=[module_org], url=LIBVIRT_URL ).create() + request.addfinalizer(compresource.delete) with pytest.raises(HTTPError): compresource.name = new_name compresource.update(['name']) @@ -491,7 +250,7 @@ def test_negative_update_invalid_name(setup, new_name): @pytest.mark.tier2 -def test_negative_update_same_name(setup): +def test_negative_update_same_name(request, module_target_sat, module_org, module_location): """Attempt to update a compute resource with already existing name :id: 4d7c5eb0-b8cb-414f-aa10-fe464a164ab4 @@ -503,21 +262,26 @@ def test_negative_update_same_name(setup): :CaseLevel: Component """ name = gen_string('alphanumeric') - entities.LibvirtComputeResource( - location=[setup.loc], name=name, organization=[setup.org], url=setup.current_libvirt_url + compresource = module_target_sat.api.LibvirtComputeResource( + location=[module_location], name=name, organization=[module_org], url=LIBVIRT_URL ).create() - new_compresource = entities.LibvirtComputeResource( - location=[setup.loc], organization=[setup.org], url=setup.current_libvirt_url + new_compresource = module_target_sat.api.LibvirtComputeResource( + location=[module_location], organization=[module_org], url=LIBVIRT_URL ).create() with pytest.raises(HTTPError): new_compresource.name = name new_compresource.update(['name']) assert new_compresource.read().name != name + @request.addfinalizer + def _finalize(): + compresource.delete() + new_compresource.delete() + @pytest.mark.tier2 @pytest.mark.parametrize('url', **parametrized({'random': gen_string('alpha'), 'empty': ''})) -def test_negative_update_url(setup, url): +def test_negative_update_url(url, request, module_target_sat, module_org, module_location): """Attempt to update a compute resource with invalid url :id: b5256090-2ceb-4976-b54e-60d60419fe50 @@ -530,9 +294,10 @@ def test_negative_update_url(setup, url): :parametrized: yes """ - compresource = entities.LibvirtComputeResource( - location=[setup.loc], organization=[setup.org], url=setup.current_libvirt_url + compresource = module_target_sat.api.LibvirtComputeResource( + location=[module_location], organization=[module_org], url=LIBVIRT_URL ).create() + request.addfinalizer(compresource.delete) with pytest.raises(HTTPError): compresource.url = url compresource.update(['url']) diff --git a/tests/foreman/api/test_host.py b/tests/foreman/api/test_host.py index 11897ec1590..e43c2f7669e 100644 --- a/tests/foreman/api/test_host.py +++ b/tests/foreman/api/test_host.py @@ -423,7 +423,6 @@ def test_positive_create_and_update_with_subnet(module_location, module_org, mod @pytest.mark.tier2 -@pytest.mark.on_premises_provisioning def test_positive_create_and_update_with_compresource( module_org, module_location, module_cr_libvirt ): @@ -689,7 +688,6 @@ def test_positive_end_to_end_with_host_parameters(module_org, module_location): @pytest.mark.tier2 @pytest.mark.e2e -@pytest.mark.on_premises_provisioning def test_positive_end_to_end_with_image( module_org, module_location, module_cr_libvirt, module_libvirt_image ): @@ -721,7 +719,6 @@ def test_positive_end_to_end_with_image( @pytest.mark.tier1 -@pytest.mark.on_premises_provisioning @pytest.mark.parametrize('method', ['build', 'image']) def test_positive_create_with_provision_method( method, module_org, module_location, module_cr_libvirt diff --git a/tests/foreman/ui/test_organization.py b/tests/foreman/ui/test_organization.py index 45025939870..278c26cda11 100644 --- a/tests/foreman/ui/test_organization.py +++ b/tests/foreman/ui/test_organization.py @@ -223,7 +223,6 @@ def test_positive_create_with_all_users(session): @pytest.mark.skip_if_not_set('libvirt') -@pytest.mark.on_premises_provisioning @pytest.mark.tier2 def test_positive_update_compresource(session): """Add/Remove compute resource from/to organization. From 8d26edf5a2987944489bc911c9a78747b3ca6590 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 25 Aug 2023 07:24:00 -0400 Subject: [PATCH 161/586] [6.14.z] Remove compute_resource_docker (#12353) Remove compute_resource_docker (#12121) remove docker_CR Signed-off-by: Adarsh Dubey (cherry picked from commit 597df7512a4d683d1203acb3f95d046f3c14ff60) Co-authored-by: Adarsh dubey --- .../cli/test_computeresource_docker.py | 117 ------------------ 1 file changed, 117 deletions(-) delete mode 100644 tests/foreman/cli/test_computeresource_docker.py diff --git a/tests/foreman/cli/test_computeresource_docker.py b/tests/foreman/cli/test_computeresource_docker.py deleted file mode 100644 index d6c38b3c9a4..00000000000 --- a/tests/foreman/cli/test_computeresource_docker.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Tests for the Docker Compute Resource feature - -:Requirement: Provisioning - -:CaseAutomation: Automated - -:CaseLevel: Component - -:TestType: Functional - -:Team: Rocket - -:CaseComponent: Provisioning - -:CaseImportance: High - -:Upstream: No -""" -import pytest -from fauxfactory import gen_string - -from robottelo.cli.factory import make_product_wait -from robottelo.cli.factory import make_repository -from robottelo.cli.factory import Repository -from robottelo.constants import CONTAINER_REGISTRY_HUB -from robottelo.constants import CONTAINER_UPSTREAM_NAME -from robottelo.constants import REPO_TYPE - - -@pytest.mark.skip_if_not_set('docker') -@pytest.mark.tier3 -@pytest.mark.upgrade -def test_positive_upload_image(module_org, target_sat, container_contenthost): - """A Docker-enabled client can create a new ``Dockerfile`` - pointing to an existing Docker image from a Satellite 6 and modify it. - Then, using ``docker build`` generate a new image which can then be - uploaded back onto the Satellite 6 as a new repository. - - :id: 2c47559c-b27f-436e-9b1e-df5c3633b007 - - :Steps: - - 1. Create a local docker compute resource - 2. Create a container and start it - 3. [on docker host] Commit a new image from the container - 4. [on docker host] Export the image to tar - 5. scp the image to satellite box - 6. create a new docker repo - 7. upload the image to the new repo - - :expectedresults: Client can create a new image based off an existing - Docker image from a Satellite instance, add a new package and - upload the modified image (plus layer) back to the Satellite. - - :parametrized: yes - """ - try: - """ - These functions were removed, but let's leave them here - to maintain overall test logic - in case required functionality - is eventually implemented - - compute_resource = make_compute_resource({ - 'organization-ids': [module_org.id], - 'provider': 'Docker', - 'url': f'http://{container_contenthost.ip_addr}:2375', - }) - container = make_container({ - 'compute-resource-id': compute_resource['id'], - 'organization-ids': [module_org.id], - }) - Docker.container.start({'id': container['id']}) - """ - container = {'uuid': 'stubbed test'} - repo_name = gen_string('alphanumeric').lower() - - # Commit a new docker image and verify image was created - image_name = f'{repo_name}/{CONTAINER_UPSTREAM_NAME}' - result = container_contenthost.execute( - f'docker commit {container["uuid"]} {image_name}:latest && ' - f'docker images --all | grep {image_name}' - ) - assert result.status == 0 - - # Save the image to a tar archive - result = container_contenthost.execute(f'docker save -o {repo_name}.tar {image_name}') - assert result.status == 0 - - tar_file = f'{repo_name}.tar' - container_contenthost.get(remote_path=tar_file) - target_sat.put( - local_path=tar_file, - remote_path=f'/tmp/{tar_file}', - ) - - # Upload tarred repository - product = make_product_wait({'organization-id': module_org.id}) - repo = make_repository( - { - 'content-type': REPO_TYPE['docker'], - 'docker-upstream-name': CONTAINER_UPSTREAM_NAME, - 'name': gen_string('alpha', 5), - 'product-id': product['id'], - 'url': CONTAINER_REGISTRY_HUB, - } - ) - Repository.upload_content({'id': repo['id'], 'path': f'/tmp/{tar_file}'}) - - # Verify repository was uploaded successfully - repo = Repository.info({'id': repo['id']}) - assert target_sat.hostname == repo['published-at'] - - repo_name = '-'.join((module_org.label, product['label'], repo['label'])).lower() - assert repo_name in repo['published-at'] - finally: - # Remove the archive - target_sat.execute(f'rm -f /tmp/{tar_file}') From 21a87f866a96f5cadc49fc1cf2e4e5c361aa361f Mon Sep 17 00:00:00 2001 From: Griffin Sullivan <48397354+Griffin-Sullivan@users.noreply.github.com> Date: Fri, 25 Aug 2023 14:41:47 -0400 Subject: [PATCH 162/586] [6.14.z] Add service list check (#12357) Add service list check --- tests/foreman/maintain/test_service.py | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/foreman/maintain/test_service.py b/tests/foreman/maintain/test_service.py index ed6f9262ff7..510c0d0a017 100644 --- a/tests/foreman/maintain/test_service.py +++ b/tests/foreman/maintain/test_service.py @@ -23,9 +23,33 @@ from robottelo.constants import HAMMER_CONFIG from robottelo.constants import MAINTAIN_HAMMER_YML from robottelo.constants import SATELLITE_ANSWER_FILE +from robottelo.hosts import Satellite pytestmark = pytest.mark.destructive +SATELLITE_SERVICES = [ + "dynflow-sidekiq@.service", + "foreman-proxy.service", + "foreman.service", + "httpd.service", + "postgresql.service", + "pulpcore-api.service", + "pulpcore-content.service", + "pulpcore-worker@.service", + "redis.service", + "tomcat.service", +] + +CAPSULE_SERVICES = [ + "foreman-proxy.service", + "httpd.service", + "postgresql.service", + "pulpcore-api.service", + "pulpcore-content.service", + "pulpcore-worker@.service", + "redis.service", +] + @pytest.fixture def missing_hammer_config(request, sat_maintain): @@ -64,6 +88,9 @@ def test_positive_service_list(sat_maintain): result = sat_maintain.cli.Service.list() assert 'FAIL' not in result.stdout assert result.status == 0 + services = SATELLITE_SERVICES if type(sat_maintain) is Satellite else CAPSULE_SERVICES + for service in services: + assert service in result.stdout @pytest.mark.include_capsule From bea49fe4f03f0ee9d46ba26808ce3b45e697da26 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 28 Aug 2023 02:24:53 -0400 Subject: [PATCH 163/586] [6.14.z] Bump python-box from 7.0.1 to 7.1.1 (#12368) Bump python-box from 7.0.1 to 7.1.1 (#12360) (cherry picked from commit 2abd71488e20c8e53767be3591097fd2ec5e3bee) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2888594b554..a7d7bc6f698 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ manifester==0.0.13 navmazing==1.1.6 productmd==1.36 pyotp==2.9.0 -python-box==7.0.1 +python-box==7.1.1 pytest==7.4.0 pytest-services==2.2.1 pytest-mock==3.11.1 From ec21b3d3f32e138e7f25cecec770f32ab2423566 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 28 Aug 2023 02:26:24 -0400 Subject: [PATCH 164/586] [6.14.z] Bump dynaconf[vault] from 3.2.1 to 3.2.2 (#12362) Bump dynaconf[vault] from 3.2.1 to 3.2.2 (#12361) (cherry picked from commit c429bbd720fdee87b6538c4f66f1052a65346119) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a7d7bc6f698..effc5cddadc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ betelgeuse==1.10.0 broker[docker]==0.3.3 cryptography==41.0.3 deepdiff==6.3.1 -dynaconf[vault]==3.2.1 +dynaconf[vault]==3.2.2 fauxfactory==3.1.0 jinja2==3.1.2 manifester==0.0.13 From 8a4ac7f4ca77b6972fb4a85812ab3a807652d109 Mon Sep 17 00:00:00 2001 From: vsedmik <46570670+vsedmik@users.noreply.github.com> Date: Mon, 28 Aug 2023 15:19:57 +0200 Subject: [PATCH 165/586] [6.14.z] Decapitalize katello-tracer to match BZ component (#12383) Decapitalize katello-tracer to match BZ component --- testimony.yaml | 2 +- tests/foreman/cli/test_host.py | 2 +- tests/foreman/ui/test_host.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/testimony.yaml b/testimony.yaml index e197b61388c..46d0b289f43 100644 --- a/testimony.yaml +++ b/testimony.yaml @@ -66,7 +66,7 @@ CaseComponent: - Installer - InterSatelliteSync - katello-agent - - Katello-tracer + - katello-tracer - LDAP - Leappintegration - LifecycleEnvironments diff --git a/tests/foreman/cli/test_host.py b/tests/foreman/cli/test_host.py index a9d5f688b5b..d94a880f4fa 100644 --- a/tests/foreman/cli/test_host.py +++ b/tests/foreman/cli/test_host.py @@ -2458,7 +2458,7 @@ def test_positive_tracer_list_and_resolve(tracer_host): :CaseImportance: Medium - :CaseComponent: Katello-tracer + :CaseComponent: katello-tracer :Team: Phoenix-subscriptions diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index 7ef4330fd90..803abbd7778 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -2588,7 +2588,7 @@ def test_positive_tracer_enable_reload(tracer_install_host, target_sat): :id: c9ebd4a8-6db3-4d0e-92a2-14951c26769b - :CaseComponent: Katello-tracer + :CaseComponent: katello-tracer :Team: Phoenix-subscriptions From 818b7d818307114dc1b1c463444f0a74671793b3 Mon Sep 17 00:00:00 2001 From: Jameer Pathan Date: Mon, 28 Aug 2023 18:39:11 +0200 Subject: [PATCH 166/586] [cherry-pick-6.14.z] Refactor tests/foreman/ui/test_rhcloud_insights.py (#12375) --- pytest_fixtures/component/rh_cloud.py | 92 ++--- pytest_fixtures/core/sat_cap_factory.py | 9 - robottelo/constants/__init__.py | 10 + robottelo/hosts.py | 38 +- tests/foreman/api/test_rhcloud_inventory.py | 109 +++--- tests/foreman/cli/test_rhcloud_insights.py | 9 +- tests/foreman/cli/test_rhcloud_inventory.py | 32 +- tests/foreman/ui/test_rhc.py | 2 +- tests/foreman/ui/test_rhcloud_insights.py | 392 +++++++------------- tests/foreman/ui/test_rhcloud_inventory.py | 65 ++-- 10 files changed, 329 insertions(+), 429 deletions(-) diff --git a/pytest_fixtures/component/rh_cloud.py b/pytest_fixtures/component/rh_cloud.py index e788f8e006e..617261c98c5 100644 --- a/pytest_fixtures/component/rh_cloud.py +++ b/pytest_fixtures/component/rh_cloud.py @@ -1,51 +1,43 @@ import pytest -from broker import Broker - -@pytest.fixture(scope='module') -def rhcloud_sat_host(satellite_factory): - """A module level fixture that provides a Satellite based on config settings""" - new_sat = satellite_factory() - yield new_sat - new_sat.teardown() - Broker(hosts=[new_sat]).checkin() +from robottelo.constants import CAPSULE_REGISTRATION_OPTS @pytest.fixture(scope='module') -def rhcloud_manifest_org(rhcloud_sat_host, module_sca_manifest): +def rhcloud_manifest_org(module_target_sat, module_extra_rhel_entitlement_manifest): """A module level fixture to get organization with manifest.""" - org = rhcloud_sat_host.api.Organization().create() - rhcloud_sat_host.upload_manifest(org.id, module_sca_manifest.content) + org = module_target_sat.api.Organization().create() + module_target_sat.upload_manifest(org.id, module_extra_rhel_entitlement_manifest.content) return org @pytest.fixture(scope='module') -def organization_ak_setup(rhcloud_sat_host, rhcloud_manifest_org): +def rhcloud_activation_key(module_target_sat, rhcloud_manifest_org): """A module-level fixture to create an Activation key in module_org""" purpose_addons = "test-addon1, test-addon2" - ak = rhcloud_sat_host.api.ActivationKey( + ak = module_target_sat.api.ActivationKey( content_view=rhcloud_manifest_org.default_content_view, organization=rhcloud_manifest_org, - environment=rhcloud_sat_host.api.LifecycleEnvironment(id=rhcloud_manifest_org.library.id), + environment=module_target_sat.api.LifecycleEnvironment(id=rhcloud_manifest_org.library.id), purpose_addons=[purpose_addons], service_level='Self-Support', purpose_usage='test-usage', purpose_role='test-role', auto_attach=False, ).create() - yield rhcloud_manifest_org, ak - ak.delete() + yield ak @pytest.fixture(scope='module') -def rhcloud_registered_hosts(organization_ak_setup, mod_content_hosts, rhcloud_sat_host): +def rhcloud_registered_hosts( + rhcloud_activation_key, rhcloud_manifest_org, mod_content_hosts, module_target_sat +): """Fixture that registers content hosts to Satellite and Insights.""" - org, ak = organization_ak_setup for vm in mod_content_hosts: vm.configure_rhai_client( - satellite=rhcloud_sat_host, - activation_key=ak.name, - org=org.label, + satellite=module_target_sat, + activation_key=rhcloud_activation_key.name, + org=rhcloud_manifest_org.label, rhel_distro=f"rhel{vm.os_version.major}", ) assert vm.subscribed @@ -53,35 +45,49 @@ def rhcloud_registered_hosts(organization_ak_setup, mod_content_hosts, rhcloud_s @pytest.fixture -def rhel_insights_vm(rhcloud_sat_host, organization_ak_setup, rhel_contenthost): - """A module-level fixture to create rhel content host registered with insights.""" - # settings.supportability.content_hosts.rhel.versions - org, ak = organization_ak_setup - rhel_contenthost.configure_rex(satellite=rhcloud_sat_host, org=org, register=False) +def rhel_insights_vm( + module_target_sat, rhcloud_activation_key, rhcloud_manifest_org, rhel_contenthost +): + """A function-level fixture to create rhel content host registered with insights.""" + rhel_contenthost.configure_rex( + satellite=module_target_sat, org=rhcloud_manifest_org, register=False + ) rhel_contenthost.configure_rhai_client( - satellite=rhcloud_sat_host, - activation_key=ak.name, - org=org.label, + satellite=module_target_sat, + activation_key=rhcloud_activation_key.name, + org=rhcloud_manifest_org.label, rhel_distro=f"rhel{rhel_contenthost.os_version.major}", ) + # Generate report + module_target_sat.generate_inventory_report(rhcloud_manifest_org) + # Sync inventory status + module_target_sat.sync_inventory_status(rhcloud_manifest_org) yield rhel_contenthost @pytest.fixture -def inventory_settings(rhcloud_sat_host): - hostnames_setting = rhcloud_sat_host.update_setting('obfuscate_inventory_hostnames', False) - ip_setting = rhcloud_sat_host.update_setting('obfuscate_inventory_ips', False) - packages_setting = rhcloud_sat_host.update_setting('exclude_installed_packages', False) - parameter_tags_setting = rhcloud_sat_host.update_setting('include_parameter_tags', False) +def inventory_settings(module_target_sat): + hostnames_setting = module_target_sat.update_setting('obfuscate_inventory_hostnames', False) + ip_setting = module_target_sat.update_setting('obfuscate_inventory_ips', False) + packages_setting = module_target_sat.update_setting('exclude_installed_packages', False) + parameter_tags_setting = module_target_sat.update_setting('include_parameter_tags', False) yield - rhcloud_sat_host.update_setting('obfuscate_inventory_hostnames', hostnames_setting) - rhcloud_sat_host.update_setting('obfuscate_inventory_ips', ip_setting) - rhcloud_sat_host.update_setting('exclude_installed_packages', packages_setting) - rhcloud_sat_host.update_setting('include_parameter_tags', parameter_tags_setting) + module_target_sat.update_setting('obfuscate_inventory_hostnames', hostnames_setting) + module_target_sat.update_setting('obfuscate_inventory_ips', ip_setting) + module_target_sat.update_setting('exclude_installed_packages', packages_setting) + module_target_sat.update_setting('include_parameter_tags', parameter_tags_setting) -@pytest.fixture -def rhcloud_capsule(capsule_host, rhcloud_sat_host): +@pytest.fixture(scope='module') +def rhcloud_capsule(module_capsule_host, module_target_sat, rhcloud_manifest_org, default_location): """Configure the capsule instance with the satellite from settings.server.hostname""" - capsule_host.capsule_setup(sat_host=rhcloud_sat_host) - yield capsule_host + org = rhcloud_manifest_org + module_capsule_host.capsule_setup(sat_host=module_target_sat, **CAPSULE_REGISTRATION_OPTS) + module_target_sat.cli.Capsule.update( + { + 'name': module_capsule_host.hostname, + 'organization-ids': org.id, + 'location-ids': default_location.id, + } + ) + yield module_capsule_host diff --git a/pytest_fixtures/core/sat_cap_factory.py b/pytest_fixtures/core/sat_cap_factory.py index 0ca8431ff9a..c0a9798d09f 100644 --- a/pytest_fixtures/core/sat_cap_factory.py +++ b/pytest_fixtures/core/sat_cap_factory.py @@ -221,15 +221,6 @@ def module_capsule_configured_async_ssh(module_capsule_configured): yield module_capsule_configured -@pytest.fixture(scope='module') -def rhcloud_sat_host(satellite_factory): - """A module level fixture that provides a Satellite based on config settings""" - new_sat = satellite_factory() - yield new_sat - new_sat.teardown() - Broker(hosts=[new_sat]).checkin() - - @pytest.fixture(scope='module', params=['IDM', 'AD']) def parametrized_enrolled_sat( request, diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index 067bd813420..20aaa27bc91 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -872,6 +872,10 @@ class Colored(Box): 'enable-foreman-cli-puppet', ] PUPPET_CAPSULE_INSTALLER = ['enable-puppet'] +CAPSULE_REGISTRATION_OPTS = { + 'foreman-proxy-registration': 'true', + 'foreman-proxy-templates': 'true', +} KICKSTART_CONTENT = [ 'treeinfo', @@ -1961,6 +1965,12 @@ class Colored(Box): "PATCH", ] +OPENSSH_RECOMMENDATION = 'Decreased security: OpenSSH config permissions' +DNF_RECOMMENDATION = ( + 'The dnf installs lower versions of packages when the "best" ' + 'option is not present in the /etc/dnf/dnf.conf' +) + # Data File Paths class DataFile(Box): diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 71cbd1e40c3..e1ad644bce9 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -6,6 +6,7 @@ import time from configparser import ConfigParser from contextlib import contextmanager +from datetime import datetime from functools import cached_property from functools import lru_cache from pathlib import Path @@ -1481,7 +1482,7 @@ def get_features(self): """Get capsule features""" return requests.get(f'https://{self.hostname}:9090/features', verify=False).text - def capsule_setup(self, sat_host=None, **installer_kwargs): + 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() @@ -1510,7 +1511,9 @@ def capsule_setup(self, sat_host=None, **installer_kwargs): raise CapsuleHostError(f'The satellite-capsule package was not found\n{result.stdout}') # Generate certificate, copy it to Capsule, run installer, check it succeeds - certs_tar, _, installer = self.satellite.capsule_certs_generate(self, **installer_kwargs) + if not capsule_cert_opts: + capsule_cert_opts = {} + certs_tar, _, installer = self.satellite.capsule_certs_generate(self, **capsule_cert_opts) self.satellite.session.remote_copy(certs_tar, self) installer.update(**installer_kwargs) result = self.install(installer) @@ -2050,6 +2053,37 @@ def enroll_ad_and_configure_external_auth(self, ad_data): self.execute('systemctl daemon-reload && systemctl restart httpd.service').status == 0 ) + def generate_inventory_report(self, org): + """Function to perform inventory upload.""" + generate_report_task = 'ForemanInventoryUpload::Async::UploadReportJob' + timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M') + self.api.Organization(id=org.id).rh_cloud_generate_report() + wait_for( + lambda: self.api.ForemanTask() + .search(query={'search': f'{generate_report_task} and started_at >= "{timestamp}"'})[0] + .result + == 'success', + timeout=400, + delay=15, + silent_failure=True, + handle_exception=True, + ) + + def sync_inventory_status(self, org): + """Perform inventory sync""" + inventory_sync = self.api.Organization(id=org.id).rh_cloud_inventory_sync() + wait_for( + lambda: self.api.ForemanTask() + .search(query={'search': f'id = {inventory_sync["task"]["id"]}'})[0] + .result + == 'success', + timeout=400, + delay=15, + silent_failure=True, + handle_exception=True, + ) + return inventory_sync + class SSOHost(Host): """Class for RHSSO functions and setup""" diff --git a/tests/foreman/api/test_rhcloud_inventory.py b/tests/foreman/api/test_rhcloud_inventory.py index 98e2f3355cf..57af28af261 100644 --- a/tests/foreman/api/test_rhcloud_inventory.py +++ b/tests/foreman/api/test_rhcloud_inventory.py @@ -16,36 +16,15 @@ :Upstream: No """ -from datetime import datetime - import pytest from fauxfactory import gen_alphanumeric from fauxfactory import gen_string -from wait_for import wait_for from robottelo.config import robottelo_tmp_dir from robottelo.utils.io import get_local_file_data from robottelo.utils.io import get_report_data from robottelo.utils.io import get_report_metadata -generate_report_task = 'ForemanInventoryUpload::Async::UploadReportJob' - - -def generate_inventory_report(satellite, org): - """Function to generate inventory report.""" - timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M') - satellite.api.Organization(id=org.id).rh_cloud_generate_report() - wait_for( - lambda: satellite.api.ForemanTask() - .search(query={'search': f'{generate_report_task} and started_at >= "{timestamp}"'})[0] - .result - == 'success', - timeout=400, - delay=15, - silent_failure=True, - handle_exception=True, - ) - def common_assertion(report_path): """Function to perform common assertions""" @@ -66,7 +45,10 @@ def common_assertion(report_path): @pytest.mark.tier3 @pytest.mark.e2e def test_rhcloud_inventory_api_e2e( - inventory_settings, organization_ak_setup, rhcloud_registered_hosts, rhcloud_sat_host + inventory_settings, + rhcloud_manifest_org, + rhcloud_registered_hosts, + module_target_sat, ): """Generate report using rh_cloud plugin api's and verify its basic properties. @@ -91,21 +73,21 @@ def test_rhcloud_inventory_api_e2e( :customerscenario: true """ - org, ak = organization_ak_setup + org = rhcloud_manifest_org virtual_host, baremetal_host = rhcloud_registered_hosts local_report_path = robottelo_tmp_dir.joinpath(f'{gen_alphanumeric()}_{org.id}.tar.xz') # Generate report - generate_inventory_report(rhcloud_sat_host, org) + module_target_sat.generate_inventory_report(org) # Download report - rhcloud_sat_host.api.Organization(id=org.id).rh_cloud_download_report( + module_target_sat.api.Organization(id=org.id).rh_cloud_download_report( destination=local_report_path ) common_assertion(local_report_path) # Assert Hostnames, IP addresses, and installed packages are present in report. json_data = get_report_data(local_report_path) json_meta_data = get_report_metadata(local_report_path) - prefix = 'tfm-' if rhcloud_sat_host.os_version.major < 8 else '' - package_version = rhcloud_sat_host.run( + prefix = 'tfm-' if module_target_sat.os_version.major < 8 else '' + package_version = module_target_sat.run( f'rpm -qa --qf "%{{VERSION}}" {prefix}rubygem-foreman_rh_cloud' ).stdout.strip() assert json_meta_data['source_metadata']['foreman_rh_cloud_version'] == str(package_version) @@ -137,9 +119,9 @@ def test_rhcloud_inventory_api_e2e( @pytest.mark.e2e @pytest.mark.tier3 def test_rhcloud_inventory_api_hosts_synchronization( - organization_ak_setup, + rhcloud_manifest_org, rhcloud_registered_hosts, - rhcloud_sat_host, + module_target_sat, ): """Test RH Cloud plugin api to synchronize list of available hosts from cloud. @@ -161,23 +143,13 @@ def test_rhcloud_inventory_api_hosts_synchronization( :CaseAutomation: Automated """ - org, ak = organization_ak_setup + org = rhcloud_manifest_org virtual_host, baremetal_host = rhcloud_registered_hosts # Generate report - generate_inventory_report(rhcloud_sat_host, org) + module_target_sat.generate_inventory_report(org) # Sync inventory status - inventory_sync = rhcloud_sat_host.api.Organization(id=org.id).rh_cloud_inventory_sync() - wait_for( - lambda: rhcloud_sat_host.api.ForemanTask() - .search(query={'search': f'id = {inventory_sync["task"]["id"]}'})[0] - .result - == 'success', - timeout=400, - delay=15, - silent_failure=True, - handle_exception=True, - ) - task_output = rhcloud_sat_host.api.ForemanTask().search( + inventory_sync = module_target_sat.sync_inventory_status(org) + task_output = module_target_sat.api.ForemanTask().search( query={'search': f'id = {inventory_sync["task"]["id"]}'} ) assert task_output[0].output['host_statuses']['sync'] == 2 @@ -216,7 +188,10 @@ def test_rhcloud_inventory_mtu_field(): @pytest.mark.run_in_one_thread @pytest.mark.tier2 def test_system_purpose_sla_field( - inventory_settings, organization_ak_setup, rhcloud_registered_hosts, rhcloud_sat_host + inventory_settings, + rhcloud_manifest_org, + rhcloud_registered_hosts, + module_target_sat, ): """Verify that system_purpose_sla field is present in the inventory report for the host subscribed using Activation key with service level set in it. @@ -242,12 +217,12 @@ def test_system_purpose_sla_field( :CaseAutomation: Automated """ - org, ak = organization_ak_setup + org = rhcloud_manifest_org virtual_host, baremetal_host = rhcloud_registered_hosts local_report_path = robottelo_tmp_dir.joinpath(f'{gen_alphanumeric()}_{org.id}.tar.xz') - generate_inventory_report(rhcloud_sat_host, org) + module_target_sat.generate_inventory_report(org) # Download report - rhcloud_sat_host.api.Organization(id=org.id).rh_cloud_download_report( + module_target_sat.api.Organization(id=org.id).rh_cloud_download_report( destination=local_report_path ) json_data = get_report_data(local_report_path) @@ -316,7 +291,10 @@ def test_inventory_upload_with_http_proxy(): @pytest.mark.run_in_one_thread @pytest.mark.tier2 def test_include_parameter_tags_setting( - inventory_settings, organization_ak_setup, rhcloud_registered_hosts, rhcloud_sat_host + inventory_settings, + rhcloud_manifest_org, + rhcloud_registered_hosts, + module_target_sat, ): """Verify that include_parameter_tags setting doesn't cause invalid report to be generated. @@ -339,13 +317,13 @@ def test_include_parameter_tags_setting( :CaseAutomation: Automated """ - org, ak = organization_ak_setup + org = rhcloud_manifest_org virtual_host, baremetal_host = rhcloud_registered_hosts local_report_path = robottelo_tmp_dir.joinpath(f'{gen_alphanumeric()}_{org.id}.tar.xz') - rhcloud_sat_host.update_setting('include_parameter_tags', True) - generate_inventory_report(rhcloud_sat_host, org) + module_target_sat.update_setting('include_parameter_tags', True) + module_target_sat.generate_inventory_report(org) # Download report - rhcloud_sat_host.api.Organization(id=org.id).rh_cloud_download_report( + module_target_sat.api.Organization(id=org.id).rh_cloud_download_report( destination=local_report_path ) json_data = get_report_data(local_report_path) @@ -359,7 +337,10 @@ def test_include_parameter_tags_setting( @pytest.mark.tier3 def test_rh_cloud_tag_values( - inventory_settings, organization_ak_setup, rhcloud_sat_host, rhcloud_registered_hosts + inventory_settings, + rhcloud_manifest_org, + module_target_sat, + rhcloud_registered_hosts, ): """Verify that tag values are escaped properly when hostgroup name contains " (double quote) in it. @@ -384,20 +365,20 @@ def test_rh_cloud_tag_values( :CaseAutomation: Automated """ - org, ak = organization_ak_setup + org = rhcloud_manifest_org host_col_name = gen_string('alpha') host_name = rhcloud_registered_hosts[0].hostname - host = rhcloud_sat_host.api.Host().search(query={'search': host_name})[0] - host_collection = rhcloud_sat_host.api.HostCollection( + host = module_target_sat.api.Host().search(query={'search': host_name})[0] + host_collection = module_target_sat.api.HostCollection( organization=org, name=f'"{host_col_name}"', host=[host] ).create() assert len(host_collection.host) == 1 local_report_path = robottelo_tmp_dir.joinpath(f'{gen_alphanumeric()}_{org.id}.tar.xz') # Generate report - generate_inventory_report(rhcloud_sat_host, org) - rhcloud_sat_host.api.Organization(id=org.id).rh_cloud_download_report( + module_target_sat.generate_inventory_report(org) + module_target_sat.api.Organization(id=org.id).rh_cloud_download_report( destination=local_report_path ) common_assertion(local_report_path) @@ -414,9 +395,9 @@ def test_rh_cloud_tag_values( @pytest.mark.tier2 def test_positive_tag_values_max_length( inventory_settings, - organization_ak_setup, + rhcloud_manifest_org, rhcloud_registered_hosts, - rhcloud_sat_host, + module_target_sat, target_sat, ): """Verify that tags values are truncated properly for the host parameter @@ -443,12 +424,12 @@ def test_positive_tag_values_max_length( param_value = gen_string('alpha', length=260) target_sat.api.CommonParameter(name=param_name, value=param_value).create() - org, ak = organization_ak_setup + org = rhcloud_manifest_org local_report_path = robottelo_tmp_dir.joinpath(f'{gen_alphanumeric()}_{org.id}.tar.xz') - rhcloud_sat_host.update_setting('include_parameter_tags', True) - generate_inventory_report(rhcloud_sat_host, org) + module_target_sat.update_setting('include_parameter_tags', True) + module_target_sat.generate_inventory_report(org) # Download report - rhcloud_sat_host.api.Organization(id=org.id).rh_cloud_download_report( + module_target_sat.api.Organization(id=org.id).rh_cloud_download_report( destination=local_report_path ) json_data = get_report_data(local_report_path) diff --git a/tests/foreman/cli/test_rhcloud_insights.py b/tests/foreman/cli/test_rhcloud_insights.py index 8f9ebb246c1..9682fc0d86c 100644 --- a/tests/foreman/cli/test_rhcloud_insights.py +++ b/tests/foreman/cli/test_rhcloud_insights.py @@ -25,7 +25,9 @@ @pytest.mark.e2e @pytest.mark.tier4 @pytest.mark.parametrize('distro', ['rhel7', 'rhel8']) -def test_positive_connection_option(organization_ak_setup, rhcloud_sat_host, distro): +def test_positive_connection_option( + rhcloud_activation_key, rhcloud_manifest_org, module_target_sat, distro +): """Verify that 'insights-client --test-connection' successfully tests the proxy connection via the Satellite. @@ -45,9 +47,10 @@ def test_positive_connection_option(organization_ak_setup, rhcloud_sat_host, dis :CaseImportance: Critical """ - org, activation_key = organization_ak_setup + org = rhcloud_manifest_org + ak = rhcloud_activation_key with Broker(nick=distro, host_class=ContentHost) as vm: - vm.configure_rhai_client(rhcloud_sat_host, activation_key.name, org.label, distro) + vm.configure_rhai_client(module_target_sat, ak.name, org.label, distro) result = vm.run('insights-client --test-connection') assert result.status == 0, ( 'insights-client --test-connection failed.\n' diff --git a/tests/foreman/cli/test_rhcloud_inventory.py b/tests/foreman/cli/test_rhcloud_inventory.py index 4d67f6c6c0e..0330420dd45 100644 --- a/tests/foreman/cli/test_rhcloud_inventory.py +++ b/tests/foreman/cli/test_rhcloud_inventory.py @@ -33,7 +33,7 @@ @pytest.mark.tier3 @pytest.mark.e2e def test_positive_inventory_generate_upload_cli( - organization_ak_setup, rhcloud_registered_hosts, rhcloud_sat_host + rhcloud_manifest_org, rhcloud_registered_hosts, module_target_sat ): """Tests Insights inventory generation and upload via foreman-rake commands: https://github.com/theforeman/foreman_rh_cloud/blob/master/README.md @@ -68,10 +68,10 @@ def test_positive_inventory_generate_upload_cli( :CaseLevel: System """ - org, _ = organization_ak_setup + org = rhcloud_manifest_org cmd = f'organization_id={org.id} foreman-rake rh_cloud_inventory:report:generate_upload' upload_success_msg = f"Generated and uploaded inventory report for organization '{org.name}'" - result = rhcloud_sat_host.execute(cmd) + result = module_target_sat.execute(cmd) assert result.status == 0 assert upload_success_msg in result.stdout @@ -80,7 +80,7 @@ def test_positive_inventory_generate_upload_cli( f'/var/lib/foreman/red_hat_inventory/uploads/done/report_for_{org.id}.tar.xz' ) wait_for( - lambda: rhcloud_sat_host.get( + lambda: module_target_sat.get( remote_path=str(remote_report_path), local_path=str(local_report_path) ), timeout=60, @@ -89,7 +89,7 @@ def test_positive_inventory_generate_upload_cli( handle_exception=True, ) local_file_data = get_local_file_data(local_report_path) - assert local_file_data['checksum'] == get_remote_report_checksum(rhcloud_sat_host, org.id) + assert local_file_data['checksum'] == get_remote_report_checksum(module_target_sat, org.id) assert local_file_data['size'] > 0 assert local_file_data['extractable'] assert local_file_data['json_files_parsable'] @@ -104,9 +104,9 @@ def test_positive_inventory_generate_upload_cli( @pytest.mark.e2e @pytest.mark.tier3 def test_positive_inventory_recommendation_sync( - organization_ak_setup, + rhcloud_manifest_org, rhcloud_registered_hosts, - rhcloud_sat_host, + module_target_sat, ): """Tests Insights recommendation sync via foreman-rake commands: https://github.com/theforeman/foreman_rh_cloud/blob/master/README.md @@ -127,12 +127,12 @@ def test_positive_inventory_recommendation_sync( :CaseLevel: System """ - org, ak = organization_ak_setup + org = rhcloud_manifest_org cmd = f'organization_id={org.id} foreman-rake rh_cloud_insights:sync' timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M') - result = rhcloud_sat_host.execute(cmd) + result = module_target_sat.execute(cmd) wait_for( - lambda: rhcloud_sat_host.api.ForemanTask() + lambda: module_target_sat.api.ForemanTask() .search(query={'search': f'Insights full sync and started_at >= "{timestamp}"'})[0] .result == 'success', @@ -148,9 +148,9 @@ def test_positive_inventory_recommendation_sync( @pytest.mark.e2e @pytest.mark.tier3 def test_positive_sync_inventory_status( - organization_ak_setup, + rhcloud_manifest_org, rhcloud_registered_hosts, - rhcloud_sat_host, + module_target_sat, ): """Sync inventory status via foreman-rake commands: https://github.com/theforeman/foreman_rh_cloud/blob/master/README.md @@ -172,16 +172,16 @@ def test_positive_sync_inventory_status( :CaseLevel: System """ - org, ak = organization_ak_setup + org = rhcloud_manifest_org cmd = f'organization_id={org.id} foreman-rake rh_cloud_inventory:sync' success_msg = f"Synchronized inventory for organization '{org.name}'" timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M') - result = rhcloud_sat_host.execute(cmd) + result = module_target_sat.execute(cmd) assert result.status == 0 assert success_msg in result.stdout # Check task details wait_for( - lambda: rhcloud_sat_host.api.ForemanTask() + lambda: module_target_sat.api.ForemanTask() .search(query={'search': f'{inventory_sync_task} and started_at >= "{timestamp}"'})[0] .result == 'success', @@ -190,7 +190,7 @@ def test_positive_sync_inventory_status( silent_failure=True, handle_exception=True, ) - task_output = rhcloud_sat_host.api.ForemanTask().search( + task_output = module_target_sat.api.ForemanTask().search( query={'search': f'{inventory_sync_task} and started_at >= "{timestamp}"'} ) assert task_output[0].output['host_statuses']['sync'] == 2 diff --git a/tests/foreman/ui/test_rhc.py b/tests/foreman/ui/test_rhc.py index d704b50094b..dfcf3890810 100644 --- a/tests/foreman/ui/test_rhc.py +++ b/tests/foreman/ui/test_rhc.py @@ -146,7 +146,7 @@ def test_positive_configure_cloud_connector( host.host_parameters_attributes = parameters host.update(['host_parameters_attributes']) - with session: + with module_target_sat.ui_session() as session: session.organization.select(org_name=module_rhc_org.name) if session.cloudinventory.is_cloud_connector_configured(): pytest.skip( diff --git a/tests/foreman/ui/test_rhcloud_insights.py b/tests/foreman/ui/test_rhcloud_insights.py index 7bd8a83759e..a57488414e3 100644 --- a/tests/foreman/ui/test_rhcloud_insights.py +++ b/tests/foreman/ui/test_rhcloud_insights.py @@ -19,21 +19,30 @@ from datetime import datetime import pytest -from airgun.session import Session from wait_for import wait_for from robottelo.config import settings from robottelo.constants import DEFAULT_LOC +from robottelo.constants import DNF_RECOMMENDATION +from robottelo.constants import OPENSSH_RECOMMENDATION + + +def create_insights_vulnerability(insights_vm): + """Function to create vulnerabilities that can be remediated.""" + insights_vm.run( + 'chmod 777 /etc/ssh/sshd_config;dnf update -y dnf;' + 'sed -i -e "/^best/d" /etc/dnf/dnf.conf;insights-client' + ) @pytest.mark.e2e -@pytest.mark.run_in_one_thread @pytest.mark.tier3 -@pytest.mark.rhel_ver_list([8, 9]) +@pytest.mark.no_containers +@pytest.mark.rhel_ver_list([7, 8, 9]) def test_rhcloud_insights_e2e( rhel_insights_vm, - organization_ak_setup, - rhcloud_sat_host, + rhcloud_manifest_org, + module_target_sat, ): """Synchronize hits data from cloud, verify it is displayed in Satellite and run remediation. @@ -41,9 +50,9 @@ def test_rhcloud_insights_e2e( :Steps: 1. Prepare misconfigured machine and upload its data to Insights. - 2. In Satellite UI, Configure -> Insights -> Sync recommendations. - 3. Run remediation for "OpenSSH config permissions" recommendation against rhel8/rhel9 host. - 4. Assert that job completed successfully. + 2. In Satellite UI, go to Configure -> Insights -> Sync recommendations. + 3. Run remediation for "OpenSSH config permissions" recommendation against host. + 4. Verify that the remediation job completed successfully. 5. Sync Insights recommendations. 6. Search for previously remediated issue. @@ -63,20 +72,21 @@ def test_rhcloud_insights_e2e( :CaseAutomation: Automated """ - org, ak = organization_ak_setup - # Create a vulnerability which can be remediated - rhel_insights_vm.run('chmod 777 /etc/ssh/sshd_config;insights-client') - query = 'Decreased security: OpenSSH config permissions' job_query = ( f'Remote action: Insights remediations for selected issues on {rhel_insights_vm.hostname}' ) - with Session(hostname=rhcloud_sat_host.hostname) as session: + org = rhcloud_manifest_org + # Prepare misconfigured machine and upload data to Insights + create_insights_vulnerability(rhel_insights_vm) + + with module_target_sat.ui_session() as session: session.organization.select(org_name=org.name) session.location.select(loc_name=DEFAULT_LOC) timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M') + # Sync Insights recommendations session.cloudinsights.sync_hits() wait_for( - lambda: rhcloud_sat_host.api.ForemanTask() + lambda: module_target_sat.api.ForemanTask() .search(query={'search': f'Insights full sync and started_at >= "{timestamp}"'})[0] .result == 'success', @@ -87,13 +97,17 @@ def test_rhcloud_insights_e2e( ) # Workaround for alert message causing search to fail. See airgun issue 584. session.browser.refresh() - result = session.cloudinsights.search(query)[0] + # Search for Insights recommendation. + result = session.cloudinsights.search( + f'hostname= "{rhel_insights_vm.hostname}" and title = "{OPENSSH_RECOMMENDATION}"' + )[0] assert result['Hostname'] == rhel_insights_vm.hostname - assert result['Recommendation'] == query + assert result['Recommendation'] == OPENSSH_RECOMMENDATION + # Run remediation and verify job completion. timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M') - session.cloudinsights.remediate(query) + session.cloudinsights.remediate(OPENSSH_RECOMMENDATION) wait_for( - lambda: rhcloud_sat_host.api.ForemanTask() + lambda: module_target_sat.api.ForemanTask() .search(query={'search': f'{job_query} and started_at >= "{timestamp}"'})[0] .result == 'success', @@ -102,10 +116,11 @@ def test_rhcloud_insights_e2e( silent_failure=True, handle_exception=True, ) + # Search for Insights recommendations again. timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M') session.cloudinsights.sync_hits() wait_for( - lambda: rhcloud_sat_host.api.ForemanTask() + lambda: module_target_sat.api.ForemanTask() .search(query={'search': f'Insights full sync and started_at >= "{timestamp}"'})[0] .result == 'success', @@ -116,7 +131,10 @@ def test_rhcloud_insights_e2e( ) # Workaround for alert message causing search to fail. See airgun issue 584. session.browser.refresh() - assert not session.cloudinsights.search(query) + # Verify that the insights recommendation is not listed anymore. + assert not session.cloudinsights.search( + f'hostname= "{rhel_insights_vm.hostname}" and title = "{OPENSSH_RECOMMENDATION}"' + ) @pytest.mark.stubbed @@ -220,195 +238,59 @@ def test_host_sorting_based_on_recommendation_count(): """ -@pytest.mark.run_in_one_thread @pytest.mark.tier2 -@pytest.mark.rhel_ver_list([8]) +@pytest.mark.no_containers +@pytest.mark.rhel_ver_list([7, 8, 9]) def test_host_details_page( rhel_insights_vm, - organization_ak_setup, - rhcloud_sat_host, + rhcloud_manifest_org, + module_target_sat, ): """Test host details page for host having insights recommendations. :id: e079ed10-c9f5-4331-9cb3-70b224b1a584 + :customerscenario: true + :Steps: 1. Prepare misconfigured machine and upload its data to Insights. 2. Sync insights recommendations. 3. Sync RH Cloud inventory status. 4. Go to Hosts -> All Hosts - 5. Assert there is "Recommendations" column containing insights recommendation count. + 5. Verify there is a "Recommendations" column containing insights recommendation count. 6. Check popover status of host. - 7. Assert that host properties shows reporting inventory upload status. - 8. Click on "Recommendations" tab. + 7. Verify that host properties shows "reporting" inventory upload status. + 8. Read the recommendations listed in Insights tab present on host details page. + 9. Click on "Recommendations" tab. + 10. Try to delete host. :expectedresults: 1. There's Insights column with number of recommendations. 2. Inventory upload status is displayed in popover status of host. 3. Insights registration status is displayed in popover status of host. 4. Inventory upload status is present in host properties table. - 5. Clicking on "Recommendations" tab takes user to Insights page with insights + 5. Verify the contents of Insights tab. + 6. Clicking on "Recommendations" tab takes user to Insights page with insights recommendations selected for that host. + 7. Host having insights recommendations is deleted from Satellite. - :BZ: 1974578 - - :parametrized: yes - - :CaseAutomation: Automated - """ - org, ak = organization_ak_setup - # Create a vulnerability which can be remediated - rhel_insights_vm.run('dnf update -y dnf;sed -i -e "/^best/d" /etc/dnf/dnf.conf;insights-client') - # Sync inventory status - inventory_sync = rhcloud_sat_host.api.Organization(id=org.id).rh_cloud_inventory_sync() - wait_for( - lambda: rhcloud_sat_host.api.ForemanTask() - .search(query={'search': f'id = {inventory_sync["task"]["id"]}'})[0] - .result - == 'success', - timeout=400, - delay=15, - silent_failure=True, - handle_exception=True, - ) - with Session(hostname=rhcloud_sat_host.hostname) as session: - session.organization.select(org_name=org.name) - session.location.select(loc_name=DEFAULT_LOC) - # Sync insights recommendations - session.cloudinsights.sync_hits() - result = session.host.host_status(rhel_insights_vm.hostname) - assert 'Insights: Reporting' in result - assert 'Inventory: Successfully uploaded to your RH cloud inventory' in result - result = session.host.search(rhel_insights_vm.hostname)[0] - assert result['Name'] == rhel_insights_vm.hostname - assert int(result['Recommendations']) > 0 - values = session.host.get_details(rhel_insights_vm.hostname) - # Note: Reading host properties adds 'clear' to original value. - assert ( - values['properties']['properties_table']['Inventory'] - == 'Successfully uploaded to your RH cloud inventory clear' - ) - recommendations = session.host.read_insights_recommendations(rhel_insights_vm.hostname) - assert len(recommendations), 'No recommendations were found' - assert recommendations[0]['Hostname'] == rhel_insights_vm.hostname - assert int(result['Recommendations']) == len(recommendations) - - -@pytest.mark.run_in_one_thread -@pytest.mark.tier2 -def test_rh_cloud_insights_clean_statuses( - rhel7_contenthost, - rhel8_contenthost, - organization_ak_setup, - rhcloud_sat_host, -): - """Test rh_cloud_insights:clean_statuses rake command. - - :id: 6416ed31-cafb-4278-b205-bf3da9ab2ee4 - - :Steps: - 1. Prepare misconfigured machine and upload its data to Insights - 2. Go to Hosts -> All Hosts - 3. Assert that host properties shows reporting status for insights. - 4. Run rh_cloud_insights:clean_statuses rake command - 5. Assert that host properties doesn't contain insights status. - - :expectedresults: - 1. rake command deletes insights reporting status of host. - - :BZ: 1962930 - - :parametrized: yes - - :CaseAutomation: Automated - """ - org, ak = organization_ak_setup - rhel7_contenthost.configure_rhai_client( - satellite=rhcloud_sat_host, activation_key=ak.name, org=org.label, rhel_distro='rhel7' - ) - rhel8_contenthost.configure_rhai_client( - satellite=rhcloud_sat_host, activation_key=ak.name, org=org.label, rhel_distro='rhel8' - ) - with Session(hostname=rhcloud_sat_host.hostname) as session: - session.organization.select(org_name=org.name) - session.location.select(loc_name=DEFAULT_LOC) - values = session.host.get_details(rhel7_contenthost.hostname) - assert values['properties']['properties_table']['Insights'] == 'Reporting clear' - values = session.host.get_details(rhel8_contenthost.hostname) - assert values['properties']['properties_table']['Insights'] == 'Reporting clear' - # Clean insights status - result = rhcloud_sat_host.run( - f'foreman-rake rh_cloud_insights:clean_statuses SEARCH="{rhel7_contenthost.hostname}"' - ) - assert 'Deleted 1 insights statuses' in result.stdout - assert result.status == 0 - values = session.host.get_details(rhel7_contenthost.hostname) - with pytest.raises(KeyError): - values['properties']['properties_table']['Insights'] - values = session.host.get_details(rhel8_contenthost.hostname) - assert values['properties']['properties_table']['Insights'] == 'Reporting clear' - result = rhel7_contenthost.run('insights-client') - assert result.status == 0 - values = session.host.get_details(rhel7_contenthost.hostname) - assert values['properties']['properties_table']['Insights'] == 'Reporting clear' - - -@pytest.mark.run_in_one_thread -@pytest.mark.tier2 -@pytest.mark.rhel_ver_list([8]) -def test_delete_host_having_insights_recommendation( - rhel_insights_vm, - organization_ak_setup, - rhcloud_sat_host, -): - """Verify that host having insights recommendations can be deleted from Satellite. - - :id: 07914ff7-e230-4416-8664-7d357e9966f3 - - :customerscenario: true - - :Steps: - 1. Prepare misconfigured machine and upload its data to Insights. - 2. Sync insights recommendations. - 3. Sync RH Cloud inventory status. - 4. Go to Hosts -> All Hosts - 5. Assert there is "Recommendations" column containing insights recommendation count. - 6. Try to delete host. - - :expectedresults: - 1. host having insights recommendations is deleted from Satellite. - - :CaseImportance: Critical - - :BZ: 1860422, 1928652 + :BZ: 1974578, 1860422, 1928652, 1865876, 1879448 :parametrized: yes :CaseAutomation: Automated """ - org, ak = organization_ak_setup - # Create a vulnerability which can be remediated - rhel_insights_vm.run('dnf update -y dnf;sed -i -e "/^best/d" /etc/dnf/dnf.conf;insights-client') - # Sync inventory status - inventory_sync = rhcloud_sat_host.api.Organization(id=org.id).rh_cloud_inventory_sync() - wait_for( - lambda: rhcloud_sat_host.api.ForemanTask() - .search(query={'search': f'id = {inventory_sync["task"]["id"]}'})[0] - .result - == 'success', - timeout=400, - delay=15, - silent_failure=True, - handle_exception=True, - ) - with Session(hostname=rhcloud_sat_host.hostname) as session: + org = rhcloud_manifest_org + # Prepare misconfigured machine and upload data to Insights + create_insights_vulnerability(rhel_insights_vm) + with module_target_sat.ui_session() as session: session.organization.select(org_name=org.name) session.location.select(loc_name=DEFAULT_LOC) # Sync insights recommendations timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M') session.cloudinsights.sync_hits() wait_for( - lambda: rhcloud_sat_host.api.ForemanTask() + lambda: module_target_sat.api.ForemanTask() .search(query={'search': f'Insights full sync and started_at >= "{timestamp}"'})[0] .result == 'success', @@ -417,6 +299,10 @@ def test_delete_host_having_insights_recommendation( silent_failure=True, handle_exception=True, ) + # Verify Insights status of host. + result = session.host.host_status(rhel_insights_vm.hostname) + assert 'Insights: Reporting' in result + assert 'Inventory: Successfully uploaded to your RH cloud inventory' in result result = session.host.search(rhel_insights_vm.hostname)[0] assert result['Name'] == rhel_insights_vm.hostname assert int(result['Recommendations']) > 0 @@ -426,87 +312,36 @@ def test_delete_host_having_insights_recommendation( values['properties']['properties_table']['Inventory'] == 'Successfully uploaded to your RH cloud inventory clear' ) - assert values['properties']['properties_table']['Insights'] == 'Reporting clear' - # Delete host - session.host.delete(rhel_insights_vm.hostname) - assert not rhcloud_sat_host.api.Host().search( - query={'search': f'name="{rhel_insights_vm.hostname}"'} - ) - - -@pytest.mark.tier2 -@pytest.mark.rhel_ver_list([8]) -def test_insights_tab_on_host_details_page( - rhel_insights_vm, - organization_ak_setup, - rhcloud_sat_host, -): - """Test recommendations count in hosts index is a link and contents - of Insights tab on host details page. - - :id: d969ad38-7ee5-4c57-8538-1cf6c1705707 - - :Steps: - 1. Prepare misconfigured machine and upload its data to Insights. - 2. In Satellite UI, Configure -> Insights -> Sync now. - 3. Go to Hosts -> All Hosts. - 4. Click on recommendation count. - 5. Assert the contents of Insights tab. - - :expectedresults: - 1. There's Insights recommendation column with number of recommendations and - link to Insights tab on host details page. - 2. Insights tab shows recommendations for the host. - - :CaseImportance: High - - :BZ: 1865876, 1879448 - - :parametrized: yes - - :CaseAutomation: Automated - """ - org, ak = organization_ak_setup - # Create a vulnerability which can be remediated - rhel_insights_vm.run('dnf update -y dnf;sed -i -e "/^best/d" /etc/dnf/dnf.conf;insights-client') - dnf_issue = ( - 'The dnf installs lower versions of packages when the ' - '"best" option is not present in the /etc/dnf/dnf.conf' - ) - with Session(hostname=rhcloud_sat_host.hostname) as session: - session.organization.select(org_name=org.name) - session.location.select(loc_name=DEFAULT_LOC) - # Sync insights recommendations - timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M') - session.cloudinsights.sync_hits() - wait_for( - lambda: rhcloud_sat_host.api.ForemanTask() - .search(query={'search': f'Insights full sync and started_at >= "{timestamp}"'})[0] - .result - == 'success', - timeout=400, - delay=15, - silent_failure=True, - handle_exception=True, - ) - result = session.host.search(rhel_insights_vm.hostname)[0] - assert result['Name'] == rhel_insights_vm.hostname - assert int(result['Recommendations']) > 0 - insights_recommendations = session.host.insights_tab(rhel_insights_vm.hostname) + # Read the recommendations listed in Insights tab present on host details page + insights_recommendations = session.host_new.insights_tab(rhel_insights_vm.hostname) for recommendation in insights_recommendations: - if recommendation['name'] == dnf_issue: + if recommendation['name'] == DNF_RECOMMENDATION: assert recommendation['label'] == 'Moderate' - assert dnf_issue in recommendation['text'] + assert DNF_RECOMMENDATION in recommendation['text'] assert len(insights_recommendations) == int(result['Recommendations']) + # Test Recommendation button present on host details page + recommendations = session.host.read_insights_recommendations(rhel_insights_vm.hostname) + assert len(recommendations), 'No recommendations were found' + assert recommendations[0]['Hostname'] == rhel_insights_vm.hostname + assert int(result['Recommendations']) == len(recommendations) + # Delete host + rhel_insights_vm.nailgun_host.delete() + assert not rhel_insights_vm.nailgun_host @pytest.mark.e2e @pytest.mark.no_containers +@pytest.mark.rhel_ver_list([7, 8, 9]) def test_insights_registration_with_capsule( - rhcloud_capsule, organization_ak_setup, rhcloud_sat_host, rhel7_contenthost, default_os + rhcloud_capsule, + rhcloud_activation_key, + rhcloud_manifest_org, + module_target_sat, + rhel_contenthost, + default_os, ): """Registering host with insights having traffic going through - external capsule. + external capsule and also test rh_cloud_insights:clean_statuses rake command. :id: 9db1d307-664c-4d4a-89de-da986224f071 @@ -515,23 +350,35 @@ def test_insights_registration_with_capsule( :steps: 1. Integrate a capsule with satellite. 2. open the global registration form and select the same capsule. - 3. Overide Insights and Rex parameters. + 3. Override Insights and Rex parameters. 4. check host is registered successfully with selected capsule. 5. Test insights client connection & reporting status. + 6. Run rh_cloud_insights:clean_statuses rake command + 7. Verify that host properties doesn't contain insights status. - :expectedresults: Host is successfully registered with capsule host, - having remote execution and insights. + :expectedresults: + 1. Host is successfully registered with capsule host, + having remote execution and insights. + 2. rake command deletes insights reporting status of host. - :BZ: 2110222, 2112386 - """ - org, ak = organization_ak_setup - capsule = rhcloud_sat_host.api.SmartProxy(name=rhcloud_capsule.hostname).search()[0] - org.smart_proxy.append(capsule) - org.update(['smart_proxy']) + :BZ: 2110222, 2112386, 1962930 - with Session(hostname=rhcloud_sat_host.hostname) as session: + :parametrized: yes + """ + org = rhcloud_manifest_org + ak = rhcloud_activation_key + # Enable rhel repos and install insights-client + rhelver = rhel_contenthost.os_version.major + if rhelver > 7: + rhel_contenthost.create_custom_repos(**settings.repos[f'rhel{rhelver}_os']) + else: + rhel_contenthost.create_custom_repos( + **{f'rhel{rhelver}_os': settings.repos[f'rhel{rhelver}_os']} + ) + with module_target_sat.ui_session() as session: session.organization.select(org_name=org.name) session.location.select(loc_name=DEFAULT_LOC) + # Generate host registration command cmd = session.host.get_register_command( { 'general.operating_system': default_os.title, @@ -543,11 +390,28 @@ def test_insights_registration_with_capsule( 'advanced.setup_rex': 'Yes (override)', } ) - # TODO: Make this test parametirzed for all rhel versions after verifying BZ:2129254 - rhel7_contenthost.create_custom_repos(rhel7=settings.repos.rhel7_os) - assert rhel7_contenthost.execute('yum install -y insights-client').status == 0 - rhel7_contenthost.execute(cmd) - assert rhel7_contenthost.subscribed - assert rhel7_contenthost.execute('insights-client --test-connection').status == 0 - values = session.host.get_details(rhel7_contenthost.hostname) + # Register host with Satellite and Insights. + rhel_contenthost.execute(cmd) + assert rhel_contenthost.subscribed + assert rhel_contenthost.execute('insights-client --test-connection').status == 0 + values = session.host.get_details(rhel_contenthost.hostname) + assert values['properties']['properties_table']['Insights'] == 'Reporting clear' + # Clean insights status + result = module_target_sat.run( + f'foreman-rake rh_cloud_insights:clean_statuses SEARCH="{rhel_contenthost.hostname}"' + ) + assert 'Deleted 1 insights statuses' in result.stdout + assert result.status == 0 + # Workaround for not reading old data. + session.browser.refresh() + # Verify that Insights status is cleared. + values = session.host.get_details(rhel_contenthost.hostname) + with pytest.raises(KeyError): + values['properties']['properties_table']['Insights'] + result = rhel_contenthost.run('insights-client') + assert result.status == 0 + # Workaround for not reading old data. + session.browser.refresh() + # Verify that Insights status again. + values = session.host.get_details(rhel_contenthost.hostname) assert values['properties']['properties_table']['Insights'] == 'Reporting clear' diff --git a/tests/foreman/ui/test_rhcloud_inventory.py b/tests/foreman/ui/test_rhcloud_inventory.py index cca98d346d0..32ad7f9d030 100644 --- a/tests/foreman/ui/test_rhcloud_inventory.py +++ b/tests/foreman/ui/test_rhcloud_inventory.py @@ -20,7 +20,6 @@ from datetime import timedelta import pytest -from airgun.session import Session from wait_for import wait_for from robottelo.constants import DEFAULT_LOC @@ -59,7 +58,10 @@ def common_assertion(report_path, inventory_data, org, satellite): @pytest.mark.run_in_one_thread @pytest.mark.tier3 def test_rhcloud_inventory_e2e( - inventory_settings, organization_ak_setup, rhcloud_registered_hosts, rhcloud_sat_host + inventory_settings, + rhcloud_manifest_org, + rhcloud_registered_hosts, + module_target_sat, ): """Generate report and verify its basic properties @@ -82,15 +84,15 @@ def test_rhcloud_inventory_e2e( :BZ: 1807829, 1926100 """ - org, ak = organization_ak_setup + org = rhcloud_manifest_org virtual_host, baremetal_host = rhcloud_registered_hosts - with Session(hostname=rhcloud_sat_host.hostname) as session: + with module_target_sat.ui_session() as session: session.organization.select(org_name=org.name) session.location.select(loc_name=DEFAULT_LOC) timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime('%Y-%m-%d %H:%M') session.cloudinventory.generate_report(org.name) wait_for( - lambda: rhcloud_sat_host.api.ForemanTask() + lambda: module_target_sat.api.ForemanTask() .search( query={ 'search': f'label = ForemanInventoryUpload::Async::GenerateReportJob ' @@ -107,7 +109,7 @@ def test_rhcloud_inventory_e2e( report_path = session.cloudinventory.download_report(org.name) inventory_data = session.cloudinventory.read(org.name) - common_assertion(report_path, inventory_data, org, rhcloud_sat_host) + common_assertion(report_path, inventory_data, org, module_target_sat) json_data = get_report_data(report_path) hostnames = [host['fqdn'] for host in json_data['hosts']] assert virtual_host.hostname in hostnames @@ -130,7 +132,10 @@ def test_rhcloud_inventory_e2e( @pytest.mark.run_in_one_thread @pytest.mark.tier3 def test_obfuscate_host_names( - rhcloud_sat_host, inventory_settings, organization_ak_setup, rhcloud_registered_hosts + module_target_sat, + inventory_settings, + rhcloud_manifest_org, + rhcloud_registered_hosts, ): """Test whether `Obfuscate host names` setting works as expected. @@ -152,9 +157,9 @@ def test_obfuscate_host_names( :CaseAutomation: Automated """ - org, ak = organization_ak_setup + org = rhcloud_manifest_org virtual_host, baremetal_host = rhcloud_registered_hosts - with Session(hostname=rhcloud_sat_host.hostname) as session: + with module_target_sat.ui_session() as session: session.organization.select(org_name=org.name) session.location.select(loc_name=DEFAULT_LOC) # Enable obfuscate_hostnames setting on inventory page. @@ -163,7 +168,7 @@ def test_obfuscate_host_names( session.cloudinventory.generate_report(org.name) # wait_for_tasks report generation task to finish. wait_for( - lambda: rhcloud_sat_host.api.ForemanTask() + lambda: module_target_sat.api.ForemanTask() .search( query={ 'search': f'label = ForemanInventoryUpload::Async::GenerateReportJob ' @@ -183,7 +188,7 @@ def test_obfuscate_host_names( # Assert that obfuscate_hostnames is enabled. assert inventory_data['obfuscate_hostnames'] is True # Assert that generated archive is valid. - common_assertion(report_path, inventory_data, org, rhcloud_sat_host) + common_assertion(report_path, inventory_data, org, module_target_sat) # Get report data for assertion json_data = get_report_data(report_path) hostnames = [host['fqdn'] for host in json_data['hosts']] @@ -197,12 +202,12 @@ def test_obfuscate_host_names( session.cloudinventory.update({'obfuscate_hostnames': False}) # Enable obfuscate_hostnames setting. - rhcloud_sat_host.update_setting('obfuscate_inventory_hostnames', True) + module_target_sat.update_setting('obfuscate_inventory_hostnames', True) timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime('%Y-%m-%d %H:%M') session.cloudinventory.generate_report(org.name) # wait_for_tasks report generation task to finish. wait_for( - lambda: rhcloud_sat_host.api.ForemanTask() + lambda: module_target_sat.api.ForemanTask() .search( query={ 'search': f'label = ForemanInventoryUpload::Async::GenerateReportJob ' @@ -232,7 +237,10 @@ def test_obfuscate_host_names( @pytest.mark.run_in_one_thread @pytest.mark.tier3 def test_obfuscate_host_ipv4_addresses( - rhcloud_sat_host, inventory_settings, organization_ak_setup, rhcloud_registered_hosts + module_target_sat, + inventory_settings, + rhcloud_manifest_org, + rhcloud_registered_hosts, ): """Test whether `Obfuscate host ipv4 addresses` setting works as expected. @@ -258,9 +266,9 @@ def test_obfuscate_host_ipv4_addresses( :CaseAutomation: Automated """ - org, ak = organization_ak_setup + org = rhcloud_manifest_org virtual_host, baremetal_host = rhcloud_registered_hosts - with Session(hostname=rhcloud_sat_host.hostname) as session: + with module_target_sat.ui_session() as session: session.organization.select(org_name=org.name) session.location.select(loc_name=DEFAULT_LOC) # Enable obfuscate_ips setting on inventory page. @@ -269,7 +277,7 @@ def test_obfuscate_host_ipv4_addresses( session.cloudinventory.generate_report(org.name) # wait_for_tasks report generation task to finish. wait_for( - lambda: rhcloud_sat_host.api.ForemanTask() + lambda: module_target_sat.api.ForemanTask() .search( query={ 'search': f'label = ForemanInventoryUpload::Async::GenerateReportJob ' @@ -288,7 +296,7 @@ def test_obfuscate_host_ipv4_addresses( # Assert that obfuscate_ips is enabled. assert inventory_data['obfuscate_ips'] is True # Assert that generated archive is valid. - common_assertion(report_path, inventory_data, org, rhcloud_sat_host) + common_assertion(report_path, inventory_data, org, module_target_sat) # Get report data for assertion json_data = get_report_data(report_path) hostnames = [host['fqdn'] for host in json_data['hosts']] @@ -308,12 +316,12 @@ def test_obfuscate_host_ipv4_addresses( session.cloudinventory.update({'obfuscate_ips': False}) # Enable obfuscate_inventory_ips setting. - rhcloud_sat_host.update_setting('obfuscate_inventory_ips', True) + module_target_sat.update_setting('obfuscate_inventory_ips', True) timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime('%Y-%m-%d %H:%M') session.cloudinventory.generate_report(org.name) # wait_for_tasks report generation task to finish. wait_for( - lambda: rhcloud_sat_host.api.ForemanTask() + lambda: module_target_sat.api.ForemanTask() .search( query={ 'search': f'label = ForemanInventoryUpload::Async::GenerateReportJob ' @@ -350,7 +358,10 @@ def test_obfuscate_host_ipv4_addresses( @pytest.mark.run_in_one_thread @pytest.mark.tier3 def test_exclude_packages_setting( - rhcloud_sat_host, inventory_settings, organization_ak_setup, rhcloud_registered_hosts + module_target_sat, + inventory_settings, + rhcloud_manifest_org, + rhcloud_registered_hosts, ): """Test whether `Exclude Packages` setting works as expected. @@ -377,9 +388,9 @@ def test_exclude_packages_setting( :CaseAutomation: Automated """ - org, ak = organization_ak_setup + org = rhcloud_manifest_org virtual_host, baremetal_host = rhcloud_registered_hosts - with Session(hostname=rhcloud_sat_host.hostname) as session: + with module_target_sat.ui_session() as session: session.organization.select(org_name=org.name) session.location.select(loc_name=DEFAULT_LOC) # Enable exclude_packages setting on inventory page. @@ -387,7 +398,7 @@ def test_exclude_packages_setting( timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime('%Y-%m-%d %H:%M') session.cloudinventory.generate_report(org.name) wait_for( - lambda: rhcloud_sat_host.api.ForemanTask() + lambda: module_target_sat.api.ForemanTask() .search( query={ 'search': f'label = ForemanInventoryUpload::Async::GenerateReportJob ' @@ -407,7 +418,7 @@ def test_exclude_packages_setting( # Disable exclude_packages setting on inventory page. session.cloudinventory.update({'exclude_packages': False}) # Assert that generated archive is valid. - common_assertion(report_path, inventory_data, org, rhcloud_sat_host) + common_assertion(report_path, inventory_data, org, module_target_sat) # Get report data for assertion json_data = get_report_data(report_path) # Assert that right hosts are present in report. @@ -420,11 +431,11 @@ def test_exclude_packages_setting( assert 'installed_packages' not in host_profiles # Enable exclude_installed_packages setting. - rhcloud_sat_host.update_setting('exclude_installed_packages', True) + module_target_sat.update_setting('exclude_installed_packages', True) timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime('%Y-%m-%d %H:%M') session.cloudinventory.generate_report(org.name) wait_for( - lambda: rhcloud_sat_host.api.ForemanTask() + lambda: module_target_sat.api.ForemanTask() .search( query={ 'search': f'label = ForemanInventoryUpload::Async::GenerateReportJob ' From 9fb8ac913658d8bd7eb74126e3d9bbd1e7297b07 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 29 Aug 2023 03:29:53 -0400 Subject: [PATCH 167/586] [6.14.z] Add Libvirt CR setup helper to fix UI E2E tests (#12394) --- robottelo/host_helpers/__init__.py | 5 +++- robottelo/host_helpers/satellite_mixins.py | 27 +++++++++++++++++++ .../ui/test_computeresource_libvirt.py | 25 +++++++++-------- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/robottelo/host_helpers/__init__.py b/robottelo/host_helpers/__init__.py index 9757660606b..87e78daae75 100644 --- a/robottelo/host_helpers/__init__.py +++ b/robottelo/host_helpers/__init__.py @@ -6,6 +6,7 @@ from robottelo.host_helpers.satellite_mixins import ContentInfo from robottelo.host_helpers.satellite_mixins import EnablePluginsSatellite from robottelo.host_helpers.satellite_mixins import Factories +from robottelo.host_helpers.satellite_mixins import ProvisioningSetup from robottelo.host_helpers.satellite_mixins import SystemInfo @@ -17,5 +18,7 @@ class CapsuleMixins(CapsuleInfo, EnablePluginsCapsule): pass -class SatelliteMixins(ContentInfo, Factories, SystemInfo, EnablePluginsSatellite): +class SatelliteMixins( + ContentInfo, Factories, SystemInfo, EnablePluginsSatellite, ProvisioningSetup +): pass diff --git a/robottelo/host_helpers/satellite_mixins.py b/robottelo/host_helpers/satellite_mixins.py index 82f4a4275c8..4f469ef0d90 100644 --- a/robottelo/host_helpers/satellite_mixins.py +++ b/robottelo/host_helpers/satellite_mixins.py @@ -310,6 +310,33 @@ def validate_pulp_filepath( return result.stdout +class ProvisioningSetup: + """Provisioning tests setup helper methods""" + + def configure_libvirt_cr(self, server_fqdn=settings.libvirt.libvirt_hostname): + """Configures Libvirt ComputeResource to communicate with Satellite + + :param server_fqdn: Libvirt server FQDN + :return: None + """ + # Geneate SSH key-pair for foreman user and copy public key to libvirt server + self.execute('sudo -u foreman ssh-keygen -q -t rsa -f ~foreman/.ssh/id_rsa -N "" <<< y') + self.execute(f'ssh-keyscan -t ecdsa {server_fqdn} >> ~foreman/.ssh/known_hosts') + self.execute( + f'sshpass -p {settings.server.ssh_password} ssh-copy-id -o StrictHostKeyChecking=no ' + f'-i ~foreman/.ssh/id_rsa root@{server_fqdn}' + ) + # Install libvirt-client, and verify foreman user is able to communicate with Libvirt server + self.register_to_cdn() + self.execute('dnf -y --disableplugin=foreman-protector install libvirt-client') + assert ( + self.execute( + f'su foreman -s /bin/bash -c "virsh -c qemu+ssh://root@{server_fqdn}/system list"' + ).status + == 0 + ) + + class Factories: """Mixin that provides attributes for each factory type""" diff --git a/tests/foreman/ui/test_computeresource_libvirt.py b/tests/foreman/ui/test_computeresource_libvirt.py index 73eb14507e8..53815d45ec7 100644 --- a/tests/foreman/ui/test_computeresource_libvirt.py +++ b/tests/foreman/ui/test_computeresource_libvirt.py @@ -20,7 +20,6 @@ import pytest from fauxfactory import gen_string -from nailgun import entities from robottelo.config import settings from robottelo.constants import COMPUTE_PROFILE_SMALL @@ -29,14 +28,12 @@ pytestmark = [pytest.mark.skip_if_not_set('libvirt')] - -@pytest.fixture(scope='module') -def module_libvirt_url(): - return LIBVIRT_RESOURCE_URL % settings.libvirt.libvirt_hostname +LIBVIRT_URL = LIBVIRT_RESOURCE_URL % settings.libvirt.libvirt_hostname @pytest.mark.tier2 -def test_positive_end_to_end(session, module_org, module_location, module_libvirt_url): +@pytest.mark.e2e +def test_positive_end_to_end(session, module_target_sat, module_org, module_location): """Perform end to end testing for compute resource Libvirt component. :id: 7ef925ac-5aec-4e9d-b786-328a9b219c01 @@ -53,17 +50,18 @@ def test_positive_end_to_end(session, module_org, module_location, module_libvir cr_description = gen_string('alpha') new_cr_name = gen_string('alpha') new_cr_description = gen_string('alpha') - new_org = entities.Organization().create() - new_loc = entities.Location().create() + new_org = module_target_sat.api.Organization().create() + new_loc = module_target_sat.api.Location().create() display_type = choice(('VNC', 'SPICE')) console_passwords = choice((True, False)) + module_target_sat.configure_libvirt_cr() with session: session.computeresource.create( { 'name': cr_name, 'description': cr_description, 'provider': FOREMAN_PROVIDERS['libvirt'], - 'provider_content.url': module_libvirt_url, + 'provider_content.url': LIBVIRT_URL, 'provider_content.display_type': display_type, 'provider_content.console_passwords': console_passwords, 'organizations.resources.assigned': [module_org.name], @@ -73,7 +71,7 @@ def test_positive_end_to_end(session, module_org, module_location, module_libvir cr_values = session.computeresource.read(cr_name) assert cr_values['name'] == cr_name assert cr_values['description'] == cr_description - assert cr_values['provider_content']['url'] == module_libvirt_url + assert cr_values['provider_content']['url'] == LIBVIRT_URL assert cr_values['provider_content']['display_type'] == display_type assert cr_values['provider_content']['console_passwords'] == console_passwords assert cr_values['organizations']['resources']['assigned'] == [module_org.name] @@ -102,7 +100,7 @@ def test_positive_end_to_end(session, module_org, module_location, module_libvir # check that the compute resource is listed in one of the default compute profiles profile_cr_values = session.computeprofile.list_resources(COMPUTE_PROFILE_SMALL) profile_cr_names = [cr['Compute Resource'] for cr in profile_cr_values] - assert '{} ({})'.format(new_cr_name, FOREMAN_PROVIDERS['libvirt']) in profile_cr_names + assert f'{new_cr_name} ({FOREMAN_PROVIDERS["libvirt"]})' in profile_cr_names session.computeresource.update_computeprofile( new_cr_name, COMPUTE_PROFILE_SMALL, @@ -112,8 +110,9 @@ def test_positive_end_to_end(session, module_org, module_location, module_libvir new_cr_name, COMPUTE_PROFILE_SMALL ) assert cr_profile_values['compute_profile'] == COMPUTE_PROFILE_SMALL - assert cr_profile_values['compute_resource'] == '{} ({})'.format( - new_cr_name, FOREMAN_PROVIDERS['libvirt'] + assert ( + cr_profile_values['compute_resource'] + == f'{new_cr_name} ({FOREMAN_PROVIDERS["libvirt"]})' ) assert cr_profile_values['provider_content']['cpus'] == '16' assert cr_profile_values['provider_content']['memory'] == '8192 MB' From c39b8f2d885092123aa8798419f28b47054e6075 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 29 Aug 2023 08:20:29 -0400 Subject: [PATCH 168/586] [6.14.z] Make old foreman task 32 days older (#12398) --- tests/foreman/maintain/test_health.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/foreman/maintain/test_health.py b/tests/foreman/maintain/test_health.py index fe58b8e7c7e..ae3d45435c1 100644 --- a/tests/foreman/maintain/test_health.py +++ b/tests/foreman/maintain/test_health.py @@ -399,12 +399,14 @@ def test_positive_health_check_old_foreman_tasks(sat_maintain): :expectedresults: check-old-foreman-tasks health check should pass. """ - rake_command = 'foreman-rake console <<< ' - find_task = '\'t = ForemanTasks::Task.where(state: "stopped").first;' - update_task = "t.started_at = t.started_at - 31.day;t.save(:validate => false)'" error_message = 'paused or stopped task(s) older than 30 days' delete_message = 'Deleted old tasks:' - sat_maintain.execute(rake_command + find_task + update_task) + rake_command = ( + 't = ForemanTasks::Task.where(state: "stopped").first;' + 't.started_at = t.started_at - 32.day;' + 't.save(:validate => false)' + ) + sat_maintain.execute(f"foreman-rake console <<< '{rake_command}'") result = sat_maintain.cli.Health.check( options={'label': 'check-old-foreman-tasks', 'assumeyes': True} ) From 7f85456c03c3b4b2a29b6b3a6b1963990f3f8056 Mon Sep 17 00:00:00 2001 From: Samuel Bible Date: Tue, 29 Aug 2023 08:21:40 -0500 Subject: [PATCH 169/586] Fix unreliable assertion (#12176) * Remove unreliable assertion * Remove unreliable assertion * Assert public_ip is in the dictionary instead * Use alternate function for reading virt card * Rework some fields to be more reliable, and add missing fields * Turn VM on if it's off, and check IP addr isn't none * Add try/waitfor to ensure VM has fully powered on (cherry picked from commit ac29b95851e103e5d7648e97d5621aa006ca2975) --- .../foreman/ui/test_computeresource_vmware.py | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/tests/foreman/ui/test_computeresource_vmware.py b/tests/foreman/ui/test_computeresource_vmware.py index 9a51147201d..c5576bdfcee 100644 --- a/tests/foreman/ui/test_computeresource_vmware.py +++ b/tests/foreman/ui/test_computeresource_vmware.py @@ -593,18 +593,39 @@ def test_positive_virt_card( module_vmware_settings['vm_name'], ) host_name = module_vmware_settings['vm_name'] + '.' + domain.name - virt_card = session.host_new.get_details(host_name, widget_names='details.virtualization')[ - 'details' - ]['virtualization'] + power_status = session.computeresource.vm_status(cr_name, module_vmware_settings['vm_name']) + if power_status is False: + session.computeresource.vm_poweron(cr_name, module_vmware_settings['vm_name']) + try: + wait_for( + lambda: ( + session.browser.refresh(), + session.computeresource.vm_status( + cr_name, module_vmware_settings['vm_name'] + ), + )[1] + is not power_status, + timeout=30, + delay=2, + ) + except TimedOutError: + raise AssertionError('Timed out waiting for VM to toggle power state') + + virt_card = session.host_new.get_virtualization(host_name)['details'] assert virt_card['datacenter'] == module_vmware_settings['datacenter'] assert virt_card['cluster'] == module_vmware_settings['cluster'] assert virt_card['memory'] == '2 GB' - assert virt_card['public_ip_address'] + assert virt_card['public_ip_address'] != '' assert virt_card['mac_address'] == module_vmware_settings['mac_address'] assert virt_card['cpus'] == '1' + assert virt_card['disk_label'] == 'Hard disk 1' + assert virt_card['disk_capacity'] != '' + assert virt_card['partition_capacity'] != '' + assert virt_card['partition_path'] == '/boot' + assert virt_card['partition_allocation'] != '' assert virt_card['cores_per_socket'] == '1' assert virt_card['firmware'] == 'bios' - assert virt_card['hypervisor'] == module_vmware_settings['hypervisor'] + assert virt_card['hypervisor'] != '' assert virt_card['connection_state'] == 'connected' assert virt_card['overall_status'] == 'green' assert virt_card['annotation_notes'] == '' From 876f246bac43229a1a68bfba39a883f2561c5827 Mon Sep 17 00:00:00 2001 From: Samuel Bible Date: Tue, 29 Aug 2023 12:59:20 -0500 Subject: [PATCH 170/586] Check for existance of certain fields before asserting on them (#12410) --- tests/foreman/ui/test_computeresource_vmware.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/foreman/ui/test_computeresource_vmware.py b/tests/foreman/ui/test_computeresource_vmware.py index c5576bdfcee..39398bd3c4d 100644 --- a/tests/foreman/ui/test_computeresource_vmware.py +++ b/tests/foreman/ui/test_computeresource_vmware.py @@ -618,11 +618,16 @@ def test_positive_virt_card( assert virt_card['public_ip_address'] != '' assert virt_card['mac_address'] == module_vmware_settings['mac_address'] assert virt_card['cpus'] == '1' - assert virt_card['disk_label'] == 'Hard disk 1' - assert virt_card['disk_capacity'] != '' - assert virt_card['partition_capacity'] != '' - assert virt_card['partition_path'] == '/boot' - assert virt_card['partition_allocation'] != '' + if virt_card['disk_label']: + assert virt_card['disk_label'] == 'Hard disk 1' + if virt_card['disk_capacity']: + assert virt_card['disk_capacity'] != '' + if virt_card['partition_capacity']: + assert virt_card['partition_capacity'] != '' + if virt_card['partition_path']: + assert virt_card['partition_path'] == '/boot' + if virt_card['partition_allocation']: + assert virt_card['partition_allocation'] != '' assert virt_card['cores_per_socket'] == '1' assert virt_card['firmware'] == 'bios' assert virt_card['hypervisor'] != '' From cf009fa3bb6a1a4af5c02058a5930e3699dead24 Mon Sep 17 00:00:00 2001 From: Samuel Bible Date: Tue, 29 Aug 2023 15:14:12 -0500 Subject: [PATCH 171/586] Syntax fix. --- tests/foreman/ui/test_computeresource_vmware.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/foreman/ui/test_computeresource_vmware.py b/tests/foreman/ui/test_computeresource_vmware.py index 39398bd3c4d..38d99f620b0 100644 --- a/tests/foreman/ui/test_computeresource_vmware.py +++ b/tests/foreman/ui/test_computeresource_vmware.py @@ -618,15 +618,15 @@ def test_positive_virt_card( assert virt_card['public_ip_address'] != '' assert virt_card['mac_address'] == module_vmware_settings['mac_address'] assert virt_card['cpus'] == '1' - if virt_card['disk_label']: + if 'disk_label' in virt_card: assert virt_card['disk_label'] == 'Hard disk 1' - if virt_card['disk_capacity']: + if 'disk_capacity' in virt_card: assert virt_card['disk_capacity'] != '' - if virt_card['partition_capacity']: + if 'partition_capacity' in virt_card: assert virt_card['partition_capacity'] != '' - if virt_card['partition_path']: + if 'partition_path' in virt_card: assert virt_card['partition_path'] == '/boot' - if virt_card['partition_allocation']: + if 'partition_allocation' in virt_card: assert virt_card['partition_allocation'] != '' assert virt_card['cores_per_socket'] == '1' assert virt_card['firmware'] == 'bios' From 4def16d062d3131386f9b37acff00520339b912b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 30 Aug 2023 02:00:18 -0400 Subject: [PATCH 172/586] [6.14.z] Remove some redundant tests from Errata-management (#12406) Remove some redundant tests from Errata-management (cherry picked from commit 0202f1a64ce18ad5905f20f55a6976f14ddb0350) Co-authored-by: David Moore --- tests/foreman/api/test_errata.py | 50 -------------- tests/foreman/cli/test_errata.py | 109 ------------------------------- 2 files changed, 159 deletions(-) diff --git a/tests/foreman/api/test_errata.py b/tests/foreman/api/test_errata.py index 775420e6c8d..7e0afba949d 100644 --- a/tests/foreman/api/test_errata.py +++ b/tests/foreman/api/test_errata.py @@ -211,56 +211,6 @@ def test_positive_install_in_hc(module_org, activation_key, custom_repo, target_ assert result.status == 0 -@pytest.mark.tier3 -@pytest.mark.rhel_ver_list([7, 8, 9]) -@pytest.mark.no_containers -def test_positive_install_in_host( - module_org, activation_key, custom_repo, rhel_contenthost, target_sat -): - """Install errata in a host - - :id: 1e6fc159-b0d6-436f-b945-2a5731c46df5 - - :Setup: Errata synced on satellite server. - - :Steps: POST /api/v2/job_invocations/{hash} - - :expectedresults: errata is installed in the host. - - :parametrized: yes - - :CaseLevel: System - - :BZ: 1983043 - """ - rhel_contenthost.install_katello_ca(target_sat) - rhel_contenthost.register_contenthost(module_org.label, activation_key.name) - assert rhel_contenthost.subscribed - host_id = rhel_contenthost.nailgun_host.id - _install_package( - module_org, - clients=[rhel_contenthost], - host_ids=[host_id], - package_name=constants.FAKE_1_CUSTOM_PACKAGE, - ) - rhel_contenthost.add_rex_key(satellite=target_sat) - task_id = target_sat.api.JobInvocation().run( - data={ - 'feature': 'katello_errata_install', - 'inputs': {'errata': str(CUSTOM_REPO_ERRATA_ID)}, - 'targeting_type': 'static_query', - 'search_query': f'name = {rhel_contenthost.hostname}', - 'organization_id': module_org.id, - }, - )['id'] - target_sat.wait_for_tasks( - search_query=(f'label = Actions::RemoteExecution::RunHostsJob and id = {task_id}'), - search_rate=15, - max_tries=10, - ) - _validate_package_installed([rhel_contenthost], constants.FAKE_2_CUSTOM_PACKAGE) - - @pytest.mark.tier3 @pytest.mark.rhel_ver_list([7, 8, 9]) @pytest.mark.no_containers diff --git a/tests/foreman/cli/test_errata.py b/tests/foreman/cli/test_errata.py index 6e2c562ee3d..7d8577c3816 100644 --- a/tests/foreman/cli/test_errata.py +++ b/tests/foreman/cli/test_errata.py @@ -23,7 +23,6 @@ import pytest from broker import Broker -from fauxfactory import gen_string from nailgun import entities from robottelo.cli.activationkey import ActivationKey @@ -33,22 +32,16 @@ from robottelo.cli.erratum import Erratum from robottelo.cli.factory import make_content_view_filter from robottelo.cli.factory import make_content_view_filter_rule -from robottelo.cli.factory import make_filter from robottelo.cli.factory import make_host_collection from robottelo.cli.factory import make_repository -from robottelo.cli.factory import make_role -from robottelo.cli.factory import make_user from robottelo.cli.factory import setup_org_for_a_custom_repo from robottelo.cli.factory import setup_org_for_a_rh_repo -from robottelo.cli.filter import Filter from robottelo.cli.host import Host from robottelo.cli.hostcollection import HostCollection from robottelo.cli.job_invocation import JobInvocation -from robottelo.cli.org import Org from robottelo.cli.package import Package from robottelo.cli.repository import Repository from robottelo.cli.repository_set import RepositorySet -from robottelo.cli.user import User from robottelo.config import settings from robottelo.constants import DEFAULT_ARCHITECTURE from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME @@ -1205,108 +1198,6 @@ def test_positive_list_filter_by_cve(module_entitlement_manifest_org, rh_repo): } -@pytest.mark.tier3 -@pytest.mark.upgrade -def test_positive_user_permission(products_with_repos): - """Show errata only if the User has permissions to view them - - :id: f350c13b-8cf9-4aa5-8c3a-1c48397ea514 - - :Setup: - - 1. Create two products with one repo each. Sync them. - 2. Make sure that they both have errata. - 3. Create a user with view access on one product and not on the - other. - - :Steps: erratum list --organization-id= - - :expectedresults: Check that the new user is able to see errata for one - product only. - - :BZ: 1403947 - """ - user_password = gen_string('alphanumeric') - user_name = gen_string('alphanumeric') - - product = products_with_repos[3] - org = product.organization - - # get the available permissions - permissions = Filter.available_permissions() - user_required_permissions_names = ['view_products'] - # get the user required permissions ids - user_required_permissions_ids = [ - permission['id'] - for permission in permissions - if permission['name'] in user_required_permissions_names - ] - assert len(user_required_permissions_ids) > 0 - - # create a role - role = make_role({'organization-ids': org.id}) - - # create a filter with the required permissions for role with product - # one only - make_filter( - { - 'permission-ids': user_required_permissions_ids, - 'role-id': role['id'], - 'search': f"name = {product.name}", - } - ) - - # create a new user and assign him the created role permissions - user = make_user( - { - 'admin': False, - 'login': user_name, - 'password': user_password, - 'organization-ids': [org.id], - 'default-organization-id': org.id, - } - ) - User.add_role({'id': user['id'], 'role-id': role['id']}) - - # make sure the user is not admin and has only the permissions assigned - user = User.info({'id': user['id']}) - assert user['admin'] == 'no' - assert set(user['roles']) == {role['name']} - - # try to get organization info - # get the info as admin user first - org_info = Org.info({'id': org.id}) - assert str(org.id) == org_info['id'] - assert org.name == org_info['name'] - - # get the organization info as the created user - with pytest.raises(CLIReturnCodeError) as context: - Org.with_user(user_name, user_password).info({'id': org.id}) - assert 'Missing one of the required permissions: view_organizations' in context.value.stderr - - # try to get the erratum products list by organization id only - # ensure that all products erratum are accessible by admin user - admin_org_errata_ids = [ - errata['errata-id'] for errata in Erratum.list({'organization-id': org.id}) - ] - assert REPOS_WITH_ERRATA[2]['errata_id'] in admin_org_errata_ids - assert REPOS_WITH_ERRATA[3]['errata_id'] in admin_org_errata_ids - - assert len(admin_org_errata_ids) == ( - REPOS_WITH_ERRATA[2]['errata_count'] + REPOS_WITH_ERRATA[3]['errata_count'] - ) - - # ensure that the created user see only the erratum product that was - # assigned in permissions - user_org_errata_ids = [ - errata['errata-id'] - for errata in Erratum.with_user(user_name, user_password).list({'organization-id': org.id}) - ] - assert len(user_org_errata_ids) == REPOS_WITH_ERRATA[3]['errata_count'] - assert REPOS_WITH_ERRATA[3]['errata_id'] in user_org_errata_ids - assert REPOS_WITH_ERRATA[2]['errata_id'] not in user_org_errata_ids - - @pytest.mark.tier3 def test_positive_check_errata_dates(module_entitlement_manifest_org): """Check for errata dates in `hammer erratum list` From 947c703fc28eb4187775f682e7ac907b30f0f064 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 30 Aug 2023 02:01:41 -0400 Subject: [PATCH 173/586] [6.14.z] Use stash to only run module_synced_repos one time (#12401) Use stash to only run module_synced_repos one time (cherry picked from commit c69dd0ee966821e2d1d460f2e4a52f9c48cb2895) Co-authored-by: Griffin Sullivan --- pytest_fixtures/component/maintain.py | 82 +++++++++++++++++---------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/pytest_fixtures/component/maintain.py b/pytest_fixtures/component/maintain.py index b69f76025db..ff8e8387c4b 100644 --- a/pytest_fixtures/component/maintain.py +++ b/pytest_fixtures/component/maintain.py @@ -6,8 +6,20 @@ from robottelo import constants from robottelo.config import settings from robottelo.constants import SATELLITE_MAINTAIN_YML +from robottelo.hosts import Capsule from robottelo.hosts import Satellite +synced_repos = pytest.StashKey[dict] + + +@pytest.fixture(scope='module') +def module_stash(request): + """Module scoped stash for storing data between tests""" + # Please refer the documentation for more details on stash + # https://docs.pytest.org/en/latest/reference/reference.html#stash + request.node.stash[synced_repos] = {} + yield request.node.stash + @pytest.fixture(scope='session') def sat_maintain(request, session_target_sat, session_capsule_configured): @@ -30,35 +42,44 @@ def _finalize(): @pytest.fixture(scope='module') -def module_synced_repos(sat_maintain, session_capsule_configured, module_sca_manifest): - org = sat_maintain.api.Organization().create() - sat_maintain.upload_manifest(org.id, module_sca_manifest.content) - # sync custom repo - cust_prod = sat_maintain.api.Product(organization=org).create() - cust_repo = sat_maintain.api.Repository( - url=settings.repos.yum_1.url, product=cust_prod - ).create() - cust_repo.sync() - - # sync RH repo - product = sat_maintain.api.Product(name=constants.PRDS['rhae'], organization=org.id).search()[0] - r_set = sat_maintain.api.RepositorySet( - name=constants.REPOSET['rhae2'], product=product - ).search()[0] - payload = {'basearch': constants.DEFAULT_ARCHITECTURE, 'product_id': product.id} - r_set.enable(data=payload) - result = sat_maintain.api.Repository(name=constants.REPOS['rhae2']['name']).search( - query={'organization_id': org.id} - ) - rh_repo_id = result[0].id - rh_repo = sat_maintain.api.Repository(id=rh_repo_id).read() - rh_repo.sync() - - if not settings.remotedb.server: +def module_synced_repos( + sat_maintain, session_capsule_configured, module_sca_manifest, module_stash +): + if not module_stash[synced_repos]: + org = sat_maintain.satellite.api.Organization().create() + sat_maintain.satellite.upload_manifest(org.id, module_sca_manifest.content) + # sync custom repo + cust_prod = sat_maintain.satellite.api.Product(organization=org).create() + cust_repo = sat_maintain.satellite.api.Repository( + url=settings.repos.yum_1.url, product=cust_prod + ).create() + cust_repo.sync() + + # sync RH repo + product = sat_maintain.satellite.api.Product( + name=constants.PRDS['rhae'], organization=org.id + ).search()[0] + r_set = sat_maintain.satellite.api.RepositorySet( + name=constants.REPOSET['rhae2'], product=product + ).search()[0] + payload = {'basearch': constants.DEFAULT_ARCHITECTURE, 'product_id': product.id} + r_set.enable(data=payload) + result = sat_maintain.satellite.api.Repository( + name=constants.REPOS['rhae2']['name'] + ).search(query={'organization_id': org.id}) + rh_repo_id = result[0].id + rh_repo = sat_maintain.satellite.api.Repository(id=rh_repo_id).read() + rh_repo.sync() + + module_stash[synced_repos]['rh_repo'] = rh_repo + module_stash[synced_repos]['cust_repo'] = cust_repo + module_stash[synced_repos]['org'] = org + + if type(sat_maintain) is Capsule: # assign the Library LCE to the Capsule - lce = sat_maintain.api.LifecycleEnvironment(organization=org).search( - query={'search': f'name={constants.ENVIRONMENT}'} - )[0] + lce = sat_maintain.satellite.api.LifecycleEnvironment( + organization=module_stash[synced_repos]['org'] + ).search(query={'search': f'name={constants.ENVIRONMENT}'})[0] session_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( data={'environment_id': lce.id} ) @@ -68,7 +89,10 @@ def module_synced_repos(sat_maintain, session_capsule_configured, module_sca_man sync_status = session_capsule_configured.nailgun_capsule.content_sync() assert sync_status['result'] == 'success' - yield {'custom': cust_repo, 'rh': rh_repo} + yield { + 'custom': module_stash[synced_repos]['cust_repo'], + 'rh': module_stash[synced_repos]['rh_repo'], + } @pytest.fixture From 9762756edb52a3e44f7313a9d5ac41be462e8bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Gajdu=C5=A1ek?= Date: Wed, 30 Aug 2023 08:02:21 +0200 Subject: [PATCH 174/586] [6.14.z] Introduce and enhance robottelo.hosts features (#12144) (#12313) Introduce and enhance robottelo.hosts features (#12144) * Introduce and enhance robottelo.hosts features * Apply suggestions from code review Co-authored-by: Jake Callahan * Revert Jake's suggestion as cached_property stores data in __dict__ * Clean cached properties of LEAPPed host to get non-cached OS version * Use redhat-release to keep RHEL 6 compatibility * Reverse operations --------- Co-authored-by: Jake Callahan (cherry picked from commit 49c4692d03acb0117d63a39590185d22f6ad8db8) --- .gitignore | 4 + pytest_fixtures/core/broker.py | 10 +- pytest_fixtures/core/sat_cap_factory.py | 4 +- pytest_fixtures/core/upgrade.py | 4 +- pytest_fixtures/core/xdist.py | 8 +- robottelo/host_helpers/contenthost_mixins.py | 19 +- robottelo/hosts.py | 162 ++++++++++++++---- tests/foreman/api/test_provisioning.py | 12 +- tests/foreman/api/test_provisioning_puppet.py | 12 +- .../destructive/test_leapp_satellite.py | 6 +- tests/upgrades/test_client.py | 14 +- tests/upgrades/test_errata.py | 5 +- tests/upgrades/test_repository.py | 9 +- tests/upgrades/test_subscription.py | 6 +- 14 files changed, 176 insertions(+), 99 deletions(-) diff --git a/.gitignore b/.gitignore index 07bd9ff5076..37a548e7d4f 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,7 @@ ven*/ # pytest-fixture-tools artifacts artifacts/ + +# pyvcloud artifacts +# workaround till https://github.com/vmware/pyvcloud/pull/766 is merged and released +**vcd_sdk.log diff --git a/pytest_fixtures/core/broker.py b/pytest_fixtures/core/broker.py index 36822ad2461..4c89dea35c5 100644 --- a/pytest_fixtures/core/broker.py +++ b/pytest_fixtures/core/broker.py @@ -5,6 +5,7 @@ from broker import Broker from robottelo.config import settings +from robottelo.hosts import ContentHostError from robottelo.hosts import lru_sat_ready_rhel from robottelo.hosts import Satellite @@ -13,12 +14,9 @@ def _default_sat(align_to_satellite): """Returns a Satellite object for settings.server.hostname""" if settings.server.hostname: - hosts = Broker(host_class=Satellite).from_inventory( - filter=f'@inv.hostname == "{settings.server.hostname}"' - ) - if hosts: - return hosts[0] - else: + try: + return Satellite.get_host_by_hostname(settings.server.hostname) + except ContentHostError: return Satellite() diff --git a/pytest_fixtures/core/sat_cap_factory.py b/pytest_fixtures/core/sat_cap_factory.py index c0a9798d09f..018a89fdc24 100644 --- a/pytest_fixtures/core/sat_cap_factory.py +++ b/pytest_fixtures/core/sat_cap_factory.py @@ -322,7 +322,5 @@ def installer_satellite(request): if 'sanity' not in request.config.option.markexpr: sanity_sat = Satellite(sat.hostname) sanity_sat.unregister() - broker_sat = Broker(host_class=Satellite).from_inventory( - filter=f'@inv.hostname == "{sanity_sat.hostname}"' - )[0] + broker_sat = Satellite.get_host_by_hostname(sanity_sat.hostname) Broker(hosts=[broker_sat]).checkin() diff --git a/pytest_fixtures/core/upgrade.py b/pytest_fixtures/core/upgrade.py index fd9c2fd364b..291bae18fbf 100644 --- a/pytest_fixtures/core/upgrade.py +++ b/pytest_fixtures/core/upgrade.py @@ -38,6 +38,6 @@ def pre_configured_capsule(worker_id, session_target_sat): logger.debug(f'Capsules found: {intersect}') assert len(intersect) == 1, "More than one Capsule found in the inventory" target_capsule = intersect.pop() - hosts = Broker(host_class=Capsule).from_inventory(filter=f'@inv.hostname == "{target_capsule}"') + host = Capsule.get_host_by_hostname(target_capsule) logger.info(f'xdist worker {worker_id} was assigned pre-configured Capsule {target_capsule}') - return hosts[0] + return host diff --git a/pytest_fixtures/core/xdist.py b/pytest_fixtures/core/xdist.py index 7c4917725be..18088f8a572 100644 --- a/pytest_fixtures/core/xdist.py +++ b/pytest_fixtures/core/xdist.py @@ -19,9 +19,7 @@ def align_to_satellite(request, worker_id, satellite_factory): if settings.server.hostname: sanity_sat = Satellite(settings.server.hostname) sanity_sat.unregister() - broker_sat = Broker(host_class=Satellite).from_inventory( - filter=f'@inv.hostname == "{sanity_sat.hostname}"' - )[0] + broker_sat = Satellite.get_host_by_hostname(sanity_sat.hostname) Broker(hosts=[broker_sat]).checkin() else: # clear any hostname that may have been previously set @@ -35,9 +33,7 @@ def align_to_satellite(request, worker_id, satellite_factory): # attempt to add potential satellites from the broker inventory file if settings.server.inventory_filter: - hosts = Broker(host_class=Satellite).from_inventory( - filter=settings.server.inventory_filter - ) + hosts = Satellite.get_hosts_from_inventory(filter=settings.server.inventory_filter) settings.server.hostnames += [host.hostname for host in hosts] # attempt to align a worker to a satellite diff --git a/robottelo/host_helpers/contenthost_mixins.py b/robottelo/host_helpers/contenthost_mixins.py index a4ab9457762..30fbf576e1f 100644 --- a/robottelo/host_helpers/contenthost_mixins.py +++ b/robottelo/host_helpers/contenthost_mixins.py @@ -20,10 +20,21 @@ def _v_major(self): @cached_property def REPOSET(self): - return { - 'rhel': constants.REPOSET[f'rhel{self._v_major}'], - 'rhst': constants.REPOSET[f'rhst{self._v_major}'], - } + try: + if self._v_major > 7: + sys_reposets = { + 'rhel_bos': constants.REPOSET[f'rhel{self._v_major}_bos'], + 'rhel_aps': constants.REPOSET[f'rhel{self._v_major}_aps'], + } + else: + sys_reposets = { + 'rhel': constants.REPOSET[f'rhel{self._v_major}'], + 'rhscl': constants.REPOSET[f'rhscl{self._v_major}'], + } + reposets = {'rhst': constants.REPOSET[f'rhst{self._v_major}']} + except KeyError as err: + raise ValueError(f'Unsupported system version: {self._v_major}') from err + return sys_reposets | reposets @cached_property def REPOS(self): diff --git a/robottelo/hosts.py b/robottelo/hosts.py index e1ad644bce9..c14636267e6 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1,3 +1,4 @@ +import contextlib import importlib import io import json @@ -198,6 +199,26 @@ def __init__(self, hostname, auth=None, **kwargs): self.blank = kwargs.get('blank', False) super().__init__(hostname=hostname, **kwargs) + @classmethod + def get_hosts_from_inventory(cls, filter): + """Get an instance of a host from inventory using a filter""" + inv_hosts = Broker(host_class=cls).from_inventory(filter) + logger.debug('Found %s instances from inventory by filter: %s', len(inv_hosts), filter) + return inv_hosts + + @classmethod + def get_host_by_hostname(cls, hostname): + """Get an instance of a host from inventory by hostname""" + logger.info('Getting %s instance from inventory by hostname: %s', cls.__name__, hostname) + inv_hosts = cls.get_hosts_from_inventory(filter=f'@inv.hostname == "{hostname}"') + if not inv_hosts: + raise ContentHostError(f'No {cls.__name__} found in inventory by hostname {hostname}') + if len(inv_hosts) > 1: + raise ContentHostError( + f'Multiple {cls.__name__} found in inventory by hostname {hostname}' + ) + return inv_hosts[0] + @property def satellite(self): if not self._satellite: @@ -248,31 +269,107 @@ def arch(self): @cached_property def _redhat_release(self): - """Process redhat-release file for distro and version information""" + """Process redhat-release file for distro and version information + This is a fallback for when /etc/os-release is not available + """ result = self.execute('cat /etc/redhat-release') if result.status != 0: raise ContentHostError(f'Not able to cat /etc/redhat-release "{result.stderr}"') - match = re.match(r'(?P.+) release (?P\d+)(.(?P\d+))?', result.stdout) + match = re.match(r'(?P.+) release (?P\d+)(.(?P\d+))?', result.stdout) if match is None: raise ContentHostError(f'Not able to parse release string "{result.stdout}"') - return match.groupdict() + r_release = match.groupdict() + + # /etc/os-release compatibility layer + r_release['VERSION_ID'] = r_release['major'] + # not every release have a minor version + r_release['VERSION_ID'] += f'.{r_release["minor"]}' if r_release['minor'] else '' + + distro_map = { + 'Fedora': {'NAME': 'Fedora Linux', 'ID': 'fedora'}, + 'CentOS': {'ID': 'centos'}, + 'Red Hat Enterprise Linux': {'ID': 'rhel'}, + } + # Use the version map to set the NAME and ID fields + for distro, properties in distro_map.items(): + if distro in r_release['NAME']: + r_release.update(properties) + break + return r_release @cached_property + def _os_release(self): + """Process os-release file for distro and version information""" + facts = {} + regex = r'^(["\'])(.*)(\1)$' + result = self.execute('cat /etc/os-release') + if result.status != 0: + logger.info( + f'Not able to cat /etc/os-release "{result.stderr}", ' + 'falling back to /etc/redhat-release' + ) + return self._redhat_release + for ln in [line for line in result.stdout.splitlines() if line.strip()]: + line = ln.strip() + if line.startswith('#'): + continue + key, value = line.split('=') + if key and value: + facts[key] = re.sub(regex, r'\2', value).replace('\\', '') + return facts + + @property def os_distro(self): """Get host's distro information""" - groups = self._redhat_release - return groups['distro'] + return self._os_release['NAME'] - @cached_property + @property def os_version(self): """Get host's OS version information :returns: A ``packaging.version.Version`` instance """ - groups = self._redhat_release - minor_version = '' if groups['minor'] is None else f'.{groups["minor"]}' - version_string = f'{groups["major"]}{minor_version}' - return Version(version=version_string) + return Version(self._os_release['VERSION_ID']) + + @property + def os_id(self): + """Get host's OS ID information""" + return self._os_release['ID'] + + @cached_property + def is_el(self): + """Boolean representation of whether this host is an EL host""" + return self.execute('stat /etc/redhat-release').status == 0 + + @property + def is_rhel(self): + """Boolean representation of whether this host is a RHEL host""" + return self.os_id == 'rhel' + + @property + def is_centos(self): + """Boolean representation of whether this host is a CentOS host""" + return self.os_id == 'centos' + + def list_cached_properties(self): + """Return a list of cached property names of this class""" + import inspect + + return [ + name + for name, value in inspect.getmembers(self.__class__) + if isinstance(value, cached_property) + ] + + def get_cached_properties(self): + """Return a dictionary of cached properties for this class""" + return {name: getattr(self, name) for name in self.list_cached_properties()} + + def clean_cached_properties(self): + """Delete all cached properties for this class""" + for name in self.list_cached_properties(): + with contextlib.suppress(KeyError): # ignore if property is not cached + del self.__dict__[name] def setup(self): if not self.blank: @@ -280,18 +377,9 @@ def setup(self): def teardown(self): if not self.blank and not getattr(self, '_skip_context_checkin', False): - if self.nailgun_host: - self.nailgun_host.delete() self.unregister() - # Strip most unnecessary attributes from our instance for checkin - keep_keys = set(self.to_dict()) | { - 'release', - '_prov_inst', - '_cont_inst', - '_skip_context_checkin', - } - self.__dict__ = {k: v for k, v in self.__dict__.items() if k in keep_keys} - self.__class__ = Host + if type(self) is not Satellite and self.nailgun_host: + self.nailgun_host.delete() def power_control(self, state=VmState.RUNNING, ensure=True): """Lookup the host workflow for power on and execute @@ -305,6 +393,8 @@ def power_control(self, state=VmState.RUNNING, ensure=True): BrokerError: various error types to do with broker execution ContentHostError: if the workflow status isn't successful and broker didn't raise """ + if getattr(self, '_cont_inst', None): + raise NotImplementedError('Power control not supported for container instances') try: vm_operation = POWER_OPERATIONS.get(state) workflow_name = settings.broker.host_workflows.power_control @@ -331,8 +421,23 @@ def power_control(self, state=VmState.RUNNING, ensure=True): self.connect, fail_condition=lambda res: res is not None, handle_exception=True ) # really broad diaper here, but connection exceptions could be a ton of types - except TimedOutError: - raise ContentHostError('Unable to connect to host that should be running') + except TimedOutError as toe: + raise ContentHostError('Unable to connect to host that should be running') from toe + + def wait_for_connection(self, timeout=180): + try: + wait_for( + self.connect, + fail_condition=lambda res: res is not None, + handle_exception=True, + raise_original=True, + timeout=timeout, + delay=1, + ) + except (ConnectionRefusedError, ConnectionAbortedError, TimedOutError) as err: + raise ContentHostError( + f'Unable to establsh SSH connection to host {self} after {timeout} seconds' + ) from err def download_file(self, file_url, local_path=None, file_name=None): """Downloads file from given fileurl to directory specified by local_path by given filename @@ -542,6 +647,8 @@ def remove_katello_ca(self): :return: None. :raises robottelo.hosts.ContentHostError: If katello-ca wasn't removed. """ + # unregister host from CDN to avoid subscription leakage + self.execute('subscription-manager unregister') # Not checking the status here, as rpm can be not even installed # and deleting may fail self.execute('yum erase -y $(rpm -qa |grep katello-ca-consumer)') @@ -1394,12 +1501,9 @@ def satellite(self): answers = Box(yaml.load(data, yaml.FullLoader)) sat_hostname = urlparse(answers.foreman_proxy.foreman_base_url).netloc # get the Satellite hostname from the answer file - hosts = Broker(host_class=Satellite).from_inventory( - filter=f'@inv.hostname == "{sat_hostname}"' - ) - if hosts: - self._satellite = hosts[0] - else: + try: + self._satellite = Satellite.get_host_by_hostname(sat_hostname) + except ContentHostError: logger.debug( f'No Satellite host found in inventory for {self.hostname}. ' 'Satellite object with the same hostname will be created anyway.' diff --git a/tests/foreman/api/test_provisioning.py b/tests/foreman/api/test_provisioning.py index 4d54a403672..30fd3587c66 100644 --- a/tests/foreman/api/test_provisioning.py +++ b/tests/foreman/api/test_provisioning.py @@ -104,17 +104,7 @@ def test_rhel_pxe_provisioning( provisioning_host.blank = False # Wait for the host to be rebooted and SSH daemon to be started. - try: - wait_for( - provisioning_host.connect, - fail_condition=lambda res: res is not None, - handle_exception=True, - raise_original=True, - timeout=180, - delay=1, - ) - except ConnectionRefusedError: - raise ConnectionRefusedError("Timed out waiting for SSH daemon to start on the host") + provisioning_host.wait_for_connection() # Perform version check host_os = host.operatingsystem.read() diff --git a/tests/foreman/api/test_provisioning_puppet.py b/tests/foreman/api/test_provisioning_puppet.py index 2a317c071be..195f458b3b9 100644 --- a/tests/foreman/api/test_provisioning_puppet.py +++ b/tests/foreman/api/test_provisioning_puppet.py @@ -185,17 +185,7 @@ def test_host_provisioning_with_external_puppetserver( provisioning_host.blank = False # Wait for the host to be rebooted and SSH daemon to be started. - try: - wait_for( - provisioning_host.connect, - fail_condition=lambda res: res is not None, - handle_exception=True, - raise_original=True, - timeout=180, - delay=1, - ) - except ConnectionRefusedError: - raise ConnectionRefusedError('Timed out waiting for SSH daemon to start on the host') + provisioning_host.wait_for_connection() # Perform version check host_os = host.operatingsystem.read() diff --git a/tests/foreman/destructive/test_leapp_satellite.py b/tests/foreman/destructive/test_leapp_satellite.py index e4683e2f067..5bd6a19b9d7 100644 --- a/tests/foreman/destructive/test_leapp_satellite.py +++ b/tests/foreman/destructive/test_leapp_satellite.py @@ -47,10 +47,12 @@ def test_positive_leapp(target_sat): ) # Recreate the session object within a Satellite object after upgrading target_sat.connect() + # Clean cached properties after the upgrade + target_sat.clean_cached_properties() # Get RHEL version after upgrading - result = target_sat.execute('cat /etc/redhat-release | grep -Po "\\d"') + res_rhel_version = target_sat.os_version.major # Check if RHEL was upgraded - assert result.stdout[0] == str(orig_rhel_ver + 1), 'RHEL was not upgraded' + assert res_rhel_version == orig_rhel_ver + 1, 'RHEL was not upgraded' # Check satellite's health sat_health = target_sat.execute('satellite-maintain health check') assert sat_health.status == 0, 'Satellite health check failed' diff --git a/tests/upgrades/test_client.py b/tests/upgrades/test_client.py index 95c93aebcae..ed2069af5e3 100644 --- a/tests/upgrades/test_client.py +++ b/tests/upgrades/test_client.py @@ -20,7 +20,6 @@ :Upstream: No """ import pytest -from broker import Broker from robottelo.constants import FAKE_0_CUSTOM_PACKAGE_NAME from robottelo.constants import FAKE_4_CUSTOM_PACKAGE_NAME @@ -93,17 +92,12 @@ def test_post_scenario_pre_client_package_installation( :expectedresults: The package is installed on client """ - client_name = pre_upgrade_data.get('rhel_client') - client_id = ( - module_target_sat.api.Host().search(query={'search': f'name={client_name}'})[0].id - ) + client_hostname = pre_upgrade_data.get('rhel_client') + rhel_client = ContentHost.get_host_by_hostname(client_hostname) module_target_sat.cli.Host.package_install( - {'host-id': client_id, 'packages': FAKE_0_CUSTOM_PACKAGE_NAME} + {'host-id': rhel_client.nailgun_host.id, 'packages': FAKE_0_CUSTOM_PACKAGE_NAME} ) - # Verifies that package is really installed - rhel_client = Broker(host_class=ContentHost).from_inventory( - filter=f'@inv.hostname == "{client_name}"' - )[0] + # Verify that package is really installed result = rhel_client.execute(f"rpm -q {FAKE_0_CUSTOM_PACKAGE_NAME}") assert FAKE_0_CUSTOM_PACKAGE_NAME in result.stdout diff --git a/tests/upgrades/test_errata.py b/tests/upgrades/test_errata.py index c8d8d229b6b..0f4bc84e2f6 100644 --- a/tests/upgrades/test_errata.py +++ b/tests/upgrades/test_errata.py @@ -17,7 +17,6 @@ :Upstream: No """ import pytest -from broker import Broker from wait_for import wait_for from robottelo import constants @@ -221,9 +220,7 @@ def test_post_scenario_errata_count_installation(self, target_sat, pre_upgrade_d custom_repo_id = pre_upgrade_data.get('custom_repo_id') activation_key = pre_upgrade_data.get('activation_key') organization_id = pre_upgrade_data.get('organization_id') - rhel_client = Broker(host_class=ContentHost).from_inventory( - filter=f'@inv.hostname == "{client_hostname}"' - )[0] + rhel_client = ContentHost.get_host_by_hostname(client_hostname) custom_yum_repo = target_sat.api.Repository(id=custom_repo_id).read() host = target_sat.api.Host().search(query={'search': f'activation_key={activation_key}'})[0] assert host.id == rhel_client.nailgun_host.id, 'Host not found in Satellite' diff --git a/tests/upgrades/test_repository.py b/tests/upgrades/test_repository.py index 4a54bfbcab5..a2f5188669c 100644 --- a/tests/upgrades/test_repository.py +++ b/tests/upgrades/test_repository.py @@ -17,7 +17,6 @@ :Upstream: No """ import pytest -from broker import Broker from robottelo.config import settings from robottelo.constants import FAKE_0_CUSTOM_PACKAGE_NAME @@ -198,9 +197,7 @@ def test_post_scenario_custom_repo_check(self, target_sat, pre_upgrade_data): data={'environment_ids': lce_id} ) - rhel_client = Broker(host_class=ContentHost).from_inventory( - filter=f'@inv.hostname == "{client_hostname}"' - )[0] + rhel_client = ContentHost.get_host_by_hostname(client_hostname) result = rhel_client.execute(f'yum install -y {FAKE_4_CUSTOM_PACKAGE_NAME}') assert result.status == 0 @@ -299,9 +296,7 @@ def test_post_scenario_custom_repo_sca_toggle(self, pre_upgrade_data): org_name = pre_upgrade_data.get('org_name') product_name = pre_upgrade_data.get('product_name') repo_name = pre_upgrade_data.get('repo_name') - rhel_client = Broker(host_class=ContentHost).from_inventory( - filter=f'@inv.hostname == "{client_hostname}"' - )[0] + rhel_client = ContentHost.get_host_by_hostname(client_hostname) result = rhel_client.execute('subscription-manager repo-override --list') assert 'enabled: 1' in result.stdout assert f'{org_name}_{product_name}_{repo_name}' in result.stdout diff --git a/tests/upgrades/test_subscription.py b/tests/upgrades/test_subscription.py index 75a3f49fee5..a8345ce1859 100644 --- a/tests/upgrades/test_subscription.py +++ b/tests/upgrades/test_subscription.py @@ -17,11 +17,11 @@ :Upstream: No """ import pytest -from broker import Broker from manifester import Manifester from robottelo import constants from robottelo.config import settings +from robottelo.hosts import ContentHost class TestManifestScenarioRefresh: @@ -157,9 +157,7 @@ def test_post_subscription_scenario_auto_attach(self, request, target_sat, pre_u 1. Pre-upgrade content host should get subscribed. 2. All the cleanup should be completed successfully. """ - rhel_contenthost = Broker().from_inventory( - filter=f'@inv.hostname == "{pre_upgrade_data.rhel_client}"' - )[0] + rhel_contenthost = ContentHost.get_host_by_hostname(pre_upgrade_data.rhel_client) host = target_sat.api.Host().search(query={'search': f'name={rhel_contenthost.hostname}'})[ 0 ] From b264eb714483dbb0db05b8ffd829a32de0f96211 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 30 Aug 2023 02:26:18 -0400 Subject: [PATCH 175/586] [6.14.z] Fix setting_update fixture to handle None value (#12418) --- pytest_fixtures/component/settings.py | 2 +- tests/foreman/ui/test_http_proxy.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pytest_fixtures/component/settings.py b/pytest_fixtures/component/settings.py index 0a7acbb075a..937379bd6d6 100644 --- a/pytest_fixtures/component/settings.py +++ b/pytest_fixtures/component/settings.py @@ -12,7 +12,7 @@ def setting_update(request, target_sat): key_val = request.param setting, new_value = tuple(key_val.split('=')) if '=' in key_val else (key_val, None) setting_object = target_sat.api.Setting().search(query={'search': f'name={setting}'})[0] - default_setting_value = setting_object.value + default_setting_value = '' if setting_object.value is None else setting_object.value if new_value is not None: setting_object.value = new_value setting_object.update({'value'}) diff --git a/tests/foreman/ui/test_http_proxy.py b/tests/foreman/ui/test_http_proxy.py index 50fb7909ebe..cddc7cd1e42 100644 --- a/tests/foreman/ui/test_http_proxy.py +++ b/tests/foreman/ui/test_http_proxy.py @@ -214,7 +214,6 @@ def test_set_default_http_proxy(module_org, module_location, setting_update, tar :CaseLevel: Acceptance """ - property_name = setting_update.name http_proxy_a = target_sat.api.HTTPProxy( @@ -225,6 +224,8 @@ def test_set_default_http_proxy(module_org, module_location, setting_update, tar ).create() with target_sat.ui_session() as session: + session.organization.select(org_name=module_org.name) + session.location.select(loc_name=module_location.name) session.settings.update( f'name = {property_name}', f'{http_proxy_a.name} ({http_proxy_a.url})' ) From b7d61e4bccb49244ca92179cfab472463447e105 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 30 Aug 2023 09:57:09 -0400 Subject: [PATCH 176/586] [6.14.z] Just check for presence of IP address (#12419) --- tests/foreman/ui/test_computeresource_vmware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/ui/test_computeresource_vmware.py b/tests/foreman/ui/test_computeresource_vmware.py index 38d99f620b0..5502ed6292e 100644 --- a/tests/foreman/ui/test_computeresource_vmware.py +++ b/tests/foreman/ui/test_computeresource_vmware.py @@ -615,7 +615,7 @@ def test_positive_virt_card( assert virt_card['datacenter'] == module_vmware_settings['datacenter'] assert virt_card['cluster'] == module_vmware_settings['cluster'] assert virt_card['memory'] == '2 GB' - assert virt_card['public_ip_address'] != '' + assert 'public_ip_address' in virt_card assert virt_card['mac_address'] == module_vmware_settings['mac_address'] assert virt_card['cpus'] == '1' if 'disk_label' in virt_card: From e4e8bd48b6646b885c3ca51c8fc0a93ef69e2b67 Mon Sep 17 00:00:00 2001 From: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> Date: Wed, 30 Aug 2023 19:42:38 +0530 Subject: [PATCH 177/586] [6.14.z] Modify provisioning test to check root password is set for provisioned VM (#12334) Check root password for provisioned vm --- tests/foreman/api/test_provisioning.py | 27 ++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/tests/foreman/api/test_provisioning.py b/tests/foreman/api/test_provisioning.py index 30fd3587c66..a3e71c95c06 100644 --- a/tests/foreman/api/test_provisioning.py +++ b/tests/foreman/api/test_provisioning.py @@ -18,9 +18,10 @@ """ import pytest from fauxfactory import gen_string -from packaging.version import Version from wait_for import wait_for +from robottelo.config import settings + @pytest.mark.e2e @pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) @@ -64,10 +65,6 @@ def test_rhel_pxe_provisioning( hostgroup=provisioning_hostgroup, organization=module_sca_manifest_org, location=module_location, - content_facet_attributes={ - 'content_view_id': module_provisioning_rhel_content.cv.id, - 'lifecycle_environment_id': module_lce_library.id, - }, name=gen_string('alpha').lower(), mac=host_mac_addr, operatingsystem=module_provisioning_rhel_content.os, @@ -106,11 +103,25 @@ def test_rhel_pxe_provisioning( # Wait for the host to be rebooted and SSH daemon to be started. provisioning_host.wait_for_connection() - # Perform version check + # Perform version check and check if root password is properly updated host_os = host.operatingsystem.read() - expected_rhel_version = Version(f'{host_os.major}.{host_os.minor}') + 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 = module_provisioning_sat.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 ( - provisioning_host.os_version == expected_rhel_version + expected_rhel_version in host_ssh_os.stdout ), 'Different than the expected OS version was installed' # Verify provisioning log exists on host at correct path From 5e35bbb805dcaef8a3cffeb6fdae4a376c34e590 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 30 Aug 2023 14:54:54 -0400 Subject: [PATCH 178/586] [6.14.z] Fix Capsule AC test (#12423) --- tests/foreman/api/test_capsulecontent.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/tests/foreman/api/test_capsulecontent.py b/tests/foreman/api/test_capsulecontent.py index 732aac21655..7a9e576bdeb 100644 --- a/tests/foreman/api/test_capsulecontent.py +++ b/tests/foreman/api/test_capsulecontent.py @@ -956,14 +956,13 @@ def test_positive_sync_container_repo_end_to_end( ) assert result.status == 0 - @pytest.mark.skip_if_open("BZ:2121583") @pytest.mark.tier4 @pytest.mark.skip_if_not_set('capsule') def test_positive_sync_collection_repo( self, + request, target_sat, module_capsule_configured, - rhel7_contenthost, function_product, function_lce_library, ): @@ -1013,22 +1012,11 @@ def test_positive_sync_collection_repo( repo.sync(timeout=600) repo = repo.read() assert repo.content_counts['ansible_collection'] == 2 - module_capsule_configured.wait_for_sync() - # Configure the content host to fetch collections from capsule - rhel7_contenthost.install_katello_ca(module_capsule_configured) - rhel7_contenthost.create_custom_repos( - **{ - 'server': settings.repos.rhel7_os, - 'ansible': settings.repos.ansible_repo, - } - ) - result = rhel7_contenthost.execute('yum -y install ansible') - assert result.status == 0 - repo_path = repo.full_path.replace(target_sat.hostname, module_capsule_configured.hostname) coll_path = './collections' + cfg_path = './ansible.cfg' cfg = ( '[defaults]\n' f'collections_paths = {coll_path}\n\n' @@ -1037,16 +1025,18 @@ def test_positive_sync_collection_repo( '[galaxy_server.capsule_galaxy]\n' f'url={repo_path}\n' ) - rhel7_contenthost.execute(f'echo "{cfg}" > ./ansible.cfg') + + request.addfinalizer(lambda: target_sat.execute(f'rm -rf {cfg_path} {coll_path}')) # Try to install collections from the Capsule - result = rhel7_contenthost.execute( + target_sat.execute(f'echo "{cfg}" > {cfg_path}') + result = target_sat.execute( 'ansible-galaxy collection install theforeman.foreman theforeman.operations' ) assert result.status == 0 assert 'error' not in result.stdout.lower() - result = rhel7_contenthost.execute(f'ls {coll_path}/ansible_collections/theforeman/') + result = target_sat.execute(f'ls {coll_path}/ansible_collections/theforeman/') assert result.status == 0 assert 'foreman' in result.stdout assert 'operations' in result.stdout From e62694bc41ea2fa25214fbddb04abf6ded1da22a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 23:56:48 -0400 Subject: [PATCH 179/586] Bump sphinx from 7.2.4 to 7.2.5 (#12425) --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 13b53f9163d..6a0e2ce2e83 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -5,7 +5,7 @@ redis==5.0.0 pre-commit==3.3.3 # For generating documentation. -sphinx==7.2.3 +sphinx==7.2.5 sphinx-autoapi==2.1.1 # For 'manage' interactive shell From 5cf8329f5d5700b39a611103c0d6b598ac642961 Mon Sep 17 00:00:00 2001 From: David Moore Date: Tue, 29 Aug 2023 14:07:11 -0400 Subject: [PATCH 180/586] testing fixes for _installable_errata Start w/ oudated pkg: applying errata upgrades it but not found in Applicable Search Fix to use template Available Errata, input_value Installability Remove date check, will fail if test is run near 0:00 UTC --- tests/foreman/api/test_reporttemplates.py | 72 ++++++++++++++--------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/tests/foreman/api/test_reporttemplates.py b/tests/foreman/api/test_reporttemplates.py index c3f88b952f7..1838d7c1c81 100644 --- a/tests/foreman/api/test_reporttemplates.py +++ b/tests/foreman/api/test_reporttemplates.py @@ -16,8 +16,6 @@ :Upstream: No """ -from datetime import datetime - import pytest from broker import Broker from fauxfactory import gen_string @@ -410,6 +408,7 @@ def test_positive_applied_errata( result = rhel_contenthost.register(module_org, module_location, activation_key.name, target_sat) assert f'The registered system name is: {rhel_contenthost.hostname}' in result.stdout assert rhel_contenthost.subscribed + assert rhel_contenthost.execute(r'subscription-manager repos --enable \*').status == 0 assert rhel_contenthost.execute(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}').status == 0 assert rhel_contenthost.execute(f'rpm -q {FAKE_1_CUSTOM_PACKAGE}').status == 0 task_id = target_sat.api.JobInvocation().run( @@ -717,18 +716,21 @@ def test_positive_generate_job_report(setup_content, target_sat, rhel7_contentho @pytest.mark.no_containers @pytest.mark.rhel_ver_match(r'^(?!6$)\d+$') def test_positive_installable_errata( - module_org, module_target_sat, module_location, module_cv, module_lce, rhel_contenthost + module_target_sat, module_org, module_lce, module_location, module_cv, rhel_contenthost ): - """Generate an Installable Errata report + """Generate an Installable Errata report using the Report Template - Available Errata, + with the option of 'Installable'. :id: 6263a0fa-5021-4553-939b-84fb71c81d59 :setup: A Host with some applied errata :steps: - 1. Downgrade a package contained within the applied errata - 2. Perform a search for any applicable Erratum - 3. Generate an Installable Errata report + 1. Install an outdated package version + 2. Apply some errata which updates the package + 3. Downgrade the package impacted by the erratum + 4. Perform a search for any Available Errata + 5. Generate an Installable Report from the Available Errata :expectedresults: A report is generated with the installable errata listed @@ -741,10 +743,10 @@ def test_positive_installable_errata( activation_key = module_target_sat.api.ActivationKey( environment=module_lce, organization=module_org ).create() - ERRATUM_ID = str(settings.repos.yum_9.errata[0]) + ERRATUM_ID = str(settings.repos.yum_6.errata[2]) module_target_sat.cli_factory.setup_org_for_a_custom_repo( { - 'url': settings.repos.yum_9.url, + 'url': settings.repos.yum_6.url, 'organization-id': module_org.id, 'content-view-id': module_cv.id, 'lifecycle-environment-id': module_lce.id, @@ -756,8 +758,11 @@ def test_positive_installable_errata( ) assert f'The registered system name is: {rhel_contenthost.hostname}' in result.stdout assert rhel_contenthost.subscribed - result = rhel_contenthost.run(f'yum install -y {FAKE_2_CUSTOM_PACKAGE}') - assert result.status == 0 + + # Install the outdated package version + assert rhel_contenthost.execute(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}').status == 0 + rhel_contenthost.add_rex_key(satellite=module_target_sat) + # Install/Apply the errata task_id = module_target_sat.api.JobInvocation().run( data={ @@ -773,33 +778,44 @@ def test_positive_installable_errata( search_rate=15, max_tries=10, ) - # Downgrade package impacted by the erratum - result = rhel_contenthost.run(f'yum downgrade -y {FAKE_1_CUSTOM_PACKAGE}') - assert result.status == 0 + # Check that applying erratum updated the package + assert ( + rhel_contenthost.execute(f'rpm -q {FAKE_1_CUSTOM_PACKAGE_NAME}').stdout.strip() + == FAKE_2_CUSTOM_PACKAGE + ) + # Downgrade the package + assert rhel_contenthost.execute(f'yum downgrade -y {FAKE_1_CUSTOM_PACKAGE}').status == 0 - _data_for_generate = { + # Data to generate Installable Errata report + _rt_input_data = { 'organization_id': module_org.id, 'report_format': "json", 'input_values': { - 'Filter Errata Type': 'all', - 'Include Last Reboot': 'no', - 'Status': 'all', + 'Installability': 'installable', }, } - search_query = {'search': 'name="Host - Applicable Errata"'} - template = module_target_sat.api.ReportTemplate() - # Allow search to collect results, it may take some time - # Wait until a generated report is populated + + # Gather Errata using the template 'Available Errata', may take some time wait_for( lambda: ( - [] != template.search(query=search_query)[0].read().generate(data=_data_for_generate) + [] + != module_target_sat.api.ReportTemplate() + .search(query={'search': 'name="Host - Available Errata"'})[0] + .read() + .generate(data=_rt_input_data) ), timeout=120, delay=10, ) # Now that a populated report is ready, generate a final time - report = template.search(query=search_query)[0].read().generate(data=_data_for_generate) - assert report != [] - assert datetime.now().strftime("%Y-%m-%d") in report[0]['Available since'] - assert FAKE_1_CUSTOM_PACKAGE_NAME in report[0]['Packages'] - assert report[0]['Erratum'] == ERRATUM_ID + report = ( + module_target_sat.api.ReportTemplate() + .search(query={'search': 'name="Host - Available Errata"'})[0] + .read() + .generate(data=_rt_input_data) + ) + + assert len(report) > 0 + installable_errata = report[0] + assert FAKE_1_CUSTOM_PACKAGE_NAME in installable_errata['Packages'] + assert installable_errata['Erratum'] == ERRATUM_ID From 81ce55325216da5ed89dd320ecb718684099b255 Mon Sep 17 00:00:00 2001 From: David Moore Date: Wed, 30 Aug 2023 15:11:09 -0400 Subject: [PATCH 181/586] Addressing comments --- tests/foreman/api/test_reporttemplates.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/foreman/api/test_reporttemplates.py b/tests/foreman/api/test_reporttemplates.py index 1838d7c1c81..dd978a3fdac 100644 --- a/tests/foreman/api/test_reporttemplates.py +++ b/tests/foreman/api/test_reporttemplates.py @@ -408,7 +408,6 @@ def test_positive_applied_errata( result = rhel_contenthost.register(module_org, module_location, activation_key.name, target_sat) assert f'The registered system name is: {rhel_contenthost.hostname}' in result.stdout assert rhel_contenthost.subscribed - assert rhel_contenthost.execute(r'subscription-manager repos --enable \*').status == 0 assert rhel_contenthost.execute(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}').status == 0 assert rhel_contenthost.execute(f'rpm -q {FAKE_1_CUSTOM_PACKAGE}').status == 0 task_id = target_sat.api.JobInvocation().run( @@ -760,8 +759,8 @@ def test_positive_installable_errata( assert rhel_contenthost.subscribed # Install the outdated package version + rhel_contenthost.execute(r'subscription-manager repos --enable \*') assert rhel_contenthost.execute(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}').status == 0 - rhel_contenthost.add_rex_key(satellite=module_target_sat) # Install/Apply the errata task_id = module_target_sat.api.JobInvocation().run( @@ -798,8 +797,7 @@ def test_positive_installable_errata( # Gather Errata using the template 'Available Errata', may take some time wait_for( lambda: ( - [] - != module_target_sat.api.ReportTemplate() + module_target_sat.api.ReportTemplate() .search(query={'search': 'name="Host - Available Errata"'})[0] .read() .generate(data=_rt_input_data) From cb7618a71558a53fdddb30da8ac4e96a8ef26855 Mon Sep 17 00:00:00 2001 From: David Moore Date: Wed, 30 Aug 2023 16:34:55 -0400 Subject: [PATCH 182/586] Using pulp repos for prt --- tests/foreman/api/test_reporttemplates.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/foreman/api/test_reporttemplates.py b/tests/foreman/api/test_reporttemplates.py index dd978a3fdac..24d57b0ce1d 100644 --- a/tests/foreman/api/test_reporttemplates.py +++ b/tests/foreman/api/test_reporttemplates.py @@ -742,10 +742,12 @@ def test_positive_installable_errata( activation_key = module_target_sat.api.ActivationKey( environment=module_lce, organization=module_org ).create() - ERRATUM_ID = str(settings.repos.yum_6.errata[2]) + # ERRATUM_ID = str(settings.repos.yum_6.errata[2]) + ERRATUM_ID = 'RHEA-2012:0055' module_target_sat.cli_factory.setup_org_for_a_custom_repo( { - 'url': settings.repos.yum_6.url, + # 'url': settings.repos.yum_6.url, + 'url': 'https://fixtures.pulpproject.org/rpm-advisory-diff-repo/', 'organization-id': module_org.id, 'content-view-id': module_cv.id, 'lifecycle-environment-id': module_lce.id, From 8c1456e37840bf56dddb1f4e02190525298d3cdd Mon Sep 17 00:00:00 2001 From: David Moore Date: Thu, 31 Aug 2023 08:55:11 -0400 Subject: [PATCH 183/586] for prt run --- tests/foreman/api/test_reporttemplates.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/foreman/api/test_reporttemplates.py b/tests/foreman/api/test_reporttemplates.py index 24d57b0ce1d..b1f2e8e6a47 100644 --- a/tests/foreman/api/test_reporttemplates.py +++ b/tests/foreman/api/test_reporttemplates.py @@ -742,12 +742,12 @@ def test_positive_installable_errata( activation_key = module_target_sat.api.ActivationKey( environment=module_lce, organization=module_org ).create() - # ERRATUM_ID = str(settings.repos.yum_6.errata[2]) - ERRATUM_ID = 'RHEA-2012:0055' + ERRATUM_ID = str(settings.repos.yum_6.errata[2]) + # ERRATUM_ID = 'RHEA-2012:0055' module_target_sat.cli_factory.setup_org_for_a_custom_repo( { - # 'url': settings.repos.yum_6.url, - 'url': 'https://fixtures.pulpproject.org/rpm-advisory-diff-repo/', + 'url': settings.repos.yum_6.url, + # 'url': 'https://fixtures.pulpproject.org/rpm-advisory-diff-repo/', 'organization-id': module_org.id, 'content-view-id': module_cv.id, 'lifecycle-environment-id': module_lce.id, @@ -760,9 +760,15 @@ def test_positive_installable_errata( assert f'The registered system name is: {rhel_contenthost.hostname}' in result.stdout assert rhel_contenthost.subscribed - # Install the outdated package version rhel_contenthost.execute(r'subscription-manager repos --enable \*') + # Remove package if already installed on this host + rhel_contenthost.execute(f'yum remove -y {FAKE_1_CUSTOM_PACKAGE_NAME}') + # Install the outdated package version assert rhel_contenthost.execute(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}').status == 0 + assert ( + rhel_contenthost.execute(f'rpm -q {FAKE_1_CUSTOM_PACKAGE_NAME}').stdout.strip() + == FAKE_1_CUSTOM_PACKAGE + ) # Install/Apply the errata task_id = module_target_sat.api.JobInvocation().run( @@ -803,18 +809,17 @@ def test_positive_installable_errata( .search(query={'search': 'name="Host - Available Errata"'})[0] .read() .generate(data=_rt_input_data) + != [] ), timeout=120, delay=10, ) - # Now that a populated report is ready, generate a final time report = ( module_target_sat.api.ReportTemplate() .search(query={'search': 'name="Host - Available Errata"'})[0] .read() .generate(data=_rt_input_data) ) - assert len(report) > 0 installable_errata = report[0] assert FAKE_1_CUSTOM_PACKAGE_NAME in installable_errata['Packages'] From cb23ddaea90d79fae8f37c564e95cca3feeef7dc Mon Sep 17 00:00:00 2001 From: David Moore Date: Thu, 31 Aug 2023 09:46:08 -0400 Subject: [PATCH 184/586] Wrap up, remove commented code --- tests/foreman/api/test_reporttemplates.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/foreman/api/test_reporttemplates.py b/tests/foreman/api/test_reporttemplates.py index b1f2e8e6a47..281d3dcf95e 100644 --- a/tests/foreman/api/test_reporttemplates.py +++ b/tests/foreman/api/test_reporttemplates.py @@ -743,11 +743,9 @@ def test_positive_installable_errata( environment=module_lce, organization=module_org ).create() ERRATUM_ID = str(settings.repos.yum_6.errata[2]) - # ERRATUM_ID = 'RHEA-2012:0055' module_target_sat.cli_factory.setup_org_for_a_custom_repo( { 'url': settings.repos.yum_6.url, - # 'url': 'https://fixtures.pulpproject.org/rpm-advisory-diff-repo/', 'organization-id': module_org.id, 'content-view-id': module_cv.id, 'lifecycle-environment-id': module_lce.id, From b2f154830021a72cad261d60759124d09cd2f98c Mon Sep 17 00:00:00 2001 From: David Moore Date: Thu, 31 Aug 2023 14:25:58 -0400 Subject: [PATCH 185/586] CI fixes --- tests/foreman/api/test_reporttemplates.py | 25 ++++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/tests/foreman/api/test_reporttemplates.py b/tests/foreman/api/test_reporttemplates.py index 281d3dcf95e..a696ecf3571 100644 --- a/tests/foreman/api/test_reporttemplates.py +++ b/tests/foreman/api/test_reporttemplates.py @@ -376,7 +376,7 @@ def test_negative_create_report_without_name(): @pytest.mark.rhel_ver_match(r'^(?!6$)\d+$') @pytest.mark.no_containers def test_positive_applied_errata( - module_org, module_location, module_cv, module_lce, rhel_contenthost, target_sat + function_org, function_location, function_lce, rhel_contenthost, target_sat ): """Generate an Applied Errata report @@ -393,21 +393,25 @@ def test_positive_applied_errata( :CaseImportance: Medium """ activation_key = target_sat.api.ActivationKey( - environment=module_lce, organization=module_org + environment=function_lce, organization=function_org ).create() + cv = target_sat.api.ContentView(organization=function_org).create() ERRATUM_ID = str(settings.repos.yum_6.errata[2]) target_sat.cli_factory.setup_org_for_a_custom_repo( { 'url': settings.repos.yum_9.url, - 'organization-id': module_org.id, - 'content-view-id': module_cv.id, - 'lifecycle-environment-id': module_lce.id, + 'organization-id': function_org.id, + 'content-view-id': cv.id, + 'lifecycle-environment-id': function_lce.id, 'activationkey-id': activation_key.id, } ) - result = rhel_contenthost.register(module_org, module_location, activation_key.name, target_sat) + result = rhel_contenthost.register( + function_org, function_location, activation_key.name, target_sat + ) assert f'The registered system name is: {rhel_contenthost.hostname}' in result.stdout assert rhel_contenthost.subscribed + rhel_contenthost.execute(r'subscription-manager repos --enable \*') assert rhel_contenthost.execute(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}').status == 0 assert rhel_contenthost.execute(f'rpm -q {FAKE_1_CUSTOM_PACKAGE}').status == 0 task_id = target_sat.api.JobInvocation().run( @@ -416,7 +420,7 @@ def test_positive_applied_errata( 'inputs': {'errata': ERRATUM_ID}, 'targeting_type': 'static_query', 'search_query': f'name = {rhel_contenthost.hostname}', - 'organization_id': module_org.id, + 'organization_id': function_org.id, }, )['id'] target_sat.wait_for_tasks( @@ -431,7 +435,7 @@ def test_positive_applied_errata( ) res = rt.generate( data={ - 'organization_id': module_org.id, + 'organization_id': function_org.id, 'report_format': 'json', 'input_values': { 'Filter Errata Type': 'all', @@ -758,10 +762,10 @@ def test_positive_installable_errata( assert f'The registered system name is: {rhel_contenthost.hostname}' in result.stdout assert rhel_contenthost.subscribed - rhel_contenthost.execute(r'subscription-manager repos --enable \*') # Remove package if already installed on this host rhel_contenthost.execute(f'yum remove -y {FAKE_1_CUSTOM_PACKAGE_NAME}') # Install the outdated package version + rhel_contenthost.execute(r'subscription-manager repos --enable \*') assert rhel_contenthost.execute(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}').status == 0 assert ( rhel_contenthost.execute(f'rpm -q {FAKE_1_CUSTOM_PACKAGE_NAME}').stdout.strip() @@ -801,6 +805,7 @@ def test_positive_installable_errata( } # Gather Errata using the template 'Available Errata', may take some time + # When condition is met, newest Report Template will have Errata entries wait_for( lambda: ( module_target_sat.api.ReportTemplate() @@ -818,7 +823,7 @@ def test_positive_installable_errata( .read() .generate(data=_rt_input_data) ) - assert len(report) > 0 + assert report installable_errata = report[0] assert FAKE_1_CUSTOM_PACKAGE_NAME in installable_errata['Packages'] assert installable_errata['Erratum'] == ERRATUM_ID From 6393ef299dc1ec98b31694296a86f4e37e5e337c Mon Sep 17 00:00:00 2001 From: David Moore Date: Thu, 31 Aug 2023 18:23:29 -0400 Subject: [PATCH 186/586] target_sat, function fixtures --- tests/foreman/api/test_reporttemplates.py | 29 ++++++++++++----------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/tests/foreman/api/test_reporttemplates.py b/tests/foreman/api/test_reporttemplates.py index a696ecf3571..955d9ae1942 100644 --- a/tests/foreman/api/test_reporttemplates.py +++ b/tests/foreman/api/test_reporttemplates.py @@ -719,7 +719,7 @@ def test_positive_generate_job_report(setup_content, target_sat, rhel7_contentho @pytest.mark.no_containers @pytest.mark.rhel_ver_match(r'^(?!6$)\d+$') def test_positive_installable_errata( - module_target_sat, module_org, module_lce, module_location, module_cv, rhel_contenthost + target_sat, function_org, function_lce, function_location, rhel_contenthost ): """Generate an Installable Errata report using the Report Template - Available Errata, with the option of 'Installable'. @@ -743,21 +743,22 @@ def test_positive_installable_errata( :BZ: 1726504 """ - activation_key = module_target_sat.api.ActivationKey( - environment=module_lce, organization=module_org + activation_key = target_sat.api.ActivationKey( + environment=function_lce, organization=function_org ).create() + custom_cv = target_sat.api.ContentView(organization=function_org).create() ERRATUM_ID = str(settings.repos.yum_6.errata[2]) - module_target_sat.cli_factory.setup_org_for_a_custom_repo( + target_sat.cli_factory.setup_org_for_a_custom_repo( { 'url': settings.repos.yum_6.url, - 'organization-id': module_org.id, - 'content-view-id': module_cv.id, - 'lifecycle-environment-id': module_lce.id, + 'organization-id': function_org.id, + 'content-view-id': custom_cv.id, + 'lifecycle-environment-id': function_lce.id, 'activationkey-id': activation_key.id, } ) result = rhel_contenthost.register( - module_org, module_location, activation_key.name, module_target_sat + function_org, function_location, activation_key.name, target_sat ) assert f'The registered system name is: {rhel_contenthost.hostname}' in result.stdout assert rhel_contenthost.subscribed @@ -773,16 +774,16 @@ def test_positive_installable_errata( ) # Install/Apply the errata - task_id = module_target_sat.api.JobInvocation().run( + task_id = target_sat.api.JobInvocation().run( data={ 'feature': 'katello_errata_install', 'inputs': {'errata': ERRATUM_ID}, 'targeting_type': 'static_query', 'search_query': f'name = {rhel_contenthost.hostname}', - 'organization_id': module_org.id, + 'organization_id': function_org.id, }, )['id'] - module_target_sat.wait_for_tasks( + target_sat.wait_for_tasks( search_query=(f'label = Actions::RemoteExecution::RunHostsJob and id = {task_id}'), search_rate=15, max_tries=10, @@ -797,7 +798,7 @@ def test_positive_installable_errata( # Data to generate Installable Errata report _rt_input_data = { - 'organization_id': module_org.id, + 'organization_id': function_org.id, 'report_format': "json", 'input_values': { 'Installability': 'installable', @@ -808,7 +809,7 @@ def test_positive_installable_errata( # When condition is met, newest Report Template will have Errata entries wait_for( lambda: ( - module_target_sat.api.ReportTemplate() + target_sat.api.ReportTemplate() .search(query={'search': 'name="Host - Available Errata"'})[0] .read() .generate(data=_rt_input_data) @@ -818,7 +819,7 @@ def test_positive_installable_errata( delay=10, ) report = ( - module_target_sat.api.ReportTemplate() + target_sat.api.ReportTemplate() .search(query={'search': 'name="Host - Available Errata"'})[0] .read() .generate(data=_rt_input_data) From 070b82e3ae82c42d8f486a2e317e3934994b374b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:09:48 -0400 Subject: [PATCH 187/586] [6.14.z] Fix Sync Plan tests (#12441) --- tests/foreman/api/test_syncplan.py | 64 +++++++++++++++++------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/tests/foreman/api/test_syncplan.py b/tests/foreman/api/test_syncplan.py index 8719c0629b6..abb8713a642 100644 --- a/tests/foreman/api/test_syncplan.py +++ b/tests/foreman/api/test_syncplan.py @@ -220,7 +220,7 @@ def test_positive_create_with_interval(module_org, interval): @pytest.mark.parametrize('sync_delta', **parametrized(sync_date_deltas)) @pytest.mark.tier1 -def test_positive_create_with_sync_date(module_org, sync_delta): +def test_positive_create_with_sync_date(module_org, sync_delta, target_sat): """Create a sync plan and update its sync date. :id: bdb6e0a9-0d3b-4811-83e2-2140b7bb62e3 @@ -232,11 +232,11 @@ def test_positive_create_with_sync_date(module_org, sync_delta): :CaseImportance: Critical """ sync_date = datetime.now() + timedelta(seconds=sync_delta) - sync_plan = entities.SyncPlan( + sync_plan = target_sat.api.SyncPlan( enabled=False, organization=module_org, sync_date=sync_date ).create() sync_plan = sync_plan.read() - assert sync_date.strftime('%Y-%m-%d %H:%M:%S UTC') == sync_plan.sync_date + assert sync_date.strftime('%Y-%m-%d %H:%M:%S +0000') == sync_plan.sync_date @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) @@ -419,7 +419,7 @@ def test_positive_update_interval_custom_cron(module_org, interval): @pytest.mark.parametrize('sync_delta', **parametrized(sync_date_deltas)) @pytest.mark.tier1 -def test_positive_update_sync_date(module_org, sync_delta): +def test_positive_update_sync_date(module_org, sync_delta, target_sat): """Updated sync plan's sync date. :id: fad472c7-01b4-453b-ae33-0845c9e0dfd4 @@ -431,13 +431,13 @@ def test_positive_update_sync_date(module_org, sync_delta): :CaseImportance: Critical """ sync_date = datetime.now() + timedelta(seconds=sync_delta) - sync_plan = entities.SyncPlan( + sync_plan = target_sat.api.SyncPlan( enabled=False, organization=module_org, sync_date=datetime.now() + timedelta(days=10) ).create() sync_plan.sync_date = sync_date sync_plan.update(['sync_date']) sync_plan = sync_plan.read() - assert sync_date.strftime('%Y-%m-%d %H:%M:%S UTC') == sync_plan.sync_date + assert sync_date.strftime('%Y-%m-%d %H:%M:%S +0000') == sync_plan.sync_date @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) @@ -481,7 +481,7 @@ def test_negative_update_interval(module_org, interval): @pytest.mark.tier2 -def test_positive_add_product(module_org): +def test_positive_add_product(module_org, target_sat): """Create a sync plan and add one product to it. :id: 036dea02-f73d-4fc1-9c41-5515b6659c79 @@ -493,8 +493,9 @@ def test_positive_add_product(module_org): :CaseImportance: Critical """ - sync_plan = entities.SyncPlan(enabled=False, organization=module_org).create() - product = entities.Product(organization=module_org).create() + sync_plan = target_sat.api.SyncPlan(enabled=False, organization=module_org).create() + product = target_sat.api.Product(organization=module_org).create() + target_sat.api.Repository(product=product).create() sync_plan.add_products(data={'product_ids': [product.id]}) sync_plan = sync_plan.read() assert len(sync_plan.product) == 1 @@ -502,7 +503,7 @@ def test_positive_add_product(module_org): @pytest.mark.tier2 -def test_positive_add_products(module_org): +def test_positive_add_products(module_org, target_sat): """Create a sync plan and add two products to it. :id: 2a80ecad-2245-46d8-bbc6-0b802e68d50c @@ -512,8 +513,9 @@ def test_positive_add_products(module_org): :CaseLevel: Integration """ - sync_plan = entities.SyncPlan(enabled=False, organization=module_org).create() - products = [entities.Product(organization=module_org).create() for _ in range(2)] + sync_plan = target_sat.api.SyncPlan(enabled=False, organization=module_org).create() + products = [target_sat.api.Product(organization=module_org).create() for _ in range(2)] + [target_sat.api.Repository(product=product).create() for product in products] sync_plan.add_products(data={'product_ids': [product.id for product in products]}) sync_plan = sync_plan.read() assert len(sync_plan.product) == 2 @@ -521,7 +523,7 @@ def test_positive_add_products(module_org): @pytest.mark.tier2 -def test_positive_remove_product(module_org): +def test_positive_remove_product(module_org, target_sat): """Create a sync plan with two products and then remove one product from it. @@ -534,8 +536,9 @@ def test_positive_remove_product(module_org): :BZ: 1199150 """ - sync_plan = entities.SyncPlan(enabled=False, organization=module_org).create() - products = [entities.Product(organization=module_org).create() for _ in range(2)] + sync_plan = target_sat.api.SyncPlan(enabled=False, organization=module_org).create() + products = [target_sat.api.Product(organization=module_org).create() for _ in range(2)] + [target_sat.api.Repository(product=product).create() for product in products] sync_plan.add_products(data={'product_ids': [product.id for product in products]}) assert len(sync_plan.read().product) == 2 sync_plan.remove_products(data={'product_ids': [products[0].id]}) @@ -546,7 +549,7 @@ def test_positive_remove_product(module_org): @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_remove_products(module_org): +def test_positive_remove_products(module_org, target_sat): """Create a sync plan with two products and then remove both products from it. @@ -557,8 +560,9 @@ def test_positive_remove_products(module_org): :CaseLevel: Integration """ - sync_plan = entities.SyncPlan(enabled=False, organization=module_org).create() - products = [entities.Product(organization=module_org).create() for _ in range(2)] + sync_plan = target_sat.api.SyncPlan(enabled=False, organization=module_org).create() + products = [target_sat.api.Product(organization=module_org).create() for _ in range(2)] + [target_sat.api.Repository(product=product).create() for product in products] sync_plan.add_products(data={'product_ids': [product.id for product in products]}) assert len(sync_plan.read().product) == 2 sync_plan.remove_products(data={'product_ids': [product.id for product in products]}) @@ -578,9 +582,10 @@ def test_positive_repeatedly_add_remove(module_org, request, target_sat): :BZ: 1199150 """ - sync_plan = entities.SyncPlan(organization=module_org).create() + sync_plan = target_sat.api.SyncPlan(organization=module_org).create() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) - product = entities.Product(organization=module_org).create() + product = target_sat.api.Product(organization=module_org).create() + target_sat.api.Repository(product=product).create() for _ in range(5): sync_plan.add_products(data={'product_ids': [product.id]}) assert len(sync_plan.read().product) == 1 @@ -602,11 +607,12 @@ def test_positive_add_remove_products_custom_cron(module_org, request, target_sa """ cron_expression = gen_choice(valid_cron_expressions()) - sync_plan = entities.SyncPlan( + sync_plan = target_sat.api.SyncPlan( organization=module_org, interval='custom cron', cron_expression=cron_expression ).create() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) - products = [entities.Product(organization=module_org).create() for _ in range(2)] + products = [target_sat.api.Product(organization=module_org).create() for _ in range(2)] + [target_sat.api.Repository(product=product).create() for product in products] sync_plan.add_products(data={'product_ids': [product.id for product in products]}) assert len(sync_plan.read().product) == 2 sync_plan.remove_products(data={'product_ids': [product.id for product in products]}) @@ -1020,7 +1026,7 @@ def test_positive_synchronize_custom_product_weekly_recurrence(module_org, reque @pytest.mark.tier2 -def test_positive_delete_one_product(module_org): +def test_positive_delete_one_product(module_org, target_sat): """Create a sync plan with one product and delete it. :id: e565c464-33e2-4bca-8eca-15d5a7d4b155 @@ -1030,8 +1036,9 @@ def test_positive_delete_one_product(module_org): :CaseLevel: Integration """ - sync_plan = entities.SyncPlan(organization=module_org).create() - product = entities.Product(organization=module_org).create() + sync_plan = target_sat.api.SyncPlan(organization=module_org).create() + product = target_sat.api.Product(organization=module_org).create() + target_sat.api.Repository(product=product).create() sync_plan.add_products(data={'product_ids': [product.id]}) sync_plan.delete() with pytest.raises(HTTPError): @@ -1039,7 +1046,7 @@ def test_positive_delete_one_product(module_org): @pytest.mark.tier2 -def test_positive_delete_products(module_org): +def test_positive_delete_products(module_org, target_sat): """Create a sync plan with two products and delete them. :id: f21bd57f-369e-4acd-a492-5532349a3804 @@ -1049,8 +1056,9 @@ def test_positive_delete_products(module_org): :CaseLevel: Integration """ - sync_plan = entities.SyncPlan(organization=module_org).create() - products = [entities.Product(organization=module_org).create() for _ in range(2)] + sync_plan = target_sat.api.SyncPlan(organization=module_org).create() + products = [target_sat.api.Product(organization=module_org).create() for _ in range(2)] + [target_sat.api.Repository(product=product).create() for product in products] sync_plan.add_products(data={'product_ids': [product.id for product in products]}) sync_plan.delete() with pytest.raises(HTTPError): From 83715ba13993b71312270a08e353d49f1bbf344b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 4 Sep 2023 02:22:46 -0400 Subject: [PATCH 188/586] [6.14.z] Fix capsule sync status persists (#12443) Fix capsule sync status persists (#12216) (cherry picked from commit a4afc362710a5bc0ad5f4f6fdd50ce5850eff3f3) Co-authored-by: vsedmik <46570670+vsedmik@users.noreply.github.com> --- tests/foreman/api/test_capsulecontent.py | 31 ++++++++++-------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/tests/foreman/api/test_capsulecontent.py b/tests/foreman/api/test_capsulecontent.py index 7a9e576bdeb..5c390007bb5 100644 --- a/tests/foreman/api/test_capsulecontent.py +++ b/tests/foreman/api/test_capsulecontent.py @@ -1214,7 +1214,7 @@ def test_positive_capsule_sync_status_persists( 4. Publish CV 5. Promote to lifecycle env 6. Sync Capsule - 7. Delete the task using foreman-rake console + 7. Delete all sync tasks using foreman-rake console 8. Verify the status of capsule is still synced :bz: 1956985 @@ -1237,35 +1237,30 @@ def test_positive_capsule_sync_status_persists( cv = cv.read() cvv = cv.version[-1].read() + timestamp = datetime.utcnow() cvv.promote(data={'environment_ids': function_lce.id}) - cvv = cvv.read() - - timestamp = (datetime.utcnow()).strftime('%Y-%m-%d %H:%M') module_capsule_configured.wait_for_sync() - search_result = target_sat.wait_for_tasks( - search_query='label = Actions::Katello::CapsuleContent::Sync' - f' and organization_id = {function_org.id}' - f' and started_at >= "{timestamp}"', - search_rate=15, - max_tries=5, - ) - # Delete the task using UUID (search_result[0].id) + # Delete all capsule sync tasks so that we fall back for audits. task_result = target_sat.execute( - f"""echo "ForemanTasks::Task.find( - '{search_result[0].id}').destroy!" | foreman-rake console""" + """echo "ForemanTasks::Task.where(action:'Synchronize capsule """ + f"""\\'{module_capsule_configured.hostname}\\'').delete_all" | foreman-rake console""" ) assert task_result.status == 0 - # Ensure task record was deleted. + + # Ensure task records were deleted. task_result = target_sat.execute( - f"""echo "ForemanTasks::Task.find('{search_result[0].id}')" | foreman-rake console""" + """echo "ForemanTasks::Task.where(action:'Synchronize capsule """ + f"""\\'{module_capsule_configured.hostname}\\'')" | foreman-rake console""" ) assert task_result.status == 0 - assert 'RecordNotFound' in task_result.stdout + assert '[]' in task_result.stdout # Check sync status again, and ensure last_sync_time is still correct sync_status = module_capsule_configured.nailgun_capsule.content_get_sync() - assert sync_status['last_sync_time'] >= timestamp + assert ( + datetime.strptime(sync_status['last_sync_time'], '%Y-%m-%d %H:%M:%S UTC') >= timestamp + ) @pytest.mark.tier4 @pytest.mark.skip_if_not_set('capsule') From ced0c7151d7df9385eb3c410ffa867a038d08114 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 4 Sep 2023 03:53:37 -0400 Subject: [PATCH 189/586] [6.14.z] Bump pre-commit from 3.3.3 to 3.4.0 (#12452) Bump pre-commit from 3.3.3 to 3.4.0 (#12449) (cherry picked from commit 5f35f42e74108ef5b03062176e5d5c83338490e0) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 6a0e2ce2e83..a461862b7d5 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -2,7 +2,7 @@ flake8==6.1.0 pytest-cov==4.1.0 redis==5.0.0 -pre-commit==3.3.3 +pre-commit==3.4.0 # For generating documentation. sphinx==7.2.5 From 55c44c431639d4f4a443d4c0b495cc11fe70d403 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 4 Sep 2023 03:54:55 -0400 Subject: [PATCH 190/586] [6.14.z] Bump pytest from 7.4.0 to 7.4.1 (#12456) Bump pytest from 7.4.0 to 7.4.1 (#12448) (cherry picked from commit 62cf09070791051c2a6ba74916ff35e44d5fefce) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index effc5cddadc..98427c90bc8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ navmazing==1.1.6 productmd==1.36 pyotp==2.9.0 python-box==7.1.1 -pytest==7.4.0 +pytest==7.4.1 pytest-services==2.2.1 pytest-mock==3.11.1 pytest-reportportal==5.2.1 From 6d7ff564d50613400c4437be80115afc8f990ea4 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 4 Sep 2023 08:11:04 -0400 Subject: [PATCH 191/586] [6.14.z] Bump deepdiff from 6.3.1 to 6.4.1 (#12462) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 98427c90bc8..646daa56c1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ betelgeuse==1.10.0 broker[docker]==0.3.3 cryptography==41.0.3 -deepdiff==6.3.1 +deepdiff==6.4.1 dynaconf[vault]==3.2.2 fauxfactory==3.1.0 jinja2==3.1.2 From 14fd0fe9a5142a83c3662d70301ecaf8b17d7f84 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 5 Sep 2023 05:17:11 -0400 Subject: [PATCH 192/586] [6.14.z] Fix CLI SyncPlan tests (#12482) Fix CLI SyncPlan tests (#12473) - fix for several occurrences missed during helpers refactor - force explicit time format (cherry picked from commit 9d1c912680fa08ee77967fbdc47a65b81c356047) Co-authored-by: vsedmik <46570670+vsedmik@users.noreply.github.com> --- tests/foreman/cli/test_syncplan.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/foreman/cli/test_syncplan.py b/tests/foreman/cli/test_syncplan.py index d0ddf18d081..488c2637e2b 100644 --- a/tests/foreman/cli/test_syncplan.py +++ b/tests/foreman/cli/test_syncplan.py @@ -42,7 +42,7 @@ from robottelo.utils.datafactory import parametrized from robottelo.utils.datafactory import valid_data_list -SYNC_DATE_FMT = '%Y-%m-%d %H:%M:%S' +SYNC_DATE_FMT = '%Y-%m-%d %H:%M:%S UTC' @filtered_datapoint @@ -437,7 +437,7 @@ def test_positive_synchronize_custom_product_past_sync_date(module_org, request, sleep(delay / 4) # Verify product has not been synced yet with pytest.raises(AssertionError): - validate_task_status(repo['id'], module_org.id, max_tries=1) + validate_task_status(target_sat, repo['id'], module_org.id, max_tries=1) validate_repo_content(repo, ['errata', 'packages'], after_sync=False) # Wait until the first recurrence logger.info( @@ -446,7 +446,7 @@ def test_positive_synchronize_custom_product_past_sync_date(module_org, request, ) sleep(delay * 3 / 4) # Verify product was synced successfully - validate_task_status(repo['id'], module_org.id) + validate_task_status(target_sat, repo['id'], module_org.id) validate_repo_content(repo, ['errata', 'package-groups', 'packages']) @@ -552,7 +552,7 @@ def test_positive_synchronize_custom_products_future_sync_date(module_org, reque ) for repo in repos: with pytest.raises(AssertionError): - validate_task_status(repo['id'], module_org.id, max_tries=1) + validate_task_status(target_sat, repo['id'], module_org.id, max_tries=1) # Associate sync plan with products for product in products: Product.set_sync_plan({'id': product['id'], 'sync-plan-id': new_sync_plan['id']}) @@ -565,7 +565,7 @@ def test_positive_synchronize_custom_products_future_sync_date(module_org, reque # Verify products have not been synced yet for repo in repos: with pytest.raises(AssertionError): - validate_task_status(repo['id'], module_org.id, max_tries=1) + validate_task_status(target_sat, repo['id'], module_org.id, max_tries=1) # Wait the rest of expected time logger.info( f"Waiting {(delay * 4 / 5)} seconds to check product {products[0]['name']}" @@ -574,7 +574,7 @@ def test_positive_synchronize_custom_products_future_sync_date(module_org, reque sleep(delay * 4 / 5) # Verify products were synced successfully for repo in repos: - validate_task_status(repo['id'], module_org.id) + validate_task_status(target_sat, repo['id'], module_org.id) validate_repo_content(repo, ['errata', 'package-groups', 'packages']) @@ -634,7 +634,7 @@ def test_positive_synchronize_rh_product_past_sync_date( sleep(delay / 4) # Verify product has not been synced yet with pytest.raises(AssertionError): - validate_task_status(repo['id'], org.id, max_tries=1) + validate_task_status(target_sat, repo['id'], org.id, max_tries=1) validate_repo_content(repo, ['errata', 'packages'], after_sync=False) # Wait the rest of expected time logger.info( @@ -643,7 +643,7 @@ def test_positive_synchronize_rh_product_past_sync_date( ) sleep(delay * 3 / 4) # Verify product was synced successfully - validate_task_status(repo['id'], org.id) + validate_task_status(target_sat, repo['id'], org.id) validate_repo_content(repo, ['errata', 'packages']) @@ -698,7 +698,7 @@ def test_positive_synchronize_rh_product_future_sync_date( request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) # Verify product is not synced and doesn't have any content with pytest.raises(AssertionError): - validate_task_status(repo['id'], org.id, max_tries=1) + validate_task_status(target_sat, repo['id'], org.id, max_tries=1) validate_repo_content(repo, ['errata', 'packages'], after_sync=False) # Associate sync plan with product Product.set_sync_plan({'id': product['id'], 'sync-plan-id': new_sync_plan['id']}) @@ -710,7 +710,7 @@ def test_positive_synchronize_rh_product_future_sync_date( sleep(delay / 5) # Verify product has not been synced yet with pytest.raises(AssertionError): - validate_task_status(repo['id'], org.id, max_tries=1) + validate_task_status(target_sat, repo['id'], org.id, max_tries=1) validate_repo_content(repo, ['errata', 'packages'], after_sync=False) # Wait the rest of expected time logger.info( @@ -719,7 +719,7 @@ def test_positive_synchronize_rh_product_future_sync_date( ) sleep(delay * 4 / 5) # Verify product was synced successfully - validate_task_status(repo['id'], org.id) + validate_task_status(target_sat, repo['id'], org.id) validate_repo_content(repo, ['errata', 'packages']) @@ -811,7 +811,7 @@ def test_positive_synchronize_custom_product_weekly_recurrence(module_org, reque sleep(delay / 4) # Verify product has not been synced yet with pytest.raises(AssertionError): - validate_task_status(repo['id'], module_org.id, max_tries=1) + validate_task_status(target_sat, repo['id'], module_org.id, max_tries=1) validate_repo_content(repo, ['errata', 'packages'], after_sync=False) # Wait until the first recurrence logger.info( @@ -820,5 +820,5 @@ def test_positive_synchronize_custom_product_weekly_recurrence(module_org, reque ) sleep(delay * 3 / 4) # Verify product was synced successfully - validate_task_status(repo['id'], module_org.id) + validate_task_status(target_sat, repo['id'], module_org.id) validate_repo_content(repo, ['errata', 'package-groups', 'packages']) From 0169bc56f981a9d302be10dcdf348c41153ed348 Mon Sep 17 00:00:00 2001 From: Jitendra Yejare Date: Tue, 5 Sep 2023 17:01:59 +0530 Subject: [PATCH 193/586] [6.14.z] Bump actions/checkout from 3 to 4 (#12484) Bump actions/checkout from 3 to 4 (#12475) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto_cherry_pick.yml | 2 +- .github/workflows/dispatch_release.yml | 2 +- .github/workflows/pull_request.yml | 2 +- .github/workflows/update_robottelo_image.yml | 2 +- .github/workflows/weekly.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/auto_cherry_pick.yml b/.github/workflows/auto_cherry_pick.yml index 3ef661b252f..1a1f814a736 100644 --- a/.github/workflows/auto_cherry_pick.yml +++ b/.github/workflows/auto_cherry_pick.yml @@ -40,7 +40,7 @@ jobs: steps: ## Robottelo Repo Checkout - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 if: ${{ startsWith(matrix.label, '6.') && matrix.label != github.base_ref }} with: fetch-depth: 0 diff --git a/.github/workflows/dispatch_release.yml b/.github/workflows/dispatch_release.yml index dc57c0bc877..51b6b90919a 100644 --- a/.github/workflows/dispatch_release.yml +++ b/.github/workflows/dispatch_release.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Git User setup run: "git config --local user.email Satellite-QE.satqe.com && git config --local user.name Satellite-QE" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index cc58fb9177a..582725d6e85 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -18,7 +18,7 @@ jobs: python-version: ['3.10', '3.11'] steps: - name: Checkout Robottelo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set Up Python-${{ matrix.python-version }} uses: actions/setup-python@v4 diff --git a/.github/workflows/update_robottelo_image.yml b/.github/workflows/update_robottelo_image.yml index 2b2e4654bdc..945f76b15dc 100644 --- a/.github/workflows/update_robottelo_image.yml +++ b/.github/workflows/update_robottelo_image.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Get image tag id: image_tag diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index e19f5b02a4d..dc60df544b3 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -17,7 +17,7 @@ jobs: python-version: [3.9] steps: - name: Checkout Robottelo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set Up Python-${{ matrix.python-version }} uses: actions/setup-python@v4 From 71964cc2b071fe031b2ae984f908ddf08ecabfde Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:37:55 -0400 Subject: [PATCH 194/586] [6.14.z] content with longer name (#12499) --- .../foreman/cli/test_container_management.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/foreman/cli/test_container_management.py b/tests/foreman/cli/test_container_management.py index 3574b764165..e3c2f419f3c 100644 --- a/tests/foreman/cli/test_container_management.py +++ b/tests/foreman/cli/test_container_management.py @@ -322,3 +322,79 @@ def test_positive_container_admin_end_to_end_pull( # 9. Pull in docker image result = container_contenthost.execute(docker_pull_command) assert result.status == 0 + + def test_negative_pull_content_with_longer_name( + self, target_sat, container_contenthost, module_org + ): + """Verify that long name CV publishes when CV & docker repo both have a larger name. + + :id: e0ac0be4-f5ff-4a88-bb29-33aa2d874f46 + + :steps: + + 1. Create Product, docker repo, CV and LCE with a long name + 2. Sync the repos + 3. Add repository to CV, Publish, and then Promote CV to LCE + 4. Pull in docker image + + :expectedresults: + + 1. Long Product, repository, CV and LCE should create successfully + 2. Sync repository successfully + 3. Publish & Promote should success + 4. Can pull in docker images + + :BZ: 2127470 + + :customerscenario: true + """ + pattern_postfix = gen_string('alpha', 10).lower() + + product_name = f'containers-{pattern_postfix}' + repo_name = f'repo-{pattern_postfix}' + lce_name = f'lce-{pattern_postfix}' + cv_name = f'cv-{pattern_postfix}' + + # 1. Create Product, docker repo, CV and LCE with a long name + product = target_sat.cli_factory.make_product_wait( + {'name': product_name, 'organization-id': module_org.id} + ) + + repo = _repo(product['id'], name=repo_name, upstream_name=CONTAINER_UPSTREAM_NAME) + + # 2. Sync the repos + target_sat.cli.Repository.synchronize({'id': repo['id']}) + + lce = target_sat.cli_factory.make_lifecycle_environment( + {'name': lce_name, 'organization-id': module_org.id} + ) + cv = target_sat.cli_factory.make_content_view( + {'name': cv_name, 'composite': False, 'organization-id': module_org.id} + ) + + # 3. Add repository to CV, Publish, and then Promote CV to LCE + target_sat.cli.ContentView.add_repository({'id': cv['id'], 'repository-id': repo['id']}) + + target_sat.cli.ContentView.publish({'id': cv['id']}) + cv = target_sat.cli.ContentView.info({'id': cv['id']}) + target_sat.cli.ContentView.version_promote( + {'id': cv['versions'][0]['id'], 'to-lifecycle-environment-id': lce['id']} + ) + + podman_pull_command = ( + f"podman pull --tls-verify=false {target_sat.hostname}/{module_org.label.lower()}" + f"-{lce['label'].lower()}-{cv['label'].lower()}-{product['label'].lower()}-{repo_name}" + ) + + # 4. Pull in docker image + assert ( + container_contenthost.execute( + f'podman login -u {settings.server.admin_username}' + f' -p {settings.server.admin_password} {target_sat.hostname}' + ).status + == 0 + ) + + assert container_contenthost.execute(podman_pull_command).status == 0 + + assert container_contenthost.execute(f'podman logout {target_sat.hostname}').status == 0 From e9ad5fb243f091866707cc125a2a021c31778248 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 5 Sep 2023 17:01:40 -0400 Subject: [PATCH 195/586] [6.14.z] Log end of test phases and broker host setup and teardown (#12493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Log end of test phases and broker host setup and teardown (#12472) * Log broker host setup and teardown * Log result of each pytest node phase at its end (cherry picked from commit 209f6511f8a56ac6937af608c07aa5febcba4a18) Co-authored-by: Ondřej Gajdušek --- pytest_plugins/logging_hooks.py | 6 ++++-- robottelo/hosts.py | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pytest_plugins/logging_hooks.py b/pytest_plugins/logging_hooks.py index f7bcaf43620..1cf24d89ee1 100644 --- a/pytest_plugins/logging_hooks.py +++ b/pytest_plugins/logging_hooks.py @@ -73,5 +73,7 @@ def pytest_runtest_logstart(nodeid, location): logger.info(f'Started Test: {nodeid}') -def pytest_runtest_logfinish(nodeid, location): - logger.info(f'Finished Test: {nodeid}') +def pytest_runtest_logreport(report): + """Process the TestReport produced for each of the setup, + call and teardown runtest phases of an item.""" + logger.info('Finished %s for test: %s, result: %s', report.when, report.nodeid, report.outcome) diff --git a/robottelo/hosts.py b/robottelo/hosts.py index c14636267e6..5d78a2b1906 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -372,15 +372,21 @@ def clean_cached_properties(self): del self.__dict__[name] def setup(self): + logger.debug('START: setting up host %s', self) if not self.blank: self.remove_katello_ca() + logger.debug('END: setting up host %s', self) + def teardown(self): + logger.debug('START: tearing down host %s', self) if not self.blank and not getattr(self, '_skip_context_checkin', False): self.unregister() if type(self) is not Satellite and self.nailgun_host: self.nailgun_host.delete() + logger.debug('END: tearing down host %s', self) + def power_control(self, state=VmState.RUNNING, ensure=True): """Lookup the host workflow for power on and execute From 5b7dc338edb835cf9eb13817f4e0fa51c1b91b50 Mon Sep 17 00:00:00 2001 From: Jameer Pathan Date: Fri, 1 Sep 2023 16:02:05 +0200 Subject: [PATCH 196/586] Fix foreman-maintain tests failing because of automation issues --- pytest_fixtures/component/maintain.py | 31 ++-- robottelo/constants/__init__.py | 1 + robottelo/host_helpers/contenthost_mixins.py | 9 +- tests/foreman/maintain/test_advanced.py | 145 ++++++------------ tests/foreman/maintain/test_health.py | 8 +- .../foreman/maintain/test_maintenance_mode.py | 2 - tests/foreman/maintain/test_service.py | 13 +- tests/foreman/maintain/test_upgrade.py | 15 +- 8 files changed, 91 insertions(+), 133 deletions(-) diff --git a/pytest_fixtures/component/maintain.py b/pytest_fixtures/component/maintain.py index ff8e8387c4b..6cac4f6f8c8 100644 --- a/pytest_fixtures/component/maintain.py +++ b/pytest_fixtures/component/maintain.py @@ -8,6 +8,8 @@ from robottelo.constants import SATELLITE_MAINTAIN_YML from robottelo.hosts import Capsule from robottelo.hosts import Satellite +from robottelo.hosts import SatelliteHostError +from robottelo.logging import logger synced_repos = pytest.StashKey[dict] @@ -21,16 +23,27 @@ def module_stash(request): yield request.node.stash -@pytest.fixture(scope='session') -def sat_maintain(request, session_target_sat, session_capsule_configured): +@pytest.fixture(scope='module') +def sat_maintain(request, module_target_sat, module_capsule_configured): if settings.remotedb.server: yield Satellite(settings.remotedb.server) else: - session_target_sat.register_to_cdn(pool_ids=settings.subscription.fm_rhn_poolid.split()) - hosts = {'satellite': session_target_sat, 'capsule': session_capsule_configured} + module_target_sat.register_to_cdn(pool_ids=settings.subscription.fm_rhn_poolid.split()) + hosts = {'satellite': module_target_sat, 'capsule': module_capsule_configured} yield hosts[request.param] +@pytest.fixture +def start_satellite_services(sat_maintain): + """Teardown for satellite-maintain tests to ensure that all Satellite services are started""" + yield + logger.info('Ensuring that all %s services are running', sat_maintain.__class__.__name__) + result = sat_maintain.cli.Service.start() + if result.status != 0: + logger.error('Unable to start all %s services', sat_maintain.__class__.__name__) + raise SatelliteHostError('Failed to start Satellite services') + + @pytest.fixture def setup_backup_tests(request, sat_maintain): """Teardown for backup/restore tests""" @@ -42,9 +55,7 @@ def _finalize(): @pytest.fixture(scope='module') -def module_synced_repos( - sat_maintain, session_capsule_configured, module_sca_manifest, module_stash -): +def module_synced_repos(sat_maintain, module_capsule_configured, module_sca_manifest, module_stash): if not module_stash[synced_repos]: org = sat_maintain.satellite.api.Organization().create() sat_maintain.satellite.upload_manifest(org.id, module_sca_manifest.content) @@ -80,13 +91,13 @@ def module_synced_repos( lce = sat_maintain.satellite.api.LifecycleEnvironment( organization=module_stash[synced_repos]['org'] ).search(query={'search': f'name={constants.ENVIRONMENT}'})[0] - session_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( + module_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( data={'environment_id': lce.id} ) - result = session_capsule_configured.nailgun_capsule.content_lifecycle_environments() + result = module_capsule_configured.nailgun_capsule.content_lifecycle_environments() assert lce.id in [capsule_lce['id'] for capsule_lce in result['results']] # sync the Capsule - sync_status = session_capsule_configured.nailgun_capsule.content_sync() + sync_status = module_capsule_configured.nailgun_capsule.content_sync() assert sync_status['result'] == 'success' yield { diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index 20aaa27bc91..cfff118ca4b 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -18,6 +18,7 @@ class Colored(Box): # This should be updated after each version branch SATELLITE_VERSION = "6.14" SATELLITE_OS_VERSION = "8" +SAT_NON_GA_VERSIONS = ['6.14', '6.15'] # Default system ports HTTPS_PORT = '443' diff --git a/robottelo/host_helpers/contenthost_mixins.py b/robottelo/host_helpers/contenthost_mixins.py index 30fbf576e1f..72fbc585b7c 100644 --- a/robottelo/host_helpers/contenthost_mixins.py +++ b/robottelo/host_helpers/contenthost_mixins.py @@ -65,7 +65,7 @@ def OSCAP(self): 'cbrhel': constants.OSCAP_PROFILE[f'cbrhel{self._v_major}'], } - def _dogfood_helper(self, product, release, snap, repo=None): + def _dogfood_helper(self, product, release, repo=None): """Function to return repository related attributes based on the input and the host object """ @@ -89,18 +89,17 @@ def _dogfood_helper(self, product, release, snap, repo=None): 'or the version of the Satellite object. ' f'settings: {settings_release}, parameter: {release}' ) - snap = str(snap or settings.server.version.get("snap")) - return product, release, snap, v_major, repo + return product, release, v_major, repo def download_repofile(self, product=None, release=None, snap=''): """Downloads the tools/client, capsule, or satellite repos on the machine""" - product, release, snap, v_major, _ = self._dogfood_helper(product, release, snap) + product, release, v_major, _ = self._dogfood_helper(product, release) url = dogfood_repofile_url(settings.ohsnap, product, release, v_major, snap) self.execute(f'curl -o /etc/yum.repos.d/dogfood.repo -L {url}') def dogfood_repository(self, repo=None, product=None, release=None, snap=''): """Returns a repository definition based on the arguments provided""" - product, release, snap, v_major, repo = self._dogfood_helper(product, release, snap, repo) + product, release, v_major, repo = self._dogfood_helper(product, release, repo) return dogfood_repository(settings.ohsnap, repo, product, release, v_major, snap, self.arch) def enable_tools_repo(self, organization_id): diff --git a/tests/foreman/maintain/test_advanced.py b/tests/foreman/maintain/test_advanced.py index 49faf4e4730..eaf8ee304ec 100644 --- a/tests/foreman/maintain/test_advanced.py +++ b/tests/foreman/maintain/test_advanced.py @@ -22,67 +22,26 @@ from robottelo.config import robottelo_tmp_dir from robottelo.config import settings from robottelo.constants import MAINTAIN_HAMMER_YML +from robottelo.constants import SAT_NON_GA_VERSIONS +from robottelo.hosts import get_sat_rhel_version +from robottelo.hosts import get_sat_version -pytestmark = pytest.mark.destructive - - -# Common repositories for Satellite and Capsule -common_repos = ['rhel-8-for-x86_64-baseos-rpms', 'rhel-8-for-x86_64-appstream-rpms'] - -# Satellite repositories -sat_611_repos = [ - 'satellite-6.11-for-rhel-8-x86_64-rpms', - 'satellite-maintenance-6.11-for-rhel-8-x86_64-rpms', -] + common_repos - -sat_612_repos = [ - 'satellite-6.12-for-rhel-8-x86_64-rpms', - 'satellite-maintenance-6.12-for-rhel-8-x86_64-rpms', -] + common_repos - -sat_613_repos = [ - 'satellite-6.13-for-rhel-8-x86_64-rpms', - 'satellite-maintenance-6.13-for-rhel-8-x86_64-rpms', -] + common_repos - -sat_614_repos = [ - 'satellite-6.14-for-rhel-8-x86_64-rpms', - 'satellite-maintenance-6.14-for-rhel-8-x86_64-rpms', -] + common_repos - -# Capsule repositories -cap_611_repos = [ - 'satellite-capsule-6.11-for-rhel-8-x86_64-rpms', - 'satellite-maintenance-6.11-for-rhel-8-x86_64-rpms', -] + common_repos - -cap_612_repos = [ - 'satellite-capsule-6.12-for-rhel-8-x86_64-rpms', - 'satellite-maintenance-6.12-for-rhel-8-x86_64-rpms', -] + common_repos - -cap_613_repos = [ - 'satellite-capsule-6.13-for-rhel-8-x86_64-rpms', - 'satellite-maintenance-6.13-for-rhel-8-x86_64-rpms', -] + common_repos - -cap_614_repos = [ - 'satellite-capsule-6.14-for-rhel-8-x86_64-rpms', - 'satellite-maintenance-6.14-for-rhel-8-x86_64-rpms', -] + common_repos - -sat_repos = { - '6.11': sat_611_repos, - '6.12': sat_612_repos, - '6.13': sat_613_repos, - '6.14': sat_614_repos, -} -cap_repos = { - '6.11': cap_611_repos, - '6.12': cap_612_repos, - '6.13': cap_613_repos, - '6.14': cap_614_repos, -} + +sat_x_y_release = f'{get_sat_version().major}.{get_sat_version().minor}' + + +def get_satellite_capsule_repos( + x_y_release=sat_x_y_release, product='satellite', os_major_ver=get_sat_rhel_version().major +): + if product == 'capsule': + product = 'satellite-capsule' + repos = [ + f'{product}-{x_y_release}-for-rhel-{os_major_ver}-x86_64-rpms', + f'satellite-maintenance-{x_y_release}-for-rhel-{os_major_ver}-x86_64-rpms', + f'rhel-{os_major_ver}-for-x86_64-baseos-rpms', + f'rhel-{os_major_ver}-for-x86_64-appstream-rpms', + ] + return repos def test_positive_advanced_run_service_restart(sat_maintain): @@ -299,7 +258,7 @@ def test_positive_sync_plan_with_hammer_defaults(request, sat_maintain, module_o :customerscenario: true """ - sat_maintain.cli.Defaults.add({'param-name': 'organization_id', 'param-value': 1}) + sat_maintain.cli.Defaults.add({'param-name': 'organization_id', 'param-value': module_org.id}) sync_plans = [] for name in ['plan1', 'plan2']: @@ -321,6 +280,11 @@ def test_positive_sync_plan_with_hammer_defaults(request, sat_maintain, module_o def _finalize(): sat_maintain.cli.Defaults.delete({'param-name': 'organization_id'}) sync_plans[1].delete() + sync_plan = sat_maintain.api.SyncPlan(organization=module_org.id).search( + query={'search': f'name="{sync_plans[0]}"'} + ) + if sync_plan: + sync_plans[0].delete() @pytest.mark.e2e @@ -336,21 +300,21 @@ def test_positive_satellite_repositories_setup(sat_maintain): :expectedresults: Required Satellite repositories for install/upgrade should get enabled """ - supported_versions = ['6.11', '6.12', '6.13'] - for ver in supported_versions: - result = sat_maintain.cli.Advanced.run_repositories_setup(options={'version': ver}) + sat_version = ".".join(sat_maintain.version.split('.')[0:2]) + result = sat_maintain.cli.Advanced.run_repositories_setup(options={'version': sat_version}) + if sat_version not in SAT_NON_GA_VERSIONS: assert result.status == 0 assert 'FAIL' not in result.stdout result = sat_maintain.execute('yum repolist') - for repo in sat_repos[ver]: + for repo in get_satellite_capsule_repos(sat_version): assert repo in result.stdout - # 6.14 till not GA - result = sat_maintain.cli.Advanced.run_repositories_setup(options={'version': '6.14'}) - assert result.status == 1 - assert 'FAIL' in result.stdout - for repo in sat_repos['6.14']: - assert repo in result.stdout + # for non-ga versions + else: + assert result.status == 1 + assert 'FAIL' in result.stdout + for repo in get_satellite_capsule_repos(sat_version): + assert repo in result.stdout @pytest.mark.e2e @@ -369,36 +333,17 @@ def test_positive_capsule_repositories_setup(sat_maintain): :expectedresults: Required Capsule repositories should get enabled """ - supported_versions = ['6.11', '6.12'] - for ver in supported_versions: - result = sat_maintain.cli.Advanced.run_repositories_setup(options={'version': ver}) + sat_version = ".".join(sat_maintain.version.split('.')[0:2]) + result = sat_maintain.cli.Advanced.run_repositories_setup(options={'version': sat_version}) + if sat_version not in SAT_NON_GA_VERSIONS: assert result.status == 0 assert 'FAIL' not in result.stdout result = sat_maintain.execute('yum repolist') - for repo in cap_repos[ver]: + for repo in get_satellite_capsule_repos(sat_version, 'capsule'): + assert repo in result.stdout + # for non-ga versions + else: + assert result.status == 1 + assert 'FAIL' in result.stdout + for repo in get_satellite_capsule_repos(sat_version, 'capsule'): assert repo in result.stdout - - # 6.13 till not GA - result = sat_maintain.cli.Advanced.run_repositories_setup(options={'version': '6.13'}) - assert result.status == 1 - assert 'FAIL' in result.stdout - for repo in cap_repos['6.13']: - assert repo in result.stdout - - # Verify that all required beta repositories gets enabled - # maintain beta repo is unavailable for EL8 https://bugzilla.redhat.com/show_bug.cgi?id=2106750 - cap_beta_repo = common_repos - missing_beta_el8_repos = [ - 'satellite-capsule-6-beta-for-rhel-8-x86_64-rpms', - 'satellite-maintenance-6-beta-for-rhel-8-x86_64-rpms', - ] - result = sat_maintain.cli.Advanced.run_repositories_setup( - options={'version': '6.12'}, env_var='FOREMAN_MAINTAIN_USE_BETA=1' - ) - assert result.status != 0 - assert 'FAIL' in result.stdout - for repo in missing_beta_el8_repos: - assert f"Error: '{repo}' does not match a valid repository ID" in result.stdout - result = sat_maintain.execute('yum repolist') - for repo in cap_beta_repo: - assert repo in result.stdout diff --git a/tests/foreman/maintain/test_health.py b/tests/foreman/maintain/test_health.py index ae3d45435c1..bb4711a17bd 100644 --- a/tests/foreman/maintain/test_health.py +++ b/tests/foreman/maintain/test_health.py @@ -165,7 +165,8 @@ def test_positive_health_check_server_ping(sat_maintain): assert 'FAIL' not in result.stdout -def test_health_check_server_ping(sat_maintain, request): +@pytest.mark.usefixtures('start_satellite_services') +def test_health_check_server_ping(sat_maintain): """Verify health check server-ping :id: ecdc5bfb-2adf-49f6-948d-995dae34bcd3 @@ -185,10 +186,6 @@ def test_health_check_server_ping(sat_maintain, request): assert result.status == 0 assert 'FAIL' in result.stdout - @request.addfinalizer - def _finalize(): - assert sat_maintain.cli.Service.start().status == 0 - @pytest.mark.include_capsule def test_negative_health_check_upstream_repository(sat_maintain, request): @@ -225,7 +222,6 @@ def _finalize(): sat_maintain.execute('dnf clean all') -@pytest.mark.include_capsule def test_positive_health_check_available_space(sat_maintain): """Verify available-space check diff --git a/tests/foreman/maintain/test_maintenance_mode.py b/tests/foreman/maintain/test_maintenance_mode.py index 8f24e62281a..f72ac4468d0 100644 --- a/tests/foreman/maintain/test_maintenance_mode.py +++ b/tests/foreman/maintain/test_maintenance_mode.py @@ -21,8 +21,6 @@ from robottelo.config import robottelo_tmp_dir -pytestmark = pytest.mark.destructive - @pytest.mark.e2e @pytest.mark.tier2 diff --git a/tests/foreman/maintain/test_service.py b/tests/foreman/maintain/test_service.py index 510c0d0a017..ee0122af3c6 100644 --- a/tests/foreman/maintain/test_service.py +++ b/tests/foreman/maintain/test_service.py @@ -25,8 +25,6 @@ from robottelo.constants import SATELLITE_ANSWER_FILE from robottelo.hosts import Satellite -pytestmark = pytest.mark.destructive - SATELLITE_SERVICES = [ "dynflow-sidekiq@.service", "foreman-proxy.service", @@ -94,6 +92,7 @@ def test_positive_service_list(sat_maintain): @pytest.mark.include_capsule +@pytest.mark.usefixtures('start_satellite_services') def test_positive_service_stop_start(sat_maintain): """Start/Stop services using satellite-maintain service subcommand @@ -124,6 +123,7 @@ def test_positive_service_stop_start(sat_maintain): @pytest.mark.include_capsule +@pytest.mark.usefixtures('start_satellite_services') def test_positive_service_stop_restart(sat_maintain): """Disable services using satellite-maintain service @@ -156,6 +156,7 @@ def test_positive_service_stop_restart(sat_maintain): @pytest.mark.include_capsule +@pytest.mark.usefixtures('start_satellite_services') def test_positive_service_enable_disable(sat_maintain): """Enable/Disable services using satellite-maintain service subcommand @@ -180,7 +181,8 @@ def test_positive_service_enable_disable(sat_maintain): assert result.status == 0 -def test_positive_foreman_service(request, sat_maintain): +@pytest.mark.usefixtures('start_satellite_services') +def test_positive_foreman_service(sat_maintain): """Validate httpd service should work as expected even stopping of the foreman service :id: 08a29ea2-2e49-11eb-a22b-d46d6dd3b5b2 @@ -201,10 +203,7 @@ def test_positive_foreman_service(request, sat_maintain): result = sat_maintain.cli.Health.check(options={'assumeyes': True}) assert result.status == 0 assert 'foreman' in result.stdout - - @request.addfinalizer - def _finalize(): - assert sat_maintain.cli.Service.start(options={'only': 'foreman'}).status == 0 + assert sat_maintain.cli.Service.start(options={'only': 'foreman'}).status == 0 @pytest.mark.include_capsule diff --git a/tests/foreman/maintain/test_upgrade.py b/tests/foreman/maintain/test_upgrade.py index ed378096913..4caae7ceb59 100644 --- a/tests/foreman/maintain/test_upgrade.py +++ b/tests/foreman/maintain/test_upgrade.py @@ -19,8 +19,7 @@ import pytest from robottelo.config import settings - -pytestmark = pytest.mark.destructive +from robottelo.constants import SATELLITE_VERSION def last_y_stream_version(release): @@ -66,6 +65,10 @@ def test_positive_satellite_maintain_upgrade_list(sat_maintain): @pytest.mark.include_capsule +@pytest.mark.skipif( + (settings.server.version.release == 'stream'), + reason='Upgrade path is not available for stream yet', +) def test_positive_repositories_validate(sat_maintain): """Test repositories-validate pre-upgrade check is skipped when system is subscribed using custom activationkey. @@ -106,6 +109,10 @@ def test_positive_repositories_validate(sat_maintain): ids=['default', 'medium'], indirect=True, ) +@pytest.mark.skipif( + (settings.server.version.release == 'stream'), + reason='Upgrade path is not available for stream yet', +) def test_negative_pre_upgrade_tuning_profile_check(request, custom_host): """Negative test that verifies a satellite with less than tuning profile hardware requirements fails on pre-upgrade check. @@ -124,7 +131,9 @@ def test_negative_pre_upgrade_tuning_profile_check(request, custom_host): # Register to CDN for RHEL8 repos, download and enable last y stream's ohsnap repos, # and enable the satellite module and install it on the host custom_host.register_to_cdn() - last_y_stream = last_y_stream_version(settings.server.version.release) + last_y_stream = last_y_stream_version( + SATELLITE_VERSION if sat_version == 'stream' else sat_version + ) custom_host.download_repofile(product='satellite', release=last_y_stream) custom_host.execute('dnf -y module enable satellite:el8 && dnf -y install satellite') # Install without system checks to get around installer checks From 53c352ccc4e426d4711e7a7b7037a38cc751d897 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 6 Sep 2023 11:57:24 -0400 Subject: [PATCH 197/586] [6.14.z] Add health check test for verify_dhcp_config_syntax (#12510) Add health check test for verify_dhcp_config_syntax (#12446) (cherry picked from commit 7dd8c61cf8bfdb4a3c8db5986b3969bda137fd2a) Co-authored-by: Griffin Sullivan <48397354+Griffin-Sullivan@users.noreply.github.com> --- tests/foreman/maintain/test_health.py | 50 +++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/tests/foreman/maintain/test_health.py b/tests/foreman/maintain/test_health.py index bb4711a17bd..129de58724b 100644 --- a/tests/foreman/maintain/test_health.py +++ b/tests/foreman/maintain/test_health.py @@ -22,6 +22,7 @@ from fauxfactory import gen_string from robottelo.config import settings +from robottelo.utils.installer import InstallerCommand upstream_url = { @@ -544,8 +545,7 @@ def test_positive_health_check_env_proxy(sat_maintain): assert 'FAIL' not in result.stdout -@pytest.mark.stubbed -def test_positive_health_check_foreman_proxy_verify_dhcp_config_syntax(): +def test_positive_health_check_foreman_proxy_verify_dhcp_config_syntax(sat_maintain): """Verify foreman-proxy-verify-dhcp-config-syntax :id: 43ca5cc7-9888-490d-b1ba-f3298e737039 @@ -569,8 +569,52 @@ def test_positive_health_check_foreman_proxy_verify_dhcp_config_syntax(): :CaseImportance: Medium - :CaseAutomation: NotAutomated + :CaseAutomation: Automated """ + # Set dhcp.yml to `:use_provider: dhcp_isc` + sat_maintain.execute( + r"sed -i '/:use_provider: dhcp_infoblox/c\:use_provider: dhcp_isc'" + " /etc/foreman-proxy/settings.d/dhcp.yml" + ) + result = sat_maintain.execute("cat /etc/foreman-proxy/settings.d/dhcp.yml") + assert ':use_provider: dhcp_isc' in result.stdout + # Run health list and check and verify nothing comes back + result = sat_maintain.cli.Health.list() + assert 'foreman-proxy-verify-dhcp-config-syntax' not in result.stdout + result = sat_maintain.cli.Health.check( + options={'label': 'foreman-proxy-verify-dhcp-config-syntax'} + ) + assert ( + 'No scenario matching label' and 'foreman-proxy-verify-dhcp-config-syntax' in result.stdout + ) + # Enable DHCP + installer = sat_maintain.install( + InstallerCommand('enable-foreman-proxy-plugin-dhcp-remote-isc', 'foreman-proxy-dhcp true') + ) + assert 'Success!' in installer.stdout + # Run health list and check and verify check is made + result = sat_maintain.cli.Health.list() + assert 'foreman-proxy-verify-dhcp-config-syntax' in result.stdout + result = sat_maintain.cli.Health.check( + options={'label': 'foreman-proxy-verify-dhcp-config-syntax'} + ) + assert 'OK' in result.stdout + # Set dhcp.yml `:use_provider: dhcp_infoblox` + sat_maintain.execute( + r"sed -i '/:use_provider: dhcp_isc/c\:use_provider: dhcp_infoblox'" + " /etc/foreman-proxy/settings.d/dhcp.yml" + ) + result = sat_maintain.execute("cat /etc/foreman-proxy/settings.d/dhcp.yml") + assert ':use_provider: dhcp_infoblox' in result.stdout + # Run health list and check and verify nothing comes back + result = sat_maintain.cli.Health.list() + assert 'foreman-proxy-verify-dhcp-config-syntax' not in result.stdout + result = sat_maintain.cli.Health.check( + options={'label': 'foreman-proxy-verify-dhcp-config-syntax'} + ) + assert ( + 'No scenario matching label' and 'foreman-proxy-verify-dhcp-config-syntax' in result.stdout + ) def test_positive_remove_job_file(sat_maintain): From f889cabd323d37e7eb7747edb2d8630d544fc26a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 6 Sep 2023 14:02:38 -0400 Subject: [PATCH 198/586] [6.14.z] Expect exception instead of false return value (#12516) --- tests/foreman/ui/test_organization.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/foreman/ui/test_organization.py b/tests/foreman/ui/test_organization.py index 278c26cda11..bd57330b3b1 100644 --- a/tests/foreman/ui/test_organization.py +++ b/tests/foreman/ui/test_organization.py @@ -116,7 +116,12 @@ def test_positive_end_to_end(session): ) assert session.organization.search(new_name) org_values = session.organization.read(new_name, widget_names=widget_list) - assert not session.organization.delete(new_name) + with pytest.raises(AssertionError) as context: + assert not session.organization.delete(new_name) + assert ( + 'The current organization cannot be deleted. Please switch to a ' + 'different organization before deleting.' in str(context.value) + ) assert user.login in org_values['users']['resources']['assigned'] assert media.name in org_values['media']['resources']['assigned'] assert template.name in org_values['provisioning_templates']['resources']['assigned'] From 6fabc9ee81b9432bb5251a3841289f158e575526 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Sep 2023 04:37:25 -0400 Subject: [PATCH 199/586] [6.14.z] Refactor tests/foreman/ui/test_rhcloud_inventory.py (#12514) Refactor tests/foreman/ui/test_rhcloud_inventory.py (#12511) (cherry picked from commit 87c1c92575a009cb2a5862aa1bd423e68c28169c) Co-authored-by: Jameer Pathan --- tests/foreman/api/test_rhcloud_inventory.py | 28 -- tests/foreman/ui/test_rhcloud_inventory.py | 341 ++++---------------- 2 files changed, 58 insertions(+), 311 deletions(-) diff --git a/tests/foreman/api/test_rhcloud_inventory.py b/tests/foreman/api/test_rhcloud_inventory.py index 57af28af261..178797abb4a 100644 --- a/tests/foreman/api/test_rhcloud_inventory.py +++ b/tests/foreman/api/test_rhcloud_inventory.py @@ -157,34 +157,6 @@ def test_rhcloud_inventory_api_hosts_synchronization( # To Do: Add support in Nailgun to get Insights and Inventory host properties. -@pytest.mark.stubbed -def test_rhcloud_inventory_mtu_field(): - """Verify that the hosts having mtu field value as string in foreman's Nic object - is present in the inventory report. - - :id: df6d5f4f-5ee1-4f34-bf24-b93fbd089322 - - :customerscenario: true - - :Steps: - 1. Register a content host. - 2. If value of mtu field is not a string then use foreman-rake to change it. - 3. Generate inventory report. - 4. Assert that host is listed in the inventory report. - 5. Assert that value of mtu field in generated report is a number. - - :CaseImportance: Medium - - :expectedresults: - 1. Host having string mtu field value is present in the inventory report. - 2. Value of mtu field in generated inventory report is a number. - - :BZ: 1893439 - - :CaseAutomation: ManualOnly - """ - - @pytest.mark.run_in_one_thread @pytest.mark.tier2 def test_system_purpose_sla_field( diff --git a/tests/foreman/ui/test_rhcloud_inventory.py b/tests/foreman/ui/test_rhcloud_inventory.py index 32ad7f9d030..2da1e461141 100644 --- a/tests/foreman/ui/test_rhcloud_inventory.py +++ b/tests/foreman/ui/test_rhcloud_inventory.py @@ -78,7 +78,7 @@ def test_rhcloud_inventory_e2e( 5. JSON files inside report can be parsed 6. metadata.json lists all and only slice JSON files in tar 7. Host counts in metadata matches host counts in slices - 8. Assert Hostnames, IP addresses, and installed packages are present in report. + 8. Assert Hostnames, IP addresses, and installed packages are present in the report. :CaseImportance: Critical @@ -91,6 +91,7 @@ def test_rhcloud_inventory_e2e( session.location.select(loc_name=DEFAULT_LOC) timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime('%Y-%m-%d %H:%M') session.cloudinventory.generate_report(org.name) + # wait_for_tasks report generation task to finish. wait_for( lambda: module_target_sat.api.ForemanTask() .search( @@ -108,12 +109,15 @@ def test_rhcloud_inventory_e2e( ) report_path = session.cloudinventory.download_report(org.name) inventory_data = session.cloudinventory.read(org.name) - + # Verify that generated archive is valid. common_assertion(report_path, inventory_data, org, module_target_sat) + # Get report data for assertion json_data = get_report_data(report_path) + # Verify that hostnames are present in the report. hostnames = [host['fqdn'] for host in json_data['hosts']] assert virtual_host.hostname in hostnames assert baremetal_host.hostname in hostnames + # Verify that ip_addresses are present report. ip_addresses = [ host['system_profile']['network_interfaces'][0]['ipv4_addresses'][0] for host in json_data['hosts'] @@ -123,6 +127,7 @@ def test_rhcloud_inventory_e2e( assert baremetal_host.ip_addr in ip_addresses assert virtual_host.ip_addr in ipv4_addresses assert baremetal_host.ip_addr in ipv4_addresses + # Verify that packages are included in report all_host_profiles = [host['system_profile'] for host in json_data['hosts']] for host_profiles in all_host_profiles: assert 'installed_packages' in host_profiles @@ -131,29 +136,45 @@ def test_rhcloud_inventory_e2e( @pytest.mark.run_in_one_thread @pytest.mark.tier3 -def test_obfuscate_host_names( +def test_rh_cloud_inventory_settings( module_target_sat, inventory_settings, rhcloud_manifest_org, rhcloud_registered_hosts, ): - """Test whether `Obfuscate host names` setting works as expected. + """Test whether `Obfuscate host names`, `Obfuscate host ipv4 addresses` + and `Exclude Packages` setting works as expected. :id: 3c3a36b6-6566-446b-b803-3f8f9aab2511 + :customerscenario: true + :Steps: 1. Prepare machine and upload its data to Insights. 2. Go to Configure > Inventory upload > enable “Obfuscate host names” setting. - 3. Generate report after enabling the setting. - 4. Check if host names are obfuscated in generated reports. - 5. Disable previous setting. - 6. Go to Administer > Settings > RH Cloud and enable "Obfuscate host names" setting. - 7. Generate report after enabling the setting. - 8. Check if host names are obfuscated in generated reports. + 3. Go to Configure > Inventory upload > enable “Obfuscate host ipv4 addresses” setting. + 4. Go to Configure > Inventory upload > enable “Exclude Packages” setting. + 5. Generate report after enabling the settings. + 6. Check if host names are obfuscated in generated reports. + 7. Check if hosts ipv4 addresses are obfuscated in generated reports. + 8. Check if packages are excluded from generated reports. + 9. Disable previous setting. + 10. Go to Administer > Settings > RH Cloud and enable "Obfuscate host names" setting. + 11. Go to Administer > Settings > RH Cloud and enable "Obfuscate IPs" setting. + 12. Go to Administer > Settings > RH Cloud and enable + "Don't upload installed packages" setting. + 13. Generate report after enabling the setting. + 14. Check if host names are obfuscated in generated reports. + 15. Check if hosts ipv4 addresses are obfuscated in generated reports. + 16. Check if packages are excluded from generated reports. :expectedresults: 1. Obfuscated host names in reports generated. + 2. Obfuscated host ipv4 addresses in generated reports. + 3. Packages are excluded from reports generated. + + :BZ: 1852594, 1889690, 1852594 :CaseAutomation: Automated """ @@ -162,8 +183,10 @@ def test_obfuscate_host_names( with module_target_sat.ui_session() as session: session.organization.select(org_name=org.name) session.location.select(loc_name=DEFAULT_LOC) - # Enable obfuscate_hostnames setting on inventory page. + # Enable settings on inventory page. session.cloudinventory.update({'obfuscate_hostnames': True}) + session.cloudinventory.update({'obfuscate_ips': True}) + session.cloudinventory.update({'exclude_packages': True}) timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime('%Y-%m-%d %H:%M') session.cloudinventory.generate_report(org.name) # wait_for_tasks report generation task to finish. @@ -184,125 +207,19 @@ def test_obfuscate_host_names( ) report_path = session.cloudinventory.download_report(org.name) inventory_data = session.cloudinventory.read(org.name) - - # Assert that obfuscate_hostnames is enabled. + # Verify settings are enabled. assert inventory_data['obfuscate_hostnames'] is True - # Assert that generated archive is valid. + assert inventory_data['obfuscate_ips'] is True + assert inventory_data['exclude_packages'] is True + # Verify that generated archive is valid. common_assertion(report_path, inventory_data, org, module_target_sat) # Get report data for assertion json_data = get_report_data(report_path) + # Verify that hostnames are obfuscated from the report. hostnames = [host['fqdn'] for host in json_data['hosts']] assert virtual_host.hostname not in hostnames assert baremetal_host.hostname not in hostnames - # Assert that host ip_addresses are present in the report. - ipv4_addresses = [host['ip_addresses'][0] for host in json_data['hosts']] - assert virtual_host.ip_addr in ipv4_addresses - assert baremetal_host.ip_addr in ipv4_addresses - # Disable obfuscate_hostnames setting on inventory page. - session.cloudinventory.update({'obfuscate_hostnames': False}) - - # Enable obfuscate_hostnames setting. - module_target_sat.update_setting('obfuscate_inventory_hostnames', True) - timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime('%Y-%m-%d %H:%M') - session.cloudinventory.generate_report(org.name) - # wait_for_tasks report generation task to finish. - wait_for( - lambda: module_target_sat.api.ForemanTask() - .search( - query={ - 'search': f'label = ForemanInventoryUpload::Async::GenerateReportJob ' - f'and started_at >= "{timestamp}"' - } - )[0] - .result - == 'success', - timeout=400, - delay=15, - silent_failure=True, - handle_exception=True, - ) - report_path = session.cloudinventory.download_report(org.name) - inventory_data = session.cloudinventory.read(org.name) - - assert inventory_data['obfuscate_hostnames'] is True - json_data = get_report_data(report_path) - hostnames = [host['fqdn'] for host in json_data['hosts']] - assert virtual_host.hostname not in hostnames - assert baremetal_host.hostname not in hostnames - ipv4_addresses = [host['ip_addresses'][0] for host in json_data['hosts']] - assert virtual_host.ip_addr in ipv4_addresses - assert baremetal_host.ip_addr in ipv4_addresses - - -@pytest.mark.run_in_one_thread -@pytest.mark.tier3 -def test_obfuscate_host_ipv4_addresses( - module_target_sat, - inventory_settings, - rhcloud_manifest_org, - rhcloud_registered_hosts, -): - """Test whether `Obfuscate host ipv4 addresses` setting works as expected. - - :id: c0fc4ee9-a6a1-42c0-83f0-0f131ca9ab41 - - :customerscenario: true - - :Steps: - - 1. Prepare machine and upload its data to Insights. - 2. Go to Configure > Inventory upload > enable “Obfuscate host ipv4 addresses” setting. - 3. Generate report after enabling the setting. - 4. Check if hosts ipv4 addresses are obfuscated in generated reports. - 5. Disable previous setting. - 6. Go to Administer > Settings > RH Cloud and enable "Obfuscate IPs" setting. - 7. Generate report after enabling the setting. - 8. Check if hosts ipv4 addresses are obfuscated in generated reports. - - :expectedresults: - 1. Obfuscated host ipv4 addresses in generated reports. - - :BZ: 1852594, 1889690 - - :CaseAutomation: Automated - """ - org = rhcloud_manifest_org - virtual_host, baremetal_host = rhcloud_registered_hosts - with module_target_sat.ui_session() as session: - session.organization.select(org_name=org.name) - session.location.select(loc_name=DEFAULT_LOC) - # Enable obfuscate_ips setting on inventory page. - session.cloudinventory.update({'obfuscate_ips': True}) - timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime('%Y-%m-%d %H:%M') - session.cloudinventory.generate_report(org.name) - # wait_for_tasks report generation task to finish. - wait_for( - lambda: module_target_sat.api.ForemanTask() - .search( - query={ - 'search': f'label = ForemanInventoryUpload::Async::GenerateReportJob ' - f'and started_at >= "{timestamp}"' - } - )[0] - .result - == 'success', - timeout=400, - delay=15, - silent_failure=True, - handle_exception=True, - ) - report_path = session.cloudinventory.download_report(org.name) - inventory_data = session.cloudinventory.read(org.name) - # Assert that obfuscate_ips is enabled. - assert inventory_data['obfuscate_ips'] is True - # Assert that generated archive is valid. - common_assertion(report_path, inventory_data, org, module_target_sat) - # Get report data for assertion - json_data = get_report_data(report_path) - hostnames = [host['fqdn'] for host in json_data['hosts']] - assert virtual_host.hostname in hostnames - assert baremetal_host.hostname in hostnames - # Assert that ip_addresses are obfuscated from report. + # Verify that ip_addresses are obfuscated from the report. ip_addresses = [ host['system_profile']['network_interfaces'][0]['ipv4_addresses'][0] for host in json_data['hosts'] @@ -312,11 +229,18 @@ def test_obfuscate_host_ipv4_addresses( assert baremetal_host.ip_addr not in ip_addresses assert virtual_host.ip_addr not in ipv4_addresses assert baremetal_host.ip_addr not in ipv4_addresses - # Disable obfuscate_ips setting on inventory page. + # Verify that packages are excluded from report + all_host_profiles = [host['system_profile'] for host in json_data['hosts']] + for host_profiles in all_host_profiles: + assert 'installed_packages' not in host_profiles + # Disable settings on inventory page. + session.cloudinventory.update({'obfuscate_hostnames': False}) session.cloudinventory.update({'obfuscate_ips': False}) - - # Enable obfuscate_inventory_ips setting. + session.cloudinventory.update({'exclude_packages': False}) + # Enable settings, the one on the main settings page. + module_target_sat.update_setting('obfuscate_inventory_hostnames', True) module_target_sat.update_setting('obfuscate_inventory_ips', True) + module_target_sat.update_setting('exclude_installed_packages', True) timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime('%Y-%m-%d %H:%M') session.cloudinventory.generate_report(org.name) # wait_for_tasks report generation task to finish. @@ -337,13 +261,17 @@ def test_obfuscate_host_ipv4_addresses( ) report_path = session.cloudinventory.download_report(org.name) inventory_data = session.cloudinventory.read(org.name) - + # Verify settings are enabled. + assert inventory_data['obfuscate_hostnames'] is True assert inventory_data['obfuscate_ips'] is True + assert inventory_data['exclude_packages'] is True # Get report data for assertion json_data = get_report_data(report_path) + # Verify that hostnames are obfuscated from the report. hostnames = [host['fqdn'] for host in json_data['hosts']] - assert virtual_host.hostname in hostnames - assert baremetal_host.hostname in hostnames + assert virtual_host.hostname not in hostnames + assert baremetal_host.hostname not in hostnames + # Verify that ip_addresses are obfuscated from the report. ip_addresses = [ host['system_profile']['network_interfaces'][0]['ipv4_addresses'][0] for host in json_data['hosts'] @@ -353,109 +281,7 @@ def test_obfuscate_host_ipv4_addresses( assert baremetal_host.ip_addr not in ip_addresses assert virtual_host.ip_addr not in ipv4_addresses assert baremetal_host.ip_addr not in ipv4_addresses - - -@pytest.mark.run_in_one_thread -@pytest.mark.tier3 -def test_exclude_packages_setting( - module_target_sat, - inventory_settings, - rhcloud_manifest_org, - rhcloud_registered_hosts, -): - """Test whether `Exclude Packages` setting works as expected. - - :id: 646093fa-fdd6-4f70-82aa-725e31fa3f12 - - :customerscenario: true - - :Steps: - - 1. Prepare machine and upload its data to Insights - 2. Go to Configure > Inventory upload > enable “Exclude Packages” setting. - 3. Generate report after enabling the setting. - 4. Check if packages are excluded from generated reports. - 5. Disable previous setting. - 6. Go to Administer > Settings > RH Cloud and enable - "Don't upload installed packages" setting. - 7. Generate report after enabling the setting. - 8. Check if packages are excluded from generated reports. - - :expectedresults: - 1. Packages are excluded from reports generated. - - :BZ: 1852594 - - :CaseAutomation: Automated - """ - org = rhcloud_manifest_org - virtual_host, baremetal_host = rhcloud_registered_hosts - with module_target_sat.ui_session() as session: - session.organization.select(org_name=org.name) - session.location.select(loc_name=DEFAULT_LOC) - # Enable exclude_packages setting on inventory page. - session.cloudinventory.update({'exclude_packages': True}) - timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime('%Y-%m-%d %H:%M') - session.cloudinventory.generate_report(org.name) - wait_for( - lambda: module_target_sat.api.ForemanTask() - .search( - query={ - 'search': f'label = ForemanInventoryUpload::Async::GenerateReportJob ' - f'and started_at >= "{timestamp}"' - } - )[0] - .result - == 'success', - timeout=400, - delay=15, - silent_failure=True, - handle_exception=True, - ) - report_path = session.cloudinventory.download_report(org.name) - inventory_data = session.cloudinventory.read(org.name) - assert inventory_data['exclude_packages'] is True - # Disable exclude_packages setting on inventory page. - session.cloudinventory.update({'exclude_packages': False}) - # Assert that generated archive is valid. - common_assertion(report_path, inventory_data, org, module_target_sat) - # Get report data for assertion - json_data = get_report_data(report_path) - # Assert that right hosts are present in report. - hostnames = [host['fqdn'] for host in json_data['hosts']] - assert virtual_host.hostname in hostnames - assert baremetal_host.hostname in hostnames - # Assert that packages are excluded from report - all_host_profiles = [host['system_profile'] for host in json_data['hosts']] - for host_profiles in all_host_profiles: - assert 'installed_packages' not in host_profiles - - # Enable exclude_installed_packages setting. - module_target_sat.update_setting('exclude_installed_packages', True) - timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime('%Y-%m-%d %H:%M') - session.cloudinventory.generate_report(org.name) - wait_for( - lambda: module_target_sat.api.ForemanTask() - .search( - query={ - 'search': f'label = ForemanInventoryUpload::Async::GenerateReportJob ' - f'and started_at >= "{timestamp}"' - } - )[0] - .result - == 'success', - timeout=400, - delay=15, - silent_failure=True, - handle_exception=True, - ) - report_path = session.cloudinventory.download_report(org.name) - inventory_data = session.cloudinventory.read(org.name) - assert inventory_data['exclude_packages'] is True - json_data = get_report_data(report_path) - hostnames = [host['fqdn'] for host in json_data['hosts']] - assert virtual_host.hostname in hostnames - assert baremetal_host.hostname in hostnames + # Verify that packages are excluded from report all_host_profiles = [host['system_profile'] for host in json_data['hosts']] for host_profiles in all_host_profiles: assert 'installed_packages' not in host_profiles @@ -529,54 +355,3 @@ def test_rhcloud_inventory_without_manifest(session, module_org, target_sat): f'Skipping organization {module_org.name}, no candlepin certificate defined.' in inventory_data['uploading']['terminal'] ) - - -@pytest.mark.stubbed -def test_automatic_inventory_upload_enabled_setting(): - """Test "Automatic inventory upload" setting. - - :id: e84790c6-1700-46c4-9bf8-d8f1e63a7f1f - - :Steps: - 1. Register satellite content host with insights. - 2. Sync inventory status. - 3. Wait for "Inventory scheduled sync" task to execute. - (Change wait time to 1 minute for testing.) - 4. Check whether the satellite shows successful inventory upload for the host. - 5. Disable "Automatic inventory upload" setting. - 6. Unregister host from insights OR Delete host from cloud. - 7. Wait for "Inventory scheduled sync" task to execute. - - :expectedresults: - 1. When "Automatic inventory upload" setting is disabled then - "Inventory scheduled sync" task doesn't sync the inventory status. - - :CaseImportance: Medium - - :BZ: 1965239 - - :CaseAutomation: ManualOnly - """ - - -@pytest.mark.stubbed -def test_automatic_inventory_upload_disabled_setting(): - """Test "Automatic inventory upload" setting. - - :id: 2c830833-3f92-497c-bbb9-f485a1d8eb47 - - :Steps: - 1. Register few hosts with satellite. - 2. Enable "Automatic inventory upload" setting. - 3. Wait for "Inventory scheduled sync" recurring logic to run. - - :expectedresults: - 1. Satellite has "Inventory scheduled sync" recurring logic, which syncs - inventory status automatically if "Automatic inventory upload" setting is enabled. - - :CaseImportance: Medium - - :BZ: 1962695 - - :CaseAutomation: ManualOnly - """ From 353ca64c5e89cc680f7c5f7dfd27b9ca447dfac9 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Sep 2023 05:38:55 -0400 Subject: [PATCH 200/586] [6.14.z] Convert2Rhel test fixes (#12523) --- tests/foreman/api/test_convert2rhel.py | 150 ++++++++++++++++--------- 1 file changed, 94 insertions(+), 56 deletions(-) diff --git a/tests/foreman/api/test_convert2rhel.py b/tests/foreman/api/test_convert2rhel.py index 544f01b3e86..a9a6d9ace0f 100644 --- a/tests/foreman/api/test_convert2rhel.py +++ b/tests/foreman/api/test_convert2rhel.py @@ -49,6 +49,12 @@ def create_activation_key(sat, org, lce, cv, subscription_id): environment=lce, ).create() act_key.add_subscriptions(data={'subscription_id': subscription_id}) + content = sat.cli.ActivationKey.product_content({'id': act_key.id, 'organization-id': org.id}) + act_key.content_override( + data={'content_overrides': [{'content_label': content[0]['label'], 'value': '1'}]} + ) + ak_subscriptions = act_key.product_content()['results'] + ak_subscriptions[0]['enabled'] = True return act_key @@ -61,11 +67,11 @@ def update_cv(sat, cv, lce, repos): return cv -def register_host(sat, act_key, module_org, module_loc, host, ubi=None): +def register_host(sat, act_key, org, module_loc, host, ubi=None): """Register host to satellite""" # generate registration command command = sat.api.RegistrationCommand( - organization=module_org, + organization=org, activation_keys=[act_key.name], location=module_loc, insecure=True, @@ -85,13 +91,21 @@ def ssl_cert(module_target_sat, module_org): @pytest.fixture -def activation_key_rhel(target_sat, module_org, module_lce, module_promoted_cv, version): +def activation_key_rhel( + module_target_sat, module_entitlement_manifest_org, module_lce, module_promoted_cv, version +): """Create activation key that will be used after conversion for registration""" - subs = target_sat.api.Subscription(organization=module_org).search( - query={'search': f'{DEFAULT_SUBSCRIPTION_NAME}'} - ) + subs = module_target_sat.api.Subscription( + organization=module_entitlement_manifest_org.id + ).search(query={'search': f'{DEFAULT_SUBSCRIPTION_NAME}'}) assert subs - return create_activation_key(target_sat, module_org, module_lce, module_promoted_cv, subs[0].id) + return create_activation_key( + module_target_sat, + module_entitlement_manifest_org, + module_lce, + module_promoted_cv, + subs[0].id, + ) @pytest.fixture(scope='module') @@ -125,6 +139,8 @@ def enable_rhel_subscriptions(module_target_sat, module_entitlement_manifest_org module_target_sat.wait_for_tasks( search_query=(f'id = {task["id"]}'), poll_timeout=2500, + search_rate=20, + max_tries=10, ) task_status = module_target_sat.api.ForemanTask(id=task['id']).poll() assert task_status['result'] == 'success' @@ -133,9 +149,9 @@ def enable_rhel_subscriptions(module_target_sat, module_entitlement_manifest_org @pytest.fixture def centos( - target_sat, + module_target_sat, centos_host, - module_org, + module_entitlement_manifest_org, smart_proxy_location, module_promoted_cv, module_lce, @@ -146,15 +162,28 @@ def centos( # updating centos packages on CentOS 8 is necessary for conversion major = version.split('.')[0] if major == '8': - centos_host.execute("yum update -y centos-*") + centos_host.execute('yum -y update centos-*') repo_url = settings.repos.convert2rhel.convert_to_rhel_repo.format(major) - repo = create_repo(target_sat, module_org, repo_url) - cv = update_cv(target_sat, module_promoted_cv, module_lce, enable_rhel_subscriptions + [repo]) - c2r_sub = target_sat.api.Subscription(organization=module_org, name=repo.product.name).search()[ - 0 - ] - act_key = create_activation_key(target_sat, module_org, module_lce, cv, c2r_sub.id) - register_host(target_sat, act_key, module_org, smart_proxy_location, centos_host) + repo = create_repo(module_target_sat, module_entitlement_manifest_org, repo_url) + cv = update_cv( + module_target_sat, module_promoted_cv, module_lce, enable_rhel_subscriptions + [repo] + ) + c2r_sub = module_target_sat.api.Subscription( + organization=module_entitlement_manifest_org.id, name=repo.product.name + ).search()[0] + act_key = create_activation_key( + module_target_sat, module_entitlement_manifest_org, module_lce, cv, c2r_sub.id + ) + register_host( + module_target_sat, + act_key, + module_entitlement_manifest_org, + smart_proxy_location, + centos_host, + ) + centos_host.execute('yum -y update kernel*') + if centos_host.execute('needs-restarting -r').status == 1: + centos_host.power_control(state='reboot') yield centos_host # close ssh session before teardown, because of reboot in conversion it may cause problems centos_host.close() @@ -162,9 +191,9 @@ def centos( @pytest.fixture def oracle( - target_sat, + module_target_sat, oracle_host, - module_org, + module_entitlement_manifest_org, smart_proxy_location, module_promoted_cv, module_lce, @@ -185,16 +214,27 @@ def oracle( oracle_host.power_control(state='reboot') major = version.split('.')[0] repo_url = settings.repos.convert2rhel.convert_to_rhel_repo.format(major) - repo = create_repo(target_sat, module_org, repo_url, ssl_cert) - cv = update_cv(target_sat, module_promoted_cv, module_lce, enable_rhel_subscriptions + [repo]) - c2r_sub = target_sat.api.Subscription(organization=module_org, name=repo.product.name).search()[ - 0 - ] - act_key = create_activation_key(target_sat, module_org, module_lce, cv, c2r_sub.id) + repo = create_repo(module_target_sat, module_entitlement_manifest_org, repo_url, ssl_cert) + cv = update_cv( + module_target_sat, module_promoted_cv, module_lce, enable_rhel_subscriptions + [repo] + ) + c2r_sub = module_target_sat.api.Subscription( + organization=module_entitlement_manifest_org, name=repo.product.name + ).search()[0] + act_key = create_activation_key( + module_target_sat, module_entitlement_manifest_org, module_lce, cv, c2r_sub.id + ) ubi_url = settings.repos.convert2rhel.ubi7 if major == '7' else settings.repos.convert2rhel.ubi8 - ubi = create_repo(target_sat, module_org, ubi_url) + ubi = create_repo(module_target_sat, module_entitlement_manifest_org, ubi_url) ubi_repo = ubi.full_path.replace('https', 'http') - register_host(target_sat, act_key, module_org, smart_proxy_location, oracle_host, ubi_repo) + register_host( + module_target_sat, + act_key, + module_entitlement_manifest_org, + smart_proxy_location, + oracle_host, + ubi_repo, + ) yield oracle_host # close ssh session before teardown, because of reboot in conversion it may cause problems oracle_host.close() @@ -203,7 +243,7 @@ def oracle( @pytest.fixture(scope='module') def version(request): """Version of converted OS""" - return settings.content_host.get(request.param).vm.release + return settings.content_host.get(request.param).vm.deploy_rhel_version @pytest.mark.e2e @@ -212,7 +252,7 @@ def version(request): ['oracle7', 'oracle8'], indirect=True, ) -def test_convert2rhel_oracle(target_sat, oracle, activation_key_rhel, version): +def test_convert2rhel_oracle(module_target_sat, oracle, activation_key_rhel, version): """Convert Oracle linux to RHEL :id: 7fd393f0-551a-4de0-acdd-7f026b485f79 @@ -229,46 +269,42 @@ def test_convert2rhel_oracle(target_sat, oracle, activation_key_rhel, version): :CaseImportance: Medium """ - host_content = target_sat.api.Host(id=oracle.hostname).read_json() + host_content = module_target_sat.api.Host(id=oracle.hostname).read_json() assert host_content['operatingsystem_name'] == f"OracleLinux {version}" # execute job 'Convert 2 RHEL' on host template_id = ( - target_sat.api.JobTemplate().search(query={'search': 'name="Convert to RHEL"'})[0].id + module_target_sat.api.JobTemplate().search(query={'search': 'name="Convert to RHEL"'})[0].id ) - job = target_sat.api.JobInvocation().run( + job = module_target_sat.api.JobInvocation().run( synchronous=False, data={ 'job_template_id': template_id, 'inputs': { 'Activation Key': activation_key_rhel.id, 'Restart': 'yes', + 'Data telemetry': 'yes', }, 'targeting_type': 'static_query', 'search_query': f'name = {oracle.hostname}', }, ) # wait for job to complete - target_sat.wait_for_tasks( - f'resource_type = JobInvocation and resource_id = {job["id"]}', poll_timeout=1000 + module_target_sat.wait_for_tasks( + f'resource_type = JobInvocation and resource_id = {job["id"]}', poll_timeout=2500 ) - result = target_sat.api.JobInvocation(id=job['id']).read() + result = module_target_sat.api.JobInvocation(id=job['id']).read() assert result.succeeded == 1 # check facts: correct os and valid subscription status - host_content = target_sat.api.Host(id=oracle.hostname).read_json() - # workaround for BZ 2080347 - assert ( - host_content['operatingsystem_name'].startswith(f"RHEL Server {version}") - or host_content['operatingsystem_name'].startswith(f"RedHat {version}") - or host_content['operatingsystem_name'].startswith(f"RHEL {version}") - ) + host_content = module_target_sat.api.Host(id=oracle.hostname).read_json() + assert host_content['subscription_status'] == 0 @pytest.mark.e2e -@pytest.mark.parametrize("version", ['centos7', 'centos8'], indirect=True) -def test_convert2rhel_centos(target_sat, centos, activation_key_rhel, version): +@pytest.mark.parametrize('version', ['centos7', 'centos8'], indirect=True) +def test_convert2rhel_centos(module_target_sat, centos, activation_key_rhel, version): """Convert Centos linux to RHEL :id: 6f698440-7d85-4deb-8dd9-363ea9003b92 @@ -285,39 +321,41 @@ def test_convert2rhel_centos(target_sat, centos, activation_key_rhel, version): :CaseImportance: Medium """ - host_content = target_sat.api.Host(id=centos.hostname).read_json() + host_content = module_target_sat.api.Host(id=centos.hostname).read_json() major = version.split('.')[0] - assert host_content['operatingsystem_name'] == f"CentOS {major}" - + assert host_content['operatingsystem_name'] == f'CentOS {major}' # execute job 'Convert 2 RHEL' on host template_id = ( - target_sat.api.JobTemplate().search(query={'search': 'name="Convert to RHEL"'})[0].id + module_target_sat.api.JobTemplate().search(query={'search': 'name="Convert to RHEL"'})[0].id ) - job = target_sat.api.JobInvocation().run( + job = module_target_sat.api.JobInvocation().run( synchronous=False, data={ 'job_template_id': template_id, 'inputs': { 'Activation Key': activation_key_rhel.id, 'Restart': 'yes', + 'Data telemetry': 'yes', }, 'targeting_type': 'static_query', 'search_query': f'name = {centos.hostname}', }, ) # wait for job to complete - target_sat.wait_for_tasks( - f'resource_type = JobInvocation and resource_id = {job["id"]}', poll_timeout=1000 + module_target_sat.wait_for_tasks( + f'resource_type = JobInvocation and resource_id = {job["id"]}', + poll_timeout=2500, + search_rate=20, ) - result = target_sat.api.JobInvocation(id=job['id']).read() + result = module_target_sat.api.JobInvocation(id=job['id']).read() assert result.succeeded == 1 # check facts: correct os and valid subscription status - host_content = target_sat.api.Host(id=centos.hostname).read_json() + host_content = module_target_sat.api.Host(id=centos.hostname).read_json() # workaround for BZ 2080347 assert ( - host_content['operatingsystem_name'].startswith(f"RHEL Server {version}") - or host_content['operatingsystem_name'].startswith(f"RedHat {version}") - or host_content['operatingsystem_name'].startswith(f"RHEL {version}") + host_content['operatingsystem_name'].startswith(f'RHEL Server {version}') + or host_content['operatingsystem_name'].startswith(f'RedHat {version}') + or host_content['operatingsystem_name'].startswith(f'RHEL {version}') ) assert host_content['subscription_status'] == 0 From ee5d54b03690ae3891a8760d966af190daf2c9b6 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Sep 2023 07:06:40 -0400 Subject: [PATCH 201/586] [6.14.z] Move SCAPPlugin Component to Endeavour Team (#12525) Move SCAPPlugin Component to Endeavour Team (#12520) (cherry picked from commit afe2678781538f180de331bc4a75444f4229293c) Co-authored-by: Shweta Singh --- tests/foreman/api/test_oscap_tailoringfiles.py | 2 +- tests/foreman/api/test_oscappolicy.py | 2 +- tests/foreman/cli/test_oscap.py | 2 +- tests/foreman/cli/test_oscap_tailoringfiles.py | 2 +- tests/foreman/longrun/test_oscap.py | 2 +- tests/foreman/ui/test_oscapcontent.py | 2 +- tests/foreman/ui/test_oscappolicy.py | 2 +- tests/foreman/ui/test_oscaptailoringfile.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/foreman/api/test_oscap_tailoringfiles.py b/tests/foreman/api/test_oscap_tailoringfiles.py index 039754324cc..acb748203b2 100644 --- a/tests/foreman/api/test_oscap_tailoringfiles.py +++ b/tests/foreman/api/test_oscap_tailoringfiles.py @@ -8,7 +8,7 @@ :CaseComponent: SCAPPlugin -:Team: Rocket +:Team: Endeavour :TestType: Functional diff --git a/tests/foreman/api/test_oscappolicy.py b/tests/foreman/api/test_oscappolicy.py index 3f1d0269326..60a55c2dbaa 100644 --- a/tests/foreman/api/test_oscappolicy.py +++ b/tests/foreman/api/test_oscappolicy.py @@ -8,7 +8,7 @@ :CaseComponent: SCAPPlugin -:Team: Rocket +:Team: Endeavour :TestType: Functional diff --git a/tests/foreman/cli/test_oscap.py b/tests/foreman/cli/test_oscap.py index f5e442476aa..a4a9c67c3ca 100644 --- a/tests/foreman/cli/test_oscap.py +++ b/tests/foreman/cli/test_oscap.py @@ -6,7 +6,7 @@ :CaseComponent: SCAPPlugin -:Team: Rocket +:Team: Endeavour :TestType: Functional diff --git a/tests/foreman/cli/test_oscap_tailoringfiles.py b/tests/foreman/cli/test_oscap_tailoringfiles.py index cb3c4f697bb..38f91464a48 100644 --- a/tests/foreman/cli/test_oscap_tailoringfiles.py +++ b/tests/foreman/cli/test_oscap_tailoringfiles.py @@ -6,7 +6,7 @@ :CaseComponent: SCAPPlugin -:Team: Rocket +:Team: Endeavour :TestType: Functional diff --git a/tests/foreman/longrun/test_oscap.py b/tests/foreman/longrun/test_oscap.py index a7f163c2829..705150f23d9 100644 --- a/tests/foreman/longrun/test_oscap.py +++ b/tests/foreman/longrun/test_oscap.py @@ -8,7 +8,7 @@ :CaseComponent: SCAPPlugin -:Team: Rocket +:Team: Endeavour :TestType: Functional diff --git a/tests/foreman/ui/test_oscapcontent.py b/tests/foreman/ui/test_oscapcontent.py index 6a86a8ce9e1..e87f4984f9f 100644 --- a/tests/foreman/ui/test_oscapcontent.py +++ b/tests/foreman/ui/test_oscapcontent.py @@ -8,7 +8,7 @@ :CaseComponent: SCAPPlugin -:Team: Rocket +:Team: Endeavour :TestType: Functional diff --git a/tests/foreman/ui/test_oscappolicy.py b/tests/foreman/ui/test_oscappolicy.py index e0d47e152a3..f7ef21c6ab4 100644 --- a/tests/foreman/ui/test_oscappolicy.py +++ b/tests/foreman/ui/test_oscappolicy.py @@ -8,7 +8,7 @@ :CaseComponent: SCAPPlugin -:Team: Rocket +:Team: Endeavour :TestType: Functional diff --git a/tests/foreman/ui/test_oscaptailoringfile.py b/tests/foreman/ui/test_oscaptailoringfile.py index 007f0b44557..b42ef268f61 100644 --- a/tests/foreman/ui/test_oscaptailoringfile.py +++ b/tests/foreman/ui/test_oscaptailoringfile.py @@ -8,7 +8,7 @@ :CaseComponent: SCAPPlugin -:Team: Rocket +:Team: Endeavour :TestType: Functional From 97a024b45f52cbdc05a9498da243a9628c4b4032 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Sep 2023 12:18:25 -0400 Subject: [PATCH 202/586] [6.14.z] Delete Host record on teardown (#12532) --- robottelo/hosts.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 5d78a2b1906..9ee3f549059 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -225,16 +225,31 @@ def satellite(self): self._satellite = Satellite() return self._satellite + @property + def _sat_host_record(self): + """Provide access to this host's Host record if it exists.""" + hosts = self.satellite.api.Host().search(query={'search': self.hostname}) + if not hosts: + logger.debug('No host record found for %s on Satellite', self.hostname) + return None + return hosts[0] + + def _delete_host_record(self): + """Delete the Host record of this host from Satellite.""" + if h_record := self._sat_host_record: + logger.debug('Deleting host record for %s from Satellite', self.hostname) + h_record.delete() + @property def nailgun_host(self): """If this host is subscribed, provide access to its nailgun object""" if self.identity.get('registered_to') == self.satellite.hostname: try: - host_list = self.satellite.api.Host().search(query={'search': self.hostname})[0] + host = self._sat_host_record except Exception as err: logger.error(f'Failed to get nailgun host for {self.hostname}: {err}') - host_list = None - return host_list + host = None + return host else: logger.warning(f'Host {self.hostname} not registered to {self.satellite.hostname}') @@ -382,8 +397,8 @@ def teardown(self): logger.debug('START: tearing down host %s', self) if not self.blank and not getattr(self, '_skip_context_checkin', False): self.unregister() - if type(self) is not Satellite and self.nailgun_host: - self.nailgun_host.delete() + if type(self) is not Satellite: # do not delete Satellite's host record + self._delete_host_record() logger.debug('END: tearing down host %s', self) From 94e646db1e681e3afda9eac74f86624051c94c59 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Sep 2023 15:44:04 -0400 Subject: [PATCH 203/586] [6.14.z] Fix UI SyncPlan (#12539) --- tests/foreman/ui/test_syncplan.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/foreman/ui/test_syncplan.py b/tests/foreman/ui/test_syncplan.py index 0632af2a034..8262d62def3 100644 --- a/tests/foreman/ui/test_syncplan.py +++ b/tests/foreman/ui/test_syncplan.py @@ -54,7 +54,7 @@ def validate_repo_content(repo, content_types, after_sync=True): @pytest.mark.tier2 -def test_positive_end_to_end(session, module_org): +def test_positive_end_to_end(session, module_org, target_sat): """Perform end to end scenario for sync plan component :id: 39c140a6-ca65-4b6a-a640-4a023a2f0f12 @@ -99,7 +99,8 @@ def test_positive_end_to_end(session, module_org): assert syncplan_values['details']['description'] == new_description # Create and add two products to sync plan for _ in range(2): - product = entities.Product(organization=module_org).create() + product = target_sat.api.Product(organization=module_org).create() + target_sat.api.Repository(product=product).create() session.syncplan.add_product(plan_name, product.name) # Remove a product and assert syncplan still searchable session.syncplan.remove_product(plan_name, product.name) From eb81162474c10ef7555df061e867707bc9c60f19 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Sep 2023 16:26:12 -0400 Subject: [PATCH 204/586] [6.14.z] Passing more correct metadata sat version for ibutsu (#12537) Passing more correct metadata sat version for ibutsu (#12488) (cherry picked from commit 1cf7f651ebfd2e6e9a6aff684f13cc572451ad8a) Co-authored-by: Omkar Khatavkar --- pytest_plugins/metadata_markers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pytest_plugins/metadata_markers.py b/pytest_plugins/metadata_markers.py index f409dd804c7..70aee47c6c7 100644 --- a/pytest_plugins/metadata_markers.py +++ b/pytest_plugins/metadata_markers.py @@ -6,7 +6,6 @@ from robottelo.config import settings from robottelo.hosts import get_sat_rhel_version -from robottelo.hosts import get_sat_version from robottelo.logging import collection_logger as logger FMT_XUNIT_TIME = '%Y-%m-%dT%H:%M:%S' @@ -59,7 +58,7 @@ def pytest_configure(config): @pytest.hookimpl(tryfirst=True) -def pytest_collection_modifyitems(session, items, config): +def pytest_collection_modifyitems(items, config): """Add markers and user_properties for testimony token metadata user_properties is used by the junit plugin, and thus by many test report systems @@ -74,7 +73,7 @@ def pytest_collection_modifyitems(session, items, config): """ # get RHEL version of the satellite rhel_version = get_sat_rhel_version().base_version - sat_version = get_sat_version().base_version + sat_version = settings.server.version.get('release') snap_version = settings.server.version.get('snap', '') # split the option string and handle no option, single option, multiple From 6f5b342938d3c11a233fd023a5acb1c24c133897 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 8 Sep 2023 03:56:18 -0400 Subject: [PATCH 205/586] [6.14.z] Fix katello-agent (#12518) Fix katello-agent (#12395) Let katello-agent run on a standard VM (cherry picked from commit f85639f1ca6fff7bf6dbc9f1850cbd27cf5885a0) Co-authored-by: vsedmik <46570670+vsedmik@users.noreply.github.com> --- pytest_fixtures/component/katello_agent.py | 4 +++- robottelo/hosts.py | 5 ++++- tests/foreman/destructive/test_katello_agent.py | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pytest_fixtures/component/katello_agent.py b/pytest_fixtures/component/katello_agent.py index b25304cb8ae..690e726c0d4 100644 --- a/pytest_fixtures/component/katello_agent.py +++ b/pytest_fixtures/component/katello_agent.py @@ -48,7 +48,9 @@ def katello_agent_client(sat_with_katello_agent, rhel_contenthost): org = sat_with_katello_agent.api.Organization().create() client_repo = settings.repos['SATCLIENT_REPO'][f'RHEL{rhel_contenthost.os_version.major}'] sat_with_katello_agent.register_host_custom_repo( - org, rhel_contenthost, [client_repo, settings.repos.yum_1.url] + org, + rhel_contenthost, + [client_repo, settings.repos.yum_1.url], ) rhel_contenthost.install_katello_agent() host_info = sat_with_katello_agent.cli.Host.info({'name': rhel_contenthost.hostname}) diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 9ee3f549059..5a30d73decc 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -2006,7 +2006,7 @@ def register_host_custom_repo(self, module_org, rhel_contenthost, repo_urls): """Register content host to Satellite and sync repos :param module_org: Org where contenthost will be registered. - :param rhel_contenthost: contenthost to be register with Satellite. + :param rhel_contenthost: contenthost to be registered with Satellite. :param repo_urls: List of URLs to be synced and made available to contenthost via subscription-manager. :return: None @@ -2066,6 +2066,9 @@ def register_host_custom_repo(self, module_org, rhel_contenthost, repo_urls): # refresh repository metadata on the host rhel_contenthost.execute('subscription-manager repos --list') + # Override the repos to enabled + rhel_contenthost.execute(r'subscription-manager repos --enable \*') + def enroll_ad_and_configure_external_auth(self, ad_data): """Enroll Satellite Server to an AD Server. diff --git a/tests/foreman/destructive/test_katello_agent.py b/tests/foreman/destructive/test_katello_agent.py index f1af4dab41c..97b92c495c8 100644 --- a/tests/foreman/destructive/test_katello_agent.py +++ b/tests/foreman/destructive/test_katello_agent.py @@ -24,6 +24,7 @@ pytestmark = [ pytest.mark.run_in_one_thread, pytest.mark.destructive, + pytest.mark.no_containers, pytest.mark.tier5, pytest.mark.upgrade, ] From f4b8d2c70c6db66143b4b7455320c066eb28bd88 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 8 Sep 2023 04:08:17 -0400 Subject: [PATCH 206/586] [6.14.z] Bump pytest from 7.4.1 to 7.4.2 (#12541) Bump pytest from 7.4.1 to 7.4.2 (#12540) (cherry picked from commit 46677a9ae1a6a9cbf18d56866896be3e423bfc68) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 646daa56c1a..28994bfbe7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ navmazing==1.1.6 productmd==1.36 pyotp==2.9.0 python-box==7.1.1 -pytest==7.4.1 +pytest==7.4.2 pytest-services==2.2.1 pytest-mock==3.11.1 pytest-reportportal==5.2.1 From 8f2dd18bfb0881e14f606adedd51dd65e64481ae Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 8 Sep 2023 04:44:49 -0400 Subject: [PATCH 207/586] [6.14.z] Add automation for BZ 2106753 (#12546) --- .../foreman/api/test_provisioningtemplate.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/foreman/api/test_provisioningtemplate.py b/tests/foreman/api/test_provisioningtemplate.py index cf8225bb933..01538c58705 100644 --- a/tests/foreman/api/test_provisioningtemplate.py +++ b/tests/foreman/api/test_provisioningtemplate.py @@ -474,3 +474,37 @@ def test_positive_template_subnet_with_boot_mode( for template in pxe_templates: rendered = host.read_template(data={'template_kind': f'{template}'})['template'] assert f'ks={ks_param}' in rendered + + def test_positive_template_use_graphical_installer( + self, module_target_sat, module_sca_manifest_org, module_location, default_os + ): + """Check whether use_graphical_installer paremeter is properly rendered + in the provisioning templates + + :id: 2decc787-59b0-41e6-96be-5dd9371c8967 + + :expectedresults: Rendered template should contain value set as per use_graphical_installer + host parameter for respective rhel hosts. + + :BZ: 2106753 + + :customerscenario: true + """ + host = module_target_sat.api.Host( + name=gen_string('alpha'), + organization=module_sca_manifest_org, + location=module_location, + operatingsystem=default_os, + ).create() + # Host will default boot into text mode with kickstart's skipx command + render = host.read_template(data={'template_kind': 'provision'})['template'] + assert 'skipx' in render + assert 'text' in render + # Using use_graphical_installer host param to override and use graphical mode to boot + host.host_parameters_attributes = [ + {'name': 'use_graphical_installer', 'value': 'true', 'parameter_type': 'boolean'} + ] + host.update(['host_parameters_attributes']) + render = host.read_template(data={'template_kind': 'provision'})['template'] + assert 'graphical' in render + assert 'skipx' not in render From 7e8d2623def8063a39c524f2e9c717ebf3da13c0 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 8 Sep 2023 04:54:19 -0400 Subject: [PATCH 208/586] [6.14.z] Fix libvirt provisioning tests to use new fixtures (#12458) Fix libvirt provisioning tests to use new fixtures (#12335) Signed-off-by: Gaurav Talreja (cherry picked from commit 3194afa0e302f10c0de551d0455d7b01bf991baa) Co-authored-by: Gaurav Talreja --- .../component/provision_libvirt.py | 7 + .../cli/test_computeresource_libvirt.py | 84 +++++++ tests/foreman/cli/test_host.py | 39 --- .../ui/test_computeresource_libvirt.py | 85 +++++++ tests/foreman/ui/test_host.py | 237 ------------------ 5 files changed, 176 insertions(+), 276 deletions(-) diff --git a/pytest_fixtures/component/provision_libvirt.py b/pytest_fixtures/component/provision_libvirt.py index 16f2d962e27..d8638d9a32a 100644 --- a/pytest_fixtures/component/provision_libvirt.py +++ b/pytest_fixtures/component/provision_libvirt.py @@ -12,3 +12,10 @@ def module_cr_libvirt(module_target_sat, module_org, module_location): @pytest.fixture(scope='module') def module_libvirt_image(module_target_sat, module_cr_libvirt): return module_target_sat.api.Image(compute_resource=module_cr_libvirt).create() + + +@pytest.fixture(scope='module') +def module_libvirt_provisioning_sat(module_provisioning_sat): + # Configure Libvirt CR for provisioning + module_provisioning_sat.sat.configure_libvirt_cr() + yield module_provisioning_sat diff --git a/tests/foreman/cli/test_computeresource_libvirt.py b/tests/foreman/cli/test_computeresource_libvirt.py index 7147844bfb9..748a5c36a30 100644 --- a/tests/foreman/cli/test_computeresource_libvirt.py +++ b/tests/foreman/cli/test_computeresource_libvirt.py @@ -38,6 +38,7 @@ import pytest from fauxfactory import gen_string from fauxfactory import gen_url +from wait_for import wait_for from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.computeresource import ComputeResource @@ -48,6 +49,8 @@ from robottelo.constants import LIBVIRT_RESOURCE_URL from robottelo.utils.datafactory import parametrized +LIBVIRT_URL = LIBVIRT_RESOURCE_URL % settings.libvirt.libvirt_hostname + def valid_name_desc_data(): """Random data for valid name and description""" @@ -422,3 +425,84 @@ def test_positive_update_console_password(libvirt_url, set_console_password): cr_name = gen_string('utf8') ComputeResource.create({'name': cr_name, 'provider': 'Libvirt', 'url': gen_url()}) ComputeResource.update({'name': cr_name, 'set-console-password': set_console_password}) + + +@pytest.mark.e2e +@pytest.mark.on_premises_provisioning +@pytest.mark.tier3 +@pytest.mark.rhel_ver_match('[^6]') +@pytest.mark.parametrize('setting_update', ['destroy_vm_on_host_delete=True'], indirect=True) +def test_positive_provision_end_to_end( + request, + setting_update, + module_libvirt_provisioning_sat, + module_sca_manifest_org, + module_location, + provisioning_hostgroup, +): + """Provision a host on Libvirt compute resource with the help of hostgroup. + + :id: b003faa9-2810-4176-94d2-ea84bed248ec + + :setup: Hostgroup and provisioning setup like domain, subnet etc. + + :steps: + 1. Create a Libvirt compute resource. + 2. Create a host on Libvirt compute resource using the Hostgroup + 3. Use compute-attributes parameter to specify key-value parameters + regarding the virtual machine. + 4. Provision the host. + + :expectedresults: Host should be provisioned with hostgroup + + :parametrized: yes + """ + sat = module_libvirt_provisioning_sat.sat + cr_name = gen_string('alpha') + hostname = gen_string('alpha').lower() + libvirt_cr = sat.cli.ComputeResource.create( + { + 'name': cr_name, + 'provider': FOREMAN_PROVIDERS['libvirt'], + 'url': LIBVIRT_URL, + 'organizations': module_sca_manifest_org.name, + 'locations': module_location.name, + } + ) + assert libvirt_cr['name'] == cr_name + host = sat.cli.Host.create( + { + 'name': hostname, + 'location': module_location.name, + 'organization': module_sca_manifest_org.name, + 'hostgroup': provisioning_hostgroup.name, + 'compute-resource-id': libvirt_cr['id'], + 'ip': None, + 'mac': None, + 'compute-attributes': 'cpus=1, memory=6442450944, cpu_mode=default, start=1', + 'interface': f'compute_type=bridge,compute_bridge=br-{settings.provisioning.vlan_id}', + 'volume': 'capacity=10', + 'provision-method': 'build', + } + ) + # teardown + request.addfinalizer(lambda: sat.cli.Host.delete({'id': host['id']})) + + # checks + hostname = f'{hostname}.{module_libvirt_provisioning_sat.domain.name}' + assert hostname == host['name'] + host_info = sat.cli.Host.info({'name': hostname}) + # Check on Libvirt, if VM exists + result = sat.execute( + f'su foreman -s /bin/bash -c "virsh -c {LIBVIRT_URL} list --state-running"' + ) + assert hostname in result.stdout + + wait_for( + lambda: sat.cli.Host.info({'name': hostname})['status']['build-status'] + != 'Pending installation', + timeout=1800, + delay=30, + ) + host_info = sat.cli.Host.info({'id': host['id']}) + assert host_info['status']['build-status'] == 'Installed' diff --git a/tests/foreman/cli/test_host.py b/tests/foreman/cli/test_host.py index d94a880f4fa..23d8c02b257 100644 --- a/tests/foreman/cli/test_host.py +++ b/tests/foreman/cli/test_host.py @@ -682,45 +682,6 @@ def test_positive_list_infrastructure_hosts( assert target_sat.hostname in hostnames -@pytest.mark.skip_if_not_set('libvirt') -@pytest.mark.cli_host_create -@pytest.mark.libvirt_discovery -@pytest.mark.on_premises_provisioning -@pytest.mark.tier1 -def test_positive_create_using_libvirt_without_mac(target_sat, module_location, module_org): - """Create a libvirt host and not specify a MAC address. - - :id: b003faa9-2810-4176-94d2-ea84bed248eb - - :expectedresults: Host is created - - :CaseImportance: Critical - """ - compute_resource = target_sat.api.LibvirtComputeResource( - url=f'qemu+ssh://root@{settings.libvirt.libvirt_hostname}/system', - organization=[module_org.id], - location=[module_location.id], - ).create() - host = target_sat.api.Host(organization=module_org.id, location=module_location.id) - host.create_missing() - result = make_host( - { - 'architecture-id': host.architecture.id, - 'compute-resource-id': compute_resource.id, - 'domain-id': host.domain.id, - 'location-id': host.location.id, - 'medium-id': host.medium.id, - 'name': host.name, - 'operatingsystem-id': host.operatingsystem.id, - 'organization-id': host.organization.id, - 'partition-table-id': host.ptable.id, - 'root-password': host.root_pass, - } - ) - assert result['name'] == host.name + '.' + host.domain.name - Host.delete({'id': result['id']}) - - @pytest.mark.cli_host_create @pytest.mark.tier2 def test_positive_create_inherit_lce_cv( diff --git a/tests/foreman/ui/test_computeresource_libvirt.py b/tests/foreman/ui/test_computeresource_libvirt.py index 53815d45ec7..801064ae2ce 100644 --- a/tests/foreman/ui/test_computeresource_libvirt.py +++ b/tests/foreman/ui/test_computeresource_libvirt.py @@ -20,6 +20,7 @@ import pytest from fauxfactory import gen_string +from wait_for import wait_for from robottelo.config import settings from robottelo.constants import COMPUTE_PROFILE_SMALL @@ -118,3 +119,87 @@ def test_positive_end_to_end(session, module_target_sat, module_org, module_loca assert cr_profile_values['provider_content']['memory'] == '8192 MB' session.computeresource.delete(new_cr_name) assert not session.computeresource.search(new_cr_name) + + +@pytest.mark.on_premises_provisioning +@pytest.mark.tier4 +@pytest.mark.rhel_ver_match('[^6]') +@pytest.mark.parametrize('setting_update', ['destroy_vm_on_host_delete=True'], indirect=True) +def test_positive_provision_end_to_end( + request, + session, + setting_update, + module_sca_manifest_org, + module_location, + provisioning_hostgroup, + module_libvirt_provisioning_sat, +): + """Provision Host on libvirt compute resource, and delete it afterwards + + :id: 2678f95f-0c0e-4b46-a3c1-3f9a954d3bde + + :expectedresults: Host is provisioned successfully + + :CaseLevel: System + + :customerscenario: true + + :BZ: 1243223 + + :parametrized: yes + """ + sat = module_libvirt_provisioning_sat.sat + hostname = gen_string('alpha').lower() + cr = sat.api.LibvirtComputeResource( + provider=FOREMAN_PROVIDERS['libvirt'], + url=LIBVIRT_URL, + display_type='VNC', + location=[module_location], + organization=[module_sca_manifest_org], + ).create() + with session: + session.host.create( + { + 'host.name': hostname, + 'host.organization': module_sca_manifest_org.name, + 'host.location': module_location.name, + 'host.hostgroup': provisioning_hostgroup.name, + 'host.inherit_deploy_option': False, + 'host.deploy': f'{cr.name} (Libvirt)', + 'provider_content.virtual_machine.memory': '6144', + 'interfaces.interface.network_type': 'Physical (Bridge)', + 'interfaces.interface.network': f'br-{settings.provisioning.vlan_id}', + 'additional_information.comment': 'Libvirt provision using valid data', + } + ) + name = f'{hostname}.{module_libvirt_provisioning_sat.domain.name}' + assert session.host.search(name)[0]['Name'] == name + + # teardown + @request.addfinalizer + def _finalize(): + host = sat.api.Host().search(query={'search': f'name="{name}"'}) + if host: + host[0].delete() + + # Check on Libvirt, if VM exists + result = sat.execute( + f'su foreman -s /bin/bash -c "virsh -c {LIBVIRT_URL} list --state-running"' + ) + assert hostname in result.stdout + # Wait for provisioning to complete and report status back to Satellite + wait_for( + lambda: session.host.get_details(name)['properties']['properties_table']['Build'] + != 'Pending installation clear', + timeout=1800, + delay=30, + fail_func=session.browser.refresh, + silent_failure=True, + handle_exception=True, + ) + assert ( + session.host.get_details(name)['properties']['properties_table']['Build'] + == 'Installed clear' + ) + session.host.delete(name) + assert not sat.api.Host().search(query={'search': f'name="{name}"'}) diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index 803abbd7778..413a0398107 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -40,7 +40,6 @@ from robottelo.constants import FAKE_7_CUSTOM_PACKAGE from robottelo.constants import FAKE_8_CUSTOM_PACKAGE from robottelo.constants import FAKE_8_CUSTOM_PACKAGE_NAME -from robottelo.constants import FOREMAN_PROVIDERS from robottelo.constants import OSCAP_PERIOD from robottelo.constants import OSCAP_WEEKDAY from robottelo.constants import PERMISSIONS @@ -97,134 +96,6 @@ def module_global_params(module_target_sat): global_parameter.delete() -@pytest.fixture(scope='module') -def module_libvirt_resource(module_org, smart_proxy_location, module_target_sat): - # Search if Libvirt compute-resource already exists - # If so, just update its relevant fields otherwise, - # Create new compute-resource with 'libvirt' provider. - resource_url = f'qemu+ssh://root@{settings.libvirt.libvirt_hostname}/system' - comp_res = [ - res - for res in module_target_sat.api.LibvirtComputeResource().search() - if res.provider == FOREMAN_PROVIDERS['libvirt'] and res.url == resource_url - ] - if len(comp_res) > 0: - computeresource = module_target_sat.api.LibvirtComputeResource(id=comp_res[0].id).read() - computeresource.location.append(smart_proxy_location) - computeresource.organization.append(module_org) - computeresource = computeresource.update(['location', 'organization']) - else: - # Create Libvirt compute-resource - computeresource = module_target_sat.api.LibvirtComputeResource( - provider=FOREMAN_PROVIDERS['libvirt'], - url=resource_url, - set_console_password=False, - display_type='VNC', - location=[smart_proxy_location], - organization=[module_org], - ).create() - return f'{computeresource.name} (Libvirt)' - - -@pytest.fixture(scope='module') -def module_libvirt_domain(module_org, smart_proxy_location, default_domain): - default_domain.location.append(smart_proxy_location) - default_domain.organization.append(module_org) - default_domain.update(['location', 'organization']) - return default_domain - - -@pytest.fixture(scope='module') -def module_libvirt_subnet( - module_org, smart_proxy_location, module_libvirt_domain, default_smart_proxy, module_target_sat -): - # Search if subnet is defined with given network. - # If so, just update its relevant fields otherwise, - # Create new subnet - network = settings.vlan_networking.subnet - subnet = module_target_sat.api.Subnet().search(query={'search': f'network={network}'}) - if len(subnet) > 0: - subnet = subnet[0].read() - subnet.domain.append(module_libvirt_domain) - subnet.location.append(smart_proxy_location) - subnet.organization.append(module_org) - subnet.dns = default_smart_proxy - subnet.dhcp = default_smart_proxy - subnet.ipam = 'DHCP' - subnet.tftp = default_smart_proxy - subnet.discovery = default_smart_proxy - subnet = subnet.update( - ['domain', 'discovery', 'dhcp', 'dns', 'ipam', 'location', 'organization', 'tftp'] - ) - else: - # Create new subnet - subnet = module_target_sat.api.Subnet( - network=network, - mask=settings.vlan_networking.netmask, - location=[smart_proxy_location], - organization=[module_org], - domain=[module_libvirt_domain], - ipam='DHCP', - dns=default_smart_proxy, - dhcp=default_smart_proxy, - tftp=default_smart_proxy, - discovery=default_smart_proxy, - ).create() - return subnet - - -@pytest.fixture(scope='module') -def module_libvirt_media(module_org, smart_proxy_location, os_path, default_os, module_target_sat): - media = module_target_sat.api.Media().search(query={'search': f'path="{os_path}"'}) - if len(media) > 0: - # Media with this path already exist, make sure it is correct - media = media[0].read() - media.organization.append(module_org) - media.location.append(smart_proxy_location) - media.operatingsystem.append(default_os) - media.os_family = 'Redhat' - media = media.update(['organization', 'location', 'operatingsystem', 'os_family']) - else: - # Create new media - media = module_target_sat.api.Media( - organization=[module_org], - location=[smart_proxy_location], - operatingsystem=[default_os], - path_=os_path, - os_family='Redhat', - ).create() - return media - - -@pytest.fixture(scope='module') -def module_libvirt_hostgroup( - module_org, - smart_proxy_location, - default_partition_table, - default_architecture, - default_os, - module_libvirt_media, - module_libvirt_subnet, - default_smart_proxy, - module_libvirt_domain, - module_lce, - module_cv_repo, - module_target_sat, -): - return module_target_sat.api.HostGroup( - architecture=default_architecture, - domain=module_libvirt_domain, - subnet=module_libvirt_subnet, - lifecycle_environment=module_lce, - content_view=module_cv_repo, - location=[smart_proxy_location], - operatingsystem=default_os, - organization=[module_org], - ptable=default_partition_table, - medium=module_libvirt_media, - ).create() - - @pytest.fixture(scope='module') def module_activation_key(module_entitlement_manifest_org, module_target_sat): """Create activation key using default CV and library environment.""" @@ -1779,114 +1650,6 @@ def test_positive_bulk_delete_host(session, smart_proxy_location, target_sat, fu assert not values['table'] -@pytest.mark.on_premises_provisioning -@pytest.mark.tier4 -def test_positive_provision_end_to_end( - session, - module_org, - smart_proxy_location, - module_libvirt_domain, - module_libvirt_hostgroup, - module_libvirt_resource, - target_sat, -): - """Provision Host on libvirt compute resource - - :id: 2678f95f-0c0e-4b46-a3c1-3f9a954d3bde - - :expectedresults: Host is provisioned successfully - - :CaseLevel: System - """ - hostname = gen_string('alpha').lower() - root_pwd = gen_string('alpha', 15) - with session: - session.host.create( - { - 'host.name': hostname, - 'host.organization': module_org.name, - 'host.location': smart_proxy_location.name, - 'host.hostgroup': module_libvirt_hostgroup.name, - 'host.inherit_deploy_option': False, - 'host.deploy': module_libvirt_resource, - 'provider_content.virtual_machine.memory': '2 GB', - 'operating_system.root_password': root_pwd, - 'interfaces.interface.network_type': 'Physical (Bridge)', - 'interfaces.interface.network': settings.vlan_networking.bridge, - 'additional_information.comment': 'Libvirt provision using valid data', - } - ) - name = f'{hostname}.{module_libvirt_domain.name}' - assert session.host.search(name)[0]['Name'] == name - wait_for( - lambda: session.host.get_details(name)['properties']['properties_table']['Build'] - != 'Pending installation', - timeout=1800, - delay=30, - fail_func=session.browser.refresh, - silent_failure=True, - handle_exception=True, - ) - target_sat.api.Host( - id=target_sat.api.Host().search(query={'search': f'name={name}'})[0].id - ).delete() - assert ( - session.host.get_details(name)['properties']['properties_table']['Build'] == 'Installed' - ) - - -@pytest.mark.on_premises_provisioning -@pytest.mark.run_in_one_thread -@pytest.mark.tier4 -@pytest.mark.parametrize('setting_update', ['destroy_vm_on_host_delete=True'], indirect=True) -def test_positive_delete_libvirt( - session, - module_org, - smart_proxy_location, - module_libvirt_domain, - module_libvirt_hostgroup, - module_libvirt_resource, - setting_update, - target_sat, -): - """Create a new Host on libvirt compute resource and delete it - afterwards - - :id: 6a9175e7-bb96-4de3-bc45-ba6c10dd14a4 - - :customerscenario: true - - :expectedresults: Proper warning message is displayed on delete attempt - and host deleted successfully afterwards - - :BZ: 1243223 - - :CaseLevel: System - """ - hostname = gen_string('alpha').lower() - root_pwd = gen_string('alpha', 15) - with session: - session.host.create( - { - 'host.name': hostname, - 'host.organization': module_org.name, - 'host.location': smart_proxy_location.name, - 'host.hostgroup': module_libvirt_hostgroup.name, - 'host.inherit_deploy_option': False, - 'host.deploy': module_libvirt_resource, - 'provider_content.virtual_machine.memory': '1 GB', - 'operating_system.root_password': root_pwd, - 'interfaces.interface.network_type': 'Physical (Bridge)', - 'interfaces.interface.network': settings.vlan_networking.bridge, - 'additional_information.comment': 'Delete host that provisioned on Libvirt', - } - ) - name = f'{hostname}.{module_libvirt_domain.name}' - assert session.host.search(name)[0]['Name'] == name - session.host.delete(name) - assert not target_sat.api.Host().search(query={'search': f'name="{hostname}"'}) - - # ------------------------------ NEW HOST UI DETAILS ---------------------------- @pytest.mark.tier4 def test_positive_read_details_page_from_new_ui(session, host_ui_options): From 5dfd95c2ea04faa4736e9894b99f74ac74e3a82f Mon Sep 17 00:00:00 2001 From: Jameer Pathan Date: Fri, 8 Sep 2023 13:02:46 +0200 Subject: [PATCH 209/586] [6.14.z] Update tests to use snap version when getting ohsnap repo urls (#12528) (#12551) Update tests to use snap version when getting ohsnap repo urls (#12528) --- pytest_fixtures/core/sat_cap_factory.py | 6 +++++- tests/foreman/destructive/test_clone.py | 4 +++- tests/foreman/installer/test_installer.py | 12 ++++++++++-- tests/foreman/maintain/test_upgrade.py | 4 +++- tests/foreman/sys/test_katello_certs_check.py | 6 +++++- 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/pytest_fixtures/core/sat_cap_factory.py b/pytest_fixtures/core/sat_cap_factory.py index 018a89fdc24..f064fec6da1 100644 --- a/pytest_fixtures/core/sat_cap_factory.py +++ b/pytest_fixtures/core/sat_cap_factory.py @@ -301,7 +301,11 @@ def installer_satellite(request): sat.setup_firewall() # # Register for RHEL8 repos, get Ohsnap repofile, and enable and download satellite sat.register_to_cdn() - sat.download_repofile(product='satellite', release=settings.server.version.release) + sat.download_repofile( + product='satellite', + release=settings.server.version.release, + snap=settings.server.version.snap, + ) sat.execute('dnf -y module enable satellite:el8 && dnf -y install satellite') installed_version = sat.execute('rpm --query satellite').stdout assert sat_version in installed_version diff --git a/tests/foreman/destructive/test_clone.py b/tests/foreman/destructive/test_clone.py index 44fbfd8c13a..5ef2f42abdf 100644 --- a/tests/foreman/destructive/test_clone.py +++ b/tests/foreman/destructive/test_clone.py @@ -94,7 +94,9 @@ def test_positive_clone_backup(target_sat, sat_ready_rhel, backup_type, skip_pul # Disabling repositories assert sat_ready_rhel.execute('subscription-manager repos --disable=*').status == 0 # Getting satellite maintenace repo - sat_ready_rhel.download_repofile(product='satellite', release=sat_version) + sat_ready_rhel.download_repofile( + product='satellite', release=sat_version, snap=settings.server.version.snap + ) # Enabling repositories for repo in getattr(constants, f"OHSNAP_RHEL{rhel_version}_REPOS"): sat_ready_rhel.enable_repo(repo, force=True) diff --git a/tests/foreman/installer/test_installer.py b/tests/foreman/installer/test_installer.py index 66b9c0de11b..3f5c24564cc 100644 --- a/tests/foreman/installer/test_installer.py +++ b/tests/foreman/installer/test_installer.py @@ -1340,7 +1340,11 @@ def common_sat_install_assertions(satellite): def install_satellite(satellite, installer_args): # Register for RHEL8 repos, get Ohsnap repofile, and enable and download satellite satellite.register_to_cdn() - satellite.download_repofile(product='satellite', release=settings.server.version.release) + satellite.download_repofile( + product='satellite', + release=settings.server.version.release, + snap=settings.server.version.snap, + ) satellite.execute('dnf -y module enable satellite:el8 && dnf -y install satellite') # Configure Satellite firewall to open communication satellite.execute( @@ -1398,7 +1402,11 @@ def test_capsule_installation(sat_default_install, cap_ready_rhel, default_org): """ # Get Capsule repofile, and enable and download satellite-capsule cap_ready_rhel.register_to_cdn() - cap_ready_rhel.download_repofile(product='capsule', release=settings.server.version.release) + cap_ready_rhel.download_repofile( + product='capsule', + release=settings.server.version.release, + snap=settings.server.version.snap, + ) cap_ready_rhel.execute( 'dnf -y module enable satellite-capsule:el8 && dnf -y install satellite-capsule' ) diff --git a/tests/foreman/maintain/test_upgrade.py b/tests/foreman/maintain/test_upgrade.py index 4caae7ceb59..002fbe49bd8 100644 --- a/tests/foreman/maintain/test_upgrade.py +++ b/tests/foreman/maintain/test_upgrade.py @@ -142,7 +142,9 @@ def test_negative_pre_upgrade_tuning_profile_check(request, custom_host): timeout='30m', ) # Get current Satellite version's repofile - custom_host.download_repofile(product='satellite', release=sat_version) + custom_host.download_repofile( + product='satellite', release=sat_version, snap=settings.server.version.snap + ) # Run satellite-maintain to have it self update to the newest version, # however, this will not actually execute the command after updating custom_host.execute('satellite-maintain upgrade list-versions') diff --git a/tests/foreman/sys/test_katello_certs_check.py b/tests/foreman/sys/test_katello_certs_check.py index 073f7b22fb1..f058328e2e4 100644 --- a/tests/foreman/sys/test_katello_certs_check.py +++ b/tests/foreman/sys/test_katello_certs_check.py @@ -43,7 +43,11 @@ def test_positive_install_sat_with_katello_certs(certs_data, sat_ready_rhel): :CaseAutomation: Automated """ - sat_ready_rhel.download_repofile(product='satellite', release=settings.server.version.release) + sat_ready_rhel.download_repofile( + product='satellite', + release=settings.server.version.release, + snap=settings.server.version.snap, + ) sat_ready_rhel.register_to_cdn() sat_ready_rhel.execute('dnf -y update') result = sat_ready_rhel.execute( From 2d00c3930a44d6b08d94607980ca9e1061cf74e2 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Sat, 9 Sep 2023 09:12:05 -0400 Subject: [PATCH 210/586] [6.14.z] Set timeout as backup takes long time (#12561) Set timeout as backup takes long time (#12554) (cherry picked from commit 6a64678e06a7567554f755987bdbd8dd63159460) Co-authored-by: Lukas Pramuk --- tests/foreman/maintain/test_backup_restore.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/foreman/maintain/test_backup_restore.py b/tests/foreman/maintain/test_backup_restore.py index 90e4a2c3cf3..0743684b658 100644 --- a/tests/foreman/maintain/test_backup_restore.py +++ b/tests/foreman/maintain/test_backup_restore.py @@ -523,6 +523,7 @@ def test_positive_backup_restore( backup_dir=subdir, backup_type=backup_type, options={'assumeyes': True, 'plaintext': True, 'skip-pulp-content': skip_pulp}, + timeout='30m', ) assert result.status == 0 assert 'FAIL' not in result.stdout From 6ca84e031ab2004d1be7d3f02e292274234eded9 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Sat, 9 Sep 2023 09:14:31 -0400 Subject: [PATCH 211/586] [6.14.z] leapp scenario 8.8 to 9.2 (#12562) leapp scenario 8.8 to 9.2 (#11553) leapp sceanrio 8.8 to 9.2 (cherry picked from commit 0bb9a37843ee8d4e453c87c80777b6e78212a977) Co-authored-by: vijay sawant --- testimony.yaml | 2 +- tests/foreman/cli/test_leapp_client.py | 293 +++++++++++++++++++++++++ 2 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 tests/foreman/cli/test_leapp_client.py diff --git a/testimony.yaml b/testimony.yaml index 46d0b289f43..047f3900ebf 100644 --- a/testimony.yaml +++ b/testimony.yaml @@ -68,7 +68,7 @@ CaseComponent: - katello-agent - katello-tracer - LDAP - - Leappintegration + - LeappIntegration - LifecycleEnvironments - LocalizationInternationalization - Logging diff --git a/tests/foreman/cli/test_leapp_client.py b/tests/foreman/cli/test_leapp_client.py new file mode 100644 index 00000000000..77e4a7e0486 --- /dev/null +++ b/tests/foreman/cli/test_leapp_client.py @@ -0,0 +1,293 @@ +"""Tests for leapp upgrade of content hosts with Satellite + +:Requirement: leapp + +:CaseLevel: Integration + +:CaseComponent: LeappIntegration + +:Team: Rocket + +:TestType: Functional + +:CaseImportance: High + +:CaseAutomation: Automated + +:Upstream: No +""" +import pytest +from broker import Broker + +from robottelo.config import settings +from robottelo.constants import PRDS +from robottelo.hosts import ContentHost +from robottelo.logging import logger + +synced_repos = pytest.StashKey[dict] + +RHEL7_VER = '7.9' +RHEL8_VER = '8.8' +RHEL9_VER = '9.2' + +RHEL_REPOS = { + 'rhel7_server': { + 'id': 'rhel-7-server-rpms', + 'name': f'Red Hat Enterprise Linux 7 Server RPMs x86_64 {RHEL7_VER}', + 'releasever': RHEL7_VER, + 'reposet': 'Red Hat Enterprise Linux 7 Server (RPMs)', + 'product': 'Red Hat Enterprise Linux Server', + }, + 'rhel7_server_extras': { + 'id': 'rhel-7-server-extras-rpms', + 'name': 'Red Hat Enterprise Linux 7 Server - Extras RPMs x86_64', + 'releasever': '7', + 'reposet': 'Red Hat Enterprise Linux 7 Server - Extras (RPMs)', + 'product': 'Red Hat Enterprise Linux Server', + }, + 'rhel8_bos': { + 'id': 'rhel-8-for-x86_64-baseos-rpms', + 'name': f'Red Hat Enterprise Linux 8 for x86_64 - BaseOS RPMs {RHEL8_VER}', + 'releasever': RHEL8_VER, + 'reposet': 'Red Hat Enterprise Linux 8 for x86_64 - BaseOS (RPMs)', + }, + 'rhel8_aps': { + 'id': 'rhel-8-for-x86_64-appstream-rpms', + 'name': f'Red Hat Enterprise Linux 8 for x86_64 - AppStream RPMs {RHEL8_VER}', + 'releasever': RHEL8_VER, + 'reposet': 'Red Hat Enterprise Linux 8 for x86_64 - AppStream (RPMs)', + }, + 'rhel9_bos': { + 'id': 'rhel-9-for-x86_64-baseos-rpms', + 'name': f'Red Hat Enterprise Linux 9 for x86_64 - BaseOS RPMs {RHEL9_VER}', + 'releasever': RHEL9_VER, + 'reposet': 'Red Hat Enterprise Linux 9 for x86_64 - BaseOS (RPMs)', + }, + 'rhel9_aps': { + 'id': 'rhel-9-for-x86_64-appstream-rpms', + 'name': f'Red Hat Enterprise Linux 9 for x86_64 - AppStream RPMs {RHEL9_VER}', + 'releasever': RHEL9_VER, + 'reposet': 'Red Hat Enterprise Linux 9 for x86_64 - AppStream (RPMs)', + }, +} + + +@pytest.fixture(scope='module') +def module_stash(request): + """Module scoped stash for storing data between tests""" + # Please refer the documentation for more details on stash + # https://docs.pytest.org/en/latest/reference/reference.html#stash + request.node.stash[synced_repos] = {} + yield request.node.stash + + +@pytest.fixture(scope='module') +def module_leapp_lce(module_target_sat, module_sca_manifest_org): + return module_target_sat.api.LifecycleEnvironment(organization=module_sca_manifest_org).create() + + +@pytest.fixture +def function_leapp_cv(module_target_sat, module_sca_manifest_org, leapp_repos, module_leapp_lce): + function_leapp_cv = module_target_sat.api.ContentView( + organization=module_sca_manifest_org + ).create() + function_leapp_cv.repository = leapp_repos + function_leapp_cv = function_leapp_cv.update(['repository']) + function_leapp_cv.publish() + cvv = function_leapp_cv.read().version[0] + cvv.promote(data={'environment_ids': module_leapp_lce.id, 'force': True}) + function_leapp_cv = function_leapp_cv.read() + return function_leapp_cv + + +@pytest.fixture +def function_leapp_ak( + module_target_sat, + function_leapp_cv, + module_leapp_lce, + module_sca_manifest_org, +): + return module_target_sat.api.ActivationKey( + content_view=function_leapp_cv, + environment=module_leapp_lce, + organization=module_sca_manifest_org, + ).create() + + +@pytest.fixture +def leapp_repos( + default_architecture, + module_stash, + upgrade_path, + module_target_sat, + module_sca_manifest_org, +): + """Enable and sync RHEL BaseOS, AppStream repositories""" + source = upgrade_path['source_version'] + target = upgrade_path['target_version'] + all_repos = [] + for rh_repo_key in RHEL_REPOS.keys(): + release_version = RHEL_REPOS[rh_repo_key]['releasever'] + if release_version in str(source) or release_version in target: + prod = 'rhel' if 'rhel7' in rh_repo_key else rh_repo_key.split('_')[0] + if module_stash[synced_repos].get(rh_repo_key, None): + logger.info('Repo %s already synced, not syncing it', rh_repo_key) + else: + module_stash[synced_repos][rh_repo_key] = True + repo_id = module_target_sat.api_factory.enable_rhrepo_and_fetchid( + basearch=default_architecture.name, + org_id=module_sca_manifest_org.id, + product=PRDS[prod], + repo=RHEL_REPOS[rh_repo_key]['name'], + reposet=RHEL_REPOS[rh_repo_key]['reposet'], + releasever=release_version, + ) + rh_repo = module_target_sat.api.Repository(id=repo_id).read() + all_repos.append(rh_repo) + rh_repo.sync(timeout=1800) + return all_repos + + +@pytest.fixture +def verify_target_repo_on_satellite( + module_target_sat, + function_leapp_cv, + module_sca_manifest_org, + module_leapp_lce, + upgrade_path, +): + """Verify target rhel version repositories have been added in correct CV, LCE on Satellite""" + target_rhel_major_ver = upgrade_path['target_version'].split('.')[0] + cmd_out = module_target_sat.cli.Repository.list( + { + 'search': f'content_label ~ rhel-{target_rhel_major_ver}', + 'content-view-id': function_leapp_cv.id, + 'organization-id': module_sca_manifest_org.id, + 'lifecycle-environment-id': module_leapp_lce.id, + } + ) + repo_names = [out['name'] for out in cmd_out] + if target_rhel_major_ver == '9': + assert RHEL_REPOS['rhel9_bos']['name'] in repo_names + assert RHEL_REPOS['rhel9_aps']['name'] in repo_names + else: + assert RHEL_REPOS['rhel8_bos']['name'] in repo_names + assert RHEL_REPOS['rhel8_aps']['name'] in repo_names + + +@pytest.fixture +def custom_leapp_host(upgrade_path, module_target_sat, module_sca_manifest_org, function_leapp_ak): + """Checkout content host and register with satellite""" + deploy_args = {} + deploy_args['deploy_rhel_version'] = upgrade_path['source_version'] + with Broker( + workflow='deploy-rhel', + host_class=ContentHost, + deploy_rhel_version=upgrade_path['source_version'], + deploy_flavor=settings.flavors.default, + ) as chost: + result = chost.register( + module_sca_manifest_org, None, function_leapp_ak.name, module_target_sat + ) + assert result.status == 0, f'Failed to register host: {result.stderr}' + yield chost + + +@pytest.fixture +def precondition_check_upgrade_and_install_leapp_tool(custom_leapp_host): + """Clean-up directory if in-place upgrade already performed, + set rhel release version, update system and install leapp-upgrade""" + source_rhel = custom_leapp_host.os_version.base_version + custom_leapp_host.run('rm -rf /root/tmp_leapp_py3') + custom_leapp_host.run('yum repolist') + custom_leapp_host.run(f'subscription-manager release --set {source_rhel}') + assert custom_leapp_host.run('yum update -y').status == 0 + assert custom_leapp_host.run('yum install leapp-upgrade -y').status == 0 + if custom_leapp_host.run('needs-restarting -r').status == 1: + custom_leapp_host.power_control(state='reboot', ensure=True) + + +@pytest.mark.parametrize( + 'upgrade_path', + [ + # {'source_version': RHEL7_VER, 'target_version': RHEL8_VER}, + {'source_version': RHEL8_VER, 'target_version': RHEL9_VER}, + ], + ids=lambda upgrade_path: f'{upgrade_path["source_version"]}' + f'_to_{upgrade_path["target_version"]}', +) +def test_leapp_upgrade_rhel( + module_target_sat, + custom_leapp_host, + upgrade_path, + verify_target_repo_on_satellite, + precondition_check_upgrade_and_install_leapp_tool, +): + """Test to upgrade RHEL host to next major RHEL release using leapp preupgrade and leapp upgrade + job templates + + :id: 8eccc689-3bea-4182-84f3-c121e95d54c3 + + :Steps: + 1. Import a subscription manifest and enable, sync source & target repositories + 2. Create LCE, Create CV, add repositories to it, publish and promote CV, Create AK, etc. + 3. Register content host with AK + 4. Verify that target rhel repositories are enabled on Satellite + 5. Update all packages, install leapp tool and fix inhibitors + 6. Run Leapp Preupgrade and Leapp Upgrade job template + + :expectedresults: + 1. Update RHEL OS major version to another major version + """ + # Fixing known inhibitors for source rhel version 8 + if custom_leapp_host.os_version.major == 8: + # Inhibitor - Firewalld Configuration AllowZoneDrifting Is Unsupported + custom_leapp_host.run( + 'sed -i "s/^AllowZoneDrifting=.*/AllowZoneDrifting=no/" /etc/firewalld/firewalld.conf' + ) + # Run LEAPP-PREUPGRADE Job Template- + template_id = ( + module_target_sat.api.JobTemplate() + .search(query={'search': 'name="Run preupgrade via Leapp"'})[0] + .id + ) + job = module_target_sat.api.JobInvocation().run( + synchronous=False, + data={ + 'job_template_id': template_id, + 'targeting_type': 'static_query', + 'search_query': f'name = {custom_leapp_host.hostname}', + }, + ) + module_target_sat.wait_for_tasks( + f'resource_type = JobInvocation and resource_id = {job["id"]}', poll_timeout=1800 + ) + result = module_target_sat.api.JobInvocation(id=job['id']).read() + assert result.succeeded == 1 + + # Run LEAPP-UPGRADE Job Template- + template_id = ( + module_target_sat.api.JobTemplate() + .search(query={'search': 'name="Run upgrade via Leapp"'})[0] + .id + ) + job = module_target_sat.api.JobInvocation().run( + synchronous=False, + data={ + 'job_template_id': template_id, + 'targeting_type': 'static_query', + 'search_query': f'name = {custom_leapp_host.hostname}', + 'inputs': {'Reboot': 'true'}, + }, + ) + module_target_sat.wait_for_tasks( + f'resource_type = JobInvocation and resource_id = {job["id"]}', poll_timeout=1800 + ) + result = module_target_sat.api.JobInvocation(id=job['id']).read() + assert result.succeeded == 1 + # Wait for the host to be rebooted and SSH daemon to be started. + custom_leapp_host.wait_for_connection() + + custom_leapp_host.clean_cached_properties() + new_ver = str(custom_leapp_host.os_version) + assert new_ver == upgrade_path['target_version'] From c02c9f28655a44eb7e493a1379fdfe628ab077e8 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 11 Sep 2023 02:51:00 -0400 Subject: [PATCH 212/586] [6.14.z] AK update must contain org id, it is a required field (#12565) --- tests/foreman/api/test_permission.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/foreman/api/test_permission.py b/tests/foreman/api/test_permission.py index 68068453295..1dac5452ade 100644 --- a/tests/foreman/api/test_permission.py +++ b/tests/foreman/api/test_permission.py @@ -367,8 +367,18 @@ def test_positive_check_update(self, entity_cls, class_org, class_location): new_entity = new_entity.create() name = new_entity.get_fields()['name'].gen_value() with pytest.raises(HTTPError): - entity_cls(self.cfg, id=new_entity.id, name=name).update(['name']) + if entity_cls is entities.ActivationKey: + entity_cls(self.cfg, id=new_entity.id, name=name, organization=class_org).update( + ['name'] + ) + else: + entity_cls(self.cfg, id=new_entity.id, name=name).update(['name']) self.give_user_permission(_permission_name(entity_cls, 'update')) # update() calls read() under the hood, which triggers # permission error - entity_cls(self.cfg, id=new_entity.id, name=name).update_json(['name']) + if entity_cls is entities.ActivationKey: + entity_cls(self.cfg, id=new_entity.id, name=name, organization=class_org).update_json( + ['name'] + ) + else: + entity_cls(self.cfg, id=new_entity.id, name=name).update_json(['name']) From 789656de38fbd43c2d8c695ef2e0fa512552da08 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 11 Sep 2023 03:35:01 -0400 Subject: [PATCH 213/586] [6.14.z] Update clocale test to use UTF-8 (#12568) --- tests/foreman/maintain/test_service.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/foreman/maintain/test_service.py b/tests/foreman/maintain/test_service.py index ee0122af3c6..0970bf9e2c2 100644 --- a/tests/foreman/maintain/test_service.py +++ b/tests/foreman/maintain/test_service.py @@ -122,6 +122,8 @@ def test_positive_service_stop_start(sat_maintain): assert result.status == 0 +@pytest.mark.stream +@pytest.mark.upgrade @pytest.mark.include_capsule @pytest.mark.usefixtures('start_satellite_services') def test_positive_service_stop_restart(sat_maintain): @@ -215,11 +217,11 @@ def test_positive_status_clocale(sat_maintain): :parametrized: yes :steps: - 1. Run LC_ALL=C satellite-maintain service stop + 1. Run LC_ALL=C.UTF-8 satellite-maintain service status :expectedresults: service status works with C locale """ - assert sat_maintain.cli.Service.status(env_var='LC_ALL=C').status == 0 + assert sat_maintain.cli.Service.status(env_var='LC_ALL=C.UTF-8').status == 0 def test_positive_service_restart_without_hammer_config(missing_hammer_config, sat_maintain): From ec0bd5b53df58489a5bab3ecd45dca7f9672e3cd Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 11 Sep 2023 04:09:15 -0400 Subject: [PATCH 214/586] [6.14.z] Workaround the issue as long as BZ2228820 is open (#12569) --- tests/foreman/api/test_organization.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/foreman/api/test_organization.py b/tests/foreman/api/test_organization.py index 4fa1c5f6397..540a466f239 100644 --- a/tests/foreman/api/test_organization.py +++ b/tests/foreman/api/test_organization.py @@ -34,6 +34,7 @@ from robottelo.utils.datafactory import filtered_datapoint from robottelo.utils.datafactory import invalid_values_list from robottelo.utils.datafactory import parametrized +from robottelo.utils.issue_handlers import is_open @filtered_datapoint @@ -77,7 +78,10 @@ def test_positive_create(self): headers={'content-type': 'text/plain'}, verify=False, ) - assert http.client.UNSUPPORTED_MEDIA_TYPE == response.status_code + if is_open('BZ:2228820'): + assert response.status_code in [http.client.UNSUPPORTED_MEDIA_TYPE, 500] + else: + assert http.client.UNSUPPORTED_MEDIA_TYPE == response.status_code @pytest.mark.tier1 @pytest.mark.build_sanity From 9f1d14be2ac0c475817100d86b30f94ef8ceeed2 Mon Sep 17 00:00:00 2001 From: Jameer Pathan Date: Thu, 7 Sep 2023 18:19:50 +0200 Subject: [PATCH 215/586] Fix test_negative_pre_upgrade_tuning_profile_check --- robottelo/constants/__init__.py | 1 + tests/foreman/maintain/test_upgrade.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index cfff118ca4b..524ec39d866 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -1739,6 +1739,7 @@ class Colored(Box): HAMMER_CONFIG = "~/.hammer/cli.modules.d/foreman.yml" HAMMER_SESSIONS = "~/.hammer/sessions" +INSTALLER_CONFIG_FILE = '/etc/foreman-installer/scenarios.d/satellite.yaml' SATELLITE_ANSWER_FILE = "/etc/foreman-installer/scenarios.d/satellite-answers.yaml" CAPSULE_ANSWER_FILE = "/etc/foreman-installer/scenarios.d/capsule-answers.yaml" MAINTAIN_HAMMER_YML = "/etc/foreman-maintain/foreman-maintain-hammer.yml" diff --git a/tests/foreman/maintain/test_upgrade.py b/tests/foreman/maintain/test_upgrade.py index 002fbe49bd8..afc946547b5 100644 --- a/tests/foreman/maintain/test_upgrade.py +++ b/tests/foreman/maintain/test_upgrade.py @@ -19,6 +19,7 @@ import pytest from robottelo.config import settings +from robottelo.constants import INSTALLER_CONFIG_FILE from robottelo.constants import SATELLITE_VERSION @@ -136,11 +137,15 @@ def test_negative_pre_upgrade_tuning_profile_check(request, custom_host): ) custom_host.download_repofile(product='satellite', release=last_y_stream) custom_host.execute('dnf -y module enable satellite:el8 && dnf -y install satellite') - # Install without system checks to get around installer checks + # Install with development tuning profile to get around installer checks custom_host.execute( - f'satellite-installer --scenario satellite --disable-system-checks --tuning {profile}', + 'satellite-installer --scenario satellite --tuning development', timeout='30m', ) + # Change to correct tuning profile (default or medium) + custom_host.execute( + f'sed -i "s/tuning: development/tuning: {profile}/g" {INSTALLER_CONFIG_FILE}' + ) # Get current Satellite version's repofile custom_host.download_repofile( product='satellite', release=sat_version, snap=settings.server.version.snap From dff691558c7a164f73e8e5fb874d0787189bd8d4 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 11 Sep 2023 11:44:47 -0400 Subject: [PATCH 216/586] [6.14.z] change in import/export directory context (#12572) --- pytest_fixtures/component/templatesync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_fixtures/component/templatesync.py b/pytest_fixtures/component/templatesync.py index 6b5881a95fc..b2c681e8f95 100644 --- a/pytest_fixtures/component/templatesync.py +++ b/pytest_fixtures/component/templatesync.py @@ -24,7 +24,7 @@ def create_import_export_local_dir(target_sat): f'mkdir -p {dir_path} && ' f'chown foreman -R {root_dir} && ' f'restorecon -R -v {root_dir} && ' - f'chcon -t httpd_sys_rw_content_t {dir_path} -R' + f'chcon -t foreman_lib_t {dir_path} -R' ) if result.status != 0: logger.debug(result.stdout) From 7af4835ff9bb8bafcf24f7c28904724191a92bf1 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 12 Sep 2023 04:58:07 -0400 Subject: [PATCH 217/586] [6.14.z] Bump manifester from 0.0.13 to 0.0.14 (#12577) Bump manifester from 0.0.13 to 0.0.14 (#12573) (cherry picked from commit eca12c37e694a18a30fbd9c302602c432f6e7fe0) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 28994bfbe7b..c7335345af5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ deepdiff==6.4.1 dynaconf[vault]==3.2.2 fauxfactory==3.1.0 jinja2==3.1.2 -manifester==0.0.13 +manifester==0.0.14 navmazing==1.1.6 productmd==1.36 pyotp==2.9.0 From 5e5fdf3c8eb8c6f6468664b33f469df261483df1 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 12 Sep 2023 05:28:32 -0400 Subject: [PATCH 218/586] [6.14.z] capsule component review (#12580) --- pytest_fixtures/component/smartproxy.py | 5 - robottelo/hosts.py | 13 + tests/foreman/api/test_capsule.py | 220 ++++++++++++ tests/foreman/api/test_smartproxy.py | 377 -------------------- tests/foreman/cli/test_capsule.py | 352 +----------------- tests/foreman/cli/test_capsule_installer.py | 151 -------- tests/foreman/longrun/test_n_1_upgrade.py | 208 ----------- 7 files changed, 247 insertions(+), 1079 deletions(-) create mode 100644 tests/foreman/api/test_capsule.py delete mode 100644 tests/foreman/api/test_smartproxy.py delete mode 100644 tests/foreman/cli/test_capsule_installer.py delete mode 100644 tests/foreman/longrun/test_n_1_upgrade.py diff --git a/pytest_fixtures/component/smartproxy.py b/pytest_fixtures/component/smartproxy.py index 3b711dfbeff..d879eff5d3a 100644 --- a/pytest_fixtures/component/smartproxy.py +++ b/pytest_fixtures/component/smartproxy.py @@ -13,11 +13,6 @@ def default_smart_proxy(session_target_sat): return session_target_sat.api.SmartProxy(id=smart_proxy.id).read() -@pytest.fixture(scope='session') -def import_puppet_classes(default_smart_proxy): - default_smart_proxy.import_puppetclasses(environment='production') - - @pytest.fixture(scope='module') def module_fake_proxy(request, module_target_sat): """Create a Proxy and register the cleanup function""" diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 5a30d73decc..ee493836d90 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1675,6 +1675,19 @@ def set_rex_script_mode_provider(self, mode='ssh'): if result.status != 0: raise SatelliteHostError(f'Failed to enable pull provider: {result.stdout}') + def run_installer_arg(self, *args, timeout='20m'): + """Run an installer argument on capsule""" + installer_args = list(args) + installer_command = InstallerCommand( + installer_args=installer_args, + ) + result = self.execute( + installer_command.get_command(), + timeout=timeout, + ) + if result.status != 0: + raise SatelliteHostError(f'Failed to execute with argument: {result.stderr}') + def set_mqtt_resend_interval(self, value): """Set the time interval in seconds at which the notification should be re-sent to the mqtt host until the job is picked up or cancelled""" diff --git a/tests/foreman/api/test_capsule.py b/tests/foreman/api/test_capsule.py new file mode 100644 index 00000000000..1d86ecf2300 --- /dev/null +++ b/tests/foreman/api/test_capsule.py @@ -0,0 +1,220 @@ +"""Tests for the ``smart_proxies`` paths. + +:Requirement: Smartproxy + +:CaseAutomation: Automated + +:CaseLevel: Component + +:CaseComponent: Capsule + +:Team: Endeavour + +:TestType: Functional + +:CaseImportance: Critical + +:Upstream: No +""" +import pytest +from fauxfactory import gen_string +from fauxfactory import gen_url +from requests import HTTPError + +from robottelo.config import user_nailgun_config + + +@pytest.mark.e2e +@pytest.mark.upgrade +@pytest.mark.tier1 +def test_positive_update_capsule(target_sat, module_capsule_configured): + """Update various capsule properties + + :id: a3d3eaa9-ed8d-42e6-9c83-20251e5ca9af + + :steps: + 1. Get deployed capsule from fixture + 2. Refresh features + 3. Update capsule organization + 4. Update capsule location + 5. Update capsule name + + :expectedresults: All capsule properties are updated + + :bz: 2077824 + + :customerscenario: true + + """ + new_name = f'{gen_string("alpha")}-{module_capsule_configured.name}' + capsule = target_sat.api.SmartProxy().search( + query={'search': f'name = {module_capsule_configured.hostname}'} + )[0] + + # refresh features + features = capsule.refresh() + module_capsule_configured.run_installer_arg('enable-foreman-proxy-plugin-openscap') + features_new = capsule.refresh() + assert len(features_new["features"]) == len(features["features"]) + 1 + assert 'Openscap' in [feature["name"] for feature in features_new["features"]] + + # update organizations + organizations = [target_sat.api.Organization().create() for _ in range(2)] + capsule.organization = organizations + capsule = capsule.update(['organization']) + assert {org.id for org in capsule.organization} == {org.id for org in organizations} + + # update locations + locations = [target_sat.api.Location().create() for _ in range(2)] + capsule.location = locations + capsule = capsule.update(['location']) + assert {loc.id for loc in capsule.organization} == {loc.id for loc in organizations} + + # update name + capsule.name = new_name + capsule = capsule.update(['name']) + assert capsule.name == new_name + + # serching for non-default capsule BZ#2077824 + capsules = target_sat.api.SmartProxy().search(query={'search': 'id != 1'}) + assert len(capsules) > 0 + assert capsule.url in [cps.url for cps in capsules] + assert capsule.name in [cps.name for cps in capsules] + + +@pytest.mark.skip_if_not_set('fake_capsules') +@pytest.mark.tier1 +def test_negative_create_with_url(target_sat): + """Capsule creation with random URL + + :id: e48a6260-97e0-4234-a69c-77bbbcde85d6 + + :expectedresults: Proxy is not created + + """ + # Create a random proxy + with pytest.raises(HTTPError) as context: + target_sat.api.SmartProxy(url=gen_url(scheme='https')).create() + assert 'Unable to communicate' in context.value.response.text + + +@pytest.mark.skip_if_not_set('fake_capsules') +@pytest.mark.tier1 +@pytest.mark.upgrade +def test_positive_delete(target_sat): + """Capsule deletion + + :id: 872bf12e-736d-43d1-87cf-2923966b59d0 + + :expectedresults: Capsule is deleted + + :BZ: 1398695 + """ + new_port = target_sat.available_capsule_port + with target_sat.default_url_on_new_port(9090, new_port) as url: + proxy = target_sat.api.SmartProxy(url=url).create() + proxy.delete() + with pytest.raises(HTTPError): + proxy.read() + + +@pytest.mark.skip_if_not_set('fake_capsules') +@pytest.mark.tier1 +def test_positive_update_url(request, target_sat): + """Capsule url updated + + :id: 0305fd54-4e0c-4dd9-a537-d342c3dc867e + + :expectedresults: Capsule has the url updated + + """ + # Create fake capsule with name + name = gen_string('alpha') + port = target_sat.available_capsule_port + with target_sat.default_url_on_new_port(9090, port) as url: + proxy = target_sat.api.SmartProxy(url=url, name=name).create() + assert proxy.name == name + # Open another tunnel to update url + new_port = target_sat.available_capsule_port + with target_sat.default_url_on_new_port(9090, new_port) as url: + proxy.url = url + proxy = proxy.update(['url']) + assert proxy.url == url + + +@pytest.mark.skip_if_not_set('fake_capsules') +@pytest.mark.tier2 +@pytest.mark.upgrade +def test_positive_import_puppet_classes( + request, + session_puppet_enabled_sat, + puppet_proxy_port_range, + module_puppet_org, + module_puppet_loc, +): + """Import puppet classes from proxy for admin and non-admin user + + :id: 385efd1b-6146-47bf-babf-0127ce5955ed + + :expectedresults: Puppet classes are imported from proxy + + :CaseComponent: Puppet + + :CaseLevel: Integration + + :BZ: 1398695, 2142555 + + :customerscenario: true + """ + puppet_sat = session_puppet_enabled_sat + update_msg = ( + 'Successfully updated environment and puppetclasses from the on-disk puppet installation' + ) + no_update_msg = 'No changes to your environments detected' + # Create role, add permissions and create non-admin user + user_login = gen_string('alpha') + user_password = gen_string('alpha') + role = puppet_sat.api.Role().create() + puppet_sat.api_factory.create_role_permissions( + role, + { + 'ForemanPuppet::Puppetclass': [ + 'view_puppetclasses', + 'create_puppetclasses', + 'import_puppetclasses', + ] + }, + ) + user = puppet_sat.api.User( + role=[role], + admin=True, + login=user_login, + password=user_password, + organization=[module_puppet_org], + location=[module_puppet_loc], + ).create() + request.addfinalizer(user.delete) + request.addfinalizer(role.delete) + + new_port = puppet_sat.available_capsule_port + with puppet_sat.default_url_on_new_port(9090, new_port) as url: + proxy = puppet_sat.api.SmartProxy(url=url).create() + + result = proxy.import_puppetclasses() + assert result['message'] in [update_msg, no_update_msg] + # Import puppetclasses with environment + result = proxy.import_puppetclasses(environment='production') + assert result['message'] in [update_msg, no_update_msg] + + # Non-Admin user with access to import_puppetclasses + user_cfg = user_nailgun_config(user_login, user_password) + user_cfg.url = f'https://{puppet_sat.hostname}' + user_proxy = puppet_sat.api.SmartProxy(server_config=user_cfg, id=proxy.id).read() + + result = user_proxy.import_puppetclasses() + assert result['message'] in [update_msg, no_update_msg] + # Import puppetclasses with environment + result = user_proxy.import_puppetclasses(environment='production') + assert result['message'] in [update_msg, no_update_msg] + + request.addfinalizer(puppet_sat.api.SmartProxy(id=proxy.id).delete) diff --git a/tests/foreman/api/test_smartproxy.py b/tests/foreman/api/test_smartproxy.py deleted file mode 100644 index a18634c45d2..00000000000 --- a/tests/foreman/api/test_smartproxy.py +++ /dev/null @@ -1,377 +0,0 @@ -"""Tests for the ``smart_proxies`` paths. - -:Requirement: Smartproxy - -:CaseAutomation: Automated - -:CaseLevel: Component - -:CaseComponent: Capsule - -:Team: Endeavour - -:TestType: Functional - -:CaseImportance: Critical - -:Upstream: No -""" -import pytest -from fauxfactory import gen_string -from fauxfactory import gen_url -from requests import HTTPError - -from robottelo.config import user_nailgun_config -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list -from robottelo.utils.issue_handlers import is_open - - -pytestmark = [pytest.mark.run_in_one_thread] - - -@pytest.fixture(scope='module') -def module_proxy_attrs(module_target_sat): - """Find a ``SmartProxy``. - - Every Satellite has a built-in smart proxy, so searching for an - existing smart proxy should always succeed. - """ - smart_proxy = module_target_sat.api.SmartProxy().search( - query={'search': f'url = {module_target_sat.url}:9090'} - ) - # Check that proxy is found and unpack it from the list - assert len(smart_proxy) > 0, "No smart proxy is found" - smart_proxy = smart_proxy[0] - return set(smart_proxy.update_json([]).keys()) - - -def _create_smart_proxy(request, target_sat, **kwargs): - """Create a Smart Proxy and add the finalizer""" - proxy = target_sat.api.SmartProxy(**kwargs).create() - - @request.addfinalizer - def _cleanup(): - target_sat.api.SmartProxy(id=proxy.id).delete() - - return proxy - - -@pytest.mark.skip_if_not_set('fake_capsules') -@pytest.mark.tier1 -def test_negative_create_with_url(target_sat): - """Proxy creation with random URL - - :id: e48a6260-97e0-4234-a69c-77bbbcde85d6 - - :expectedresults: Proxy is not created - - :CaseLevel: Component - - """ - # Create a random proxy - with pytest.raises(HTTPError) as context: - target_sat.api.SmartProxy(url=gen_url(scheme='https')).create() - assert 'Unable to communicate' in context.value.response.text - - -@pytest.mark.skip_if_not_set('fake_capsules') -@pytest.mark.tier1 -@pytest.mark.parametrize('name', **parametrized(valid_data_list())) -def test_positive_create_with_name(request, target_sat, name): - """Proxy creation with valid name - - :id: 0ffe0dc5-675e-45f4-b7e1-a14d3dd81f6e - - :expectedresults: Proxy is created - - :CaseLevel: Component - - :Parametrized: Yes - - :BZ: 2084661 - """ - if is_open('BZ:2084661') and 'html' in request.node.name: - pytest.skip() - new_port = target_sat.available_capsule_port - with target_sat.default_url_on_new_port(9090, new_port) as url: - proxy = _create_smart_proxy(request, target_sat, name=name, url=url) - assert proxy.name == name - - -@pytest.mark.skip_if_not_set('fake_capsules') -@pytest.mark.tier1 -@pytest.mark.upgrade -def test_positive_delete(target_sat): - """Proxy deletion - - :id: 872bf12e-736d-43d1-87cf-2923966b59d0 - - :expectedresults: Proxy is deleted - - :CaseLevel: Component - - :BZ: 1398695 - """ - new_port = target_sat.available_capsule_port - with target_sat.default_url_on_new_port(9090, new_port) as url: - proxy = target_sat.api.SmartProxy(url=url).create() - proxy.delete() - with pytest.raises(HTTPError): - proxy.read() - - -@pytest.mark.skip_if_not_set('fake_capsules') -@pytest.mark.tier1 -def test_positive_update_name(request, target_sat): - """Proxy name update - - :id: f279640e-d7e9-48a3-aed8-7bf406e9d6f2 - - :expectedresults: Proxy has the name updated - - :CaseLevel: Component - - """ - new_port = target_sat.available_capsule_port - with target_sat.default_url_on_new_port(9090, new_port) as url: - proxy = _create_smart_proxy(request, target_sat, url=url) - for new_name in valid_data_list(): - proxy.name = new_name - proxy = proxy.update(['name']) - assert proxy.name == new_name - - -@pytest.mark.skip_if_not_set('fake_capsules') -@pytest.mark.tier1 -def test_positive_update_url(request, target_sat): - """Proxy url update - - :id: 0305fd54-4e0c-4dd9-a537-d342c3dc867e - - :expectedresults: Proxy has the url updated - - :CaseLevel: Component - - """ - # Create fake capsule - port = target_sat.available_capsule_port - with target_sat.default_url_on_new_port(9090, port) as url: - proxy = _create_smart_proxy(request, target_sat, url=url) - # Open another tunnel to update url - new_port = target_sat.available_capsule_port - with target_sat.default_url_on_new_port(9090, new_port) as url: - proxy.url = url - proxy = proxy.update(['url']) - assert proxy.url == url - - -@pytest.mark.skip_if_not_set('fake_capsules') -@pytest.mark.tier1 -def test_positive_update_organization(request, target_sat): - """Proxy name update with the home proxy - - :id: 62631275-7a92-4d34-a949-c56e0c4063f1 - - :expectedresults: Proxy has the name updated - - :CaseLevel: Component - - """ - organizations = [target_sat.api.Organization().create() for _ in range(2)] - newport = target_sat.available_capsule_port - with target_sat.default_url_on_new_port(9090, newport) as url: - proxy = _create_smart_proxy(request, target_sat, url=url) - proxy.organization = organizations - proxy = proxy.update(['organization']) - assert {org.id for org in proxy.organization} == {org.id for org in organizations} - - -@pytest.mark.skip_if_not_set('fake_capsules') -@pytest.mark.tier1 -def test_positive_update_location(request, target_sat): - """Proxy name update with the home proxy - - :id: e08eaaa9-7c11-4cda-bbe7-6d1f7c732569 - - :expectedresults: Proxy has the name updated - - :CaseLevel: Component - - """ - locations = [target_sat.api.Location().create() for _ in range(2)] - new_port = target_sat.available_capsule_port - with target_sat.default_url_on_new_port(9090, new_port) as url: - proxy = _create_smart_proxy(request, target_sat, url=url) - proxy.location = locations - proxy = proxy.update(['location']) - assert {loc.id for loc in proxy.location} == {loc.id for loc in locations} - - -@pytest.mark.skip_if_not_set('fake_capsules') -@pytest.mark.tier2 -@pytest.mark.upgrade -def test_positive_refresh_features(request, target_sat): - """Refresh smart proxy features, search for proxy by id - - :id: d0237546-702e-4d1a-9212-8391295174da - - :expectedresults: Proxy features are refreshed - - :CaseLevel: Integration - - """ - # Since we want to run multiple commands against our fake capsule, we - # need the tunnel kept open in order not to allow different concurrent - # test to claim it. Thus we want to manage the tunnel manually. - - # get an available port for our fake capsule - new_port = target_sat.available_capsule_port - with target_sat.default_url_on_new_port(9090, new_port) as url: - proxy = _create_smart_proxy(request, target_sat, url=url) - proxy.refresh() - - -@pytest.mark.skip_if_not_set('fake_capsules') -@pytest.mark.tier2 -def test_positive_import_puppet_classes( - request, - session_puppet_enabled_sat, - puppet_proxy_port_range, - module_puppet_org, - module_puppet_loc, -): - """Import puppet classes from proxy for admin and non-admin user - - :id: 385efd1b-6146-47bf-babf-0127ce5955ed - - :expectedresults: Puppet classes are imported from proxy - - :CaseLevel: Integration - - :BZ: 1398695, 2142555 - - :customerscenario: true - """ - puppet_sat = session_puppet_enabled_sat - update_msg = ( - 'Successfully updated environment and puppetclasses from the on-disk puppet installation' - ) - no_update_msg = 'No changes to your environments detected' - # Create role, add permissions and create non-admin user - user_login = gen_string('alpha') - user_password = gen_string('alpha') - role = puppet_sat.api.Role().create() - puppet_sat.api_factory.create_role_permissions( - role, - { - 'ForemanPuppet::Puppetclass': [ - 'view_puppetclasses', - 'create_puppetclasses', - 'import_puppetclasses', - ] - }, - ) - user = puppet_sat.api.User( - role=[role], - admin=True, - login=user_login, - password=user_password, - organization=[module_puppet_org], - location=[module_puppet_loc], - ).create() - request.addfinalizer(user.delete) - request.addfinalizer(role.delete) - - new_port = puppet_sat.available_capsule_port - with puppet_sat.default_url_on_new_port(9090, new_port) as url: - proxy = puppet_sat.api.SmartProxy(url=url).create() - - result = proxy.import_puppetclasses() - assert result['message'] in [update_msg, no_update_msg] - # Import puppetclasses with environment - result = proxy.import_puppetclasses(environment='production') - assert result['message'] in [update_msg, no_update_msg] - - # Non-Admin user with access to import_puppetclasses - user_cfg = user_nailgun_config(user_login, user_password) - user_cfg.url = f'https://{puppet_sat.hostname}' - user_proxy = puppet_sat.api.SmartProxy(server_config=user_cfg, id=proxy.id).read() - - result = user_proxy.import_puppetclasses() - assert result['message'] in [update_msg, no_update_msg] - # Import puppetclasses with environment - result = user_proxy.import_puppetclasses(environment='production') - assert result['message'] in [update_msg, no_update_msg] - - request.addfinalizer(puppet_sat.api.SmartProxy(id=proxy.id).delete) - - -"""Tests to see if the server returns the attributes it should. - -Satellite should return a full description of an entity each time an entity -is created, read or updated. These tests verify that certain attributes -really are returned. The ``one_to_*_names`` functions know what names -Satellite may assign to fields. -""" - - -@pytest.mark.tier1 -def test_positive_update_loc(module_proxy_attrs): - """Update a smart proxy. Inspect the server's response. - - :id: 42d6b749-c047-4fd2-90ee-ffab7be558f9 - - :expectedresults: The response contains some value for the ``location`` - field. - - :BZ: 1262037 - - :CaseImportance: High - - :CaseLevel: Component - - """ - names = {'location', 'location_ids', 'locations'} - assert len(names & module_proxy_attrs) >= 1, f'None of {names} are in {module_proxy_attrs}' - - -@pytest.mark.tier1 -def test_positive_update_org(module_proxy_attrs): - """Update a smart proxy. Inspect the server's response. - - :id: fbde9f87-33db-4b95-a5f7-71a618460c84 - - :expectedresults: The response contains some value for the - ``organization`` field. - - :BZ: 1262037 - - :CaseImportance: High - - :CaseLevel: Component - - """ - names = {'organization', 'organization_ids', 'organizations'} - assert len(names & module_proxy_attrs) >= 1, f'None of {names} are in {module_proxy_attrs}' - - -@pytest.mark.skip_if_not_set('fake_capsules') -@pytest.mark.tier1 -def test_positive_search_nondefault_proxy(request, target_sat): - """Search non-default proxy with id!=1 - - :id: caf51662-6b4e-11ed-baba-2b9d7b368002 - - :expectedresults: Non-default proxy can be searched - - :BZ: 2077824 - - :customerscenario: true - """ - with target_sat.default_url_on_new_port(9090, target_sat.available_capsule_port) as url: - proxy = _create_smart_proxy(request, target_sat, name=gen_string('alpha'), url=url) - capsules = target_sat.api.Capsule().search(query={'search': 'id != 1'}) - assert len(capsules) == 1 - assert capsules[0].url == proxy.url - assert capsules[0].name == proxy.name diff --git a/tests/foreman/cli/test_capsule.py b/tests/foreman/cli/test_capsule.py index bc47379c515..d93d2bcbf7f 100644 --- a/tests/foreman/cli/test_capsule.py +++ b/tests/foreman/cli/test_capsule.py @@ -17,173 +17,12 @@ :Upstream: No """ import pytest -from fauxfactory import gen_alphanumeric -from fauxfactory import gen_string -from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.proxy import Proxy -from robottelo.host_helpers.cli_factory import CLIFactoryError -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list -from robottelo.utils.issue_handlers import is_open - pytestmark = [pytest.mark.run_in_one_thread] -def _make_proxy(request, target_sat, options=None): - """Create a Proxy and add the finalizer""" - proxy = target_sat.cli_factory.make_proxy(options) - - @request.addfinalizer - def _cleanup(): - target_sat.cli.Proxy.delete({'id': proxy['id']}) - - return proxy - - -@pytest.mark.skip_if_not_set('fake_capsules') -@pytest.mark.tier1 -def test_negative_create_with_url(target_sat): - """Proxy creation with random URL - - :id: 9050b362-c710-43ba-9d77-7680b8f9ed8c - - :expectedresults: Proxy is not created - - :CaseLevel: Component - - """ - # Create a random proxy - with pytest.raises(CLIFactoryError, match='Could not create the proxy:'): - target_sat.cli_factory.make_proxy( - { - 'url': f"http://{gen_string('alpha', 6)}:{gen_string('numeric', 4)}", - } - ) - - -@pytest.mark.skip_if_not_set('fake_capsules') -@pytest.mark.tier1 -@pytest.mark.parametrize('name', **parametrized(valid_data_list())) -def test_positive_create_with_name(request, target_sat, name): - """Proxy creation with the home proxy - - :id: 7decd7a3-2d35-43ff-9a20-de44e83c7389 - - :expectedresults: Proxy is created - - :CaseLevel: Component - - :Parametrized: Yes - - :BZ: 1398695, 2084661 - """ - if is_open('BZ:2084661') and 'html' in request.node.name: - pytest.skip() - proxy = _make_proxy(request, target_sat, options={'name': name}) - assert proxy['name'] == name - - -@pytest.mark.skip_if_not_set('fake_capsules') -@pytest.mark.tier1 -@pytest.mark.parametrize('name', **parametrized(valid_data_list())) -def test_positive_delete_by_id(request, name, target_sat): - """Proxy deletion with the home proxy - - :id: 1b6973b1-259d-4866-b36f-c2d5fb154035 - - :expectedresults: Proxy is deleted - - :CaseLevel: Component - - :Parametrized: Yes - - :BZ: 1398695, 2084661 - """ - if is_open('BZ:2084661') and 'html' in request.node.name: - pytest.skip() - proxy = target_sat.cli_factory.make_proxy({'name': name}) - Proxy.delete({'id': proxy['id']}) - with pytest.raises(CLIReturnCodeError): - Proxy.info({'id': proxy['id']}) - - -@pytest.mark.skip_if_not_set('fake_capsules') -@pytest.mark.tier1 -def test_positive_update_name(request, target_sat): - """Proxy name update with the home proxy - - :id: 1a02a06b-e9ab-4b9b-bcb0-ac7060188316 - - :expectedresults: Proxy has the name updated - - :CaseLevel: Component - - :BZ: 1398695, 2084661 - """ - proxy = _make_proxy(request, target_sat, options={'name': gen_alphanumeric()}) - valid_data = valid_data_list() - if is_open('BZ:2084661') and 'html' in valid_data: - del valid_data['html'] - for new_name in valid_data.values(): - newport = target_sat.available_capsule_port - with target_sat.default_url_on_new_port(9090, newport) as url: - Proxy.update({'id': proxy['id'], 'name': new_name, 'url': url}) - proxy = Proxy.info({'id': proxy['id']}) - assert proxy['name'] == new_name - - -@pytest.mark.skip_if_not_set('fake_capsules') -@pytest.mark.tier2 -def test_positive_refresh_features_by_id(request, target_sat): - """Refresh smart proxy features, search for proxy by id - - :id: d3db63ce-b877-40eb-a863-294c12489ddd - - :expectedresults: Proxy features are refreshed - - :CaseLevel: Integration - - :CaseImportance: High - - """ - # Since we want to run multiple commands against our fake capsule, we - # need the tunnel kept open in order not to allow different concurrent - # test to claim it. Thus we want to manage the tunnel manually. - - # get an available port for our fake capsule - port = target_sat.available_capsule_port - with target_sat.default_url_on_new_port(9090, port) as url: - proxy = _make_proxy(request, target_sat, options={'url': url}) - Proxy.refresh_features({'id': proxy['id']}) - - -@pytest.mark.skip_if_not_set('fake_capsules') -@pytest.mark.tier2 -def test_positive_refresh_features_by_name(request, target_sat): - """Refresh smart proxy features, search for proxy by name - - :id: 2ddd0097-8f65-430e-963d-a3b5dcffe86b - - :expectedresults: Proxy features are refreshed - - :CaseLevel: Integration - - :CaseImportance: High - - """ - # Since we want to run multiple commands against our fake capsule, we - # need the tunnel kept open in order not to allow different concurrent - # test to claim it. Thus we want to manage the tunnel manually. - - # get an available port for our fake capsule - port = target_sat.available_capsule_port - with target_sat.default_url_on_new_port(9090, port) as url: - proxy = _make_proxy(request, target_sat, options={'url': url}) - Proxy.refresh_features({'id': proxy['name']}) - - @pytest.mark.skip_if_not_set('fake_capsules') @pytest.mark.tier1 def test_positive_import_puppet_classes(session_puppet_enabled_sat, puppet_proxy_port_range): @@ -194,7 +33,6 @@ def test_positive_import_puppet_classes(session_puppet_enabled_sat, puppet_proxy :expectedresults: Puppet classes are imported from proxy :CaseLevel: Component - """ with session_puppet_enabled_sat as puppet_sat: port = puppet_sat.available_capsule_port @@ -205,188 +43,26 @@ def test_positive_import_puppet_classes(session_puppet_enabled_sat, puppet_proxy @pytest.mark.stubbed -def test_positive_provision(): - """User can provision through a capsule - - :id: 1b91e6ed-56bb-4a21-9b69-8b41242458c5 - - :Setup: Some valid, functional compute resource (perhaps one variation - of this case for each supported compute resource type). Also, - functioning capsule with proxy is required. - - :Steps: - - 1. Attempt to route provisioning content through capsule that is - using a proxy - 2. Attempt to provision instance - - :expectedresults: Instance can be provisioned, with content coming - through proxy-enabled capsule. - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -def test_positive_register(): - """User can register system through proxy-enabled capsule - - :id: dc544ec8-0320-4897-a6ca-ce9ebad27975 - - :Steps: attempt to register a system trhough a proxy-enabled capsule - - :expectedresults: system is successfully registered - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -def test_positive_unregister(): - """User can unregister system through proxy-enabled capsule - - :id: 9b7714da-74be-4c0a-9209-9d15c2c98eaa - - :Steps: attempt to unregister a system through a proxy-enabled capsule - - :expectedresults: system is successfully unregistered - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -def test_positive_subscribe(): - """User can subscribe system to content through proxy-enabled - capsule - - :id: 091bba73-bc78-4b8c-ac27-5c10e9838cfb - - :Setup: Content source types configured/synced for [RH, Custom, Puppet, - Docker] etc. - - :Steps: attempt to subscribe a system to a content type variation, via - a proxy-enabled capsule - - :expectedresults: system is successfully subscribed to each content - type - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -def test_positive_consume_content(): - """User can consume content on system, from a content source, - through proxy-enabled capsule - - :id: a3fb9879-7799-4743-99a8-963701e687c1 - - :Setup: Content source types configured/synced for [RH, Custom, Puppet, - Docker] etc. - - :Steps: - - 1. attempt to subscribe a system to a content type variation, via a - proxy-enabled capsule - 2. Attempt to install content RPMs via - proxy-enabled capsule - - :expectedresults: system successfully consume content - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -def test_positive_unsubscribe(): - """User can unsubscribe system from content through - proxy-enabled capsule - - :id: 0d34713d-3d60-4e5a-ada6-9a24aa865cb4 - - :Setup: Content source types configured/synced for [RH, Custom, Puppet] - etc. - - :Steps: - - 1. attempt to subscribe a system to a content type variation, via a - proxy-enabled capsule - 2. attempt to unsubscribe a system from said content type(s) via a - proxy-enabled capsule - - :expectedresults: system is successfully unsubscribed from each content - type - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -def test_positive_reregister_with_capsule_cert(): - """system can register via capsule using cert provided by - the capsule itself. - - :id: 785b94ea-ffbf-4c18-8160-f705e3d7cbe6 - - :Setup: functional capsule and certs rpm installed on target client. - - :Steps: - - 1. Attempt to register from parent satellite; unregister and remove - cert rpm - 2. Attempt to reregister using same credentials and certs from a - functional capsule. - - :expectedresults: Registration works , and certs RPM installed from - capsule. - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -def test_positive_ssl_capsule(): - """Assure SSL functionality for capsules - - :id: 4d19bee6-15d4-4fd5-b3de-9144608cdba7 - - :Setup: A capsule installed with SSL enabled. - - :Steps: Execute basic steps from above (register, subscribe, consume, - unsubscribe, unregister) while connected to a capsule that is - SSL-enabled - - :expectedresults: No failures executing said test scenarios against - SSL, baseline functionality identical to non-SSL - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -def test_positive_enable_bmc(): - """Enable BMC feature on smart-proxy +@pytest.mark.e2e +@pytest.mark.upgrade +def test_positive_capsule_content(): + """Registered and provisioned hosts can consume content from capsule - :id: 9cc4db2f-3bec-4e51-89a2-18a0a6167012 + :id: 7e5493de-b27b-4adc-ba18-4dc2e94e7305 - :Setup: A capsule installed with SSL enabled. + :Setup: Capsule with some content synced :Steps: - 1. Enable BMC feature on proxy by running installer with: - ``katello-installer --foreman-proxy-bmc 'true'`` - 2. Please make sure to check default values to other BMC options. - Should be like below: ``--foreman-proxy-bmc-default-provider - BMC default provider. (default: "ipmitool")`` - ``--foreman-proxy-bmc-listen-on BMC proxy to listen on https, - http, or both (default: "https")`` - 3. Check if BMC plugin is enabled with: ``#cat - /etc/foreman-proxy/settings.d/bmc.yml | grep enabled`` - 4. Restart foreman-proxy service + 1. Register a host to the capsule + 2. Sync content from capsule to the host + 3. Unregister host from the capsule + 4. Provision host via capsule + 5. Provisioned host syncs content from capsule - :expectedresults: Katello installer should show the options to enable - BMC + :expectedresults: Hosts can be successfully registered to the capsule, + consume content and unregister afterwards. Hosts can be successfully + provisioned via capsule and consume content as well. :CaseAutomation: NotAutomated """ diff --git a/tests/foreman/cli/test_capsule_installer.py b/tests/foreman/cli/test_capsule_installer.py deleted file mode 100644 index 7d619ece0c2..00000000000 --- a/tests/foreman/cli/test_capsule_installer.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Test for capsule installer CLI - -:Requirement: Capsule Installer - -:CaseAutomation: Automated - -:CaseLevel: System - -:CaseComponent: Capsule - -:Team: Endeavour - -:TestType: Functional - -:CaseImportance: High - -:Upstream: No -""" -import pytest - - -@pytest.mark.stubbed -def test_positive_basic(): - """perform a basic install of capsule. - - :id: 47445685-5924-4980-89d0-bbb2fb608f4d - - :Steps: - - 1. Assure your target capsule has ONLY the Capsule repo enabled. In - other words, the Satellite repo itself is not enabled by - default. - 2. attempt to perform a basic, functional install the capsule using - `capsule-installer`. - - :expectedresults: product is installed - - :CaseAutomation: NotAutomated - - """ - - -@pytest.mark.stubbed -def test_positive_option_qpid_router(): - """assure the --qpid-router flag can be used in - capsule-installer to enable katello-agent functionality via - remote clients - - :id: d040a72d-72b2-41cf-b14e-a8e37e80200d - - :Steps: Install capsule-installer with the '--qpid-router=true` flag - - :expectedresults: Capsule installs correctly and qpid functionality is - enabled. - - :CaseAutomation: NotAutomated - - """ - - -@pytest.mark.stubbed -def test_positive_option_reverse_proxy(): - """assure the --reverse-proxy flag can be used in - capsule-installer to enable katello-agent functionality via - remote clients - - :id: 756fd76a-0183-4637-93c8-fe7c375be751 - - :Steps: Install using the '--reverse-proxy=true' flag - - :expectedresults: Capsule installs correctly and functionality is - enabled. - - :CaseAutomation: NotAutomated - - """ - - -@pytest.mark.stubbed -def test_negative_invalid_parameters(): - """invalid (non-boolean) parameters cannot be passed to flag - - :id: f4366c87-e436-42b4-ada4-55f0e66a481e - - :Steps: attempt to provide a variety of invalid parameters to installer - (strings, numerics, whitespace, etc.) - - :expectedresults: user is told that such parameters are invalid and - install aborts. - - :CaseAutomation: NotAutomated - - """ - - -@pytest.mark.stubbed -def test_negative_option_parent_reverse_proxy_port(): - """invalid (non-integer) parameters cannot be passed to flag - - :id: a1af16d3-84da-4e94-818e-90bc82cc5698 - - :Setup: na - - :Steps: attempt to provide a variety of invalid parameters to - --parent-reverse-proxy-port flag (strings, numerics, whitespace, - etc.) - - :expectedresults: user told parameters are invalid; install aborts. - - :CaseAutomation: NotAutomated - - """ - - -@pytest.mark.stubbed -def test_positive_option_parent_reverse_proxy(): - """valid parameters can be passed to --parent-reverse-proxy - (true) - - :id: a905f4ca-a729-4efb-84fc-43923737f75b - - :Setup: note that this requires an accompanying, valid port value - - :Steps: Attempt to provide a value of "true" to --parent-reverse-proxy - - :expectedresults: Install commences/completes with proxy installed - correctly. - - :CaseAutomation: NotAutomated - - """ - - -@pytest.mark.stubbed -def test_positive_option_parent_reverse_proxy_port(): - """valid parameters can be passed to - --parent-reverse-proxy-port (integer) - - :id: 32238045-53e2-4ed4-ac86-57917e7aedcd - - :Setup: note that this requires an accompanying, valid host for proxy - parameter - - :Steps: Attempt to provide a valid proxy port # to flag - - :expectedresults: Install commences and completes with proxy installed - correctly. - - :CaseAutomation: NotAutomated - - """ diff --git a/tests/foreman/longrun/test_n_1_upgrade.py b/tests/foreman/longrun/test_n_1_upgrade.py deleted file mode 100644 index 8251763de78..00000000000 --- a/tests/foreman/longrun/test_n_1_upgrade.py +++ /dev/null @@ -1,208 +0,0 @@ -"""Test class for N-1 upgrade feature - -:Requirement: Satellite one version ahead from Capsule - -:CaseAutomation: ManualOnly - -:CaseLevel: System - -:CaseComponent: Capsule - -:Team: Endeavour - -:TestType: Functional - -:CaseImportance: Critical - -:Upstream: No -""" -import pytest - - -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_n_1_setup(): - """ - Prepare the environment to test the N-1 Capsule sync scenarios. - - :id: 62c86858-4803-417a-80c7-0070df228355 - - :steps: - 1. Login Satellite with admin rights. - 2. Add the Red Hat and Custom Repository. - 3. Create the lifecycle environment. - 4. Create the Content view and select all the repository. - 5. Publish the content view and promote the content view with created lifecycle - environment. - 6. Create the activation key and add the content view. - 7. Add the subscription, if it is missing in the activation key. - - :expectedresults: Initial setup should complete successfully - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_capsule_optimize_sync(): - """ - Check the N-1 Capsule sync operation when the synchronization type is Optimize. - - :id: 482da3ab-a29f-46cd-91cc-d3bd7937bd11 - - :steps: - 1. Go to the infrastructure --> Capsules - 2. Add the content view in the capsules - 3. Click on the synchronization and select the optimize sync. - 4. Optimize sync starts successfully - - :expectedresults: - 1. Optimize sync should be start and complete successfully. - 2. All the contents properly populate on the capsule side. - 3. Event of the optimize sync populated in the Monitors--> task section. - 4. Check the events in the log section. - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_capsule_complete_sync(): - """ - Check the N-1 Capsule sync operation when the synchronization type is Complete. - - :id: b8f7ecbf-e359-495e-acd9-3e0ba44d8c12 - - :steps: - 1. Go to the infrastructure --> Capsules - 2. Add the content view in the capsules - 3. Click on the synchronization and select the complete sync. - 4. Complete Sync starts successfully - - :expectedresults: - 1. Optimize sync should be start and complete successfully. - 2. All the contents properly populate on the capsule side. - 3. Event of the optimize sync populated in the Monitors--> task section. - 4. Check the events in log section. - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_content_update_on_capsule_registered_host(): - """ - Check the packages update on the capsule registered content host. - - :id: d331c36f-f69b-4504-89d7-f87c808b1c16 - - :steps: - 1. Go to the hosts --> Content-hosts - 2. Add the content view in the capsules - 3. Click on "Register Content host" - 4. Select the N-1 capsule and copy the command mentioned like - a. curl --insecure --output katello-ca-consumer-latest.noarch.rpm - https://capsule.com/pub/katello-ca-consumer-latest.noarch.rpm - b. yum localinstall katello-ca-consumer-latest.noarch.rpm - 5. Download the katello-ca-consumer certificate and install it on the content host. - 6. Check the Custom repository's contents. - 7. Check the Red Hat repositories contents. - 8. Install the katello-agent from the custom repo. - 9. Install few packages from Red Hat repository. - - :expectedresults: - 1. Content host should be registered successfully from N-1 Capsule. - 2. All the contents should be properly reflected on the registered host side.. - 3. Katello-agent package should be successfully installed via custom repo. - 4. Few Red Hat Packages should be successfully installed via Red Hat repo. - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_provisioning_from_n_1_capsule(): - """ - Check the RHEL7/8 provisioning from N-1 Capsule. - - :id: c65e4812-c461-41b7-975a-6ac34f398232 - - :steps: - 1. Create the activation key of all the required RHEL8 contents. - 2. Create the DNS name, subnet, and hostgroup. - 3. Provision the RHEL8 host from the N-1 Capsule. - - :expectedresults: - 1. Host provisioned successfully. - 2. katello-agent and puppet agent packages should be installed after provisioning - 3. Provisioned host should be registered. - 4. Remote job should be executed on that provisioned host successfully. - 5. Host counts should be updated for the puppet environment. - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_puppet_environment_import(): - """ - Check the puppet environment import from satellite and N-1 capsule - - :id: 59facd73-60be-4eb0-b389-4d2ae6886c35 - - :steps: - 1. import the puppet environment from satellite. - 2. import the puppet environment from N-1 capsule. - - :expectedresults: - 1. Puppet environment should be imported successfully from satellite - as well as capsule. - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_puppet_fact_update(): - """ - Verify the facts successfully fetched from the N-1 Capsule's provisioned host - - :id: dc15da39-c75d-4f1b-8590-3df36c6531de - - :steps: - 1. Register host with N-1 Capsule content source. - 2. Add the activation key with tool report. - 3. Install the puppte agent on the content host. - 4. Update some puppet facts in the smart class - - :expectedresults: - 1. Changed facts should be reflected on the content host - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_ansible_role_import(): - """ - Check the Ansible import operation from N-1 capsule - - :id: 8be51495-2398-45eb-a192-24a4ea09a1d7 - - :steps: - 1. Import ansible roles from N-1 capsule. - - :expectedresults: - 1. Roles import should work from N-1 Capsule. - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -def rex_job_execution_from_n_1_capsule(): - """ - Check the updated ansible templates should work as expected from content-host - (part of n-1 capsule). - - :id: d5f4ab23-109f-43f4-934a-cc8f948211f1 - - :steps: - 1. Update the ansible template. - 2. Run the ansible roles on N-1 registered content host - - :expectedresults: - 1. Ansible job should be completed successfully. - """ From 096744ff1afa117a7dd0ef97c0830519348c8404 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 12 Sep 2023 16:39:59 -0400 Subject: [PATCH 219/586] [6.14.z] Add automation for BZ 2024175 (#12592) --- .../foreman/api/test_provisioningtemplate.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/foreman/api/test_provisioningtemplate.py b/tests/foreman/api/test_provisioningtemplate.py index 01538c58705..3c80cbf265d 100644 --- a/tests/foreman/api/test_provisioningtemplate.py +++ b/tests/foreman/api/test_provisioningtemplate.py @@ -23,6 +23,7 @@ import pytest from fauxfactory import gen_choice +from fauxfactory import gen_integer from fauxfactory import gen_mac from fauxfactory import gen_string from nailgun import client @@ -508,3 +509,60 @@ def test_positive_template_use_graphical_installer( render = host.read_template(data={'template_kind': 'provision'})['template'] assert 'graphical' in render assert 'skipx' not in render + + @pytest.mark.parametrize('module_sync_kickstart_content', [8], indirect=True) + def test_positive_template_check_aap_snippet( + self, + module_sync_kickstart_content, + module_target_sat, + module_sca_manifest_org, + module_location, + module_default_org_view, + module_lce_library, + default_architecture, + default_partitiontable, + ): + """Read the kickstart default template and verify ansible_provisioning_callback + snippet is rendered correctly + + :id: 065ef48f-bec5-4535-8be7-d8527fa21564 + + :expectedresults: Rendered template should contain values set for AAP snippet + host parameter for respective rhel hosts. + + :BZ: 2024175 + + :customerscenario: true + """ + aap_fqdn = 'env-aap.example.com' + template_id = gen_integer(1, 10) + extra_vars_dict = '{"package_install": "zsh"}' + config_key = gen_string('alpha') + host_params = [ + {'name': 'ansible_tower_provisioning', 'value': 'true', 'parameter_type': 'boolean'}, + {'name': 'ansible_tower_fqdn', 'value': aap_fqdn, 'parameter_type': 'string'}, + {'name': 'ansible_host_config_key', 'value': config_key, 'parameter_type': 'string'}, + {'name': 'ansible_job_template_id', 'value': template_id, 'parameter_type': 'integer'}, + {'name': 'ansible_extra_vars', 'value': extra_vars_dict, 'parameter_type': 'string'}, + ] + host = module_target_sat.api.Host( + organization=module_sca_manifest_org, + location=module_location, + name=gen_string('alpha').lower(), + operatingsystem=module_sync_kickstart_content.os, + architecture=default_architecture, + domain=module_sync_kickstart_content.domain, + root_pass=settings.provisioning.host_root_password, + ptable=default_partitiontable, + content_facet_attributes={ + 'content_source_id': module_target_sat.nailgun_smart_proxy.id, + 'content_view_id': module_default_org_view.id, + 'lifecycle_environment_id': module_lce_library.id, + }, + host_parameters_attributes=host_params, + ).create() + render = host.read_template(data={'template_kind': 'provision'})['template'] + assert f'https://{aap_fqdn}/api/v2/job_templates/{template_id}/callback/' in render + assert 'systemctl enable ansible-callback' in render + assert f'"host_config_key":"{config_key}"' in render + assert '{"package_install": "zsh"}' in render From a73146328763c34e9209e93cc9199b7dc63dbff3 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 12 Sep 2023 17:01:11 -0400 Subject: [PATCH 220/586] [6.14.z] Debug failover to cloned manifests (#12589) Debug failover to cloned manifests (#12515) This PR, in combination with https://github.com/SatelliteQE/manifester/pull/21, addresses some of the errors that have been occurring in CI in cases where Manifester times out when exporting a manifest due to an upstream RHSM issue. These changes should enable Robottelo to successfully fail over to using cloned manifests in those cases. (cherry picked from commit b2f69d5e886161e98b264dfa0fe64bfb55c4f88a) Co-authored-by: synkd <48261305+synkd@users.noreply.github.com> --- robottelo/host_helpers/satellite_mixins.py | 4 ++-- robottelo/hosts.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/robottelo/host_helpers/satellite_mixins.py b/robottelo/host_helpers/satellite_mixins.py index 4f469ef0d90..f6dbe08b7e4 100644 --- a/robottelo/host_helpers/satellite_mixins.py +++ b/robottelo/host_helpers/satellite_mixins.py @@ -141,13 +141,13 @@ def upload_manifest(self, org_id, manifest=None, interface='API', timeout=None): :returns: the manifest upload result """ - if manifest is None: + if manifest.content is None: manifest = clone() if timeout is None: # Set the timeout to 1500 seconds to align with the API timeout. timeout = 1500000 if interface == 'CLI': - if isinstance(manifest.content, (bytes, io.BytesIO)): + if hasattr(manifest, 'path'): self.put(f'{manifest.path}', f'{manifest.name}') result = self.cli.Subscription.upload( {'file': manifest.name, 'organization-id': org_id}, timeout=timeout diff --git a/robottelo/hosts.py b/robottelo/hosts.py index ee493836d90..0bebe334353 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -895,7 +895,7 @@ def put(self, local_path, remote_path=None): If local_path is a manifest object, write its contents to a temporary file then continue with the upload. """ - if 'utils.Manifest' in str(local_path): + if 'utils.manifest' in str(local_path): with NamedTemporaryFile(dir=robottelo_tmp_dir) as content_file: content_file.write(local_path.content.read()) content_file.flush() From 04c5ec03ff5bda7f3ee1677489ba3d0c8584365d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 13 Sep 2023 03:48:53 -0400 Subject: [PATCH 221/586] [6.14.z] Bump deepdiff from 6.4.1 to 6.5.0 (#12599) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c7335345af5..10061db43bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ betelgeuse==1.10.0 broker[docker]==0.3.3 cryptography==41.0.3 -deepdiff==6.4.1 +deepdiff==6.5.0 dynaconf[vault]==3.2.2 fauxfactory==3.1.0 jinja2==3.1.2 From 6e9b2f3456a5f69c33717a4007da38fbd622ca63 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 13 Sep 2023 05:14:55 -0400 Subject: [PATCH 222/586] [6.14.z] Avoid using medium tuning profile due to the lack of resources (#12601) --- tests/upgrades/test_performance_tuning.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/upgrades/test_performance_tuning.py b/tests/upgrades/test_performance_tuning.py index ba5f79d1b8b..16033bbd521 100644 --- a/tests/upgrades/test_performance_tuning.py +++ b/tests/upgrades/test_performance_tuning.py @@ -31,7 +31,7 @@ class TestScenarioPerformanceTuning: Test Steps:: 1. Before satellite upgrade. - - Apply the medium tune size using satellite-installer. + - Apply non-default tuning size using satellite-installer. - Check the tuning status and their set tuning parameters after applying the new size. 2. Upgrade the satellite. 3. Verify the following points. @@ -49,17 +49,17 @@ class TestScenarioPerformanceTuning: @pytest.mark.pre_upgrade def test_pre_performance_tuning_apply(self, target_sat): - """In preupgrade scenario we apply the medium tuning size. + """In preupgrade scenario we apply non-default tuning size. :id: preupgrade-83404326-20b7-11ea-a370-48f17f1fc2e1 :steps: 1. collect the custom_hira.yaml file before upgrade. - 2. Update the tuning size to medium. + 2. Update the tuning size to non-default. 3. Check the updated tuning size. 4. If something gets wrong with updated tune size then restore the default tune size. - :expectedresults: Medium tuning parameter should be applied. + :expectedresults: Non-default tuning parameter should be applied. """ try: @@ -67,12 +67,12 @@ def test_pre_performance_tuning_apply(self, target_sat): local_path="custom-hiera-before-upgrade.yaml", remote_path="/etc/foreman-installer/custom-hiera.yaml", ) - installer_obj = InstallerCommand(tuning='medium') + installer_obj = InstallerCommand(tuning='development') command_output = target_sat.execute(installer_obj.get_command(), timeout='30m') assert 'Success!' in command_output.stdout installer_obj = InstallerCommand(help='tuning') command_output = target_sat.execute(installer_obj.get_command()) - assert 'default: "medium"' in command_output.stdout + assert 'default: "development"' in command_output.stdout except Exception as exp: logger.critical(exp) @@ -92,17 +92,17 @@ def test_post_performance_tuning_apply(self, target_sat): :steps: 1. Check the tuning size. 2. Compare the custom-hiera.yaml file. - 3. Change the tuning size from medium to default. + 3. Change the tuning size from non-default to default. :expectedresults: - 1. medium tune parameter should be unchanged after upgrade. + 1. non-default tuning parameter should be set after upgrade. 2. custom-hiera.yaml file should be unchanged after upgrade. 3. tuning parameter update should work after upgrade. """ installer_obj = InstallerCommand(help='tuning') command_output = target_sat.execute(installer_obj.get_command(), timeout='30m') - assert 'default: "medium"' in command_output.stdout + assert 'default: "development"' in command_output.stdout target_sat.get( local_path="custom-hiera-after-upgrade.yaml", remote_path="/etc/foreman-installer/custom-hiera.yaml", From b2cb851e9eebbc4499dd109b0b58286071f74375 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 14 Sep 2023 02:39:24 -0400 Subject: [PATCH 223/586] [6.14.z] Bump sphinx from 7.2.5 to 7.2.6 (#12611) Bump sphinx from 7.2.5 to 7.2.6 (#12607) (cherry picked from commit 3907ad1f85669fed58bce9c1a1341c474227f384) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index a461862b7d5..cc96a855781 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -5,7 +5,7 @@ redis==5.0.0 pre-commit==3.4.0 # For generating documentation. -sphinx==7.2.5 +sphinx==7.2.6 sphinx-autoapi==2.1.1 # For 'manage' interactive shell From e1bbea719dff10b3197dfc3278713f85ec3cea21 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 14 Sep 2023 02:56:39 -0400 Subject: [PATCH 224/586] [6.14.z] Bump dynaconf[vault] from 3.2.2 to 3.2.3 (#12613) Bump dynaconf[vault] from 3.2.2 to 3.2.3 (#12606) (cherry picked from commit dcce6f942389d68139ce4e5f701a7501bf36d2dc) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 10061db43bb..0c01a4a792b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ betelgeuse==1.10.0 broker[docker]==0.3.3 cryptography==41.0.3 deepdiff==6.5.0 -dynaconf[vault]==3.2.2 +dynaconf[vault]==3.2.3 fauxfactory==3.1.0 jinja2==3.1.2 manifester==0.0.14 From 2e9a10f513bd73d88ed38f598f1dd7bdc8c36741 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 14 Sep 2023 03:35:14 -0400 Subject: [PATCH 225/586] [6.14.z] Fix regression from cloned manifest failover (#12617) --- robottelo/host_helpers/satellite_mixins.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/robottelo/host_helpers/satellite_mixins.py b/robottelo/host_helpers/satellite_mixins.py index f6dbe08b7e4..8e718f2e71d 100644 --- a/robottelo/host_helpers/satellite_mixins.py +++ b/robottelo/host_helpers/satellite_mixins.py @@ -141,8 +141,9 @@ def upload_manifest(self, org_id, manifest=None, interface='API', timeout=None): :returns: the manifest upload result """ - if manifest.content is None: - manifest = clone() + if not isinstance(manifest, (bytes, io.BytesIO)): + if manifest.content is None: + manifest = clone() if timeout is None: # Set the timeout to 1500 seconds to align with the API timeout. timeout = 1500000 From aeebfdf8d8131d381f302e1401ea2fbd2238cad2 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 14 Sep 2023 06:19:53 -0400 Subject: [PATCH 226/586] [6.14.z] Fix satellite_maintain_upgrade_list_versions scenarios (#12624) --- tests/upgrades/test_satellite_maintain.py | 120 ++++------------------ 1 file changed, 21 insertions(+), 99 deletions(-) diff --git a/tests/upgrades/test_satellite_maintain.py b/tests/upgrades/test_satellite_maintain.py index fe7a3bb10af..81af10fdf1b 100644 --- a/tests/upgrades/test_satellite_maintain.py +++ b/tests/upgrades/test_satellite_maintain.py @@ -1,4 +1,4 @@ -"""Test for Satellite-maintain related Upgrade Scenario's +"""satellite-maintain Upgrade Scenarios :Requirement: UpgradedSatellite @@ -16,12 +16,13 @@ :Upstream: No """ +import re + import pytest class TestSatelliteMaintain: - """The test class contains pre-upgrade and post-upgrade scenarios to test - satellite-maintain utility + """Pre-upgrade and post-upgrade scenarios to test satellite-maintain utility. Test Steps: 1. Before Satellite upgrade, Perform test for "satellite-maintain upgrade list-versions" @@ -32,74 +33,19 @@ class TestSatelliteMaintain: @staticmethod def satellite_upgradable_version_list(sat_obj): - """ - This function is used to collect the details of satellite version and upgradable - version list. - - :return: satellite_version, upgradeable_version, major_version_change - """ - - cmd = "rpm -q satellite > /dev/null && rpm -q satellite --queryformat=%{VERSION}" - # leaving this section as-is for now but this could be refactored to use sat_obj.version - satellite_version = sat_obj.execute(cmd) - if satellite_version.status == 0: - satellite_version = satellite_version.stdout - else: - return [], [], None, None - satellite_maintain_version = sat_obj.execute( - "satellite-maintain upgrade list-versions --disable-self-upgrade" - ) - upgradeable_version = [ - version for version in satellite_maintain_version.stdout if version != '' - ] - version_change = 0 - for version in upgradeable_version: - version_change += int(version.split('.')[0]) - if version_change % 2 == 0: - major_version_change = False - y_version = '' - else: - major_version_change = True - y_version = list(set(satellite_maintain_version) - set(satellite_version))[0].split( - '.' - )[-1] - - return satellite_version, upgradeable_version, major_version_change, y_version + """Obtain upgradable version list by satellite-maintain. - @staticmethod - def version_details( - satellite_version, major_version_change, y_version, upgrade_stage="pre-upgrade" - ): - """ - This function is used to update the details of zstream upgrade and - next version upgrade - :param str satellite_version: satellite version would be like 6.5.0, 6.6.0, 6.7.0 - :param bool major_version_change: For major version upgrade like 6.8 to 7.0, 7.0 - to 8.0 etc, then major_version_change would be True. - :param str y_version: y_version change depends on major_version_change - :param str upgrade_stage: upgrade stage would be pre or post. - :return: zstream_version, next_version + :return: upgradeable_versions """ - - major_version = satellite_version.split('.')[0:1] - if major_version_change: - major_version = [int(major_version[0]) + 1].append(y_version) - else: - y_version = int(satellite_version.split('.')[0:2][-1]) - zstream_version = '' - if upgrade_stage == "pre-upgrade": - major_version.append(str(y_version + 1)) - zstream_version = ".".join(satellite_version.split('.')[0:2]) + ".z" - else: - major_version.append(str(y_version)) - major_version.append("z") - next_version = ".".join(major_version) - return zstream_version, next_version + cmd = 'satellite-maintain upgrade list-versions --disable-self-upgrade' + list_versions = sat_obj.execute(cmd).stdout + regex = re.compile(r'^\d+\.\d+') + upgradeable_versions = [version for version in list_versions if regex.match(version)] + return upgradeable_versions @pytest.mark.pre_upgrade def test_pre_satellite_maintain_upgrade_list_versions(self, target_sat): - """Pre-upgrade sceanrio that tests list of satellite version - which satellite can be upgraded. + """Test list of satellite target versions before upgrade. :id: preupgrade-fc2c54b2-2663-11ea-b47c-48f17f1fc2e1 @@ -107,29 +53,15 @@ def test_pre_satellite_maintain_upgrade_list_versions(self, target_sat): 1. Run satellite-maintain upgrade list-versions :expectedresults: Versions should be current z-stream. - """ - ( - satellite_version, - upgradable_version, - major_version_change, - y_version, - ) = self.satellite_upgradable_version_list(target_sat) - if satellite_version: - # In future If satellite-maintain packages update add before - # pre-upgrade test case execution then next version kind of - # stuff check we can add it here. - zstream_version, next_version = self.version_details( - satellite_version[0], major_version_change, y_version - ) - else: - zstream_version = -1 - assert zstream_version in upgradable_version + zstream = '.'.join(target_sat.version.split('.')[0:2]) + '.z' + upgradable_versions = self.satellite_upgradable_version_list(target_sat) + # only possible target should be appropriate zstream + assert set(upgradable_versions) - {zstream} == set() @pytest.mark.post_upgrade def test_post_satellite_maintain_upgrade_list_versions(self, target_sat): - """Post-upgrade sceanrio that tests list of satellite version - which satellite can be upgraded. + """Test list of satellite target versions after upgrade. :id: postupgrade-0bce689c-2664-11ea-b47c-48f17f1fc2e1 @@ -137,18 +69,8 @@ def test_post_satellite_maintain_upgrade_list_versions(self, target_sat): 1. Run satellite-maintain upgrade list-versions. :expectedresults: Versions should be next z-stream. - """ - ( - satellite_version, - upgradable_version, - major_version_change, - y_version, - ) = self.satellite_upgradable_version_list(target_sat) - if satellite_version: - zstream_version, next_version = self.version_details( - satellite_version[0], major_version_change, y_version, upgrade_stage="post-upgrade" - ) - else: - next_version = -1 - assert next_version in upgradable_version + zstream = '.'.join(target_sat.version.split('.')[0:2]) + '.z' + upgradable_versions = self.satellite_upgradable_version_list(target_sat) + # only possible target should be appropriate zstream + assert set(upgradable_versions) - {zstream} == set() From 9ad4804c981fc43bac26d610b941b9a127b7e77d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 15 Sep 2023 06:06:57 -0400 Subject: [PATCH 227/586] [6.14.z] Bump broker[docker] from 0.3.3 to 0.4.0 (#12471) Bump broker[docker] from 0.3.3 to 0.4.0 (#12436) Bumps [broker[docker]](https://github.com/SatelliteQE/broker) from 0.3.3 to 0.4.0. - [Release notes](https://github.com/SatelliteQE/broker/releases) - [Commits](https://github.com/SatelliteQE/broker/compare/0.3.3...0.4.0) --- updated-dependencies: - dependency-name: broker[docker] dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit c0138a4313e2b3073a656f72a64d76d8a9ecdd7d) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0c01a4a792b..06d0a175004 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # Version updates managed by dependabot betelgeuse==1.10.0 -broker[docker]==0.3.3 +broker[docker]==0.4.0 cryptography==41.0.3 deepdiff==6.5.0 dynaconf[vault]==3.2.3 From 5c8288af8435d8da367bfac1e96076a139b1ce45 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 15 Sep 2023 06:11:15 -0400 Subject: [PATCH 228/586] [6.14.z] change in upgrade marks for hammer tests (#12634) change in upgrade marks for hammer tests (#12584) (cherry picked from commit 2525b6495025c988b509d195e43c7ed9fe7bdc52) Co-authored-by: Peter Ondrejka --- tests/foreman/cli/test_hammer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/foreman/cli/test_hammer.py b/tests/foreman/cli/test_hammer.py index 966c53b2117..a2eff81403a 100644 --- a/tests/foreman/cli/test_hammer.py +++ b/tests/foreman/cli/test_hammer.py @@ -30,7 +30,7 @@ HAMMER_COMMANDS = json.loads(DataFile.HAMMER_COMMANDS_JSON.read_text()) -pytestmark = [pytest.mark.tier1, pytest.mark.upgrade] +pytestmark = [pytest.mark.tier1] def fetch_command_info(command): @@ -127,6 +127,7 @@ def test_positive_all_options(target_sat): pytest.fail(format_commands_diff(differences)) +@pytest.mark.upgrade def test_positive_disable_hammer_defaults(request, function_product, target_sat): """Verify hammer disable defaults command. @@ -166,6 +167,7 @@ def _finalize(): assert str(function_product.organization.id) not in result.stdout +@pytest.mark.upgrade def test_positive_check_debug_log_levels(target_sat): """Enabling debug log level in candlepin via hammer logging @@ -193,6 +195,7 @@ def test_positive_check_debug_log_levels(target_sat): @pytest.mark.e2e +@pytest.mark.upgrade def test_positive_hammer_shell(target_sat): """Verify that hammer shell runs a command when input is provided via interactive/bash From 7b0674efca0ada6212a4b63637ec1ab61642ad04 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 15 Sep 2023 09:27:51 -0400 Subject: [PATCH 229/586] [6.14.z] iPXE provisioning test (#12631) iPXE provisioning test (#12604) ipxe provisioning test Signed-off-by: Shubham Ganar (cherry picked from commit 504bd424d015379376cec2fe3e482562a3d28ecc) Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> --- pytest_fixtures/component/provision_pxe.py | 1 + tests/foreman/api/test_provisioning.py | 131 ++++++++++++++++++++- 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/pytest_fixtures/component/provision_pxe.py b/pytest_fixtures/component/provision_pxe.py index aaa9837b810..8ea993e561f 100644 --- a/pytest_fixtures/component/provision_pxe.py +++ b/pytest_fixtures/component/provision_pxe.py @@ -285,6 +285,7 @@ def pxe_loader(request): PXE_LOADER_MAP = { 'bios': {'vm_firmware': 'bios', 'pxe_loader': 'PXELinux BIOS'}, 'uefi': {'vm_firmware': 'uefi', 'pxe_loader': 'Grub2 UEFI'}, + 'ipxe': {'vm_firmware': 'bios', 'pxe_loader': 'iPXE Embedded'}, } return Box(PXE_LOADER_MAP[getattr(request, 'param', 'bios')]) diff --git a/tests/foreman/api/test_provisioning.py b/tests/foreman/api/test_provisioning.py index a3e71c95c06..42b5ecb6a69 100644 --- a/tests/foreman/api/test_provisioning.py +++ b/tests/foreman/api/test_provisioning.py @@ -21,6 +21,7 @@ from wait_for import wait_for from robottelo.config import settings +from robottelo.utils.installer import InstallerCommand @pytest.mark.e2e @@ -114,7 +115,7 @@ def test_rhel_pxe_provisioning( ).status == 0 ) - host_ssh_os = module_provisioning_sat.sat.execute( + 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' @@ -147,3 +148,131 @@ def test_rhel_pxe_provisioning( # assert that the host is subscribed and consumes # subsctiption provided by the activation key assert provisioning_host.subscribed, 'Host is not subscribed' + + +@pytest.mark.e2e +@pytest.mark.parametrize('pxe_loader', ['ipxe'], indirect=True) +@pytest.mark.on_premises_provisioning +@pytest.mark.rhel_ver_match('[^6]') +def test_rhel_ipxe_provisioning( + request, + module_provisioning_sat, + module_sca_manifest_org, + module_location, + provisioning_host, + pxe_loader, + module_provisioning_rhel_content, + provisioning_hostgroup, + module_lce_library, + module_default_org_view, +): + """Provision a host using iPXE workflow + + :id: 9e016e1d-757a-48e7-9159-131bb65dc4ed + + :steps: + 1. Configure satellite for provisioning + 2. provision a host + 3. Check that resulting host is registered to Satellite + 4. Check host is subscribed to Satellite + + :expectedresults: + 1. Provisioning via iPXE is successful + 2. Host installs right version of RHEL + 3. Satellite is able to run REX job on the host + 4. Host is registered to Satellite and subscription status is 'Success' + + :parametrized: yes + """ + # TODO: parametrize iPXE Chain BIOS as pxe loader after #BZ:2171172 is fixed + sat = module_provisioning_sat.sat + # set http url + ipxe_http_url = sat.install( + InstallerCommand( + f'foreman-proxy-dhcp-ipxefilename "http://{sat.hostname}/unattended/iPXE?bootstrap=1"' + ) + ) + assert ipxe_http_url.status == 0 + host_mac_addr = provisioning_host._broker_args['provisioning_nic_mac_addr'] + host = sat.api.Host( + hostgroup=provisioning_hostgroup, + organization=module_sca_manifest_org, + location=module_location, + name=gen_string('alpha').lower(), + mac=host_mac_addr, + operatingsystem=module_provisioning_rhel_content.os, + subnet=module_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) + + # TODO: Implement Satellite log capturing logic to verify that + # all the events are captured in the logs. + + # 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 = {sat.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' From 66c797cf8b733de2ff4e5b55472095084007f702 Mon Sep 17 00:00:00 2001 From: Jameer Pathan <21165044+jameerpathan111@users.noreply.github.com> Date: Fri, 15 Sep 2023 15:54:46 +0200 Subject: [PATCH 230/586] [6.14.z] refactor rh_cloud tests (#12629) refactor rh_cloud tests --- robottelo/hosts.py | 2 + tests/foreman/api/test_rhcloud_inventory.py | 197 ++++---------------- tests/foreman/cli/test_rhcloud_insights.py | 60 ------ tests/foreman/cli/test_rhcloud_inventory.py | 2 +- tests/foreman/ui/test_rhcloud_insights.py | 25 +-- 5 files changed, 44 insertions(+), 242 deletions(-) delete mode 100644 tests/foreman/cli/test_rhcloud_insights.py diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 0bebe334353..0478a64c07f 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1146,6 +1146,8 @@ def configure_rhai_client( # Register client if self.execute('insights-client --register').status != 0: raise ContentHostError('Unable to register client to Insights through Satellite') + if self.execute('insights-client --test-connection').status != 0: + raise ContentHostError('Test connection failed via insights.') def unregister_insights(self): """Unregister insights client. diff --git a/tests/foreman/api/test_rhcloud_inventory.py b/tests/foreman/api/test_rhcloud_inventory.py index 178797abb4a..0208aed3040 100644 --- a/tests/foreman/api/test_rhcloud_inventory.py +++ b/tests/foreman/api/test_rhcloud_inventory.py @@ -66,10 +66,11 @@ def test_rhcloud_inventory_api_e2e( 8. metadata contains source and foreman_rh_cloud_version keys. 9. Assert Hostnames, IP addresses, infrastructure type, and installed packages are present in report. + 10. Assert that system_purpose_sla field is present in the inventory report. :CaseImportance: Critical - :BZ: 1807829, 1926100, 1965234, 1824183, 1879453 + :BZ: 1807829, 1926100, 1965234, 1824183, 1879453, 1845113 :customerscenario: true """ @@ -83,18 +84,20 @@ def test_rhcloud_inventory_api_e2e( destination=local_report_path ) common_assertion(local_report_path) - # Assert Hostnames, IP addresses, and installed packages are present in report. json_data = get_report_data(local_report_path) json_meta_data = get_report_metadata(local_report_path) + # Verify that metadata contains source and foreman_rh_cloud_version keys. prefix = 'tfm-' if module_target_sat.os_version.major < 8 else '' package_version = module_target_sat.run( f'rpm -qa --qf "%{{VERSION}}" {prefix}rubygem-foreman_rh_cloud' ).stdout.strip() assert json_meta_data['source_metadata']['foreman_rh_cloud_version'] == str(package_version) assert json_meta_data['source'] == 'Satellite' + # Verify Hostnames are present in report. hostnames = [host['fqdn'] for host in json_data['hosts']] assert virtual_host.hostname in hostnames assert baremetal_host.hostname in hostnames + # Verify IP addresses are present in report. ip_addresses = [ host['system_profile']['network_interfaces'][0]['ipv4_addresses'][0] for host in json_data['hosts'] @@ -104,16 +107,21 @@ def test_rhcloud_inventory_api_e2e( assert baremetal_host.ip_addr in ip_addresses assert virtual_host.ip_addr in ipv4_addresses assert baremetal_host.ip_addr in ipv4_addresses - + # Verify infrastructure type. infrastructure_type = [ host['system_profile']['infrastructure_type'] for host in json_data['hosts'] ] assert 'physical' and 'virtual' in infrastructure_type - + # Verify installed packages are present in report. all_host_profiles = [host['system_profile'] for host in json_data['hosts']] for host_profiles in all_host_profiles: assert 'installed_packages' in host_profiles assert len(host_profiles['installed_packages']) > 1 + # Verify that system_purpose_sla field is present in the inventory report. + for host in json_data['hosts']: + assert host['facts'][0]['facts']['system_purpose_role'] == 'test-role' + assert host['facts'][0]['facts']['system_purpose_sla'] == 'Self-Support' + assert host['facts'][0]['facts']['system_purpose_usage'] == 'test-usage' @pytest.mark.e2e @@ -136,7 +144,7 @@ def test_rhcloud_inventory_api_hosts_synchronization( 5. Assert inventory status for the host. :expectedresults: - 1. Task detail should contain should contain number of hosts + 1. Task detail should contain number of hosts synchronized and disconnected. :BZ: 1970223 @@ -157,53 +165,6 @@ def test_rhcloud_inventory_api_hosts_synchronization( # To Do: Add support in Nailgun to get Insights and Inventory host properties. -@pytest.mark.run_in_one_thread -@pytest.mark.tier2 -def test_system_purpose_sla_field( - inventory_settings, - rhcloud_manifest_org, - rhcloud_registered_hosts, - module_target_sat, -): - """Verify that system_purpose_sla field is present in the inventory report - for the host subscribed using Activation key with service level set in it. - - :id: 3974338c-3a66-41ac-af32-ee76e3c37aef - - :customerscenario: true - - :Steps: - 1. Create an activation key with service level set in it. - 2. Register a content host using the created activation key. - 3. Generate inventory report. - 4. Assert that host is listed in the inventory report. - 5. Assert that system_purpose_sla field is present in the inventory report. - - :CaseImportance: Medium - - :expectedresults: - 1. Host is present in the inventory report. - 2. system_purpose_sla field is present in the inventory report. - - :BZ: 1845113 - - :CaseAutomation: Automated - """ - org = rhcloud_manifest_org - virtual_host, baremetal_host = rhcloud_registered_hosts - local_report_path = robottelo_tmp_dir.joinpath(f'{gen_alphanumeric()}_{org.id}.tar.xz') - module_target_sat.generate_inventory_report(org) - # Download report - module_target_sat.api.Organization(id=org.id).rh_cloud_download_report( - destination=local_report_path - ) - json_data = get_report_data(local_report_path) - for host in json_data['hosts']: - assert host['facts'][0]['facts']['system_purpose_role'] == 'test-role' - assert host['facts'][0]['facts']['system_purpose_sla'] == 'Self-Support' - assert host['facts'][0]['facts']['system_purpose_usage'] == 'test-usage' - - @pytest.mark.stubbed def test_rhcloud_inventory_auto_upload_setting(): """Verify that Automatic inventory upload setting works as expected. @@ -227,7 +188,7 @@ def test_rhcloud_inventory_auto_upload_setting(): 2. If "Automatic inventory upload" setting is disable then satellite does not generate and upload inventory report automatically. - :BZ: 1793017 + :BZ: 1793017, 1865879 :CaseAutomation: ManualOnly """ @@ -244,8 +205,8 @@ def test_inventory_upload_with_http_proxy(): :Steps: 1. Create a http proxy which is using port 80. - 2. Register a content host with satellite. - 3. Set Default HTTP Proxy setting. + 2. Update general and content proxy in Satellite settings. + 3. Register a content host with satellite. 4. Generate and upload inventory report. 5. Assert that host is listed in the inventory report. 6. Assert that upload process finished successfully. @@ -276,14 +237,20 @@ def test_include_parameter_tags_setting( :Steps: 1. Enable include_parameter_tags setting. 2. Register a content host with satellite. - 3. Generate inventory report. - 4. Assert that generated report contains valid json file. + 3. Create a host parameter with long text value. + 4. Create Hostcollection with name containing double quotes. + 5. Generate inventory report. + 6. Assert that generated report contains valid json file. + 7. Observe the tag generated from the parameter. :expectedresults: 1. Valid json report is created. 2. satellite_parameter values are string. + 3. Parameter tag value must not be created after the + allowed length. + 4. Tag value is escaped properly. - :BZ: 1981869, 1967438 + :BZ: 1981869, 1967438, 2035204, 1874587, 1874619 :customerscenario: true @@ -291,113 +258,21 @@ def test_include_parameter_tags_setting( """ org = rhcloud_manifest_org virtual_host, baremetal_host = rhcloud_registered_hosts - local_report_path = robottelo_tmp_dir.joinpath(f'{gen_alphanumeric()}_{org.id}.tar.xz') - module_target_sat.update_setting('include_parameter_tags', True) - module_target_sat.generate_inventory_report(org) - # Download report - module_target_sat.api.Organization(id=org.id).rh_cloud_download_report( - destination=local_report_path - ) - json_data = get_report_data(local_report_path) - common_assertion(local_report_path) - for host in json_data['hosts']: - for tag in host['tags']: - if tag['namespace'] == 'satellite_parameter': - assert type(tag['value']) is str - break - - -@pytest.mark.tier3 -def test_rh_cloud_tag_values( - inventory_settings, - rhcloud_manifest_org, - module_target_sat, - rhcloud_registered_hosts, -): - """Verify that tag values are escaped properly when hostgroup name - contains " (double quote) in it. - - :id: ea7cd7ca-4157-4aac-ad8e-e66b88740ce3 - - :customerscenario: true - - :Steps: - 1. Create Hostcollection with name containing double quotes. - 2. Register a content host with satellite. - 3. Add a content host to hostgroup. - 4. Generate inventory report. - 5. Assert that generated report contains valid json file. - 6. Assert that hostcollection tag value is escaped properly. - - :expectedresults: - 1. Valid json report is created. - 2. Tag value is escaped properly. - - :BZ: 1874587, 1874619 - - :CaseAutomation: Automated - """ - org = rhcloud_manifest_org - + # Create a host parameter with long text value. + param_name = gen_string('alpha') + param_value = gen_string('alpha', length=260) + module_target_sat.api.CommonParameter(name=param_name, value=param_value).create() + # Create Hostcollection with name containing double quotes. host_col_name = gen_string('alpha') host_name = rhcloud_registered_hosts[0].hostname host = module_target_sat.api.Host().search(query={'search': host_name})[0] host_collection = module_target_sat.api.HostCollection( organization=org, name=f'"{host_col_name}"', host=[host] ).create() - assert len(host_collection.host) == 1 + # Generate inventory report local_report_path = robottelo_tmp_dir.joinpath(f'{gen_alphanumeric()}_{org.id}.tar.xz') - # Generate report - module_target_sat.generate_inventory_report(org) - module_target_sat.api.Organization(id=org.id).rh_cloud_download_report( - destination=local_report_path - ) - common_assertion(local_report_path) - json_data = get_report_data(local_report_path) - for host in json_data['hosts']: - if host['fqdn'] == host_name: - for tag in host['tags']: - if tag['key'] == 'host_collection': - assert tag['value'] == f'"{host_col_name}"' - break - - -@pytest.mark.run_in_one_thread -@pytest.mark.tier2 -def test_positive_tag_values_max_length( - inventory_settings, - rhcloud_manifest_org, - rhcloud_registered_hosts, - module_target_sat, - target_sat, -): - """Verify that tags values are truncated properly for the host parameter - with max length. - - :id: dbcc7245-88af-4c35-87b8-92de01030cb5 - - :Steps: - 1. Enable include_parameter_tags setting - 2. Create a host parameter with long text value. - 3. Generate a rh_cloud report. - 4. Observe the tag generated from the parameter. - - :expectedresults: - 1. Parameter tag value must not be created after the - allowed length. - - :BZ: 2035204 - - :CaseAutomation: Automated - """ - - param_name = gen_string('alpha') - param_value = gen_string('alpha', length=260) - target_sat.api.CommonParameter(name=param_name, value=param_value).create() - - org = rhcloud_manifest_org - local_report_path = robottelo_tmp_dir.joinpath(f'{gen_alphanumeric()}_{org.id}.tar.xz') + # Enable include_parameter_tags setting module_target_sat.update_setting('include_parameter_tags', True) module_target_sat.generate_inventory_report(org) # Download report @@ -406,8 +281,16 @@ def test_positive_tag_values_max_length( ) json_data = get_report_data(local_report_path) common_assertion(local_report_path) + # Verify that parameter tag value is not be created. for host in json_data['hosts']: for tag in host['tags']: if tag['key'] == param_name: assert tag['value'] == "Original value exceeds 250 characters" break + # Verify that hostcollection tag value is escaped properly. + for host in json_data['hosts']: + if host['fqdn'] == host_name: + for tag in host['tags']: + if tag['key'] == 'host_collection': + assert tag['value'] == f'"{host_col_name}"' + break diff --git a/tests/foreman/cli/test_rhcloud_insights.py b/tests/foreman/cli/test_rhcloud_insights.py deleted file mode 100644 index 9682fc0d86c..00000000000 --- a/tests/foreman/cli/test_rhcloud_insights.py +++ /dev/null @@ -1,60 +0,0 @@ -"""CLI tests for Insights part of RH Cloud - Inventory plugin. - -:Requirement: RH Cloud - Inventory - -:CaseAutomation: Automated - -:CaseLevel: System - -:CaseComponent: RHCloud-Inventory - -:Team: Platform - -:TestType: Functional - -:CaseImportance: High - -:Upstream: No -""" -import pytest -from broker import Broker - -from robottelo.hosts import ContentHost - - -@pytest.mark.e2e -@pytest.mark.tier4 -@pytest.mark.parametrize('distro', ['rhel7', 'rhel8']) -def test_positive_connection_option( - rhcloud_activation_key, rhcloud_manifest_org, module_target_sat, distro -): - """Verify that 'insights-client --test-connection' successfully tests the proxy connection via - the Satellite. - - :id: 61a4a39e-b484-49f4-a6fd-46ffc7736e50 - - :customerscenario: true - - :Steps: - - 1. Create RHEL7 and RHEL8 VM and register to insights within org having manifest. - - 2. Run 'insights-client --test-connection'. - - :expectedresults: 'insights-client --test-connection' should return 0. - - :BZ: 1976754 - - :CaseImportance: Critical - """ - org = rhcloud_manifest_org - ak = rhcloud_activation_key - with Broker(nick=distro, host_class=ContentHost) as vm: - vm.configure_rhai_client(module_target_sat, ak.name, org.label, distro) - result = vm.run('insights-client --test-connection') - assert result.status == 0, ( - 'insights-client --test-connection failed.\n' - f'status: {result.status}\n' - f'stdout: {result.stdout}\n' - f'stderr: {result.stderr}' - ) diff --git a/tests/foreman/cli/test_rhcloud_inventory.py b/tests/foreman/cli/test_rhcloud_inventory.py index 0330420dd45..34d2f4a989f 100644 --- a/tests/foreman/cli/test_rhcloud_inventory.py +++ b/tests/foreman/cli/test_rhcloud_inventory.py @@ -209,7 +209,7 @@ def test_max_org_size_variable(): 1. Register few content hosts with satellite. 2. Change value of max_org_size for testing purpose(See BZ#1962694#c2). 3. Start report generation and upload using - ForemanInventoryUpload::Async::GenerateAllReportsJob.perform_now + ForemanTasks.sync_task(ForemanInventoryUpload::Async::GenerateAllReportsJob) :expectedresults: If organization had more hosts than specified by max_org_size variable then report won't be uploaded. diff --git a/tests/foreman/ui/test_rhcloud_insights.py b/tests/foreman/ui/test_rhcloud_insights.py index a57488414e3..861450011b4 100644 --- a/tests/foreman/ui/test_rhcloud_insights.py +++ b/tests/foreman/ui/test_rhcloud_insights.py @@ -64,7 +64,7 @@ def test_rhcloud_insights_e2e( :CaseImportance: Critical - :BZ: 1965901, 1962048 + :BZ: 1965901, 1962048, 1976754 :customerscenario: true @@ -189,29 +189,6 @@ def test_recommendation_sync_for_satellite(): """ -@pytest.mark.stubbed -def test_allow_auto_insights_sync_setting(): - """Test "allow_auto_insights_sync" setting. - - :id: ddc4ed5b-43c0-4121-bf2c-b8e040e45379 - - :Steps: - 1. Register few satellite content host with insights. - 2. Enable "allow_auto_insights_sync" setting. - 3. Wait for "InsightsScheduledSync" task to run. - - :expectedresults: - 1. Satellite has "Inventory scheduled sync" recurring logic, which syncs - inventory status automatically if "Automatic inventory upload" setting is enabled. - - :CaseImportance: Medium - - :BZ: 1865879 - - :CaseAutomation: ManualOnly - """ - - @pytest.mark.stubbed def test_host_sorting_based_on_recommendation_count(): """Verify that hosts can be sorted and filtered based on insights From ceb320e6a68c18c6a7d4e7ca2d1a915ed12321bd Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 15 Sep 2023 10:18:59 -0400 Subject: [PATCH 231/586] [6.14.z] Use sAMAccountName in AD LDAP, i.e. 'domain\username' (#12637) --- tests/foreman/api/test_role.py | 2 +- tests/foreman/api/test_user.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/foreman/api/test_role.py b/tests/foreman/api/test_role.py index e669dd9d19b..a7e67a6c2cf 100644 --- a/tests/foreman/api/test_role.py +++ b/tests/foreman/api/test_role.py @@ -186,7 +186,7 @@ def create_ldap(self, ad_data, target_sat, module_location, module_org): ldap_user_passwd=ad_data['ldap_user_passwd'], authsource=entities.AuthSourceLDAP( onthefly_register=True, - account=ad_data['ldap_user_name'], + account=fr"{ad_data['workgroup']}\{ad_data['ldap_user_name']}", account_password=ad_data['ldap_user_passwd'], base_dn=ad_data['base_dn'], groups_base=ad_data['group_base_dn'], diff --git a/tests/foreman/api/test_user.py b/tests/foreman/api/test_user.py index bc2dfe83c14..85531ae543a 100644 --- a/tests/foreman/api/test_user.py +++ b/tests/foreman/api/test_user.py @@ -666,7 +666,7 @@ def create_ldap(self, ad_data, module_target_sat): ldap_user_passwd=ad_data['ldap_user_passwd'], authsource=module_target_sat.api.AuthSourceLDAP( onthefly_register=True, - account=ad_data['ldap_user_name'], + account=fr"{ad_data['workgroup']}\{ad_data['ldap_user_name']}", account_password=ad_data['ldap_user_passwd'], base_dn=ad_data['base_dn'], groups_base=ad_data['group_base_dn'], @@ -741,7 +741,7 @@ def test_positive_access_entities_from_ldap_org_admin(self, create_ldap): :steps: - 1. Create Org Admin and assign taxonomies to it + 1. Create Org Admin role and assign taxonomies to it 2. Create LDAP user with same taxonomies as role above 3. Assign Org Admin role to user above 4. Login with LDAP user and attempt to access resources @@ -862,7 +862,7 @@ def test_positive_access_entities_from_ipa_org_admin(self, create_ldap): :steps: - 1. Create Org Admin and assign taxonomies to it + 1. Create Org Admin role and assign taxonomies to it 2. Create FreeIPA user with same taxonomies as role above 3. Assign Org Admin role to user above 4. Login with FreeIPA user and attempt to access resources From 16c1319ee4901c0a6e8fabf18a40af85d01c7fbd Mon Sep 17 00:00:00 2001 From: Jitendra Yejare Date: Mon, 18 Sep 2023 12:49:26 +0530 Subject: [PATCH 232/586] Removing Auto Release WF from non-master branch (#12625) Removing Release Dispatch WF from non-master branch --- .github/workflows/dispatch_release.yml | 31 -------------------------- 1 file changed, 31 deletions(-) delete mode 100644 .github/workflows/dispatch_release.yml diff --git a/.github/workflows/dispatch_release.yml b/.github/workflows/dispatch_release.yml deleted file mode 100644 index 51b6b90919a..00000000000 --- a/.github/workflows/dispatch_release.yml +++ /dev/null @@ -1,31 +0,0 @@ -### The auto release workflow triggered through dispatch request from CI -name: auto-release - -# Run on workflow dispatch from CI -on: - workflow_dispatch: - inputs: - tag_name: - type: string - description: Name of the tag - -jobs: - auto-tag-and-release: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Git User setup - run: "git config --local user.email Satellite-QE.satqe.com && git config --local user.name Satellite-QE" - - - name: Tag latest commit - run: "git tag -a ${{ github.event.inputs.tag_name }} -m 'Tagged By SatelliteQE Automation User'" - - - name: Push the tag to the upstream - run: "git push ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git --tags" - - - name: create a new release from the tag - env: - credentials: ${{ secrets.GH_TOKEN }} - run: "curl -L -X POST -H \"Authorization: Bearer ${{ secrets.SATQE_GH_TOKEN }}\" ${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases -d '{\"tag_name\": \"${{ github.event.inputs.tag_name }}\", \"target_commitish\":\"master\", \"name\":\"${{ github.event.inputs.tag_name }}\", \"draft\":false, \"prerelease\":true, \"generate_release_notes\": true}'" From a038b3d0321c9ede2db06a93ffffe468eccce859 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 18 Sep 2023 08:54:29 -0400 Subject: [PATCH 233/586] [6.14.z] UI Utils module depreciation and new ui_factory module (#12641) UI Utils module depreciation and new ui_factory module (#12225) Depreciation of UI Utils (cherry picked from commit ab2102352e0137f7148448c764587f71df7ce1d3) Co-authored-by: Jitendra Yejare --- robottelo/host_helpers/satellite_mixins.py | 6 +++ robottelo/host_helpers/ui_factory.py | 59 ++++++++++++++++++++++ robottelo/ui/__init__.py | 0 robottelo/ui/utils.py | 47 ----------------- tests/foreman/ui/test_reporttemplates.py | 16 +++--- 5 files changed, 75 insertions(+), 53 deletions(-) create mode 100644 robottelo/host_helpers/ui_factory.py delete mode 100644 robottelo/ui/__init__.py delete mode 100644 robottelo/ui/utils.py diff --git a/robottelo/host_helpers/satellite_mixins.py b/robottelo/host_helpers/satellite_mixins.py index 8e718f2e71d..bd7bedc7d57 100644 --- a/robottelo/host_helpers/satellite_mixins.py +++ b/robottelo/host_helpers/satellite_mixins.py @@ -3,6 +3,7 @@ import os import random import re +from functools import cache import requests @@ -15,6 +16,7 @@ from robottelo.constants import PUPPET_SATELLITE_INSTALLER from robottelo.host_helpers.api_factory import APIFactory from robottelo.host_helpers.cli_factory import CLIFactory +from robottelo.host_helpers.ui_factory import UIFactory from robottelo.logging import logger from robottelo.utils.installer import InstallerCommand from robottelo.utils.manifest import clone @@ -352,3 +354,7 @@ def api_factory(self): if not getattr(self, '_api_factory', None): self._api_factory = APIFactory(self) return self._api_factory + + @cache + def ui_factory(self, session): + return UIFactory(self, session=session) diff --git a/robottelo/host_helpers/ui_factory.py b/robottelo/host_helpers/ui_factory.py new file mode 100644 index 00000000000..334b4986b24 --- /dev/null +++ b/robottelo/host_helpers/ui_factory.py @@ -0,0 +1,59 @@ +""" +It is not meant to be used directly, but as part of a robottelo.hosts.Satellite instance +Need to pass the existing session object to the ui_factory method as a parameter +example: my_satellite.ui_factory(session).ui_method() +""" +from fauxfactory import gen_string + +from robottelo.constants import DEFAULT_CV +from robottelo.constants import ENVIRONMENT + + +class UIFactory: + """This class is part of a mixin and not to be used directly. See robottelo.hosts.Satellite""" + + def __init__(self, satellite, session=None): + self._satellite = satellite + self._session = session + + def create_fake_host( + self, + host, + interface_id=gen_string('alpha'), + global_parameters=None, + host_parameters=None, + extra_values=None, + new_host_details=False, + ): + if extra_values is None: + extra_values = {} + os_name = f'{host.operatingsystem.name} {host.operatingsystem.major}' + name = host.name if host.name is not None else gen_string('alpha').lower() + values = { + 'host.name': name, + 'host.organization': host.organization.name, + 'host.location': host.location.name, + 'host.lce': ENVIRONMENT, + 'host.content_view': DEFAULT_CV, + 'operating_system.architecture': host.architecture.name, + 'operating_system.operating_system': os_name, + 'operating_system.media_type': 'All Media', + 'operating_system.media': host.medium.name, + 'operating_system.ptable': host.ptable.name, + 'operating_system.root_password': host.root_pass, + 'interfaces.interface.interface_type': 'Interface', + 'interfaces.interface.device_identifier': interface_id, + 'interfaces.interface.mac': host.mac, + 'interfaces.interface.domain': host.domain.name, + 'interfaces.interface.primary': True, + 'interfaces.interface.interface_additional_data.virtual_nic': False, + 'parameters.global_params': global_parameters, + 'parameters.host_params': host_parameters, + 'additional_information.comment': 'Host with fake data', + } + values.update(extra_values) + if new_host_details: + self._session.host_new.create(values) + else: + self._session.host.create(values) + return f'{name}.{host.domain.name}' diff --git a/robottelo/ui/__init__.py b/robottelo/ui/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/robottelo/ui/utils.py b/robottelo/ui/utils.py deleted file mode 100644 index d22bb25dae5..00000000000 --- a/robottelo/ui/utils.py +++ /dev/null @@ -1,47 +0,0 @@ -from fauxfactory import gen_string - -from robottelo.constants import DEFAULT_CV -from robottelo.constants import ENVIRONMENT - - -def create_fake_host( - session, - host, - interface_id=gen_string('alpha'), - global_parameters=None, - host_parameters=None, - extra_values=None, - new_host_details=False, -): - if extra_values is None: - extra_values = {} - os_name = f'{host.operatingsystem.name} {host.operatingsystem.major}' - name = host.name if host.name is not None else gen_string('alpha').lower() - values = { - 'host.name': name, - 'host.organization': host.organization.name, - 'host.location': host.location.name, - 'host.lce': ENVIRONMENT, - 'host.content_view': DEFAULT_CV, - 'operating_system.architecture': host.architecture.name, - 'operating_system.operating_system': os_name, - 'operating_system.media_type': 'All Media', - 'operating_system.media': host.medium.name, - 'operating_system.ptable': host.ptable.name, - 'operating_system.root_password': host.root_pass, - 'interfaces.interface.interface_type': 'Interface', - 'interfaces.interface.device_identifier': interface_id, - 'interfaces.interface.mac': host.mac, - 'interfaces.interface.domain': host.domain.name, - 'interfaces.interface.primary': True, - 'interfaces.interface.interface_additional_data.virtual_nic': False, - 'parameters.global_params': global_parameters, - 'parameters.host_params': host_parameters, - 'additional_information.comment': 'Host with fake data', - } - values.update(extra_values) - if new_host_details: - session.host_new.create(values) - else: - session.host.create(values) - return f'{name}.{host.domain.name}' diff --git a/tests/foreman/ui/test_reporttemplates.py b/tests/foreman/ui/test_reporttemplates.py index f8eed10e58f..ed4c84515f7 100644 --- a/tests/foreman/ui/test_reporttemplates.py +++ b/tests/foreman/ui/test_reporttemplates.py @@ -35,7 +35,6 @@ from robottelo.constants import PRDS from robottelo.constants import REPOS from robottelo.constants import REPOSET -from robottelo.ui.utils import create_fake_host from robottelo.utils.datafactory import gen_string @@ -238,7 +237,7 @@ def test_positive_end_to_end(session, module_org, module_location): @pytest.mark.upgrade @pytest.mark.tier2 -def test_positive_generate_registered_hosts_report(session, module_org, module_location): +def test_positive_generate_registered_hosts_report(target_sat, module_org, module_location): """Use provided Host - Registered Content Hosts report for testing :id: b44d4cd8-a78e-47cf-9993-0bb871ac2c96 @@ -252,17 +251,22 @@ def test_positive_generate_registered_hosts_report(session, module_org, module_l """ # generate Host Status report os_name = 'comma,' + gen_string('alpha') - os = entities.OperatingSystem(name=os_name).create() + os = target_sat.api.OperatingSystem(name=os_name).create() host_cnt = 3 host_templates = [ - entities.Host(organization=module_org, location=module_location, operatingsystem=os) + target_sat.api.Host(organization=module_org, location=module_location, operatingsystem=os) for i in range(host_cnt) ] for host_template in host_templates: host_template.create_missing() - with session: + with target_sat.ui_session() as session: + session.organization.select(module_org.name) + session.location.select(module_location.name) # create multiple hosts to test filtering - host_names = [create_fake_host(session, host_template) for host_template in host_templates] + host_names = [ + target_sat.ui_factory(session).create_fake_host(host_template) + for host_template in host_templates + ] host_name = host_names[1] # pick some that is not first and is not last file_path = session.reporttemplate.generate( 'Host - Registered Content Hosts', values={'hosts_filter': host_name} From 0e52c687dc61370f75b8216c57d2348397c46e80 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 20 Sep 2023 04:13:33 -0400 Subject: [PATCH 234/586] [6.14.z] Fix remotedb backup expected files (#12654) --- tests/foreman/maintain/test_backup_restore.py | 57 ++++++++----------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/tests/foreman/maintain/test_backup_restore.py b/tests/foreman/maintain/test_backup_restore.py index 0743684b658..e2e5e909c77 100644 --- a/tests/foreman/maintain/test_backup_restore.py +++ b/tests/foreman/maintain/test_backup_restore.py @@ -33,10 +33,10 @@ BASIC_FILES = {"config_files.tar.gz", ".config.snar", "metadata.yml"} +OFFLINE_FILES = {"pgsql_data.tar.gz", ".postgres.snar"} | BASIC_FILES +ONLINE_SAT_FILES = {"candlepin.dump", "foreman.dump", "pulpcore.dump"} | BASIC_FILES +ONLINE_CAPS_FILES = {"pulpcore.dump"} | BASIC_FILES CONTENT_FILES = {"pulp_data.tar", ".pulp.snar"} -OFFLINE_FILES = {"pgsql_data.tar.gz", ".postgres.snar"} -ONLINE_SAT_FILES = {"candlepin.dump", "foreman.dump", "pulpcore.dump"} -ONLINE_CAPS_FILES = {"pulpcore.dump"} NODIR_MSG = "ERROR: parameter 'BACKUP_DIR': no value provided" @@ -46,22 +46,18 @@ assert_msg = "Some required backup files are missing" -def get_exp_files(sat_maintain, backup_type): +def get_exp_files(sat_maintain, backup_type, skip_pulp=False): if type(sat_maintain) is Satellite: - if sat_maintain.is_remote_db(): - expected_files = BASIC_FILES | ONLINE_SAT_FILES - else: - expected_files = ( - BASIC_FILES | OFFLINE_FILES - if backup_type == 'offline' - else BASIC_FILES | ONLINE_SAT_FILES - ) - else: + # for remote db you get always online backup regardless specified backup type expected_files = ( - BASIC_FILES | OFFLINE_FILES - if backup_type == 'offline' - else BASIC_FILES | ONLINE_CAPS_FILES + ONLINE_SAT_FILES + if backup_type == 'online' or sat_maintain.is_remote_db() + else OFFLINE_FILES ) + else: + expected_files = ONLINE_CAPS_FILES if backup_type == 'online' else OFFLINE_FILES + if not skip_pulp: + expected_files = expected_files | CONTENT_FILES return expected_files @@ -101,7 +97,7 @@ def test_positive_backup_preserve_directory( files = [i for i in files if not re.compile(r'^\.*$').search(i)] expected_files = get_exp_files(sat_maintain, backup_type) - assert set(files).issuperset(expected_files | CONTENT_FILES), assert_msg + assert set(files).issuperset(expected_files), assert_msg @pytest.mark.include_capsule @@ -149,7 +145,7 @@ def test_positive_backup_split_pulp_tar( files = [i for i in files if not re.compile(r'^\.*$').search(i)] expected_files = get_exp_files(sat_maintain, backup_type) - assert set(files).issuperset(expected_files | CONTENT_FILES), assert_msg + assert set(files).issuperset(expected_files), assert_msg # Check the split works result = sat_maintain.execute(f'du {backup_dir}/pulp_data.tar') @@ -193,7 +189,7 @@ def test_positive_backup_capsule_features( files = [i for i in files if not re.compile(r'^\.*$').search(i)] expected_files = get_exp_files(sat_maintain, backup_type) - assert set(files).issuperset(expected_files | CONTENT_FILES), assert_msg + assert set(files).issuperset(expected_files), assert_msg @pytest.mark.include_capsule @@ -276,12 +272,12 @@ def test_positive_backup_offline_logical(sat_maintain, setup_backup_tests, modul if type(sat_maintain) is Satellite: if sat_maintain.is_remote_db(): - expected_files = BASIC_FILES | ONLINE_SAT_FILES + expected_files = ONLINE_SAT_FILES | CONTENT_FILES else: - expected_files = BASIC_FILES | OFFLINE_FILES | ONLINE_SAT_FILES + expected_files = OFFLINE_FILES | ONLINE_SAT_FILES | CONTENT_FILES else: - expected_files = BASIC_FILES | OFFLINE_FILES | ONLINE_CAPS_FILES - assert set(files).issuperset(expected_files | CONTENT_FILES), assert_msg + expected_files = OFFLINE_FILES | ONLINE_CAPS_FILES | CONTENT_FILES + assert set(files).issuperset(expected_files), assert_msg @pytest.mark.include_capsule @@ -448,7 +444,7 @@ def test_positive_puppet_backup_restore( files = [i for i in files if not re.compile(r'^\.*$').search(i)] expected_files = get_exp_files(sat_maintain, backup_type) - assert set(files).issuperset(expected_files | CONTENT_FILES), assert_msg + assert set(files).issuperset(expected_files), assert_msg # Run restore sat_maintain.execute('rm -rf /var/lib/pulp/media/artifact') @@ -533,11 +529,8 @@ def test_positive_backup_restore( files = sat_maintain.execute(f'ls -a {backup_dir}').stdout.split('\n') files = [i for i in files if not re.compile(r'^\.*$').search(i)] - expected_files = get_exp_files(sat_maintain, backup_type) - if not skip_pulp: - assert set(files).issuperset(expected_files | CONTENT_FILES), assert_msg - else: - assert set(files).issuperset(expected_files), assert_msg + expected_files = get_exp_files(sat_maintain, backup_type, skip_pulp) + assert set(files).issuperset(expected_files), assert_msg # Run restore if not skip_pulp: @@ -639,10 +632,8 @@ def test_positive_backup_restore_incremental( files = sat_maintain.execute(f'ls -a {inc_backup_dir}').stdout.split('\n') files = [i for i in files if not re.compile(r'^\.*$').search(i)] - expected_files = ( - BASIC_FILES | OFFLINE_FILES if backup_type == 'offline' else BASIC_FILES | ONLINE_SAT_FILES - ) - assert set(files).issuperset(expected_files | CONTENT_FILES), assert_msg + expected_files = get_exp_files(sat_maintain, backup_type) + assert set(files).issuperset(expected_files), assert_msg # restore initial backup and check system health result = sat_maintain.cli.Restore.run( From 8ca619deab8f2e5f51a4bbde46ef6fdb2d4b78d6 Mon Sep 17 00:00:00 2001 From: Gaurav Talreja Date: Fri, 15 Sep 2023 21:11:11 +0530 Subject: [PATCH 235/586] Update leapp component name to match ohsnap Signed-off-by: Gaurav Talreja (cherry picked from commit 3457dadebd0deb8a7ec684ecb10ffd5cf2e45a05) --- testimony.yaml | 2 +- tests/foreman/cli/test_leapp_client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/testimony.yaml b/testimony.yaml index 047f3900ebf..46d0b289f43 100644 --- a/testimony.yaml +++ b/testimony.yaml @@ -68,7 +68,7 @@ CaseComponent: - katello-agent - katello-tracer - LDAP - - LeappIntegration + - Leappintegration - LifecycleEnvironments - LocalizationInternationalization - Logging diff --git a/tests/foreman/cli/test_leapp_client.py b/tests/foreman/cli/test_leapp_client.py index 77e4a7e0486..92e295303d7 100644 --- a/tests/foreman/cli/test_leapp_client.py +++ b/tests/foreman/cli/test_leapp_client.py @@ -4,7 +4,7 @@ :CaseLevel: Integration -:CaseComponent: LeappIntegration +:CaseComponent: Leappintegration :Team: Rocket From 554722826ca152c90692bd39554367ea9a15d921 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Sep 2023 00:03:23 -0400 Subject: [PATCH 236/586] Bump cryptography from 41.0.3 to 41.0.4 (#12657) (cherry picked from commit 9c46a9819755b8c1a52cb91bee645808e2d47d5b) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 06d0a175004..e4924d8b2ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ betelgeuse==1.10.0 broker[docker]==0.4.0 -cryptography==41.0.3 +cryptography==41.0.4 deepdiff==6.5.0 dynaconf[vault]==3.2.3 fauxfactory==3.1.0 From 240fe5664b372f3fc3f86f15beb4c9090b2e08ae Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 21 Sep 2023 03:13:48 -0400 Subject: [PATCH 237/586] [6.14.z] Switch ownership of Capsule to Team Platform (#12678) Switch ownership of Capsule to Team Platform (#12675) (cherry picked from commit 60574f1f3cff437ea1316acd73168b038564b290) Co-authored-by: Griffin Sullivan <48397354+Griffin-Sullivan@users.noreply.github.com> --- tests/foreman/api/test_capsule.py | 2 +- tests/foreman/cli/test_capsule.py | 2 +- tests/foreman/destructive/test_capsule.py | 2 +- tests/foreman/destructive/test_capsule_loadbalancer.py | 2 +- tests/upgrades/test_capsule.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/foreman/api/test_capsule.py b/tests/foreman/api/test_capsule.py index 1d86ecf2300..0b15f867ae3 100644 --- a/tests/foreman/api/test_capsule.py +++ b/tests/foreman/api/test_capsule.py @@ -8,7 +8,7 @@ :CaseComponent: Capsule -:Team: Endeavour +:Team: Platform :TestType: Functional diff --git a/tests/foreman/cli/test_capsule.py b/tests/foreman/cli/test_capsule.py index d93d2bcbf7f..8dee2e6a3dc 100644 --- a/tests/foreman/cli/test_capsule.py +++ b/tests/foreman/cli/test_capsule.py @@ -8,7 +8,7 @@ :CaseComponent: Capsule -:Team: Endeavour +:Team: Platform :TestType: Functional diff --git a/tests/foreman/destructive/test_capsule.py b/tests/foreman/destructive/test_capsule.py index cc0b23b19eb..5739054af43 100644 --- a/tests/foreman/destructive/test_capsule.py +++ b/tests/foreman/destructive/test_capsule.py @@ -8,7 +8,7 @@ :CaseComponent: Capsule -:Team: Endeavour +:Team: Platform :TestType: Functional diff --git a/tests/foreman/destructive/test_capsule_loadbalancer.py b/tests/foreman/destructive/test_capsule_loadbalancer.py index 5d94b3190af..49219359df2 100644 --- a/tests/foreman/destructive/test_capsule_loadbalancer.py +++ b/tests/foreman/destructive/test_capsule_loadbalancer.py @@ -8,7 +8,7 @@ :CaseComponent: Capsule -:Team: Endeavour +:Team: Platform :TestType: Functional diff --git a/tests/upgrades/test_capsule.py b/tests/upgrades/test_capsule.py index fb6fc994ca4..19f8ad87b9e 100644 --- a/tests/upgrades/test_capsule.py +++ b/tests/upgrades/test_capsule.py @@ -8,7 +8,7 @@ :CaseComponent: Capsule -:Team: Endeavour +:Team: Platform :TestType: Functional From 227faa47dd73867f14113d8ce2c5bb33eeff9f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Gajdu=C5=A1ek?= Date: Thu, 21 Sep 2023 15:56:20 +0200 Subject: [PATCH 238/586] Replace flake8 with Ruff (#12621) * Replace flake8 with Ruff * Replace flake8 with Ruff * Fix UP038 - non-pep604-isinstance * Fix F401 - remove unused imports * Fix F601 - Remove repeated dictionary key * Fix not-in-test (E713) (cherry picked from commit 187cca12259987d90c21407be188ead7e386b077) --- .flake8 | 2 -- .pre-commit-config.yaml | 13 ++++-------- pyproject.toml | 23 +++++++++++++++++++++ robottelo/constants/__init__.py | 1 - robottelo/host_helpers/satellite_mixins.py | 4 ++-- tests/foreman/api/test_multiple_paths.py | 2 +- tests/foreman/cli/test_webhook.py | 2 +- tests/foreman/endtoend/test_api_endtoend.py | 2 -- tests/foreman/ui/test_bookmarks.py | 2 +- tests/robottelo/test_datafactory.py | 2 +- tests/upgrades/conftest.py | 4 ++-- 11 files changed, 35 insertions(+), 22 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 7da1f9608ee..00000000000 --- a/.flake8 +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -max-line-length = 100 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7626efe6a69..fb023936d75 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,22 +10,17 @@ repos: hooks: - id: trailing-whitespace exclude: tests/foreman/data/ - - id: end-of-file-fixer - id: check-yaml - id: debug-statements -- repo: https://github.com/asottile/pyupgrade - rev: v3.3.0 - hooks: - - id: pyupgrade - args: [--py38-plus] - repo: https://github.com/psf/black rev: 22.10.0 hooks: - id: black -- repo: https://github.com/pycqa/flake8 - rev: 6.0.0 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.277 hooks: - - id: flake8 + - id: ruff + args: [--fix, --exit-non-zero-on-fix] - repo: local hooks: - id: fix-uuids diff --git a/pyproject.toml b/pyproject.toml index 2a45bb81ace..ae675eb552d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,29 @@ exclude = ''' )/ ''' +[tool.ruff] +target-version = "py311" +fixable = ["ALL"] + +select = [ + # "C90", # mccabe + "E", # pycodestyle + "F", # flake8 + # "Q", # flake8-quotes + "UP", # pyupgrade + "W", # pycodestyle +] + +# Allow lines to be as long as 100 characters. +line-length = 100 + + +[tool.ruff.flake8-quotes] +inline-quotes = "single" + +[tool.ruff.mccabe] +max-complexity = 20 + [tool.pytest.ini_options] junit_logging = 'all' addopts = '--show-capture=no' diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index 524ec39d866..c323d81f1b2 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -316,7 +316,6 @@ class Colored(Box): 'rhel7_extra': 'Red Hat Enterprise Linux 7 Server - Extras (RPMs)', 'rhel7_optional': 'Red Hat Enterprise Linux 7 Server - Optional (RPMs)', 'rhel7_sup': 'Red Hat Enterprise Linux 7 Server - Supplementary (RPMs)', - 'rhst7_610': 'Red Hat Satellite Tools 6.10 (for RHEL 7 Server) (RPMs)', } SM_OVERALL_STATUS = { diff --git a/robottelo/host_helpers/satellite_mixins.py b/robottelo/host_helpers/satellite_mixins.py index bd7bedc7d57..fff44e9f6a9 100644 --- a/robottelo/host_helpers/satellite_mixins.py +++ b/robottelo/host_helpers/satellite_mixins.py @@ -143,7 +143,7 @@ def upload_manifest(self, org_id, manifest=None, interface='API', timeout=None): :returns: the manifest upload result """ - if not isinstance(manifest, (bytes, io.BytesIO)): + if not isinstance(manifest, bytes | io.BytesIO): if manifest.content is None: manifest = clone() if timeout is None: @@ -161,7 +161,7 @@ def upload_manifest(self, org_id, manifest=None, interface='API', timeout=None): {'file': manifest.filename, 'organization-id': org_id}, timeout=timeout ) else: - if not isinstance(manifest, (bytes, io.BytesIO)): + if not isinstance(manifest, bytes | io.BytesIO): manifest = manifest.content result = self.api.Subscription().upload( data={'organization_id': org_id}, files={'content': manifest} diff --git a/tests/foreman/api/test_multiple_paths.py b/tests/foreman/api/test_multiple_paths.py index bf975ce7f58..f2966bfa018 100644 --- a/tests/foreman/api/test_multiple_paths.py +++ b/tests/foreman/api/test_multiple_paths.py @@ -92,7 +92,7 @@ def _get_readable_attributes(entity): for field_name in list(attributes.keys()): if isinstance( entity.get_fields()[field_name], - (entity_fields.OneToOneField, entity_fields.OneToManyField), + entity_fields.OneToOneField | entity_fields.OneToManyField, ): del attributes[field_name] diff --git a/tests/foreman/cli/test_webhook.py b/tests/foreman/cli/test_webhook.py index 110b8a08860..002d1b360ec 100644 --- a/tests/foreman/cli/test_webhook.py +++ b/tests/foreman/cli/test_webhook.py @@ -57,7 +57,7 @@ def _create_webhook(org, loc, options=None): def assert_created(options, hook): for option in options.items(): - if not option[0] in ['event', 'organization-id', 'location-id']: + if option[0] not in ['event', 'organization-id', 'location-id']: assert hook[option[0]] == option[1] diff --git a/tests/foreman/endtoend/test_api_endtoend.py b/tests/foreman/endtoend/test_api_endtoend.py index e29032e918f..f9471e1bbb7 100644 --- a/tests/foreman/endtoend/test_api_endtoend.py +++ b/tests/foreman/endtoend/test_api_endtoend.py @@ -17,7 +17,6 @@ :Upstream: No """ import http -import random from collections import defaultdict from pprint import pformat @@ -35,7 +34,6 @@ from robottelo.config import user_nailgun_config from robottelo.constants.repos import CUSTOM_RPM_REPO from robottelo.utils.issue_handlers import is_open -from robottelo.utils.manifest import clone API_PATHS = { diff --git a/tests/foreman/ui/test_bookmarks.py b/tests/foreman/ui/test_bookmarks.py index f33483a0c42..cb0201a814b 100644 --- a/tests/foreman/ui/test_bookmarks.py +++ b/tests/foreman/ui/test_bookmarks.py @@ -42,7 +42,7 @@ def ui_entity(module_org, module_location, request): # Skip the entities, which can't be tested ATM (not implemented in # airgun or have open BZs) skip = entity.get('skip_for_ui') - if isinstance(skip, (tuple, list)): + if isinstance(skip, tuple | list): open_issues = {issue for issue in skip if is_open(issue)} pytest.skip(f'There is/are an open issue(s) {open_issues} with entity {entity_name}') # entities with 1 organization and location diff --git a/tests/robottelo/test_datafactory.py b/tests/robottelo/test_datafactory.py index 7d5a6c12b7e..04e081a9b39 100644 --- a/tests/robottelo/test_datafactory.py +++ b/tests/robottelo/test_datafactory.py @@ -127,7 +127,7 @@ def test_return_type(self): ): assert isinstance(item, str) for item in datafactory.invalid_id_list(): - if not (isinstance(item, (str, int)) or item is None): + if not (isinstance(item, str | int) or item is None): pytest.fail('Unexpected data type') diff --git a/tests/upgrades/conftest.py b/tests/upgrades/conftest.py index 16d021445ed..25d3c7d57a1 100644 --- a/tests/upgrades/conftest.py +++ b/tests/upgrades/conftest.py @@ -230,7 +230,7 @@ def test_something_post_upgrade(pre_upgrade_data): dependant_on_functions = [] for marker in request.node.iter_markers(POST_UPGRADE_MARK): depend_on = marker.kwargs.get('depend_on') - if isinstance(depend_on, (list, tuple)): + if isinstance(depend_on, list | tuple): dependant_on_functions.extend(depend_on) elif depend_on is not None: dependant_on_functions.append(depend_on) @@ -373,7 +373,7 @@ def pytest_collection_modifyitems(items, config): dependant_on_functions = [] for marker in item.iter_markers(POST_UPGRADE_MARK): depend_on = marker.kwargs.get('depend_on') - if isinstance(depend_on, (list, tuple)): + if isinstance(depend_on, list | tuple): dependant_on_functions.extend(depend_on) elif depend_on is not None: dependant_on_functions.append(depend_on) From 10ea708a0c5bf6cf4b2505e6c2f99efed83cf49a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 21 Sep 2023 15:34:54 -0400 Subject: [PATCH 239/586] [6.14.z] Fix end-to-end and bulke2e (#12694) Fix end-to-end and bulke2e (#12413) * Fix end-to-end and bulke2e * Move repo enable to commonly used fixture (cherry picked from commit 144877b9f9f6a7fd532786786c893694dd270e16) Co-authored-by: Samuel Bible --- tests/foreman/ui/test_contenthost.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/foreman/ui/test_contenthost.py b/tests/foreman/ui/test_contenthost.py index d2bd6ded4d3..36ff6823588 100644 --- a/tests/foreman/ui/test_contenthost.py +++ b/tests/foreman/ui/test_contenthost.py @@ -78,6 +78,7 @@ def vm(module_repos_collection_with_manifest, rhel7_contenthost, target_sat): """Virtual machine registered in satellite""" module_repos_collection_with_manifest.setup_virtual_machine(rhel7_contenthost) rhel7_contenthost.add_rex_key(target_sat) + rhel7_contenthost.run(r'subscription-manager repos --enable \*') yield rhel7_contenthost From 4d632fca1edabc5f59fb18003ff19e39ff1aca22 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 22 Sep 2023 06:02:06 -0400 Subject: [PATCH 240/586] [6.14.z] update Insights test to use new host page (#12698) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit update Insights test to use new host page (#12582) * update Insights test to use new host page * update whole test to use `host_new` (cherry picked from commit b23414be98f81f55f4a2b9eed9d4c2367ac7840a) Co-authored-by: Matyáš Strelec --- tests/foreman/ui/test_rhcloud_insights.py | 46 +++++++++++------------ 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/tests/foreman/ui/test_rhcloud_insights.py b/tests/foreman/ui/test_rhcloud_insights.py index 861450011b4..aa7351767b5 100644 --- a/tests/foreman/ui/test_rhcloud_insights.py +++ b/tests/foreman/ui/test_rhcloud_insights.py @@ -277,29 +277,28 @@ def test_host_details_page( handle_exception=True, ) # Verify Insights status of host. - result = session.host.host_status(rhel_insights_vm.hostname) - assert 'Insights: Reporting' in result - assert 'Inventory: Successfully uploaded to your RH cloud inventory' in result - result = session.host.search(rhel_insights_vm.hostname)[0] + result = session.host_new.get_host_statuses(rhel_insights_vm.hostname) + assert result['Insights']['Status'] == 'Reporting' + assert result['Inventory']['Status'] == 'Successfully uploaded to your RH cloud inventory' + result = session.host_new.search(rhel_insights_vm.hostname)[0] assert result['Name'] == rhel_insights_vm.hostname assert int(result['Recommendations']) > 0 - values = session.host.get_details(rhel_insights_vm.hostname) - # Note: Reading host properties adds 'clear' to original value. - assert ( - values['properties']['properties_table']['Inventory'] - == 'Successfully uploaded to your RH cloud inventory clear' - ) + values = session.host_new.get_host_statuses(rhel_insights_vm.hostname) + assert values['Inventory']['Status'] == 'Successfully uploaded to your RH cloud inventory' # Read the recommendations listed in Insights tab present on host details page - insights_recommendations = session.host_new.insights_tab(rhel_insights_vm.hostname) + insights_recommendations = session.host_new.get_insights(rhel_insights_vm.hostname)[ + 'recommendations_table' + ] for recommendation in insights_recommendations: - if recommendation['name'] == DNF_RECOMMENDATION: - assert recommendation['label'] == 'Moderate' - assert DNF_RECOMMENDATION in recommendation['text'] + if recommendation['Recommendation'] == DNF_RECOMMENDATION: + assert recommendation['Total risk'] == 'Moderate' + assert DNF_RECOMMENDATION in recommendation['Recommendation'] assert len(insights_recommendations) == int(result['Recommendations']) # Test Recommendation button present on host details page - recommendations = session.host.read_insights_recommendations(rhel_insights_vm.hostname) + recommendations = session.host_new.get_insights(rhel_insights_vm.hostname)[ + 'recommendations_table' + ] assert len(recommendations), 'No recommendations were found' - assert recommendations[0]['Hostname'] == rhel_insights_vm.hostname assert int(result['Recommendations']) == len(recommendations) # Delete host rhel_insights_vm.nailgun_host.delete() @@ -356,7 +355,7 @@ def test_insights_registration_with_capsule( session.organization.select(org_name=org.name) session.location.select(loc_name=DEFAULT_LOC) # Generate host registration command - cmd = session.host.get_register_command( + cmd = session.host_new.get_register_command( { 'general.operating_system': default_os.title, 'general.orgnization': org.name, @@ -371,8 +370,8 @@ def test_insights_registration_with_capsule( rhel_contenthost.execute(cmd) assert rhel_contenthost.subscribed assert rhel_contenthost.execute('insights-client --test-connection').status == 0 - values = session.host.get_details(rhel_contenthost.hostname) - assert values['properties']['properties_table']['Insights'] == 'Reporting clear' + values = session.host_new.get_host_statuses(rhel_contenthost.hostname) + assert values['Insights']['Status'] == 'Reporting' # Clean insights status result = module_target_sat.run( f'foreman-rake rh_cloud_insights:clean_statuses SEARCH="{rhel_contenthost.hostname}"' @@ -382,13 +381,12 @@ def test_insights_registration_with_capsule( # Workaround for not reading old data. session.browser.refresh() # Verify that Insights status is cleared. - values = session.host.get_details(rhel_contenthost.hostname) - with pytest.raises(KeyError): - values['properties']['properties_table']['Insights'] + values = session.host_new.get_host_statuses(rhel_contenthost.hostname) + assert values['Insights']['Status'] == 'N/A' result = rhel_contenthost.run('insights-client') assert result.status == 0 # Workaround for not reading old data. session.browser.refresh() # Verify that Insights status again. - values = session.host.get_details(rhel_contenthost.hostname) - assert values['properties']['properties_table']['Insights'] == 'Reporting clear' + values = session.host_new.get_host_statuses(rhel_contenthost.hostname) + assert values['Insights']['Status'] == 'Reporting' From 67a39183fb262153663a006992e9694f12c986e8 Mon Sep 17 00:00:00 2001 From: Jake Callahan Date: Sat, 23 Sep 2023 07:02:16 -0400 Subject: [PATCH 241/586] [6.14.z] Switch import formatting to use Ruff (#12711) Switch import formatting to use Ruff This now drops the old standard of one import per line in favor of a less noisy standard (isort). --- .pre-commit-config.yaml | 4 - pyproject.toml | 13 ++- pytest_fixtures/component/acs.py | 3 +- pytest_fixtures/component/computeprofile.py | 2 +- pytest_fixtures/component/contentview.py | 2 +- pytest_fixtures/component/domain.py | 2 +- pytest_fixtures/component/host.py | 8 +- pytest_fixtures/component/hostgroup.py | 2 +- pytest_fixtures/component/katello_agent.py | 2 +- .../component/katello_certs_check.py | 2 +- pytest_fixtures/component/maintain.py | 4 +- pytest_fixtures/component/os.py | 2 +- pytest_fixtures/component/oscap.py | 9 +- pytest_fixtures/component/provision_azure.py | 16 ++-- pytest_fixtures/component/provision_gce.py | 14 +-- pytest_fixtures/component/provision_pxe.py | 2 +- .../component/provisioning_template.py | 6 +- pytest_fixtures/component/repository.py | 7 +- pytest_fixtures/component/satellite_auth.py | 21 ++--- pytest_fixtures/component/subnet.py | 2 +- pytest_fixtures/component/taxonomy.py | 5 +- pytest_fixtures/component/templatesync.py | 2 +- pytest_fixtures/component/user.py | 2 +- pytest_fixtures/component/user_role.py | 5 +- pytest_fixtures/core/broker.py | 6 +- pytest_fixtures/core/contenthosts.py | 5 +- pytest_fixtures/core/reporting.py | 6 +- pytest_fixtures/core/sat_cap_factory.py | 18 ++-- pytest_fixtures/core/ui.py | 2 +- pytest_fixtures/core/upgrade.py | 2 +- pytest_fixtures/core/xdist.py | 6 +- pytest_plugins/factory_collection.py | 3 +- pytest_plugins/fixture_markers.py | 4 +- pytest_plugins/issue_handlers.py | 17 ++-- pytest_plugins/logging_hooks.py | 17 ++-- pytest_plugins/marker_deselection.py | 1 - pytest_plugins/requirements/req_updater.py | 2 +- pytest_plugins/settings_skip.py | 3 +- pytest_plugins/upgrade/scenario_workers.py | 5 +- robottelo/cli/contentview.py | 3 +- robottelo/cli/factory.py | 32 +++---- robottelo/cli/report_template.py | 6 +- robottelo/cli/template_input.py | 3 +- robottelo/cli/webhook.py | 6 +- robottelo/config/__init__.py | 6 +- robottelo/config/validators.py | 4 +- robottelo/host_helpers/__init__.py | 23 ++--- robottelo/host_helpers/api_factory.py | 18 ++-- robottelo/host_helpers/capsule_mixins.py | 5 +- robottelo/host_helpers/cli_factory.py | 23 ++--- robottelo/host_helpers/contenthost_mixins.py | 8 +- robottelo/host_helpers/repository_mixins.py | 14 +-- robottelo/host_helpers/satellite_mixins.py | 12 +-- robottelo/host_helpers/ui_factory.py | 3 +- robottelo/hosts.py | 78 ++++++++--------- robottelo/logging.py | 5 +- robottelo/utils/__init__.py | 2 +- robottelo/utils/datafactory.py | 11 +-- robottelo/utils/decorators/__init__.py | 1 - robottelo/utils/decorators/func_locker.py | 2 +- .../utils/decorators/func_shared/shared.py | 9 +- robottelo/utils/io/__init__.py | 2 +- robottelo/utils/issue_handlers/__init__.py | 1 - robottelo/utils/issue_handlers/bugzilla.py | 13 +-- robottelo/utils/manifest.py | 5 +- robottelo/utils/ohsnap.py | 5 +- robottelo/utils/report_portal/portal.py | 4 +- robottelo/utils/virtwho.py | 6 +- scripts/config_helpers.py | 2 +- scripts/graph_entities.py | 3 +- scripts/tokenize_customer_scenario.py | 5 +- scripts/vault_login.py | 3 +- setup.py | 3 +- tests/foreman/api/test_acs.py | 5 +- tests/foreman/api/test_activationkey.py | 23 +++-- tests/foreman/api/test_ansible.py | 2 +- tests/foreman/api/test_architecture.py | 10 ++- tests/foreman/api/test_audit.py | 2 +- tests/foreman/api/test_bookmarks.py | 6 +- tests/foreman/api/test_capsule.py | 3 +- tests/foreman/api/test_capsulecontent.py | 15 ++-- tests/foreman/api/test_classparameters.py | 7 +- tests/foreman/api/test_computeprofile.py | 10 ++- .../api/test_computeresource_azurerm.py | 16 ++-- tests/foreman/api/test_computeresource_gce.py | 5 +- .../api/test_computeresource_libvirt.py | 13 +-- tests/foreman/api/test_contentcredentials.py | 10 ++- tests/foreman/api/test_contentview.py | 38 ++++----- tests/foreman/api/test_contentviewfilter.py | 17 ++-- tests/foreman/api/test_contentviewversion.py | 12 +-- tests/foreman/api/test_convert2rhel.py | 4 +- tests/foreman/api/test_discoveredhost.py | 10 +-- tests/foreman/api/test_discoveryrule.py | 6 +- tests/foreman/api/test_docker.py | 24 +++--- tests/foreman/api/test_environment.py | 12 +-- tests/foreman/api/test_errata.py | 5 +- tests/foreman/api/test_filter.py | 2 +- tests/foreman/api/test_foremantask.py | 2 +- tests/foreman/api/test_host.py | 12 +-- tests/foreman/api/test_hostcollection.py | 13 +-- tests/foreman/api/test_hostgroup.py | 14 +-- tests/foreman/api/test_http_proxy.py | 2 +- tests/foreman/api/test_ldapauthsource.py | 5 +- .../foreman/api/test_lifecycleenvironment.py | 10 ++- tests/foreman/api/test_location.py | 11 +-- tests/foreman/api/test_media.py | 13 +-- tests/foreman/api/test_multiple_paths.py | 8 +- tests/foreman/api/test_notifications.py | 8 +- tests/foreman/api/test_operatingsystem.py | 12 +-- tests/foreman/api/test_organization.py | 13 +-- .../foreman/api/test_oscap_tailoringfiles.py | 2 +- tests/foreman/api/test_oscappolicy.py | 2 +- tests/foreman/api/test_partitiontable.py | 13 +-- tests/foreman/api/test_permission.py | 4 +- tests/foreman/api/test_product.py | 20 +++-- tests/foreman/api/test_provisioning.py | 2 +- tests/foreman/api/test_provisioning_puppet.py | 4 +- .../foreman/api/test_provisioningtemplate.py | 13 +-- tests/foreman/api/test_reporttemplates.py | 21 ++--- tests/foreman/api/test_repositories.py | 2 +- tests/foreman/api/test_repository.py | 17 ++-- tests/foreman/api/test_repository_set.py | 5 +- tests/foreman/api/test_rhc.py | 2 +- tests/foreman/api/test_rhcloud_inventory.py | 7 +- tests/foreman/api/test_rhsm.py | 5 +- tests/foreman/api/test_role.py | 9 +- tests/foreman/api/test_settings.py | 12 +-- tests/foreman/api/test_subnet.py | 12 +-- tests/foreman/api/test_subscription.py | 8 +- tests/foreman/api/test_syncplan.py | 30 +++---- tests/foreman/api/test_templatesync.py | 15 ++-- tests/foreman/api/test_user.py | 26 +++--- tests/foreman/api/test_usergroup.py | 12 +-- tests/foreman/api/test_webhook.py | 8 +- tests/foreman/cli/test_acs.py | 5 +- tests/foreman/cli/test_activationkey.py | 41 ++++----- tests/foreman/cli/test_ansible.py | 2 +- tests/foreman/cli/test_architecture.py | 12 +-- tests/foreman/cli/test_auth.py | 5 +- tests/foreman/cli/test_bootdisk.py | 3 +- .../cli/test_computeresource_azurerm.py | 16 ++-- tests/foreman/cli/test_computeresource_ec2.py | 9 +- .../cli/test_computeresource_libvirt.py | 9 +- tests/foreman/cli/test_computeresource_osp.py | 2 +- .../foreman/cli/test_computeresource_rhev.py | 10 ++- .../cli/test_computeresource_vmware.py | 2 +- .../foreman/cli/test_container_management.py | 26 +++--- tests/foreman/cli/test_contentaccess.py | 12 +-- tests/foreman/cli/test_contentcredentials.py | 13 ++- tests/foreman/cli/test_contentview.py | 23 ++--- tests/foreman/cli/test_contentviewfilter.py | 13 +-- tests/foreman/cli/test_discoveryrule.py | 24 +++--- tests/foreman/cli/test_docker.py | 38 +++++---- tests/foreman/cli/test_domain.py | 15 ++-- tests/foreman/cli/test_environment.py | 11 +-- tests/foreman/cli/test_errata.py | 55 ++++++------ tests/foreman/cli/test_fact.py | 3 +- tests/foreman/cli/test_filter.py | 5 +- tests/foreman/cli/test_globalparam.py | 3 +- tests/foreman/cli/test_hammer.py | 1 - tests/foreman/cli/test_host.py | 59 +++++++------ tests/foreman/cli/test_hostcollection.py | 23 ++--- tests/foreman/cli/test_hostgroup.py | 38 +++++---- tests/foreman/cli/test_http_proxy.py | 7 +- tests/foreman/cli/test_jobtemplate.py | 8 +- tests/foreman/cli/test_ldapauthsource.py | 19 ++--- tests/foreman/cli/test_leapp_client.py | 2 +- .../foreman/cli/test_lifecycleenvironment.py | 5 +- tests/foreman/cli/test_location.py | 24 +++--- tests/foreman/cli/test_logging.py | 5 +- tests/foreman/cli/test_medium.py | 10 +-- tests/foreman/cli/test_model.py | 12 +-- tests/foreman/cli/test_operatingsystem.py | 23 ++--- tests/foreman/cli/test_organization.py | 38 +++++---- tests/foreman/cli/test_oscap.py | 26 +++--- .../foreman/cli/test_oscap_tailoringfiles.py | 16 ++-- tests/foreman/cli/test_ostreebranch.py | 12 +-- tests/foreman/cli/test_partitiontable.py | 8 +- tests/foreman/cli/test_product.py | 29 ++++--- .../foreman/cli/test_provisioningtemplate.py | 2 +- tests/foreman/cli/test_realm.py | 5 +- tests/foreman/cli/test_remoteexecution.py | 26 +++--- tests/foreman/cli/test_reporttemplates.py | 67 ++++++++------- tests/foreman/cli/test_repository.py | 85 ++++++++++--------- tests/foreman/cli/test_repository_set.py | 3 +- tests/foreman/cli/test_rhcloud_inventory.py | 5 +- tests/foreman/cli/test_role.py | 25 +++--- tests/foreman/cli/test_satellitesync.py | 30 ++++--- tests/foreman/cli/test_settings.py | 18 ++-- tests/foreman/cli/test_subnet.py | 16 ++-- tests/foreman/cli/test_subscription.py | 10 +-- tests/foreman/cli/test_syncplan.py | 29 ++++--- tests/foreman/cli/test_templatesync.py | 11 +-- tests/foreman/cli/test_user.py | 27 +++--- tests/foreman/cli/test_usergroup.py | 13 +-- .../cli/test_vm_install_products_package.py | 12 +-- tests/foreman/cli/test_webhook.py | 5 +- tests/foreman/destructive/test_auth.py | 2 +- tests/foreman/destructive/test_capsule.py | 2 +- .../destructive/test_capsule_loadbalancer.py | 3 +- .../destructive/test_capsulecontent.py | 2 +- tests/foreman/destructive/test_contenthost.py | 3 +- tests/foreman/destructive/test_contentview.py | 2 +- .../destructive/test_discoveredhost.py | 7 +- tests/foreman/destructive/test_host.py | 2 +- tests/foreman/destructive/test_infoblox.py | 3 +- tests/foreman/destructive/test_installer.py | 3 +- .../destructive/test_ldap_authentication.py | 7 +- .../destructive/test_ldapauthsource.py | 1 - .../destructive/test_leapp_satellite.py | 5 +- .../foreman/destructive/test_puppetplugin.py | 3 +- tests/foreman/destructive/test_realm.py | 3 +- .../destructive/test_remoteexecution.py | 2 +- tests/foreman/destructive/test_rename.py | 2 +- tests/foreman/destructive/test_repository.py | 2 +- tests/foreman/endtoend/test_api_endtoend.py | 20 ++--- tests/foreman/endtoend/test_cli_endtoend.py | 6 +- tests/foreman/installer/test_installer.py | 7 +- tests/foreman/longrun/test_inc_updates.py | 21 ++--- tests/foreman/longrun/test_oscap.py | 16 ++-- .../test_provisioning_computeresource.py | 8 +- tests/foreman/maintain/test_advanced.py | 10 +-- tests/foreman/maintain/test_backup_restore.py | 2 +- tests/foreman/maintain/test_health.py | 3 +- tests/foreman/maintain/test_service.py | 10 ++- tests/foreman/maintain/test_upgrade.py | 3 +- tests/foreman/sys/test_fam.py | 4 +- tests/foreman/sys/test_pulp3_filesystem.py | 2 +- tests/foreman/ui/test_activationkey.py | 5 +- tests/foreman/ui/test_ansible.py | 5 +- tests/foreman/ui/test_architecture.py | 2 +- tests/foreman/ui/test_audit.py | 2 +- tests/foreman/ui/test_bookmarks.py | 2 +- tests/foreman/ui/test_branding.py | 2 +- tests/foreman/ui/test_computeprofiles.py | 2 +- tests/foreman/ui/test_computeresource.py | 10 +-- .../ui/test_computeresource_azurerm.py | 12 +-- tests/foreman/ui/test_computeresource_ec2.py | 12 +-- tests/foreman/ui/test_computeresource_gce.py | 14 +-- .../ui/test_computeresource_libvirt.py | 10 ++- .../foreman/ui/test_computeresource_vmware.py | 21 +++-- tests/foreman/ui/test_config_group.py | 2 +- tests/foreman/ui/test_containerimagetag.py | 12 +-- tests/foreman/ui/test_contentcredentials.py | 3 +- tests/foreman/ui/test_contenthost.py | 41 +++++---- tests/foreman/ui/test_contentview.py | 43 +++++----- tests/foreman/ui/test_dashboard.py | 2 +- tests/foreman/ui/test_discoveredhost.py | 5 +- tests/foreman/ui/test_discoveryrule.py | 6 +- tests/foreman/ui/test_domain.py | 2 +- tests/foreman/ui/test_errata.py | 39 ++++----- tests/foreman/ui/test_hardwaremodel.py | 2 +- tests/foreman/ui/test_host.py | 37 ++++---- tests/foreman/ui/test_hostcollection.py | 2 +- tests/foreman/ui/test_hostgroup.py | 5 +- tests/foreman/ui/test_http_proxy.py | 7 +- tests/foreman/ui/test_jobinvocation.py | 2 +- tests/foreman/ui/test_jobtemplate.py | 2 +- tests/foreman/ui/test_ldap_authentication.py | 10 +-- tests/foreman/ui/test_lifecycleenvironment.py | 18 ++-- tests/foreman/ui/test_location.py | 9 +- tests/foreman/ui/test_media.py | 2 +- tests/foreman/ui/test_modulestreams.py | 2 +- tests/foreman/ui/test_organization.py | 6 +- tests/foreman/ui/test_oscapcontent.py | 3 +- tests/foreman/ui/test_oscappolicy.py | 2 +- tests/foreman/ui/test_oscaptailoringfile.py | 2 +- tests/foreman/ui/test_package.py | 5 +- tests/foreman/ui/test_partitiontable.py | 2 +- tests/foreman/ui/test_product.py | 16 ++-- tests/foreman/ui/test_puppetclass.py | 2 +- tests/foreman/ui/test_puppetenvironment.py | 3 +- tests/foreman/ui/test_reporttemplates.py | 24 +++--- tests/foreman/ui/test_repository.py | 34 ++++---- tests/foreman/ui/test_rhc.py | 2 +- tests/foreman/ui/test_rhcloud_insights.py | 4 +- tests/foreman/ui/test_rhcloud_inventory.py | 11 +-- tests/foreman/ui/test_role.py | 5 +- tests/foreman/ui/test_settings.py | 5 +- tests/foreman/ui/test_smartclassparameter.py | 3 +- tests/foreman/ui/test_subnet.py | 2 +- tests/foreman/ui/test_subscription.py | 18 ++-- tests/foreman/ui/test_sync.py | 16 ++-- tests/foreman/ui/test_syncplan.py | 8 +- tests/foreman/ui/test_templatesync.py | 7 +- tests/foreman/ui/test_user.py | 9 +- tests/foreman/ui/test_usergroup.py | 5 +- tests/foreman/ui/test_webhook.py | 3 +- tests/foreman/virtwho/api/test_esx.py | 22 ++--- tests/foreman/virtwho/api/test_esx_sca.py | 20 +++-- tests/foreman/virtwho/api/test_hyperv.py | 14 +-- tests/foreman/virtwho/api/test_hyperv_sca.py | 14 +-- tests/foreman/virtwho/api/test_kubevirt.py | 16 ++-- .../foreman/virtwho/api/test_kubevirt_sca.py | 14 +-- tests/foreman/virtwho/api/test_libvirt.py | 14 +-- tests/foreman/virtwho/api/test_libvirt_sca.py | 14 +-- tests/foreman/virtwho/api/test_nutanix.py | 20 +++-- tests/foreman/virtwho/api/test_nutanix_sca.py | 14 +-- tests/foreman/virtwho/cli/test_esx.py | 24 +++--- tests/foreman/virtwho/cli/test_esx_sca.py | 24 +++--- tests/foreman/virtwho/cli/test_hyperv.py | 14 +-- tests/foreman/virtwho/cli/test_hyperv_sca.py | 14 +-- tests/foreman/virtwho/cli/test_kubevirt.py | 14 +-- .../foreman/virtwho/cli/test_kubevirt_sca.py | 14 +-- tests/foreman/virtwho/cli/test_libvirt.py | 14 +-- tests/foreman/virtwho/cli/test_libvirt_sca.py | 14 +-- tests/foreman/virtwho/cli/test_nutanix.py | 18 ++-- tests/foreman/virtwho/cli/test_nutanix_sca.py | 14 +-- tests/foreman/virtwho/conftest.py | 2 +- tests/foreman/virtwho/ui/test_esx.py | 34 ++++---- tests/foreman/virtwho/ui/test_esx_sca.py | 32 +++---- tests/foreman/virtwho/ui/test_hyperv.py | 16 ++-- tests/foreman/virtwho/ui/test_hyperv_sca.py | 16 ++-- tests/foreman/virtwho/ui/test_kubevirt.py | 16 ++-- tests/foreman/virtwho/ui/test_kubevirt_sca.py | 16 ++-- tests/foreman/virtwho/ui/test_libvirt.py | 16 ++-- tests/foreman/virtwho/ui/test_libvirt_sca.py | 16 ++-- tests/foreman/virtwho/ui/test_nutanix.py | 20 +++-- tests/foreman/virtwho/ui/test_nutanix_sca.py | 20 +++-- tests/robottelo/conftest.py | 2 +- tests/robottelo/test_cli.py | 14 +-- tests/robottelo/test_dependencies.py | 10 +-- tests/robottelo/test_func_locker.py | 2 +- tests/robottelo/test_func_shared.py | 27 +++--- tests/robottelo/test_helpers.py | 3 +- tests/robottelo/test_issue_handlers.py | 12 +-- tests/upgrades/conftest.py | 2 +- tests/upgrades/test_client.py | 3 +- tests/upgrades/test_contentview.py | 5 +- tests/upgrades/test_host.py | 2 +- tests/upgrades/test_hostgroup.py | 2 +- tests/upgrades/test_repository.py | 3 +- tests/upgrades/test_subscription.py | 2 +- tests/upgrades/test_syncplan.py | 2 +- tests/upgrades/test_usergroup.py | 5 +- tests/upgrades/test_virtwho.py | 12 +-- 336 files changed, 1797 insertions(+), 1858 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fb023936d75..c86df504763 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,6 @@ # configuration for pre-commit git hooks repos: -- repo: https://github.com/asottile/reorder_python_imports - rev: v3.9.0 - hooks: - - id: reorder-python-imports - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: diff --git a/pyproject.toml b/pyproject.toml index ae675eb552d..b042ee54c72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,13 +23,22 @@ select = [ # "C90", # mccabe "E", # pycodestyle "F", # flake8 + "I", # isort # "Q", # flake8-quotes "UP", # pyupgrade "W", # pycodestyle ] -# Allow lines to be as long as 100 characters. -line-length = 100 +ignore = [ + "E501", # line too long - handled by black +] + +[tool.ruff.isort] +force-sort-within-sections = true +known-first-party = [ + "robottelo", +] +combine-as-imports = true [tool.ruff.flake8-quotes] diff --git a/pytest_fixtures/component/acs.py b/pytest_fixtures/component/acs.py index df222b90553..065e64d935f 100644 --- a/pytest_fixtures/component/acs.py +++ b/pytest_fixtures/component/acs.py @@ -1,8 +1,7 @@ # Alternate Content Sources fixtures import pytest -from robottelo.constants.repos import CUSTOM_FILE_REPO -from robottelo.constants.repos import CUSTOM_RPM_REPO +from robottelo.constants.repos import CUSTOM_FILE_REPO, CUSTOM_RPM_REPO @pytest.fixture(scope='module') diff --git a/pytest_fixtures/component/computeprofile.py b/pytest_fixtures/component/computeprofile.py index 859dc64abd0..3f2cce6043c 100644 --- a/pytest_fixtures/component/computeprofile.py +++ b/pytest_fixtures/component/computeprofile.py @@ -1,6 +1,6 @@ # Compute Profile Fixtures -import pytest from nailgun import entities +import pytest @pytest.fixture(scope='module') diff --git a/pytest_fixtures/component/contentview.py b/pytest_fixtures/component/contentview.py index e82f69d04b5..c1879f43338 100644 --- a/pytest_fixtures/component/contentview.py +++ b/pytest_fixtures/component/contentview.py @@ -1,6 +1,6 @@ # Content View Fixtures -import pytest from nailgun.entity_mixins import call_entity_method_with_timeout +import pytest from robottelo.constants import DEFAULT_CV diff --git a/pytest_fixtures/component/domain.py b/pytest_fixtures/component/domain.py index 5cc524ff3b5..c62b45f54d5 100644 --- a/pytest_fixtures/component/domain.py +++ b/pytest_fixtures/component/domain.py @@ -1,6 +1,6 @@ # Domain Fixtures -import pytest from nailgun import entities +import pytest @pytest.fixture(scope='session') diff --git a/pytest_fixtures/component/host.py b/pytest_fixtures/component/host.py index 1fb55a33001..4f070a86a64 100644 --- a/pytest_fixtures/component/host.py +++ b/pytest_fixtures/component/host.py @@ -1,14 +1,10 @@ # Host Specific Fixtures -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo.cli.factory import setup_org_for_a_rh_repo -from robottelo.constants import DEFAULT_CV -from robottelo.constants import ENVIRONMENT -from robottelo.constants import PRDS -from robottelo.constants import REPOS -from robottelo.constants import REPOSET +from robottelo.constants import DEFAULT_CV, ENVIRONMENT, PRDS, REPOS, REPOSET @pytest.fixture diff --git a/pytest_fixtures/component/hostgroup.py b/pytest_fixtures/component/hostgroup.py index 43b2668b6e2..65be6196183 100644 --- a/pytest_fixtures/component/hostgroup.py +++ b/pytest_fixtures/component/hostgroup.py @@ -1,6 +1,6 @@ # Hostgroup Fixtures -import pytest from nailgun import entities +import pytest from requests.exceptions import HTTPError from robottelo.logging import logger diff --git a/pytest_fixtures/component/katello_agent.py b/pytest_fixtures/component/katello_agent.py index 690e726c0d4..0428f91a740 100644 --- a/pytest_fixtures/component/katello_agent.py +++ b/pytest_fixtures/component/katello_agent.py @@ -1,5 +1,5 @@ -import pytest from box import Box +import pytest from robottelo.config import settings from robottelo.utils.installer import InstallerCommand diff --git a/pytest_fixtures/component/katello_certs_check.py b/pytest_fixtures/component/katello_certs_check.py index d9150fa539b..f14299ebd52 100644 --- a/pytest_fixtures/component/katello_certs_check.py +++ b/pytest_fixtures/component/katello_certs_check.py @@ -1,8 +1,8 @@ # katello_certs_check Fixtures from pathlib import Path -import pytest from fauxfactory import gen_string +import pytest from robottelo.constants import CERT_DATA as cert_data from robottelo.hosts import Capsule diff --git a/pytest_fixtures/component/maintain.py b/pytest_fixtures/component/maintain.py index 6cac4f6f8c8..152656f8dc1 100644 --- a/pytest_fixtures/component/maintain.py +++ b/pytest_fixtures/component/maintain.py @@ -6,9 +6,7 @@ from robottelo import constants from robottelo.config import settings from robottelo.constants import SATELLITE_MAINTAIN_YML -from robottelo.hosts import Capsule -from robottelo.hosts import Satellite -from robottelo.hosts import SatelliteHostError +from robottelo.hosts import Capsule, Satellite, SatelliteHostError from robottelo.logging import logger synced_repos = pytest.StashKey[dict] diff --git a/pytest_fixtures/component/os.py b/pytest_fixtures/component/os.py index 6d11e9c94fd..e6039c1b6f9 100644 --- a/pytest_fixtures/component/os.py +++ b/pytest_fixtures/component/os.py @@ -1,6 +1,6 @@ # Operating System Fixtures -import pytest from nailgun import entities +import pytest @pytest.fixture(scope='session') diff --git a/pytest_fixtures/component/oscap.py b/pytest_fixtures/component/oscap.py index 6fdd3866c29..356050f94a7 100644 --- a/pytest_fixtures/component/oscap.py +++ b/pytest_fixtures/component/oscap.py @@ -1,15 +1,12 @@ from pathlib import PurePath -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo.cli.factory import make_scapcontent -from robottelo.config import robottelo_tmp_dir -from robottelo.config import settings -from robottelo.constants import DataFile -from robottelo.constants import OSCAP_PROFILE -from robottelo.constants import OSCAP_TAILORING_FILE +from robottelo.config import robottelo_tmp_dir, settings +from robottelo.constants import OSCAP_PROFILE, OSCAP_TAILORING_FILE, DataFile @pytest.fixture(scope="session") diff --git a/pytest_fixtures/component/provision_azure.py b/pytest_fixtures/component/provision_azure.py index d1fe7f1f128..0483758b51a 100644 --- a/pytest_fixtures/component/provision_azure.py +++ b/pytest_fixtures/component/provision_azure.py @@ -1,15 +1,17 @@ # Azure CR Fixtures -import pytest from fauxfactory import gen_string +import pytest from wrapanapi import AzureSystem from robottelo.config import settings -from robottelo.constants import AZURERM_RHEL7_FT_BYOS_IMG_URN -from robottelo.constants import AZURERM_RHEL7_FT_CUSTOM_IMG_URN -from robottelo.constants import AZURERM_RHEL7_FT_GALLERY_IMG_URN -from robottelo.constants import AZURERM_RHEL7_FT_IMG_URN -from robottelo.constants import AZURERM_RHEL7_UD_IMG_URN -from robottelo.constants import DEFAULT_ARCHITECTURE +from robottelo.constants import ( + AZURERM_RHEL7_FT_BYOS_IMG_URN, + AZURERM_RHEL7_FT_CUSTOM_IMG_URN, + AZURERM_RHEL7_FT_GALLERY_IMG_URN, + AZURERM_RHEL7_FT_IMG_URN, + AZURERM_RHEL7_UD_IMG_URN, + DEFAULT_ARCHITECTURE, +) @pytest.fixture(scope='session') diff --git a/pytest_fixtures/component/provision_gce.py b/pytest_fixtures/component/provision_gce.py index fbdb2a886e9..af260bdb38c 100644 --- a/pytest_fixtures/component/provision_gce.py +++ b/pytest_fixtures/component/provision_gce.py @@ -2,16 +2,18 @@ import json from tempfile import mkstemp -import pytest from fauxfactory import gen_string +import pytest from wrapanapi.systems.google import GoogleCloudSystem from robottelo.config import settings -from robottelo.constants import DEFAULT_ARCHITECTURE -from robottelo.constants import DEFAULT_PTABLE -from robottelo.constants import FOREMAN_PROVIDERS -from robottelo.constants import GCE_RHEL_CLOUD_PROJECTS -from robottelo.constants import GCE_TARGET_RHEL_IMAGE_NAME +from robottelo.constants import ( + DEFAULT_ARCHITECTURE, + DEFAULT_PTABLE, + FOREMAN_PROVIDERS, + GCE_RHEL_CLOUD_PROJECTS, + GCE_TARGET_RHEL_IMAGE_NAME, +) from robottelo.exceptions import GCECertNotFoundError diff --git a/pytest_fixtures/component/provision_pxe.py b/pytest_fixtures/component/provision_pxe.py index 8ea993e561f..1fc0e47397b 100644 --- a/pytest_fixtures/component/provision_pxe.py +++ b/pytest_fixtures/component/provision_pxe.py @@ -3,11 +3,11 @@ import re from tempfile import mkstemp -import pytest 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 diff --git a/pytest_fixtures/component/provisioning_template.py b/pytest_fixtures/component/provisioning_template.py index 6b8e96de7e9..cb4ea6d84d5 100644 --- a/pytest_fixtures/component/provisioning_template.py +++ b/pytest_fixtures/component/provisioning_template.py @@ -1,13 +1,11 @@ # Provisioning Template Fixtures -import pytest from box import Box from nailgun import entities from packaging.version import Version +import pytest from robottelo import constants -from robottelo.constants import DEFAULT_PTABLE -from robottelo.constants import DEFAULT_PXE_TEMPLATE -from robottelo.constants import DEFAULT_TEMPLATE +from robottelo.constants import DEFAULT_PTABLE, DEFAULT_PXE_TEMPLATE, DEFAULT_TEMPLATE @pytest.fixture(scope='module') diff --git a/pytest_fixtures/component/repository.py b/pytest_fixtures/component/repository.py index 1cc17092768..2336bf12e5a 100644 --- a/pytest_fixtures/component/repository.py +++ b/pytest_fixtures/component/repository.py @@ -1,13 +1,10 @@ # Repository Fixtures -import pytest from fauxfactory import gen_string from nailgun import entities from nailgun.entity_mixins import call_entity_method_with_timeout +import pytest -from robottelo.constants import DEFAULT_ARCHITECTURE -from robottelo.constants import PRDS -from robottelo.constants import REPOS -from robottelo.constants import REPOSET +from robottelo.constants import DEFAULT_ARCHITECTURE, PRDS, REPOS, REPOSET @pytest.fixture(scope='module') diff --git a/pytest_fixtures/component/satellite_auth.py b/pytest_fixtures/component/satellite_auth.py index e37c6cb9609..9c39ed3ade3 100644 --- a/pytest_fixtures/component/satellite_auth.py +++ b/pytest_fixtures/component/satellite_auth.py @@ -1,20 +1,21 @@ import copy import socket -import pytest from box import Box from nailgun import entities +import pytest from robottelo.config import settings -from robottelo.constants import AUDIENCE_MAPPER -from robottelo.constants import CERT_PATH -from robottelo.constants import GROUP_MEMBERSHIP_MAPPER -from robottelo.constants import HAMMER_CONFIG -from robottelo.constants import HAMMER_SESSIONS -from robottelo.constants import LDAP_ATTR -from robottelo.constants import LDAP_SERVER_TYPE -from robottelo.hosts import IPAHost -from robottelo.hosts import SSOHost +from robottelo.constants import ( + AUDIENCE_MAPPER, + CERT_PATH, + GROUP_MEMBERSHIP_MAPPER, + HAMMER_CONFIG, + HAMMER_SESSIONS, + LDAP_ATTR, + LDAP_SERVER_TYPE, +) +from robottelo.hosts import IPAHost, SSOHost from robottelo.utils.datafactory import gen_string from robottelo.utils.installer import InstallerCommand from robottelo.utils.issue_handlers import is_open diff --git a/pytest_fixtures/component/subnet.py b/pytest_fixtures/component/subnet.py index e43f9952652..0e1ddb1b782 100644 --- a/pytest_fixtures/component/subnet.py +++ b/pytest_fixtures/component/subnet.py @@ -1,6 +1,6 @@ # Subnet Fixtures -import pytest from nailgun import entities +import pytest @pytest.fixture(scope='module') diff --git a/pytest_fixtures/component/taxonomy.py b/pytest_fixtures/component/taxonomy.py index 2276b3c3b88..faad21122e5 100644 --- a/pytest_fixtures/component/taxonomy.py +++ b/pytest_fixtures/component/taxonomy.py @@ -1,10 +1,9 @@ # Content Component fixtures -import pytest from manifester import Manifester +import pytest from robottelo.config import settings -from robottelo.constants import DEFAULT_LOC -from robottelo.constants import DEFAULT_ORG +from robottelo.constants import DEFAULT_LOC, DEFAULT_ORG from robottelo.utils.manifest import clone diff --git a/pytest_fixtures/component/templatesync.py b/pytest_fixtures/component/templatesync.py index b2c681e8f95..3dee193cdd3 100644 --- a/pytest_fixtures/component/templatesync.py +++ b/pytest_fixtures/component/templatesync.py @@ -1,6 +1,6 @@ +from fauxfactory import gen_string import pytest import requests -from fauxfactory import gen_string from robottelo.config import settings from robottelo.constants import FOREMAN_TEMPLATE_ROOT_DIR diff --git a/pytest_fixtures/component/user.py b/pytest_fixtures/component/user.py index fea405520e4..3f4d1034e6e 100644 --- a/pytest_fixtures/component/user.py +++ b/pytest_fixtures/component/user.py @@ -1,5 +1,5 @@ -import pytest from nailgun import entities +import pytest @pytest.fixture diff --git a/pytest_fixtures/component/user_role.py b/pytest_fixtures/component/user_role.py index 8e4f3f00bea..92b742e6966 100644 --- a/pytest_fixtures/component/user_role.py +++ b/pytest_fixtures/component/user_role.py @@ -1,7 +1,6 @@ -import pytest -from fauxfactory import gen_alphanumeric -from fauxfactory import gen_string +from fauxfactory import gen_alphanumeric, gen_string from nailgun import entities +import pytest @pytest.fixture(scope='class') diff --git a/pytest_fixtures/core/broker.py b/pytest_fixtures/core/broker.py index 4c89dea35c5..9d207fcd9d7 100644 --- a/pytest_fixtures/core/broker.py +++ b/pytest_fixtures/core/broker.py @@ -1,13 +1,11 @@ from contextlib import contextmanager -import pytest from box import Box from broker import Broker +import pytest from robottelo.config import settings -from robottelo.hosts import ContentHostError -from robottelo.hosts import lru_sat_ready_rhel -from robottelo.hosts import Satellite +from robottelo.hosts import ContentHostError, Satellite, lru_sat_ready_rhel @pytest.fixture(scope='session') diff --git a/pytest_fixtures/core/contenthosts.py b/pytest_fixtures/core/contenthosts.py index 131beda131d..586fa95eb57 100644 --- a/pytest_fixtures/core/contenthosts.py +++ b/pytest_fixtures/core/contenthosts.py @@ -4,13 +4,12 @@ The functions in this module are read in the pytest_plugins/fixture_markers.py module All functions in this module will be treated as fixtures that apply the contenthost mark """ -import pytest from broker import Broker +import pytest from robottelo import constants from robottelo.config import settings -from robottelo.hosts import ContentHost -from robottelo.hosts import Satellite +from robottelo.hosts import ContentHost, Satellite def host_conf(request): diff --git a/pytest_fixtures/core/reporting.py b/pytest_fixtures/core/reporting.py index 24f1124f4c6..baf9d3cde7d 100644 --- a/pytest_fixtures/core/reporting.py +++ b/pytest_fixtures/core/reporting.py @@ -1,12 +1,10 @@ import datetime -import pytest from _pytest.junitxml import xml_key +import pytest from xdist import get_xdist_worker_id -from robottelo.config import setting_is_set -from robottelo.config import settings - +from robottelo.config import setting_is_set, settings FMT_XUNIT_TIME = '%Y-%m-%dT%H:%M:%S' diff --git a/pytest_fixtures/core/sat_cap_factory.py b/pytest_fixtures/core/sat_cap_factory.py index f064fec6da1..f08e722cbbf 100644 --- a/pytest_fixtures/core/sat_cap_factory.py +++ b/pytest_fixtures/core/sat_cap_factory.py @@ -1,17 +1,17 @@ from contextlib import contextmanager -import pytest from broker import Broker +import pytest from wait_for import wait_for -from robottelo.config import configure_airgun -from robottelo.config import configure_nailgun -from robottelo.config import settings -from robottelo.hosts import Capsule -from robottelo.hosts import get_sat_rhel_version -from robottelo.hosts import IPAHost -from robottelo.hosts import lru_sat_ready_rhel -from robottelo.hosts import Satellite +from robottelo.config import configure_airgun, configure_nailgun, settings +from robottelo.hosts import ( + Capsule, + IPAHost, + Satellite, + get_sat_rhel_version, + lru_sat_ready_rhel, +) from robottelo.logging import logger from robottelo.utils.installer import InstallerCommand diff --git a/pytest_fixtures/core/ui.py b/pytest_fixtures/core/ui.py index 0addff500df..7edbd39b731 100644 --- a/pytest_fixtures/core/ui.py +++ b/pytest_fixtures/core/ui.py @@ -1,5 +1,5 @@ -import pytest from fauxfactory import gen_string +import pytest from requests.exceptions import HTTPError from robottelo.logging import logger diff --git a/pytest_fixtures/core/upgrade.py b/pytest_fixtures/core/upgrade.py index 291bae18fbf..a79f4867979 100644 --- a/pytest_fixtures/core/upgrade.py +++ b/pytest_fixtures/core/upgrade.py @@ -1,5 +1,5 @@ -import pytest from broker import Broker +import pytest from robottelo.hosts import Capsule from robottelo.logging import logger diff --git a/pytest_fixtures/core/xdist.py b/pytest_fixtures/core/xdist.py index 18088f8a572..4d02fe026d0 100644 --- a/pytest_fixtures/core/xdist.py +++ b/pytest_fixtures/core/xdist.py @@ -1,12 +1,10 @@ """Fixtures specific to or relating to pytest's xdist plugin""" import random -import pytest from broker import Broker +import pytest -from robottelo.config import configure_airgun -from robottelo.config import configure_nailgun -from robottelo.config import settings +from robottelo.config import configure_airgun, configure_nailgun, settings from robottelo.hosts import Satellite from robottelo.logging import logger diff --git a/pytest_plugins/factory_collection.py b/pytest_plugins/factory_collection.py index f8933e45a7b..6472c2b909c 100644 --- a/pytest_plugins/factory_collection.py +++ b/pytest_plugins/factory_collection.py @@ -1,5 +1,4 @@ -from inspect import getmembers -from inspect import isfunction +from inspect import getmembers, isfunction def pytest_configure(config): diff --git a/pytest_plugins/fixture_markers.py b/pytest_plugins/fixture_markers.py index e2126d86dad..888a720d13e 100644 --- a/pytest_plugins/fixture_markers.py +++ b/pytest_plugins/fixture_markers.py @@ -1,10 +1,8 @@ +from inspect import getmembers, isfunction import re -from inspect import getmembers -from inspect import isfunction from robottelo.config import settings - TARGET_FIXTURES = [ 'rhel_contenthost', 'content_hosts', diff --git a/pytest_plugins/issue_handlers.py b/pytest_plugins/issue_handlers.py index d2dfb3be55e..3da2044faff 100644 --- a/pytest_plugins/issue_handlers.py +++ b/pytest_plugins/issue_handlers.py @@ -1,20 +1,21 @@ +from collections import defaultdict +from datetime import datetime import inspect import json import re -from collections import defaultdict -from datetime import datetime import pytest from robottelo.config import settings from robottelo.logging import collection_logger as logger from robottelo.utils import slugify_component -from robottelo.utils.issue_handlers import add_workaround -from robottelo.utils.issue_handlers import bugzilla -from robottelo.utils.issue_handlers import is_open -from robottelo.utils.issue_handlers import should_deselect -from robottelo.utils.version import search_version_key -from robottelo.utils.version import VersionEncoder +from robottelo.utils.issue_handlers import ( + add_workaround, + bugzilla, + is_open, + should_deselect, +) +from robottelo.utils.version import VersionEncoder, search_version_key DEFAULT_BZ_CACHE_FILE = 'bz_cache.json' diff --git a/pytest_plugins/logging_hooks.py b/pytest_plugins/logging_hooks.py index 1cf24d89ee1..9cd7ea4c84f 100644 --- a/pytest_plugins/logging_hooks.py +++ b/pytest_plugins/logging_hooks.py @@ -4,16 +4,17 @@ import pytest from xdist import is_xdist_worker -from robottelo.logging import broker_log_setup -from robottelo.logging import DEFAULT_DATE_FORMAT -from robottelo.logging import logger -from robottelo.logging import logging_yaml -from robottelo.logging import robottelo_log_dir -from robottelo.logging import robottelo_log_file +from robottelo.logging import ( + DEFAULT_DATE_FORMAT, + broker_log_setup, + logger, + logging_yaml, + robottelo_log_dir, + robottelo_log_file, +) try: - from pytest_reportportal import RPLogger - from pytest_reportportal import RPLogHandler + from pytest_reportportal import RPLogger, RPLogHandler except ImportError: pass diff --git a/pytest_plugins/marker_deselection.py b/pytest_plugins/marker_deselection.py index ca68ead0165..ee2cd4c1485 100644 --- a/pytest_plugins/marker_deselection.py +++ b/pytest_plugins/marker_deselection.py @@ -2,7 +2,6 @@ from robottelo.logging import collection_logger as logger - non_satCI_components = ['Virt-whoConfigurePlugin'] diff --git a/pytest_plugins/requirements/req_updater.py b/pytest_plugins/requirements/req_updater.py index a647fc51e26..664a9bb92a5 100644 --- a/pytest_plugins/requirements/req_updater.py +++ b/pytest_plugins/requirements/req_updater.py @@ -1,5 +1,5 @@ -import subprocess from functools import cached_property +import subprocess class ReqUpdater: diff --git a/pytest_plugins/settings_skip.py b/pytest_plugins/settings_skip.py index 7a211cd32ae..6ee053adee5 100644 --- a/pytest_plugins/settings_skip.py +++ b/pytest_plugins/settings_skip.py @@ -1,7 +1,6 @@ import pytest -from robottelo.config import setting_is_set -from robottelo.config import settings +from robottelo.config import setting_is_set, settings def pytest_configure(config): diff --git a/pytest_plugins/upgrade/scenario_workers.py b/pytest_plugins/upgrade/scenario_workers.py index 87d84b97f86..099ff1c0fe3 100644 --- a/pytest_plugins/upgrade/scenario_workers.py +++ b/pytest_plugins/upgrade/scenario_workers.py @@ -3,10 +3,7 @@ import pytest -from robottelo.config import configure_airgun -from robottelo.config import configure_nailgun -from robottelo.config import settings - +from robottelo.config import configure_airgun, configure_nailgun, settings _json_file = 'upgrade_workers.json' json_file = Path(_json_file) diff --git a/robottelo/cli/contentview.py b/robottelo/cli/contentview.py index bd1a96fe4c5..fb8001f89d7 100644 --- a/robottelo/cli/contentview.py +++ b/robottelo/cli/contentview.py @@ -34,8 +34,7 @@ -h, --help print help """ from robottelo.cli import hammer -from robottelo.cli.base import Base -from robottelo.cli.base import CLIError +from robottelo.cli.base import Base, CLIError class ContentViewFilterRule(Base): diff --git a/robottelo/cli/factory.py b/robottelo/cli/factory.py index 78deaf5ff29..c1ac9cb4268 100644 --- a/robottelo/cli/factory.py +++ b/robottelo/cli/factory.py @@ -3,20 +3,22 @@ """ import datetime import os +from os import chmod import pprint import random -from os import chmod from tempfile import mkstemp from time import sleep -from fauxfactory import gen_alphanumeric -from fauxfactory import gen_choice -from fauxfactory import gen_integer -from fauxfactory import gen_ipaddr -from fauxfactory import gen_mac -from fauxfactory import gen_netmask -from fauxfactory import gen_string -from fauxfactory import gen_url +from fauxfactory import ( + gen_alphanumeric, + gen_choice, + gen_integer, + gen_ipaddr, + gen_mac, + gen_netmask, + gen_string, + gen_url, +) from robottelo import constants from robottelo.cli.activationkey import ActivationKey @@ -24,9 +26,11 @@ from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.computeresource import ComputeResource from robottelo.cli.content_credentials import ContentCredential -from robottelo.cli.contentview import ContentView -from robottelo.cli.contentview import ContentViewFilter -from robottelo.cli.contentview import ContentViewFilterRule +from robottelo.cli.contentview import ( + ContentView, + ContentViewFilter, + ContentViewFilterRule, +) from robottelo.cli.discoveryrule import DiscoveryRule from robottelo.cli.domain import Domain from robottelo.cli.environment import Environment @@ -60,8 +64,7 @@ from robottelo.cli.template import Template from robottelo.cli.template_input import TemplateInput from robottelo.cli.user import User -from robottelo.cli.usergroup import UserGroup -from robottelo.cli.usergroup import UserGroupExternal +from robottelo.cli.usergroup import UserGroup, UserGroupExternal from robottelo.cli.virt_who_config import VirtWhoConfig from robottelo.config import settings from robottelo.logging import logger @@ -70,7 +73,6 @@ from robottelo.utils.decorators import cacheable from robottelo.utils.manifest import clone - ORG_KEYS = ['organization', 'organization-id', 'organization-label'] CONTENT_VIEW_KEYS = ['content-view', 'content-view-id'] LIFECYCLE_KEYS = ['lifecycle-environment', 'lifecycle-environment-id'] diff --git a/robottelo/cli/report_template.py b/robottelo/cli/report_template.py index 43b92d0bb98..e8a60b9beb6 100644 --- a/robottelo/cli/report_template.py +++ b/robottelo/cli/report_template.py @@ -25,10 +25,8 @@ from tempfile import mkstemp from robottelo import ssh -from robottelo.cli.base import Base -from robottelo.cli.base import CLIError -from robottelo.constants import DataFile -from robottelo.constants import REPORT_TEMPLATE_FILE +from robottelo.cli.base import Base, CLIError +from robottelo.constants import REPORT_TEMPLATE_FILE, DataFile class ReportTemplate(Base): diff --git a/robottelo/cli/template_input.py b/robottelo/cli/template_input.py index 1393642cb35..cfe548384c3 100644 --- a/robottelo/cli/template_input.py +++ b/robottelo/cli/template_input.py @@ -15,8 +15,7 @@ info Show template input details list List template inputs """ -from robottelo.cli.base import Base -from robottelo.cli.base import CLIError +from robottelo.cli.base import Base, CLIError class TemplateInput(Base): diff --git a/robottelo/cli/webhook.py b/robottelo/cli/webhook.py index 70edda993e9..379555630c5 100644 --- a/robottelo/cli/webhook.py +++ b/robottelo/cli/webhook.py @@ -13,10 +13,8 @@ Options: -h, --help Print help """ -from robottelo.cli.base import Base -from robottelo.cli.base import CLIError -from robottelo.constants import WEBHOOK_EVENTS -from robottelo.constants import WEBHOOK_METHODS +from robottelo.cli.base import Base, CLIError +from robottelo.constants import WEBHOOK_EVENTS, WEBHOOK_METHODS class Webhook(Base): diff --git a/robottelo/config/__init__.py b/robottelo/config/__init__.py index 0062ce17b6f..5bd85ab5737 100644 --- a/robottelo/config/__init__.py +++ b/robottelo/config/__init__.py @@ -9,8 +9,7 @@ from nailgun.config import ServerConfig from robottelo.config.validators import VALIDATORS -from robottelo.logging import logger -from robottelo.logging import robottelo_root_dir +from robottelo.logging import logger, robottelo_root_dir if not os.getenv('ROBOTTELO_DIR'): # dynaconf robottelo file uses ROBOTELLO_DIR for screenshots @@ -150,8 +149,7 @@ def configure_nailgun(): of this. * Set a default value for ``nailgun.entities.GPGKey.content``. """ - from nailgun import entities - from nailgun import entity_mixins + from nailgun import entities, entity_mixins from nailgun.config import ServerConfig entity_mixins.CREATE_MISSING = True diff --git a/robottelo/config/validators.py b/robottelo/config/validators.py index dcd242f2b5b..b605a1cd229 100644 --- a/robottelo/config/validators.py +++ b/robottelo/config/validators.py @@ -1,8 +1,6 @@ from dynaconf import Validator -from robottelo.constants import AZURERM_VALID_REGIONS -from robottelo.constants import VALID_GCE_ZONES - +from robottelo.constants import AZURERM_VALID_REGIONS, VALID_GCE_ZONES VALIDATORS = dict( supportability=[ diff --git a/robottelo/host_helpers/__init__.py b/robottelo/host_helpers/__init__.py index 87e78daae75..fa564dda27f 100644 --- a/robottelo/host_helpers/__init__.py +++ b/robottelo/host_helpers/__init__.py @@ -1,13 +1,16 @@ -from robottelo.host_helpers.capsule_mixins import CapsuleInfo -from robottelo.host_helpers.capsule_mixins import EnablePluginsCapsule -from robottelo.host_helpers.contenthost_mixins import HostInfo -from robottelo.host_helpers.contenthost_mixins import SystemFacts -from robottelo.host_helpers.contenthost_mixins import VersionedContent -from robottelo.host_helpers.satellite_mixins import ContentInfo -from robottelo.host_helpers.satellite_mixins import EnablePluginsSatellite -from robottelo.host_helpers.satellite_mixins import Factories -from robottelo.host_helpers.satellite_mixins import ProvisioningSetup -from robottelo.host_helpers.satellite_mixins import SystemInfo +from robottelo.host_helpers.capsule_mixins import CapsuleInfo, EnablePluginsCapsule +from robottelo.host_helpers.contenthost_mixins import ( + HostInfo, + SystemFacts, + VersionedContent, +) +from robottelo.host_helpers.satellite_mixins import ( + ContentInfo, + EnablePluginsSatellite, + Factories, + ProvisioningSetup, + SystemInfo, +) class ContentHostMixins(HostInfo, SystemFacts, VersionedContent): diff --git a/robottelo/host_helpers/api_factory.py b/robottelo/host_helpers/api_factory.py index 23219148a53..92f40652d34 100644 --- a/robottelo/host_helpers/api_factory.py +++ b/robottelo/host_helpers/api_factory.py @@ -2,23 +2,23 @@ It is not meant to be used directly, but as part of a robottelo.hosts.Satellite instance example: my_satellite.api_factory.api_method() """ -import time from contextlib import contextmanager +import time -from fauxfactory import gen_ipaddr -from fauxfactory import gen_mac -from fauxfactory import gen_string +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 from requests import HTTPError from robottelo.config import settings -from robottelo.constants import DEFAULT_ARCHITECTURE -from robottelo.constants import DEFAULT_PTABLE -from robottelo.constants import DEFAULT_PXE_TEMPLATE -from robottelo.constants import DEFAULT_TEMPLATE -from robottelo.constants import REPO_TYPE +from robottelo.constants import ( + DEFAULT_ARCHITECTURE, + DEFAULT_PTABLE, + DEFAULT_PXE_TEMPLATE, + DEFAULT_TEMPLATE, + REPO_TYPE, +) from robottelo.exceptions import ImproperlyConfigured from robottelo.host_helpers.repository_mixins import initiate_repo_helpers diff --git a/robottelo/host_helpers/capsule_mixins.py b/robottelo/host_helpers/capsule_mixins.py index f9c5d9652bc..712bf98f533 100644 --- a/robottelo/host_helpers/capsule_mixins.py +++ b/robottelo/host_helpers/capsule_mixins.py @@ -1,8 +1,7 @@ -import time from datetime import datetime +import time -from robottelo.constants import PUPPET_CAPSULE_INSTALLER -from robottelo.constants import PUPPET_COMMON_INSTALLER_OPTS +from robottelo.constants import PUPPET_CAPSULE_INSTALLER, PUPPET_COMMON_INSTALLER_OPTS from robottelo.logging import logger from robottelo.utils.installer import InstallerCommand diff --git a/robottelo/host_helpers/cli_factory.py b/robottelo/host_helpers/cli_factory.py index b78370bd9e8..cf7d8a28058 100644 --- a/robottelo/host_helpers/cli_factory.py +++ b/robottelo/host_helpers/cli_factory.py @@ -4,25 +4,26 @@ example: my_satellite.cli_factory.make_org() """ import datetime +from functools import lru_cache, partial import inspect import os +from os import chmod import pprint import random -from functools import lru_cache -from functools import partial -from os import chmod from tempfile import mkstemp from time import sleep from box import Box -from fauxfactory import gen_alpha -from fauxfactory import gen_alphanumeric -from fauxfactory import gen_choice -from fauxfactory import gen_integer -from fauxfactory import gen_ipaddr -from fauxfactory import gen_mac -from fauxfactory import gen_netmask -from fauxfactory import gen_url +from fauxfactory import ( + gen_alpha, + gen_alphanumeric, + gen_choice, + gen_integer, + gen_ipaddr, + gen_mac, + gen_netmask, + gen_url, +) from robottelo import constants from robottelo.cli.base import CLIReturnCodeError diff --git a/robottelo/host_helpers/contenthost_mixins.py b/robottelo/host_helpers/contenthost_mixins.py index 72fbc585b7c..f009503666a 100644 --- a/robottelo/host_helpers/contenthost_mixins.py +++ b/robottelo/host_helpers/contenthost_mixins.py @@ -1,14 +1,12 @@ """A collection of mixins for robottelo.hosts classes""" -import json from functools import cached_property +import json from tempfile import NamedTemporaryFile from robottelo import constants -from robottelo.config import robottelo_tmp_dir -from robottelo.config import settings +from robottelo.config import robottelo_tmp_dir, settings from robottelo.logging import logger -from robottelo.utils.ohsnap import dogfood_repofile_url -from robottelo.utils.ohsnap import dogfood_repository +from robottelo.utils.ohsnap import dogfood_repofile_url, dogfood_repository class VersionedContent: diff --git a/robottelo/host_helpers/repository_mixins.py b/robottelo/host_helpers/repository_mixins.py index a60affdd75d..99b5576edb9 100644 --- a/robottelo/host_helpers/repository_mixins.py +++ b/robottelo/host_helpers/repository_mixins.py @@ -7,12 +7,14 @@ from robottelo import constants from robottelo.config import settings -from robottelo.exceptions import DistroNotSupportedError -from robottelo.exceptions import OnlyOneOSRepositoryAllowed -from robottelo.exceptions import ReposContentSetupWasNotPerformed -from robottelo.exceptions import RepositoryAlreadyCreated -from robottelo.exceptions import RepositoryAlreadyDefinedError -from robottelo.exceptions import RepositoryDataNotFound +from robottelo.exceptions import ( + DistroNotSupportedError, + OnlyOneOSRepositoryAllowed, + ReposContentSetupWasNotPerformed, + RepositoryAlreadyCreated, + RepositoryAlreadyDefinedError, + RepositoryDataNotFound, +) def initiate_repo_helpers(satellite): diff --git a/robottelo/host_helpers/satellite_mixins.py b/robottelo/host_helpers/satellite_mixins.py index fff44e9f6a9..263510f6783 100644 --- a/robottelo/host_helpers/satellite_mixins.py +++ b/robottelo/host_helpers/satellite_mixins.py @@ -1,19 +1,21 @@ import contextlib +from functools import cache import io import os import random import re -from functools import cache import requests from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.proxy import CapsuleTunnelError from robottelo.config import settings -from robottelo.constants import PULP_EXPORT_DIR -from robottelo.constants import PULP_IMPORT_DIR -from robottelo.constants import PUPPET_COMMON_INSTALLER_OPTS -from robottelo.constants import PUPPET_SATELLITE_INSTALLER +from robottelo.constants import ( + PULP_EXPORT_DIR, + PULP_IMPORT_DIR, + PUPPET_COMMON_INSTALLER_OPTS, + PUPPET_SATELLITE_INSTALLER, +) from robottelo.host_helpers.api_factory import APIFactory from robottelo.host_helpers.cli_factory import CLIFactory from robottelo.host_helpers.ui_factory import UIFactory diff --git a/robottelo/host_helpers/ui_factory.py b/robottelo/host_helpers/ui_factory.py index 334b4986b24..cb85436051e 100644 --- a/robottelo/host_helpers/ui_factory.py +++ b/robottelo/host_helpers/ui_factory.py @@ -5,8 +5,7 @@ """ from fauxfactory import gen_string -from robottelo.constants import DEFAULT_CV -from robottelo.constants import ENVIRONMENT +from robottelo.constants import DEFAULT_CV, ENVIRONMENT class UIFactory: diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 0478a64c07f..0a81a6de7ac 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1,70 +1,64 @@ +from configparser import ConfigParser import contextlib +from contextlib import contextmanager +from datetime import datetime +from functools import cached_property, lru_cache import importlib import io import json +from pathlib import Path, PurePath import random import re -import time -from configparser import ConfigParser -from contextlib import contextmanager -from datetime import datetime -from functools import cached_property -from functools import lru_cache -from pathlib import Path -from pathlib import PurePath from tempfile import NamedTemporaryFile -from urllib.parse import urljoin -from urllib.parse import urlparse -from urllib.parse import urlunsplit +import time +from urllib.parse import urljoin, urlparse, urlunsplit -import requests -import yaml from box import Box from broker import Broker from broker.hosts import Host from dynaconf.vendor.box.exceptions import BoxKeyError -from fauxfactory import gen_alpha -from fauxfactory import gen_string +from fauxfactory import gen_alpha, gen_string from manifester import Manifester from nailgun import entities from packaging.version import Version +import requests from ssh2.exceptions import AuthenticationError -from wait_for import TimedOutError -from wait_for import wait_for +from wait_for import TimedOutError, wait_for from wrapanapi.entities.vm import VmState +import yaml from robottelo import constants from robottelo.cli.base import Base from robottelo.cli.factory import CLIFactoryError -from robottelo.config import configure_airgun -from robottelo.config import configure_nailgun -from robottelo.config import robottelo_tmp_dir -from robottelo.config import settings -from robottelo.constants import CUSTOM_PUPPET_MODULE_REPOS -from robottelo.constants import CUSTOM_PUPPET_MODULE_REPOS_PATH -from robottelo.constants import CUSTOM_PUPPET_MODULE_REPOS_VERSION -from robottelo.constants import DEFAULT_ARCHITECTURE -from robottelo.constants import HAMMER_CONFIG -from robottelo.constants import KEY_CLOAK_CLI -from robottelo.constants import PRDS -from robottelo.constants import REPOS -from robottelo.constants import REPOSET -from robottelo.constants import RHSSO_NEW_GROUP -from robottelo.constants import RHSSO_NEW_USER -from robottelo.constants import RHSSO_RESET_PASSWORD -from robottelo.constants import RHSSO_USER_UPDATE -from robottelo.constants import SATELLITE_VERSION -from robottelo.exceptions import DownloadFileError -from robottelo.exceptions import HostPingFailed -from robottelo.host_helpers import CapsuleMixins -from robottelo.host_helpers import ContentHostMixins -from robottelo.host_helpers import SatelliteMixins +from robottelo.config import ( + configure_airgun, + configure_nailgun, + robottelo_tmp_dir, + settings, +) +from robottelo.constants import ( + CUSTOM_PUPPET_MODULE_REPOS, + CUSTOM_PUPPET_MODULE_REPOS_PATH, + CUSTOM_PUPPET_MODULE_REPOS_VERSION, + DEFAULT_ARCHITECTURE, + HAMMER_CONFIG, + KEY_CLOAK_CLI, + PRDS, + REPOS, + REPOSET, + RHSSO_NEW_GROUP, + RHSSO_NEW_USER, + RHSSO_RESET_PASSWORD, + RHSSO_USER_UPDATE, + SATELLITE_VERSION, +) +from robottelo.exceptions import DownloadFileError, HostPingFailed +from robottelo.host_helpers import CapsuleMixins, ContentHostMixins, SatelliteMixins from robottelo.logging import logger from robottelo.utils import validate_ssh_pub_key from robottelo.utils.datafactory import valid_emails_list from robottelo.utils.installer import InstallerCommand - POWER_OPERATIONS = { VmState.RUNNING: 'running', VmState.STOPPED: 'stopped', @@ -1266,9 +1260,9 @@ def virt_who_hypervisor_config( :param bool upload_manifest: whether to upload the organization manifest :param list extra_repos: (Optional) repositories dict options to setup additionally. """ - from robottelo.cli.org import Org from robottelo.cli import factory as cli_factory from robottelo.cli.lifecycleenvironment import LifecycleEnvironment + from robottelo.cli.org import Org from robottelo.cli.subscription import Subscription from robottelo.cli.virt_who_config import VirtWhoConfig diff --git a/robottelo/logging.py b/robottelo/logging.py index bea19024454..5b09fdeef68 100644 --- a/robottelo/logging.py +++ b/robottelo/logging.py @@ -2,12 +2,11 @@ import os from pathlib import Path -import logzero -import yaml from box import Box from broker.logger import setup_logzero as broker_log_setup +import logzero from manifester.logger import setup_logzero as manifester_log_setup - +import yaml robottelo_root_dir = Path(os.environ.get('ROBOTTELO_DIR', Path(__file__).resolve().parent.parent)) robottelo_log_dir = robottelo_root_dir.joinpath('logs') diff --git a/robottelo/utils/__init__.py b/robottelo/utils/__init__.py index 369c3e35585..e7d0f1e94c6 100644 --- a/robottelo/utils/__init__.py +++ b/robottelo/utils/__init__.py @@ -2,8 +2,8 @@ # Independent utility functions that doesnt need separate module import base64 import os -import re from pathlib import Path +import re from cryptography.hazmat.backends import default_backend as crypto_default_backend from cryptography.hazmat.primitives import serialization as crypto_serialization diff --git a/robottelo/utils/datafactory.py b/robottelo/utils/datafactory.py index 27bf92fabe5..7de716e1046 100644 --- a/robottelo/utils/datafactory.py +++ b/robottelo/utils/datafactory.py @@ -1,18 +1,13 @@ """Data Factory for all entities""" +from functools import wraps import random import string -from functools import wraps from urllib.parse import quote_plus -from fauxfactory import gen_alpha -from fauxfactory import gen_integer -from fauxfactory import gen_string -from fauxfactory import gen_url -from fauxfactory import gen_utf8 +from fauxfactory import gen_alpha, gen_integer, gen_string, gen_url, gen_utf8 from robottelo.config import settings -from robottelo.constants import DOMAIN -from robottelo.constants import STRING_TYPES +from robottelo.constants import DOMAIN, STRING_TYPES class InvalidArgumentError(Exception): diff --git a/robottelo/utils/decorators/__init__.py b/robottelo/utils/decorators/__init__.py index 1d79cba7a26..371afd50f16 100644 --- a/robottelo/utils/decorators/__init__.py +++ b/robottelo/utils/decorators/__init__.py @@ -1,7 +1,6 @@ """Implements various decorators""" from functools import wraps - OBJECT_CACHE = {} diff --git a/robottelo/utils/decorators/func_locker.py b/robottelo/utils/decorators/func_locker.py index 723c4be1ff1..2d575d8f560 100644 --- a/robottelo/utils/decorators/func_locker.py +++ b/robottelo/utils/decorators/func_locker.py @@ -39,11 +39,11 @@ def test_that_conflict_with_test_to_lock(self) with locking_function(self.test_to_lock): # do some operations that conflict with test_to_lock """ +from contextlib import contextmanager import functools import inspect import os import tempfile -from contextlib import contextmanager from pytest_services.locks import file_lock diff --git a/robottelo/utils/decorators/func_shared/shared.py b/robottelo/utils/decorators/func_shared/shared.py index 568f6adf212..62e529b8d8a 100644 --- a/robottelo/utils/decorators/func_shared/shared.py +++ b/robottelo/utils/decorators/func_shared/shared.py @@ -87,24 +87,21 @@ def shared_class_method(cls, org=None, repo=None): import datetime import functools import hashlib +from importlib import import_module import inspect import os import sys import traceback import uuid -from importlib import import_module from nailgun.entities import Entity -from robottelo.config import setting_is_set -from robottelo.config import settings +from robottelo.config import setting_is_set, settings from robottelo.logging import logger -from robottelo.utils.decorators.func_shared import file_storage -from robottelo.utils.decorators.func_shared import redis_storage +from robottelo.utils.decorators.func_shared import file_storage, redis_storage from robottelo.utils.decorators.func_shared.file_storage import FileStorageHandler from robottelo.utils.decorators.func_shared.redis_storage import RedisStorageHandler - _storage_handlers = {'file': FileStorageHandler, 'redis': RedisStorageHandler} DEFAULT_STORAGE_HANDLER = 'file' diff --git a/robottelo/utils/io/__init__.py b/robottelo/utils/io/__init__.py index 2a00c546720..33a4381fd4c 100644 --- a/robottelo/utils/io/__init__.py +++ b/robottelo/utils/io/__init__.py @@ -1,8 +1,8 @@ # Helper methods for tests requiring I/0 import hashlib import json -import tarfile from pathlib import Path +import tarfile def get_local_file_data(path): diff --git a/robottelo/utils/issue_handlers/__init__.py b/robottelo/utils/issue_handlers/__init__.py index d48c1948f24..4789bbc8a74 100644 --- a/robottelo/utils/issue_handlers/__init__.py +++ b/robottelo/utils/issue_handlers/__init__.py @@ -1,7 +1,6 @@ # Methods related to issue handlers in general from robottelo.utils.issue_handlers import bugzilla - handler_methods = {'BZ': bugzilla.is_open_bz} SUPPORTED_HANDLERS = tuple(f"{handler}:" for handler in handler_methods.keys()) diff --git a/robottelo/utils/issue_handlers/bugzilla.py b/robottelo/utils/issue_handlers/bugzilla.py index 6fca11eb894..20836a3660d 100644 --- a/robottelo/utils/issue_handlers/bugzilla.py +++ b/robottelo/utils/issue_handlers/bugzilla.py @@ -1,21 +1,16 @@ -import re from collections import defaultdict +import re +from packaging.version import Version import pytest import requests -from packaging.version import Version -from tenacity import retry -from tenacity import stop_after_attempt -from tenacity import wait_fixed +from tenacity import retry, stop_after_attempt, wait_fixed from robottelo.config import settings -from robottelo.constants import CLOSED_STATUSES -from robottelo.constants import OPEN_STATUSES -from robottelo.constants import WONTFIX_RESOLUTIONS +from robottelo.constants import CLOSED_STATUSES, OPEN_STATUSES, WONTFIX_RESOLUTIONS from robottelo.hosts import get_sat_version from robottelo.logging import logger - # match any version as in `sat-6.2.x` or `sat-6.2.0` or `6.2.9` # The .version group being a `d.d` string that can be casted to Version() VERSION_RE = re.compile(r'(?:sat-)*?(?P\d\.\d)\.\w*') diff --git a/robottelo/utils/manifest.py b/robottelo/utils/manifest.py index 58539b6cb36..52c220f3c59 100644 --- a/robottelo/utils/manifest.py +++ b/robottelo/utils/manifest.py @@ -4,11 +4,10 @@ import uuid import zipfile -import requests from cryptography.hazmat.backends import default_backend as crypto_default_backend -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives import serialization as crypto_serialization +from cryptography.hazmat.primitives import hashes, serialization as crypto_serialization from cryptography.hazmat.primitives.asymmetric import padding +import requests from robottelo.config import settings diff --git a/robottelo/utils/ohsnap.py b/robottelo/utils/ohsnap.py index ac9bf6b81c1..74494766114 100644 --- a/robottelo/utils/ohsnap.py +++ b/robottelo/utils/ohsnap.py @@ -1,12 +1,11 @@ """Utility module to communicate with Ohsnap API""" -import requests from box import Box from packaging.version import Version +import requests from wait_for import wait_for from robottelo import constants -from robottelo.exceptions import InvalidArgumentError -from robottelo.exceptions import RepositoryDataNotFound +from robottelo.exceptions import InvalidArgumentError, RepositoryDataNotFound from robottelo.logging import logger diff --git a/robottelo/utils/report_portal/portal.py b/robottelo/utils/report_portal/portal.py index bf377d52f20..3d44ac6c691 100644 --- a/robottelo/utils/report_portal/portal.py +++ b/robottelo/utils/report_portal/portal.py @@ -1,7 +1,5 @@ import requests -from tenacity import retry -from tenacity import stop_after_attempt -from tenacity import wait_fixed +from tenacity import retry, stop_after_attempt, wait_fixed from robottelo.config import settings from robottelo.logging import logger diff --git a/robottelo/utils/virtwho.py b/robottelo/utils/virtwho.py index b53480265b1..8c7a6c48658 100644 --- a/robottelo/utils/virtwho.py +++ b/robottelo/utils/virtwho.py @@ -3,11 +3,9 @@ import re import uuid -import requests -from fauxfactory import gen_integer -from fauxfactory import gen_string -from fauxfactory import gen_url +from fauxfactory import gen_integer, gen_string, gen_url from nailgun import entities +import requests from wait_for import wait_for from robottelo import ssh diff --git a/scripts/config_helpers.py b/scripts/config_helpers.py index 6176fa1cb5e..85283bea05e 100644 --- a/scripts/config_helpers.py +++ b/scripts/config_helpers.py @@ -3,8 +3,8 @@ import click import deepdiff -import yaml from logzero import logger +import yaml def merge_nested_dictionaries(original, new, overwrite=False): diff --git a/scripts/graph_entities.py b/scripts/graph_entities.py index ff21877c31c..a404c616911 100755 --- a/scripts/graph_entities.py +++ b/scripts/graph_entities.py @@ -9,8 +9,7 @@ """ import inspect -from nailgun import entities -from nailgun import entity_mixins +from nailgun import entities, entity_mixins def graph(): diff --git a/scripts/tokenize_customer_scenario.py b/scripts/tokenize_customer_scenario.py index 273a2075d4d..14a0cbf75a5 100644 --- a/scripts/tokenize_customer_scenario.py +++ b/scripts/tokenize_customer_scenario.py @@ -14,12 +14,9 @@ $ python scripts/tokenize_customer_scenario.py """ import codemod -from codemod import Query -from codemod import regex_suggestor -from codemod import run_interactive +from codemod import Query, regex_suggestor, run_interactive from codemod.helpers import path_filter - codemod.base.yes_to_all = True # this script can be changed to accept this list as param diff --git a/scripts/vault_login.py b/scripts/vault_login.py index 6f311bfeadc..22d3313b270 100755 --- a/scripts/vault_login.py +++ b/scripts/vault_login.py @@ -2,15 +2,14 @@ # This Enables and Disables individuals OIDC token to access secrets from vault import json import os +from pathlib import Path import re import subprocess import sys -from pathlib import Path from robottelo.constants import Colored from robottelo.utils import export_vault_env_vars - HELP_TEXT = ( "Vault CLI in not installed in your system, " "refer link https://learn.hashicorp.com/tutorials/vault/getting-started-install to " diff --git a/setup.py b/setup.py index e173ae35c1e..7e0feb9f2b3 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ #!/usr/bin/env python -from setuptools import find_packages -from setuptools import setup +from setuptools import find_packages, setup with open('README.rst') as f: README = f.read() diff --git a/tests/foreman/api/test_acs.py b/tests/foreman/api/test_acs.py index 773b2b300fe..bb4cfaf125e 100644 --- a/tests/foreman/api/test_acs.py +++ b/tests/foreman/api/test_acs.py @@ -16,12 +16,11 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from requests.exceptions import HTTPError -from robottelo.constants.repos import PULP_FIXTURE_ROOT -from robottelo.constants.repos import PULP_SUBPATHS_COMBINED +from robottelo.constants.repos import PULP_FIXTURE_ROOT, PULP_SUBPATHS_COMBINED @pytest.mark.e2e diff --git a/tests/foreman/api/test_activationkey.py b/tests/foreman/api/test_activationkey.py index 0a4c48de8a3..96da8a8f4c8 100644 --- a/tests/foreman/api/test_activationkey.py +++ b/tests/foreman/api/test_activationkey.py @@ -18,22 +18,19 @@ """ import http +from fauxfactory import gen_integer, gen_string +from nailgun import client, entities import pytest -from fauxfactory import gen_integer -from fauxfactory import gen_string -from nailgun import client -from nailgun import entities from requests.exceptions import HTTPError -from robottelo.config import get_credentials -from robottelo.config import user_nailgun_config -from robottelo.constants import PRDS -from robottelo.constants import REPOS -from robottelo.constants import REPOSET -from robottelo.utils.datafactory import filtered_datapoint -from robottelo.utils.datafactory import invalid_names_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.config import get_credentials, user_nailgun_config +from robottelo.constants import PRDS, REPOS, REPOSET +from robottelo.utils.datafactory import ( + filtered_datapoint, + invalid_names_list, + parametrized, + valid_data_list, +) @filtered_datapoint diff --git a/tests/foreman/api/test_ansible.py b/tests/foreman/api/test_ansible.py index 3a9b8f0a726..b6435d77f8a 100644 --- a/tests/foreman/api/test_ansible.py +++ b/tests/foreman/api/test_ansible.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings diff --git a/tests/foreman/api/test_architecture.py b/tests/foreman/api/test_architecture.py index cb34a1513ec..f88c8adba18 100644 --- a/tests/foreman/api/test_architecture.py +++ b/tests/foreman/api/test_architecture.py @@ -16,14 +16,16 @@ :Upstream: No """ -import pytest from fauxfactory import gen_choice from nailgun import entities +import pytest from requests.exceptions import HTTPError -from robottelo.utils.datafactory import invalid_names_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + invalid_names_list, + parametrized, + valid_data_list, +) @pytest.mark.tier1 diff --git a/tests/foreman/api/test_audit.py b/tests/foreman/api/test_audit.py index 5169e44dbca..59b19d0806c 100644 --- a/tests/foreman/api/test_audit.py +++ b/tests/foreman/api/test_audit.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from nailgun import entities +import pytest from robottelo.utils.datafactory import gen_string diff --git a/tests/foreman/api/test_bookmarks.py b/tests/foreman/api/test_bookmarks.py index 91c8ef2df8a..71ae30391ab 100644 --- a/tests/foreman/api/test_bookmarks.py +++ b/tests/foreman/api/test_bookmarks.py @@ -18,15 +18,13 @@ """ import random -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from requests.exceptions import HTTPError from robottelo.constants import BOOKMARK_ENTITIES -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import valid_data_list - +from robottelo.utils.datafactory import invalid_values_list, valid_data_list # List of unique bookmark controller values, preserving order CONTROLLERS = list(dict.fromkeys(entity['controller'] for entity in BOOKMARK_ENTITIES)) diff --git a/tests/foreman/api/test_capsule.py b/tests/foreman/api/test_capsule.py index 0b15f867ae3..b8304979e70 100644 --- a/tests/foreman/api/test_capsule.py +++ b/tests/foreman/api/test_capsule.py @@ -16,9 +16,8 @@ :Upstream: No """ +from fauxfactory import gen_string, gen_url import pytest -from fauxfactory import gen_string -from fauxfactory import gen_url from requests import HTTPError from robottelo.config import user_nailgun_config diff --git a/tests/foreman/api/test_capsulecontent.py b/tests/foreman/api/test_capsulecontent.py index 5c390007bb5..120b0c0354e 100644 --- a/tests/foreman/api/test_capsulecontent.py +++ b/tests/foreman/api/test_capsulecontent.py @@ -17,22 +17,23 @@ :Upstream: No """ -import re from datetime import datetime +import re from time import sleep -import pytest -from nailgun import client -from nailgun import entities +from nailgun import client, entities from nailgun.entity_mixins import call_entity_method_with_timeout +import pytest from robottelo import constants from robottelo.config import settings from robottelo.constants import DataFile from robottelo.constants.repos import ANSIBLE_GALAXY -from robottelo.content_info import get_repo_files_by_url -from robottelo.content_info import get_repomd -from robottelo.content_info import get_repomd_revision +from robottelo.content_info import ( + get_repo_files_by_url, + get_repomd, + get_repomd_revision, +) from robottelo.utils.issue_handlers import is_open diff --git a/tests/foreman/api/test_classparameters.py b/tests/foreman/api/test_classparameters.py index fb4d3da4f2f..de34f81ae11 100644 --- a/tests/foreman/api/test_classparameters.py +++ b/tests/foreman/api/test_classparameters.py @@ -19,15 +19,12 @@ import json from random import choice +from fauxfactory import gen_boolean, gen_integer, gen_string import pytest -from fauxfactory import gen_boolean -from fauxfactory import gen_integer -from fauxfactory import gen_string from requests import HTTPError from robottelo.config import settings -from robottelo.utils.datafactory import filtered_datapoint -from robottelo.utils.datafactory import parametrized +from robottelo.utils.datafactory import filtered_datapoint, parametrized @filtered_datapoint diff --git a/tests/foreman/api/test_computeprofile.py b/tests/foreman/api/test_computeprofile.py index 4cb695a9290..d0ee003c7a3 100644 --- a/tests/foreman/api/test_computeprofile.py +++ b/tests/foreman/api/test_computeprofile.py @@ -16,13 +16,15 @@ :Upstream: No """ -import pytest from nailgun import entities +import pytest from requests.exceptions import HTTPError -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + invalid_values_list, + parametrized, + valid_data_list, +) @pytest.mark.parametrize('name', **parametrized(valid_data_list())) diff --git a/tests/foreman/api/test_computeresource_azurerm.py b/tests/foreman/api/test_computeresource_azurerm.py index 89501467e70..74d6a733093 100644 --- a/tests/foreman/api/test_computeresource_azurerm.py +++ b/tests/foreman/api/test_computeresource_azurerm.py @@ -16,16 +16,18 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.constants import AZURERM_FILE_URI -from robottelo.constants import AZURERM_PLATFORM_DEFAULT -from robottelo.constants import AZURERM_PREMIUM_OS_Disk -from robottelo.constants import AZURERM_RHEL7_FT_CUSTOM_IMG_URN -from robottelo.constants import AZURERM_RHEL7_UD_IMG_URN -from robottelo.constants import AZURERM_VM_SIZE_DEFAULT +from robottelo.constants import ( + AZURERM_FILE_URI, + AZURERM_PLATFORM_DEFAULT, + AZURERM_RHEL7_FT_CUSTOM_IMG_URN, + AZURERM_RHEL7_UD_IMG_URN, + AZURERM_VM_SIZE_DEFAULT, + AZURERM_PREMIUM_OS_Disk, +) class TestAzureRMComputeResourceTestCase: diff --git a/tests/foreman/api/test_computeresource_gce.py b/tests/foreman/api/test_computeresource_gce.py index eb841508176..69bcdd7cbdf 100644 --- a/tests/foreman/api/test_computeresource_gce.py +++ b/tests/foreman/api/test_computeresource_gce.py @@ -21,12 +21,11 @@ """ import random -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.constants import GCE_RHEL_CLOUD_PROJECTS -from robottelo.constants import VALID_GCE_ZONES +from robottelo.constants import GCE_RHEL_CLOUD_PROJECTS, VALID_GCE_ZONES @pytest.mark.skip_if_not_set('gce') diff --git a/tests/foreman/api/test_computeresource_libvirt.py b/tests/foreman/api/test_computeresource_libvirt.py index a76e4d84ee5..e7e0112a0d8 100644 --- a/tests/foreman/api/test_computeresource_libvirt.py +++ b/tests/foreman/api/test_computeresource_libvirt.py @@ -20,16 +20,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from requests.exceptions import HTTPError from robottelo.config import settings -from robottelo.constants import FOREMAN_PROVIDERS -from robottelo.constants import LIBVIRT_RESOURCE_URL -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.constants import FOREMAN_PROVIDERS, LIBVIRT_RESOURCE_URL +from robottelo.utils.datafactory import ( + invalid_values_list, + parametrized, + valid_data_list, +) pytestmark = [pytest.mark.skip_if_not_set('libvirt')] diff --git a/tests/foreman/api/test_contentcredentials.py b/tests/foreman/api/test_contentcredentials.py index 71053b2900f..81602c953e4 100644 --- a/tests/foreman/api/test_contentcredentials.py +++ b/tests/foreman/api/test_contentcredentials.py @@ -18,15 +18,17 @@ """ from copy import copy -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from requests import HTTPError from robottelo.constants import DataFile -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + invalid_values_list, + parametrized, + valid_data_list, +) key_content = DataFile.VALID_GPG_KEY_FILE.read_text() diff --git a/tests/foreman/api/test_contentview.py b/tests/foreman/api/test_contentview.py index 7bacc6b7b6b..0e1e94c88d4 100644 --- a/tests/foreman/api/test_contentview.py +++ b/tests/foreman/api/test_contentview.py @@ -18,28 +18,28 @@ """ import random -import pytest -from fauxfactory import gen_integer -from fauxfactory import gen_string -from fauxfactory import gen_utf8 +from fauxfactory import gen_integer, gen_string, gen_utf8 from nailgun import entities +import pytest from requests.exceptions import HTTPError -from robottelo.config import settings -from robottelo.config import user_nailgun_config -from robottelo.constants import CONTAINER_REGISTRY_HUB -from robottelo.constants import CUSTOM_RPM_SHA_512_FEED_COUNT -from robottelo.constants import DataFile -from robottelo.constants import FILTER_ERRATA_TYPE -from robottelo.constants import PERMISSIONS -from robottelo.constants import PRDS -from robottelo.constants import REPOS -from robottelo.constants import REPOSET -from robottelo.constants.repos import CUSTOM_RPM_SHA_512 -from robottelo.constants.repos import FEDORA_OSTREE_REPO -from robottelo.utils.datafactory import invalid_names_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.config import settings, user_nailgun_config +from robottelo.constants import ( + CONTAINER_REGISTRY_HUB, + CUSTOM_RPM_SHA_512_FEED_COUNT, + FILTER_ERRATA_TYPE, + PERMISSIONS, + PRDS, + REPOS, + REPOSET, + DataFile, +) +from robottelo.constants.repos import CUSTOM_RPM_SHA_512, FEDORA_OSTREE_REPO +from robottelo.utils.datafactory import ( + invalid_names_list, + parametrized, + valid_data_list, +) # Some tests repeatedly publish content views or promote content view versions. # How many times should that be done? A higher number means a more interesting diff --git a/tests/foreman/api/test_contentviewfilter.py b/tests/foreman/api/test_contentviewfilter.py index e07fcd0d6f4..b0325ceecec 100644 --- a/tests/foreman/api/test_contentviewfilter.py +++ b/tests/foreman/api/test_contentviewfilter.py @@ -23,19 +23,18 @@ import http from random import randint +from fauxfactory import gen_integer, gen_string +from nailgun import client, entities import pytest -from fauxfactory import gen_integer -from fauxfactory import gen_string -from nailgun import client -from nailgun import entities from requests.exceptions import HTTPError -from robottelo.config import get_credentials -from robottelo.config import settings +from robottelo.config import get_credentials, settings from robottelo.constants import CONTAINER_REGISTRY_HUB -from robottelo.utils.datafactory import invalid_names_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + invalid_names_list, + parametrized, + valid_data_list, +) @pytest.fixture(scope='module') diff --git a/tests/foreman/api/test_contentviewversion.py b/tests/foreman/api/test_contentviewversion.py index 50cabbadcb2..ca0e5e7cf36 100644 --- a/tests/foreman/api/test_contentviewversion.py +++ b/tests/foreman/api/test_contentviewversion.py @@ -16,16 +16,18 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from requests.exceptions import HTTPError from robottelo.config import settings -from robottelo.constants import CONTAINER_REGISTRY_HUB -from robottelo.constants import DataFile -from robottelo.constants import DEFAULT_CV -from robottelo.constants import ENVIRONMENT +from robottelo.constants import ( + CONTAINER_REGISTRY_HUB, + DEFAULT_CV, + ENVIRONMENT, + DataFile, +) @pytest.fixture(scope='module') diff --git a/tests/foreman/api/test_convert2rhel.py b/tests/foreman/api/test_convert2rhel.py index a9a6d9ace0f..ce2e78d85b2 100644 --- a/tests/foreman/api/test_convert2rhel.py +++ b/tests/foreman/api/test_convert2rhel.py @@ -18,9 +18,7 @@ import requests from robottelo.config import settings -from robottelo.constants import DEFAULT_ARCHITECTURE -from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME -from robottelo.constants import REPOS +from robottelo.constants import DEFAULT_ARCHITECTURE, DEFAULT_SUBSCRIPTION_NAME, REPOS def create_repo(sat, org, repo_url, ssl_cert=None): diff --git a/tests/foreman/api/test_discoveredhost.py b/tests/foreman/api/test_discoveredhost.py index ee2f1aa92ff..9b4a5ae5e22 100644 --- a/tests/foreman/api/test_discoveredhost.py +++ b/tests/foreman/api/test_discoveredhost.py @@ -16,14 +16,10 @@ """ import re -import pytest -from fauxfactory import gen_choice -from fauxfactory import gen_ipaddr -from fauxfactory import gen_mac -from fauxfactory import gen_string +from fauxfactory import gen_choice, gen_ipaddr, gen_mac, gen_string from nailgun import entity_mixins -from wait_for import TimedOutError -from wait_for import wait_for +import pytest +from wait_for import TimedOutError, wait_for from robottelo.logging import logger from robottelo.utils.datafactory import valid_data_list diff --git a/tests/foreman/api/test_discoveryrule.py b/tests/foreman/api/test_discoveryrule.py index 2c3680e2985..f55a287d675 100644 --- a/tests/foreman/api/test_discoveryrule.py +++ b/tests/foreman/api/test_discoveryrule.py @@ -16,11 +16,9 @@ :Upstream: No """ -import pytest -from fauxfactory import gen_choice -from fauxfactory import gen_integer -from fauxfactory import gen_string +from fauxfactory import gen_choice, gen_integer, gen_string from nailgun import entities +import pytest from requests.exceptions import HTTPError from robottelo.utils.datafactory import valid_data_list diff --git a/tests/foreman/api/test_docker.py b/tests/foreman/api/test_docker.py index 5d5fe969a72..e87dac66fb6 100644 --- a/tests/foreman/api/test_docker.py +++ b/tests/foreman/api/test_docker.py @@ -12,23 +12,21 @@ :Upstream: No """ -from random import choice -from random import randint -from random import shuffle +from random import choice, randint, shuffle -import pytest -from fauxfactory import gen_string -from fauxfactory import gen_url +from fauxfactory import gen_string, gen_url from nailgun import entities +import pytest from requests.exceptions import HTTPError -from robottelo.constants import CONTAINER_REGISTRY_HUB -from robottelo.constants import CONTAINER_UPSTREAM_NAME -from robottelo.utils.datafactory import generate_strings_list -from robottelo.utils.datafactory import invalid_docker_upstream_names -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_docker_repository_names -from robottelo.utils.datafactory import valid_docker_upstream_names +from robottelo.constants import CONTAINER_REGISTRY_HUB, CONTAINER_UPSTREAM_NAME +from robottelo.utils.datafactory import ( + generate_strings_list, + invalid_docker_upstream_names, + parametrized, + valid_docker_repository_names, + valid_docker_upstream_names, +) DOCKER_PROVIDER = 'Docker' diff --git a/tests/foreman/api/test_environment.py b/tests/foreman/api/test_environment.py index 1225659cb18..cb965244211 100644 --- a/tests/foreman/api/test_environment.py +++ b/tests/foreman/api/test_environment.py @@ -20,14 +20,16 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from requests.exceptions import HTTPError -from robottelo.utils.datafactory import invalid_environments_list -from robottelo.utils.datafactory import invalid_names_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_environments_list +from robottelo.utils.datafactory import ( + invalid_environments_list, + invalid_names_list, + parametrized, + valid_environments_list, +) @pytest.mark.e2e diff --git a/tests/foreman/api/test_errata.py b/tests/foreman/api/test_errata.py index 7e0afba949d..f33931fe833 100644 --- a/tests/foreman/api/test_errata.py +++ b/tests/foreman/api/test_errata.py @@ -19,12 +19,11 @@ # For ease of use hc refers to host-collection throughout this document from time import sleep -import pytest from nailgun import entities +import pytest from robottelo import constants -from robottelo.cli.factory import setup_org_for_a_custom_repo -from robottelo.cli.factory import setup_org_for_a_rh_repo +from robottelo.cli.factory import setup_org_for_a_custom_repo, setup_org_for_a_rh_repo from robottelo.cli.host import Host from robottelo.config import settings from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME diff --git a/tests/foreman/api/test_filter.py b/tests/foreman/api/test_filter.py index 98cf6c26f51..75d42707e4a 100644 --- a/tests/foreman/api/test_filter.py +++ b/tests/foreman/api/test_filter.py @@ -20,8 +20,8 @@ :Upstream: No """ -import pytest from nailgun import entities +import pytest from requests.exceptions import HTTPError diff --git a/tests/foreman/api/test_foremantask.py b/tests/foreman/api/test_foremantask.py index 6ddcc3c0884..f7e8377e82f 100644 --- a/tests/foreman/api/test_foremantask.py +++ b/tests/foreman/api/test_foremantask.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from nailgun import entities +import pytest from requests.exceptions import HTTPError diff --git a/tests/foreman/api/test_host.py b/tests/foreman/api/test_host.py index e43c2f7669e..8393dd5ecf0 100644 --- a/tests/foreman/api/test_host.py +++ b/tests/foreman/api/test_host.py @@ -22,19 +22,13 @@ """ import http +from fauxfactory import gen_choice, gen_integer, gen_ipaddr, gen_mac, gen_string +from nailgun import client, entities import pytest -from fauxfactory import gen_choice -from fauxfactory import gen_integer -from fauxfactory import gen_ipaddr -from fauxfactory import gen_mac -from fauxfactory import gen_string -from nailgun import client -from nailgun import entities from requests.exceptions import HTTPError from robottelo.config import get_credentials -from robottelo.constants import DEFAULT_CV -from robottelo.constants import ENVIRONMENT +from robottelo.constants import DEFAULT_CV, ENVIRONMENT from robottelo.utils import datafactory diff --git a/tests/foreman/api/test_hostcollection.py b/tests/foreman/api/test_hostcollection.py index 5484dfd4ea9..0b5c0147ead 100644 --- a/tests/foreman/api/test_hostcollection.py +++ b/tests/foreman/api/test_hostcollection.py @@ -16,18 +16,19 @@ :Upstream: No """ -from random import choice -from random import randint +from random import choice, randint -import pytest from broker import Broker from nailgun import entities +import pytest from requests.exceptions import HTTPError from robottelo.hosts import ContentHost -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + invalid_values_list, + parametrized, + valid_data_list, +) @pytest.fixture(scope='module') diff --git a/tests/foreman/api/test_hostgroup.py b/tests/foreman/api/test_hostgroup.py index 405ed180dea..9e3c3aa0c21 100644 --- a/tests/foreman/api/test_hostgroup.py +++ b/tests/foreman/api/test_hostgroup.py @@ -18,17 +18,17 @@ """ from random import randint -import pytest from fauxfactory import gen_string -from nailgun import client -from nailgun import entities -from nailgun import entity_fields +from nailgun import client, entities, entity_fields +import pytest from requests.exceptions import HTTPError from robottelo.config import get_credentials -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_hostgroups_list +from robottelo.utils.datafactory import ( + invalid_values_list, + parametrized, + valid_hostgroups_list, +) @pytest.fixture diff --git a/tests/foreman/api/test_http_proxy.py b/tests/foreman/api/test_http_proxy.py index 5244450d409..f9c3023c77e 100644 --- a/tests/foreman/api/test_http_proxy.py +++ b/tests/foreman/api/test_http_proxy.py @@ -16,9 +16,9 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo import constants from robottelo.config import settings diff --git a/tests/foreman/api/test_ldapauthsource.py b/tests/foreman/api/test_ldapauthsource.py index e896397c4eb..10b9851b3d6 100644 --- a/tests/foreman/api/test_ldapauthsource.py +++ b/tests/foreman/api/test_ldapauthsource.py @@ -16,12 +16,11 @@ :Upstream: No """ -import pytest from nailgun import entities +import pytest from requests.exceptions import HTTPError -from robottelo.constants import LDAP_ATTR -from robottelo.constants import LDAP_SERVER_TYPE +from robottelo.constants import LDAP_ATTR, LDAP_SERVER_TYPE from robottelo.utils.datafactory import generate_strings_list diff --git a/tests/foreman/api/test_lifecycleenvironment.py b/tests/foreman/api/test_lifecycleenvironment.py index 57effa04b1a..b8e7b3ef347 100644 --- a/tests/foreman/api/test_lifecycleenvironment.py +++ b/tests/foreman/api/test_lifecycleenvironment.py @@ -20,15 +20,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from requests.exceptions import HTTPError from robottelo.constants import ENVIRONMENT -from robottelo.utils.datafactory import invalid_names_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + invalid_names_list, + parametrized, + valid_data_list, +) @pytest.fixture(scope='module') diff --git a/tests/foreman/api/test_location.py b/tests/foreman/api/test_location.py index 22546cd06cd..ad226389c57 100644 --- a/tests/foreman/api/test_location.py +++ b/tests/foreman/api/test_location.py @@ -21,15 +21,16 @@ """ from random import randint +from fauxfactory import gen_integer, gen_string import pytest -from fauxfactory import gen_integer -from fauxfactory import gen_string from requests.exceptions import HTTPError from robottelo.constants import DEFAULT_LOC -from robottelo.utils.datafactory import filtered_datapoint -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized +from robottelo.utils.datafactory import ( + filtered_datapoint, + invalid_values_list, + parametrized, +) @filtered_datapoint diff --git a/tests/foreman/api/test_media.py b/tests/foreman/api/test_media.py index 1f2abbbdefe..e5524d914fd 100644 --- a/tests/foreman/api/test_media.py +++ b/tests/foreman/api/test_media.py @@ -18,16 +18,17 @@ """ import random -import pytest -from fauxfactory import gen_string -from fauxfactory import gen_url +from fauxfactory import gen_string, gen_url from nailgun import entities +import pytest from requests.exceptions import HTTPError from robottelo.constants import OPERATING_SYSTEMS -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + invalid_values_list, + parametrized, + valid_data_list, +) class TestMedia: diff --git a/tests/foreman/api/test_multiple_paths.py b/tests/foreman/api/test_multiple_paths.py index f2966bfa018..66b4bc7ea9a 100644 --- a/tests/foreman/api/test_multiple_paths.py +++ b/tests/foreman/api/test_multiple_paths.py @@ -18,17 +18,13 @@ """ import http +from nailgun import client, entities, entity_fields import pytest -from nailgun import client -from nailgun import entities -from nailgun import entity_fields -from robottelo.config import get_credentials -from robottelo.config import user_nailgun_config +from robottelo.config import get_credentials, user_nailgun_config from robottelo.logging import logger from robottelo.utils.datafactory import parametrized - VALID_ENTITIES = { entities.ActivationKey, entities.Architecture, diff --git a/tests/foreman/api/test_notifications.py b/tests/foreman/api/test_notifications.py index c832bfd0990..a8b4c24ebf8 100644 --- a/tests/foreman/api/test_notifications.py +++ b/tests/foreman/api/test_notifications.py @@ -20,14 +20,12 @@ from re import findall from tempfile import mkstemp -import pytest from fauxfactory import gen_string -from wait_for import TimedOutError -from wait_for import wait_for +import pytest +from wait_for import TimedOutError, wait_for from robottelo.config import settings -from robottelo.constants import DEFAULT_LOC -from robottelo.constants import DEFAULT_ORG +from robottelo.constants import DEFAULT_LOC, DEFAULT_ORG from robottelo.utils.issue_handlers import is_open diff --git a/tests/foreman/api/test_operatingsystem.py b/tests/foreman/api/test_operatingsystem.py index 722362063a3..4cdb27a04a2 100644 --- a/tests/foreman/api/test_operatingsystem.py +++ b/tests/foreman/api/test_operatingsystem.py @@ -16,17 +16,19 @@ :Upstream: No """ -import random from http.client import NOT_FOUND +import random -import pytest from fauxfactory import gen_string +import pytest from requests.exceptions import HTTPError from robottelo.constants import OPERATING_SYSTEMS -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + invalid_values_list, + parametrized, + valid_data_list, +) class TestOperatingSystemParameter: diff --git a/tests/foreman/api/test_organization.py b/tests/foreman/api/test_organization.py index 540a466f239..692504faf79 100644 --- a/tests/foreman/api/test_organization.py +++ b/tests/foreman/api/test_organization.py @@ -23,17 +23,18 @@ import json from random import randint -import pytest from fauxfactory import gen_string -from nailgun import client -from nailgun import entities +from nailgun import client, entities +import pytest from requests.exceptions import HTTPError from robottelo.config import get_credentials from robottelo.constants import DEFAULT_ORG -from robottelo.utils.datafactory import filtered_datapoint -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized +from robottelo.utils.datafactory import ( + filtered_datapoint, + invalid_values_list, + parametrized, +) from robottelo.utils.issue_handlers import is_open diff --git a/tests/foreman/api/test_oscap_tailoringfiles.py b/tests/foreman/api/test_oscap_tailoringfiles.py index acb748203b2..d29eed1ca95 100644 --- a/tests/foreman/api/test_oscap_tailoringfiles.py +++ b/tests/foreman/api/test_oscap_tailoringfiles.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from nailgun import entities +import pytest from robottelo.utils.datafactory import gen_string diff --git a/tests/foreman/api/test_oscappolicy.py b/tests/foreman/api/test_oscappolicy.py index 60a55c2dbaa..1efdd3e3779 100644 --- a/tests/foreman/api/test_oscappolicy.py +++ b/tests/foreman/api/test_oscappolicy.py @@ -16,9 +16,9 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest class TestOscapPolicy: diff --git a/tests/foreman/api/test_partitiontable.py b/tests/foreman/api/test_partitiontable.py index a8046111449..b1f64b8f5ac 100644 --- a/tests/foreman/api/test_partitiontable.py +++ b/tests/foreman/api/test_partitiontable.py @@ -22,16 +22,17 @@ """ import random +from fauxfactory import gen_integer, gen_string import pytest -from fauxfactory import gen_integer -from fauxfactory import gen_string from requests.exceptions import HTTPError from robottelo.constants import OPERATING_SYSTEMS -from robottelo.utils.datafactory import generate_strings_list -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + generate_strings_list, + invalid_values_list, + parametrized, + valid_data_list, +) class TestPartitionTable: diff --git a/tests/foreman/api/test_permission.py b/tests/foreman/api/test_permission.py index 1dac5452ade..f8eb16a1500 100644 --- a/tests/foreman/api/test_permission.py +++ b/tests/foreman/api/test_permission.py @@ -20,14 +20,14 @@ :Upstream: No """ +from itertools import chain import json import re -from itertools import chain -import pytest from fauxfactory import gen_alphanumeric from nailgun import entities from nailgun.entity_fields import OneToManyField +import pytest from requests.exceptions import HTTPError from robottelo.config import user_nailgun_config diff --git a/tests/foreman/api/test_product.py b/tests/foreman/api/test_product.py index 47010338795..84b6d78f866 100644 --- a/tests/foreman/api/test_product.py +++ b/tests/foreman/api/test_product.py @@ -19,19 +19,23 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from requests.exceptions import HTTPError from robottelo.config import settings -from robottelo.constants import CONTAINER_REGISTRY_HUB -from robottelo.constants import CONTAINER_UPSTREAM_NAME -from robottelo.constants import DataFile -from robottelo.constants import REPO_TYPE -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.constants import ( + CONTAINER_REGISTRY_HUB, + CONTAINER_UPSTREAM_NAME, + REPO_TYPE, + DataFile, +) +from robottelo.utils.datafactory import ( + invalid_values_list, + parametrized, + valid_data_list, +) @pytest.mark.tier1 diff --git a/tests/foreman/api/test_provisioning.py b/tests/foreman/api/test_provisioning.py index 42b5ecb6a69..3b4ded29dc8 100644 --- a/tests/foreman/api/test_provisioning.py +++ b/tests/foreman/api/test_provisioning.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from wait_for import wait_for from robottelo.config import settings diff --git a/tests/foreman/api/test_provisioning_puppet.py b/tests/foreman/api/test_provisioning_puppet.py index 195f458b3b9..a32d542abb3 100644 --- a/tests/foreman/api/test_provisioning_puppet.py +++ b/tests/foreman/api/test_provisioning_puppet.py @@ -16,10 +16,10 @@ :Upstream: No """ -import pytest -import requests from fauxfactory import gen_string from packaging.version import Version +import pytest +import requests from wait_for import wait_for diff --git a/tests/foreman/api/test_provisioningtemplate.py b/tests/foreman/api/test_provisioningtemplate.py index 3c80cbf265d..ac0567181c5 100644 --- a/tests/foreman/api/test_provisioningtemplate.py +++ b/tests/foreman/api/test_provisioningtemplate.py @@ -21,18 +21,13 @@ """ from random import choice -import pytest -from fauxfactory import gen_choice -from fauxfactory import gen_integer -from fauxfactory import gen_mac -from fauxfactory import gen_string +from fauxfactory import gen_choice, gen_integer, gen_mac, gen_string from nailgun import client +import pytest from requests.exceptions import HTTPError -from robottelo.config import settings -from robottelo.config import user_nailgun_config -from robottelo.utils.datafactory import invalid_names_list -from robottelo.utils.datafactory import valid_data_list +from robottelo.config import settings, user_nailgun_config +from robottelo.utils.datafactory import invalid_names_list, valid_data_list @pytest.fixture(scope='module') diff --git a/tests/foreman/api/test_reporttemplates.py b/tests/foreman/api/test_reporttemplates.py index 955d9ae1942..5047a9a616c 100644 --- a/tests/foreman/api/test_reporttemplates.py +++ b/tests/foreman/api/test_reporttemplates.py @@ -16,24 +16,25 @@ :Upstream: No """ -import pytest from broker import Broker from fauxfactory import gen_string from nailgun import entities +import pytest from requests import HTTPError from wait_for import wait_for from robottelo.config import settings -from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE_NAME -from robottelo.constants import FAKE_2_CUSTOM_PACKAGE -from robottelo.constants import PRDS -from robottelo.constants import REPOS -from robottelo.constants import REPOSET +from robottelo.constants import ( + DEFAULT_SUBSCRIPTION_NAME, + FAKE_1_CUSTOM_PACKAGE, + FAKE_1_CUSTOM_PACKAGE_NAME, + FAKE_2_CUSTOM_PACKAGE, + PRDS, + REPOS, + REPOSET, +) from robottelo.hosts import ContentHost -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import parametrized, valid_data_list from robottelo.utils.issue_handlers import is_open diff --git a/tests/foreman/api/test_repositories.py b/tests/foreman/api/test_repositories.py index 29675dfa2eb..590ae50ae98 100644 --- a/tests/foreman/api/test_repositories.py +++ b/tests/foreman/api/test_repositories.py @@ -16,10 +16,10 @@ :Upstream: No """ -import pytest from manifester import Manifester from nailgun import entities from nailgun.entity_mixins import call_entity_method_with_timeout +import pytest from requests.exceptions import HTTPError from robottelo import constants diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index b12c7389cc3..bd63ad606cf 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -17,25 +17,20 @@ :Upstream: No """ import re +from string import punctuation import tempfile import time -from string import punctuation -from urllib.parse import urljoin -from urllib.parse import urlparse -from urllib.parse import urlunparse +from urllib.parse import urljoin, urlparse, urlunparse -import pytest from fauxfactory import gen_string -from nailgun import client -from nailgun import entities -from nailgun.entity_mixins import call_entity_method_with_timeout -from nailgun.entity_mixins import TaskFailedError +from nailgun import client, entities +from nailgun.entity_mixins import TaskFailedError, call_entity_method_with_timeout +import pytest from requests.exceptions import HTTPError from robottelo import constants from robottelo.config import settings -from robottelo.constants import DataFile -from robottelo.constants import repos as repo_constants +from robottelo.constants import DataFile, repos as repo_constants from robottelo.content_info import get_repo_files_by_url from robottelo.logging import logger from robottelo.utils import datafactory diff --git a/tests/foreman/api/test_repository_set.py b/tests/foreman/api/test_repository_set.py index 7dd560c9cb9..449bb1be33c 100644 --- a/tests/foreman/api/test_repository_set.py +++ b/tests/foreman/api/test_repository_set.py @@ -19,11 +19,10 @@ :Upstream: No """ -import pytest from nailgun import entities +import pytest -from robottelo.constants import PRDS -from robottelo.constants import REPOSET +from robottelo.constants import PRDS, REPOSET pytestmark = [pytest.mark.run_in_one_thread, pytest.mark.tier1] diff --git a/tests/foreman/api/test_rhc.py b/tests/foreman/api/test_rhc.py index abc35f87744..992d1017c77 100644 --- a/tests/foreman/api/test_rhc.py +++ b/tests/foreman/api/test_rhc.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.utils.issue_handlers import is_open diff --git a/tests/foreman/api/test_rhcloud_inventory.py b/tests/foreman/api/test_rhcloud_inventory.py index 0208aed3040..de1fe67d8cb 100644 --- a/tests/foreman/api/test_rhcloud_inventory.py +++ b/tests/foreman/api/test_rhcloud_inventory.py @@ -16,14 +16,11 @@ :Upstream: No """ +from fauxfactory import gen_alphanumeric, gen_string import pytest -from fauxfactory import gen_alphanumeric -from fauxfactory import gen_string from robottelo.config import robottelo_tmp_dir -from robottelo.utils.io import get_local_file_data -from robottelo.utils.io import get_report_data -from robottelo.utils.io import get_report_metadata +from robottelo.utils.io import get_local_file_data, get_report_data, get_report_metadata def common_assertion(report_path): diff --git a/tests/foreman/api/test_rhsm.py b/tests/foreman/api/test_rhsm.py index 9229fb355df..2ebd5b517e0 100644 --- a/tests/foreman/api/test_rhsm.py +++ b/tests/foreman/api/test_rhsm.py @@ -22,11 +22,10 @@ """ import http -import pytest from nailgun import client +import pytest -from robottelo.config import get_credentials -from robottelo.config import get_url +from robottelo.config import get_credentials, get_url @pytest.mark.tier1 diff --git a/tests/foreman/api/test_role.py b/tests/foreman/api/test_role.py index a7e67a6c2cf..d75ff03e84b 100644 --- a/tests/foreman/api/test_role.py +++ b/tests/foreman/api/test_role.py @@ -20,17 +20,14 @@ :Upstream: No """ -import pytest from nailgun import entities from nailgun.config import ServerConfig +import pytest from requests.exceptions import HTTPError from robottelo.cli.ldapauthsource import LDAPAuthSource -from robottelo.constants import LDAP_ATTR -from robottelo.constants import LDAP_SERVER_TYPE -from robottelo.utils.datafactory import gen_string -from robottelo.utils.datafactory import generate_strings_list -from robottelo.utils.datafactory import parametrized +from robottelo.constants import LDAP_ATTR, LDAP_SERVER_TYPE +from robottelo.utils.datafactory import gen_string, generate_strings_list, parametrized from robottelo.utils.issue_handlers import is_open diff --git a/tests/foreman/api/test_settings.py b/tests/foreman/api/test_settings.py index e9dd50f25ff..b5d0542d35d 100644 --- a/tests/foreman/api/test_settings.py +++ b/tests/foreman/api/test_settings.py @@ -18,14 +18,16 @@ """ import random -import pytest from nailgun import entities +import pytest from requests.exceptions import HTTPError -from robottelo.utils.datafactory import filtered_datapoint -from robottelo.utils.datafactory import generate_strings_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + filtered_datapoint, + generate_strings_list, + parametrized, + valid_data_list, +) @filtered_datapoint diff --git a/tests/foreman/api/test_subnet.py b/tests/foreman/api/test_subnet.py index 10390260db5..c5188d12808 100644 --- a/tests/foreman/api/test_subnet.py +++ b/tests/foreman/api/test_subnet.py @@ -23,14 +23,16 @@ """ import re -import pytest from nailgun import entities +import pytest from requests.exceptions import HTTPError -from robottelo.utils.datafactory import gen_string -from robottelo.utils.datafactory import generate_strings_list -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized +from robottelo.utils.datafactory import ( + gen_string, + generate_strings_list, + invalid_values_list, + parametrized, +) @pytest.mark.tier1 diff --git a/tests/foreman/api/test_subscription.py b/tests/foreman/api/test_subscription.py index 85b19bd8a85..8b1da648d81 100644 --- a/tests/foreman/api/test_subscription.py +++ b/tests/foreman/api/test_subscription.py @@ -20,19 +20,15 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities from nailgun.config import ServerConfig from nailgun.entity_mixins import TaskFailedError +import pytest from requests.exceptions import HTTPError from robottelo.cli.subscription import Subscription -from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME -from robottelo.constants import PRDS -from robottelo.constants import REPOS -from robottelo.constants import REPOSET - +from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME, PRDS, REPOS, REPOSET pytestmark = [pytest.mark.run_in_one_thread] diff --git a/tests/foreman/api/test_syncplan.py b/tests/foreman/api/test_syncplan.py index abb8713a642..9653322bd55 100644 --- a/tests/foreman/api/test_syncplan.py +++ b/tests/foreman/api/test_syncplan.py @@ -20,30 +20,24 @@ :Upstream: No """ -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta from time import sleep +from fauxfactory import gen_choice, gen_string +from nailgun import client, entities import pytest -from fauxfactory import gen_choice -from fauxfactory import gen_string -from nailgun import client -from nailgun import entities from requests.exceptions import HTTPError -from robottelo.config import get_credentials -from robottelo.config import get_url -from robottelo.constants import PRDS -from robottelo.constants import REPOS -from robottelo.constants import REPOSET -from robottelo.constants import SYNC_INTERVAL +from robottelo.config import get_credentials, get_url +from robottelo.constants import PRDS, REPOS, REPOSET, SYNC_INTERVAL from robottelo.logging import logger -from robottelo.utils.datafactory import filtered_datapoint -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_cron_expressions -from robottelo.utils.datafactory import valid_data_list - +from robottelo.utils.datafactory import ( + filtered_datapoint, + invalid_values_list, + parametrized, + valid_cron_expressions, + valid_data_list, +) sync_date_deltas = { # Today diff --git a/tests/foreman/api/test_templatesync.py b/tests/foreman/api/test_templatesync.py index de0ba3b4175..061b8db1dd1 100644 --- a/tests/foreman/api/test_templatesync.py +++ b/tests/foreman/api/test_templatesync.py @@ -18,19 +18,20 @@ import json import time -import pytest -import requests from fauxfactory import gen_string from nailgun import entities +import pytest +import requests from robottelo.config import settings -from robottelo.constants import FOREMAN_TEMPLATE_IMPORT_API_URL -from robottelo.constants import FOREMAN_TEMPLATE_IMPORT_URL -from robottelo.constants import FOREMAN_TEMPLATE_ROOT_DIR -from robottelo.constants import FOREMAN_TEMPLATE_TEST_TEMPLATE +from robottelo.constants import ( + FOREMAN_TEMPLATE_IMPORT_API_URL, + FOREMAN_TEMPLATE_IMPORT_URL, + FOREMAN_TEMPLATE_ROOT_DIR, + FOREMAN_TEMPLATE_TEST_TEMPLATE, +) from robottelo.logging import logger - git = settings.git diff --git a/tests/foreman/api/test_user.py b/tests/foreman/api/test_user.py index 85531ae543a..59e8ba515fc 100644 --- a/tests/foreman/api/test_user.py +++ b/tests/foreman/api/test_user.py @@ -23,25 +23,25 @@ import json import re -import pytest from nailgun import entities from nailgun.config import ServerConfig +import pytest from requests.exceptions import HTTPError from robottelo.config import settings -from robottelo.constants import DataFile -from robottelo.constants import LDAP_ATTR -from robottelo.constants import LDAP_SERVER_TYPE +from robottelo.constants import LDAP_ATTR, LDAP_SERVER_TYPE, DataFile from robottelo.utils import gen_ssh_keypairs -from robottelo.utils.datafactory import gen_string -from robottelo.utils.datafactory import generate_strings_list -from robottelo.utils.datafactory import invalid_emails_list -from robottelo.utils.datafactory import invalid_names_list -from robottelo.utils.datafactory import invalid_usernames_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list -from robottelo.utils.datafactory import valid_emails_list -from robottelo.utils.datafactory import valid_usernames_list +from robottelo.utils.datafactory import ( + gen_string, + generate_strings_list, + invalid_emails_list, + invalid_names_list, + invalid_usernames_list, + parametrized, + valid_data_list, + valid_emails_list, + valid_usernames_list, +) @pytest.fixture(scope='module') diff --git a/tests/foreman/api/test_usergroup.py b/tests/foreman/api/test_usergroup.py index cb9e6477576..01dcef27010 100644 --- a/tests/foreman/api/test_usergroup.py +++ b/tests/foreman/api/test_usergroup.py @@ -21,15 +21,17 @@ """ from random import randint -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from requests.exceptions import HTTPError -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list -from robottelo.utils.datafactory import valid_usernames_list +from robottelo.utils.datafactory import ( + invalid_values_list, + parametrized, + valid_data_list, + valid_usernames_list, +) class TestUserGroup: diff --git a/tests/foreman/api/test_webhook.py b/tests/foreman/api/test_webhook.py index c9a2bd5c60b..b806dbb24c0 100644 --- a/tests/foreman/api/test_webhook.py +++ b/tests/foreman/api/test_webhook.py @@ -18,15 +18,13 @@ """ import re -import pytest from nailgun import entities +import pytest from requests.exceptions import HTTPError -from wait_for import TimedOutError -from wait_for import wait_for +from wait_for import TimedOutError, wait_for from robottelo.config import settings -from robottelo.constants import WEBHOOK_EVENTS -from robottelo.constants import WEBHOOK_METHODS +from robottelo.constants import WEBHOOK_EVENTS, WEBHOOK_METHODS from robottelo.logging import logger from robottelo.utils.datafactory import parametrized diff --git a/tests/foreman/cli/test_acs.py b/tests/foreman/cli/test_acs.py index 941b9d066e4..39ba1701dee 100644 --- a/tests/foreman/cli/test_acs.py +++ b/tests/foreman/cli/test_acs.py @@ -16,12 +16,11 @@ :Upstream: No """ -import pytest from fauxfactory import gen_alphanumeric +import pytest from robottelo.cli.base import CLIReturnCodeError -from robottelo.constants.repos import PULP_FIXTURE_ROOT -from robottelo.constants.repos import PULP_SUBPATHS_COMBINED +from robottelo.constants.repos import PULP_FIXTURE_ROOT, PULP_SUBPATHS_COMBINED ACS_UPDATED = 'Alternate Content Source updated.' ACS_DELETED = 'Alternate Content Source deleted.' diff --git a/tests/foreman/cli/test_activationkey.py b/tests/foreman/cli/test_activationkey.py index 329a57c3558..126b4144d3c 100644 --- a/tests/foreman/cli/test_activationkey.py +++ b/tests/foreman/cli/test_activationkey.py @@ -16,40 +16,41 @@ :Upstream: No """ -import re from random import choice +import re -import pytest from broker import Broker -from fauxfactory import gen_alphanumeric -from fauxfactory import gen_string +from fauxfactory import gen_alphanumeric, gen_string +import pytest from robottelo.cli.activationkey import ActivationKey from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.contentview import ContentView from robottelo.cli.defaults import Defaults -from robottelo.cli.factory import add_role_permissions -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_activation_key -from robottelo.cli.factory import make_content_view -from robottelo.cli.factory import make_host_collection -from robottelo.cli.factory import make_lifecycle_environment -from robottelo.cli.factory import make_role -from robottelo.cli.factory import make_user -from robottelo.cli.factory import setup_org_for_a_custom_repo -from robottelo.cli.factory import setup_org_for_a_rh_repo +from robottelo.cli.factory import ( + CLIFactoryError, + add_role_permissions, + make_activation_key, + make_content_view, + make_host_collection, + make_lifecycle_environment, + make_role, + make_user, + setup_org_for_a_custom_repo, + setup_org_for_a_rh_repo, +) from robottelo.cli.lifecycleenvironment import LifecycleEnvironment from robottelo.cli.repository import Repository from robottelo.cli.subscription import Subscription from robottelo.cli.user import User from robottelo.config import settings -from robottelo.constants import PRDS -from robottelo.constants import REPOS -from robottelo.constants import REPOSET +from robottelo.constants import PRDS, REPOS, REPOSET from robottelo.hosts import ContentHost -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + invalid_values_list, + parametrized, + valid_data_list, +) from robottelo.utils.issue_handlers import is_open diff --git a/tests/foreman/cli/test_ansible.py b/tests/foreman/cli/test_ansible.py index 922660d5d5c..a684cdbd440 100644 --- a/tests/foreman/cli/test_ansible.py +++ b/tests/foreman/cli/test_ansible.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings diff --git a/tests/foreman/cli/test_architecture.py b/tests/foreman/cli/test_architecture.py index b68ff28b618..ec212894310 100644 --- a/tests/foreman/cli/test_architecture.py +++ b/tests/foreman/cli/test_architecture.py @@ -16,16 +16,18 @@ :Upstream: No """ -import pytest from fauxfactory import gen_choice +import pytest from robottelo.cli.architecture import Architecture from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.factory import make_architecture -from robottelo.utils.datafactory import invalid_id_list -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + invalid_id_list, + invalid_values_list, + parametrized, + valid_data_list, +) class TestArchitecture: diff --git a/tests/foreman/cli/test_auth.py b/tests/foreman/cli/test_auth.py index ddcb174585d..51945a6445a 100644 --- a/tests/foreman/cli/test_auth.py +++ b/tests/foreman/cli/test_auth.py @@ -18,11 +18,10 @@ """ from time import sleep -import pytest from fauxfactory import gen_string +import pytest -from robottelo.cli.auth import Auth -from robottelo.cli.auth import AuthLogin +from robottelo.cli.auth import Auth, AuthLogin from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.factory import make_user from robottelo.cli.org import Org diff --git a/tests/foreman/cli/test_bootdisk.py b/tests/foreman/cli/test_bootdisk.py index 698ebbe1a75..7e8ad4fca9c 100644 --- a/tests/foreman/cli/test_bootdisk.py +++ b/tests/foreman/cli/test_bootdisk.py @@ -16,9 +16,8 @@ :Upstream: No """ +from fauxfactory import gen_mac, gen_string import pytest -from fauxfactory import gen_mac -from fauxfactory import gen_string from robottelo.config import settings from robottelo.constants import HTTPS_MEDIUM_URL diff --git a/tests/foreman/cli/test_computeresource_azurerm.py b/tests/foreman/cli/test_computeresource_azurerm.py index a25ad7761b8..998175e48ed 100644 --- a/tests/foreman/cli/test_computeresource_azurerm.py +++ b/tests/foreman/cli/test_computeresource_azurerm.py @@ -16,18 +16,20 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.cli.computeresource import ComputeResource from robottelo.cli.host import Host from robottelo.config import settings -from robottelo.constants import AZURERM_FILE_URI -from robottelo.constants import AZURERM_PLATFORM_DEFAULT -from robottelo.constants import AZURERM_PREMIUM_OS_Disk -from robottelo.constants import AZURERM_RHEL7_FT_CUSTOM_IMG_URN -from robottelo.constants import AZURERM_RHEL7_UD_IMG_URN -from robottelo.constants import AZURERM_VM_SIZE_DEFAULT +from robottelo.constants import ( + AZURERM_FILE_URI, + AZURERM_PLATFORM_DEFAULT, + AZURERM_RHEL7_FT_CUSTOM_IMG_URN, + AZURERM_RHEL7_UD_IMG_URN, + AZURERM_VM_SIZE_DEFAULT, + AZURERM_PREMIUM_OS_Disk, +) @pytest.fixture(scope='class') diff --git a/tests/foreman/cli/test_computeresource_ec2.py b/tests/foreman/cli/test_computeresource_ec2.py index 4bbbb5cd77e..e0f3d8cc4c3 100644 --- a/tests/foreman/cli/test_computeresource_ec2.py +++ b/tests/foreman/cli/test_computeresource_ec2.py @@ -13,16 +13,13 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest -from robottelo.cli.factory import make_compute_resource -from robottelo.cli.factory import make_location -from robottelo.cli.factory import make_org +from robottelo.cli.factory import make_compute_resource, make_location, make_org from robottelo.cli.org import Org from robottelo.config import settings -from robottelo.constants import EC2_REGION_CA_CENTRAL_1 -from robottelo.constants import FOREMAN_PROVIDERS +from robottelo.constants import EC2_REGION_CA_CENTRAL_1, FOREMAN_PROVIDERS @pytest.fixture(scope='module') diff --git a/tests/foreman/cli/test_computeresource_libvirt.py b/tests/foreman/cli/test_computeresource_libvirt.py index 748a5c36a30..e5e1997fa3b 100644 --- a/tests/foreman/cli/test_computeresource_libvirt.py +++ b/tests/foreman/cli/test_computeresource_libvirt.py @@ -35,18 +35,15 @@ """ import random +from fauxfactory import gen_string, gen_url import pytest -from fauxfactory import gen_string -from fauxfactory import gen_url from wait_for import wait_for from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.computeresource import ComputeResource -from robottelo.cli.factory import make_compute_resource -from robottelo.cli.factory import make_location +from robottelo.cli.factory import make_compute_resource, make_location from robottelo.config import settings -from robottelo.constants import FOREMAN_PROVIDERS -from robottelo.constants import LIBVIRT_RESOURCE_URL +from robottelo.constants import FOREMAN_PROVIDERS, LIBVIRT_RESOURCE_URL from robottelo.utils.datafactory import parametrized LIBVIRT_URL = LIBVIRT_RESOURCE_URL % settings.libvirt.libvirt_hostname diff --git a/tests/foreman/cli/test_computeresource_osp.py b/tests/foreman/cli/test_computeresource_osp.py index 170a7410ea9..c9018995dc2 100644 --- a/tests/foreman/cli/test_computeresource_osp.py +++ b/tests/foreman/cli/test_computeresource_osp.py @@ -15,9 +15,9 @@ :Upstream: No """ -import pytest from box import Box from fauxfactory import gen_string +import pytest from robottelo.cli.factory import CLIReturnCodeError from robottelo.config import settings diff --git a/tests/foreman/cli/test_computeresource_rhev.py b/tests/foreman/cli/test_computeresource_rhev.py index 35c60e0860e..12e2fe9b345 100644 --- a/tests/foreman/cli/test_computeresource_rhev.py +++ b/tests/foreman/cli/test_computeresource_rhev.py @@ -15,15 +15,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from wait_for import wait_for from wrapanapi import RHEVMSystem from robottelo.cli.computeresource import ComputeResource -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import CLIReturnCodeError -from robottelo.cli.factory import make_compute_resource +from robottelo.cli.factory import ( + CLIFactoryError, + CLIReturnCodeError, + make_compute_resource, +) from robottelo.cli.host import Host from robottelo.config import settings diff --git a/tests/foreman/cli/test_computeresource_vmware.py b/tests/foreman/cli/test_computeresource_vmware.py index f093ed84928..ed1164f063c 100644 --- a/tests/foreman/cli/test_computeresource_vmware.py +++ b/tests/foreman/cli/test_computeresource_vmware.py @@ -13,8 +13,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.cli.factory import make_compute_resource from robottelo.cli.org import Org diff --git a/tests/foreman/cli/test_container_management.py b/tests/foreman/cli/test_container_management.py index e3c2f419f3c..70de242f8ac 100644 --- a/tests/foreman/cli/test_container_management.py +++ b/tests/foreman/cli/test_container_management.py @@ -12,21 +12,25 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from wait_for import wait_for -from robottelo.cli.factory import ContentView -from robottelo.cli.factory import LifecycleEnvironment -from robottelo.cli.factory import make_content_view -from robottelo.cli.factory import make_lifecycle_environment -from robottelo.cli.factory import make_product_wait -from robottelo.cli.factory import make_repository -from robottelo.cli.factory import Repository +from robottelo.cli.factory import ( + ContentView, + LifecycleEnvironment, + Repository, + make_content_view, + make_lifecycle_environment, + make_product_wait, + make_repository, +) from robottelo.config import settings -from robottelo.constants import CONTAINER_REGISTRY_HUB -from robottelo.constants import CONTAINER_UPSTREAM_NAME -from robottelo.constants import REPO_TYPE +from robottelo.constants import ( + CONTAINER_REGISTRY_HUB, + CONTAINER_UPSTREAM_NAME, + REPO_TYPE, +) from robottelo.logging import logger diff --git a/tests/foreman/cli/test_contentaccess.py b/tests/foreman/cli/test_contentaccess.py index 55b1a933328..fb8aaca5228 100644 --- a/tests/foreman/cli/test_contentaccess.py +++ b/tests/foreman/cli/test_contentaccess.py @@ -16,16 +16,18 @@ """ import time -import pytest from nailgun import entities +import pytest from robottelo.cli.host import Host from robottelo.cli.package import Package from robottelo.config import settings -from robottelo.constants import REAL_0_ERRATA_ID -from robottelo.constants import REAL_RHEL7_0_2_PACKAGE_FILENAME -from robottelo.constants import REAL_RHEL7_0_2_PACKAGE_NAME -from robottelo.constants import REPOS +from robottelo.constants import ( + REAL_0_ERRATA_ID, + REAL_RHEL7_0_2_PACKAGE_FILENAME, + REAL_RHEL7_0_2_PACKAGE_NAME, + REPOS, +) pytestmark = [ pytest.mark.skipif( diff --git a/tests/foreman/cli/test_contentcredentials.py b/tests/foreman/cli/test_contentcredentials.py index 935d41e3e90..269170ffdc1 100644 --- a/tests/foreman/cli/test_contentcredentials.py +++ b/tests/foreman/cli/test_contentcredentials.py @@ -20,18 +20,17 @@ """ from tempfile import mkstemp +from fauxfactory import gen_alphanumeric, gen_choice, gen_integer, gen_string import pytest -from fauxfactory import gen_alphanumeric -from fauxfactory import gen_choice -from fauxfactory import gen_integer -from fauxfactory import gen_string from robottelo.cli.base import CLIReturnCodeError from robottelo.constants import DataFile from robottelo.host_helpers.cli_factory import CLIFactoryError -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + invalid_values_list, + parametrized, + valid_data_list, +) VALID_GPG_KEY_FILE_PATH = DataFile.VALID_GPG_KEY_FILE diff --git a/tests/foreman/cli/test_contentview.py b/tests/foreman/cli/test_contentview.py index 1b71e7e42cb..4effe29ecf4 100644 --- a/tests/foreman/cli/test_contentview.py +++ b/tests/foreman/cli/test_contentview.py @@ -18,10 +18,9 @@ """ import random -import pytest -from fauxfactory import gen_alphanumeric -from fauxfactory import gen_string +from fauxfactory import gen_alphanumeric, gen_string from nailgun import entities +import pytest from wrapanapi.entities.vm import VmState from robottelo import constants @@ -42,13 +41,17 @@ from robottelo.cli.role import Role from robottelo.cli.user import User from robottelo.config import settings -from robottelo.constants import DataFile -from robottelo.constants import FAKE_2_CUSTOM_PACKAGE -from robottelo.constants import FAKE_2_CUSTOM_PACKAGE_NAME -from robottelo.utils.datafactory import generate_strings_list -from robottelo.utils.datafactory import invalid_names_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_names_list +from robottelo.constants import ( + FAKE_2_CUSTOM_PACKAGE, + FAKE_2_CUSTOM_PACKAGE_NAME, + DataFile, +) +from robottelo.utils.datafactory import ( + generate_strings_list, + invalid_names_list, + parametrized, + valid_names_list, +) @pytest.fixture(scope='module') diff --git a/tests/foreman/cli/test_contentviewfilter.py b/tests/foreman/cli/test_contentviewfilter.py index 40326ad9ba6..c19091df054 100644 --- a/tests/foreman/cli/test_contentviewfilter.py +++ b/tests/foreman/cli/test_contentviewfilter.py @@ -18,19 +18,20 @@ """ import random -import pytest from fauxfactory import gen_string +import pytest from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.contentview import ContentView from robottelo.cli.defaults import Defaults -from robottelo.cli.factory import make_content_view -from robottelo.cli.factory import make_repository +from robottelo.cli.factory import make_content_view, make_repository from robottelo.cli.repository import Repository from robottelo.constants import CONTAINER_REGISTRY_HUB -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + invalid_values_list, + parametrized, + valid_data_list, +) @pytest.fixture(scope='module') diff --git a/tests/foreman/cli/test_discoveryrule.py b/tests/foreman/cli/test_discoveryrule.py index e51468fbbf5..efdc081d79f 100644 --- a/tests/foreman/cli/test_discoveryrule.py +++ b/tests/foreman/cli/test_discoveryrule.py @@ -16,26 +16,24 @@ :Upstream: No """ -import random from functools import partial +import random -import pytest from box import Box -from fauxfactory import gen_choice -from fauxfactory import gen_integer -from fauxfactory import gen_string -from nailgun.entities import Role as RoleEntity -from nailgun.entities import User as UserEntity +from fauxfactory import gen_choice, gen_integer, gen_string +from nailgun.entities import Role as RoleEntity, User as UserEntity +import pytest from requests import HTTPError from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_discoveryrule +from robottelo.cli.factory import CLIFactoryError, make_discoveryrule from robottelo.logging import logger -from robottelo.utils.datafactory import filtered_datapoint -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + filtered_datapoint, + invalid_values_list, + parametrized, + valid_data_list, +) @filtered_datapoint diff --git a/tests/foreman/cli/test_docker.py b/tests/foreman/cli/test_docker.py index 91ac3446a17..9e7ca7b5ed4 100644 --- a/tests/foreman/cli/test_docker.py +++ b/tests/foreman/cli/test_docker.py @@ -12,34 +12,38 @@ :Upstream: No """ -from random import choice -from random import randint +from random import choice, randint +from fauxfactory import gen_string, gen_url import pytest -from fauxfactory import gen_string -from fauxfactory import gen_url from robottelo.cli.activationkey import ActivationKey from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.contentview import ContentView from robottelo.cli.docker import Docker -from robottelo.cli.factory import make_activation_key -from robottelo.cli.factory import make_content_view -from robottelo.cli.factory import make_lifecycle_environment -from robottelo.cli.factory import make_product_wait -from robottelo.cli.factory import make_repository +from robottelo.cli.factory import ( + make_activation_key, + make_content_view, + make_lifecycle_environment, + make_product_wait, + make_repository, +) from robottelo.cli.lifecycleenvironment import LifecycleEnvironment from robottelo.cli.product import Product from robottelo.cli.repository import Repository from robottelo.config import settings -from robottelo.constants import CONTAINER_REGISTRY_HUB -from robottelo.constants import CONTAINER_RH_REGISTRY_UPSTREAM_NAME -from robottelo.constants import CONTAINER_UPSTREAM_NAME -from robottelo.constants import REPO_TYPE -from robottelo.utils.datafactory import invalid_docker_upstream_names -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_docker_repository_names -from robottelo.utils.datafactory import valid_docker_upstream_names +from robottelo.constants import ( + CONTAINER_REGISTRY_HUB, + CONTAINER_RH_REGISTRY_UPSTREAM_NAME, + CONTAINER_UPSTREAM_NAME, + REPO_TYPE, +) +from robottelo.utils.datafactory import ( + invalid_docker_upstream_names, + parametrized, + valid_docker_repository_names, + valid_docker_upstream_names, +) def _repo(product_id, name=None, upstream_name=None, url=None): diff --git a/tests/foreman/cli/test_domain.py b/tests/foreman/cli/test_domain.py index 1da37871243..b0d6aeca207 100644 --- a/tests/foreman/cli/test_domain.py +++ b/tests/foreman/cli/test_domain.py @@ -16,18 +16,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.domain import Domain -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_domain -from robottelo.cli.factory import make_location -from robottelo.cli.factory import make_org -from robottelo.utils.datafactory import filtered_datapoint -from robottelo.utils.datafactory import invalid_id_list -from robottelo.utils.datafactory import parametrized +from robottelo.cli.factory import CLIFactoryError, make_domain, make_location, make_org +from robottelo.utils.datafactory import ( + filtered_datapoint, + invalid_id_list, + parametrized, +) @filtered_datapoint diff --git a/tests/foreman/cli/test_environment.py b/tests/foreman/cli/test_environment.py index d033c952711..ae6e0d7e947 100644 --- a/tests/foreman/cli/test_environment.py +++ b/tests/foreman/cli/test_environment.py @@ -18,15 +18,16 @@ """ from random import choice +from fauxfactory import gen_alphanumeric, gen_string import pytest -from fauxfactory import gen_alphanumeric -from fauxfactory import gen_string from robottelo.cli.base import CLIReturnCodeError from robottelo.config import settings -from robottelo.utils.datafactory import invalid_id_list -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized +from robottelo.utils.datafactory import ( + invalid_id_list, + invalid_values_list, + parametrized, +) @pytest.fixture(scope='module') diff --git a/tests/foreman/cli/test_errata.py b/tests/foreman/cli/test_errata.py index 7d8577c3816..988a578ab12 100644 --- a/tests/foreman/cli/test_errata.py +++ b/tests/foreman/cli/test_errata.py @@ -16,26 +16,25 @@ :Upstream: No """ -from datetime import date -from datetime import datetime -from datetime import timedelta +from datetime import date, datetime, timedelta from operator import itemgetter -import pytest from broker import Broker from nailgun import entities +import pytest from robottelo.cli.activationkey import ActivationKey from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.contentview import ContentView -from robottelo.cli.contentview import ContentViewFilter +from robottelo.cli.contentview import ContentView, ContentViewFilter from robottelo.cli.erratum import Erratum -from robottelo.cli.factory import make_content_view_filter -from robottelo.cli.factory import make_content_view_filter_rule -from robottelo.cli.factory import make_host_collection -from robottelo.cli.factory import make_repository -from robottelo.cli.factory import setup_org_for_a_custom_repo -from robottelo.cli.factory import setup_org_for_a_rh_repo +from robottelo.cli.factory import ( + make_content_view_filter, + make_content_view_filter_rule, + make_host_collection, + make_repository, + setup_org_for_a_custom_repo, + setup_org_for_a_rh_repo, +) from robottelo.cli.host import Host from robottelo.cli.hostcollection import HostCollection from robottelo.cli.job_invocation import JobInvocation @@ -43,21 +42,23 @@ from robottelo.cli.repository import Repository from robottelo.cli.repository_set import RepositorySet from robottelo.config import settings -from robottelo.constants import DEFAULT_ARCHITECTURE -from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE -from robottelo.constants import FAKE_2_CUSTOM_PACKAGE -from robottelo.constants import FAKE_2_CUSTOM_PACKAGE_NAME -from robottelo.constants import FAKE_4_CUSTOM_PACKAGE -from robottelo.constants import FAKE_4_CUSTOM_PACKAGE_NAME -from robottelo.constants import FAKE_5_CUSTOM_PACKAGE -from robottelo.constants import PRDS -from robottelo.constants import REAL_0_ERRATA_ID -from robottelo.constants import REAL_4_ERRATA_CVES -from robottelo.constants import REAL_4_ERRATA_ID -from robottelo.constants import REAL_RHEL7_0_2_PACKAGE_NAME -from robottelo.constants import REPOS -from robottelo.constants import REPOSET +from robottelo.constants import ( + DEFAULT_ARCHITECTURE, + DEFAULT_SUBSCRIPTION_NAME, + FAKE_1_CUSTOM_PACKAGE, + FAKE_2_CUSTOM_PACKAGE, + FAKE_2_CUSTOM_PACKAGE_NAME, + FAKE_4_CUSTOM_PACKAGE, + FAKE_4_CUSTOM_PACKAGE_NAME, + FAKE_5_CUSTOM_PACKAGE, + PRDS, + REAL_0_ERRATA_ID, + REAL_4_ERRATA_CVES, + REAL_4_ERRATA_ID, + REAL_RHEL7_0_2_PACKAGE_NAME, + REPOS, + REPOSET, +) from robottelo.hosts import ContentHost PER_PAGE = 10 diff --git a/tests/foreman/cli/test_fact.py b/tests/foreman/cli/test_fact.py index 8a484521ea0..96fee50c126 100644 --- a/tests/foreman/cli/test_fact.py +++ b/tests/foreman/cli/test_fact.py @@ -16,12 +16,11 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.cli.fact import Fact - pytestmark = [pytest.mark.tier1] diff --git a/tests/foreman/cli/test_filter.py b/tests/foreman/cli/test_filter.py index ee873349d73..da5eccb86c2 100644 --- a/tests/foreman/cli/test_filter.py +++ b/tests/foreman/cli/test_filter.py @@ -19,10 +19,7 @@ import pytest from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_filter -from robottelo.cli.factory import make_location -from robottelo.cli.factory import make_org -from robottelo.cli.factory import make_role +from robottelo.cli.factory import make_filter, make_location, make_org, make_role from robottelo.cli.filter import Filter from robottelo.cli.role import Role diff --git a/tests/foreman/cli/test_globalparam.py b/tests/foreman/cli/test_globalparam.py index 4f42b83579c..4fb6b1949cb 100644 --- a/tests/foreman/cli/test_globalparam.py +++ b/tests/foreman/cli/test_globalparam.py @@ -18,12 +18,11 @@ """ from functools import partial -import pytest from fauxfactory import gen_string +import pytest from robottelo.cli.globalparam import GlobalParameter - pytestmark = [pytest.mark.tier1] diff --git a/tests/foreman/cli/test_hammer.py b/tests/foreman/cli/test_hammer.py index a2eff81403a..8a424bb62ee 100644 --- a/tests/foreman/cli/test_hammer.py +++ b/tests/foreman/cli/test_hammer.py @@ -27,7 +27,6 @@ from robottelo.constants import DataFile from robottelo.logging import logger - HAMMER_COMMANDS = json.loads(DataFile.HAMMER_COMMANDS_JSON.read_text()) pytestmark = [pytest.mark.tier1] diff --git a/tests/foreman/cli/test_host.py b/tests/foreman/cli/test_host.py index 23d8c02b257..538c8ebe27a 100644 --- a/tests/foreman/cli/test_host.py +++ b/tests/foreman/cli/test_host.py @@ -16,49 +16,48 @@ :Upstream: No """ -import re from random import choice +import re +from fauxfactory import gen_choice, gen_integer, gen_ipaddr, gen_mac, gen_string import pytest +from wait_for import TimedOutError, wait_for import yaml -from fauxfactory import gen_choice -from fauxfactory import gen_integer -from fauxfactory import gen_ipaddr -from fauxfactory import gen_mac -from fauxfactory import gen_string -from wait_for import TimedOutError -from wait_for import wait_for from robottelo.cli.activationkey import ActivationKey from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import add_role_permissions -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_fake_host -from robottelo.cli.factory import make_host -from robottelo.cli.factory import setup_org_for_a_rh_repo -from robottelo.cli.host import Host -from robottelo.cli.host import HostInterface -from robottelo.cli.host import HostTraces +from robottelo.cli.factory import ( + CLIFactoryError, + add_role_permissions, + make_fake_host, + make_host, + setup_org_for_a_rh_repo, +) +from robottelo.cli.host import Host, HostInterface, HostTraces from robottelo.cli.job_invocation import JobInvocation from robottelo.cli.package import Package from robottelo.cli.user import User from robottelo.config import settings -from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE_NAME -from robottelo.constants import FAKE_2_CUSTOM_PACKAGE -from robottelo.constants import FAKE_7_CUSTOM_PACKAGE -from robottelo.constants import FAKE_8_CUSTOM_PACKAGE -from robottelo.constants import FAKE_8_CUSTOM_PACKAGE_NAME -from robottelo.constants import PRDS -from robottelo.constants import REPOS -from robottelo.constants import REPOSET -from robottelo.constants import SM_OVERALL_STATUS +from robottelo.constants import ( + DEFAULT_SUBSCRIPTION_NAME, + FAKE_1_CUSTOM_PACKAGE, + FAKE_1_CUSTOM_PACKAGE_NAME, + FAKE_2_CUSTOM_PACKAGE, + FAKE_7_CUSTOM_PACKAGE, + FAKE_8_CUSTOM_PACKAGE, + FAKE_8_CUSTOM_PACKAGE_NAME, + PRDS, + REPOS, + REPOSET, + SM_OVERALL_STATUS, +) from robottelo.hosts import ContentHostError from robottelo.logging import logger -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import valid_data_list -from robottelo.utils.datafactory import valid_hosts_list +from robottelo.utils.datafactory import ( + invalid_values_list, + valid_data_list, + valid_hosts_list, +) from robottelo.utils.issue_handlers import is_open diff --git a/tests/foreman/cli/test_hostcollection.py b/tests/foreman/cli/test_hostcollection.py index 80b11a56350..fa41aa9067e 100644 --- a/tests/foreman/cli/test_hostcollection.py +++ b/tests/foreman/cli/test_hostcollection.py @@ -16,26 +16,29 @@ :Upstream: No """ -import pytest from broker import Broker from fauxfactory import gen_string +import pytest from robottelo.cli.activationkey import ActivationKey from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.contentview import ContentView -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_fake_host -from robottelo.cli.factory import make_host_collection -from robottelo.cli.factory import make_org +from robottelo.cli.factory import ( + CLIFactoryError, + make_fake_host, + make_host_collection, + make_org, +) from robottelo.cli.host import Host from robottelo.cli.hostcollection import HostCollection from robottelo.cli.lifecycleenvironment import LifecycleEnvironment -from robottelo.constants import DEFAULT_CV -from robottelo.constants import ENVIRONMENT +from robottelo.constants import DEFAULT_CV, ENVIRONMENT from robottelo.hosts import ContentHost -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + invalid_values_list, + parametrized, + valid_data_list, +) def _make_fake_host_helper(module_org): diff --git a/tests/foreman/cli/test_hostgroup.py b/tests/foreman/cli/test_hostgroup.py index e5fee3a7738..d1fa7190b5a 100644 --- a/tests/foreman/cli/test_hostgroup.py +++ b/tests/foreman/cli/test_hostgroup.py @@ -16,31 +16,35 @@ :Upstream: No """ -import pytest from fauxfactory import gen_integer from nailgun import entities +import pytest from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.contentview import ContentView -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_architecture -from robottelo.cli.factory import make_content_view -from robottelo.cli.factory import make_domain -from robottelo.cli.factory import make_environment -from robottelo.cli.factory import make_hostgroup -from robottelo.cli.factory import make_lifecycle_environment -from robottelo.cli.factory import make_location -from robottelo.cli.factory import make_medium -from robottelo.cli.factory import make_os -from robottelo.cli.factory import make_partition_table -from robottelo.cli.factory import make_subnet +from robottelo.cli.factory import ( + CLIFactoryError, + make_architecture, + make_content_view, + make_domain, + make_environment, + make_hostgroup, + make_lifecycle_environment, + make_location, + make_medium, + make_os, + make_partition_table, + make_subnet, +) from robottelo.cli.hostgroup import HostGroup from robottelo.cli.proxy import Proxy from robottelo.config import settings -from robottelo.utils.datafactory import invalid_id_list -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_hostgroups_list +from robottelo.utils.datafactory import ( + invalid_id_list, + invalid_values_list, + parametrized, + valid_hostgroups_list, +) pytestmark = [ pytest.mark.skipif( diff --git a/tests/foreman/cli/test_http_proxy.py b/tests/foreman/cli/test_http_proxy.py index 4f7808e645c..50c71313a89 100644 --- a/tests/foreman/cli/test_http_proxy.py +++ b/tests/foreman/cli/test_http_proxy.py @@ -16,14 +16,11 @@ :Upstream: No """ +from fauxfactory import gen_integer, gen_string, gen_url import pytest -from fauxfactory import gen_integer -from fauxfactory import gen_string -from fauxfactory import gen_url from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_product -from robottelo.cli.factory import make_repository +from robottelo.cli.factory import make_product, make_repository from robottelo.cli.http_proxy import HttpProxy from robottelo.cli.product import Product from robottelo.cli.repository import Repository diff --git a/tests/foreman/cli/test_jobtemplate.py b/tests/foreman/cli/test_jobtemplate.py index a90aa0fb6ad..916fdea7a33 100644 --- a/tests/foreman/cli/test_jobtemplate.py +++ b/tests/foreman/cli/test_jobtemplate.py @@ -16,16 +16,14 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo import ssh from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_job_template +from robottelo.cli.factory import CLIFactoryError, make_job_template from robottelo.cli.job_template import JobTemplate -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized +from robottelo.utils.datafactory import invalid_values_list, parametrized TEMPLATE_FILE = 'template_file.txt' TEMPLATE_FILE_EMPTY = 'template_file_empty.txt' diff --git a/tests/foreman/cli/test_ldapauthsource.py b/tests/foreman/cli/test_ldapauthsource.py index e9f299ab213..92d5185cc83 100644 --- a/tests/foreman/cli/test_ldapauthsource.py +++ b/tests/foreman/cli/test_ldapauthsource.py @@ -16,23 +16,22 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo.cli.auth import Auth from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_ldap_auth_source -from robottelo.cli.factory import make_usergroup -from robottelo.cli.factory import make_usergroup_external +from robottelo.cli.factory import ( + make_ldap_auth_source, + make_usergroup, + make_usergroup_external, +) from robottelo.cli.ldapauthsource import LDAPAuthSource from robottelo.cli.role import Role -from robottelo.cli.usergroup import UserGroup -from robottelo.cli.usergroup import UserGroupExternal -from robottelo.constants import LDAP_ATTR -from robottelo.constants import LDAP_SERVER_TYPE -from robottelo.utils.datafactory import generate_strings_list -from robottelo.utils.datafactory import parametrized +from robottelo.cli.usergroup import UserGroup, UserGroupExternal +from robottelo.constants import LDAP_ATTR, LDAP_SERVER_TYPE +from robottelo.utils.datafactory import generate_strings_list, parametrized @pytest.fixture() diff --git a/tests/foreman/cli/test_leapp_client.py b/tests/foreman/cli/test_leapp_client.py index 92e295303d7..37d1784926c 100644 --- a/tests/foreman/cli/test_leapp_client.py +++ b/tests/foreman/cli/test_leapp_client.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from broker import Broker +import pytest from robottelo.config import settings from robottelo.constants import PRDS diff --git a/tests/foreman/cli/test_lifecycleenvironment.py b/tests/foreman/cli/test_lifecycleenvironment.py index fc05395c79c..b9a77b5220f 100644 --- a/tests/foreman/cli/test_lifecycleenvironment.py +++ b/tests/foreman/cli/test_lifecycleenvironment.py @@ -18,12 +18,11 @@ """ from math import ceil -import pytest from fauxfactory import gen_string +import pytest from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_lifecycle_environment -from robottelo.cli.factory import make_org +from robottelo.cli.factory import make_lifecycle_environment, make_org from robottelo.cli.lifecycleenvironment import LifecycleEnvironment from robottelo.constants import ENVIRONMENT diff --git a/tests/foreman/cli/test_location.py b/tests/foreman/cli/test_location.py index 7baebfb0d1d..ee157c4ef76 100644 --- a/tests/foreman/cli/test_location.py +++ b/tests/foreman/cli/test_location.py @@ -16,23 +16,25 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.computeresource import ComputeResource from robottelo.cli.domain import Domain from robottelo.cli.environment import Environment -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_compute_resource -from robottelo.cli.factory import make_domain -from robottelo.cli.factory import make_environment -from robottelo.cli.factory import make_hostgroup -from robottelo.cli.factory import make_location -from robottelo.cli.factory import make_medium -from robottelo.cli.factory import make_subnet -from robottelo.cli.factory import make_template -from robottelo.cli.factory import make_user +from robottelo.cli.factory import ( + CLIFactoryError, + make_compute_resource, + make_domain, + make_environment, + make_hostgroup, + make_location, + make_medium, + make_subnet, + make_template, + make_user, +) from robottelo.cli.hostgroup import HostGroup from robottelo.cli.location import Location from robottelo.cli.medium import Medium diff --git a/tests/foreman/cli/test_logging.py b/tests/foreman/cli/test_logging.py index f926d96a7c8..45692e2e2a2 100644 --- a/tests/foreman/cli/test_logging.py +++ b/tests/foreman/cli/test_logging.py @@ -18,12 +18,11 @@ """ import re -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest -from robottelo.cli.factory import make_product -from robottelo.cli.factory import make_repository +from robottelo.cli.factory import make_product, make_repository from robottelo.cli.product import Product from robottelo.cli.repository import Repository from robottelo.config import settings diff --git a/tests/foreman/cli/test_medium.py b/tests/foreman/cli/test_medium.py index 8f3d7ad2400..d9f835fdb0b 100644 --- a/tests/foreman/cli/test_medium.py +++ b/tests/foreman/cli/test_medium.py @@ -16,17 +16,13 @@ :Upstream: No """ -import pytest from fauxfactory import gen_alphanumeric +import pytest from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_location -from robottelo.cli.factory import make_medium -from robottelo.cli.factory import make_org -from robottelo.cli.factory import make_os +from robottelo.cli.factory import make_location, make_medium, make_org, make_os from robottelo.cli.medium import Medium -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import parametrized, valid_data_list URL = "http://mirror.fakeos.org/%s/$major.$minor/os/$arch" OSES = ['Archlinux', 'Debian', 'Gentoo', 'Redhat', 'Solaris', 'Suse', 'Windows'] diff --git a/tests/foreman/cli/test_model.py b/tests/foreman/cli/test_model.py index aa2d5c8f704..fe8050d13e9 100644 --- a/tests/foreman/cli/test_model.py +++ b/tests/foreman/cli/test_model.py @@ -16,16 +16,18 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.factory import make_model from robottelo.cli.model import Model -from robottelo.utils.datafactory import invalid_id_list -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + invalid_id_list, + invalid_values_list, + parametrized, + valid_data_list, +) class TestModel: diff --git a/tests/foreman/cli/test_operatingsystem.py b/tests/foreman/cli/test_operatingsystem.py index eeb5394cba2..587ceeb67a0 100644 --- a/tests/foreman/cli/test_operatingsystem.py +++ b/tests/foreman/cli/test_operatingsystem.py @@ -16,20 +16,23 @@ :Upstream: No """ +from fauxfactory import gen_alphanumeric, gen_string import pytest -from fauxfactory import gen_alphanumeric -from fauxfactory import gen_string from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_architecture -from robottelo.cli.factory import make_medium -from robottelo.cli.factory import make_partition_table -from robottelo.cli.factory import make_template +from robottelo.cli.factory import ( + make_architecture, + make_medium, + make_partition_table, + make_template, +) from robottelo.constants import DEFAULT_ORG -from robottelo.utils.datafactory import filtered_datapoint -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + filtered_datapoint, + invalid_values_list, + parametrized, + valid_data_list, +) @filtered_datapoint diff --git a/tests/foreman/cli/test_organization.py b/tests/foreman/cli/test_organization.py index d0095c1974e..2d4a8b8b8b8 100644 --- a/tests/foreman/cli/test_organization.py +++ b/tests/foreman/cli/test_organization.py @@ -16,31 +16,35 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_compute_resource -from robottelo.cli.factory import make_domain -from robottelo.cli.factory import make_hostgroup -from robottelo.cli.factory import make_lifecycle_environment -from robottelo.cli.factory import make_location -from robottelo.cli.factory import make_medium -from robottelo.cli.factory import make_org -from robottelo.cli.factory import make_subnet -from robottelo.cli.factory import make_template -from robottelo.cli.factory import make_user +from robottelo.cli.factory import ( + CLIFactoryError, + make_compute_resource, + make_domain, + make_hostgroup, + make_lifecycle_environment, + make_location, + make_medium, + make_org, + make_subnet, + make_template, + make_user, +) from robottelo.cli.lifecycleenvironment import LifecycleEnvironment from robottelo.cli.org import Org from robottelo.cli.user import User from robottelo.config import settings from robottelo.constants import FOREMAN_PROVIDERS -from robottelo.utils.datafactory import filtered_datapoint -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list -from robottelo.utils.datafactory import valid_org_names_list +from robottelo.utils.datafactory import ( + filtered_datapoint, + invalid_values_list, + parametrized, + valid_data_list, + valid_org_names_list, +) @filtered_datapoint diff --git a/tests/foreman/cli/test_oscap.py b/tests/foreman/cli/test_oscap.py index a4a9c67c3ca..5088d25fb60 100644 --- a/tests/foreman/cli/test_oscap.py +++ b/tests/foreman/cli/test_oscap.py @@ -16,26 +16,28 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_hostgroup -from robottelo.cli.factory import make_scap_policy -from robottelo.cli.factory import make_scapcontent -from robottelo.cli.factory import make_tailoringfile +from robottelo.cli.factory import ( + CLIFactoryError, + make_hostgroup, + make_scap_policy, + make_scapcontent, + make_tailoringfile, +) from robottelo.cli.host import Host from robottelo.cli.scap_policy import Scappolicy from robottelo.cli.scapcontent import Scapcontent from robottelo.config import settings -from robottelo.constants import OSCAP_DEFAULT_CONTENT -from robottelo.constants import OSCAP_PERIOD -from robottelo.constants import OSCAP_WEEKDAY -from robottelo.utils.datafactory import invalid_names_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.constants import OSCAP_DEFAULT_CONTENT, OSCAP_PERIOD, OSCAP_WEEKDAY +from robottelo.utils.datafactory import ( + invalid_names_list, + parametrized, + valid_data_list, +) class TestOpenScap: diff --git a/tests/foreman/cli/test_oscap_tailoringfiles.py b/tests/foreman/cli/test_oscap_tailoringfiles.py index 38f91464a48..285a907df3b 100644 --- a/tests/foreman/cli/test_oscap_tailoringfiles.py +++ b/tests/foreman/cli/test_oscap_tailoringfiles.py @@ -16,18 +16,18 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_tailoringfile +from robottelo.cli.factory import CLIFactoryError, make_tailoringfile from robottelo.cli.scap_tailoring_files import TailoringFiles -from robottelo.constants import DataFile -from robottelo.constants import SNIPPET_DATA_FILE -from robottelo.utils.datafactory import invalid_names_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.constants import SNIPPET_DATA_FILE, DataFile +from robottelo.utils.datafactory import ( + invalid_names_list, + parametrized, + valid_data_list, +) class TestTailoringFiles: diff --git a/tests/foreman/cli/test_ostreebranch.py b/tests/foreman/cli/test_ostreebranch.py index 9f44e365e3a..23516360e48 100644 --- a/tests/foreman/cli/test_ostreebranch.py +++ b/tests/foreman/cli/test_ostreebranch.py @@ -18,14 +18,16 @@ """ import random -import pytest from nailgun import entities +import pytest from robottelo.cli.contentview import ContentView -from robottelo.cli.factory import make_content_view -from robottelo.cli.factory import make_org_with_credentials -from robottelo.cli.factory import make_product_with_credentials -from robottelo.cli.factory import make_repository_with_credentials +from robottelo.cli.factory import ( + make_content_view, + make_org_with_credentials, + make_product_with_credentials, + make_repository_with_credentials, +) from robottelo.cli.ostreebranch import OstreeBranch from robottelo.cli.repository import Repository from robottelo.config import settings diff --git a/tests/foreman/cli/test_partitiontable.py b/tests/foreman/cli/test_partitiontable.py index 95854b1f0f1..6540a5d755b 100644 --- a/tests/foreman/cli/test_partitiontable.py +++ b/tests/foreman/cli/test_partitiontable.py @@ -18,15 +18,13 @@ """ from random import randint -import pytest from fauxfactory import gen_string +import pytest from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_os -from robottelo.cli.factory import make_partition_table +from robottelo.cli.factory import make_os, make_partition_table from robottelo.cli.partitiontable import PartitionTable -from robottelo.utils.datafactory import generate_strings_list -from robottelo.utils.datafactory import parametrized +from robottelo.utils.datafactory import generate_strings_list, parametrized class TestPartitionTable: diff --git a/tests/foreman/cli/test_product.py b/tests/foreman/cli/test_product.py index be4eedbdf50..e11b09f6367 100644 --- a/tests/foreman/cli/test_product.py +++ b/tests/foreman/cli/test_product.py @@ -16,29 +16,30 @@ :Upstream: No """ +from fauxfactory import gen_alphanumeric, gen_integer, gen_string, gen_url import pytest -from fauxfactory import gen_alphanumeric -from fauxfactory import gen_integer -from fauxfactory import gen_string -from fauxfactory import gen_url from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.defaults import Defaults -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_content_credential -from robottelo.cli.factory import make_org -from robottelo.cli.factory import make_product -from robottelo.cli.factory import make_repository -from robottelo.cli.factory import make_sync_plan +from robottelo.cli.factory import ( + CLIFactoryError, + make_content_credential, + make_org, + make_product, + make_repository, + make_sync_plan, +) from robottelo.cli.package import Package from robottelo.cli.product import Product from robottelo.cli.repository import Repository from robottelo.config import settings from robottelo.constants import FAKE_0_YUM_REPO_PACKAGES_COUNT -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list -from robottelo.utils.datafactory import valid_labels_list +from robottelo.utils.datafactory import ( + invalid_values_list, + parametrized, + valid_data_list, + valid_labels_list, +) @pytest.mark.tier1 diff --git a/tests/foreman/cli/test_provisioningtemplate.py b/tests/foreman/cli/test_provisioningtemplate.py index db2cca0dcda..71214e84615 100644 --- a/tests/foreman/cli/test_provisioningtemplate.py +++ b/tests/foreman/cli/test_provisioningtemplate.py @@ -19,8 +19,8 @@ import random from random import randint -import pytest from fauxfactory import gen_string +import pytest from robottelo import constants from robottelo.cli.base import CLIReturnCodeError diff --git a/tests/foreman/cli/test_realm.py b/tests/foreman/cli/test_realm.py index 1d7eb9c4b20..595a123e9e8 100644 --- a/tests/foreman/cli/test_realm.py +++ b/tests/foreman/cli/test_realm.py @@ -18,12 +18,11 @@ """ import random -import pytest from fauxfactory import gen_string +import pytest from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_realm +from robottelo.cli.factory import CLIFactoryError, make_realm from robottelo.cli.realm import Realm diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index 9de5569c81b..09e1eda28af 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -17,22 +17,22 @@ :Upstream: No """ from calendar import monthrange -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta from time import sleep -import pytest from broker import Broker -from dateutil.relativedelta import FR -from dateutil.relativedelta import relativedelta +from dateutil.relativedelta import FR, relativedelta from fauxfactory import gen_string +import pytest -from robottelo.cli.factory import make_filter -from robottelo.cli.factory import make_job_invocation -from robottelo.cli.factory import make_job_invocation_with_credentials -from robottelo.cli.factory import make_job_template -from robottelo.cli.factory import make_role -from robottelo.cli.factory import make_user +from robottelo.cli.factory import ( + make_filter, + make_job_invocation, + make_job_invocation_with_credentials, + make_job_template, + make_role, + make_user, +) from robottelo.cli.filter import Filter from robottelo.cli.globalparam import GlobalParameter from robottelo.cli.host import Host @@ -43,9 +43,7 @@ from robottelo.cli.task import Task from robottelo.cli.user import User from robottelo.config import settings -from robottelo.constants import PRDS -from robottelo.constants import REPOS -from robottelo.constants import REPOSET +from robottelo.constants import PRDS, REPOS, REPOSET from robottelo.hosts import ContentHost from robottelo.utils import ohsnap diff --git a/tests/foreman/cli/test_reporttemplates.py b/tests/foreman/cli/test_reporttemplates.py index 151312a72a7..8f2729046b3 100644 --- a/tests/foreman/cli/test_reporttemplates.py +++ b/tests/foreman/cli/test_reporttemplates.py @@ -15,32 +15,33 @@ :Upstream: No """ -import pytest from broker import Broker from fauxfactory import gen_alpha +import pytest from robottelo.cli.activationkey import ActivationKey -from robottelo.cli.base import Base -from robottelo.cli.base import CLIReturnCodeError +from robottelo.cli.base import Base, CLIReturnCodeError from robottelo.cli.contentview import ContentView -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_activation_key -from robottelo.cli.factory import make_architecture -from robottelo.cli.factory import make_content_view -from robottelo.cli.factory import make_fake_host -from robottelo.cli.factory import make_filter -from robottelo.cli.factory import make_lifecycle_environment -from robottelo.cli.factory import make_medium -from robottelo.cli.factory import make_os -from robottelo.cli.factory import make_partition_table -from robottelo.cli.factory import make_product -from robottelo.cli.factory import make_report_template -from robottelo.cli.factory import make_repository -from robottelo.cli.factory import make_role -from robottelo.cli.factory import make_template_input -from robottelo.cli.factory import make_user -from robottelo.cli.factory import setup_org_for_a_custom_repo -from robottelo.cli.factory import setup_org_for_a_rh_repo +from robottelo.cli.factory import ( + CLIFactoryError, + make_activation_key, + make_architecture, + make_content_view, + make_fake_host, + make_filter, + make_lifecycle_environment, + make_medium, + make_os, + make_partition_table, + make_product, + make_report_template, + make_repository, + make_role, + make_template_input, + make_user, + setup_org_for_a_custom_repo, + setup_org_for_a_rh_repo, +) from robottelo.cli.filter import Filter from robottelo.cli.host import Host from robottelo.cli.location import Location @@ -51,17 +52,19 @@ from robottelo.cli.subscription import Subscription from robottelo.cli.user import User from robottelo.config import settings -from robottelo.constants import DEFAULT_LOC -from robottelo.constants import DEFAULT_ORG -from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME -from robottelo.constants import FAKE_0_CUSTOM_PACKAGE_NAME -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE_NAME -from robottelo.constants import FAKE_2_CUSTOM_PACKAGE -from robottelo.constants import PRDS -from robottelo.constants import REPORT_TEMPLATE_FILE -from robottelo.constants import REPOS -from robottelo.constants import REPOSET +from robottelo.constants import ( + DEFAULT_LOC, + DEFAULT_ORG, + DEFAULT_SUBSCRIPTION_NAME, + FAKE_0_CUSTOM_PACKAGE_NAME, + FAKE_1_CUSTOM_PACKAGE, + FAKE_1_CUSTOM_PACKAGE_NAME, + FAKE_2_CUSTOM_PACKAGE, + PRDS, + REPORT_TEMPLATE_FILE, + REPOS, + REPOSET, +) from robottelo.hosts import ContentHost diff --git a/tests/foreman/cli/test_repository.py b/tests/foreman/cli/test_repository.py index 46e0e849a47..42fd08f4f8a 100644 --- a/tests/foreman/cli/test_repository.py +++ b/tests/foreman/cli/test_repository.py @@ -19,30 +19,29 @@ from random import choice from string import punctuation +from fauxfactory import gen_alphanumeric, gen_integer, gen_string, gen_url +from nailgun import entities import pytest import requests -from fauxfactory import gen_alphanumeric -from fauxfactory import gen_integer -from fauxfactory import gen_string -from fauxfactory import gen_url -from nailgun import entities from wait_for import wait_for from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.content_export import ContentExport from robottelo.cli.content_import import ContentImport from robottelo.cli.contentview import ContentView -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_content_credential -from robottelo.cli.factory import make_content_view -from robottelo.cli.factory import make_filter -from robottelo.cli.factory import make_lifecycle_environment -from robottelo.cli.factory import make_location -from robottelo.cli.factory import make_org -from robottelo.cli.factory import make_product -from robottelo.cli.factory import make_repository -from robottelo.cli.factory import make_role -from robottelo.cli.factory import make_user +from robottelo.cli.factory import ( + CLIFactoryError, + make_content_credential, + make_content_view, + make_filter, + make_lifecycle_environment, + make_location, + make_org, + make_product, + make_repository, + make_role, + make_user, +) from robottelo.cli.file import File from robottelo.cli.filter import Filter from robottelo.cli.module_stream import ModuleStream @@ -57,31 +56,37 @@ from robottelo.cli.task import Task from robottelo.cli.user import User from robottelo.config import settings -from robottelo.constants import CONTAINER_REGISTRY_HUB -from robottelo.constants import CONTAINER_UPSTREAM_NAME -from robottelo.constants import CUSTOM_FILE_REPO_FILES_COUNT -from robottelo.constants import CUSTOM_LOCAL_FOLDER -from robottelo.constants import DataFile -from robottelo.constants import DOWNLOAD_POLICIES -from robottelo.constants import MIRRORING_POLICIES -from robottelo.constants import OS_TEMPLATE_DATA_FILE -from robottelo.constants import REPO_TYPE -from robottelo.constants import RPM_TO_UPLOAD -from robottelo.constants import SRPM_TO_UPLOAD -from robottelo.constants.repos import ANSIBLE_GALAXY -from robottelo.constants.repos import CUSTOM_3RD_PARTY_REPO -from robottelo.constants.repos import CUSTOM_FILE_REPO -from robottelo.constants.repos import CUSTOM_RPM_SHA -from robottelo.constants.repos import FAKE_5_YUM_REPO -from robottelo.constants.repos import FAKE_YUM_DRPM_REPO -from robottelo.constants.repos import FAKE_YUM_MD5_REPO -from robottelo.constants.repos import FAKE_YUM_SRPM_REPO +from robottelo.constants import ( + CONTAINER_REGISTRY_HUB, + CONTAINER_UPSTREAM_NAME, + CUSTOM_FILE_REPO_FILES_COUNT, + CUSTOM_LOCAL_FOLDER, + DOWNLOAD_POLICIES, + MIRRORING_POLICIES, + OS_TEMPLATE_DATA_FILE, + REPO_TYPE, + RPM_TO_UPLOAD, + SRPM_TO_UPLOAD, + DataFile, +) +from robottelo.constants.repos import ( + ANSIBLE_GALAXY, + CUSTOM_3RD_PARTY_REPO, + CUSTOM_FILE_REPO, + CUSTOM_RPM_SHA, + FAKE_5_YUM_REPO, + FAKE_YUM_DRPM_REPO, + FAKE_YUM_MD5_REPO, + FAKE_YUM_SRPM_REPO, +) from robottelo.logging import logger -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list -from robottelo.utils.datafactory import valid_docker_repository_names -from robottelo.utils.datafactory import valid_http_credentials +from robottelo.utils.datafactory import ( + invalid_values_list, + parametrized, + valid_data_list, + valid_docker_repository_names, + valid_http_credentials, +) # from robottelo.constants.repos import FEDORA_OSTREE_REPO diff --git a/tests/foreman/cli/test_repository_set.py b/tests/foreman/cli/test_repository_set.py index cf9f991991a..0be98f8e32d 100644 --- a/tests/foreman/cli/test_repository_set.py +++ b/tests/foreman/cli/test_repository_set.py @@ -20,8 +20,7 @@ from robottelo.cli.product import Product from robottelo.cli.repository_set import RepositorySet -from robottelo.constants import PRDS -from robottelo.constants import REPOSET +from robottelo.constants import PRDS, REPOSET pytestmark = [pytest.mark.run_in_one_thread, pytest.mark.tier1] diff --git a/tests/foreman/cli/test_rhcloud_inventory.py b/tests/foreman/cli/test_rhcloud_inventory.py index 34d2f4a989f..cdbf63c2d2f 100644 --- a/tests/foreman/cli/test_rhcloud_inventory.py +++ b/tests/foreman/cli/test_rhcloud_inventory.py @@ -16,15 +16,14 @@ :Upstream: No """ -import time from datetime import datetime +import time import pytest from wait_for import wait_for from robottelo.config import robottelo_tmp_dir -from robottelo.utils.io import get_local_file_data -from robottelo.utils.io import get_remote_report_checksum +from robottelo.utils.io import get_local_file_data, get_remote_report_checksum inventory_sync_task = 'InventorySync::Async::InventoryFullSync' generate_report_jobs = 'ForemanInventoryUpload::Async::GenerateAllReportsJob' diff --git a/tests/foreman/cli/test_role.py b/tests/foreman/cli/test_role.py index 67c96671ef5..feeb1c928be 100644 --- a/tests/foreman/cli/test_role.py +++ b/tests/foreman/cli/test_role.py @@ -16,28 +16,27 @@ :Upstream: No """ -import re from math import ceil from random import choice +import re -import pytest from fauxfactory import gen_string +import pytest -from robottelo.cli.base import CLIDataBaseError -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_filter -from robottelo.cli.factory import make_location -from robottelo.cli.factory import make_org -from robottelo.cli.factory import make_role -from robottelo.cli.factory import make_user +from robottelo.cli.base import CLIDataBaseError, CLIReturnCodeError +from robottelo.cli.factory import ( + make_filter, + make_location, + make_org, + make_role, + make_user, +) from robottelo.cli.filter import Filter from robottelo.cli.role import Role from robottelo.cli.settings import Settings from robottelo.cli.user import User -from robottelo.constants import PERMISSIONS -from robottelo.constants import ROLES -from robottelo.utils.datafactory import generate_strings_list -from robottelo.utils.datafactory import parametrized +from robottelo.constants import PERMISSIONS, ROLES +from robottelo.utils.datafactory import generate_strings_list, parametrized class TestRole: diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index 4d47712da50..e6a0c37d91d 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -16,31 +16,35 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.content_export import ContentExport from robottelo.cli.content_import import ContentImport from robottelo.cli.contentview import ContentView -from robottelo.cli.factory import make_content_view -from robottelo.cli.factory import make_lifecycle_environment -from robottelo.cli.factory import make_org -from robottelo.cli.factory import make_product -from robottelo.cli.factory import make_repository +from robottelo.cli.factory import ( + make_content_view, + make_lifecycle_environment, + make_org, + make_product, + make_repository, +) from robottelo.cli.file import File from robottelo.cli.package import Package from robottelo.cli.product import Product from robottelo.cli.repository import Repository from robottelo.cli.settings import Settings from robottelo.config import settings -from robottelo.constants import CONTAINER_REGISTRY_HUB -from robottelo.constants import DEFAULT_ARCHITECTURE -from robottelo.constants import DEFAULT_CV -from robottelo.constants import PULP_EXPORT_DIR -from robottelo.constants import PULP_IMPORT_DIR -from robottelo.constants import REPO_TYPE -from robottelo.constants import REPOS +from robottelo.constants import ( + CONTAINER_REGISTRY_HUB, + DEFAULT_ARCHITECTURE, + DEFAULT_CV, + PULP_EXPORT_DIR, + PULP_IMPORT_DIR, + REPO_TYPE, + REPOS, +) from robottelo.constants.repos import ANSIBLE_GALAXY diff --git a/tests/foreman/cli/test_settings.py b/tests/foreman/cli/test_settings.py index 8e0c6144d35..3a47b6500a5 100644 --- a/tests/foreman/cli/test_settings.py +++ b/tests/foreman/cli/test_settings.py @@ -24,14 +24,16 @@ from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.settings import Settings from robottelo.config import settings -from robottelo.utils.datafactory import gen_string -from robottelo.utils.datafactory import generate_strings_list -from robottelo.utils.datafactory import invalid_boolean_strings -from robottelo.utils.datafactory import invalid_emails_list -from robottelo.utils.datafactory import valid_data_list -from robottelo.utils.datafactory import valid_emails_list -from robottelo.utils.datafactory import valid_url_list -from robottelo.utils.datafactory import xdist_adapter +from robottelo.utils.datafactory import ( + gen_string, + generate_strings_list, + invalid_boolean_strings, + invalid_emails_list, + valid_data_list, + valid_emails_list, + valid_url_list, + xdist_adapter, +) @pytest.mark.stubbed diff --git a/tests/foreman/cli/test_subnet.py b/tests/foreman/cli/test_subnet.py index 90ec40d3d05..6dc36a640f7 100644 --- a/tests/foreman/cli/test_subnet.py +++ b/tests/foreman/cli/test_subnet.py @@ -19,20 +19,18 @@ import random import re +from fauxfactory import gen_choice, gen_integer, gen_ipaddr import pytest -from fauxfactory import gen_choice -from fauxfactory import gen_integer -from fauxfactory import gen_ipaddr from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_domain -from robottelo.cli.factory import make_subnet +from robottelo.cli.factory import CLIFactoryError, make_domain, make_subnet from robottelo.cli.subnet import Subnet from robottelo.constants import SUBNET_IPAM_TYPES -from robottelo.utils.datafactory import filtered_datapoint -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + filtered_datapoint, + parametrized, + valid_data_list, +) @filtered_datapoint diff --git a/tests/foreman/cli/test_subscription.py b/tests/foreman/cli/test_subscription.py index 4628cb02ca3..d6feeb1381d 100644 --- a/tests/foreman/cli/test_subscription.py +++ b/tests/foreman/cli/test_subscription.py @@ -16,21 +16,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_activation_key -from robottelo.cli.factory import make_product -from robottelo.cli.factory import make_repository +from robottelo.cli.factory import make_activation_key, make_product, make_repository from robottelo.cli.host import Host from robottelo.cli.repository import Repository from robottelo.cli.repository_set import RepositorySet from robottelo.cli.subscription import Subscription -from robottelo.constants import PRDS -from robottelo.constants import REPOS -from robottelo.constants import REPOSET +from robottelo.constants import PRDS, REPOS, REPOSET pytestmark = [pytest.mark.run_in_one_thread] diff --git a/tests/foreman/cli/test_syncplan.py b/tests/foreman/cli/test_syncplan.py index 488c2637e2b..c22dcbe198e 100644 --- a/tests/foreman/cli/test_syncplan.py +++ b/tests/foreman/cli/test_syncplan.py @@ -16,31 +16,32 @@ :Upstream: No """ -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta from time import sleep -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_product -from robottelo.cli.factory import make_repository -from robottelo.cli.factory import make_sync_plan +from robottelo.cli.factory import ( + CLIFactoryError, + make_product, + make_repository, + make_sync_plan, +) from robottelo.cli.product import Product from robottelo.cli.repository import Repository from robottelo.cli.repository_set import RepositorySet from robottelo.cli.syncplan import SyncPlan -from robottelo.constants import PRDS -from robottelo.constants import REPOS -from robottelo.constants import REPOSET +from robottelo.constants import PRDS, REPOS, REPOSET from robottelo.logging import logger -from robottelo.utils.datafactory import filtered_datapoint -from robottelo.utils.datafactory import invalid_values_list -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import ( + filtered_datapoint, + invalid_values_list, + parametrized, + valid_data_list, +) SYNC_DATE_FMT = '%Y-%m-%d %H:%M:%S UTC' diff --git a/tests/foreman/cli/test_templatesync.py b/tests/foreman/cli/test_templatesync.py index c2bd4af9235..c1fa8ab41dc 100644 --- a/tests/foreman/cli/test_templatesync.py +++ b/tests/foreman/cli/test_templatesync.py @@ -16,17 +16,18 @@ """ import base64 -import pytest -import requests from fauxfactory import gen_string from nailgun import entities +import pytest +import requests from robottelo.cli.template import Template from robottelo.cli.template_sync import TemplateSync from robottelo.config import settings -from robottelo.constants import FOREMAN_TEMPLATE_IMPORT_URL -from robottelo.constants import FOREMAN_TEMPLATE_TEST_TEMPLATE - +from robottelo.constants import ( + FOREMAN_TEMPLATE_IMPORT_URL, + FOREMAN_TEMPLATE_TEST_TEMPLATE, +) git = settings.git diff --git a/tests/foreman/cli/test_user.py b/tests/foreman/cli/test_user.py index 3d681f67f95..c44d1807070 100644 --- a/tests/foreman/cli/test_user.py +++ b/tests/foreman/cli/test_user.py @@ -26,27 +26,30 @@ import random from time import sleep -import pytest -from fauxfactory import gen_alphanumeric -from fauxfactory import gen_string +from fauxfactory import gen_alphanumeric, gen_string from nailgun import entities +import pytest from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_filter -from robottelo.cli.factory import make_location -from robottelo.cli.factory import make_org -from robottelo.cli.factory import make_role -from robottelo.cli.factory import make_user +from robottelo.cli.factory import ( + make_filter, + make_location, + make_org, + make_role, + make_user, +) from robottelo.cli.filter import Filter from robottelo.cli.org import Org from robottelo.cli.user import User from robottelo.config import settings from robottelo.constants import LOCALES from robottelo.utils import gen_ssh_keypairs -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list -from robottelo.utils.datafactory import valid_emails_list -from robottelo.utils.datafactory import valid_usernames_list +from robottelo.utils.datafactory import ( + parametrized, + valid_data_list, + valid_emails_list, + valid_usernames_list, +) class TestUser: diff --git a/tests/foreman/cli/test_usergroup.py b/tests/foreman/cli/test_usergroup.py index 1d63f59a13c..57e3c8b9b17 100644 --- a/tests/foreman/cli/test_usergroup.py +++ b/tests/foreman/cli/test_usergroup.py @@ -21,15 +21,16 @@ import pytest from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_role -from robottelo.cli.factory import make_user -from robottelo.cli.factory import make_usergroup -from robottelo.cli.factory import make_usergroup_external +from robottelo.cli.factory import ( + make_role, + make_user, + make_usergroup, + make_usergroup_external, +) from robottelo.cli.ldapauthsource import LDAPAuthSource from robottelo.cli.task import Task from robottelo.cli.user import User -from robottelo.cli.usergroup import UserGroup -from robottelo.cli.usergroup import UserGroupExternal +from robottelo.cli.usergroup import UserGroup, UserGroupExternal from robottelo.utils.datafactory import valid_usernames_list diff --git a/tests/foreman/cli/test_vm_install_products_package.py b/tests/foreman/cli/test_vm_install_products_package.py index b60bcae7c90..1e16997457d 100644 --- a/tests/foreman/cli/test_vm_install_products_package.py +++ b/tests/foreman/cli/test_vm_install_products_package.py @@ -16,15 +16,17 @@ :Upstream: No """ -import pytest from broker import Broker +import pytest from robottelo.cli.factory import make_lifecycle_environment from robottelo.config import settings -from robottelo.constants import CONTAINER_REGISTRY_HUB -from robottelo.constants import CONTAINER_UPSTREAM_NAME -from robottelo.constants import DISTROS_SUPPORTED -from robottelo.constants import FAKE_0_CUSTOM_PACKAGE +from robottelo.constants import ( + CONTAINER_REGISTRY_HUB, + CONTAINER_UPSTREAM_NAME, + DISTROS_SUPPORTED, + FAKE_0_CUSTOM_PACKAGE, +) from robottelo.hosts import ContentHost diff --git a/tests/foreman/cli/test_webhook.py b/tests/foreman/cli/test_webhook.py index 002d1b360ec..3ca83dbe70f 100644 --- a/tests/foreman/cli/test_webhook.py +++ b/tests/foreman/cli/test_webhook.py @@ -19,14 +19,13 @@ from functools import partial from random import choice -import pytest from box import Box from fauxfactory import gen_alphanumeric +import pytest from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.webhook import Webhook -from robottelo.constants import WEBHOOK_EVENTS -from robottelo.constants import WEBHOOK_METHODS +from robottelo.constants import WEBHOOK_EVENTS, WEBHOOK_METHODS @pytest.fixture(scope='function') diff --git a/tests/foreman/destructive/test_auth.py b/tests/foreman/destructive/test_auth.py index 30f08f09b65..7ab8f29e2a7 100644 --- a/tests/foreman/destructive/test_auth.py +++ b/tests/foreman/destructive/test_auth.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings from robottelo.constants import HAMMER_CONFIG diff --git a/tests/foreman/destructive/test_capsule.py b/tests/foreman/destructive/test_capsule.py index 5739054af43..0051e062be5 100644 --- a/tests/foreman/destructive/test_capsule.py +++ b/tests/foreman/destructive/test_capsule.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.hosts import Capsule from robottelo.utils.installer import InstallerCommand diff --git a/tests/foreman/destructive/test_capsule_loadbalancer.py b/tests/foreman/destructive/test_capsule_loadbalancer.py index 49219359df2..3d9fdf02604 100644 --- a/tests/foreman/destructive/test_capsule_loadbalancer.py +++ b/tests/foreman/destructive/test_capsule_loadbalancer.py @@ -19,8 +19,7 @@ import pytest from robottelo.config import settings -from robottelo.constants import CLIENT_PORT -from robottelo.constants import DataFile +from robottelo.constants import CLIENT_PORT, DataFile from robottelo.utils.installer import InstallerCommand pytestmark = [pytest.mark.no_containers, pytest.mark.destructive] diff --git a/tests/foreman/destructive/test_capsulecontent.py b/tests/foreman/destructive/test_capsulecontent.py index c5a74334721..f7a6618ad3e 100644 --- a/tests/foreman/destructive/test_capsulecontent.py +++ b/tests/foreman/destructive/test_capsulecontent.py @@ -16,9 +16,9 @@ :Upstream: No """ -import pytest from box import Box from fauxfactory import gen_alpha +import pytest from robottelo import constants diff --git a/tests/foreman/destructive/test_contenthost.py b/tests/foreman/destructive/test_contenthost.py index 1f624475026..3d07a2b493d 100644 --- a/tests/foreman/destructive/test_contenthost.py +++ b/tests/foreman/destructive/test_contenthost.py @@ -19,8 +19,7 @@ import pytest from robottelo.config import settings -from robottelo.constants import FAKE_0_CUSTOM_PACKAGE -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE +from robottelo.constants import FAKE_0_CUSTOM_PACKAGE, FAKE_1_CUSTOM_PACKAGE pytestmark = pytest.mark.destructive diff --git a/tests/foreman/destructive/test_contentview.py b/tests/foreman/destructive/test_contentview.py index 99b1ea2a54b..76f91ac0f42 100644 --- a/tests/foreman/destructive/test_contentview.py +++ b/tests/foreman/destructive/test_contentview.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from nailgun.entity_mixins import TaskFailedError +import pytest from robottelo import constants diff --git a/tests/foreman/destructive/test_discoveredhost.py b/tests/foreman/destructive/test_discoveredhost.py index ace6c579106..4466747f2ed 100644 --- a/tests/foreman/destructive/test_discoveredhost.py +++ b/tests/foreman/destructive/test_discoveredhost.py @@ -14,13 +14,12 @@ :Upstream: No """ -import re from copy import copy +import re -import pytest from nailgun import entity_mixins -from wait_for import TimedOutError -from wait_for import wait_for +import pytest +from wait_for import TimedOutError, wait_for from robottelo.logging import logger diff --git a/tests/foreman/destructive/test_host.py b/tests/foreman/destructive/test_host.py index c0fe45f1b98..65d7ebe184d 100644 --- a/tests/foreman/destructive/test_host.py +++ b/tests/foreman/destructive/test_host.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from airgun.exceptions import NoSuchElementException +import pytest from robottelo.constants import ANY_CONTEXT diff --git a/tests/foreman/destructive/test_infoblox.py b/tests/foreman/destructive/test_infoblox.py index 11cef9636e3..113a4e4f067 100644 --- a/tests/foreman/destructive/test_infoblox.py +++ b/tests/foreman/destructive/test_infoblox.py @@ -14,10 +14,9 @@ :Upstream: No """ +from fauxfactory import gen_mac, gen_string import pytest import requests -from fauxfactory import gen_mac -from fauxfactory import gen_string from requests.exceptions import HTTPError from robottelo.config import settings diff --git a/tests/foreman/destructive/test_installer.py b/tests/foreman/destructive/test_installer.py index 2bade1108a3..19459340458 100644 --- a/tests/foreman/destructive/test_installer.py +++ b/tests/foreman/destructive/test_installer.py @@ -16,9 +16,8 @@ :Upstream: No """ +from fauxfactory import gen_domain, gen_string import pytest -from fauxfactory import gen_domain -from fauxfactory import gen_string from robottelo.config import settings from robottelo.utils.installer import InstallerCommand diff --git a/tests/foreman/destructive/test_ldap_authentication.py b/tests/foreman/destructive/test_ldap_authentication.py index 2288ed0cd7a..39f5a6b4f65 100644 --- a/tests/foreman/destructive/test_ldap_authentication.py +++ b/tests/foreman/destructive/test_ldap_authentication.py @@ -19,16 +19,13 @@ import os from time import sleep +from navmazing import NavigationTriesExceeded import pyotp import pytest -from navmazing import NavigationTriesExceeded from robottelo.cli.base import CLIReturnCodeError from robottelo.config import settings -from robottelo.constants import CERT_PATH -from robottelo.constants import HAMMER_CONFIG -from robottelo.constants import HAMMER_SESSIONS -from robottelo.constants import LDAP_ATTR +from robottelo.constants import CERT_PATH, HAMMER_CONFIG, HAMMER_SESSIONS, LDAP_ATTR from robottelo.logging import logger from robottelo.utils.datafactory import gen_string diff --git a/tests/foreman/destructive/test_ldapauthsource.py b/tests/foreman/destructive/test_ldapauthsource.py index 3d476be6e7d..fdbc2da11ba 100644 --- a/tests/foreman/destructive/test_ldapauthsource.py +++ b/tests/foreman/destructive/test_ldapauthsource.py @@ -24,7 +24,6 @@ from robottelo.config import settings from robottelo.constants import HAMMER_CONFIG - pytestmark = [pytest.mark.destructive] diff --git a/tests/foreman/destructive/test_leapp_satellite.py b/tests/foreman/destructive/test_leapp_satellite.py index 5bd6a19b9d7..f32d8bdfd13 100644 --- a/tests/foreman/destructive/test_leapp_satellite.py +++ b/tests/foreman/destructive/test_leapp_satellite.py @@ -14,11 +14,10 @@ :Upstream: No """ -import pytest from broker import Broker +import pytest -from robottelo.hosts import get_sat_rhel_version -from robottelo.hosts import get_sat_version +from robottelo.hosts import get_sat_rhel_version, get_sat_version @pytest.mark.e2e diff --git a/tests/foreman/destructive/test_puppetplugin.py b/tests/foreman/destructive/test_puppetplugin.py index faef6531427..3e79850a263 100644 --- a/tests/foreman/destructive/test_puppetplugin.py +++ b/tests/foreman/destructive/test_puppetplugin.py @@ -18,8 +18,7 @@ """ import pytest -from robottelo.constants import PUPPET_CAPSULE_INSTALLER -from robottelo.constants import PUPPET_COMMON_INSTALLER_OPTS +from robottelo.constants import PUPPET_CAPSULE_INSTALLER, PUPPET_COMMON_INSTALLER_OPTS from robottelo.hosts import Satellite from robottelo.utils.installer import InstallerCommand diff --git a/tests/foreman/destructive/test_realm.py b/tests/foreman/destructive/test_realm.py index f0dfdbfc3fe..de5a9b1ebc3 100644 --- a/tests/foreman/destructive/test_realm.py +++ b/tests/foreman/destructive/test_realm.py @@ -18,12 +18,11 @@ """ import random -import pytest from fauxfactory import gen_string +import pytest from robottelo.cli.base import CLIReturnCodeError - pytestmark = [pytest.mark.run_in_one_thread, pytest.mark.destructive] diff --git a/tests/foreman/destructive/test_remoteexecution.py b/tests/foreman/destructive/test_remoteexecution.py index 2be17e31d58..83962c4bf18 100644 --- a/tests/foreman/destructive/test_remoteexecution.py +++ b/tests/foreman/destructive/test_remoteexecution.py @@ -16,10 +16,10 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import client from nailgun.entity_mixins import TaskFailedError +import pytest from robottelo.config import get_credentials from robottelo.hosts import get_sat_version diff --git a/tests/foreman/destructive/test_rename.py b/tests/foreman/destructive/test_rename.py index 2204e3df5b6..493f3042c2d 100644 --- a/tests/foreman/destructive/test_rename.py +++ b/tests/foreman/destructive/test_rename.py @@ -17,8 +17,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.cli import hammer from robottelo.config import settings diff --git a/tests/foreman/destructive/test_repository.py b/tests/foreman/destructive/test_repository.py index f2ef0ddd4cf..2ff48fb13b2 100644 --- a/tests/foreman/destructive/test_repository.py +++ b/tests/foreman/destructive/test_repository.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from nailgun.entity_mixins import TaskFailedError +import pytest from robottelo import constants diff --git a/tests/foreman/endtoend/test_api_endtoend.py b/tests/foreman/endtoend/test_api_endtoend.py index f9471e1bbb7..47c28ffbefd 100644 --- a/tests/foreman/endtoend/test_api_endtoend.py +++ b/tests/foreman/endtoend/test_api_endtoend.py @@ -16,26 +16,26 @@ :Upstream: No """ -import http from collections import defaultdict +import http from pprint import pformat -import pytest from deepdiff import DeepDiff from fauxfactory import gen_string -from nailgun import client -from nailgun import entities +from nailgun import client, entities +import pytest from robottelo import constants -from robottelo.config import get_credentials -from robottelo.config import get_url -from robottelo.config import setting_is_set -from robottelo.config import settings -from robottelo.config import user_nailgun_config +from robottelo.config import ( + get_credentials, + get_url, + setting_is_set, + settings, + user_nailgun_config, +) from robottelo.constants.repos import CUSTOM_RPM_REPO from robottelo.utils.issue_handlers import is_open - API_PATHS = { # flake8:noqa (line-too-long) 'activation_keys': ( diff --git a/tests/foreman/endtoend/test_cli_endtoend.py b/tests/foreman/endtoend/test_cli_endtoend.py index 0fb77f62b26..0001e482043 100644 --- a/tests/foreman/endtoend/test_cli_endtoend.py +++ b/tests/foreman/endtoend/test_cli_endtoend.py @@ -16,9 +16,8 @@ :Upstream: No """ +from fauxfactory import gen_alphanumeric, gen_ipaddr import pytest -from fauxfactory import gen_alphanumeric -from fauxfactory import gen_ipaddr from robottelo import constants from robottelo.cli.activationkey import ActivationKey @@ -37,8 +36,7 @@ from robottelo.cli.subnet import Subnet from robottelo.cli.subscription import Subscription from robottelo.cli.user import User -from robottelo.config import setting_is_set -from robottelo.config import settings +from robottelo.config import setting_is_set, settings from robottelo.constants.repos import CUSTOM_RPM_REPO diff --git a/tests/foreman/installer/test_installer.py b/tests/foreman/installer/test_installer.py index 3f5c24564cc..f8ea4958087 100644 --- a/tests/foreman/installer/test_installer.py +++ b/tests/foreman/installer/test_installer.py @@ -21,15 +21,10 @@ from robottelo import ssh from robottelo.config import settings -from robottelo.constants import DEFAULT_ORG -from robottelo.constants import FOREMAN_SETTINGS_YML -from robottelo.constants import PRDS -from robottelo.constants import REPOS -from robottelo.constants import REPOSET +from robottelo.constants import DEFAULT_ORG, FOREMAN_SETTINGS_YML, PRDS, REPOS, REPOSET from robottelo.hosts import setup_capsule from robottelo.utils.installer import InstallerCommand - PREVIOUS_INSTALLER_OPTIONS = { '-', '--[no-]colors', diff --git a/tests/foreman/longrun/test_inc_updates.py b/tests/foreman/longrun/test_inc_updates.py index cb960fb0d5c..d305c9fe180 100644 --- a/tests/foreman/longrun/test_inc_updates.py +++ b/tests/foreman/longrun/test_inc_updates.py @@ -16,20 +16,21 @@ :Upstream: No """ -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta -import pytest from nailgun import entities +import pytest from robottelo.config import settings -from robottelo.constants import DEFAULT_ARCHITECTURE -from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME -from robottelo.constants import ENVIRONMENT -from robottelo.constants import FAKE_4_CUSTOM_PACKAGE -from robottelo.constants import PRDS -from robottelo.constants import REPOS -from robottelo.constants import REPOSET +from robottelo.constants import ( + DEFAULT_ARCHITECTURE, + DEFAULT_SUBSCRIPTION_NAME, + ENVIRONMENT, + FAKE_4_CUSTOM_PACKAGE, + PRDS, + REPOS, + REPOSET, +) pytestmark = [pytest.mark.run_in_one_thread] diff --git a/tests/foreman/longrun/test_oscap.py b/tests/foreman/longrun/test_oscap.py index 705150f23d9..3d292092c86 100644 --- a/tests/foreman/longrun/test_oscap.py +++ b/tests/foreman/longrun/test_oscap.py @@ -16,28 +16,28 @@ :Upstream: No """ -import pytest from broker import Broker from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo.cli.ansible import Ansible from robottelo.cli.arfreport import Arfreport -from robottelo.cli.factory import make_hostgroup -from robottelo.cli.factory import make_scap_policy +from robottelo.cli.factory import make_hostgroup, make_scap_policy from robottelo.cli.host import Host from robottelo.cli.job_invocation import JobInvocation from robottelo.cli.proxy import Proxy from robottelo.cli.scapcontent import Scapcontent from robottelo.config import settings -from robottelo.constants import OSCAP_DEFAULT_CONTENT -from robottelo.constants import OSCAP_PERIOD -from robottelo.constants import OSCAP_PROFILE -from robottelo.constants import OSCAP_WEEKDAY +from robottelo.constants import ( + OSCAP_DEFAULT_CONTENT, + OSCAP_PERIOD, + OSCAP_PROFILE, + OSCAP_WEEKDAY, +) from robottelo.exceptions import ProxyError from robottelo.hosts import ContentHost - rhel6_content = OSCAP_DEFAULT_CONTENT['rhel6_content'] rhel7_content = OSCAP_DEFAULT_CONTENT['rhel7_content'] rhel8_content = OSCAP_DEFAULT_CONTENT['rhel8_content'] diff --git a/tests/foreman/longrun/test_provisioning_computeresource.py b/tests/foreman/longrun/test_provisioning_computeresource.py index 3825a2c6e93..65444304df0 100644 --- a/tests/foreman/longrun/test_provisioning_computeresource.py +++ b/tests/foreman/longrun/test_provisioning_computeresource.py @@ -11,16 +11,14 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from wrapanapi import VMWareSystem -from robottelo.cli.factory import make_compute_resource -from robottelo.cli.factory import make_host +from robottelo.cli.factory import make_compute_resource, make_host from robottelo.cli.host import Host from robottelo.config import settings -from robottelo.constants import FOREMAN_PROVIDERS -from robottelo.constants import VMWARE_CONSTANTS +from robottelo.constants import FOREMAN_PROVIDERS, VMWARE_CONSTANTS @pytest.fixture(scope="module") diff --git a/tests/foreman/maintain/test_advanced.py b/tests/foreman/maintain/test_advanced.py index eaf8ee304ec..279ce48c4eb 100644 --- a/tests/foreman/maintain/test_advanced.py +++ b/tests/foreman/maintain/test_advanced.py @@ -19,13 +19,9 @@ import pytest import yaml -from robottelo.config import robottelo_tmp_dir -from robottelo.config import settings -from robottelo.constants import MAINTAIN_HAMMER_YML -from robottelo.constants import SAT_NON_GA_VERSIONS -from robottelo.hosts import get_sat_rhel_version -from robottelo.hosts import get_sat_version - +from robottelo.config import robottelo_tmp_dir, settings +from robottelo.constants import MAINTAIN_HAMMER_YML, SAT_NON_GA_VERSIONS +from robottelo.hosts import get_sat_rhel_version, get_sat_version sat_x_y_release = f'{get_sat_version().major}.{get_sat_version().minor}' diff --git a/tests/foreman/maintain/test_backup_restore.py b/tests/foreman/maintain/test_backup_restore.py index e2e5e909c77..473f768fa74 100644 --- a/tests/foreman/maintain/test_backup_restore.py +++ b/tests/foreman/maintain/test_backup_restore.py @@ -18,8 +18,8 @@ """ import re -import pytest from fauxfactory import gen_string +import pytest from robottelo import constants from robottelo.config import settings diff --git a/tests/foreman/maintain/test_health.py b/tests/foreman/maintain/test_health.py index 129de58724b..c1d590546ec 100644 --- a/tests/foreman/maintain/test_health.py +++ b/tests/foreman/maintain/test_health.py @@ -18,13 +18,12 @@ """ import time -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings from robottelo.utils.installer import InstallerCommand - upstream_url = { 'foreman_repo': 'https://yum.theforeman.org/releases/nightly/el8/x86_64/', 'puppet_repo': 'https://yum.puppetlabs.com/puppet/el/8/x86_64/', diff --git a/tests/foreman/maintain/test_service.py b/tests/foreman/maintain/test_service.py index 0970bf9e2c2..99bb6074ae5 100644 --- a/tests/foreman/maintain/test_service.py +++ b/tests/foreman/maintain/test_service.py @@ -16,13 +16,15 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.constants import HAMMER_CONFIG -from robottelo.constants import MAINTAIN_HAMMER_YML -from robottelo.constants import SATELLITE_ANSWER_FILE +from robottelo.constants import ( + HAMMER_CONFIG, + MAINTAIN_HAMMER_YML, + SATELLITE_ANSWER_FILE, +) from robottelo.hosts import Satellite SATELLITE_SERVICES = [ diff --git a/tests/foreman/maintain/test_upgrade.py b/tests/foreman/maintain/test_upgrade.py index afc946547b5..bc041ee4601 100644 --- a/tests/foreman/maintain/test_upgrade.py +++ b/tests/foreman/maintain/test_upgrade.py @@ -19,8 +19,7 @@ import pytest from robottelo.config import settings -from robottelo.constants import INSTALLER_CONFIG_FILE -from robottelo.constants import SATELLITE_VERSION +from robottelo.constants import INSTALLER_CONFIG_FILE, SATELLITE_VERSION def last_y_stream_version(release): diff --git a/tests/foreman/sys/test_fam.py b/tests/foreman/sys/test_fam.py index 7a41ffba451..aeab1f2cc4c 100644 --- a/tests/foreman/sys/test_fam.py +++ b/tests/foreman/sys/test_fam.py @@ -18,9 +18,7 @@ """ import pytest -from robottelo.constants import FAM_MODULE_PATH -from robottelo.constants import FOREMAN_ANSIBLE_MODULES -from robottelo.constants import RH_SAT_ROLES +from robottelo.constants import FAM_MODULE_PATH, FOREMAN_ANSIBLE_MODULES, RH_SAT_ROLES @pytest.fixture diff --git a/tests/foreman/sys/test_pulp3_filesystem.py b/tests/foreman/sys/test_pulp3_filesystem.py index ab24ca52e17..1e868cd691f 100644 --- a/tests/foreman/sys/test_pulp3_filesystem.py +++ b/tests/foreman/sys/test_pulp3_filesystem.py @@ -16,8 +16,8 @@ :Upstream: No """ -import json from datetime import datetime +import json import pytest diff --git a/tests/foreman/ui/test_activationkey.py b/tests/foreman/ui/test_activationkey.py index 91671bb1d1b..fe69f2d552c 100644 --- a/tests/foreman/ui/test_activationkey.py +++ b/tests/foreman/ui/test_activationkey.py @@ -18,18 +18,17 @@ """ import random -import pytest from airgun.session import Session from broker import Broker from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo import constants from robottelo.cli.factory import setup_org_for_a_custom_repo from robottelo.config import settings from robottelo.hosts import ContentHost -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_data_list +from robottelo.utils.datafactory import parametrized, valid_data_list @pytest.mark.e2e diff --git a/tests/foreman/ui/test_ansible.py b/tests/foreman/ui/test_ansible.py index 866ad4da049..378552b5dc5 100644 --- a/tests/foreman/ui/test_ansible.py +++ b/tests/foreman/ui/test_ansible.py @@ -16,13 +16,12 @@ :Upstream: No """ +from fauxfactory import gen_string import pytest import yaml -from fauxfactory import gen_string from robottelo import constants -from robottelo.config import robottelo_tmp_dir -from robottelo.config import settings +from robottelo.config import robottelo_tmp_dir, settings def test_positive_create_and_delete_variable(target_sat): diff --git a/tests/foreman/ui/test_architecture.py b/tests/foreman/ui/test_architecture.py index ac7067449ba..bafaa89f78f 100644 --- a/tests/foreman/ui/test_architecture.py +++ b/tests/foreman/ui/test_architecture.py @@ -16,9 +16,9 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest @pytest.mark.tier2 diff --git a/tests/foreman/ui/test_audit.py b/tests/foreman/ui/test_audit.py index ecb7c87d7ce..ca506d21292 100644 --- a/tests/foreman/ui/test_audit.py +++ b/tests/foreman/ui/test_audit.py @@ -14,9 +14,9 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo.constants import ENVIRONMENT diff --git a/tests/foreman/ui/test_bookmarks.py b/tests/foreman/ui/test_bookmarks.py index cb0201a814b..180a0bf205e 100644 --- a/tests/foreman/ui/test_bookmarks.py +++ b/tests/foreman/ui/test_bookmarks.py @@ -16,11 +16,11 @@ :Upstream: No """ -import pytest from airgun.exceptions import NoSuchElementException from airgun.session import Session from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo.config import user_nailgun_config from robottelo.constants import BOOKMARK_ENTITIES diff --git a/tests/foreman/ui/test_branding.py b/tests/foreman/ui/test_branding.py index e6d481ca3f3..3067614e02a 100644 --- a/tests/foreman/ui/test_branding.py +++ b/tests/foreman/ui/test_branding.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from airgun.session import Session +import pytest @pytest.mark.e2e diff --git a/tests/foreman/ui/test_computeprofiles.py b/tests/foreman/ui/test_computeprofiles.py index efa011af4a8..5aea38df2cf 100644 --- a/tests/foreman/ui/test_computeprofiles.py +++ b/tests/foreman/ui/test_computeprofiles.py @@ -16,9 +16,9 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest @pytest.mark.tier2 diff --git a/tests/foreman/ui/test_computeresource.py b/tests/foreman/ui/test_computeresource.py index 151143e2edc..000f95951f8 100644 --- a/tests/foreman/ui/test_computeresource.py +++ b/tests/foreman/ui/test_computeresource.py @@ -16,18 +16,14 @@ :Upstream: No """ -import pytest from nailgun import entities +import pytest from wait_for import wait_for -from robottelo.config import setting_is_set -from robottelo.config import settings -from robottelo.constants import COMPUTE_PROFILE_LARGE -from robottelo.constants import DEFAULT_LOC -from robottelo.constants import FOREMAN_PROVIDERS +from robottelo.config import setting_is_set, settings +from robottelo.constants import COMPUTE_PROFILE_LARGE, DEFAULT_LOC, FOREMAN_PROVIDERS from robottelo.utils.datafactory import gen_string - # TODO mark this on the module with a lambda for skip condition # so that this is executed during the session at run loop, instead of at module import if not setting_is_set('rhev'): diff --git a/tests/foreman/ui/test_computeresource_azurerm.py b/tests/foreman/ui/test_computeresource_azurerm.py index ed3c6770b57..aab0bdf6c53 100644 --- a/tests/foreman/ui/test_computeresource_azurerm.py +++ b/tests/foreman/ui/test_computeresource_azurerm.py @@ -16,14 +16,16 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.constants import AZURERM_FILE_URI -from robottelo.constants import AZURERM_PLATFORM_DEFAULT -from robottelo.constants import AZURERM_VM_SIZE_DEFAULT -from robottelo.constants import COMPUTE_PROFILE_SMALL +from robottelo.constants import ( + AZURERM_FILE_URI, + AZURERM_PLATFORM_DEFAULT, + AZURERM_VM_SIZE_DEFAULT, + COMPUTE_PROFILE_SMALL, +) pytestmark = [pytest.mark.skip_if_not_set('azurerm')] diff --git a/tests/foreman/ui/test_computeresource_ec2.py b/tests/foreman/ui/test_computeresource_ec2.py index a232e65125c..94c50f69547 100644 --- a/tests/foreman/ui/test_computeresource_ec2.py +++ b/tests/foreman/ui/test_computeresource_ec2.py @@ -16,15 +16,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo.config import settings -from robottelo.constants import AWS_EC2_FLAVOR_T2_MICRO -from robottelo.constants import COMPUTE_PROFILE_LARGE -from robottelo.constants import EC2_REGION_CA_CENTRAL_1 -from robottelo.constants import FOREMAN_PROVIDERS +from robottelo.constants import ( + AWS_EC2_FLAVOR_T2_MICRO, + COMPUTE_PROFILE_LARGE, + EC2_REGION_CA_CENTRAL_1, + FOREMAN_PROVIDERS, +) pytestmark = [pytest.mark.skip_if_not_set('ec2')] diff --git a/tests/foreman/ui/test_computeresource_gce.py b/tests/foreman/ui/test_computeresource_gce.py index 9d4ae1e1d99..c4618bfb608 100644 --- a/tests/foreman/ui/test_computeresource_gce.py +++ b/tests/foreman/ui/test_computeresource_gce.py @@ -19,16 +19,18 @@ import json import random -import pytest from fauxfactory import gen_string +import pytest from wait_for import wait_for from robottelo.config import settings -from robottelo.constants import COMPUTE_PROFILE_SMALL -from robottelo.constants import FOREMAN_PROVIDERS -from robottelo.constants import GCE_EXTERNAL_IP_DEFAULT -from robottelo.constants import GCE_MACHINE_TYPE_DEFAULT -from robottelo.constants import GCE_NETWORK_DEFAULT +from robottelo.constants import ( + COMPUTE_PROFILE_SMALL, + FOREMAN_PROVIDERS, + GCE_EXTERNAL_IP_DEFAULT, + GCE_MACHINE_TYPE_DEFAULT, + GCE_NETWORK_DEFAULT, +) @pytest.mark.tier2 diff --git a/tests/foreman/ui/test_computeresource_libvirt.py b/tests/foreman/ui/test_computeresource_libvirt.py index 801064ae2ce..1cb250d2346 100644 --- a/tests/foreman/ui/test_computeresource_libvirt.py +++ b/tests/foreman/ui/test_computeresource_libvirt.py @@ -18,14 +18,16 @@ """ from random import choice -import pytest from fauxfactory import gen_string +import pytest from wait_for import wait_for from robottelo.config import settings -from robottelo.constants import COMPUTE_PROFILE_SMALL -from robottelo.constants import FOREMAN_PROVIDERS -from robottelo.constants import LIBVIRT_RESOURCE_URL +from robottelo.constants import ( + COMPUTE_PROFILE_SMALL, + FOREMAN_PROVIDERS, + LIBVIRT_RESOURCE_URL, +) pytestmark = [pytest.mark.skip_if_not_set('libvirt')] diff --git a/tests/foreman/ui/test_computeresource_vmware.py b/tests/foreman/ui/test_computeresource_vmware.py index 5502ed6292e..dfe4ca1abe7 100644 --- a/tests/foreman/ui/test_computeresource_vmware.py +++ b/tests/foreman/ui/test_computeresource_vmware.py @@ -16,22 +16,21 @@ :Upstream: No """ -from math import floor -from math import log10 +from math import floor, log10 from random import choice -import pytest from nailgun import entities -from wait_for import TimedOutError -from wait_for import wait_for -from wrapanapi.systems.virtualcenter import vim -from wrapanapi.systems.virtualcenter import VMWareSystem +import pytest +from wait_for import TimedOutError, wait_for +from wrapanapi.systems.virtualcenter import VMWareSystem, vim from robottelo.config import settings -from robottelo.constants import COMPUTE_PROFILE_LARGE -from robottelo.constants import DEFAULT_LOC -from robottelo.constants import FOREMAN_PROVIDERS -from robottelo.constants import VMWARE_CONSTANTS +from robottelo.constants import ( + COMPUTE_PROFILE_LARGE, + DEFAULT_LOC, + FOREMAN_PROVIDERS, + VMWARE_CONSTANTS, +) from robottelo.utils.datafactory import gen_string pytestmark = [pytest.mark.skip_if_not_set('vmware')] diff --git a/tests/foreman/ui/test_config_group.py b/tests/foreman/ui/test_config_group.py index db2b6eba171..ada7fc5b8c4 100644 --- a/tests/foreman/ui/test_config_group.py +++ b/tests/foreman/ui/test_config_group.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest @pytest.fixture(scope='module') diff --git a/tests/foreman/ui/test_containerimagetag.py b/tests/foreman/ui/test_containerimagetag.py index dc3dba99bcd..681cb1e877d 100644 --- a/tests/foreman/ui/test_containerimagetag.py +++ b/tests/foreman/ui/test_containerimagetag.py @@ -16,13 +16,15 @@ :Upstream: No """ -import pytest from nailgun import entities +import pytest -from robottelo.constants import CONTAINER_REGISTRY_HUB -from robottelo.constants import CONTAINER_UPSTREAM_NAME -from robottelo.constants import ENVIRONMENT -from robottelo.constants import REPO_TYPE +from robottelo.constants import ( + CONTAINER_REGISTRY_HUB, + CONTAINER_UPSTREAM_NAME, + ENVIRONMENT, + REPO_TYPE, +) @pytest.fixture(scope="module") diff --git a/tests/foreman/ui/test_contentcredentials.py b/tests/foreman/ui/test_contentcredentials.py index 36d1575c796..d53d84618f6 100644 --- a/tests/foreman/ui/test_contentcredentials.py +++ b/tests/foreman/ui/test_contentcredentials.py @@ -19,8 +19,7 @@ import pytest from robottelo.config import settings -from robottelo.constants import CONTENT_CREDENTIALS_TYPES -from robottelo.constants import DataFile +from robottelo.constants import CONTENT_CREDENTIALS_TYPES, DataFile from robottelo.utils.datafactory import gen_string empty_message = "You currently don't have any Products associated with this Content Credential." diff --git a/tests/foreman/ui/test_contenthost.py b/tests/foreman/ui/test_contenthost.py index 36ff6823588..1db98df1d70 100644 --- a/tests/foreman/ui/test_contenthost.py +++ b/tests/foreman/ui/test_contenthost.py @@ -16,34 +16,31 @@ :Upstream: No """ +from datetime import datetime, timedelta import re -from datetime import datetime -from datetime import timedelta from urllib.parse import urlparse -import pytest from airgun.session import Session -from fauxfactory import gen_integer -from fauxfactory import gen_string +from fauxfactory import gen_integer, gen_string from nailgun import entities +import pytest -from robottelo.cli.factory import CLIFactoryError -from robottelo.cli.factory import make_fake_host -from robottelo.cli.factory import make_virt_who_config -from robottelo.config import setting_is_set -from robottelo.config import settings -from robottelo.constants import DEFAULT_SYSPURPOSE_ATTRIBUTES -from robottelo.constants import FAKE_0_CUSTOM_PACKAGE -from robottelo.constants import FAKE_0_CUSTOM_PACKAGE_GROUP -from robottelo.constants import FAKE_0_CUSTOM_PACKAGE_GROUP_NAME -from robottelo.constants import FAKE_0_CUSTOM_PACKAGE_NAME -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE_NAME -from robottelo.constants import FAKE_1_ERRATA_ID -from robottelo.constants import FAKE_2_CUSTOM_PACKAGE -from robottelo.constants import FAKE_2_CUSTOM_PACKAGE_NAME -from robottelo.constants import VDC_SUBSCRIPTION_NAME -from robottelo.constants import VIRT_WHO_HYPERVISOR_TYPES +from robottelo.cli.factory import CLIFactoryError, make_fake_host, make_virt_who_config +from robottelo.config import setting_is_set, settings +from robottelo.constants import ( + DEFAULT_SYSPURPOSE_ATTRIBUTES, + FAKE_0_CUSTOM_PACKAGE, + FAKE_0_CUSTOM_PACKAGE_GROUP, + FAKE_0_CUSTOM_PACKAGE_GROUP_NAME, + FAKE_0_CUSTOM_PACKAGE_NAME, + FAKE_1_CUSTOM_PACKAGE, + FAKE_1_CUSTOM_PACKAGE_NAME, + FAKE_1_ERRATA_ID, + FAKE_2_CUSTOM_PACKAGE, + FAKE_2_CUSTOM_PACKAGE_NAME, + VDC_SUBSCRIPTION_NAME, + VIRT_WHO_HYPERVISOR_TYPES, +) from robottelo.utils.issue_handlers import is_open from robottelo.utils.virtwho import create_fake_hypervisor_content diff --git a/tests/foreman/ui/test_contentview.py b/tests/foreman/ui/test_contentview.py index 21d900a582b..bf8771092ca 100644 --- a/tests/foreman/ui/test_contentview.py +++ b/tests/foreman/ui/test_contentview.py @@ -22,36 +22,37 @@ import datetime from random import randint -import pytest -from airgun.exceptions import InvalidElementStateException -from airgun.exceptions import NoSuchElementException +from airgun.exceptions import InvalidElementStateException, NoSuchElementException from airgun.session import Session from nailgun import entities from nailgun.entity_mixins import call_entity_method_with_timeout from navmazing import NavigationTriesExceeded from productmd.common import parse_nvra +import pytest from robottelo import constants from robottelo.cli.contentview import ContentView from robottelo.config import settings -from robottelo.constants import CONTAINER_REGISTRY_HUB -from robottelo.constants import CONTAINER_UPSTREAM_NAME -from robottelo.constants import DEFAULT_ARCHITECTURE -from robottelo.constants import DEFAULT_CV -from robottelo.constants import DEFAULT_PTABLE -from robottelo.constants import ENVIRONMENT -from robottelo.constants import FAKE_0_CUSTOM_PACKAGE -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE -from robottelo.constants import FAKE_2_CUSTOM_PACKAGE -from robottelo.constants import FAKE_9_YUM_SECURITY_ERRATUM_COUNT -from robottelo.constants import FILTER_CONTENT_TYPE -from robottelo.constants import FILTER_ERRATA_TYPE -from robottelo.constants import FILTER_TYPE -from robottelo.constants import PERMISSIONS -from robottelo.constants import PRDS -from robottelo.constants import REPO_TYPE -from robottelo.constants import REPOS -from robottelo.constants import REPOSET +from robottelo.constants import ( + CONTAINER_REGISTRY_HUB, + CONTAINER_UPSTREAM_NAME, + DEFAULT_ARCHITECTURE, + DEFAULT_CV, + DEFAULT_PTABLE, + ENVIRONMENT, + FAKE_0_CUSTOM_PACKAGE, + FAKE_1_CUSTOM_PACKAGE, + FAKE_2_CUSTOM_PACKAGE, + FAKE_9_YUM_SECURITY_ERRATUM_COUNT, + FILTER_CONTENT_TYPE, + FILTER_ERRATA_TYPE, + FILTER_TYPE, + PERMISSIONS, + PRDS, + REPO_TYPE, + REPOS, + REPOSET, +) from robottelo.utils.datafactory import gen_string VERSION = 'Version 1.0' diff --git a/tests/foreman/ui/test_dashboard.py b/tests/foreman/ui/test_dashboard.py index 439f80c48e7..80b7ce16732 100644 --- a/tests/foreman/ui/test_dashboard.py +++ b/tests/foreman/ui/test_dashboard.py @@ -16,10 +16,10 @@ :Upstream: No """ -import pytest from airgun.session import Session from nailgun import entities from nailgun.entity_mixins import TaskFailedError +import pytest from robottelo.config import settings from robottelo.constants import FAKE_7_CUSTOM_PACKAGE diff --git a/tests/foreman/ui/test_discoveredhost.py b/tests/foreman/ui/test_discoveredhost.py index f694e6a86a2..ce3ea4d506e 100644 --- a/tests/foreman/ui/test_discoveredhost.py +++ b/tests/foreman/ui/test_discoveredhost.py @@ -14,10 +14,9 @@ :Upstream: No """ -import pytest -from fauxfactory import gen_ipaddr -from fauxfactory import gen_string +from fauxfactory import gen_ipaddr, gen_string from nailgun import entities +import pytest from robottelo.utils import ssh diff --git a/tests/foreman/ui/test_discoveryrule.py b/tests/foreman/ui/test_discoveryrule.py index 6efd92c5c17..e529578efca 100644 --- a/tests/foreman/ui/test_discoveryrule.py +++ b/tests/foreman/ui/test_discoveryrule.py @@ -16,11 +16,9 @@ :Upstream: No """ -import pytest from airgun.session import Session -from fauxfactory import gen_integer -from fauxfactory import gen_ipaddr -from fauxfactory import gen_string +from fauxfactory import gen_integer, gen_ipaddr, gen_string +import pytest @pytest.fixture diff --git a/tests/foreman/ui/test_domain.py b/tests/foreman/ui/test_domain.py index 48e297f591a..c082bef7d83 100644 --- a/tests/foreman/ui/test_domain.py +++ b/tests/foreman/ui/test_domain.py @@ -16,9 +16,9 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo.utils.datafactory import valid_domain_names diff --git a/tests/foreman/ui/test_errata.py b/tests/foreman/ui/test_errata.py index 36e86537e0c..fb0c703530b 100644 --- a/tests/foreman/ui/test_errata.py +++ b/tests/foreman/ui/test_errata.py @@ -16,34 +16,35 @@ :Upstream: No """ -import pytest from airgun.session import Session from broker import Broker from fauxfactory import gen_string from manifester import Manifester from nailgun import entities +import pytest from robottelo.config import settings -from robottelo.constants import DEFAULT_LOC -from robottelo.constants import FAKE_10_YUM_BUGFIX_ERRATUM -from robottelo.constants import FAKE_10_YUM_BUGFIX_ERRATUM_COUNT -from robottelo.constants import FAKE_11_YUM_ENHANCEMENT_ERRATUM -from robottelo.constants import FAKE_11_YUM_ENHANCEMENT_ERRATUM_COUNT -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE -from robottelo.constants import FAKE_2_CUSTOM_PACKAGE -from robottelo.constants import FAKE_3_YUM_OUTDATED_PACKAGES -from robottelo.constants import FAKE_4_CUSTOM_PACKAGE -from robottelo.constants import FAKE_5_CUSTOM_PACKAGE -from robottelo.constants import FAKE_9_YUM_OUTDATED_PACKAGES -from robottelo.constants import FAKE_9_YUM_SECURITY_ERRATUM -from robottelo.constants import FAKE_9_YUM_SECURITY_ERRATUM_COUNT -from robottelo.constants import PRDS -from robottelo.constants import REAL_0_RH_PACKAGE -from robottelo.constants import REAL_4_ERRATA_CVES -from robottelo.constants import REAL_4_ERRATA_ID +from robottelo.constants import ( + DEFAULT_LOC, + FAKE_1_CUSTOM_PACKAGE, + FAKE_2_CUSTOM_PACKAGE, + FAKE_3_YUM_OUTDATED_PACKAGES, + FAKE_4_CUSTOM_PACKAGE, + FAKE_5_CUSTOM_PACKAGE, + FAKE_9_YUM_OUTDATED_PACKAGES, + FAKE_9_YUM_SECURITY_ERRATUM, + FAKE_9_YUM_SECURITY_ERRATUM_COUNT, + FAKE_10_YUM_BUGFIX_ERRATUM, + FAKE_10_YUM_BUGFIX_ERRATUM_COUNT, + FAKE_11_YUM_ENHANCEMENT_ERRATUM, + FAKE_11_YUM_ENHANCEMENT_ERRATUM_COUNT, + PRDS, + REAL_0_RH_PACKAGE, + REAL_4_ERRATA_CVES, + REAL_4_ERRATA_ID, +) from robottelo.hosts import ContentHost - CUSTOM_REPO_URL = settings.repos.yum_9.url CUSTOM_REPO_ERRATA_ID = settings.repos.yum_9.errata[0] diff --git a/tests/foreman/ui/test_hardwaremodel.py b/tests/foreman/ui/test_hardwaremodel.py index 5d7d8d2f02a..4ec42851ab7 100644 --- a/tests/foreman/ui/test_hardwaremodel.py +++ b/tests/foreman/ui/test_hardwaremodel.py @@ -14,9 +14,9 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest @pytest.mark.e2e diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index 413a0398107..1d3b734e2ea 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -18,32 +18,33 @@ """ import copy import csv +from datetime import datetime import os import re -from datetime import datetime -import pytest -import yaml -from airgun.exceptions import DisabledWidgetError -from airgun.exceptions import NoSuchElementException +from airgun.exceptions import DisabledWidgetError, NoSuchElementException from airgun.session import Session +import pytest from wait_for import wait_for +import yaml from robottelo import constants from robottelo.config import settings -from robottelo.constants import ANY_CONTEXT -from robottelo.constants import DEFAULT_CV -from robottelo.constants import DEFAULT_LOC -from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME -from robottelo.constants import ENVIRONMENT -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE -from robottelo.constants import FAKE_7_CUSTOM_PACKAGE -from robottelo.constants import FAKE_8_CUSTOM_PACKAGE -from robottelo.constants import FAKE_8_CUSTOM_PACKAGE_NAME -from robottelo.constants import OSCAP_PERIOD -from robottelo.constants import OSCAP_WEEKDAY -from robottelo.constants import PERMISSIONS -from robottelo.constants import REPO_TYPE +from robottelo.constants import ( + ANY_CONTEXT, + DEFAULT_CV, + DEFAULT_LOC, + DEFAULT_SUBSCRIPTION_NAME, + ENVIRONMENT, + FAKE_1_CUSTOM_PACKAGE, + FAKE_7_CUSTOM_PACKAGE, + FAKE_8_CUSTOM_PACKAGE, + FAKE_8_CUSTOM_PACKAGE_NAME, + OSCAP_PERIOD, + OSCAP_WEEKDAY, + PERMISSIONS, + REPO_TYPE, +) from robottelo.utils.datafactory import gen_string from robottelo.utils.issue_handlers import is_open diff --git a/tests/foreman/ui/test_hostcollection.py b/tests/foreman/ui/test_hostcollection.py index 689a93afae4..2c60125c293 100644 --- a/tests/foreman/ui/test_hostcollection.py +++ b/tests/foreman/ui/test_hostcollection.py @@ -18,9 +18,9 @@ """ import time -import pytest from broker import Broker from manifester import Manifester +import pytest from robottelo import constants from robottelo.config import settings diff --git a/tests/foreman/ui/test_hostgroup.py b/tests/foreman/ui/test_hostgroup.py index 39878c78c94..c8a1d23a897 100644 --- a/tests/foreman/ui/test_hostgroup.py +++ b/tests/foreman/ui/test_hostgroup.py @@ -16,13 +16,12 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo.config import settings -from robottelo.constants import DEFAULT_CV -from robottelo.constants import ENVIRONMENT +from robottelo.constants import DEFAULT_CV, ENVIRONMENT @pytest.mark.e2e diff --git a/tests/foreman/ui/test_http_proxy.py b/tests/foreman/ui/test_http_proxy.py index cddc7cd1e42..2f48559cac2 100644 --- a/tests/foreman/ui/test_http_proxy.py +++ b/tests/foreman/ui/test_http_proxy.py @@ -16,14 +16,11 @@ :Upstream: No """ +from fauxfactory import gen_integer, gen_string, gen_url import pytest -from fauxfactory import gen_integer -from fauxfactory import gen_string -from fauxfactory import gen_url from robottelo.config import settings -from robottelo.constants import DOCKER_REPO_UPSTREAM_NAME -from robottelo.constants import REPO_TYPE +from robottelo.constants import DOCKER_REPO_UPSTREAM_NAME, REPO_TYPE @pytest.mark.tier2 diff --git a/tests/foreman/ui/test_jobinvocation.py b/tests/foreman/ui/test_jobinvocation.py index 2dbf8f0cf4a..62ab4ed5ef4 100644 --- a/tests/foreman/ui/test_jobinvocation.py +++ b/tests/foreman/ui/test_jobinvocation.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from inflection import camelize +import pytest from robottelo.utils.datafactory import gen_string diff --git a/tests/foreman/ui/test_jobtemplate.py b/tests/foreman/ui/test_jobtemplate.py index 1041878f753..798400aa976 100644 --- a/tests/foreman/ui/test_jobtemplate.py +++ b/tests/foreman/ui/test_jobtemplate.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest @pytest.mark.e2e diff --git a/tests/foreman/ui/test_ldap_authentication.py b/tests/foreman/ui/test_ldap_authentication.py index 01ac2987fe4..e1983767e77 100644 --- a/tests/foreman/ui/test_ldap_authentication.py +++ b/tests/foreman/ui/test_ldap_authentication.py @@ -18,21 +18,17 @@ """ import os -import pyotp -import pytest from airgun.session import Session from fauxfactory import gen_url from nailgun import entities from navmazing import NavigationTriesExceeded +import pyotp +import pytest from robottelo.config import settings -from robottelo.constants import ANY_CONTEXT -from robottelo.constants import CERT_PATH -from robottelo.constants import LDAP_ATTR -from robottelo.constants import PERMISSIONS +from robottelo.constants import ANY_CONTEXT, CERT_PATH, LDAP_ATTR, PERMISSIONS from robottelo.utils.datafactory import gen_string - pytestmark = [pytest.mark.run_in_one_thread] EXTERNAL_GROUP_NAME = 'foobargroup' diff --git a/tests/foreman/ui/test_lifecycleenvironment.py b/tests/foreman/ui/test_lifecycleenvironment.py index 396662708c8..5ff44751f6e 100644 --- a/tests/foreman/ui/test_lifecycleenvironment.py +++ b/tests/foreman/ui/test_lifecycleenvironment.py @@ -16,18 +16,20 @@ :Upstream: No """ -import pytest from airgun.session import Session from navmazing import NavigationTriesExceeded +import pytest from robottelo.config import settings -from robottelo.constants import ENVIRONMENT -from robottelo.constants import FAKE_0_CUSTOM_PACKAGE -from robottelo.constants import FAKE_0_CUSTOM_PACKAGE_NAME -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE_NAME -from robottelo.constants import FAKE_2_CUSTOM_PACKAGE -from robottelo.constants import FAKE_3_CUSTOM_PACKAGE_NAME +from robottelo.constants import ( + ENVIRONMENT, + FAKE_0_CUSTOM_PACKAGE, + FAKE_0_CUSTOM_PACKAGE_NAME, + FAKE_1_CUSTOM_PACKAGE, + FAKE_1_CUSTOM_PACKAGE_NAME, + FAKE_2_CUSTOM_PACKAGE, + FAKE_3_CUSTOM_PACKAGE_NAME, +) from robottelo.utils.datafactory import gen_string diff --git a/tests/foreman/ui/test_location.py b/tests/foreman/ui/test_location.py index 06b51bf2341..b86484ff392 100644 --- a/tests/foreman/ui/test_location.py +++ b/tests/foreman/ui/test_location.py @@ -16,15 +16,12 @@ :Upstream: No """ -import pytest -from fauxfactory import gen_ipaddr -from fauxfactory import gen_string +from fauxfactory import gen_ipaddr, gen_string from nailgun import entities +import pytest from robottelo.config import settings -from robottelo.constants import ANY_CONTEXT -from robottelo.constants import INSTALL_MEDIUM_URL -from robottelo.constants import LIBVIRT_RESOURCE_URL +from robottelo.constants import ANY_CONTEXT, INSTALL_MEDIUM_URL, LIBVIRT_RESOURCE_URL @pytest.mark.e2e diff --git a/tests/foreman/ui/test_media.py b/tests/foreman/ui/test_media.py index 8d944c250bb..db78fe4b414 100644 --- a/tests/foreman/ui/test_media.py +++ b/tests/foreman/ui/test_media.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.constants import INSTALL_MEDIUM_URL diff --git a/tests/foreman/ui/test_modulestreams.py b/tests/foreman/ui/test_modulestreams.py index 1bf848a7721..23461437f48 100644 --- a/tests/foreman/ui/test_modulestreams.py +++ b/tests/foreman/ui/test_modulestreams.py @@ -16,9 +16,9 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo.config import settings diff --git a/tests/foreman/ui/test_organization.py b/tests/foreman/ui/test_organization.py index bd57330b3b1..460e2ab38a7 100644 --- a/tests/foreman/ui/test_organization.py +++ b/tests/foreman/ui/test_organization.py @@ -16,14 +16,12 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo.config import settings -from robottelo.constants import DEFAULT_ORG -from robottelo.constants import INSTALL_MEDIUM_URL -from robottelo.constants import LIBVIRT_RESOURCE_URL +from robottelo.constants import DEFAULT_ORG, INSTALL_MEDIUM_URL, LIBVIRT_RESOURCE_URL from robottelo.logging import logger CUSTOM_REPO_ERRATA_ID = settings.repos.yum_0.errata[0] diff --git a/tests/foreman/ui/test_oscapcontent.py b/tests/foreman/ui/test_oscapcontent.py index e87f4984f9f..21e6367de0f 100644 --- a/tests/foreman/ui/test_oscapcontent.py +++ b/tests/foreman/ui/test_oscapcontent.py @@ -20,8 +20,7 @@ import pytest -from robottelo.config import robottelo_tmp_dir -from robottelo.config import settings +from robottelo.config import robottelo_tmp_dir, settings from robottelo.constants import DataFile from robottelo.utils.datafactory import gen_string diff --git a/tests/foreman/ui/test_oscappolicy.py b/tests/foreman/ui/test_oscappolicy.py index f7ef21c6ab4..37196d8e2cf 100644 --- a/tests/foreman/ui/test_oscappolicy.py +++ b/tests/foreman/ui/test_oscappolicy.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from nailgun import entities +import pytest from robottelo.constants import OSCAP_PROFILE from robottelo.utils.datafactory import gen_string diff --git a/tests/foreman/ui/test_oscaptailoringfile.py b/tests/foreman/ui/test_oscaptailoringfile.py index b42ef268f61..011cb9bff00 100644 --- a/tests/foreman/ui/test_oscaptailoringfile.py +++ b/tests/foreman/ui/test_oscaptailoringfile.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from nailgun import entities +import pytest from robottelo.utils.datafactory import gen_string diff --git a/tests/foreman/ui/test_package.py b/tests/foreman/ui/test_package.py index c5896e9ca24..257e571dc76 100644 --- a/tests/foreman/ui/test_package.py +++ b/tests/foreman/ui/test_package.py @@ -16,13 +16,12 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo.config import settings -from robottelo.constants import DataFile -from robottelo.constants import RPM_TO_UPLOAD +from robottelo.constants import RPM_TO_UPLOAD, DataFile @pytest.fixture(scope='module') diff --git a/tests/foreman/ui/test_partitiontable.py b/tests/foreman/ui/test_partitiontable.py index 2b3e2099b15..393a2ca634f 100644 --- a/tests/foreman/ui/test_partitiontable.py +++ b/tests/foreman/ui/test_partitiontable.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.constants import DataFile diff --git a/tests/foreman/ui/test_product.py b/tests/foreman/ui/test_product.py index 2abb16fea74..a70623620f1 100644 --- a/tests/foreman/ui/test_product.py +++ b/tests/foreman/ui/test_product.py @@ -18,18 +18,18 @@ """ from datetime import timedelta -import pytest from fauxfactory import gen_choice from nailgun import entities +import pytest from robottelo.config import settings -from robottelo.constants import DataFile -from robottelo.constants import REPO_TYPE -from robottelo.constants import SYNC_INTERVAL -from robottelo.utils.datafactory import gen_string -from robottelo.utils.datafactory import parametrized -from robottelo.utils.datafactory import valid_cron_expressions -from robottelo.utils.datafactory import valid_data_list +from robottelo.constants import REPO_TYPE, SYNC_INTERVAL, DataFile +from robottelo.utils.datafactory import ( + gen_string, + parametrized, + valid_cron_expressions, + valid_data_list, +) @pytest.fixture(scope='module') diff --git a/tests/foreman/ui/test_puppetclass.py b/tests/foreman/ui/test_puppetclass.py index c8f08292249..5c5fdb98d78 100644 --- a/tests/foreman/ui/test_puppetclass.py +++ b/tests/foreman/ui/test_puppetclass.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest @pytest.mark.tier2 diff --git a/tests/foreman/ui/test_puppetenvironment.py b/tests/foreman/ui/test_puppetenvironment.py index 729eb7b190b..6d11797d1fb 100644 --- a/tests/foreman/ui/test_puppetenvironment.py +++ b/tests/foreman/ui/test_puppetenvironment.py @@ -18,8 +18,7 @@ """ import pytest -from robottelo.constants import DEFAULT_CV -from robottelo.constants import ENVIRONMENT +from robottelo.constants import DEFAULT_CV, ENVIRONMENT from robottelo.utils.datafactory import gen_string diff --git a/tests/foreman/ui/test_reporttemplates.py b/tests/foreman/ui/test_reporttemplates.py index ed4c84515f7..1de38478844 100644 --- a/tests/foreman/ui/test_reporttemplates.py +++ b/tests/foreman/ui/test_reporttemplates.py @@ -19,22 +19,22 @@ import csv import json import os -from pathlib import Path -from pathlib import PurePath +from pathlib import Path, PurePath -import pytest -import yaml from lxml import etree from nailgun import entities +import pytest +import yaml -from robottelo.config import robottelo_tmp_dir -from robottelo.config import settings -from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME -from robottelo.constants import FAKE_0_CUSTOM_PACKAGE_NAME -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE -from robottelo.constants import PRDS -from robottelo.constants import REPOS -from robottelo.constants import REPOSET +from robottelo.config import robottelo_tmp_dir, settings +from robottelo.constants import ( + DEFAULT_SUBSCRIPTION_NAME, + FAKE_0_CUSTOM_PACKAGE_NAME, + FAKE_1_CUSTOM_PACKAGE, + PRDS, + REPOS, + REPOSET, +) from robottelo.utils.datafactory import gen_string diff --git a/tests/foreman/ui/test_repository.py b/tests/foreman/ui/test_repository.py index 37468fa4189..63b7df93577 100644 --- a/tests/foreman/ui/test_repository.py +++ b/tests/foreman/ui/test_repository.py @@ -16,29 +16,31 @@ :Upstream: No """ -from datetime import datetime -from datetime import timedelta -from random import randint -from random import shuffle +from datetime import datetime, timedelta +from random import randint, shuffle -import pytest from airgun.session import Session from nailgun import entities from navmazing import NavigationTriesExceeded +import pytest from robottelo import constants from robottelo.config import settings -from robottelo.constants import CONTAINER_REGISTRY_HUB -from robottelo.constants import DataFile -from robottelo.constants import DOWNLOAD_POLICIES -from robottelo.constants import INVALID_URL -from robottelo.constants import PRDS -from robottelo.constants import REPO_TYPE -from robottelo.constants import REPOS -from robottelo.constants import REPOSET -from robottelo.constants.repos import ANSIBLE_GALAXY -from robottelo.constants.repos import CUSTOM_3RD_PARTY_REPO -from robottelo.constants.repos import CUSTOM_RPM_SHA +from robottelo.constants import ( + CONTAINER_REGISTRY_HUB, + DOWNLOAD_POLICIES, + INVALID_URL, + PRDS, + REPO_TYPE, + REPOS, + REPOSET, + DataFile, +) +from robottelo.constants.repos import ( + ANSIBLE_GALAXY, + CUSTOM_3RD_PARTY_REPO, + CUSTOM_RPM_SHA, +) from robottelo.hosts import get_sat_version from robottelo.utils.datafactory import gen_string diff --git a/tests/foreman/ui/test_rhc.py b/tests/foreman/ui/test_rhc.py index dfcf3890810..6fa17a18589 100644 --- a/tests/foreman/ui/test_rhc.py +++ b/tests/foreman/ui/test_rhc.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from wait_for import wait_for from robottelo import constants diff --git a/tests/foreman/ui/test_rhcloud_insights.py b/tests/foreman/ui/test_rhcloud_insights.py index aa7351767b5..6ba96238d21 100644 --- a/tests/foreman/ui/test_rhcloud_insights.py +++ b/tests/foreman/ui/test_rhcloud_insights.py @@ -22,9 +22,7 @@ from wait_for import wait_for from robottelo.config import settings -from robottelo.constants import DEFAULT_LOC -from robottelo.constants import DNF_RECOMMENDATION -from robottelo.constants import OPENSSH_RECOMMENDATION +from robottelo.constants import DEFAULT_LOC, DNF_RECOMMENDATION, OPENSSH_RECOMMENDATION def create_insights_vulnerability(insights_vm): diff --git a/tests/foreman/ui/test_rhcloud_inventory.py b/tests/foreman/ui/test_rhcloud_inventory.py index 2da1e461141..743ef8925db 100644 --- a/tests/foreman/ui/test_rhcloud_inventory.py +++ b/tests/foreman/ui/test_rhcloud_inventory.py @@ -16,16 +16,17 @@ :Upstream: No """ -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta import pytest from wait_for import wait_for from robottelo.constants import DEFAULT_LOC -from robottelo.utils.io import get_local_file_data -from robottelo.utils.io import get_remote_report_checksum -from robottelo.utils.io import get_report_data +from robottelo.utils.io import ( + get_local_file_data, + get_remote_report_checksum, + get_report_data, +) def common_assertion(report_path, inventory_data, org, satellite): diff --git a/tests/foreman/ui/test_role.py b/tests/foreman/ui/test_role.py index c3418a43588..86b93fd7baa 100644 --- a/tests/foreman/ui/test_role.py +++ b/tests/foreman/ui/test_role.py @@ -18,13 +18,12 @@ """ import random -import pytest from airgun.session import Session from nailgun import entities from navmazing import NavigationTriesExceeded +import pytest -from robottelo.constants import PERMISSIONS_UI -from robottelo.constants import ROLES +from robottelo.constants import PERMISSIONS_UI, ROLES from robottelo.utils.datafactory import gen_string diff --git a/tests/foreman/ui/test_settings.py b/tests/foreman/ui/test_settings.py index de752f1c449..ade4cd9afbe 100644 --- a/tests/foreman/ui/test_settings.py +++ b/tests/foreman/ui/test_settings.py @@ -18,15 +18,14 @@ """ import math -import pytest from airgun.session import Session from fauxfactory import gen_url from nailgun import entities +import pytest from robottelo.cli.user import User from robottelo.config import settings -from robottelo.utils.datafactory import filtered_datapoint -from robottelo.utils.datafactory import gen_string +from robottelo.utils.datafactory import filtered_datapoint, gen_string @filtered_datapoint diff --git a/tests/foreman/ui/test_smartclassparameter.py b/tests/foreman/ui/test_smartclassparameter.py index 9a27ce4f0ef..98c3ba430c3 100644 --- a/tests/foreman/ui/test_smartclassparameter.py +++ b/tests/foreman/ui/test_smartclassparameter.py @@ -16,8 +16,7 @@ :Upstream: No """ -from random import choice -from random import uniform +from random import choice, uniform import pytest import yaml diff --git a/tests/foreman/ui/test_subnet.py b/tests/foreman/ui/test_subnet.py index 814ea6bae10..f188820d4ba 100644 --- a/tests/foreman/ui/test_subnet.py +++ b/tests/foreman/ui/test_subnet.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_ipaddr +import pytest from robottelo.utils.datafactory import gen_string diff --git a/tests/foreman/ui/test_subscription.py b/tests/foreman/ui/test_subscription.py index a56c5796f87..674b91d7401 100644 --- a/tests/foreman/ui/test_subscription.py +++ b/tests/foreman/ui/test_subscription.py @@ -16,22 +16,24 @@ :Upstream: No """ -import time from tempfile import mkstemp +import time -import pytest from airgun.session import Session from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo.cli.factory import make_virt_who_config from robottelo.config import settings -from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME -from robottelo.constants import PRDS -from robottelo.constants import REPOS -from robottelo.constants import REPOSET -from robottelo.constants import VDC_SUBSCRIPTION_NAME -from robottelo.constants import VIRT_WHO_HYPERVISOR_TYPES +from robottelo.constants import ( + DEFAULT_SUBSCRIPTION_NAME, + PRDS, + REPOS, + REPOSET, + VDC_SUBSCRIPTION_NAME, + VIRT_WHO_HYPERVISOR_TYPES, +) from robottelo.utils.manifest import clone pytestmark = [pytest.mark.run_in_one_thread, pytest.mark.skip_if_not_set('fake_manifest')] diff --git a/tests/foreman/ui/test_sync.py b/tests/foreman/ui/test_sync.py index ae5e0cc58bc..30ccce8c95f 100644 --- a/tests/foreman/ui/test_sync.py +++ b/tests/foreman/ui/test_sync.py @@ -16,17 +16,19 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string from nailgun import entities +import pytest from robottelo.config import settings -from robottelo.constants import CONTAINER_REGISTRY_HUB -from robottelo.constants import CONTAINER_UPSTREAM_NAME -from robottelo.constants import PRDS -from robottelo.constants import REPO_TYPE -from robottelo.constants import REPOS -from robottelo.constants import REPOSET +from robottelo.constants import ( + CONTAINER_REGISTRY_HUB, + CONTAINER_UPSTREAM_NAME, + PRDS, + REPO_TYPE, + REPOS, + REPOSET, +) from robottelo.constants.repos import FEDORA_OSTREE_REPO diff --git a/tests/foreman/ui/test_syncplan.py b/tests/foreman/ui/test_syncplan.py index 8262d62def3..10e1221cb06 100644 --- a/tests/foreman/ui/test_syncplan.py +++ b/tests/foreman/ui/test_syncplan.py @@ -16,17 +16,15 @@ :Upstream: No """ +from datetime import datetime, timedelta import time -from datetime import datetime -from datetime import timedelta -import pytest from fauxfactory import gen_choice from nailgun import entities +import pytest from robottelo.constants import SYNC_INTERVAL -from robottelo.utils.datafactory import gen_string -from robottelo.utils.datafactory import valid_cron_expressions +from robottelo.utils.datafactory import gen_string, valid_cron_expressions def validate_repo_content(repo, content_types, after_sync=True): diff --git a/tests/foreman/ui/test_templatesync.py b/tests/foreman/ui/test_templatesync.py index cb91a50df4e..cfbea828a95 100644 --- a/tests/foreman/ui/test_templatesync.py +++ b/tests/foreman/ui/test_templatesync.py @@ -14,14 +14,13 @@ :Upstream: No """ -import pytest -import requests from fauxfactory import gen_string from nailgun import entities +import pytest +import requests from robottelo.config import settings -from robottelo.constants import FOREMAN_TEMPLATE_IMPORT_URL -from robottelo.constants import FOREMAN_TEMPLATE_ROOT_DIR +from robottelo.constants import FOREMAN_TEMPLATE_IMPORT_URL, FOREMAN_TEMPLATE_ROOT_DIR @pytest.fixture(scope='module') diff --git a/tests/foreman/ui/test_user.py b/tests/foreman/ui/test_user.py index d0847a9d0d6..892024ffc8d 100644 --- a/tests/foreman/ui/test_user.py +++ b/tests/foreman/ui/test_user.py @@ -18,14 +18,11 @@ """ import random -import pytest from airgun.session import Session -from fauxfactory import gen_email -from fauxfactory import gen_string +from fauxfactory import gen_email, gen_string +import pytest -from robottelo.constants import DEFAULT_ORG -from robottelo.constants import PERMISSIONS -from robottelo.constants import ROLES +from robottelo.constants import DEFAULT_ORG, PERMISSIONS, ROLES @pytest.mark.e2e diff --git a/tests/foreman/ui/test_usergroup.py b/tests/foreman/ui/test_usergroup.py index f899d4fc099..f5e2eaa9a96 100644 --- a/tests/foreman/ui/test_usergroup.py +++ b/tests/foreman/ui/test_usergroup.py @@ -16,10 +16,9 @@ :Upstream: No """ -import pytest -from fauxfactory import gen_string -from fauxfactory import gen_utf8 +from fauxfactory import gen_string, gen_utf8 from nailgun import entities +import pytest @pytest.mark.tier2 diff --git a/tests/foreman/ui/test_webhook.py b/tests/foreman/ui/test_webhook.py index 32cebf1a4b2..f3df30aef78 100644 --- a/tests/foreman/ui/test_webhook.py +++ b/tests/foreman/ui/test_webhook.py @@ -16,9 +16,8 @@ :Upstream: No """ +from fauxfactory import gen_string, gen_url import pytest -from fauxfactory import gen_string -from fauxfactory import gen_url @pytest.mark.tier1 diff --git a/tests/foreman/virtwho/api/test_esx.py b/tests/foreman/virtwho/api/test_esx.py index 069eefc969a..32a0b69e374 100644 --- a/tests/foreman/virtwho/api/test_esx.py +++ b/tests/foreman/virtwho/api/test_esx.py @@ -16,19 +16,21 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import create_http_proxy -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_command_check -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import ETC_VIRTWHO_CONFIG -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option -from robottelo.utils.virtwho import get_guest_info +from robottelo.utils.virtwho import ( + ETC_VIRTWHO_CONFIG, + create_http_proxy, + deploy_configure_by_command, + deploy_configure_by_command_check, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, + get_guest_info, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/api/test_esx_sca.py b/tests/foreman/virtwho/api/test_esx_sca.py index 2388d0e924e..cd9cc3d3140 100644 --- a/tests/foreman/virtwho/api/test_esx_sca.py +++ b/tests/foreman/virtwho/api/test_esx_sca.py @@ -14,18 +14,20 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import create_http_proxy -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_command_check -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import ETC_VIRTWHO_CONFIG -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + ETC_VIRTWHO_CONFIG, + create_http_proxy, + deploy_configure_by_command, + deploy_configure_by_command_check, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/api/test_hyperv.py b/tests/foreman/virtwho/api/test_hyperv.py index 084975d00a3..ec7c37ae6e7 100644 --- a/tests/foreman/virtwho/api/test_hyperv.py +++ b/tests/foreman/virtwho/api/test_hyperv.py @@ -16,15 +16,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/api/test_hyperv_sca.py b/tests/foreman/virtwho/api/test_hyperv_sca.py index 5788ab68e13..67f18934c8d 100644 --- a/tests/foreman/virtwho/api/test_hyperv_sca.py +++ b/tests/foreman/virtwho/api/test_hyperv_sca.py @@ -16,15 +16,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/api/test_kubevirt.py b/tests/foreman/virtwho/api/test_kubevirt.py index 0ff6ade433d..e2fbe884f9a 100644 --- a/tests/foreman/virtwho/api/test_kubevirt.py +++ b/tests/foreman/virtwho/api/test_kubevirt.py @@ -16,16 +16,18 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option -from robottelo.utils.virtwho import get_guest_info +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, + get_guest_info, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/api/test_kubevirt_sca.py b/tests/foreman/virtwho/api/test_kubevirt_sca.py index 3dfbd125ebf..f64b719e9c4 100644 --- a/tests/foreman/virtwho/api/test_kubevirt_sca.py +++ b/tests/foreman/virtwho/api/test_kubevirt_sca.py @@ -14,15 +14,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/api/test_libvirt.py b/tests/foreman/virtwho/api/test_libvirt.py index b1f105938e5..eb0806e5753 100644 --- a/tests/foreman/virtwho/api/test_libvirt.py +++ b/tests/foreman/virtwho/api/test_libvirt.py @@ -16,15 +16,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/api/test_libvirt_sca.py b/tests/foreman/virtwho/api/test_libvirt_sca.py index afebdf01eb9..bead5b8e95f 100644 --- a/tests/foreman/virtwho/api/test_libvirt_sca.py +++ b/tests/foreman/virtwho/api/test_libvirt_sca.py @@ -14,15 +14,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/api/test_nutanix.py b/tests/foreman/virtwho/api/test_nutanix.py index 147c6246d1e..5e4d4c51f34 100644 --- a/tests/foreman/virtwho/api/test_nutanix.py +++ b/tests/foreman/virtwho/api/test_nutanix.py @@ -16,18 +16,20 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import check_message_in_rhsm_log -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option -from robottelo.utils.virtwho import get_guest_info -from robottelo.utils.virtwho import get_hypervisor_ahv_mapping +from robottelo.utils.virtwho import ( + check_message_in_rhsm_log, + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, + get_guest_info, + get_hypervisor_ahv_mapping, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/api/test_nutanix_sca.py b/tests/foreman/virtwho/api/test_nutanix_sca.py index b57b0eefb8c..05287279231 100644 --- a/tests/foreman/virtwho/api/test_nutanix_sca.py +++ b/tests/foreman/virtwho/api/test_nutanix_sca.py @@ -16,15 +16,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/cli/test_esx.py b/tests/foreman/virtwho/cli/test_esx.py index 613f69acf0f..fe2b1827bf6 100644 --- a/tests/foreman/virtwho/cli/test_esx.py +++ b/tests/foreman/virtwho/cli/test_esx.py @@ -18,22 +18,24 @@ """ import re +from fauxfactory import gen_string import pytest import requests -from fauxfactory import gen_string from robottelo.cli.user import User from robottelo.config import settings -from robottelo.utils.virtwho import create_http_proxy -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_command_check -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import ETC_VIRTWHO_CONFIG -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option -from robottelo.utils.virtwho import hypervisor_json_create -from robottelo.utils.virtwho import virtwho_package_locked +from robottelo.utils.virtwho import ( + ETC_VIRTWHO_CONFIG, + create_http_proxy, + deploy_configure_by_command, + deploy_configure_by_command_check, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, + hypervisor_json_create, + virtwho_package_locked, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/cli/test_esx_sca.py b/tests/foreman/virtwho/cli/test_esx_sca.py index c82520b30fc..df15cce8105 100644 --- a/tests/foreman/virtwho/cli/test_esx_sca.py +++ b/tests/foreman/virtwho/cli/test_esx_sca.py @@ -16,22 +16,24 @@ """ import re +from fauxfactory import gen_string import pytest import requests -from fauxfactory import gen_string from robottelo.cli.user import User from robottelo.config import settings -from robottelo.utils.virtwho import create_http_proxy -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_command_check -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import ETC_VIRTWHO_CONFIG -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option -from robottelo.utils.virtwho import hypervisor_json_create -from robottelo.utils.virtwho import virtwho_package_locked +from robottelo.utils.virtwho import ( + ETC_VIRTWHO_CONFIG, + create_http_proxy, + deploy_configure_by_command, + deploy_configure_by_command_check, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, + hypervisor_json_create, + virtwho_package_locked, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/cli/test_hyperv.py b/tests/foreman/virtwho/cli/test_hyperv.py index 6ccb39c6bd2..1fa31db9f00 100644 --- a/tests/foreman/virtwho/cli/test_hyperv.py +++ b/tests/foreman/virtwho/cli/test_hyperv.py @@ -16,15 +16,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/cli/test_hyperv_sca.py b/tests/foreman/virtwho/cli/test_hyperv_sca.py index 715ec1eafa1..e3909489e21 100644 --- a/tests/foreman/virtwho/cli/test_hyperv_sca.py +++ b/tests/foreman/virtwho/cli/test_hyperv_sca.py @@ -16,15 +16,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/cli/test_kubevirt.py b/tests/foreman/virtwho/cli/test_kubevirt.py index 32d1a75416b..c6d38de60dd 100644 --- a/tests/foreman/virtwho/cli/test_kubevirt.py +++ b/tests/foreman/virtwho/cli/test_kubevirt.py @@ -16,15 +16,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/cli/test_kubevirt_sca.py b/tests/foreman/virtwho/cli/test_kubevirt_sca.py index 4c86aa21f51..99e4335c9f6 100644 --- a/tests/foreman/virtwho/cli/test_kubevirt_sca.py +++ b/tests/foreman/virtwho/cli/test_kubevirt_sca.py @@ -14,15 +14,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/cli/test_libvirt.py b/tests/foreman/virtwho/cli/test_libvirt.py index 59508ba74a7..1f5b034b473 100644 --- a/tests/foreman/virtwho/cli/test_libvirt.py +++ b/tests/foreman/virtwho/cli/test_libvirt.py @@ -16,15 +16,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/cli/test_libvirt_sca.py b/tests/foreman/virtwho/cli/test_libvirt_sca.py index 02550772ca6..b29ffaf667f 100644 --- a/tests/foreman/virtwho/cli/test_libvirt_sca.py +++ b/tests/foreman/virtwho/cli/test_libvirt_sca.py @@ -14,15 +14,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/cli/test_nutanix.py b/tests/foreman/virtwho/cli/test_nutanix.py index 10599c55c00..9b9437d0386 100644 --- a/tests/foreman/virtwho/cli/test_nutanix.py +++ b/tests/foreman/virtwho/cli/test_nutanix.py @@ -16,17 +16,19 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import check_message_in_rhsm_log -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option -from robottelo.utils.virtwho import get_hypervisor_ahv_mapping +from robottelo.utils.virtwho import ( + check_message_in_rhsm_log, + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, + get_hypervisor_ahv_mapping, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/cli/test_nutanix_sca.py b/tests/foreman/virtwho/cli/test_nutanix_sca.py index b18a82cd670..59ba58f6f60 100644 --- a/tests/foreman/virtwho/cli/test_nutanix_sca.py +++ b/tests/foreman/virtwho/cli/test_nutanix_sca.py @@ -16,15 +16,17 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/conftest.py b/tests/foreman/virtwho/conftest.py index 81709b551ae..0bd09647bf6 100644 --- a/tests/foreman/virtwho/conftest.py +++ b/tests/foreman/virtwho/conftest.py @@ -1,6 +1,6 @@ -import pytest from airgun.session import Session from fauxfactory import gen_string +import pytest from requests.exceptions import HTTPError from robottelo.logging import logger diff --git a/tests/foreman/virtwho/ui/test_esx.py b/tests/foreman/virtwho/ui/test_esx.py index c88897294d4..f38590b8ddf 100644 --- a/tests/foreman/virtwho/ui/test_esx.py +++ b/tests/foreman/virtwho/ui/test_esx.py @@ -18,27 +18,29 @@ """ from datetime import datetime -import pytest from airgun.session import Session from fauxfactory import gen_string +import pytest from robottelo.config import settings from robottelo.utils.datafactory import valid_emails_list -from robottelo.utils.virtwho import add_configure_option -from robottelo.utils.virtwho import create_http_proxy -from robottelo.utils.virtwho import delete_configure_option -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_command_check -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import ETC_VIRTWHO_CONFIG -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_id -from robottelo.utils.virtwho import get_configure_option -from robottelo.utils.virtwho import get_guest_info -from robottelo.utils.virtwho import get_virtwho_status -from robottelo.utils.virtwho import restart_virtwho_service -from robottelo.utils.virtwho import update_configure_option +from robottelo.utils.virtwho import ( + ETC_VIRTWHO_CONFIG, + add_configure_option, + create_http_proxy, + delete_configure_option, + deploy_configure_by_command, + deploy_configure_by_command_check, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_id, + get_configure_option, + get_guest_info, + get_virtwho_status, + restart_virtwho_service, + update_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/ui/test_esx_sca.py b/tests/foreman/virtwho/ui/test_esx_sca.py index d662bf0b807..6abda09c6e4 100644 --- a/tests/foreman/virtwho/ui/test_esx_sca.py +++ b/tests/foreman/virtwho/ui/test_esx_sca.py @@ -16,26 +16,28 @@ """ from datetime import datetime -import pytest from airgun.session import Session from fauxfactory import gen_string +import pytest from robottelo.config import settings from robottelo.utils.datafactory import valid_emails_list -from robottelo.utils.virtwho import add_configure_option -from robottelo.utils.virtwho import delete_configure_option -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_command_check -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import ETC_VIRTWHO_CONFIG -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_id -from robottelo.utils.virtwho import get_configure_option -from robottelo.utils.virtwho import get_guest_info -from robottelo.utils.virtwho import get_virtwho_status -from robottelo.utils.virtwho import restart_virtwho_service -from robottelo.utils.virtwho import update_configure_option +from robottelo.utils.virtwho import ( + ETC_VIRTWHO_CONFIG, + add_configure_option, + delete_configure_option, + deploy_configure_by_command, + deploy_configure_by_command_check, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_id, + get_configure_option, + get_guest_info, + get_virtwho_status, + restart_virtwho_service, + update_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/ui/test_hyperv.py b/tests/foreman/virtwho/ui/test_hyperv.py index e6dafffd15a..56c2bbdb476 100644 --- a/tests/foreman/virtwho/ui/test_hyperv.py +++ b/tests/foreman/virtwho/ui/test_hyperv.py @@ -16,16 +16,18 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_id -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_id, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/ui/test_hyperv_sca.py b/tests/foreman/virtwho/ui/test_hyperv_sca.py index 140142bb419..d58d2c15ebd 100644 --- a/tests/foreman/virtwho/ui/test_hyperv_sca.py +++ b/tests/foreman/virtwho/ui/test_hyperv_sca.py @@ -14,16 +14,18 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_id -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_id, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/ui/test_kubevirt.py b/tests/foreman/virtwho/ui/test_kubevirt.py index 20c5e6170e3..289e4f385f1 100644 --- a/tests/foreman/virtwho/ui/test_kubevirt.py +++ b/tests/foreman/virtwho/ui/test_kubevirt.py @@ -16,16 +16,18 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_id -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_id, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/ui/test_kubevirt_sca.py b/tests/foreman/virtwho/ui/test_kubevirt_sca.py index 569c5f7caca..c4b893947e7 100644 --- a/tests/foreman/virtwho/ui/test_kubevirt_sca.py +++ b/tests/foreman/virtwho/ui/test_kubevirt_sca.py @@ -14,16 +14,18 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_id -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_id, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/ui/test_libvirt.py b/tests/foreman/virtwho/ui/test_libvirt.py index cd90836beb4..6c6b37fdc30 100644 --- a/tests/foreman/virtwho/ui/test_libvirt.py +++ b/tests/foreman/virtwho/ui/test_libvirt.py @@ -16,16 +16,18 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_id -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_id, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/ui/test_libvirt_sca.py b/tests/foreman/virtwho/ui/test_libvirt_sca.py index 56c156a8380..415ff38a37b 100644 --- a/tests/foreman/virtwho/ui/test_libvirt_sca.py +++ b/tests/foreman/virtwho/ui/test_libvirt_sca.py @@ -14,16 +14,18 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_id -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_id, + get_configure_option, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/ui/test_nutanix.py b/tests/foreman/virtwho/ui/test_nutanix.py index db04c6b6eea..d2a77961918 100644 --- a/tests/foreman/virtwho/ui/test_nutanix.py +++ b/tests/foreman/virtwho/ui/test_nutanix.py @@ -16,18 +16,20 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import check_message_in_rhsm_log -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_id -from robottelo.utils.virtwho import get_configure_option -from robottelo.utils.virtwho import get_hypervisor_ahv_mapping +from robottelo.utils.virtwho import ( + check_message_in_rhsm_log, + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_id, + get_configure_option, + get_hypervisor_ahv_mapping, +) @pytest.fixture() diff --git a/tests/foreman/virtwho/ui/test_nutanix_sca.py b/tests/foreman/virtwho/ui/test_nutanix_sca.py index 7dd104a38ec..3b53d038d71 100644 --- a/tests/foreman/virtwho/ui/test_nutanix_sca.py +++ b/tests/foreman/virtwho/ui/test_nutanix_sca.py @@ -14,18 +14,20 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.config import settings -from robottelo.utils.virtwho import check_message_in_rhsm_log -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import deploy_configure_by_script -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_id -from robottelo.utils.virtwho import get_configure_option -from robottelo.utils.virtwho import get_hypervisor_ahv_mapping +from robottelo.utils.virtwho import ( + check_message_in_rhsm_log, + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_configure_file, + get_configure_id, + get_configure_option, + get_hypervisor_ahv_mapping, +) @pytest.fixture() diff --git a/tests/robottelo/conftest.py b/tests/robottelo/conftest.py index c093c969d12..df6419a4f24 100644 --- a/tests/robottelo/conftest.py +++ b/tests/robottelo/conftest.py @@ -3,8 +3,8 @@ from pathlib import Path from tempfile import NamedTemporaryFile -import pytest from fauxfactory import gen_string +import pytest @pytest.fixture(scope='session', autouse=True) diff --git a/tests/robottelo/test_cli.py b/tests/robottelo/test_cli.py index 5c2ad05baa4..78b0f6f0cf8 100644 --- a/tests/robottelo/test_cli.py +++ b/tests/robottelo/test_cli.py @@ -1,14 +1,16 @@ -import unittest from functools import partial +import unittest from unittest import mock import pytest -from robottelo.cli.base import Base -from robottelo.cli.base import CLIBaseError -from robottelo.cli.base import CLIDataBaseError -from robottelo.cli.base import CLIError -from robottelo.cli.base import CLIReturnCodeError +from robottelo.cli.base import ( + Base, + CLIBaseError, + CLIDataBaseError, + CLIError, + CLIReturnCodeError, +) class CLIClass(Base): diff --git a/tests/robottelo/test_dependencies.py b/tests/robottelo/test_dependencies.py index 71dbee35466..185f0866737 100644 --- a/tests/robottelo/test_dependencies.py +++ b/tests/robottelo/test_dependencies.py @@ -3,10 +3,8 @@ def test_cryptography(): from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import hashes - from cryptography.hazmat.primitives import serialization - from cryptography.hazmat.primitives.asymmetric import padding - from cryptography.hazmat.primitives.asymmetric import rsa + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import padding, rsa fake_key = rsa.generate_private_key( public_exponent=65537, key_size=2048, backend=default_backend() @@ -111,9 +109,7 @@ def test_requests(): def test_tenacity(): - from tenacity import retry - from tenacity import stop_after_attempt - from tenacity import wait_fixed + from tenacity import retry, stop_after_attempt, wait_fixed @retry(stop=stop_after_attempt(3), wait=wait_fixed(1)) def test(): diff --git a/tests/robottelo/test_func_locker.py b/tests/robottelo/test_func_locker.py index e59f8b913e8..ed010e56076 100644 --- a/tests/robottelo/test_func_locker.py +++ b/tests/robottelo/test_func_locker.py @@ -1,8 +1,8 @@ import multiprocessing import os +from pathlib import Path import tempfile import time -from pathlib import Path import pytest diff --git a/tests/robottelo/test_func_shared.py b/tests/robottelo/test_func_shared.py index 871a4a01298..8ab9fb2f06d 100644 --- a/tests/robottelo/test_func_shared.py +++ b/tests/robottelo/test_func_shared.py @@ -2,19 +2,22 @@ import os import time +from fauxfactory import gen_integer, gen_string import pytest -from fauxfactory import gen_integer -from fauxfactory import gen_string - -from robottelo.utils.decorators.func_shared.file_storage import get_temp_dir -from robottelo.utils.decorators.func_shared.file_storage import TEMP_FUNC_SHARED_DIR -from robottelo.utils.decorators.func_shared.file_storage import TEMP_ROOT_DIR -from robottelo.utils.decorators.func_shared.shared import _NAMESPACE_SCOPE_KEY_TYPE -from robottelo.utils.decorators.func_shared.shared import _set_configured -from robottelo.utils.decorators.func_shared.shared import enable_shared_function -from robottelo.utils.decorators.func_shared.shared import set_default_scope -from robottelo.utils.decorators.func_shared.shared import shared -from robottelo.utils.decorators.func_shared.shared import SharedFunctionException + +from robottelo.utils.decorators.func_shared.file_storage import ( + TEMP_FUNC_SHARED_DIR, + TEMP_ROOT_DIR, + get_temp_dir, +) +from robottelo.utils.decorators.func_shared.shared import ( + _NAMESPACE_SCOPE_KEY_TYPE, + SharedFunctionException, + _set_configured, + enable_shared_function, + set_default_scope, + shared, +) DEFAULT_POOL_SIZE = 8 SIMPLE_TIMEOUT_VALUE = 3 diff --git a/tests/robottelo/test_helpers.py b/tests/robottelo/test_helpers.py index 8fa11e58836..0b39bb54b9e 100644 --- a/tests/robottelo/test_helpers.py +++ b/tests/robottelo/test_helpers.py @@ -1,8 +1,7 @@ """Tests for module ``robottelo.helpers``.""" import pytest -from robottelo.utils import slugify_component -from robottelo.utils import validate_ssh_pub_key +from robottelo.utils import slugify_component, validate_ssh_pub_key class FakeSSHResult: diff --git a/tests/robottelo/test_issue_handlers.py b/tests/robottelo/test_issue_handlers.py index bcdf0e58fba..090e031b6f7 100644 --- a/tests/robottelo/test_issue_handlers.py +++ b/tests/robottelo/test_issue_handlers.py @@ -1,18 +1,14 @@ +from collections import defaultdict import os import subprocess import sys -from collections import defaultdict -import pytest from packaging.version import Version +import pytest from pytest_plugins.issue_handlers import DEFAULT_BZ_CACHE_FILE -from robottelo.constants import CLOSED_STATUSES -from robottelo.constants import OPEN_STATUSES -from robottelo.constants import WONTFIX_RESOLUTIONS -from robottelo.utils.issue_handlers import add_workaround -from robottelo.utils.issue_handlers import is_open -from robottelo.utils.issue_handlers import should_deselect +from robottelo.constants import CLOSED_STATUSES, OPEN_STATUSES, WONTFIX_RESOLUTIONS +from robottelo.utils.issue_handlers import add_workaround, is_open, should_deselect class TestBugzillaIssueHandler: diff --git a/tests/upgrades/conftest.py b/tests/upgrades/conftest.py index 25d3c7d57a1..cfa87f2bd95 100644 --- a/tests/upgrades/conftest.py +++ b/tests/upgrades/conftest.py @@ -86,8 +86,8 @@ def test_capsule_post_upgrade_skipped(pre_upgrade_data): import json import os -import pytest from box import Box +import pytest from robottelo.logging import logger from robottelo.utils.decorators.func_locker import lock_function diff --git a/tests/upgrades/test_client.py b/tests/upgrades/test_client.py index ed2069af5e3..8877550a29f 100644 --- a/tests/upgrades/test_client.py +++ b/tests/upgrades/test_client.py @@ -21,8 +21,7 @@ """ import pytest -from robottelo.constants import FAKE_0_CUSTOM_PACKAGE_NAME -from robottelo.constants import FAKE_4_CUSTOM_PACKAGE_NAME +from robottelo.constants import FAKE_0_CUSTOM_PACKAGE_NAME, FAKE_4_CUSTOM_PACKAGE_NAME from robottelo.hosts import ContentHost diff --git a/tests/upgrades/test_contentview.py b/tests/upgrades/test_contentview.py index 68e1b5918b0..2f50ee5db3d 100644 --- a/tests/upgrades/test_contentview.py +++ b/tests/upgrades/test_contentview.py @@ -16,12 +16,11 @@ :Upstream: No """ -import pytest from fauxfactory import gen_alpha +import pytest from robottelo.config import settings -from robottelo.constants import DataFile -from robottelo.constants import RPM_TO_UPLOAD +from robottelo.constants import RPM_TO_UPLOAD, DataFile class TestContentView: diff --git a/tests/upgrades/test_host.py b/tests/upgrades/test_host.py index a3a50fa334f..72a4465eea9 100644 --- a/tests/upgrades/test_host.py +++ b/tests/upgrades/test_host.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest class TestScenarioPositiveGCEHostComputeResource: diff --git a/tests/upgrades/test_hostgroup.py b/tests/upgrades/test_hostgroup.py index dc6c4d8d91b..c09de90e186 100644 --- a/tests/upgrades/test_hostgroup.py +++ b/tests/upgrades/test_hostgroup.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest class TestHostgroup: diff --git a/tests/upgrades/test_repository.py b/tests/upgrades/test_repository.py index a2f5188669c..2119fa57816 100644 --- a/tests/upgrades/test_repository.py +++ b/tests/upgrades/test_repository.py @@ -19,8 +19,7 @@ import pytest from robottelo.config import settings -from robottelo.constants import FAKE_0_CUSTOM_PACKAGE_NAME -from robottelo.constants import FAKE_4_CUSTOM_PACKAGE_NAME +from robottelo.constants import FAKE_0_CUSTOM_PACKAGE_NAME, FAKE_4_CUSTOM_PACKAGE_NAME from robottelo.hosts import ContentHost UPSTREAM_USERNAME = 'rTtest123' diff --git a/tests/upgrades/test_subscription.py b/tests/upgrades/test_subscription.py index a8345ce1859..f72b5b13948 100644 --- a/tests/upgrades/test_subscription.py +++ b/tests/upgrades/test_subscription.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from manifester import Manifester +import pytest from robottelo import constants from robottelo.config import settings diff --git a/tests/upgrades/test_syncplan.py b/tests/upgrades/test_syncplan.py index b4310a0a871..63fdb7ee164 100644 --- a/tests/upgrades/test_syncplan.py +++ b/tests/upgrades/test_syncplan.py @@ -16,8 +16,8 @@ :Upstream: No """ -import pytest from fauxfactory import gen_choice +import pytest from robottelo.constants import SYNC_INTERVAL from robottelo.utils.datafactory import valid_cron_expressions diff --git a/tests/upgrades/test_usergroup.py b/tests/upgrades/test_usergroup.py index c18b8904aea..838a074c363 100644 --- a/tests/upgrades/test_usergroup.py +++ b/tests/upgrades/test_usergroup.py @@ -16,11 +16,10 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest -from robottelo.constants import LDAP_ATTR -from robottelo.constants import LDAP_SERVER_TYPE +from robottelo.constants import LDAP_ATTR, LDAP_SERVER_TYPE class TestUserGroupMembership: diff --git a/tests/upgrades/test_virtwho.py b/tests/upgrades/test_virtwho.py index 4b2c30b2934..826e4e1b059 100644 --- a/tests/upgrades/test_virtwho.py +++ b/tests/upgrades/test_virtwho.py @@ -16,18 +16,20 @@ :Upstream: No """ -import pytest from fauxfactory import gen_string +import pytest from robottelo.cli.host import Host from robottelo.cli.subscription import Subscription from robottelo.cli.virt_who_config import VirtWhoConfig from robottelo.config import settings from robottelo.utils.issue_handlers import is_open -from robottelo.utils.virtwho import deploy_configure_by_command -from robottelo.utils.virtwho import get_configure_command -from robottelo.utils.virtwho import get_configure_file -from robottelo.utils.virtwho import get_configure_option +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + get_configure_command, + get_configure_file, + get_configure_option, +) @pytest.fixture From 1df97370ff6d049928de36844302f0cf42bd8a22 Mon Sep 17 00:00:00 2001 From: Jitendra Yejare Date: Mon, 25 Sep 2023 13:41:40 +0530 Subject: [PATCH 242/586] [6.14.z] Fix dependecies PR automerge in zStream Branches (#12707) Fix dependecies PR automerge in zStream Branche --- .github/workflows/auto_cherry_pick.yml | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/.github/workflows/auto_cherry_pick.yml b/.github/workflows/auto_cherry_pick.yml index 1a1f814a736..f36ff0119e0 100644 --- a/.github/workflows/auto_cherry_pick.yml +++ b/.github/workflows/auto_cherry_pick.yml @@ -10,6 +10,7 @@ env: assignee: ${{ github.event.pull_request.assignee.login }} title: ${{ github.event.pull_request.title }} number: ${{ github.event.number }} + is_dependabot_pr: '' jobs: @@ -45,6 +46,12 @@ jobs: with: fetch-depth: 0 + ## Set env var for dependencies label PR + - name: Set env var is_dependabot_pr to `dependencies` to set the label + if: contains(github.event.pull_request.labels.*.name, 'dependencies') + run: | + echo "is_dependabot_pr=dependencies" >> $GITHUB_ENV + ## CherryPicking and AutoMerging - name: Cherrypicking to zStream branch id: cherrypick @@ -57,19 +64,7 @@ jobs: Auto_Cherry_Picked ${{ matrix.label }} No-CherryPick - assignees: ${{ env.assignee }} - - - name: Add dependencies label, if merged pr author is dependabot[bot] - id: dependencies - if: | - contains(github.event.pull_request.labels.*.name, 'dependencies') && - github.event.pull_request.user.login == 'dependabot[bot]' - uses: jyejare/github-cherry-pick-action@main - with: - token: ${{ secrets.CHERRYPICK_PAT }} - branch: ${{ matrix.label }} - labels: | - dependencies + ${{ env.is_dependabot_pr }} assignees: ${{ env.assignee }} - name: Add Parent PR's PRT comment to Auto_Cherry_Picked PR's From 930719a56f25e4789bf75a6029d7efaec9c8a85c Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 25 Sep 2023 04:15:36 -0400 Subject: [PATCH 243/586] [6.14.z] Bump productmd from 1.36 to 1.37 (#12715) Bump productmd from 1.36 to 1.37 (#12714) (cherry picked from commit 427ac6ff6032efe78347315739f9fc54719bd2d7) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e4924d8b2ea..1c3910cb535 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ fauxfactory==3.1.0 jinja2==3.1.2 manifester==0.0.14 navmazing==1.1.6 -productmd==1.36 +productmd==1.37 pyotp==2.9.0 python-box==7.1.1 pytest==7.4.2 From 635d637733ee507ff72e7fc2d8825a7c944389ae Mon Sep 17 00:00:00 2001 From: jyejare Date: Tue, 5 Sep 2023 16:03:42 +0530 Subject: [PATCH 244/586] Fix Attempt: ZStream Dependency Auto Merging --- .github/workflows/dependency_merge.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/dependency_merge.yml b/.github/workflows/dependency_merge.yml index ebe22b3ac74..9bd40e645a1 100644 --- a/.github/workflows/dependency_merge.yml +++ b/.github/workflows/dependency_merge.yml @@ -1,16 +1,15 @@ -name: Dependabot Auto Merge -on: pull_request - -permissions: - pull-requests: write +name: Dependabot Auto Merge - ZStream +on: + pull_request: + branches-ignore: + - master jobs: dependabot: name: dependabot-auto-merge runs-on: ubuntu-latest if: | - github.event.pull_request.user.login == 'Satellite-QE' && - contains( github.event.pull_request.labels.*.name, 'dependencies') + contains(github.event.pull_request.labels.*.name, 'dependencies') steps: - id: find-prt-comment From fad1762e9abbe7130dc0cf9fc5917c796f8955f8 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 26 Sep 2023 02:47:27 -0400 Subject: [PATCH 245/586] [6.14.z] Bump pytest-reportportal from 5.2.1 to 5.2.2 (#12735) Bump pytest-reportportal from 5.2.1 to 5.2.2 (#12734) (cherry picked from commit 671a69ec061dba0b55d21631122c4817f5ef098c) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1c3910cb535..d5a6db921a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ python-box==7.1.1 pytest==7.4.2 pytest-services==2.2.1 pytest-mock==3.11.1 -pytest-reportportal==5.2.1 +pytest-reportportal==5.2.2 pytest-xdist==3.3.1 pytest-ibutsu==2.2.4 PyYAML==6.0.1 From 8659f09071043839e9bbba5250372b9a3f11e0e8 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 26 Sep 2023 06:05:45 -0400 Subject: [PATCH 246/586] [6.14.z] ISS refactor - batch 2 (#12666) ISS refactor - batch 2 Second batch of changes coming from the ISS evaluation: - enable_rhel_repo helper replaced by parametrized fixture - added other fixtures for importing orgs - addressed updates as per PX comment - merged three cases to export/import CV using parametrization - moved to SCA org as default (cherry picked from commit a1c51f1a42bd802a63d758bb517f6c352f5b54d2) Co-authored-by: Vladimir Sedmik Co-authored-by: vsedmik <46570670+vsedmik@users.noreply.github.com> --- robottelo/constants/__init__.py | 1 + tests/foreman/cli/test_satellitesync.py | 926 ++++++++++-------------- 2 files changed, 374 insertions(+), 553 deletions(-) diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index c323d81f1b2..f8ff046e03f 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -857,6 +857,7 @@ class Colored(Box): PULP_EXPORT_DIR = '/var/lib/pulp/exports/' PULP_IMPORT_DIR = '/var/lib/pulp/imports/' +EXPORT_LIBRARY_NAME = 'Export-Library' PUPPET_COMMON_INSTALLER_OPTS = { 'foreman-proxy-puppetca': 'true', diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index e6a0c37d91d..66718f6e49a 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -17,6 +17,7 @@ :Upstream: No """ from fauxfactory import gen_string +from manifester import Manifester import pytest from robottelo.cli.base import CLIReturnCodeError @@ -25,12 +26,10 @@ from robottelo.cli.contentview import ContentView from robottelo.cli.factory import ( make_content_view, - make_lifecycle_environment, make_org, make_product, make_repository, ) -from robottelo.cli.file import File from robottelo.cli.package import Package from robottelo.cli.product import Product from robottelo.cli.repository import Repository @@ -40,6 +39,7 @@ CONTAINER_REGISTRY_HUB, DEFAULT_ARCHITECTURE, DEFAULT_CV, + EXPORT_LIBRARY_NAME, PULP_EXPORT_DIR, PULP_IMPORT_DIR, REPO_TYPE, @@ -81,6 +81,21 @@ def export_import_cleanup_module(target_sat, module_org): ) +@pytest.fixture(scope='function') +def function_import_org(target_sat): + """Creates an Organization for content import.""" + org = target_sat.api.Organization().create() + yield org + + +@pytest.fixture(scope='function') +def function_import_org_with_manifest(target_sat, function_import_org): + """Creates and sets an Organization with a brand-new manifest for content import.""" + with Manifester(manifest_category=settings.manifest.golden_ticket) as manifest: + target_sat.upload_manifest(function_import_org.id, manifest) + yield function_import_org + + @pytest.fixture(scope='class') def docker_repo(module_target_sat, module_org): product = make_product({'organization-id': module_org.id}) @@ -99,7 +114,7 @@ def docker_repo(module_target_sat, module_org): @pytest.fixture(scope='module') -def module_synced_repo(module_target_sat, module_org, module_product): +def module_synced_custom_repo(module_target_sat, module_org, module_product): repo = module_target_sat.cli_factory.make_repository( { 'content-type': 'yum', @@ -113,7 +128,7 @@ def module_synced_repo(module_target_sat, module_org, module_product): @pytest.fixture(scope='function') -def function_synced_repo(target_sat, function_org, function_product): +def function_synced_custom_repo(target_sat, function_org, function_product): repo = target_sat.cli_factory.make_repository( { 'content-type': 'yum', @@ -126,13 +141,50 @@ def function_synced_repo(target_sat, function_org, function_product): yield repo +@pytest.fixture(scope='function') +def function_synced_rhel_repo(request, target_sat, function_sca_manifest_org): + """Enable and synchronize rhel content with immediate policy""" + repo_dict = ( + REPOS['kickstart'][request.param.replace('kickstart', '')[1:]] + if 'kickstart' in request.param + else REPOS[request.param] + ) + target_sat.cli.RepositorySet.enable( + { + 'organization-id': function_sca_manifest_org.id, + 'name': repo_dict['reposet'], + 'product': repo_dict['product'], + 'releasever': repo_dict.get('releasever', None) or repo_dict.get('version', None), + 'basearch': DEFAULT_ARCHITECTURE, + } + ) + repo = target_sat.cli.Repository.info( + { + 'organization-id': function_sca_manifest_org.id, + 'name': repo_dict['name'], + 'product': repo_dict['product'], + } + ) + # Update the download policy to 'immediate' and sync + target_sat.cli.Repository.update({'download-policy': 'immediate', 'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}, timeout=7200000) + repo = target_sat.cli.Repository.info( + { + 'organization-id': function_sca_manifest_org.id, + 'name': repo_dict['name'], + 'product': repo_dict['product'], + } + ) + return repo + + @pytest.mark.run_in_one_thread class TestRepositoryExport: """Tests for exporting a repository via CLI""" @pytest.mark.tier3 def test_positive_export_version_custom_repo( - self, target_sat, export_import_cleanup_module, module_org, module_synced_repo + self, target_sat, export_import_cleanup_module, module_org, module_synced_custom_repo ): """Export custom repo via complete and incremental CV version export. @@ -166,7 +218,7 @@ def test_positive_export_version_custom_repo( { 'id': cv['id'], 'organization-id': module_org.id, - 'repository-id': module_synced_repo['id'], + 'repository-id': module_synced_custom_repo['id'], } ) target_sat.cli.ContentView.publish({'id': cv['id']}) @@ -192,7 +244,7 @@ def test_positive_export_version_custom_repo( @pytest.mark.tier3 def test_positive_export_library_custom_repo( - self, target_sat, export_import_cleanup_function, function_org, function_synced_repo + self, target_sat, export_import_cleanup_function, function_org, function_synced_custom_repo ): """Export custom repo via complete and incremental library export. @@ -221,7 +273,7 @@ def test_positive_export_library_custom_repo( { 'id': cv['id'], 'organization-id': function_org.id, - 'repository-id': function_synced_repo['id'], + 'repository-id': function_synced_custom_repo['id'], } ) target_sat.cli.ContentView.publish({'id': cv['id']}) @@ -236,100 +288,53 @@ def test_positive_export_library_custom_repo( @pytest.mark.tier3 @pytest.mark.upgrade - def test_positive_export_complete_version_rh_repo( - self, target_sat, export_import_cleanup_module, module_entitlement_manifest_org - ): - """Export RedHat repo via complete version - - :id: e17898db-ca92-4121-a723-0d4b3cf120eb - - :expectedresults: Repository was successfully exported, exported files are - present on satellite machine - - :CaseLevel: System - """ - # Enable and sync RH repository - repo = _enable_rhel_content( - sat=target_sat, - org=module_entitlement_manifest_org, - repo_dict=REPOS['rhae2'], - ) - # Create cv and publish - cv_name = gen_string('alpha') - cv = make_content_view( - {'name': cv_name, 'organization-id': module_entitlement_manifest_org.id} - ) - ContentView.add_repository( - { - 'id': cv['id'], - 'organization-id': module_entitlement_manifest_org.id, - 'repository-id': repo['id'], - } - ) - ContentView.publish({'id': cv['id']}) - cv = ContentView.info({'id': cv['id']}) - assert len(cv['versions']) == 1 - cvv = cv['versions'][0] - # Verify export directory is empty - assert ( - target_sat.validate_pulp_filepath(module_entitlement_manifest_org, PULP_EXPORT_DIR) - == '' - ) - # Export content view - ContentExport.completeVersion( - {'id': cvv['id'], 'organization-id': module_entitlement_manifest_org.id} - ) - # Verify export directory is not empty - assert ( - target_sat.validate_pulp_filepath(module_entitlement_manifest_org, PULP_EXPORT_DIR) - != '' - ) - - @pytest.mark.tier3 - @pytest.mark.upgrade + @pytest.mark.parametrize( + 'function_synced_rhel_repo', + ['rhae2'], + indirect=True, + ) def test_positive_export_complete_library_rh_repo( - self, export_import_cleanup_function, function_entitlement_manifest_org, target_sat + self, + target_sat, + export_import_cleanup_function, + function_sca_manifest_org, + function_synced_rhel_repo, ): """Export RedHat repo via complete library :id: ffae18bf-6536-4f11-8002-7bf1568bf7f1 + :parametrized: yes + + :setup: + 1. Enabled and synced RH repository. + + :steps: + 1. Create CV with the RH repo and publish. + 2. Export CV version contents to a directory. + :expectedresults: 1. Repository was successfully exported, exported files are present on satellite machine :CaseLevel: System """ - # Enable and sync RH repository - repo = _enable_rhel_content( - sat=target_sat, - org=function_entitlement_manifest_org, - repo_dict=REPOS['rhae2'], - ) # Create cv and publish cv_name = gen_string('alpha') - cv = make_content_view( - {'name': cv_name, 'organization-id': function_entitlement_manifest_org.id} - ) + cv = make_content_view({'name': cv_name, 'organization-id': function_sca_manifest_org.id}) ContentView.add_repository( { 'id': cv['id'], - 'organization-id': function_entitlement_manifest_org.id, - 'repository-id': repo['id'], + 'organization-id': function_sca_manifest_org.id, + 'repository-id': function_synced_rhel_repo['id'], } ) ContentView.publish({'id': cv['id']}) # Verify export directory is empty - assert ( - target_sat.validate_pulp_filepath(function_entitlement_manifest_org, PULP_EXPORT_DIR) - == '' - ) + assert target_sat.validate_pulp_filepath(function_sca_manifest_org, PULP_EXPORT_DIR) == '' # Export content view - ContentExport.completeLibrary({'organization-id': function_entitlement_manifest_org.id}) + ContentExport.completeLibrary({'organization-id': function_sca_manifest_org.id}) # Verify export directory is not empty - assert ( - target_sat.validate_pulp_filepath(function_entitlement_manifest_org, PULP_EXPORT_DIR) - != '' - ) + assert target_sat.validate_pulp_filepath(function_sca_manifest_org, PULP_EXPORT_DIR) != '' @pytest.mark.tier3 @pytest.mark.upgrade @@ -479,45 +484,6 @@ def _create_cv(cv_name, repo, module_org, publish=True): return content_view, cvv_id -def _enable_rhel_content(sat, org, repo_dict, ver=None, sync=True): - """Enable (and synchronize) rhel content - - :param sat: Satellite instance to work with - :param org: The organization directory into which the rhel contents will be enabled - :param repo_dict: The repository dict as defined in consts REPOS - :param bool sync: Syncs contents to repository if true else doesn't - :return: Repository cli object - """ - sat.cli.RepositorySet.enable( - { - 'organization-id': org.id, - 'name': repo_dict['reposet'], - 'product': repo_dict['product'], - 'releasever': ver or repo_dict.get('releasever', None), - 'basearch': DEFAULT_ARCHITECTURE, - } - ) - repo = sat.cli.Repository.info( - { - 'organization-id': org.id, - 'name': repo_dict['name'], - 'product': repo_dict['product'], - } - ) - # Update the download policy to 'immediate' and sync if required - sat.cli.Repository.update({'download-policy': 'immediate', 'id': repo['id']}) - if sync: - sat.cli.Repository.synchronize({'id': repo['id']}, timeout=7200000) - repo = sat.cli.Repository.info( - { - 'organization-id': org.id, - 'name': repo_dict['name'], - 'product': repo_dict['product'], - } - ) - return repo - - def _import_entities(product, repo, cv, mos='no'): """Sets same CV, product and repository in importing organization as exporting organization @@ -560,30 +526,28 @@ class TestContentViewSync: @pytest.mark.e2e def test_positive_export_import_cv_end_to_end( self, + target_sat, class_export_entities, config_export_import_settings, export_import_cleanup_module, - target_sat, module_org, + function_import_org, ): - """Export the CV and import it. Ensure that all content is same from - export to import + """Export the CV and import it. Ensure that all content is same from export to import. :id: b4fb9386-9b6a-4fc5-a8bf-96d7c80af93e - :steps: + :setup: + 1. Product with synced custom repository, published in a CV. - 1. Create product and repository with custom contents. - 2. Sync the repository. - 3. Create CV with above product and publish. - 4. Export CV version via complete version - 5. Import the exported files to satellite - 6. Check that content of export and import matches + :steps: + 1. Export CV version via complete version + 2. Import the exported files to satellite + 3. Check that content of export and import matches :expectedresults: - - 1. CV version custom contents has been exported to directory - 2. All The exported custom contents has been imported in org/satellite + 1. CV version custom contents has been exported to directory. + 2. All The exported custom contents has been imported in org/satellite. :CaseImportance: High @@ -598,47 +562,46 @@ def test_positive_export_import_cv_end_to_end( export_cvv_id = class_export_entities['exporting_cvv_id'] export_cv_description = class_export_entities['exporting_cv']['description'] import_cv_name = class_export_entities['exporting_cv_name'] - # check packages - exported_packages = Package.list({'content-view-version-id': export_cvv_id}) + # Check packages + exported_packages = target_sat.cli.Package.list({'content-view-version-id': export_cvv_id}) assert len(exported_packages) # Verify export directory is empty assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) == '' # Export cv - export = ContentExport.completeVersion( + export = target_sat.cli.ContentExport.completeVersion( {'id': export_cvv_id, 'organization-id': module_org.id} ) import_path = target_sat.move_pulp_archive(module_org, export['message']) - - # importing portion - importing_org = make_org() - # set disconnected mode - Settings.set({'name': 'subscription_connection_enabled', 'value': "No"}) - # check that files are present in import_path + # Check that files are present in import_path result = target_sat.execute(f'ls {import_path}') assert result.stdout != '' # Import files and verify content - ContentImport.version({'organization-id': importing_org['id'], 'path': import_path}) - importing_cv = ContentView.info( - {'name': import_cv_name, 'organization-id': importing_org['id']} + target_sat.cli.ContentImport.version( + {'organization-id': function_import_org.id, 'path': import_path} + ) + importing_cv = target_sat.cli.ContentView.info( + {'name': import_cv_name, 'organization-id': function_import_org.id} ) importing_cvv = importing_cv['versions'] assert importing_cv['description'] == export_cv_description assert len(importing_cvv) >= 1 - imported_packages = Package.list({'content-view-version-id': importing_cvv[0]['id']}) + imported_packages = target_sat.cli.Package.list( + {'content-view-version-id': importing_cvv[0]['id']} + ) assert len(imported_packages) assert len(exported_packages) == len(imported_packages) - exported_repo = Repository.info( + exported_repo = target_sat.cli.Repository.info( { 'name': export_repo_name, 'product': export_prod_name, 'organization-id': module_org.id, } ) - imported_repo = Repository.info( + imported_repo = target_sat.cli.Repository.info( { 'name': import_repo_name, 'product': import_prod_name, - 'organization-id': importing_org['id'], + 'organization-id': function_import_org.id, } ) for item in ['packages', 'source-rpms', 'package-groups', 'errata', 'module-streams']: @@ -646,102 +609,108 @@ def test_positive_export_import_cv_end_to_end( @pytest.mark.upgrade @pytest.mark.tier3 + @pytest.mark.parametrize( + 'function_synced_rhel_repo', + ['rhae2'], + indirect=True, + ) def test_positive_export_import_default_org_view( self, + target_sat, export_import_cleanup_function, - function_org, config_export_import_settings, - target_sat, + function_sca_manifest_org, + function_import_org_with_manifest, + function_synced_custom_repo, + function_synced_rhel_repo, ): """Export Default Organization View version contents in directory and Import them. :id: b8a2c878-cfc2-491c-a71f-74108d6bc247 - :bz: 1671319 + :parametrized: yes - :customerscenario: true + :setup: + 1. Product with synced custom repository. + 2. Enabled and synced RH repository. :steps: - - 1. Create product and repository with custom contents. - 2. Sync the repository. - 3. Create CV with above product and publish. - 4. Export `Default Organization View version` contents to a directory - using complete library - 5. Import those contents from some other org/satellite. + 1. Create CV with the custom and RH repository. + 2. Export `Default Organization View version` contents using complete library. + 3. Import those contents from some other org/satellite. :expectedresults: + 1. Default Organization View version custom contents has been exported. + 2. All the exported custom contents has been imported in org/satellite. - 1. Default Organization View version custom contents has been exported to directory - 2. All The exported custom contents has been imported in org/satellite + :CaseLevel: System - :CaseImportance: High + :BZ: 1671319 - :CaseLevel: System + :customerscenario: true """ - importing_cv_name = DEFAULT_CV + # Create cv and publish cv_name = gen_string('alpha') - export_library = 'Export-Library' - # Create custom repo - product = make_product({'organization-id': function_org.id}) - repo = make_repository( + cv = target_sat.cli_factory.make_content_view( + {'name': cv_name, 'organization-id': function_sca_manifest_org.id} + ) + target_sat.cli.ContentView.add_repository( { - 'download-policy': 'immediate', - 'organization-id': function_org.id, - 'product-id': product['id'], + 'id': cv['id'], + 'organization-id': function_sca_manifest_org.id, + 'repository-id': function_synced_custom_repo['id'], } ) - Repository.synchronize({'id': repo['id']}) - # Create cv and publish - cv = make_content_view({'name': cv_name, 'organization-id': function_org.id}) - ContentView.add_repository( + target_sat.cli.ContentView.add_repository( { 'id': cv['id'], - 'organization-id': function_org.id, - 'repository-id': repo['id'], + 'organization-id': function_sca_manifest_org.id, + 'repository-id': function_synced_rhel_repo['id'], } ) - ContentView.publish({'id': cv['id']}) - content_view = ContentView.info( + target_sat.cli.ContentView.publish({'id': cv['id']}) + content_view = target_sat.cli.ContentView.info( { 'name': cv_name, - 'organization-id': function_org.id, + 'organization-id': function_sca_manifest_org.id, } ) # Verify packages default_cvv_id = content_view['versions'][0]['id'] - cv_packages = Package.list({'content-view-version-id': default_cvv_id}) + cv_packages = target_sat.cli.Package.list({'content-view-version-id': default_cvv_id}) assert len(cv_packages) # Verify export directory is empty - assert target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) == '' + assert target_sat.validate_pulp_filepath(function_sca_manifest_org, PULP_EXPORT_DIR) == '' # Export complete library - export = ContentExport.completeLibrary({'organization-id': function_org.id}) + export = target_sat.cli.ContentExport.completeLibrary( + {'organization-id': function_sca_manifest_org.id} + ) # Verify 'export-library' is created and packages are there - import_path = target_sat.move_pulp_archive(function_org, export['message']) - export_lib_cv = ContentView.info( + import_path = target_sat.move_pulp_archive(function_sca_manifest_org, export['message']) + export_lib_cv = target_sat.cli.ContentView.info( { - 'name': export_library, - 'organization-id': function_org.id, + 'name': EXPORT_LIBRARY_NAME, + 'organization-id': function_sca_manifest_org.id, } ) export_lib_cvv_id = export_lib_cv['versions'][0]['id'] - exported_lib_packages = Package.list({'content-view-version-id': export_lib_cvv_id}) - assert len(cv_packages) + exported_lib_packages = target_sat.cli.Package.list( + {'content-view-version-id': export_lib_cvv_id} + ) + assert len(exported_lib_packages) assert exported_lib_packages == cv_packages - # importing portion - importing_org = make_org() - # set disconnected mode - Settings.set({'name': 'subscription_connection_enabled', 'value': "No"}) - # check that files are present in import_path - result = target_sat.execute(f'ls {import_path}') - assert result.stdout != '' # Import and verify content of library - ContentImport.library({'organization-id': importing_org['id'], 'path': import_path}) - importing_cvv = ContentView.info( - {'name': importing_cv_name, 'organization-id': importing_org['id']} + target_sat.cli.Settings.set({'name': 'subscription_connection_enabled', 'value': "No"}) + target_sat.cli.ContentImport.library( + {'organization-id': function_import_org_with_manifest.id, 'path': import_path} + ) + importing_cvv = target_sat.cli.ContentView.info( + {'name': DEFAULT_CV, 'organization-id': function_import_org_with_manifest.id} )['versions'] assert len(importing_cvv) >= 1 - imported_packages = Package.list({'content-view-version-id': importing_cvv[0]['id']}) + imported_packages = target_sat.cli.Package.list( + {'content-view-version-id': importing_cvv[0]['id']} + ) assert len(imported_packages) assert len(cv_packages) == len(imported_packages) @@ -840,71 +809,71 @@ def test_positive_export_import_filtered_cvv( @pytest.mark.upgrade def test_positive_export_import_promoted_cv( self, + target_sat, class_export_entities, export_import_cleanup_module, config_export_import_settings, - target_sat, module_org, + function_import_org, ): """Export promoted CV version contents in directory and Import them. :id: 315ef1f0-e2ad-43ec-adff-453fb71654a7 - :steps: + :setup: + 1. Product with synced custom repository, published in a CV. - 1. Create product and repository with contents. - 2. Sync the repository. - 3. Create CV with above product and publish. - 4. Promote the CV. - 5. Export CV version contents to a directory - 6. Import those contents from some other org/satellite. + :steps: + 1. Promote the CV. + 2. Export CV version contents to a directory. + 3. Import those contents from some other org/satellite. :expectedresults: - - 1. Promoted CV version contents has been exported to directory - 2. Promoted CV version contents has been imported successfully - 3. The imported CV should only be published and not promoted + 1. Promoted CV version contents has been exported to directory. + 2. Promoted CV version contents has been imported successfully. + 3. The imported CV should only be published and not promoted. :CaseLevel: System """ import_cv_name = class_export_entities['exporting_cv_name'] export_cv_id = class_export_entities['exporting_cv']['id'] export_cvv_id = class_export_entities['exporting_cvv_id'] - env = make_lifecycle_environment({'organization-id': module_org.id}) - ContentView.version_promote( + env = target_sat.cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) + target_sat.cli.ContentView.version_promote( { 'id': export_cvv_id, 'to-lifecycle-environment-id': env['id'], } ) - promoted_cvv_id = ContentView.info({'id': export_cv_id})['versions'][-1]['id'] - # check packages - exported_packages = Package.list({'content-view-version-id': promoted_cvv_id}) + promoted_cvv_id = target_sat.cli.ContentView.info({'id': export_cv_id})['versions'][-1][ + 'id' + ] + # Check packages + exported_packages = target_sat.cli.Package.list( + {'content-view-version-id': promoted_cvv_id} + ) assert len(exported_packages) # Verify export directory is empty assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) == '' # Export cv - export = ContentExport.completeVersion( + export = target_sat.cli.ContentExport.completeVersion( {'id': export_cvv_id, 'organization-id': module_org.id} ) import_path = target_sat.move_pulp_archive(module_org, export['message']) - - # importing portion - importing_org = make_org() - # set disconnected mode - Settings.set({'name': 'subscription_connection_enabled', 'value': "No"}) - # Move export files to import location and set permission - # Import and verify content - ContentImport.version({'organization-id': importing_org['id'], 'path': import_path}) - importing_cv_id = ContentView.info( - {'name': import_cv_name, 'organization-id': importing_org['id']} + target_sat.cli.ContentImport.version( + {'organization-id': function_import_org.id, 'path': import_path} ) - importing_cvv_id = ContentView.info( - {'name': import_cv_name, 'organization-id': importing_org['id']} + importing_cv_id = target_sat.cli.ContentView.info( + {'name': import_cv_name, 'organization-id': function_import_org.id} + ) + importing_cvv_id = target_sat.cli.ContentView.info( + {'name': import_cv_name, 'organization-id': function_import_org.id} )['versions'] assert len(importing_cvv_id) >= 1 - imported_packages = Package.list({'content-view-version-id': importing_cvv_id[0]['id']}) + imported_packages = target_sat.cli.Package.list( + {'content-view-version-id': importing_cvv_id[0]['id']} + ) assert len(imported_packages) assert len(exported_packages) == len(imported_packages) # Verify the LCE is in Library @@ -914,30 +883,37 @@ def test_positive_export_import_promoted_cv( @pytest.mark.tier3 @pytest.mark.upgrade @pytest.mark.e2e + @pytest.mark.parametrize( + 'function_synced_rhel_repo', + ['kickstart-rhel7', 'kickstart-rhel8_bos', 'rhscl7'], + indirect=True, + ) def test_positive_export_import_redhat_cv( self, + target_sat, export_import_cleanup_function, config_export_import_settings, - function_entitlement_manifest_org, - function_secondary_entitlement_manifest, - target_sat, + function_sca_manifest_org, + function_import_org_with_manifest, + function_synced_rhel_repo, ): - """Export CV version redhat contents in directory and Import them + """Export CV version with RedHat contents in directory and import them. :id: f6bd7fa9-396e-44ac-92a3-ab87ce1a7ef5 - :steps: + :parametrized: yes - 1. Enable product and repository with redhat contents. - 2. Sync the repository. - 3. Create CV with above product and publish. - 4. Export CV version contents to a directory - 5. Import those contents from some other org/satellite. + :setup: + 1. Enabled and synced RH repository. - :expectedresults: + :steps: + 1. Create CV with the RH repo and publish. + 2. Export CV version contents to a directory. + 3. Import those contents from some other org/satellite. - 1. CV version redhat contents has been exported to directory - 2. All The exported redhat contents has been imported in org/satellite + :expectedresults: + 1. CV version redhat contents has been exported to directory. + 2. All the exported redhat contents has been imported in org/satellite. :BZ: 1655239, 2040870 @@ -947,188 +923,62 @@ def test_positive_export_import_redhat_cv( :CaseLevel: System """ - # Enable and sync RH repository - repo = _enable_rhel_content( - sat=target_sat, - org=function_entitlement_manifest_org, - repo_dict=REPOS['kickstart']['rhel7'], - ver=REPOS['kickstart']['rhel7']['version'], - ) # Create cv and publish cv_name = gen_string('alpha') - cv = make_content_view( - {'name': cv_name, 'organization-id': function_entitlement_manifest_org.id} + cv = target_sat.cli_factory.make_content_view( + {'name': cv_name, 'organization-id': function_sca_manifest_org.id} ) - ContentView.add_repository( + target_sat.cli.ContentView.add_repository( { 'id': cv['id'], - 'organization-id': function_entitlement_manifest_org.id, - 'repository-id': repo['id'], + 'organization-id': function_sca_manifest_org.id, + 'repository-id': function_synced_rhel_repo['id'], } ) - ContentView.publish({'id': cv['id']}) - cv = ContentView.info({'id': cv['id']}) + target_sat.cli.ContentView.publish({'id': cv['id']}) + cv = target_sat.cli.ContentView.info({'id': cv['id']}) assert len(cv['versions']) == 1 cvv = cv['versions'][0] # Verify export directory is empty - assert ( - target_sat.validate_pulp_filepath(function_entitlement_manifest_org, PULP_EXPORT_DIR) - == '' - ) + assert target_sat.validate_pulp_filepath(function_sca_manifest_org, PULP_EXPORT_DIR) == '' # Export cv - export = ContentExport.completeVersion( - {'id': cvv['id'], 'organization-id': function_entitlement_manifest_org.id}, - timeout=7200000, - ) - import_path = target_sat.move_pulp_archive( - function_entitlement_manifest_org, export['message'] - ) - exported_packages = Package.list({'content-view-version-id': cvv['id']}) - assert len(exported_packages) - - # importing portion - importing_org = target_sat.api.Organization().create() - # check that files are present in import_path - result = target_sat.execute(f'ls {import_path}') - assert result.stdout != '' - target_sat.upload_manifest( - importing_org.id, - function_secondary_entitlement_manifest, - interface='CLI', + export = target_sat.cli.ContentExport.completeVersion( + {'id': cvv['id'], 'organization-id': function_sca_manifest_org.id}, timeout=7200000, ) - importing_org.sca_disable() - # set disconnected mode - Settings.set({'name': 'subscription_connection_enabled', 'value': "No"}) - ContentImport.version( - {'organization-id': importing_org.id, 'path': import_path}, timeout=7200000 - ) - # Import file and verify content - importing_cvv = ContentView.info({'name': cv_name, 'organization-id': importing_org.id})[ - 'versions' - ] - assert len(importing_cvv) >= 1 - imported_packages = Package.list({'content-view-version-id': importing_cvv[0]['id']}) - assert len(imported_packages) - assert len(exported_packages) == len(imported_packages) - exported_repo = Repository.info( - { - 'name': repo['name'], - 'product': repo['product']['name'], - 'organization-id': function_entitlement_manifest_org.id, - } - ) - imported_repo = Repository.info( - { - 'name': repo['name'], - 'product': repo['product']['name'], - 'organization-id': importing_org.id, - } - ) - for item in ['packages', 'source-rpms', 'package-groups', 'errata', 'module-streams']: - assert exported_repo['content-counts'][item] == imported_repo['content-counts'][item] - - @pytest.mark.tier4 - def test_positive_export_import_redhat_cv_with_huge_contents( - self, - export_import_cleanup_function, - config_export_import_settings, - target_sat, - function_entitlement_manifest_org, - function_secondary_entitlement_manifest, - ): - """Export CV version redhat contents in directory and Import them - - :id: 05eb185f-e526-466c-9c14-702dde1d49de - - :steps: - - 1. Enable product and repository with redhat repository having huge contents. - 2. Sync the repository. - 3. Create CV with above product and publish. - 4. Export CV version contents to a directory - 5. Import those contents from some other org/satellite. - - :expectedresults: - - 1. CV version redhat contents has been exported to directory - 2. All The exported redhat contents has been imported in org/satellite - - :BZ: 1655239 - - :CaseImportance: Critical + # Verify export directory is not empty + assert target_sat.validate_pulp_filepath(function_sca_manifest_org, PULP_EXPORT_DIR) != '' - :CaseLevel: Acceptance - """ - # Enable and sync RH repository - repo = _enable_rhel_content( - sat=target_sat, - org=function_entitlement_manifest_org, - repo_dict=REPOS['rhscl7'], - ) - # Create cv and publish - cv_name = gen_string('alpha') - cv = make_content_view( - {'name': cv_name, 'organization-id': function_entitlement_manifest_org.id} - ) - ContentView.add_repository( - { - 'id': cv['id'], - 'organization-id': function_entitlement_manifest_org.id, - 'repository-id': repo['id'], - } - ) - ContentView.publish({'id': cv['id']}) - cv = ContentView.info({'id': cv['id']}) - assert len(cv['versions']) == 1 - cvv = cv['versions'][0] - # Export cv - export = ContentExport.completeVersion( - {'id': cvv['id'], 'organization-id': function_entitlement_manifest_org.id}, - timeout=7200000, - ) - import_path = target_sat.move_pulp_archive( - function_entitlement_manifest_org, export['message'] - ) - exported_packages = Package.list({'content-view-version-id': cvv['id']}) + import_path = target_sat.move_pulp_archive(function_sca_manifest_org, export['message']) + exported_packages = target_sat.cli.Package.list({'content-view-version-id': cvv['id']}) assert len(exported_packages) - # importing portion - importing_org = target_sat.api.Organization().create() - # check that files are present in import_path - result = target_sat.execute(f'ls {import_path}') - assert result.stdout != '' # Import and verify content - target_sat.upload_manifest( - importing_org.id, - function_secondary_entitlement_manifest, - interface='CLI', + target_sat.cli.Settings.set({'name': 'subscription_connection_enabled', 'value': "No"}) + target_sat.cli.ContentImport.version( + {'organization-id': function_import_org_with_manifest.id, 'path': import_path}, timeout=7200000, ) - importing_org.sca_disable() - # set disconnected mode - Settings.set({'name': 'subscription_connection_enabled', 'value': "No"}) - ContentImport.version( - {'organization-id': importing_org.id, 'path': import_path}, timeout=7200000 - ) - importing_cvv = ContentView.info({'name': cv_name, 'organization-id': importing_org.id})[ - 'versions' - ] + importing_cvv = target_sat.cli.ContentView.info( + {'name': cv_name, 'organization-id': function_import_org_with_manifest.id} + )['versions'] assert len(importing_cvv) >= 1 - imported_packages = Package.list({'content-view-version-id': importing_cvv[0]['id']}) + imported_packages = target_sat.cli.Package.list( + {'content-view-version-id': importing_cvv[0]['id']} + ) assert len(imported_packages) assert len(exported_packages) == len(imported_packages) - exported_repo = Repository.info( + exported_repo = target_sat.cli.Repository.info( { - 'name': repo['name'], - 'product': repo['product']['name'], - 'organization-id': function_entitlement_manifest_org.id, + 'name': function_synced_rhel_repo['name'], + 'product': function_synced_rhel_repo['product']['name'], + 'organization-id': function_sca_manifest_org.id, } ) - imported_repo = Repository.info( + imported_repo = target_sat.cli.Repository.info( { - 'name': repo['name'], - 'product': repo['product']['name'], - 'organization-id': importing_org.id, + 'name': function_synced_rhel_repo['name'], + 'product': function_synced_rhel_repo['product']['name'], + 'organization-id': function_import_org_with_manifest.id, } ) for item in ['packages', 'source-rpms', 'package-groups', 'errata', 'module-streams']: @@ -1244,51 +1094,49 @@ def test_positive_export_cv_with_on_demand_repo( @pytest.mark.tier2 def test_negative_import_same_cv_twice( self, + target_sat, class_export_entities, export_import_cleanup_module, config_export_import_settings, - target_sat, module_org, + function_import_org, ): - """Import the same cv twice + """Import the same CV twice. :id: 15a7ddd3-c1a5-4b22-8460-6cb2b8ea4ef9 - :steps: + :setup: + 1. Product with synced custom repository, published in a CV. - 1. Create product and repository with custom contents. - 2. Sync the repository. - 3. Create CV with above product and publish. - 4. Export CV version contents to a directory - 5. Import those contents from some other org/satellite. - 6. Attempt to reimport the same contents + :steps: + 1. Export CV version contents to a directory. + 2. Import those contents from some other org/satellite. + 3. Attempt to reimport the same contents. :expectedresults: - - 1. Reimporting the contents with same version fails - 2. Satellite displays an error message + 1. Reimporting the contents with same version fails. + 2. Satellite displays an error message. """ export_cvv_id = class_export_entities['exporting_cvv_id'] export_cv_name = class_export_entities['exporting_cv_name'] # Verify export directory is empty assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) == '' # Export cv - export = ContentExport.completeVersion( + export = target_sat.cli.ContentExport.completeVersion( {'id': export_cvv_id, 'organization-id': module_org.id} ) import_path = target_sat.move_pulp_archive(module_org, export['message']) - - # importing portion - importing_org = make_org() - # set disconnected mode - Settings.set({'name': 'subscription_connection_enabled', 'value': "No"}) - # check that files are present in import_path + # Check that files are present in import_path result = target_sat.execute(f'ls {import_path}') assert result.stdout != '' # Import section - ContentImport.version({'organization-id': importing_org['id'], 'path': import_path}) + target_sat.cli.ContentImport.version( + {'organization-id': function_import_org.id, 'path': import_path} + ) with pytest.raises(CLIReturnCodeError) as error: - ContentImport.version({'organization-id': importing_org['id'], 'path': import_path}) + target_sat.cli.ContentImport.version( + {'organization-id': function_import_org.id, 'path': import_path} + ) assert ( f"Content View Version specified in the metadata - '{export_cv_name} 1.0' " 'already exists. If you wish to replace the existing version, ' @@ -1403,23 +1251,27 @@ def test_postive_export_cv_with_mixed_content_repos( assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) != '' @pytest.mark.tier3 - def test_postive_import_export_cv_with_file_content( - self, target_sat, config_export_import_settings, export_import_cleanup_module, module_org + def test_postive_export_import_cv_with_file_content( + self, + target_sat, + config_export_import_settings, + export_import_cleanup_module, + module_org, + function_import_org, ): """Exporting and Importing cv with file content :id: d00739f0-dedf-4303-8929-889dc23260a4 :steps: - 1. Create custom product and custom repo with file type 2. Sync repo 3. Create cv and add file repo created in step 1 and publish - 4. Export cv and import cv into another satellite - 5. Check imported cv has files in it + 4. Export cv and import cv into another satellite. + 5. Check imported cv has files in it. - :expectedresults: Imported cv should have the files present in the cv of - the imported system + :expectedresults: + 1. Imported cv should have the files present in the cv of the imported system. :BZ: 1995827 @@ -1427,8 +1279,8 @@ def test_postive_import_export_cv_with_file_content( """ # setup custom repo cv_name = import_cv_name = gen_string('alpha') - product = make_product({'organization-id': module_org.id}) - file_repo = make_repository( + product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) + file_repo = target_sat.cli_factory.make_repository( { 'organization-id': module_org.id, 'product-id': product['id'], @@ -1436,73 +1288,74 @@ def test_postive_import_export_cv_with_file_content( 'url': settings.repos.file_type_repo.url, } ) - Repository.synchronize({'id': file_repo['id']}) + target_sat.cli.Repository.synchronize({'id': file_repo['id']}) # create cv and publish - cv = make_content_view({'name': cv_name, 'organization-id': module_org.id}) - ContentView.add_repository( + cv = target_sat.cli_factory.make_content_view( + {'name': cv_name, 'organization-id': module_org.id} + ) + target_sat.cli.ContentView.add_repository( { 'id': cv['id'], 'organization-id': module_org.id, 'repository-id': file_repo['id'], } ) - ContentView.publish({'id': cv['id']}) - exporting_cv_id = ContentView.info({'id': cv['id']}) + target_sat.cli.ContentView.publish({'id': cv['id']}) + exporting_cv_id = target_sat.cli.ContentView.info({'id': cv['id']}) assert len(exporting_cv_id['versions']) == 1 exporting_cvv_id = exporting_cv_id['versions'][0]['id'] # check files - exported_files = File.list({'content-view-version-id': exporting_cvv_id}) + exported_files = target_sat.cli.File.list({'content-view-version-id': exporting_cvv_id}) assert len(exported_files) # Verify export directory is empty assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) == '' # Export cv - export = ContentExport.completeVersion( + export = target_sat.cli.ContentExport.completeVersion( {'id': exporting_cvv_id, 'organization-id': module_org.id} ) import_path = target_sat.move_pulp_archive(module_org, export['message']) - - # importing portion - importing_org = make_org() - # set disconnected mode - Settings.set({'name': 'subscription_connection_enabled', 'value': "No"}) - # check that files are present in import_path + # Check that files are present in import_path result = target_sat.execute(f'ls {import_path}') assert result.stdout != '' # Import files and verify content - ContentImport.version({'organization-id': importing_org['id'], 'path': import_path}) - importing_cvv = ContentView.info( - {'name': import_cv_name, 'organization-id': importing_org['id']} + target_sat.cli.ContentImport.version( + {'organization-id': function_import_org.id, 'path': import_path} + ) + importing_cvv = target_sat.cli.ContentView.info( + {'name': import_cv_name, 'organization-id': function_import_org.id} )['versions'] assert len(importing_cvv) >= 1 - imported_files = File.list({'content-view-version-id': importing_cvv[0]['id']}) + imported_files = target_sat.cli.File.list( + {'content-view-version-id': importing_cvv[0]['id']} + ) assert len(imported_files) assert len(exported_files) == len(imported_files) @pytest.mark.tier3 - def test_postive_import_export_ansible_collection_repo( + def test_postive_export_import_ansible_collection_repo( self, target_sat, config_export_import_settings, export_import_cleanup_function, function_org, + function_import_org, ): """Exporting and Importing library with ansible collection :id: 71dd1e1a-caad-48be-a180-206c8aa78639 :steps: + 1. Create custom product and custom repo with ansible collection. + 2. Sync the repo. + 3. Export library and import into another satellite. + 4. Check imported library has ansible collection in it. - 1. Create custom product and custom repo with ansible collection - 2. Sync repo - 3. Export library and import into another satellite - 4. Check imported library has ansible collection in it - - :expectedresults: Imported library should have the ansible collection present in the - imported product + :expectedresults: + 1. Imported library should have the ansible collection present in the imported product. """ # setup ansible_collection product and repo - export_product = make_product({'organization-id': function_org.id}) - ansible_repo = make_repository( + export_product = target_sat.cli_factory.make_product({'organization-id': function_org.id}) + ansible_repo = target_sat.cli_factory.make_repository( { 'organization-id': function_org.id, 'product-id': export_product['id'], @@ -1513,26 +1366,22 @@ def test_postive_import_export_ansible_collection_repo( { name: theforeman.operations, version: "0.1.0"} ]}', } ) - Repository.synchronize({'id': ansible_repo['id']}) + target_sat.cli.Repository.synchronize({'id': ansible_repo['id']}) # Export library - export = ContentExport.completeLibrary({'organization-id': function_org.id}) + export = target_sat.cli.ContentExport.completeLibrary({'organization-id': function_org.id}) import_path = target_sat.move_pulp_archive(function_org, export['message']) - - # importing portion - importing_org = make_org() - # set disconnected mode - Settings.set({'name': 'subscription_connection_enabled', 'value': "No"}) - - # check that files are present in import_path + # Check that files are present in import_path result = target_sat.execute(f'ls {import_path}') assert result.stdout != '' # Import files and verify content - ContentImport.library({'organization-id': importing_org['id'], 'path': import_path}) - assert Product.list({'organization-id': importing_org['id']}) - import_product = Product.info( + target_sat.cli.ContentImport.library( + {'organization-id': function_import_org.id, 'path': import_path} + ) + assert target_sat.cli.Product.list({'organization-id': function_import_org.id}) + import_product = target_sat.cli.Product.info( { - 'organization-id': importing_org['id'], - 'id': Product.list({'organization-id': importing_org['id']})[0]['id'], + 'organization-id': function_import_org.id, + 'id': Product.list({'organization-id': function_import_org.id})[0]['id'], } ) assert import_product['name'] == export_product['name'] @@ -1540,76 +1389,73 @@ def test_postive_import_export_ansible_collection_repo( assert import_product['content'][0]['content-type'] == "ansible_collection" @pytest.mark.tier3 + @pytest.mark.parametrize( + 'function_synced_rhel_repo', + ['rhae2'], + indirect=True, + ) def test_negative_import_redhat_cv_without_manifest( self, + target_sat, export_import_cleanup_function, config_export_import_settings, - function_entitlement_manifest_org, - target_sat, + function_sca_manifest_org, + function_synced_rhel_repo, ): """Redhat content can't be imported into satellite/organization without manifest :id: b0f5f95b-3f9f-4827-84f1-b66517dc34f1 - :steps: + :parametrized: yes - 1. Enable product and repository with redhat contents. - 2. Sync the repository. - 3. Create CV with above product and publish. - 4. Export CV version contents to a directory - 5. Import those contents to other org without manifest. + :setup: + 1. Enabled and synced RH repository. - :expectedresults: + :steps: + 1. Create CV with the RH repo and publish. + 2. Export CV version contents to a directory. + 3. Import those contents to other org without manifest. + :expectedresults: 1. Import fails with message "Could not import the archive.: No manifest found. Import a manifest with the appropriate subscriptions before importing content." - """ - # Enable and sync RH repository - repo = _enable_rhel_content( - sat=target_sat, - org=function_entitlement_manifest_org, - repo_dict=REPOS['rhae2'], - ) # Create cv and publish cv_name = gen_string('alpha') - cv = make_content_view( - {'name': cv_name, 'organization-id': function_entitlement_manifest_org.id} + cv = target_sat.cli_factory.make_content_view( + {'name': cv_name, 'organization-id': function_sca_manifest_org.id} ) - ContentView.add_repository( + target_sat.cli.ContentView.add_repository( { 'id': cv['id'], - 'organization-id': function_entitlement_manifest_org.id, - 'repository-id': repo['id'], + 'organization-id': function_sca_manifest_org.id, + 'repository-id': function_synced_rhel_repo['id'], } ) - ContentView.publish({'id': cv['id']}) - cv = ContentView.info({'id': cv['id']}) + target_sat.cli.ContentView.publish({'id': cv['id']}) + cv = target_sat.cli.ContentView.info({'id': cv['id']}) assert len(cv['versions']) == 1 cvv = cv['versions'][0] # Verify export directory is empty - assert ( - target_sat.validate_pulp_filepath(function_entitlement_manifest_org, PULP_EXPORT_DIR) - == '' - ) + assert target_sat.validate_pulp_filepath(function_sca_manifest_org, PULP_EXPORT_DIR) == '' # Export cv - export = ContentExport.completeVersion( - {'id': cvv['id'], 'organization-id': function_entitlement_manifest_org.id} - ) - import_path = target_sat.move_pulp_archive( - function_entitlement_manifest_org, export['message'] + export = target_sat.cli.ContentExport.completeVersion( + {'id': cvv['id'], 'organization-id': function_sca_manifest_org.id} ) + import_path = target_sat.move_pulp_archive(function_sca_manifest_org, export['message']) # check that files are present in import_path result = target_sat.execute(f'ls {import_path}') assert result.stdout != '' # importing portion - importing_org = make_org() + importing_org = target_sat.cli_factory.make_org() # set disconnected mode - Settings.set({'name': 'subscription_connection_enabled', 'value': "No"}) + target_sat.cli.Settings.set({'name': 'subscription_connection_enabled', 'value': "No"}) with pytest.raises(CLIReturnCodeError) as error: - ContentImport.version({'organization-id': importing_org['id'], 'path': import_path}) + target_sat.cli.ContentImport.version( + {'organization-id': importing_org['id'], 'path': import_path} + ) assert ( 'Could not import the archive.:\n No manifest found. Import a manifest with the ' 'appropriate subscriptions before importing content.' @@ -1618,25 +1464,27 @@ def test_negative_import_redhat_cv_without_manifest( @pytest.mark.tier2 def test_positive_import_content_for_disconnected_sat_with_existing_content( self, + target_sat, class_export_entities, config_export_import_settings, - target_sat, module_org, + function_import_org, ): """Import a content view into a disconnected satellite for an existing content view :id: 22c077dc-0041-4c6c-9da5-fd58e5497ae8 - :steps: + :setup: + 1. Product with synced custom repository, published in a CV. - 1. Sync a few repos - 2. Create a cv with the repo from 1 - 3. Run complete export - 4. On Disconnected satellite, create a cv with same name as cv on 2 and with - 'import-only' selected - 5. run import command + :steps: + 1. Run complete export of the CV. + 2. On Disconnected satellite, create a cv with same name as cv on 2 and with + 'import-only' selected. + 3. Run the import command. - :expectedresults: Import should run successfully + :expectedresults: + 1. Import should run successfully :bz: 2030101 @@ -1647,25 +1495,23 @@ def test_positive_import_content_for_disconnected_sat_with_existing_content( # Verify export directory is empty assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) == '' # Export cv - export = ContentExport.completeVersion( + export = target_sat.cli.ContentExport.completeVersion( {'id': export_cvv_id, 'organization-id': module_org.id} ) import_path = target_sat.move_pulp_archive(module_org, export['message']) - # importing portion - importing_org = make_org() - # set disconnected mode - Settings.set({'name': 'subscription_connection_enabled', 'value': "No"}) - # check that files are present in import_path + # Check that files are present in import_path result = target_sat.execute(f'ls {import_path}') assert result.stdout != '' # Import section # Create cv with 'import-only' set to true - make_content_view( - {'name': export_cv_name, 'import-only': True, 'organization-id': importing_org['id']} + target_sat.cli_factory.make_content_view( + {'name': export_cv_name, 'import-only': True, 'organization-id': function_import_org.id} ) - ContentImport.version({'organization-id': importing_org['id'], 'path': import_path}) - importing_cvv = ContentView.info( - {'name': export_cv_name, 'organization-id': importing_org['id']} + target_sat.cli.ContentImport.version( + {'organization-id': function_import_org.id, 'path': import_path} + ) + importing_cvv = target_sat.cli.ContentView.info( + {'name': export_cv_name, 'organization-id': function_import_org.id} )['versions'] assert len(importing_cvv) >= 1 @@ -1742,32 +1588,6 @@ def test_negative_export_repo_from_future_datetime(self): :CaseLevel: System """ - @pytest.mark.stubbed - @pytest.mark.tier3 - @pytest.mark.upgrade - def test_positive_export_import_kickstart_tree(self): - """kickstart tree is exported to specified location. - - :id: bb9e77ed-fbbb-4e43-b118-2ddcb7c6341f - - :steps: - - 1. Export the full kickstart tree. - 2. Copy exported kickstart tree contents to - /var/www/html/pub/export. - 3. Import above exported kickstart tree from other org/satellite. - - :expectedresults: - - 1. Whole kickstart tree contents has been exported to directory - specified in settings. - 2. All The exported contents has been imported in org/satellite. - - :CaseAutomation: NotAutomated - - :CaseLevel: System - """ - @pytest.mark.stubbed @pytest.mark.tier3 def test_positive_export_redhat_incremental_yum_repo(self): From 3f9e894fe8b223d899a6f84f96304141e5577882 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 26 Sep 2023 08:52:08 -0400 Subject: [PATCH 247/586] [6.14.z] Add customer test case for BZ#2212523 (#12725) Add customer test case for BZ#2212523 (#12687) (cherry picked from commit f2ab861495f62bdbbc0bc3deba14d3be7d7b274e) Co-authored-by: vsedmik <46570670+vsedmik@users.noreply.github.com> --- tests/foreman/cli/test_satellitesync.py | 91 +++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index 66718f6e49a..d5a5cfdf9f6 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -16,6 +16,8 @@ :Upstream: No """ +import os + from fauxfactory import gen_string from manifester import Manifester import pytest @@ -1515,6 +1517,95 @@ def test_positive_import_content_for_disconnected_sat_with_existing_content( )['versions'] assert len(importing_cvv) >= 1 + @pytest.mark.tier3 + @pytest.mark.parametrize( + 'function_synced_rhel_repo', + ['rhae2'], + indirect=True, + ) + def test_positive_export_incremental_syncable_check_content( + self, + target_sat, + export_import_cleanup_function, + config_export_import_settings, + function_sca_manifest_org, + function_synced_rhel_repo, + ): + """Export complete and incremental CV version in syncable format and assert that all + files referenced in the repomd.xml (including productid) are present in the exports. + + :id: 6ff771cd-39ef-4865-8ae8-629f4baf5f98 + + :setup: + 1. Enabled and synced RH repository. + + :steps: + 1. Create a CV, add the product and publish it. + 2. Export complete syncable CV version. + 3. Publish new CV version. + 4. Export incremental syncable CV version. + 5. Verify the exports contain all files listed in the repomd.xml. + + :expectedresults: + 1. Complete and incremental export succeed. + 2. All files referenced in the repomd.xml files are present in the exports. + + :CaseLevel: System + + :BZ: 2212523 + + :customerscenario: true + """ + # Create cv and publish + cv_name = gen_string('alpha') + cv = target_sat.cli_factory.make_content_view( + {'name': cv_name, 'organization-id': function_sca_manifest_org.id} + ) + target_sat.cli.ContentView.add_repository( + { + 'id': cv['id'], + 'organization-id': function_sca_manifest_org.id, + 'repository-id': function_synced_rhel_repo['id'], + } + ) + target_sat.cli.ContentView.publish({'id': cv['id']}) + cv = target_sat.cli.ContentView.info({'id': cv['id']}) + assert len(cv['versions']) == 1 + cvv = cv['versions'][0] + # Verify export directory is empty + assert target_sat.validate_pulp_filepath(function_sca_manifest_org, PULP_EXPORT_DIR) == '' + # Export complete and check the export directory + target_sat.cli.ContentExport.completeVersion({'id': cvv['id'], 'format': 'syncable'}) + assert '1.0' in target_sat.validate_pulp_filepath( + function_sca_manifest_org, PULP_EXPORT_DIR + ) + # Publish new CV version, export incremental and check the export directory + target_sat.cli.ContentView.publish({'id': cv['id']}) + cv = target_sat.cli.ContentView.info({'id': cv['id']}) + assert len(cv['versions']) == 2 + cvv = max(cv['versions'], key=lambda x: int(x['id'])) + target_sat.cli.ContentExport.incrementalVersion({'id': cvv['id'], 'format': 'syncable'}) + assert '2.0' in target_sat.validate_pulp_filepath( + function_sca_manifest_org, PULP_EXPORT_DIR + ) + # Verify that the content referenced in repomd.xml files is present in both exports + repomd_files = target_sat.execute( + f'find {PULP_EXPORT_DIR}{function_sca_manifest_org.name}/{cv_name}/ -name repomd.xml' + ).stdout.splitlines() + assert len(repomd_files) == 2, 'Unexpected count of exports identified.' + for repomd in repomd_files: + repodata_dir = os.path.split(repomd)[0] + repomd_refs = set( + target_sat.execute( + f'''grep -oP '(?<= Date: Wed, 27 Sep 2023 03:24:05 +0000 Subject: [PATCH 248/586] Bump sphinx-autoapi from 2.1.1 to 3.0.0 Bumps [sphinx-autoapi](https://github.com/readthedocs/sphinx-autoapi) from 2.1.1 to 3.0.0. - [Changelog](https://github.com/readthedocs/sphinx-autoapi/blob/main/CHANGELOG.rst) - [Commits](https://github.com/readthedocs/sphinx-autoapi/compare/v2.1.1...v3.0.0) --- updated-dependencies: - dependency-name: sphinx-autoapi dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] (cherry picked from commit 43459e8043ba148c10e2e295f593f9c212b97e7f) --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index cc96a855781..03665f3e3d8 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -6,7 +6,7 @@ pre-commit==3.4.0 # For generating documentation. sphinx==7.2.6 -sphinx-autoapi==2.1.1 +sphinx-autoapi==3.0.0 # For 'manage' interactive shell manage==0.1.15 From 4016b50a5f44b828fcfa47be3ff8674cfd1014fa Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 28 Sep 2023 09:54:08 -0400 Subject: [PATCH 249/586] [6.14.z] Add in the caching mechanism to the Satellite's api property (#12770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add in the caching mechanism to the Satellite's api property (#12765) This should have always existed but was overlooked. It will dramatically decrease the amount of time it takes to access the api property since it doesn't need to be reconstructed each time the property is accessed. Before ------ %timeit mysat.api.Host 3.62 ms ± 52.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) After ----- %timeit mysat.api.Host 189 ns ± 0.943 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each) (cherry picked from commit b3d32b6d7aa568f5532eaf02fba3a5f062c517a5) Co-authored-by: Jake Callahan --- robottelo/hosts.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 0a81a6de7ac..51348d73d55 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1723,6 +1723,7 @@ def cli(self): except AttributeError: # not everything has an mro method, we don't care about them pass + self._cli._configured = True return self._cli @@ -1775,6 +1776,7 @@ class DecClass(cls): except AttributeError: # not everything has an mro method, we don't care about them pass + self._api._configured = True return self._api @property @@ -1804,6 +1806,7 @@ def cli(self): except AttributeError: # not everything has an mro method, we don't care about them pass + self._cli._configured = True return self._cli @contextmanager From 56760dcbad6a89fc74f97d4c79ed518d3b96b937 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 29 Sep 2023 14:08:17 -0400 Subject: [PATCH 250/586] [6.14.z] removal of unnecessary tests (#12782) removal of unnecessary tests (#12744) (cherry picked from commit 865ed7088c23568765a132a22f2c922d84481284) Co-authored-by: rmynar <64528205+rmynar@users.noreply.github.com> --- tests/foreman/cli/test_installer.py | 12 --- tests/foreman/installer/test_installer.py | 126 ---------------------- 2 files changed, 138 deletions(-) diff --git a/tests/foreman/cli/test_installer.py b/tests/foreman/cli/test_installer.py index 67d0bf7ad9c..7980f40a385 100644 --- a/tests/foreman/cli/test_installer.py +++ b/tests/foreman/cli/test_installer.py @@ -36,15 +36,3 @@ def test_positive_server_installer_from_iso(): :CaseAutomation: NotAutomated """ - - -def test_positive_disconnected_util_installer(): - """Can install satellite disconnected utility successfully - via RPM - - :id: b738cf2a-9c5f-4865-b134-102a4688534c - - :expectedresults: Install of disconnected utility successful. - - :CaseAutomation: NotAutomated - """ diff --git a/tests/foreman/installer/test_installer.py b/tests/foreman/installer/test_installer.py index f8ea4958087..f76b899385b 100644 --- a/tests/foreman/installer/test_installer.py +++ b/tests/foreman/installer/test_installer.py @@ -1687,132 +1687,6 @@ def test_installer_check_on_ipv6(): """ -@pytest.mark.stubbed -@pytest.mark.tier1 -def test_installer_verbose_stdout(): - """Look for Satellite installer verbose STDOUT - - :id: 5d0fb30a-4a63-41b3-bc6f-c4057942ce3c - - :steps: - 1. Install satellite package. - 2. Run Satellite installer - 3. Observe installer STDOUT. - - :expectedresults: - 1. Installer STDOUTs following groups hooks completion. - pre_migrations, boot, init, pre_values, pre_validations, pre_commit, pre, post - 2. Installer STDOUTs system configuration completion. - 3. Finally, Installer informs running satellite url, credentials, - external capsule installation pre-requisite, upgrade capsule instruction, - running internal capsule url, log file. - - :CaseLevel: System - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -@pytest.mark.tier1 -def test_installer_answers_file(): - """Answers file to configure plugins and hooks - - :id: 5cb40e4b-1acb-49f9-a085-a7dead1664b5 - - :steps: - 1. Install satellte package - 2. Modify `/etc/foreman-installer/scenarios.d/satellite-answers.yaml` file to - configure hook/plugin on satellite - 3. Run Satellite installer - - :expectedresults: Installer configures plugins and hooks in answers file. - - :CaseLevel: System - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -@pytest.mark.tier1 -def test_capsule_installer_verbose_stdout(): - """Look for Capsule installer verbose STDOUT - - :id: 323e85e3-2ad1-4018-aa35-1d51f1e7f5a2 - - :steps: - 1. Install capsule package. - 2. Run Satellite installer --scenario capsule - 3. Observe installer STDOUT. - - :expectedresults: - 1. Installer STDOUTs following groups hooks completion. - pre_migrations, boot, init, pre_values, pre_validations, pre_commit, pre, post - 2. Installer STDOUTs system configuration completion. - 3. Finally, Installer informs running capsule url, log file. - - :CaseLevel: System - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_installer_timestamp_logs(): - """Look for Satellite installer timestamp based logs - - :id: 9b4d32f6-d471-4bdb-8a79-9bb20ecb86aa - - :steps: - 1. Install satellite package. - 2. Run Satellite installer - 3. Observe installer log file `/var/log/foreman-installer/satellite.log`. - - :expectedresults: - 1. Installer logs satellite installation with timestamps in following format - YYYY-MM-DD HH:MM:SS - - :CaseLevel: System - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_positive_capsule_installer_and_register(): - """Verify the capsule installation and their registration with the satellite. - - :id: efd03442-5a08-445d-b257-e4d346084379 - - :steps: - 1. Install the satellite. - 2. Add all the required cdn and custom repositories in satellite to - install the capsule. - 3. Create life-cycle environment,content view and activation key. - 4. Subscribe the capsule with created activation key. - 5. Run 'yum update -y' on capsule. - 6. Run 'yum install -y satellite-capsule' on capsule. - 7. Create a certificate on satellite for new installed capsule. - 8. Copy capsule certificate from satellite to capsule. - 9. Run the satellite-installer(copy the satellite-installer command from step7'th - generated output) command on capsule to integrate the capsule with satellite. - 10. Check the newly added capsule is reflected in the satellite or not. - 11. Check the capsule sync. - - :expectedresults: - - 1. Capsule integrate successfully with satellite. - 2. Capsule sync should be worked properly. - - :CaseLevel: System - - :CaseAutomation: NotAutomated - """ - - @pytest.mark.stubbed @pytest.mark.tier3 def test_positive_satellite_installer_logfile_check(): From 254fb1e4c54b3b073212ea0d9a4768953d759e8d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 29 Sep 2023 17:00:27 -0400 Subject: [PATCH 251/586] [6.14.z] Workaround nonexistent auth source in upgrade template (#12749) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workaround nonexistent auth source in upgrade template (#12722) (cherry picked from commit 23d49f5634cfa7d1af2c131383d5fff5f4fb6d43) Co-authored-by: Lukáš Hellebrandt --- tests/foreman/api/test_user.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/foreman/api/test_user.py b/tests/foreman/api/test_user.py index 59e8ba515fc..cab7b49d653 100644 --- a/tests/foreman/api/test_user.py +++ b/tests/foreman/api/test_user.py @@ -733,7 +733,7 @@ def test_positive_ad_basic_no_roles(self, create_ldap): @pytest.mark.tier3 @pytest.mark.upgrade - def test_positive_access_entities_from_ldap_org_admin(self, create_ldap): + def test_positive_access_entities_from_ldap_org_admin(self, create_ldap, module_target_sat): """LDAP User can access resources within its taxonomies if assigned role has permission for same taxonomies @@ -751,6 +751,16 @@ def test_positive_access_entities_from_ldap_org_admin(self, create_ldap): :CaseLevel: System """ + # Workaround issue where, in an upgrade template, there is already + # some auth source present with this user. That auth source instance + # doesn't really run and attempting to login as that user + # leads to ISE 500 (which is itself a bug, the error should be handled, it is + # reported as BZ2240205). + for user in module_target_sat.api.User().search( + query={'search': f'login={create_ldap["ldap_user_name"]}'} + ): + user.delete() + role_name = gen_string('alpha') default_org_admin = entities.Role().search(query={'search': 'name="Organization admin"'}) org_admin = entities.Role(id=default_org_admin[0].id).clone( From 6a4df7cb26d9d19facc4f85d83ba00bd01e7cc16 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 2 Oct 2023 12:39:21 -0400 Subject: [PATCH 252/586] [6.14.z] Missing content ID test (#12784) --- tests/foreman/api/test_repository.py | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index bd63ad606cf..e6f03383dc2 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -1549,6 +1549,40 @@ def test_positive_sync_kickstart_check_os( ) assert len(os) + @pytest.mark.tier2 + @pytest.mark.parametrize( + 'repo_options', + **datafactory.parametrized( + {'yum': {'content_type': 'yum', 'unprotected': True, 'url': 'http://example.com'}} + ), + indirect=True, + ) + def test_missing_content_id(self, repo): + """Handle several cases of missing content ID correctly + + :id: f507790a-933b-4b3f-ac93-cade6967fbd2 + + :parametrized: yes + + :expectedresults: Repository URL can be set to something new and the repo can be deleted + + :BZ:2032040 + """ + # Wait for async metadata generate task to finish + time.sleep(5) + # Get rid of the URL + repo.url = '' + repo = repo.update(['url']) + assert repo.url is None + # Now change the URL back + repo.url = 'http://example.com' + repo = repo.update(['url']) + assert repo.url == 'http://example.com' + # Now delete the Repo + repo.delete() + with pytest.raises(HTTPError): + repo.read() + class TestDockerRepository: """Tests specific to using ``Docker`` repositories.""" From 904f359a9f52d68b7abee7499845365ceed03f44 Mon Sep 17 00:00:00 2001 From: Jitendra Yejare Date: Wed, 27 Sep 2023 20:40:51 +0530 Subject: [PATCH 253/586] Pytest verifies and log-in into vault (#12378) * Pytest verifies and logs in into vault * New vault lib utils for both * PR checks without vault login --- .github/workflows/pull_request.yml | 9 ++- conftest.py | 1 + pytest_plugins/auto_vault.py | 10 +++ robottelo/utils/__init__.py | 22 ------ robottelo/utils/vault.py | 114 +++++++++++++++++++++++++++++ scripts/vault_login.py | 87 ++-------------------- 6 files changed, 141 insertions(+), 102 deletions(-) create mode 100644 pytest_plugins/auto_vault.py create mode 100644 robottelo/utils/vault.py diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 582725d6e85..fc61b228cbc 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -46,18 +46,25 @@ jobs: - name: Collect Tests run: | + # To skip vault login in pull request checks + export VAULT_SECRET_ID_FOR_DYNACONF=somesecret pytest --collect-only --disable-pytest-warnings tests/foreman/ tests/robottelo/ pytest --collect-only --disable-pytest-warnings -m pre_upgrade tests/upgrades/ pytest --collect-only --disable-pytest-warnings -m post_upgrade tests/upgrades/ - name: Collect Tests with xdist run: | + # To skip vault login in pull request checks + export VAULT_SECRET_ID_FOR_DYNACONF=somesecret pytest --collect-only --setup-plan --disable-pytest-warnings -n 2 tests/foreman/ tests/robottelo/ pytest --collect-only --setup-plan --disable-pytest-warnings -n 2 -m pre_upgrade tests/upgrades/ pytest --collect-only --setup-plan --disable-pytest-warnings -n 2 -m post_upgrade tests/upgrades/ - name: Run Robottelo's Tests - run: pytest -sv tests/robottelo/ + run: | + # To skip vault login in pull request checks + export VAULT_SECRET_ID_FOR_DYNACONF=somesecret + pytest -sv tests/robottelo/ - name: Make Docs run: | diff --git a/conftest.py b/conftest.py index 91eb0d54ae9..c58f71c991b 100644 --- a/conftest.py +++ b/conftest.py @@ -3,6 +3,7 @@ pytest_plugins = [ # Plugins + 'pytest_plugins.auto_vault', 'pytest_plugins.disable_rp_params', 'pytest_plugins.external_logging', 'pytest_plugins.fixture_markers', diff --git a/pytest_plugins/auto_vault.py b/pytest_plugins/auto_vault.py new file mode 100644 index 00000000000..e63fc7f0835 --- /dev/null +++ b/pytest_plugins/auto_vault.py @@ -0,0 +1,10 @@ +"""Plugin enables pytest to notify and update the requirements""" +import subprocess + +from robottelo.utils.vault import Vault + + +def pytest_addoption(parser): + """Options to allow user to update the requirements""" + with Vault() as vclient: + vclient.login(stdout=subprocess.PIPE, stderr=subprocess.PIPE) diff --git a/robottelo/utils/__init__.py b/robottelo/utils/__init__.py index e7d0f1e94c6..0cf4020ca5a 100644 --- a/robottelo/utils/__init__.py +++ b/robottelo/utils/__init__.py @@ -1,34 +1,12 @@ # General utility functions which does not fit into other util modules OR # Independent utility functions that doesnt need separate module import base64 -import os -from pathlib import Path import re from cryptography.hazmat.backends import default_backend as crypto_default_backend from cryptography.hazmat.primitives import serialization as crypto_serialization from cryptography.hazmat.primitives.asymmetric import rsa -from robottelo.constants import Colored -from robottelo.exceptions import InvalidVaultURLForOIDC - - -def export_vault_env_vars(filename=None, envdata=None): - if not envdata: - envdata = Path(filename or '.env').read_text() - vaulturl = re.findall('VAULT_URL_FOR_DYNACONF=(.*)', envdata)[0] - - # Vault CLI Env Var - os.environ['VAULT_ADDR'] = vaulturl - - # Dynaconf Vault Env Vars - if re.findall('VAULT_ENABLED_FOR_DYNACONF=(.*)', envdata)[0] == 'true': - if 'localhost:8200' in vaulturl: - raise InvalidVaultURLForOIDC( - f"{Colored.REDDARK}{vaulturl} doesnt supports OIDC login," - "please change url to corp vault in env file!" - ) - def gen_ssh_keypairs(): """Generates private SSH key with its public key""" diff --git a/robottelo/utils/vault.py b/robottelo/utils/vault.py new file mode 100644 index 00000000000..a4b5d48adb4 --- /dev/null +++ b/robottelo/utils/vault.py @@ -0,0 +1,114 @@ +"""Hashicorp Vault Utils where vault CLI is wrapped to perform vault operations""" +import json +import os +import re +import subprocess +import sys + +from robottelo.exceptions import InvalidVaultURLForOIDC +from robottelo.logging import logger, robottelo_root_dir + + +class Vault: + + HELP_TEXT = ( + "Vault CLI in not installed in your system, " + "refer link https://learn.hashicorp.com/tutorials/vault/getting-started-install to " + "install vault CLI as per your system spec!" + ) + + def __init__(self, env_file='.env'): + self.env_path = robottelo_root_dir.joinpath(env_file) + + def setup(self): + self.export_vault_addr() + + def teardown(self): + del os.environ['VAULT_ADDR'] + + def export_vault_addr(self): + envdata = self.env_path.read_text() + vaulturl = re.findall('VAULT_URL_FOR_DYNACONF=(.*)', envdata)[0] + + # Set Vault CLI Env Var + os.environ['VAULT_ADDR'] = vaulturl + + # Dynaconf Vault Env Vars + if re.findall('VAULT_ENABLED_FOR_DYNACONF=(.*)', envdata)[0] == 'true': + if 'localhost:8200' in vaulturl: + raise InvalidVaultURLForOIDC( + f"{vaulturl} doesnt supports OIDC login," + "please change url to corp vault in env file!" + ) + + def exec_vault_command(self, command: str, **kwargs): + """A wrapper to execute the vault CLI commands + + :param comamnd str: The vault CLI command + :param kwargs dict: Arguments to the subprocess run command to customize the run behavior + """ + vcommand = subprocess.run(command, shell=True, **kwargs) # capture_output=True + if vcommand.returncode != 0: + verror = str(vcommand.stderr) + if vcommand.returncode == 127: + logger.error(f"Error! {self.HELP_TEXT}") + sys.exit(1) + if vcommand.stderr: + if 'Error revoking token' in verror: + logger.info("Token is alredy revoked!") + elif 'Error looking up token' in verror: + logger.warning("Warning! Vault not logged in!") + else: + logger.error(f"Error! {verror}") + return vcommand + + def login(self, **kwargs): + if 'VAULT_SECRET_ID_FOR_DYNACONF' not in os.environ: + if self.status(**kwargs).returncode != 0: + logger.warning( + "Warning! The browser is about to open for vault OIDC login, " + "close the tab once the sign-in is done!" + ) + if ( + self.exec_vault_command(command="vault login -method=oidc", **kwargs).returncode + == 0 + ): + self.exec_vault_command(command="vault token renew -i 10h", **kwargs) + logger.info("Success! Vault OIDC Logged-In and extended for 10 hours!") + # Fetching tokens + token = self.exec_vault_command( + "vault token lookup --format json", capture_output=True + ).stdout + token = json.loads(str(token.decode('UTF-8')))['data']['id'] + # Setting new token in env file + envdata = self.env_path.read_text() + envdata = re.sub( + '.*VAULT_TOKEN_FOR_DYNACONF=.*', f"VAULT_TOKEN_FOR_DYNACONF={token}", envdata + ) + self.env_path.write_text(envdata) + logger.info( + "Success! New OIDC token added to .env file to access secrets from vault!" + ) + + def logout(self): + # Teardown - Setting dymmy token in env file + envdata = self.env_path.read_text() + envdata = re.sub( + '.*VAULT_TOKEN_FOR_DYNACONF=.*', "# VAULT_TOKEN_FOR_DYNACONF=myroot", envdata + ) + self.env_path.write_text(envdata) + self.exec_vault_command('vault token revoke -self') + logger.info("Success! OIDC token removed from Env file successfully!") + + def status(self, **kwargs): + vstatus = self.exec_vault_command('vault token lookup', **kwargs) + if vstatus.returncode == 0: + logger.info(str(vstatus.stdout.decode('UTF-8'))) + return vstatus + + def __enter__(self): + self.setup() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.teardown() diff --git a/scripts/vault_login.py b/scripts/vault_login.py index 22d3313b270..300d81759a5 100755 --- a/scripts/vault_login.py +++ b/scripts/vault_login.py @@ -1,85 +1,14 @@ #!/usr/bin/env python # This Enables and Disables individuals OIDC token to access secrets from vault -import json -import os -from pathlib import Path -import re -import subprocess import sys -from robottelo.constants import Colored -from robottelo.utils import export_vault_env_vars - -HELP_TEXT = ( - "Vault CLI in not installed in your system, " - "refer link https://learn.hashicorp.com/tutorials/vault/getting-started-install to " - "install vault CLI as per your system spec!" -) - - -def _vault_command(command: str): - vcommand = subprocess.run(command, capture_output=True, shell=True) - if vcommand.returncode != 0: - verror = str(vcommand.stderr) - if vcommand.returncode == 127: - print(f"{Colored.REDDARK}Error! {HELP_TEXT}") - sys.exit(1) - elif 'Error revoking token' in verror: - print(f"{Colored.GREEN}Token is alredy revoked!") - sys.exit(0) - elif 'Error looking up token' in verror: - print(f"{Colored.YELLOW}Warning! Vault not logged in, please run 'make vault-login'!") - sys.exit(2) - else: - print(f"{Colored.REDDARK}Error! {verror}") - sys.exit(1) - return vcommand - - -def _vault_login(root_path, envdata): - print( - f"{Colored.WHITELIGHT}Warning! The browser is about to open for vault OIDC login, " - "close the tab once the sign-in is done!" - ) - if _vault_command(command="vault login -method=oidc").returncode == 0: - _vault_command(command="vault token renew -i 10h") - print(f"{Colored.GREEN}Success! Vault OIDC Logged-In and extended for 10 hours!") - # Fetching token - token = _vault_command("vault token lookup --format json").stdout - token = json.loads(str(token.decode('UTF-8')))['data']['id'] - # Setting new token in env file - envdata = re.sub('.*VAULT_TOKEN_FOR_DYNACONF=.*', f"VAULT_TOKEN_FOR_DYNACONF={token}", envdata) - with open(root_path, 'w') as envfile: - envfile.write(envdata) - print( - f"{Colored.GREEN}Success! New OIDC token added to .env file to access secrets from vault!" - ) - - -def _vault_logout(root_path, envdata): - # Teardown - Setting dymmy token in env file - envdata = re.sub('.*VAULT_TOKEN_FOR_DYNACONF=.*', "# VAULT_TOKEN_FOR_DYNACONF=myroot", envdata) - with open(root_path, 'w') as envfile: - envfile.write(envdata) - _vault_command('vault token revoke -self') - print(f"{Colored.GREEN}Success! OIDC token removed from Env file successfully!") - - -def _vault_status(): - vstatus = _vault_command('vault token lookup') - if vstatus.returncode == 0: - print(str(vstatus.stdout.decode('UTF-8'))) - +from robottelo.utils.vault import Vault if __name__ == '__main__': - root_path = Path('.env') - envdata = root_path.read_text() - export_vault_env_vars(envdata=envdata) - if sys.argv[-1] == '--login': - _vault_login(root_path, envdata) - elif sys.argv[-1] == '--status': - _vault_status() - else: - _vault_logout(root_path, envdata) - # Unsetting VAULT URL - del os.environ['VAULT_ADDR'] + with Vault() as vclient: + if sys.argv[-1] == '--login': + vclient.login() + elif sys.argv[-1] == '--status': + vclient.status() + else: + vclient.logout() From 1ef2ac0731454123a210d8b8a9aa0c73c653b8b6 Mon Sep 17 00:00:00 2001 From: jyejare Date: Tue, 3 Oct 2023 16:08:39 +0530 Subject: [PATCH 254/586] Pull Request Target for Dependency merges in zStream --- .github/workflows/dependency_merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency_merge.yml b/.github/workflows/dependency_merge.yml index 9bd40e645a1..4cf94b8255e 100644 --- a/.github/workflows/dependency_merge.yml +++ b/.github/workflows/dependency_merge.yml @@ -1,6 +1,6 @@ name: Dependabot Auto Merge - ZStream on: - pull_request: + pull_request_target: branches-ignore: - master From eb328390b1de87da440f9e3f2279ab6b564cfc72 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 3 Oct 2023 10:03:54 -0400 Subject: [PATCH 255/586] [6.14.z] Add test using verify checksum on non yum repo in hammer (#12795) --- tests/foreman/cli/test_repository.py | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/foreman/cli/test_repository.py b/tests/foreman/cli/test_repository.py index 42fd08f4f8a..85a9d751bd4 100644 --- a/tests/foreman/cli/test_repository.py +++ b/tests/foreman/cli/test_repository.py @@ -913,6 +913,41 @@ def test_positive_synchronize_docker_repo(self, repo, module_product, module_org ] ) + @pytest.mark.tier2 + @pytest.mark.upgrade + @pytest.mark.parametrize( + 'repo_options', + **parametrized( + [ + { + 'content-type': 'docker', + 'docker-upstream-name': CONTAINER_UPSTREAM_NAME, + 'name': valid_docker_repository_names()[0], + 'url': CONTAINER_REGISTRY_HUB, + } + ] + ), + indirect=True, + ) + def test_verify_checksum_container_repo(self, repo): + """Check if Verify Content Checksum can be run on non container repos + + :id: c8f0eb45-3cb6-41b2-aad9-52ac847d7bf8 + + :parametrized: yes + + :expectedresults: Docker repository is created, and can be synced with + validate-contents set to True + + :BZ: 2161209 + + :customerscenario: true + """ + assert repo['sync']['status'] == 'Not Synced' + Repository.synchronize({'id': repo['id'], 'validate-contents': 'true'}) + new_repo = Repository.info({'id': repo['id']}) + assert new_repo['sync']['status'] == 'Success' + @pytest.mark.tier2 @pytest.mark.upgrade @pytest.mark.parametrize( From 394c6f63bdc4a0e2e9173ade3b5ed3405647429b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 3 Oct 2023 13:04:37 -0400 Subject: [PATCH 256/586] [6.14.z] Add customer coverage to health check (#12799) --- tests/foreman/maintain/test_health.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/foreman/maintain/test_health.py b/tests/foreman/maintain/test_health.py index c1d590546ec..0dd4320d633 100644 --- a/tests/foreman/maintain/test_health.py +++ b/tests/foreman/maintain/test_health.py @@ -96,11 +96,17 @@ def test_positive_health_check(sat_maintain): 1. Run satellite-maintain health check :expectedresults: Health check should pass. + + :BZ: 1956210 + + :customerscenario: true """ result = sat_maintain.cli.Health.check(options={'assumeyes': True}) assert result.status == 0 if 'paused tasks in the system' not in result.stdout: assert 'FAIL' not in result.stdout + result = sat_maintain.execute('tail /var/log/foreman-proxy/proxy.log') + assert 'sslv3 alert bad certificate' not in result.stdout @pytest.mark.include_capsule From 0752adecd3c38ba9d49643ad1672a56a15c2a1f7 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 3 Oct 2023 13:20:44 -0400 Subject: [PATCH 257/586] [6.14.z] Add coverage for 1964037 installer cert regeneration (#12802) --- robottelo/cli/base.py | 12 +++++++- tests/foreman/destructive/test_installer.py | 34 +++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/robottelo/cli/base.py b/robottelo/cli/base.py index 4f66a05a089..d5ce2ae224f 100644 --- a/robottelo/cli/base.py +++ b/robottelo/cli/base.py @@ -119,6 +119,16 @@ def add_operating_system(cls, options=None): return cls.execute(cls._construct_command(options)) + @classmethod + def ping(cls, options=None): + """ + Display status of Satellite. + """ + + cls.command_sub = 'ping' + + return cls.execute(cls._construct_command(options)) + @classmethod def create(cls, options=None, timeout=None): """ @@ -419,6 +429,6 @@ def _construct_command(cls, options=None): if isinstance(val, list): val = ','.join(str(el) for el in val) tail += f' --{key}="{val}"' - cmd = f"{cls.command_base} {cls.command_sub or ''} {tail.strip()} {cls.command_end or ''}" + cmd = f"{cls.command_base or ''} {cls.command_sub or ''} {tail.strip()} {cls.command_end or ''}" return cmd diff --git a/tests/foreman/destructive/test_installer.py b/tests/foreman/destructive/test_installer.py index 19459340458..aa683870591 100644 --- a/tests/foreman/destructive/test_installer.py +++ b/tests/foreman/destructive/test_installer.py @@ -20,6 +20,7 @@ import pytest from robottelo.config import settings +from robottelo.constants import SATELLITE_ANSWER_FILE from robottelo.utils.installer import InstallerCommand pytestmark = pytest.mark.destructive @@ -154,3 +155,36 @@ def test_positive_mismatched_satellite_fqdn(target_sat, set_random_fqdn): ) installer_command_output = target_sat.execute('satellite-installer').stderr assert warning_message in str(installer_command_output) + + +def test_positive_installer_certs_regenerate(target_sat): + """Ensure "satellite-installer --certs-regenerate true" command correctly generates + /etc/tomcat/cert-users.properties after editing answers file + + :id: db6152c3-4459-425b-998d-4a7992ca1f72 + + :steps: + 1. Update /etc/foreman-installer/scenarios.d/satellite-answers.yaml + 2. Fill some empty strings in certs category for 'state' + 3. Run satellite-installer --certs-regenerate true + 4. hammer ping + + :expectedresults: Correct generation of /etc/tomcat/cert-users.properties + + :BZ: 1964037 + + :customerscenario: true + """ + target_sat.execute(f'sed -i "s/state: North Carolina/state: \'\'/g" {SATELLITE_ANSWER_FILE}') + result = target_sat.execute(f'grep state: {SATELLITE_ANSWER_FILE}') + assert "state: ''" in result.stdout + result = target_sat.install( + InstallerCommand( + 'certs-update-all', + 'certs-update-server', + 'certs-update-server-ca', + certs_regenerate=['true'], + ) + ) + assert result.status == 0 + assert 'FAIL' not in target_sat.cli.Base.ping() From 3f27ab30eb65ffb28dd589341c623b0eae185272 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 3 Oct 2023 13:38:48 -0400 Subject: [PATCH 258/586] [6.14.z] Bump broker[docker] from 0.4.0 to 0.4.1 (#12790) Bump broker[docker] from 0.4.0 to 0.4.1 (#12786) Bumps [broker[docker]](https://github.com/SatelliteQE/broker) from 0.4.0 to 0.4.1. - [Release notes](https://github.com/SatelliteQE/broker/releases) - [Commits](https://github.com/SatelliteQE/broker/compare/0.4.0...0.4.1) --- updated-dependencies: - dependency-name: broker[docker] dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit a07594fccce9374c3653bc9e7e7a5e14914cebf3) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d5a6db921a7..e6f08c8262c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # Version updates managed by dependabot betelgeuse==1.10.0 -broker[docker]==0.4.0 +broker[docker]==0.4.1 cryptography==41.0.4 deepdiff==6.5.0 dynaconf[vault]==3.2.3 From 151c522315a45d266d2b0fefd36d07a46d976451 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 4 Oct 2023 03:03:09 -0400 Subject: [PATCH 259/586] [6.14.z] subscription page ui failure fix in robottelo tests (#12805) subscription page ui failure fix in robottelo tests (cherry picked from commit 592a22edb0bd294eccba6a98610ab940d20c3aec) Co-authored-by: vijaysawant --- tests/foreman/ui/test_subscription.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/tests/foreman/ui/test_subscription.py b/tests/foreman/ui/test_subscription.py index 674b91d7401..617c9753403 100644 --- a/tests/foreman/ui/test_subscription.py +++ b/tests/foreman/ui/test_subscription.py @@ -96,14 +96,8 @@ def test_positive_end_to_end(session, target_sat): """ expected_message_lines = [ 'Are you sure you want to delete the manifest?', - 'Note: Deleting a subscription manifest is STRONGLY discouraged. ' - 'Deleting a manifest will:', - 'Delete all subscriptions that are attached to running hosts.', - 'Delete all subscriptions attached to activation keys.', - 'Disable Red Hat Insights.', - 'Require you to upload the subscription-manifest and re-attach ' - 'subscriptions to hosts and activation keys.', - 'This action should only be taken in extreme circumstances or for debugging purposes.', + 'Note: Deleting a subscription manifest is STRONGLY discouraged.', + 'This action should only be taken for debugging purposes.', ] org = entities.Organization().create() _, temporary_local_manifest_path = mkstemp(prefix='manifest-', suffix='.zip') @@ -121,14 +115,11 @@ def test_positive_end_to_end(session, target_sat): ignore_error_messages=['Danger alert: Katello::Errors::UpstreamConsumerNotFound'], ) assert session.subscription.has_manifest - # dashboard check - subscription_values = session.dashboard.read('SubscriptionStatus')['subscriptions'] - assert subscription_values[0]['Subscription Status'] == 'Active Subscriptions' - assert int(subscription_values[0]['Count']) >= 1 - assert subscription_values[1]['Subscription Status'] == 'Subscriptions Expiring in 120 Days' - assert int(subscription_values[1]['Count']) == 0 - assert subscription_values[2]['Subscription Status'] == 'Recently Expired Subscriptions' - assert int(subscription_values[2]['Count']) == 0 + subscriptions = session.subscription.read_subscriptions() + assert len(subscriptions) >= 1 + assert any('Red Hat' in subscription['Name'] for subscription in subscriptions) + assert int(subscriptions[0]['Entitlements']) > 0 + assert int(subscriptions[0]['Consumed']) >= 0 # manifest delete testing delete_message = session.subscription.read_delete_manifest_message() assert ' '.join(expected_message_lines) == delete_message From 0b618187a5e848c2eaea32322cb58894292bcb58 Mon Sep 17 00:00:00 2001 From: Jitendra Yejare Date: Wed, 4 Oct 2023 13:15:04 +0530 Subject: [PATCH 260/586] [6.14.z] Pytest verifies and log-in into vault (#12766) Pytest verifies and log-in into vault (#12378) * Pytest verifies and logs in into vault * New vault lib utils for both * PR checks without vault login From 46681a062992f49b9c98a4143bb09f269667fee9 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 4 Oct 2023 04:08:43 -0400 Subject: [PATCH 261/586] [6.14.z] Add coverage for 2055790 to service enable disable (#12803) Add coverage for 2055790 to service enable disable (#12756) * Adding wait for to service enable test * Add coverage for 2055790 to enable disable (cherry picked from commit 89b779b664e521b87b5ee999424c205369df965b) Co-authored-by: Griffin Sullivan <48397354+Griffin-Sullivan@users.noreply.github.com> --- tests/foreman/maintain/test_service.py | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/foreman/maintain/test_service.py b/tests/foreman/maintain/test_service.py index 99bb6074ae5..5e9f24c6510 100644 --- a/tests/foreman/maintain/test_service.py +++ b/tests/foreman/maintain/test_service.py @@ -18,6 +18,7 @@ """ from fauxfactory import gen_string import pytest +from wait_for import wait_for from robottelo.config import settings from robottelo.constants import ( @@ -173,6 +174,10 @@ def test_positive_service_enable_disable(sat_maintain): 2. Run satellite-maintain service enable :expectedresults: Services should enable/disable + + :BZ: 1995783, 2055790 + + :customerscenario: true """ result = sat_maintain.cli.Service.stop() assert 'FAIL' not in result.stdout @@ -183,6 +188,28 @@ def test_positive_service_enable_disable(sat_maintain): result = sat_maintain.cli.Service.enable() assert 'FAIL' not in result.stdout assert result.status == 0 + sat_maintain.power_control(state='reboot') + if isinstance(sat_maintain, Satellite): + result, _ = wait_for( + sat_maintain.cli.Service.status, + func_kwargs={'options': {'brief': True, 'only': 'foreman.service'}}, + fail_condition=lambda res: "FAIL" in res.stdout, + handle_exception=True, + delay=30, + timeout=300, + ) + assert 'FAIL' not in sat_maintain.cli.Base.ping() + else: + result, _ = wait_for( + sat_maintain.cli.Service.status, + func_kwargs={'options': {'brief': True}}, + fail_condition=lambda res: "FAIL" in res.stdout, + handle_exception=True, + delay=30, + timeout=300, + ) + assert 'FAIL' not in result.stdout + assert result.status == 0 @pytest.mark.usefixtures('start_satellite_services') From 36f7756cc1f97a274fc77cf5cf76b65fec7f1bda Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 4 Oct 2023 08:38:40 -0400 Subject: [PATCH 262/586] [6.14.z] Bump redis from 5.0.0 to 5.0.1 (#12807) Bump redis from 5.0.0 to 5.0.1 (#12746) Bumps [redis](https://github.com/redis/redis-py) from 5.0.0 to 5.0.1. - [Release notes](https://github.com/redis/redis-py/releases) - [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES) - [Commits](https://github.com/redis/redis-py/compare/v5.0.0...v5.0.1) --- updated-dependencies: - dependency-name: redis dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit d16f5f682fcb904d7d6c0ba09326400e825909d2) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 03665f3e3d8..6da8ffd5441 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -1,7 +1,7 @@ # For running tests and checking code quality using these modules. flake8==6.1.0 pytest-cov==4.1.0 -redis==5.0.0 +redis==5.0.1 pre-commit==3.4.0 # For generating documentation. From ea0b0356d53afcb9a22b72c31dbe11354b82d9cd Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 4 Oct 2023 09:15:28 -0400 Subject: [PATCH 263/586] [6.14.z] Add test for satellite-maintain's self updating ability (#12812) --- tests/foreman/maintain/test_upgrade.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/foreman/maintain/test_upgrade.py b/tests/foreman/maintain/test_upgrade.py index bc041ee4601..a087bda028f 100644 --- a/tests/foreman/maintain/test_upgrade.py +++ b/tests/foreman/maintain/test_upgrade.py @@ -165,27 +165,31 @@ def test_negative_pre_upgrade_tuning_profile_check(request, custom_host): ) -@pytest.mark.stubbed @pytest.mark.include_capsule -def test_positive_self_update_for_zstream(sat_maintain): - """Test satellite-maintain self-upgrade to update maintain packages from zstream repo. +def test_positive_self_update_maintain_package(sat_maintain): + """satellite-maintain attempts to update itself when a command is run :id: 1c566768-fd73-4fe6-837b-26709a1ebed9 :parametrized: yes :steps: - 1. Run satellite-maintain upgrade check/run command. - 2. Run satellite-maintain upgrade check/run command with disable-self-upgrade option. + 1. Run satellite-maintain upgrade list-versions/check/run command. + 2. Run satellite-maintain upgrade list-versions/check/run command + with disable-self-upgrade option. :expectedresults: - 1. Update satellite-maintain package to latest version and gives message to re-run command. - 2. If disable-self-upgrade option is used then it should skip self-upgrade step for zstream + 1. satellite-maintain tries to update rubygem-foreman_maintain to a newer available version + 2. If disable-self-upgrade option is used then it should skip self-upgrade step :BZ: 1649329 - - :CaseAutomation: ManualOnly """ + result = sat_maintain.cli.Upgrade.list_versions() + assert result.status == 0 + assert 'Checking for new version of satellite-maintain...' in result.stdout + result = sat_maintain.cli.Upgrade.list_versions(options={'disable-self-upgrade': True}) + assert result.status == 0 + assert 'Checking for new version of satellite-maintain...' not in result.stdout @pytest.mark.stubbed From 79ff79cbf5c01e0fd9ba71182b40c23cb796dbaf Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 6 Oct 2023 08:33:27 -0400 Subject: [PATCH 264/586] [6.14.z] Bump deepdiff from 6.5.0 to 6.6.0 (#12821) Bump deepdiff from 6.5.0 to 6.6.0 (#12818) Bumps [deepdiff](https://github.com/seperman/deepdiff) from 6.5.0 to 6.6.0. - [Release notes](https://github.com/seperman/deepdiff/releases) - [Changelog](https://github.com/seperman/deepdiff/blob/master/docs/changelog.rst) - [Commits](https://github.com/seperman/deepdiff/commits/6.6.0) --- updated-dependencies: - dependency-name: deepdiff dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit 2a138f8e8908d0ff41b49f600eae34bfff95c2e0) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e6f08c8262c..bca8b41f01a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ betelgeuse==1.10.0 broker[docker]==0.4.1 cryptography==41.0.4 -deepdiff==6.5.0 +deepdiff==6.6.0 dynaconf[vault]==3.2.3 fauxfactory==3.1.0 jinja2==3.1.2 From df3a5ae013dfac3948cfe958c99764b2261c32dd Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 6 Oct 2023 08:40:11 -0400 Subject: [PATCH 265/586] [6.14.z] Audit - Adding coverage for Discovery provisioning via CLI (#12826) Audit - Adding coverage for Discovery provisioning via CLI (#12467) * Adding coverage for Discovery provisioning via CLI Signed-off-by: Adarsh Dubey * Adding finalizer --------- Signed-off-by: Adarsh Dubey (cherry picked from commit 272a3aafb06da6bdc093da6b1e2791385d1f7e1d) Co-authored-by: Adarsh dubey --- robottelo/cli/discoveredhost.py | 22 ++- tests/foreman/cli/test_discoveredhost.py | 202 ++++++++++++++--------- 2 files changed, 143 insertions(+), 81 deletions(-) diff --git a/robottelo/cli/discoveredhost.py b/robottelo/cli/discoveredhost.py index e2fb2bf65c9..6404dfc89ec 100644 --- a/robottelo/cli/discoveredhost.py +++ b/robottelo/cli/discoveredhost.py @@ -31,10 +31,28 @@ class DiscoveredHost(Base): def provision(cls, options=None): """Manually provision discovered host""" cls.command_sub = 'provision' - return cls.execute(cls._construct_command(options)) + return cls.execute(cls._construct_command(options), output_format='csv') @classmethod def facts(cls, options=None): """Get all the facts associated with discovered host""" cls.command_sub = 'facts' - return cls.execute(cls._construct_command(options)) + return cls.execute(cls._construct_command(options), output_format='csv') + + @classmethod + def auto_provision(cls, options=None): + """Auto provision discovered host""" + cls.command_sub = 'auto-provision' + return cls.execute(cls._construct_command(options), output_format='csv') + + @classmethod + def reboot(cls, options=None): + """Reboot discovered host""" + cls.command_sub = 'reboot' + return cls.execute(cls._construct_command(options), output_format='csv') + + @classmethod + def refresh_facts(cls, options=None): + """Refresh facts associated with discovered host""" + cls.command_sub = 'refresh-facts' + return cls.execute(cls._construct_command(options), output_format='csv') diff --git a/tests/foreman/cli/test_discoveredhost.py b/tests/foreman/cli/test_discoveredhost.py index ac138e66858..031116a2e09 100644 --- a/tests/foreman/cli/test_discoveredhost.py +++ b/tests/foreman/cli/test_discoveredhost.py @@ -14,50 +14,145 @@ :Upstream: No """ -from time import sleep - import pytest - -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.discoveredhost import DiscoveredHost +from wait_for import wait_for pytestmark = [pytest.mark.run_in_one_thread] -def _assertdiscoveredhost(hostname): - """Check if host is discovered and information about it can be - retrieved back +@pytest.mark.e2e +@pytest.mark.on_premises_provisioning +@pytest.mark.parametrize('module_provisioning_sat', ['discovery'], indirect=True) +@pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) +@pytest.mark.rhel_ver_match('8') +def test_rhel_pxe_discovery_provisioning( + module_provisioning_rhel_content, + module_discovery_sat, + provisioning_host, + provisioning_hostgroup, + request, +): + """Provision a PXE-based discovered host - Introduced a delay of 300secs by polling every 10 secs to get expected - host - """ - for _ in range(30): - try: - discovered_host = DiscoveredHost.info({'name': hostname}) - except CLIReturnCodeError: - sleep(10) - continue - return discovered_host + :id: b32a3b05-86bc-4ba6-ab6c-22b2f81e4315 + :parametrized: yes -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_positive_pxe_based_discovery(): - """Discover a host via PXE boot by setting "proxy.type=proxy" in - PXE default - - :id: 25e935fe-18f4-477e-b791-7ea5a395b4f6 + :Setup: Satellite with Provisioning and Discovery features configured - :Setup: Provisioning should be configured + :Steps: - :Steps: PXE boot a host/VM + 1. Boot up the host to discover + 2. Provision the host - :expectedresults: Host should be successfully discovered + :expectedresults: Host should be successfully discovered and provisioned :CaseImportance: Critical :BZ: 1731112 """ + sat = module_discovery_sat.sat + provisioning_host.power_control(ensure=False) + mac = provisioning_host._broker_args['provisioning_nic_mac_addr'] + + wait_for( + lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], + timeout=240, + delay=20, + ) + discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] + discovered_host.hostgroup = provisioning_hostgroup + discovered_host.location = provisioning_hostgroup.location[0] + discovered_host.organization = provisioning_hostgroup.organization[0] + discovered_host.build = True + result = sat.cli.DiscoveredHost.provision( + { + 'id': discovered_host.id, + 'hostgroup-id': discovered_host.hostgroup.id, + 'organization-id': discovered_host.organization.id, + 'location-id': discovered_host.location.id, + } + ) + # teardown + @request.addfinalizer + def _finalize(): + host.delete() + assert not sat.api.Host().search(query={"search": f'name={host.name}'}) + + assert 'Host created' in result[0]['message'] + host = sat.api.Host().search(query={"search": f'id={discovered_host.id}'})[0] + assert host + wait_for( + lambda: host.read().build_status_label != 'Pending installation', + timeout=1500, + delay=10, + ) + assert host.read().build_status_label == 'Installed' + assert not sat.api.DiscoveredHost().search(query={'mac': mac}) + + +@pytest.mark.e2e +@pytest.mark.on_premises_provisioning +@pytest.mark.parametrize('module_provisioning_sat', ['discovery'], indirect=True) +@pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) +@pytest.mark.rhel_ver_match('8') +def test_rhel_pxeless_discovery_provisioning( + module_discovery_sat, + pxeless_discovery_host, + module_provisioning_rhel_content, + provisioning_hostgroup, + request, +): + """Provision a PXE-less discovered host + + :id: e75ee13a-9edc-4182-b02a-6b106a459751 + + :Setup: Provisioning should be configured and a host should be + discovered via cli + + :expectedresults: Host should be provisioned successfully + + :CaseImportance: Critical + """ + sat = module_discovery_sat.sat + pxeless_discovery_host.power_control(ensure=False) + mac = pxeless_discovery_host._broker_args['provisioning_nic_mac_addr'] + + wait_for( + lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], + timeout=240, + delay=20, + ) + discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] + discovered_host.hostgroup = provisioning_hostgroup + discovered_host.location = provisioning_hostgroup.location[0] + discovered_host.organization = provisioning_hostgroup.organization[0] + discovered_host.build = True + result = sat.cli.DiscoveredHost.provision( + { + 'id': discovered_host.id, + 'hostgroup-id': discovered_host.hostgroup.id, + 'organization-id': discovered_host.organization.id, + 'location-id': discovered_host.location.id, + } + ) + + # teardown + @request.addfinalizer + def _finalize(): + host.delete() + assert not sat.api.Host().search(query={"search": f'name={host.name}'}) + + assert 'Host created' in result[0]['message'] + host = sat.api.Host().search(query={"search": f'id={discovered_host.id}'})[0] + assert host + wait_for( + lambda: host.read().build_status_label != 'Pending installation', + timeout=1500, + delay=10, + ) + assert host.read().build_status_label == 'Installed' + assert not sat.api.DiscoveredHost().search(query={'mac': mac}) @pytest.mark.stubbed @@ -149,57 +244,6 @@ def test_positive_provision_pxe_host_with_bios_syslinux(): """ -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_positive_provision_pxe_host_with_uefi_grub2(): - """Provision the pxe-based UEFI discovered host from cli using PXEGRUB2 - loader - - :id: 0002af1b-6f4b-40e2-8f2f-343387be6f72 - - :Setup: - 1. Create an UEFI VM and set it to boot from a network - 2. Synchronize RHEL7 kickstart repo (rhel6 kernel too old for GRUB) - 3. for getting more detailed info from FDI, remaster the image to - have ssh enabled - - :steps: - 1. Build a default PXE template - 2. Run assertion step #1 - 3. Boot the VM (from NW) - 4. Run assertion steps #2-4 - 5. Provision the discovered host - 6. Run assertion steps #5-9 - - :expectedresults: Host should be provisioned successfully - 1. Ensure the tftpboot files are updated - - 1.1 Ensure fdi-image files have been placed under tftpboot/boot/ - 1.2 Ensure the 'default' pxelinux config has been placed under - tftpboot/pxelinux.cfg/ - 1.3 Ensure the discovery section exists inside pxelinux config, - it leads to the FDI kernel and the ONTIMEOUT is set to discovery - - 2. Ensure PXE handoff goes as expected (tcpdump -p tftp) - 3. Ensure FDI loaded and successfully sent out facts - - 3.1 ping vm - 3.2 ssh to the VM and read the logs (if ssh enabled) - 3.3 optionally sniff the HTTP traffic coming from the host - - 4. Ensure host appeared in Discovered Hosts on satellite - 5. Ensure the tftpboot files are updated for the hosts mac - 6. Ensure PXE handoff goes as expected (tcpdump -p tftp) - 7. Optionally ensure anaconda loaded and the installation finished - 8. Ensure the host is provisioned with correct attributes - 9. Ensure the entry from discovered host list disappeared - - :CaseAutomation: NotAutomated - - :CaseImportance: High - """ - - @pytest.mark.stubbed @pytest.mark.tier3 def test_positive_delete(): From b75d0b5e58d23f092c38ad6fb67c56cb50ee192e Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 6 Oct 2023 09:16:38 -0400 Subject: [PATCH 266/586] [6.14.z] Fix capsule test by CVV sort (#12830) Fix capsule test by CVV sort (cherry picked from commit 35252d16b824cd4f1313d944410ec5089408dfbb) Co-authored-by: Vladimir Sedmik --- tests/foreman/api/test_capsulecontent.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/foreman/api/test_capsulecontent.py b/tests/foreman/api/test_capsulecontent.py index 120b0c0354e..cdc388fd149 100644 --- a/tests/foreman/api/test_capsulecontent.py +++ b/tests/foreman/api/test_capsulecontent.py @@ -297,6 +297,7 @@ def test_positive_sync_updated_repo( cv = cv.read() assert len(cv.version) == 2 + cv.version.sort(key=lambda version: version.id) cvv = cv.version[-1].read() cvv.promote(data={'environment_ids': function_lce.id}) cvv = cvv.read() From 881d391e667e50eb696e1aa3d0294d32b7517eb7 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 6 Oct 2023 18:43:35 -0400 Subject: [PATCH 267/586] [6.14.z] Update VMware CR tests (#12835) Update VMware CR tests (#12773) Update VMware tests Signed-off-by: Shubham Ganar (cherry picked from commit 8a3f8a40066899739913cc0f43ce113f13da1cdf) Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> --- conf/vmware.yaml.template | 36 ++- conftest.py | 1 + pytest_fixtures/component/provision_vmware.py | 59 +++++ robottelo/constants/__init__.py | 6 +- .../cli/test_computeresource_vmware.py | 231 ++++++++--------- .../test_provisioning_computeresource.py | 239 ------------------ .../foreman/ui/test_computeresource_vmware.py | 163 +++++------- 7 files changed, 253 insertions(+), 482 deletions(-) create mode 100644 pytest_fixtures/component/provision_vmware.py delete mode 100644 tests/foreman/longrun/test_provisioning_computeresource.py diff --git a/conf/vmware.yaml.template b/conf/vmware.yaml.template index 72b333cded6..27cbeadf69f 100644 --- a/conf/vmware.yaml.template +++ b/conf/vmware.yaml.template @@ -1,33 +1,31 @@ VMWARE: - # Vmware to be added as a compute resource - # vmware vcenter URL, e.g. example.com + # VMware to be added as a compute resource + # VCENTER: vmware vcenter URL VCENTER: - # Login for vmware + # USERNAME: Login for vmware USERNAME: - # Password for vmware + # PASSWORD: Password for vmware PASSWORD: - # vmware datacenter + # DATACENTER: vmware datacenter DATACENTER: - # cluster: vmware cluster + # CLUSTER: vmware cluster CLUSTER: - # Name of VM that should be used + # DATASTORE: vmware datastore + DATASTORE: + # VM_NAME: Name of VM to power On/Off & delete VM_NAME: - # mac_address: Mac address of the vm + # MAC_ADDRESS: Mac address of the vm MAC_ADDRESS: - # hypervisor: IP address of the hypervisor + # HYPERVISOR: IP address or hostname of the hypervisor HYPERVISOR: - - - # Vmware Compute resource image data - # Operating system of the image + # VMware Compute resource image data + # IMAGE_OS: Operating system of the image IMAGE_OS: - # Architecture of the image + # IMAGE_ARCH: Architecture of the image IMAGE_ARCH: - # Login to the image + # IMAGE_USERNAME: Login to the image IMAGE_USERNAME: - # Password of that user + # IMAGE_PASSWORD: Password to the image IMAGE_PASSWORD: - # Image name on the external provider + # IMAGE_NAME: Image name on the external provider IMAGE_NAME: - # Interface used for some tests; not required to work with provisioning, not required to be in VLAN - INTERFACE: diff --git a/conftest.py b/conftest.py index c58f71c991b..eb8b21a9aa6 100644 --- a/conftest.py +++ b/conftest.py @@ -52,6 +52,7 @@ 'pytest_fixtures.component.provision_gce', 'pytest_fixtures.component.provision_libvirt', 'pytest_fixtures.component.provision_pxe', + 'pytest_fixtures.component.provision_vmware', 'pytest_fixtures.component.provisioning_template', 'pytest_fixtures.component.puppet', 'pytest_fixtures.component.repository', diff --git a/pytest_fixtures/component/provision_vmware.py b/pytest_fixtures/component/provision_vmware.py new file mode 100644 index 00000000000..4517e9fc904 --- /dev/null +++ b/pytest_fixtures/component/provision_vmware.py @@ -0,0 +1,59 @@ +from fauxfactory import gen_string +import pytest + +from robottelo.config import settings + + +@pytest.fixture(scope='module') +def module_vmware_cr(module_provisioning_sat, module_sca_manifest_org, module_location): + vmware_cr = module_provisioning_sat.sat.api.VMWareComputeResource( + name=gen_string('alpha'), + provider='Vmware', + url=settings.vmware.vcenter, + user=settings.vmware.username, + password=settings.vmware.password, + datacenter=settings.vmware.datacenter, + organization=[module_sca_manifest_org], + location=[module_location], + ).create() + return vmware_cr + + +@pytest.fixture +def module_vmware_hostgroup( + module_vmware_cr, + module_provisioning_sat, + module_sca_manifest_org, + module_location, + default_architecture, + module_provisioning_rhel_content, + module_lce_library, + default_partitiontable, + module_provisioning_capsule, + pxe_loader, +): + return module_provisioning_sat.sat.api.HostGroup( + name=gen_string('alpha'), + organization=[module_sca_manifest_org], + location=[module_location], + architecture=default_architecture, + domain=module_provisioning_sat.domain, + content_source=module_provisioning_capsule.id, + content_view=module_provisioning_rhel_content.cv, + compute_resource=module_vmware_cr, + kickstart_repository=module_provisioning_rhel_content.ksrepo, + lifecycle_environment=module_lce_library, + root_pass=settings.provisioning.host_root_password, + operatingsystem=module_provisioning_rhel_content.os, + ptable=default_partitiontable, + subnet=module_provisioning_sat.subnet, + pxe_loader=pxe_loader.pxe_loader, + group_parameters_attributes=[ + # assign AK in order the hosts to be subscribed + { + 'name': 'kt_activation_keys', + 'parameter_type': 'string', + 'value': module_provisioning_rhel_content.ak.name, + }, + ], + ).create() diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index f8ff046e03f..079057b2ebc 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -1725,17 +1725,15 @@ class Colored(Box): STRING_TYPES = ['alpha', 'numeric', 'alphanumeric', 'latin1', 'utf8', 'cjk', 'html'] VMWARE_CONSTANTS = { - 'cluster': 'Satellite-Engineering', 'folder': 'vm', - 'guest_os': 'Red Hat Enterprise Linux 7 (64-bit)', + 'guest_os': 'Red Hat Enterprise Linux 8 (64-bit)', 'scsicontroller': 'LSI Logic Parallel', 'virtualhw_version': 'Default', 'pool': 'Resources', 'network_interface_name': 'VMXNET 3', - 'datastore': 'Local-Ironforge', - 'network_interfaces': 'qe_%s', } + HAMMER_CONFIG = "~/.hammer/cli.modules.d/foreman.yml" HAMMER_SESSIONS = "~/.hammer/sessions" diff --git a/tests/foreman/cli/test_computeresource_vmware.py b/tests/foreman/cli/test_computeresource_vmware.py index ed1164f063c..84ebc98af7c 100644 --- a/tests/foreman/cli/test_computeresource_vmware.py +++ b/tests/foreman/cli/test_computeresource_vmware.py @@ -11,158 +11,147 @@ :CaseImportance: High +:CaseAutomation: Automated + :Upstream: No """ from fauxfactory import gen_string import pytest +from wait_for import wait_for +from wrapanapi import VMWareSystem -from robottelo.cli.factory import make_compute_resource -from robottelo.cli.org import Org from robottelo.config import settings from robottelo.constants import FOREMAN_PROVIDERS -@pytest.fixture(scope='module') -def vmware(module_org, module_location): - vmware = type('vmware', (object,), {})() - vmware.org = module_org - vmware.loc = module_location - Org.add_location({'id': vmware.org.id, 'location-id': vmware.loc.id}) - vmware.vmware_server = settings.vmware.vcenter - vmware.vmware_password = settings.vmware.password - vmware.vmware_username = settings.vmware.username - vmware.vmware_datacenter = settings.vmware.datacenter - vmware.vmware_img_name = settings.vmware.image_name - vmware.vmware_img_arch = settings.vmware.image_arch - vmware.vmware_img_os = settings.vmware.image_os - vmware.vmware_img_user = settings.vmware.image_username - vmware.vmware_img_pass = settings.vmware.image_password - vmware.vmware_vm_name = settings.vmware.vm_name - return vmware - - @pytest.mark.tier1 -def test_positive_create_with_server(vmware): - """Create VMware compute resource with server field +@pytest.mark.e2e +@pytest.mark.upgrade +def test_positive_vmware_cr_end_to_end(target_sat, module_org, module_location): + """Create, Read, Update and Delete VMware compute resources - :id: a06b02c4-fe6a-44ef-bf61-5a28c3905527 + :id: 96faae3f-bc64-4147-a9fc-09c858e0a68f :customerscenario: true - :expectedresults: Compute resource is created, server field saved - correctly + :expectedresults: Compute resource should be created, read, updated and deleted :BZ: 1387917 - :CaseAutomation: Automated - :CaseImportance: Critical """ cr_name = gen_string('alpha') - vmware_cr = make_compute_resource( + # Create + vmware_cr = target_sat.cli.ComputeResource.create( { 'name': cr_name, + 'organization-ids': module_org.id, + 'location-ids': module_location.id, 'provider': FOREMAN_PROVIDERS['vmware'], - 'server': vmware.vmware_server, - 'user': vmware.vmware_username, - 'password': vmware.vmware_password, - 'datacenter': vmware.vmware_datacenter, + 'server': settings.vmware.vcenter, + 'user': settings.vmware.username, + 'password': settings.vmware.password, + 'datacenter': settings.vmware.datacenter, } ) assert vmware_cr['name'] == cr_name - assert vmware_cr['server'] == vmware.vmware_server - - -@pytest.mark.tier1 -def test_positive_create_with_org(vmware): - """Create VMware Compute Resource with organizations - - :id: 807a1f70-4cc3-4925-b145-0c3b26c57559 - - :customerscenario: true - - :expectedresults: VMware Compute resource is created - - :BZ: 1387917 - - :CaseAutomation: Automated - - :CaseImportance: Critical - """ - cr_name = gen_string('alpha') - vmware_cr = make_compute_resource( - { - 'name': cr_name, - 'organization-ids': vmware.org.id, - 'provider': FOREMAN_PROVIDERS['vmware'], - 'server': vmware.vmware_server, - 'user': vmware.vmware_username, - 'password': vmware.vmware_password, - 'datacenter': vmware.vmware_datacenter, - } - ) + assert vmware_cr['locations'][0] == module_location.name + assert vmware_cr['organizations'] == module_org.name + assert vmware_cr['server'] == settings.vmware.vcenter + assert vmware_cr['datacenter'] == settings.vmware.datacenter + # List + target_sat.cli.ComputeResource.list({'search': f'name="{cr_name}"'}) assert vmware_cr['name'] == cr_name - - -@pytest.mark.tier1 -def test_positive_create_with_loc(vmware): - """Create VMware Compute Resource with locations - - :id: 214a0f54-6fc2-4e7b-91ab-a45760ffb2f2 - - :customerscenario: true - - :expectedresults: VMware Compute resource is created - - :BZ: 1387917 - - :CaseAutomation: Automated - - :CaseImportance: Critical - """ - cr_name = gen_string('alpha') - vmware_cr = make_compute_resource( - { - 'name': cr_name, - 'location-ids': vmware.loc.id, - 'provider': FOREMAN_PROVIDERS['vmware'], - 'server': vmware.vmware_server, - 'user': vmware.vmware_username, - 'password': vmware.vmware_password, - 'datacenter': vmware.vmware_datacenter, - } + assert vmware_cr['provider'] == FOREMAN_PROVIDERS['vmware'] + # Update CR + new_cr_name = gen_string('alpha') + description = gen_string('alpha') + target_sat.cli.ComputeResource.update( + {'name': cr_name, 'new-name': new_cr_name, 'description': description} ) - assert vmware_cr['name'] == cr_name - - -@pytest.mark.tier1 -@pytest.mark.upgrade -def test_positive_create_with_org_and_loc(vmware): - """Create VMware Compute Resource with organizations and locations - - :id: 96faae3f-bc64-4147-a9fc-09c858e0a68f - - :customerscenario: true - - :expectedresults: VMware Compute resource is created - - :BZ: 1387917 + # Check updated values + result = target_sat.cli.ComputeResource.info({'id': vmware_cr['id']}) + assert result['name'] == new_cr_name + assert result['description'] == description + # Delete CR + target_sat.cli.ComputeResource.delete({'name': result['name']}) + assert not target_sat.cli.ComputeResource.exists(search=('name', result['name'])) + + +@pytest.mark.e2e +@pytest.mark.on_premises_provisioning +@pytest.mark.parametrize('setting_update', ['destroy_vm_on_host_delete=True'], indirect=True) +@pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) +@pytest.mark.parametrize('provision_method', ['build', 'bootdisk']) +@pytest.mark.rhel_ver_match('[^6]') +@pytest.mark.tier3 +def test_positive_provision_end_to_end( + request, + setting_update, + module_provisioning_sat, + module_sca_manifest_org, + module_location, + pxe_loader, + module_vmware_cr, + module_vmware_hostgroup, + provision_method, +): + """Provision a host on vmware compute resource with + the help of hostgroup. + + :id: ff9963fc-a2a7-4392-aa9a-190d5d1c8357 + + :steps: + + 1. Configure provisioning setup. + 2. Create VMware CR + 3. Configure host group setup. + 4. Provision a host on VMware + 5. Verify created host on VMware with wrapanapi + + :expectedresults: Host is provisioned succesfully with hostgroup :CaseAutomation: Automated - - :CaseImportance: Critical """ - cr_name = gen_string('alpha') - vmware_cr = make_compute_resource( + sat = module_provisioning_sat.sat + hostname = gen_string('alpha').lower() + host = sat.cli.Host.create( { - 'name': cr_name, - 'organization-ids': vmware.org.id, - 'location-ids': vmware.loc.id, - 'provider': FOREMAN_PROVIDERS['vmware'], - 'server': vmware.vmware_server, - 'user': vmware.vmware_username, - 'password': vmware.vmware_password, - 'datacenter': vmware.vmware_datacenter, + 'name': hostname, + 'organization': module_sca_manifest_org.name, + 'location': module_location.name, + 'hostgroup': module_vmware_hostgroup.name, + 'compute-resource-id': module_vmware_cr.id, + 'ip': None, + 'mac': None, + 'compute-attributes': f'cluster={settings.vmware.cluster},' + f'path=/Datacenters/{settings.vmware.datacenter}/vm/,' + 'scsi_controller_type=VirtualLsiLogicController,' + 'guest_id=rhel8_64Guest,firmware=automatic,' + 'cpus=1,memory_mb=6000, start=1', + 'interface': f'compute_type=VirtualVmxnet3,' + f'compute_network=VLAN {settings.provisioning.vlan_id}', + 'volume': f'name=Hard disk,size_gb=10,thin=true,eager_zero=false,datastore={settings.vmware.datastore}', + 'provision-method': provision_method, } ) - assert vmware_cr['name'] == cr_name + # teardown + request.addfinalizer(lambda: sat.cli.Host.delete({'id': host['id']})) + + hostname = f'{hostname}.{module_provisioning_sat.domain.name}' + assert hostname == host['name'] + # check if vm is created on vmware + vmware = VMWareSystem( + hostname=settings.vmware.vcenter, + username=settings.vmware.username, + password=settings.vmware.password, + ) + assert vmware.does_vm_exist(hostname) is True + wait_for( + lambda: sat.cli.Host.info({'name': hostname})['status']['build-status'] + != 'Pending installation', + timeout=1800, + delay=30, + ) + host_info = sat.cli.Host.info({'id': host['id']}) + assert host_info['status']['build-status'] == 'Installed' diff --git a/tests/foreman/longrun/test_provisioning_computeresource.py b/tests/foreman/longrun/test_provisioning_computeresource.py deleted file mode 100644 index 65444304df0..00000000000 --- a/tests/foreman/longrun/test_provisioning_computeresource.py +++ /dev/null @@ -1,239 +0,0 @@ -""" -:CaseLevel: Acceptance - -:CaseComponent: ComputeResources - -:Team: Rocket - -:TestType: Functional - -:CaseImportance: Critical - -:Upstream: No -""" -from fauxfactory import gen_string -import pytest -from wrapanapi import VMWareSystem - -from robottelo.cli.factory import make_compute_resource, make_host -from robottelo.cli.host import Host -from robottelo.config import settings -from robottelo.constants import FOREMAN_PROVIDERS, VMWARE_CONSTANTS - - -@pytest.fixture(scope="module") -def vmware(): - bridge = settings.vlan_networking.bridge - vmware = type("", (), {})() - vmware.vmware_server = settings.vmware.vcenter - vmware.vmware_password = settings.vmware.password - vmware.vmware_username = settings.vmware.username - vmware.vmware_datacenter = settings.vmware.datacenter - vmware.vmware_img_name = settings.vmware.image_name - vmware.vmware_img_arch = settings.vmware.image_arch - vmware.vmware_img_os = settings.vmware.image_os - vmware.vmware_img_user = settings.vmware.image_username - vmware.vmware_img_pass = settings.vmware.image_password - vmware.vmware_vm_name = settings.vmware.vm_name - vmware.current_interface = VMWARE_CONSTANTS.get('network_interfaces') % bridge - vmware.vmware_api = VMWareSystem( - hostname=vmware.vmware_server, - username=vmware.vmware_username, - password=vmware.vmware_password, - ) - vmware.vmware_net_id = vmware.vmware_api.get_network(vmware.current_interface)._moId - return vmware - - -@pytest.fixture(scope="module") -def provisioning(module_org, module_location, module_target_sat): - os = None - if hasattr(settings, 'rhev') and hasattr(settings.rhev, 'image_os') and settings.rhev.image_os: - os = settings.rhev.image_os - provisioning = type("", (), {})() - provisioning.org_name = module_org.name - provisioning.loc_name = module_location.name - provisioning.config_env = module_target_sat.api_factory.configure_provisioning( - compute=True, org=module_org, loc=module_location, os=os - ) - provisioning.os_name = provisioning.config_env['os'] - return provisioning - - -@pytest.fixture(scope="module") -def vmware_cr(provisioning, vmware): - return make_compute_resource( - { - 'name': gen_string('alpha'), - 'organizations': provisioning.org_name, - 'locations': provisioning.loc_name, - 'provider': FOREMAN_PROVIDERS['vmware'], - 'server': vmware.vmware_server, - 'user': vmware.vmware_username, - 'password': vmware.vmware_password, - 'datacenter': vmware.vmware_datacenter, - } - ) - - -@pytest.fixture(scope="function") -def tear_down(provisioning): - """Delete the hosts to free the resources""" - yield - hosts = Host.list({'organization': provisioning.org_name}) - for host in hosts: - Host.delete({'id': host['id']}) - - -@pytest.mark.on_premises_provisioning -@pytest.mark.vlan_networking -@pytest.mark.tier3 -def test_positive_provision_vmware_with_host_group( - vmware, provisioning, tear_down, vmware_cr, target_sat -): - """Provision a host on vmware compute resource with - the help of hostgroup. - - :Requirement: Computeresource Vmware - - :CaseComponent: ComputeResources-VMWare - - :Team: Rocket - - :customerscenario: true - - :id: ae4d5949-f0e6-44ca-93b6-c5241a02b64b - - :setup: - - 1. Vaild vmware hostname ,credentials. - 2. Configure provisioning setup. - 3. Configure host group setup. - - :steps: - - 1. Go to "Hosts --> New host". - 2. Assign the host group to the host. - 3. Select the Deploy on as vmware Compute Resource. - 4. Provision the host. - - :expectedresults: The host should be provisioned with host group - - :CaseAutomation: Automated - - :CaseLevel: System - """ - host_name = gen_string('alpha').lower() - host = make_host( - { - 'name': f'{host_name}', - 'root-password': gen_string('alpha'), - 'organization': provisioning.org_name, - 'location': provisioning.loc_name, - 'hostgroup': provisioning.config_env['host_group'], - 'pxe-loader': 'PXELinux BIOS', - 'compute-resource-id': vmware_cr.get('id'), - 'compute-attributes': "cpus=2," - "corespersocket=2," - "memory_mb=4028," - "cluster={}," - "path=/Datacenters/{}/vm/QE," - "guest_id=rhel7_64Guest," - "scsi_controller_type=VirtualLsiLogicController," - "hardware_version=Default," - "start=1".format(VMWARE_CONSTANTS['cluster'], vmware.vmware_datacenter), - 'ip': None, - 'mac': None, - 'interface': "compute_network={}," - "compute_type=VirtualVmxnet3".format(vmware.vmware_net_id), - 'volume': "name=Hard disk," - "size_gb=10," - "thin=true," - "eager_zero=false," - "datastore={}".format(VMWARE_CONSTANTS['datastore'].split()[0]), - 'provision-method': 'build', - } - ) - hostname = '{}.{}'.format(host_name, provisioning.config_env['domain']) - assert hostname == host['name'] - # Check on Vmware, if VM exists - assert vmware.vmware_api.does_vm_exist(hostname) - host_info = Host.info({'name': hostname}) - host_ip = host_info.get('network').get('ipv4-address') - # Start to run a ping check if network was established on VM - target_sat.ping_host(host=host_ip) - - -@pytest.mark.on_premises_provisioning -@pytest.mark.vlan_networking -@pytest.mark.tier3 -def test_positive_provision_vmware_with_host_group_bootdisk( - vmware, provisioning, tear_down, vmware_cr, target_sat -): - """Provision a bootdisk based host on VMWare compute resource. - - :Requirement: Computeresource Vmware - - :CaseComponent: ComputeResources-VMWare - - :id: bc5f457d-c29a-4c62-bbdc-af8f4f813519 - - :bz: 1679225 - - :setup: - - 1. Vaild VMWare hostname, credentials. - 2. Configure provisioning setup. - 3. Configure host group setup. - - :steps: Using Hammer CLI, Provision a VM on VMWare with hostgroup and - provisioning method as `bootdisk`. - - :expectedresults: The host should be provisioned with provisioning type bootdisk - - :CaseAutomation: Automated - - :customerscenario: true - - :CaseLevel: System - """ - host_name = gen_string('alpha').lower() - host = make_host( - { - 'name': f'{host_name}', - 'root-password': gen_string('alpha'), - 'organization': provisioning.org_name, - 'location': provisioning.loc_name, - 'hostgroup': provisioning.config_env['host_group'], - 'pxe-loader': 'PXELinux BIOS', - 'compute-resource-id': vmware_cr.get('id'), - 'content-source-id': '1', - 'compute-attributes': "cpus=2," - "corespersocket=2," - "memory_mb=4028," - "cluster={}," - "path=/Datacenters/{}/vm/QE," - "guest_id=rhel7_64Guest," - "scsi_controllers=`type=VirtualLsiLogicController,key=1000'," - "hardware_version=Default," - "start=1".format(VMWARE_CONSTANTS['cluster'], vmware.vmware_datacenter), - "ip": None, - "mac": None, - 'interface': "compute_network={}," - "compute_type=VirtualVmxnet3".format(vmware.vmware_net_id), - 'volume': "name=Hard disk," - "size_gb=10," - "thin=true," - "eager_zero=false," - "datastore={}".format(VMWARE_CONSTANTS['datastore'].split()[0]), - 'provision-method': 'bootdisk', - } - ) - hostname = '{}.{}'.format(host_name, provisioning.config_env['domain']) - assert hostname == host['name'] - # Check on Vmware, if VM exists - assert vmware.vmware_api.does_vm_exist(hostname) - host_info = Host.info({'name': hostname}) - host_ip = host_info.get('network').get('ipv4-address') - # Start to run a ping check if network was established on VM - target_sat.ping_host(host=host_ip) diff --git a/tests/foreman/ui/test_computeresource_vmware.py b/tests/foreman/ui/test_computeresource_vmware.py index dfe4ca1abe7..1f93bbe673a 100644 --- a/tests/foreman/ui/test_computeresource_vmware.py +++ b/tests/foreman/ui/test_computeresource_vmware.py @@ -61,7 +61,7 @@ def _get_normalized_size(size): return f'{size} {suffixes[suffix_index]}' -def _get_vmware_datastore_summary_string(data_store_name=VMWARE_CONSTANTS['datastore']): +def _get_vmware_datastore_summary_string(data_store_name=settings.vmware.datastore): """Return the datastore string summary for data_store_name For "Local-Ironforge" datastore the string looks Like: @@ -85,35 +85,8 @@ def _get_vmware_datastore_summary_string(data_store_name=VMWARE_CONSTANTS['datas return f'{data_store_name} (free: {free_space}, prov: {prov}, total: {capacity})' -@pytest.fixture(scope='module') -def module_org(): - return entities.Organization().create() - - -@pytest.fixture(scope='module') -def module_vmware_settings(): - ret = dict( - vcenter=settings.vmware.vcenter, - user=settings.vmware.username, - password=settings.vmware.password, - datacenter=settings.vmware.datacenter, - image_name=settings.vmware.image_name, - image_arch=settings.vmware.image_arch, - image_os=settings.vmware.image_os, - image_username=settings.vmware.image_username, - image_password=settings.vmware.image_password, - vm_name=settings.vmware.vm_name, - cluster=settings.vmware.cluster, - mac_address=settings.vmware.mac_address, - hypervisor=settings.vmware.hypervisor, - ) - if 'INTERFACE' in settings.vmware: - ret['interface'] = VMWARE_CONSTANTS['network_interfaces'] % settings.vmware.interface - return ret - - @pytest.mark.tier1 -def test_positive_end_to_end(session, module_org, module_location, module_vmware_settings): +def test_positive_end_to_end(session, module_org, module_location): """Perform end to end testing for compute resource VMware component. :id: 47fc9e77-5b22-46b4-a76c-3217434fde2f @@ -136,10 +109,10 @@ def test_positive_end_to_end(session, module_org, module_location, module_vmware 'name': cr_name, 'description': description, 'provider': FOREMAN_PROVIDERS['vmware'], - 'provider_content.vcenter': module_vmware_settings['vcenter'], - 'provider_content.user': module_vmware_settings['user'], - 'provider_content.password': module_vmware_settings['password'], - 'provider_content.datacenter.value': module_vmware_settings['datacenter'], + 'provider_content.vcenter': settings.vmware.vcenter, + 'provider_content.user': settings.vmware.username, + 'provider_content.password': settings.vmware.password, + 'provider_content.datacenter.value': settings.vmware.datacenter, 'provider_content.display_type': display_type, 'provider_content.vnc_console_passwords': vnc_console_passwords, 'provider_content.enable_caching': enable_caching, @@ -151,11 +124,8 @@ def test_positive_end_to_end(session, module_org, module_location, module_vmware assert cr_values['name'] == cr_name assert cr_values['description'] == description assert cr_values['provider'] == FOREMAN_PROVIDERS['vmware'] - assert cr_values['provider_content']['user'] == module_vmware_settings['user'] - assert ( - cr_values['provider_content']['datacenter']['value'] - == module_vmware_settings['datacenter'] - ) + assert cr_values['provider_content']['user'] == settings.vmware.username + assert cr_values['provider_content']['datacenter']['value'] == settings.vmware.datacenter assert cr_values['provider_content']['display_type'] == display_type assert cr_values['provider_content']['vnc_console_passwords'] == vnc_console_passwords assert cr_values['provider_content']['enable_caching'] == enable_caching @@ -189,7 +159,7 @@ def test_positive_end_to_end(session, module_org, module_location, module_vmware @pytest.mark.tier2 -def test_positive_retrieve_virtual_machine_list(session, module_vmware_settings): +def test_positive_retrieve_virtual_machine_list(session): """List the virtual machine list from vmware compute resource :id: 21ade57a-0caa-4144-9c46-c8e22f33414e @@ -206,16 +176,16 @@ def test_positive_retrieve_virtual_machine_list(session, module_vmware_settings) :CaseLevel: Integration """ cr_name = gen_string('alpha') - vm_name = module_vmware_settings['vm_name'] + vm_name = settings.vmware.vm_name with session: session.computeresource.create( { 'name': cr_name, 'provider': FOREMAN_PROVIDERS['vmware'], - 'provider_content.vcenter': module_vmware_settings['vcenter'], - 'provider_content.user': module_vmware_settings['user'], - 'provider_content.password': module_vmware_settings['password'], - 'provider_content.datacenter.value': module_vmware_settings['datacenter'], + 'provider_content.vcenter': settings.vmware.vcenter, + 'provider_content.user': settings.vmware.username, + 'provider_content.password': settings.vmware.password, + 'provider_content.datacenter.value': settings.vmware.datacenter, } ) assert session.computeresource.search(cr_name)[0]['Name'] == cr_name @@ -224,8 +194,9 @@ def test_positive_retrieve_virtual_machine_list(session, module_vmware_settings) ) +@pytest.mark.e2e @pytest.mark.tier2 -def test_positive_image_end_to_end(session, module_vmware_settings, target_sat): +def test_positive_image_end_to_end(session, target_sat): """Perform end to end testing for compute resource VMware component image. :id: 6b7949ef-c684-40aa-b181-11f8d4cd39c6 @@ -237,17 +208,17 @@ def test_positive_image_end_to_end(session, module_vmware_settings, target_sat): cr_name = gen_string('alpha') image_name = gen_string('alpha') new_image_name = gen_string('alpha') - target_sat.api_factory.check_create_os_with_title(module_vmware_settings['image_os']) + os = target_sat.api_factory.check_create_os_with_title(settings.vmware.image_os) image_user_data = choice((False, True)) with session: session.computeresource.create( { 'name': cr_name, 'provider': FOREMAN_PROVIDERS['vmware'], - 'provider_content.vcenter': module_vmware_settings['vcenter'], - 'provider_content.user': module_vmware_settings['user'], - 'provider_content.password': module_vmware_settings['password'], - 'provider_content.datacenter.value': module_vmware_settings['datacenter'], + 'provider_content.vcenter': settings.vmware.vcenter, + 'provider_content.user': settings.vmware.username, + 'provider_content.password': settings.vmware.password, + 'provider_content.datacenter.value': settings.vmware.datacenter, } ) assert session.computeresource.search(cr_name)[0]['Name'] == cr_name @@ -255,21 +226,21 @@ def test_positive_image_end_to_end(session, module_vmware_settings, target_sat): cr_name, dict( name=image_name, - operating_system=module_vmware_settings['image_os'], - architecture=module_vmware_settings['image_arch'], - username=module_vmware_settings['image_username'], + operating_system=os.title, + architecture=settings.vmware.image_arch, + username=settings.vmware.image_username, user_data=image_user_data, - password=module_vmware_settings['image_password'], - image=module_vmware_settings['image_name'], + password=settings.vmware.image_password, + image=settings.vmware.image_name, ), ) values = session.computeresource.read_image(cr_name, image_name) assert values['name'] == image_name - assert values['operating_system'] == module_vmware_settings['image_os'] - assert values['architecture'] == module_vmware_settings['image_arch'] - assert values['username'] == module_vmware_settings['image_username'] + assert values['operating_system'] == os.title + assert values['architecture'] == settings.vmware.image_arch + assert values['username'] == settings.vmware.image_username assert values['user_data'] == image_user_data - assert values['image'] == module_vmware_settings['image_name'] + assert values['image'] == settings.vmware.image_name session.computeresource.update_image(cr_name, image_name, dict(name=new_image_name)) assert session.computeresource.search_images(cr_name, image_name)[0]['Name'] != image_name assert ( @@ -285,7 +256,7 @@ def test_positive_image_end_to_end(session, module_vmware_settings, target_sat): @pytest.mark.tier2 @pytest.mark.run_in_one_thread -def test_positive_resource_vm_power_management(session, module_vmware_settings): +def test_positive_resource_vm_power_management(session): """Read current VMware Compute Resource virtual machine power status and change it to opposite one @@ -296,16 +267,16 @@ def test_positive_resource_vm_power_management(session, module_vmware_settings): :CaseLevel: Integration """ cr_name = gen_string('alpha') - vm_name = module_vmware_settings['vm_name'] + vm_name = settings.vmware.vm_name with session: session.computeresource.create( { 'name': cr_name, 'provider': FOREMAN_PROVIDERS['vmware'], - 'provider_content.vcenter': module_vmware_settings['vcenter'], - 'provider_content.user': module_vmware_settings['user'], - 'provider_content.password': module_vmware_settings['password'], - 'provider_content.datacenter.value': module_vmware_settings['datacenter'], + 'provider_content.vcenter': settings.vmware.vcenter, + 'provider_content.user': settings.vmware.username, + 'provider_content.password': settings.vmware.password, + 'provider_content.datacenter.value': settings.vmware.datacenter, } ) assert session.computeresource.search(cr_name)[0]['Name'] == cr_name @@ -329,7 +300,7 @@ def test_positive_resource_vm_power_management(session, module_vmware_settings): @pytest.mark.tier2 -def test_positive_select_vmware_custom_profile_guest_os_rhel7(session, module_vmware_settings): +def test_positive_select_vmware_custom_profile_guest_os_rhel7(session): """Select custom default (3-Large) compute profile guest OS RHEL7. :id: 24f7bb5f-2aaf-48cb-9a56-d2d0713dfe3d @@ -360,10 +331,10 @@ def test_positive_select_vmware_custom_profile_guest_os_rhel7(session, module_vm { 'name': cr_name, 'provider': FOREMAN_PROVIDERS['vmware'], - 'provider_content.vcenter': module_vmware_settings['vcenter'], - 'provider_content.user': module_vmware_settings['user'], - 'provider_content.password': module_vmware_settings['password'], - 'provider_content.datacenter.value': module_vmware_settings['datacenter'], + 'provider_content.vcenter': settings.vmware.vcenter, + 'provider_content.user': settings.vmware.username, + 'provider_content.password': settings.vmware.password, + 'provider_content.datacenter.value': settings.vmware.datacenter, } ) assert session.computeresource.search(cr_name)[0]['Name'] == cr_name @@ -375,7 +346,7 @@ def test_positive_select_vmware_custom_profile_guest_os_rhel7(session, module_vm @pytest.mark.tier2 -def test_positive_access_vmware_with_custom_profile(session, module_vmware_settings): +def test_positive_access_vmware_with_custom_profile(session): """Associate custom default (3-Large) compute profile :id: 751ef765-5091-4322-a0d9-0c9c73009cc4 @@ -402,7 +373,7 @@ def test_positive_access_vmware_with_custom_profile(session, module_vmware_setti cores_per_socket='2', memory='1024', firmware='EFI', - cluster=VMWARE_CONSTANTS.get('cluster'), + cluster=settings.vmware.cluster, resource_pool=VMWARE_CONSTANTS.get('pool'), folder=VMWARE_CONSTANTS.get('folder'), guest_os=VMWARE_CONSTANTS.get('guest_os'), @@ -412,15 +383,15 @@ def test_positive_access_vmware_with_custom_profile(session, module_vmware_setti cdrom_drive=True, annotation_notes=gen_string('alpha'), network_interfaces=[] - if 'interface' not in module_vmware_settings + if not settings.provisioning.vlan_id else [ dict( nic_type=VMWARE_CONSTANTS.get('network_interface_name'), - network=module_vmware_settings['interface'], + network='VLAN 1001', # hardcoding network here as these test won't be doing actual provisioning ), dict( nic_type=VMWARE_CONSTANTS.get('network_interface_name'), - network=module_vmware_settings['interface'], + network='VLAN 1001', ), ], storage=[ @@ -458,10 +429,10 @@ def test_positive_access_vmware_with_custom_profile(session, module_vmware_setti { 'name': cr_name, 'provider': FOREMAN_PROVIDERS['vmware'], - 'provider_content.vcenter': module_vmware_settings['vcenter'], - 'provider_content.user': module_vmware_settings['user'], - 'provider_content.password': module_vmware_settings['password'], - 'provider_content.datacenter.value': module_vmware_settings['datacenter'], + 'provider_content.vcenter': settings.vmware.vcenter, + 'provider_content.user': settings.vmware.username, + 'provider_content.password': settings.vmware.password, + 'provider_content.datacenter.value': settings.vmware.datacenter, } ) assert session.computeresource.search(cr_name)[0]['Name'] == cr_name @@ -507,9 +478,7 @@ def test_positive_access_vmware_with_custom_profile(session, module_vmware_setti @pytest.mark.tier2 -def test_positive_virt_card( - session, target_sat, module_vmware_settings, module_location, module_org -): +def test_positive_virt_card(session, target_sat, module_location, module_org): """Check to see that the Virtualization card appears for an imported VM :id: 0502d5a6-64c1-422f-a9ba-ac7c2ee7bad2 @@ -574,10 +543,10 @@ def test_positive_virt_card( { 'name': cr_name, 'provider': FOREMAN_PROVIDERS['vmware'], - 'provider_content.vcenter': module_vmware_settings['vcenter'], - 'provider_content.user': module_vmware_settings['user'], - 'provider_content.password': module_vmware_settings['password'], - 'provider_content.datacenter.value': module_vmware_settings['datacenter'], + 'provider_content.vcenter': settings.vmware.vcenter, + 'provider_content.user': settings.vmware.username, + 'provider_content.password': settings.vmware.password, + 'provider_content.datacenter.value': settings.vmware.datacenter, 'locations.resources.assigned': [module_location.name], 'organizations.resources.assigned': [module_org.name], } @@ -585,23 +554,19 @@ def test_positive_virt_card( session.hostgroup.update(hostgroup_name, {'host_group.deploy': cr_name + " (VMware)"}) session.computeresource.vm_import( cr_name, - module_vmware_settings['vm_name'], + settings.vmware.vm_name, hostgroup_name, module_location.name, - module_org.name, - module_vmware_settings['vm_name'], ) - host_name = module_vmware_settings['vm_name'] + '.' + domain.name - power_status = session.computeresource.vm_status(cr_name, module_vmware_settings['vm_name']) + host_name = '.'.join([settings.vmware.vm_name, domain.name]) + power_status = session.computeresource.vm_status(cr_name, settings.vmware.vm_name) if power_status is False: - session.computeresource.vm_poweron(cr_name, module_vmware_settings['vm_name']) + session.computeresource.vm_poweron(cr_name, settings.vmware.vm_name) try: wait_for( lambda: ( session.browser.refresh(), - session.computeresource.vm_status( - cr_name, module_vmware_settings['vm_name'] - ), + session.computeresource.vm_status(cr_name, settings.vmware.vm_name), )[1] is not power_status, timeout=30, @@ -611,11 +576,11 @@ def test_positive_virt_card( raise AssertionError('Timed out waiting for VM to toggle power state') virt_card = session.host_new.get_virtualization(host_name)['details'] - assert virt_card['datacenter'] == module_vmware_settings['datacenter'] - assert virt_card['cluster'] == module_vmware_settings['cluster'] - assert virt_card['memory'] == '2 GB' + assert virt_card['datacenter'] == settings.vmware.datacenter + assert virt_card['cluster'] == settings.vmware.cluster + assert virt_card['memory'] == '5 GB' assert 'public_ip_address' in virt_card - assert virt_card['mac_address'] == module_vmware_settings['mac_address'] + assert virt_card['mac_address'] == settings.vmware.mac_address assert virt_card['cpus'] == '1' if 'disk_label' in virt_card: assert virt_card['disk_label'] == 'Hard disk 1' From f00da596561d8c64db17e7e91ff38aba3c88b069 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 9 Oct 2023 09:59:45 -0400 Subject: [PATCH 268/586] [6.14.z] Nailgun ssl cert verification (#12841) Nailgun ssl cert verification (#12813) * Add config option to verify nailgun requests against ssl cert * Use ssl verification for all instances of ServerConfig * Use dynaconf validator --------- Co-authored-by: dosas (cherry picked from commit 0ddb4b712f659b78791e5a6437dca4bf140e8ce6) Co-authored-by: dosas --- conf/server.yaml.template | 4 +++ robottelo/config/__init__.py | 6 ++-- robottelo/config/validators.py | 1 + robottelo/hosts.py | 2 +- tests/foreman/api/test_role.py | 45 ++++++++++++++++++-------- tests/foreman/api/test_subscription.py | 5 +-- tests/foreman/api/test_user.py | 12 ++++--- 7 files changed, 52 insertions(+), 23 deletions(-) diff --git a/conf/server.yaml.template b/conf/server.yaml.template index 5c08431532b..7f876bccaba 100644 --- a/conf/server.yaml.template +++ b/conf/server.yaml.template @@ -48,6 +48,10 @@ SERVER: ADMIN_USERNAME: admin # Admin password when accessing API and UI ADMIN_PASSWORD: changeme + # Set to true to verify against the certificate given in REQUESTS_CA_BUNDLE + # Or specify path to certificate path or directory + # see: https://requests.readthedocs.io/en/latest/user/advanced/#ssl-cert-verification + VERIFY_CA: false SSH_CLIENT: # Specify port number for ssh client, Default: 22 diff --git a/robottelo/config/__init__.py b/robottelo/config/__init__.py index 5bd85ab5737..e078d2fcc63 100644 --- a/robottelo/config/__init__.py +++ b/robottelo/config/__init__.py @@ -110,7 +110,7 @@ def user_nailgun_config(username=None, password=None): """ creds = (username, password) - return ServerConfig(get_url(), creds, verify=False) + return ServerConfig(get_url(), creds, verify=settings.server.verify_ca) def setting_is_set(option): @@ -153,7 +153,9 @@ def configure_nailgun(): from nailgun.config import ServerConfig entity_mixins.CREATE_MISSING = True - entity_mixins.DEFAULT_SERVER_CONFIG = ServerConfig(get_url(), get_credentials(), verify=False) + entity_mixins.DEFAULT_SERVER_CONFIG = ServerConfig( + get_url(), get_credentials(), verify=settings.server.verify_ca + ) gpgkey_init = entities.GPGKey.__init__ def patched_gpgkey_init(self, server_config=None, **kwargs): diff --git a/robottelo/config/validators.py b/robottelo/config/validators.py index b605a1cd229..383abfb7aed 100644 --- a/robottelo/config/validators.py +++ b/robottelo/config/validators.py @@ -29,6 +29,7 @@ Validator('server.port', default=443), Validator('server.ssh_username', default='root'), Validator('server.ssh_password', default=None), + Validator('server.verify_ca', default=False), ], content_host=[ Validator('content_host.default_rhel_version', must_exist=True), diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 51348d73d55..cb052796172 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1764,7 +1764,7 @@ class DecClass(cls): self.nailgun_cfg = ServerConfig( auth=(settings.server.admin_username, settings.server.admin_password), url=f'{self.url}', - verify=False, + verify=settings.server.verify_ca, ) # add each nailgun entity to self.api, injecting our server config for name, obj in entities.__dict__.items(): diff --git a/tests/foreman/api/test_role.py b/tests/foreman/api/test_role.py index d75ff03e84b..4b42408114d 100644 --- a/tests/foreman/api/test_role.py +++ b/tests/foreman/api/test_role.py @@ -26,6 +26,7 @@ from requests.exceptions import HTTPError from robottelo.cli.ldapauthsource import LDAPAuthSource +from robottelo.config import settings from robottelo.constants import LDAP_ATTR, LDAP_SERVER_TYPE from robottelo.utils.datafactory import gen_string, generate_strings_list, parametrized from robottelo.utils.issue_handlers import is_open @@ -154,7 +155,9 @@ def user_config(self, user, satellite): :param user: The nailgun.entities.User object of an user with passwd parameter """ - return ServerConfig(auth=(user.login, user.passwd), url=satellite.url, verify=False) + return ServerConfig( + auth=(user.login, user.passwd), url=satellite.url, verify=settings.server.verify_ca + ) @pytest.fixture def role_taxonomies(self): @@ -991,7 +994,9 @@ def test_positive_user_group_users_access_as_org_admin(self, role_taxonomies, ta location=[role_taxonomies['loc'].id], ).create() for login, password in ((userone_login, userone_pass), (usertwo_login, usertwo_pass)): - sc = ServerConfig(auth=(login, password), url=target_sat.url, verify=False) + sc = ServerConfig( + auth=(login, password), url=target_sat.url, verify=settings.server.verify_ca + ) try: entities.Domain(sc).search( query={ @@ -1120,7 +1125,9 @@ def test_negative_assign_taxonomies_by_org_admin( location=[role_taxonomies['loc']], ).create() assert user_login == user.login - sc = ServerConfig(auth=(user_login, user_pass), url=target_sat.url, verify=False) + sc = ServerConfig( + auth=(user_login, user_pass), url=target_sat.url, verify=settings.server.verify_ca + ) # Getting the domain from user1 dom = entities.Domain(sc, id=dom.id).read() dom.organization = [filter_taxonomies['org']] @@ -1279,7 +1286,9 @@ def test_negative_create_roles_by_org_admin(self, role_taxonomies, target_sat): location=[role_taxonomies['loc']], ).create() assert user_login == user.login - sc = ServerConfig(auth=(user_login, user_pass), url=target_sat.url, verify=False) + sc = ServerConfig( + auth=(user_login, user_pass), url=target_sat.url, verify=settings.server.verify_ca + ) role_name = gen_string('alpha') with pytest.raises(HTTPError): entities.Role( @@ -1344,7 +1353,9 @@ def test_negative_admin_permissions_to_org_admin(self, role_taxonomies, target_s location=[role_taxonomies['loc']], ).create() assert user_login == user.login - sc = ServerConfig(auth=(user_login, user_pass), url=target_sat.url, verify=False) + sc = ServerConfig( + auth=(user_login, user_pass), url=target_sat.url, verify=settings.server.verify_ca + ) with pytest.raises(HTTPError): entities.User(sc, id=1).read() @@ -1389,7 +1400,9 @@ def test_positive_create_user_by_org_admin(self, role_taxonomies, target_sat): location=[role_taxonomies['loc']], ).create() assert user_login == user.login - sc_user = ServerConfig(auth=(user_login, user_pass), url=target_sat.url, verify=False) + sc_user = ServerConfig( + auth=(user_login, user_pass), url=target_sat.url, verify=settings.server.verify_ca + ) user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') user = entities.User( @@ -1470,7 +1483,9 @@ def test_positive_create_nested_location(self, role_taxonomies, target_sat): ) user.role = [org_admin] user = user.update(['role']) - sc = ServerConfig(auth=(user_login, user_pass), url=target_sat.url, verify=False) + sc = ServerConfig( + auth=(user_login, user_pass), url=target_sat.url, verify=settings.server.verify_ca + ) name = gen_string('alphanumeric') location = entities.Location(sc, name=name, parent=role_taxonomies['loc'].id).create() assert location.name == name @@ -1534,7 +1549,9 @@ def test_negative_create_taxonomies_by_org_admin(self, role_taxonomies, target_s location=[role_taxonomies['loc']], ).create() assert user_login == user.login - sc = ServerConfig(auth=(user_login, user_pass), url=target_sat.url, verify=False) + sc = ServerConfig( + auth=(user_login, user_pass), url=target_sat.url, verify=settings.server.verify_ca + ) with pytest.raises(HTTPError): entities.Organization(sc, name=gen_string('alpha')).create() if not is_open("BZ:1825698"): @@ -1578,7 +1595,9 @@ def test_positive_access_all_global_entities_by_org_admin( location=[role_taxonomies['loc'], filter_taxonomies['loc']], ).create() assert user_login == user.login - sc = ServerConfig(auth=(user_login, user_pass), url=target_sat.url, verify=False) + sc = ServerConfig( + auth=(user_login, user_pass), url=target_sat.url, verify=settings.server.verify_ca + ) try: for entity in [ entities.Architecture, @@ -1627,7 +1646,7 @@ def test_negative_access_entities_from_ldap_org_admin(self, role_taxonomies, cre sc = ServerConfig( auth=(create_ldap['ldap_user_name'], create_ldap['ldap_user_passwd']), url=create_ldap['sat_url'], - verify=False, + verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): entities.Architecture(sc).search() @@ -1670,7 +1689,7 @@ def test_negative_access_entities_from_ldap_user( sc = ServerConfig( auth=(create_ldap['ldap_user_name'], create_ldap['ldap_user_passwd']), url=create_ldap['sat_url'], - verify=False, + verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): entities.Architecture(sc).search() @@ -1734,7 +1753,7 @@ def test_positive_assign_org_admin_to_ldap_user_group(self, role_taxonomies, cre sc = ServerConfig( auth=(user.login, password), url=create_ldap['sat_url'], - verify=False, + verify=settings.server.verify_ca, ) # Accessing the Domain resource entities.Domain(sc, id=domain.id).read() @@ -1790,7 +1809,7 @@ def test_negative_assign_org_admin_to_ldap_user_group(self, create_ldap, role_ta sc = ServerConfig( auth=(user.login, password), url=create_ldap['sat_url'], - verify=False, + verify=settings.server.verify_ca, ) # Trying to access the Domain resource with pytest.raises(HTTPError): diff --git a/tests/foreman/api/test_subscription.py b/tests/foreman/api/test_subscription.py index 8b1da648d81..377d43555dd 100644 --- a/tests/foreman/api/test_subscription.py +++ b/tests/foreman/api/test_subscription.py @@ -28,6 +28,7 @@ from requests.exceptions import HTTPError from robottelo.cli.subscription import Subscription +from robottelo.config import settings from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME, PRDS, REPOS, REPOSET pytestmark = [pytest.mark.run_in_one_thread] @@ -191,7 +192,7 @@ def test_positive_delete_manifest_as_another_user( sc1 = ServerConfig( auth=(user1.login, user1_password), url=target_sat.url, - verify=False, + verify=settings.server.verify_ca, ) user2_password = gen_string('alphanumeric') user2 = target_sat.api.User( @@ -203,7 +204,7 @@ def test_positive_delete_manifest_as_another_user( sc2 = ServerConfig( auth=(user2.login, user2_password), url=target_sat.url, - verify=False, + verify=settings.server.verify_ca, ) # use the first admin to upload a manifest with function_entitlement_manifest as manifest: diff --git a/tests/foreman/api/test_user.py b/tests/foreman/api/test_user.py index cab7b49d653..eff47fbba0f 100644 --- a/tests/foreman/api/test_user.py +++ b/tests/foreman/api/test_user.py @@ -418,7 +418,9 @@ def test_positive_table_preferences(self, module_target_sat): user = entities.User(role=existing_roles, password=password).create() name = "hosts" columns = ["power_status", "name", "comment"] - sc = ServerConfig(auth=(user.login, password), url=module_target_sat.url, verify=False) + sc = ServerConfig( + auth=(user.login, password), url=module_target_sat.url, verify=settings.server.verify_ca + ) entities.TablePreferences(sc, user=user, name=name, columns=columns).create() table_preferences = entities.TablePreferences(sc, user=user).search() assert len(table_preferences) == 1 @@ -726,7 +728,7 @@ def test_positive_ad_basic_no_roles(self, create_ldap): sc = ServerConfig( auth=(create_ldap['ldap_user_name'], create_ldap['ldap_user_passwd']), url=create_ldap['sat_url'], - verify=False, + verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): entities.Architecture(sc).search() @@ -775,7 +777,7 @@ def test_positive_access_entities_from_ldap_org_admin(self, create_ldap, module_ sc = ServerConfig( auth=(create_ldap['ldap_user_name'], create_ldap['ldap_user_passwd']), url=create_ldap['sat_url'], - verify=False, + verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): entities.Architecture(sc).search() @@ -857,7 +859,7 @@ def test_positive_ipa_basic_no_roles(self, create_ldap): sc = ServerConfig( auth=(create_ldap['username'], create_ldap['ldap_user_passwd']), url=create_ldap['sat_url'], - verify=False, + verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): entities.Architecture(sc).search() @@ -896,7 +898,7 @@ def test_positive_access_entities_from_ipa_org_admin(self, create_ldap): sc = ServerConfig( auth=(create_ldap['username'], create_ldap['ldap_user_passwd']), url=create_ldap['sat_url'], - verify=False, + verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): entities.Architecture(sc).search() From cc41f2987ddd4890a478d212f299a063cd3a3e12 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 9 Oct 2023 14:28:28 -0400 Subject: [PATCH 269/586] [6.14.z] Change read to read_legacy_ui (#12847) --- tests/foreman/ui/test_activationkey.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/foreman/ui/test_activationkey.py b/tests/foreman/ui/test_activationkey.py index fe69f2d552c..5216cf97540 100644 --- a/tests/foreman/ui/test_activationkey.py +++ b/tests/foreman/ui/test_activationkey.py @@ -78,7 +78,12 @@ def test_positive_end_to_end_crud(session, module_org): indirect=True, ) def test_positive_end_to_end_register( - session, function_entitlement_manifest_org, repos_collection, rhel7_contenthost, target_sat + session, + function_entitlement_manifest_org, + default_location, + repos_collection, + rhel7_contenthost, + target_sat, ): """Create activation key and use it during content host registering @@ -98,10 +103,13 @@ def test_positive_end_to_end_register( repos_collection.setup_content(org.id, lce.id, upload_manifest=False) ak_name = repos_collection.setup_content_data['activation_key']['name'] - repos_collection.setup_virtual_machine(rhel7_contenthost) + repos_collection.setup_virtual_machine(rhel7_contenthost, install_katello_agent=False) with session: session.organization.select(org.name) - chost = session.contenthost.read(rhel7_contenthost.hostname, widget_names='details') + session.location.select(default_location.name) + chost = session.contenthost.read_legacy_ui( + rhel7_contenthost.hostname, widget_names='details' + ) assert chost['details']['registered_by'] == f'Activation Key {ak_name}' ak_values = session.activationkey.read(ak_name, widget_names='content_hosts') assert len(ak_values['content_hosts']['table']) == 1 From 379b0961aeac1d53e18bdde2b5c49fc7a70b6c02 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 9 Oct 2023 18:09:24 -0400 Subject: [PATCH 270/586] [6.14.z] Fix lce id option in host registration (#12852) Fix lce id option in host registration (#12849) (cherry picked from commit 2f3484e18a10873eb8bed975fd78bdc059fa4ca7) Co-authored-by: vsedmik <46570670+vsedmik@users.noreply.github.com> --- robottelo/hosts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robottelo/hosts.py b/robottelo/hosts.py index cb052796172..ef04881b5e5 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -769,7 +769,7 @@ def register( raise ValueError('Global registration method can be used with Satellite/Capsule only') if lifecycle_environment is not None: - options['lifecycle_environment_id'] = lifecycle_environment.id + options['lifecycle-environment-id'] = lifecycle_environment.id if operating_system is not None: options['operatingsystem-id'] = operating_system.id if hostgroup is not None: From 9349ba2a6f9dbd456042d1bf4e5b197b420db24b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 9 Oct 2023 18:26:02 -0400 Subject: [PATCH 271/586] [6.14.z] Fixed test_positive_assign_http_proxy_to_products_repositories failure (#12856) --- tests/foreman/ui/test_http_proxy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/foreman/ui/test_http_proxy.py b/tests/foreman/ui/test_http_proxy.py index 2f48559cac2..abe02f12002 100644 --- a/tests/foreman/ui/test_http_proxy.py +++ b/tests/foreman/ui/test_http_proxy.py @@ -110,6 +110,8 @@ def test_positive_assign_http_proxy_to_products_repositories( # Create repositories from UI. with target_sat.ui_session() as session: repo_a1_name = gen_string('alpha') + session.organization.select(org_name=module_org.name) + session.location.select(loc_name=module_location.name) session.repository.create( product_a.name, { From a0d8e71ec741aecdfbc0ec7576285d7e0d3c70a7 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 9 Oct 2023 18:52:11 -0400 Subject: [PATCH 272/586] [6.14.z] Hosts: move from RHEL 7 to RHEL 8 for glob. registration (#12858) --- tests/foreman/ui/test_host.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index 1d3b734e2ea..27ec9ca39cd 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -1313,7 +1313,7 @@ def test_global_registration_form_populate( def test_global_registration_with_capsule_host( session, capsule_configured, - rhel7_contenthost, + rhel8_contenthost, module_org, module_location, module_product, @@ -1342,7 +1342,7 @@ def test_global_registration_with_capsule_host( :CaseAutomation: Automated """ - client = rhel7_contenthost + client = rhel8_contenthost repo = target_sat.api.Repository( url=settings.repos.yum_1.url, content_type=REPO_TYPE['yum'], @@ -1412,7 +1412,7 @@ def test_global_registration_with_capsule_host( @pytest.mark.usefixtures('enable_capsule_for_registration') @pytest.mark.no_containers def test_global_registration_with_gpg_repo_and_default_package( - session, module_activation_key, default_os, default_smart_proxy, rhel7_contenthost + session, module_activation_key, default_os, default_smart_proxy, rhel8_contenthost ): """Host registration form produces a correct registration command and host is registered successfully with gpg repo enabled and have default package @@ -1435,7 +1435,7 @@ def test_global_registration_with_gpg_repo_and_default_package( :parametrized: yes """ - client = rhel7_contenthost + client = rhel8_contenthost repo_name = 'foreman_register' repo_url = settings.repos.gr_yum_repo.url repo_gpg_url = settings.repos.gr_yum_repo.gpg_url @@ -1455,7 +1455,15 @@ def test_global_registration_with_gpg_repo_and_default_package( # rhel repo required for insights client installation, # syncing it to the satellite would take too long - client.create_custom_repos(rhel7=settings.repos.rhel7_os) + rhelver = client.os_version.major + if rhelver > 7: + repos = {f'rhel{rhelver}_os': settings.repos[f'rhel{rhelver}_os']['baseos']} + else: + repos = { + 'rhel7_os': settings.repos['rhel7_os'], + 'rhel7_extras': settings.repos['rhel7_extras'], + } + client.create_custom_repos(**repos) # run curl result = client.execute(cmd) assert result.status == 0 @@ -1571,7 +1579,7 @@ def test_global_re_registration_host_with_force_ignore_error_options( @pytest.mark.tier2 @pytest.mark.usefixtures('enable_capsule_for_registration') def test_global_registration_token_restriction( - session, module_activation_key, rhel7_contenthost, default_os, default_smart_proxy, target_sat + session, module_activation_key, rhel8_contenthost, default_os, default_smart_proxy, target_sat ): """Global registration token should be only used for registration call, it should be restricted for any other api calls. @@ -1589,7 +1597,7 @@ def test_global_registration_token_restriction( :parametrized: yes """ - client = rhel7_contenthost + client = rhel8_contenthost with session: cmd = session.host.get_register_command( { @@ -1609,7 +1617,7 @@ def test_global_registration_token_restriction( for curl_cmd in (curl_users, curl_hosts): result = client.execute(curl_cmd) assert result.status == 0 - 'Unable to authenticate user' in result.stdout + assert 'Unable to authenticate user' in result.stdout @pytest.mark.tier4 From 93c27530b4914c26ac5a4adb8819cdb1f8982ff9 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 10 Oct 2023 02:43:18 -0400 Subject: [PATCH 273/586] [6.14.z] Remove stubbed FM case that is no longer needed (#12824) Remove stubbed FM case that is no longer needed (cherry picked from commit ea10c6c88ab1ab79ff6f40b1ba7d7babb09f6f79) Co-authored-by: Griffin Sullivan --- tests/foreman/maintain/test_packages.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/tests/foreman/maintain/test_packages.py b/tests/foreman/maintain/test_packages.py index a27fb510697..ba288549ab0 100644 --- a/tests/foreman/maintain/test_packages.py +++ b/tests/foreman/maintain/test_packages.py @@ -273,22 +273,3 @@ def test_positive_fm_packages_update(request, sat_maintain): def _finalize(): assert sat_maintain.execute('dnf remove -y walrus').status == 0 sat_maintain.execute('rm -rf /etc/yum.repos.d/custom_repo.repo') - - -@pytest.mark.stubbed -def test_positive_fm_packages_sat_installer(sat_maintain): - """Verify satellite-installer is not executed after install/update - of satellite-maintain/rubygem-foreman_maintain package - - :id: d73971a1-68b4-4ab2-a87c-76cc5ff80a39 - - :steps: - 1. satellite-maintain packages install/update satellite-maintain/rubygem-foreman_maintain - - :BZ: 1825841 - - :expectedresults: satellite-installer shouldn't be executed after install/update - of satellite-maintain/rubygem-foreman_maintain package - - :CaseAutomation: ManualOnly - """ From 3421aea03f4ccabff88fe28011b56a79dd2cc8e1 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 10 Oct 2023 03:41:38 -0400 Subject: [PATCH 274/586] [6.14.z] update case component with correct name (#12860) update case component with correct name (#12859) (cherry picked from commit 4ad7af6552fbbe8beb63f3e1682f60fec009a83d) Co-authored-by: vijay sawant --- tests/upgrades/test_hostcontent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/upgrades/test_hostcontent.py b/tests/upgrades/test_hostcontent.py index 54b102679b9..172039c1930 100644 --- a/tests/upgrades/test_hostcontent.py +++ b/tests/upgrades/test_hostcontent.py @@ -6,7 +6,7 @@ :CaseLevel: Acceptance -:CaseComponent: Host-Content +:CaseComponent: Hosts-Content :Team: Phoenix-subscriptions From 255761b990c2b85d98bedb9b3327c65a64f92653 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 10 Oct 2023 05:15:37 -0400 Subject: [PATCH 275/586] [6.14.z] fixture support for virt-who config api : data_form deploy_type virtwho_config (#12865) fixture support for virt-who config api : data_form deploy_type virtwho_config (#12620) * fixture support for virt-who config api : data_form deploy_type virtwho_config * remove unused import (cherry picked from commit 059830c9194d1db1230285b0ab05846452cfd6d0) Co-authored-by: yanpliu --- tests/foreman/virtwho/api/test_esx.py | 209 ++++++++---------- tests/foreman/virtwho/api/test_esx_sca.py | 161 +++++--------- tests/foreman/virtwho/api/test_hyperv.py | 62 +----- tests/foreman/virtwho/api/test_hyperv_sca.py | 62 +----- tests/foreman/virtwho/api/test_kubevirt.py | 128 +---------- .../foreman/virtwho/api/test_kubevirt_sca.py | 60 +---- tests/foreman/virtwho/api/test_libvirt.py | 61 +---- tests/foreman/virtwho/api/test_libvirt_sca.py | 61 +---- tests/foreman/virtwho/api/test_nutanix.py | 124 ++++------- tests/foreman/virtwho/api/test_nutanix_sca.py | 87 ++------ 10 files changed, 278 insertions(+), 737 deletions(-) diff --git a/tests/foreman/virtwho/api/test_esx.py b/tests/foreman/virtwho/api/test_esx.py index 32a0b69e374..1e0396d4ce5 100644 --- a/tests/foreman/virtwho/api/test_esx.py +++ b/tests/foreman/virtwho/api/test_esx.py @@ -16,7 +16,6 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest from robottelo.config import settings @@ -25,54 +24,18 @@ create_http_proxy, deploy_configure_by_command, deploy_configure_by_command_check, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_option, - get_guest_info, ) -@pytest.fixture() -def form_data(default_org, target_sat): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.esx.hypervisor_type, - 'hypervisor_server': settings.virtwho.esx.hypervisor_server, - 'organization_id': default_org.id, - 'filtering_mode': 'none', - 'satellite_url': target_sat.hostname, - 'hypervisor_username': settings.virtwho.esx.hypervisor_username, - 'hypervisor_password': settings.virtwho.esx.hypervisor_password, - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() - yield virtwho_config - virtwho_config.delete() - assert not target_sat.api.VirtWhoConfig().search(query={'search': f"name={form_data['name']}"}) - - -@pytest.fixture(autouse=True) -def delete_host(form_data, target_sat): - guest_name, _ = get_guest_info(form_data['hypervisor_type']) - results = target_sat.api.Host().search(query={'search': guest_name}) - if results: - target_sat.api.Host(id=results[0].read_json()['id']).delete() - - -@pytest.mark.usefixtures('delete_host') +@pytest.mark.delete_host class TestVirtWhoConfigforEsx: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_api', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, default_org, form_data, virtwho_config, target_sat, deploy_type + self, default_org, target_sat, virtwho_config_api, deploy_type_api ): """Verify "POST /foreman_virt_who_configure/api/v2/configs" @@ -84,23 +47,11 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config.status == 'unknown' - if deploy_type == "id": - command = get_configure_command(virtwho_config.id, default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - elif deploy_type == "script": - script = virtwho_config.deploy_script() - hypervisor_name, guest_name = deploy_configure_by_script( - script['virt_who_config_script'], - form_data['hypervisor_type'], - debug=True, - org=default_org.label, - ) + assert virtwho_config_api.status == 'unknown' + hypervisor_name, guest_name = deploy_type_api virt_who_instance = ( target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] + .search(query={'search': f'name={virtwho_config_api.name}'})[0] .status ) assert virt_who_instance == 'ok' @@ -132,7 +83,9 @@ def test_positive_deploy_configure_by_id_script( assert result['subscription_status_label'] == 'Fully entitled' @pytest.mark.tier2 - def test_positive_debug_option(self, default_org, form_data, virtwho_config, target_sat): + def test_positive_debug_option( + self, default_org, form_data_api, virtwho_config_api, target_sat + ): """Verify debug option by "PUT /foreman_virt_who_configure/api/v2/configs/:id" @@ -147,16 +100,18 @@ def test_positive_debug_option(self, default_org, form_data, virtwho_config, tar """ options = {'true': '1', 'false': '0', '1': '1', '0': '0'} for key, value in sorted(options.items(), key=lambda item: item[0]): - virtwho_config.debug = key - virtwho_config.update(['debug']) - command = get_configure_command(virtwho_config.id, default_org.name) + virtwho_config_api.debug = key + virtwho_config_api.update(['debug']) + command = get_configure_command(virtwho_config_api.id, default_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=default_org.label + command, form_data_api['hypervisor_type'], org=default_org.label ) assert get_configure_option('debug', ETC_VIRTWHO_CONFIG) == value @pytest.mark.tier2 - def test_positive_interval_option(self, default_org, form_data, virtwho_config, target_sat): + def test_positive_interval_option( + self, default_org, form_data_api, virtwho_config_api, target_sat + ): """Verify interval option by "PUT /foreman_virt_who_configure/api/v2/configs/:id" @@ -180,17 +135,17 @@ def test_positive_interval_option(self, default_org, form_data, virtwho_config, '4320': '259200', } for key, value in sorted(options.items(), key=lambda item: int(item[0])): - virtwho_config.interval = key - virtwho_config.update(['interval']) - command = get_configure_command(virtwho_config.id, default_org.name) + virtwho_config_api.interval = key + virtwho_config_api.update(['interval']) + command = get_configure_command(virtwho_config_api.id, default_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=default_org.label + command, form_data_api['hypervisor_type'], org=default_org.label ) assert get_configure_option('interval', ETC_VIRTWHO_CONFIG) == value @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, default_org, form_data, virtwho_config, target_sat + self, default_org, form_data_api, virtwho_config_api, target_sat ): """Verify hypervisor_id option by "PUT @@ -207,17 +162,19 @@ def test_positive_hypervisor_id_option( # esx and rhevm support hwuuid option values = ['uuid', 'hostname', 'hwuuid'] for value in values: - virtwho_config.hypervisor_id = value - virtwho_config.update(['hypervisor_id']) - config_file = get_configure_file(virtwho_config.id) - command = get_configure_command(virtwho_config.id, default_org.name) + virtwho_config_api.hypervisor_id = value + virtwho_config_api.update(['hypervisor_id']) + config_file = get_configure_file(virtwho_config_api.id) + command = get_configure_command(virtwho_config_api.id, default_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=default_org.label + command, form_data_api['hypervisor_type'], org=default_org.label ) assert get_configure_option('hypervisor_id', config_file) == value @pytest.mark.tier2 - def test_positive_filter_option(self, default_org, form_data, virtwho_config, target_sat): + def test_positive_filter_option( + self, default_org, form_data_api, virtwho_config_api, target_sat + ): """Verify filter option by "PUT /foreman_virt_who_configure/api/v2/configs/:id" @@ -236,26 +193,30 @@ def test_positive_filter_option(self, default_org, form_data, virtwho_config, ta whitelist['filter_host_parents'] = '.*redhat.com' blacklist['exclude_host_parents'] = '.*redhat.com' # Update Whitelist and check the result - virtwho_config.filtering_mode = whitelist['filtering_mode'] - virtwho_config.whitelist = whitelist['whitelist'] - virtwho_config.filter_host_parents = whitelist['filter_host_parents'] - virtwho_config.update(whitelist.keys()) - config_file = get_configure_file(virtwho_config.id) - command = get_configure_command(virtwho_config.id, default_org.name) - deploy_configure_by_command(command, form_data['hypervisor_type'], org=default_org.label) + virtwho_config_api.filtering_mode = whitelist['filtering_mode'] + virtwho_config_api.whitelist = whitelist['whitelist'] + virtwho_config_api.filter_host_parents = whitelist['filter_host_parents'] + virtwho_config_api.update(whitelist.keys()) + config_file = get_configure_file(virtwho_config_api.id) + command = get_configure_command(virtwho_config_api.id, default_org.name) + deploy_configure_by_command( + command, form_data_api['hypervisor_type'], org=default_org.label + ) assert get_configure_option('filter_hosts', config_file) == whitelist['whitelist'] assert ( get_configure_option('filter_host_parents', config_file) == whitelist['filter_host_parents'] ) # Update Blacklist and check the result - virtwho_config.filtering_mode = blacklist['filtering_mode'] - virtwho_config.blacklist = blacklist['blacklist'] - virtwho_config.exclude_host_parents = blacklist['exclude_host_parents'] - virtwho_config.update(blacklist.keys()) - config_file = get_configure_file(virtwho_config.id) - command = get_configure_command(virtwho_config.id, default_org.name) - deploy_configure_by_command(command, form_data['hypervisor_type'], org=default_org.label) + virtwho_config_api.filtering_mode = blacklist['filtering_mode'] + virtwho_config_api.blacklist = blacklist['blacklist'] + virtwho_config_api.exclude_host_parents = blacklist['exclude_host_parents'] + virtwho_config_api.update(blacklist.keys()) + config_file = get_configure_file(virtwho_config_api.id) + command = get_configure_command(virtwho_config_api.id, default_org.name) + deploy_configure_by_command( + command, form_data_api['hypervisor_type'], org=default_org.label + ) assert get_configure_option('exclude_hosts', config_file) == blacklist['blacklist'] assert ( get_configure_option('exclude_host_parents', config_file) @@ -263,7 +224,9 @@ def test_positive_filter_option(self, default_org, form_data, virtwho_config, ta ) @pytest.mark.tier2 - def test_positive_proxy_option(self, default_org, form_data, virtwho_config, target_sat): + def test_positive_proxy_option( + self, default_org, form_data_api, virtwho_config_api, target_sat + ): """Verify http_proxy option by "PUT /foreman_virt_who_configure/api/v2/configs/:id"" @@ -278,8 +241,10 @@ def test_positive_proxy_option(self, default_org, form_data, virtwho_config, tar :BZ: 1902199 """ - command = get_configure_command(virtwho_config.id, default_org.name) - deploy_configure_by_command(command, form_data['hypervisor_type'], org=default_org.label) + command = get_configure_command(virtwho_config_api.id, default_org.name) + deploy_configure_by_command( + command, form_data_api['hypervisor_type'], org=default_org.label + ) # Check default NO_PROXY option assert get_configure_option('no_proxy', ETC_VIRTWHO_CONFIG) == '*' # Check HTTTP Proxy and No_PROXY option @@ -287,23 +252,27 @@ def test_positive_proxy_option(self, default_org, form_data, virtwho_config, tar http_type='http', org=default_org ) no_proxy = 'test.satellite.com' - virtwho_config.http_proxy_id = http_proxy_id - virtwho_config.no_proxy = no_proxy - virtwho_config.update(['http_proxy_id', 'no_proxy']) - command = get_configure_command(virtwho_config.id, default_org.name) - deploy_configure_by_command(command, form_data['hypervisor_type'], org=default_org.label) + virtwho_config_api.http_proxy_id = http_proxy_id + virtwho_config_api.no_proxy = no_proxy + virtwho_config_api.update(['http_proxy_id', 'no_proxy']) + command = get_configure_command(virtwho_config_api.id, default_org.name) + deploy_configure_by_command( + command, form_data_api['hypervisor_type'], org=default_org.label + ) assert get_configure_option('http_proxy', ETC_VIRTWHO_CONFIG) == http_proxy_url assert get_configure_option('no_proxy', ETC_VIRTWHO_CONFIG) == no_proxy # Check HTTTPs Proxy option https_proxy_url, https_proxy_name, https_proxy_id = create_http_proxy(org=default_org) - virtwho_config.http_proxy_id = https_proxy_id - virtwho_config.update(['http_proxy_id']) - deploy_configure_by_command(command, form_data['hypervisor_type'], org=default_org.label) + virtwho_config_api.http_proxy_id = https_proxy_id + virtwho_config_api.update(['http_proxy_id']) + deploy_configure_by_command( + command, form_data_api['hypervisor_type'], org=default_org.label + ) assert get_configure_option('https_proxy', ETC_VIRTWHO_CONFIG) == https_proxy_url @pytest.mark.tier2 def test_positive_configure_organization_list( - self, default_org, form_data, virtwho_config, target_sat + self, default_org, form_data_api, virtwho_config_api, target_sat ): """Verify "GET /foreman_virt_who_configure/ @@ -317,14 +286,16 @@ def test_positive_configure_organization_list( :CaseImportance: Medium """ - command = get_configure_command(virtwho_config.id, default_org.name) - deploy_configure_by_command(command, form_data['hypervisor_type'], org=default_org.label) - search_result = virtwho_config.get_organization_configs(data={'per_page': '1000'}) - assert [item for item in search_result['results'] if item['name'] == form_data['name']] + command = get_configure_command(virtwho_config_api.id, default_org.name) + deploy_configure_by_command( + command, form_data_api['hypervisor_type'], org=default_org.label + ) + search_result = virtwho_config_api.get_organization_configs(data={'per_page': '1000'}) + assert [item for item in search_result['results'] if item['name'] == form_data_api['name']] @pytest.mark.tier2 def test_positive_deploy_configure_hypervisor_password_with_special_characters( - self, default_org, form_data, target_sat + self, default_org, form_data_api, target_sat ): """Verify " hammer virt-who-config deploy hypervisor with special characters" @@ -341,25 +312,25 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( :customerscenario: true """ # check the hypervisor password contains single quotes - form_data['hypervisor_password'] = "Tes't" - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() - assert virtwho_config.status == 'unknown' - command = get_configure_command(virtwho_config.id, default_org.name) + form_data_api['hypervisor_password'] = "Tes't" + virtwho_config_api = target_sat.api.VirtWhoConfig(**form_data_api).create() + assert virtwho_config_api.status == 'unknown' + command = get_configure_command(virtwho_config_api.id, default_org.name) deploy_status = deploy_configure_by_command_check(command) assert deploy_status == 'Finished successfully' - config_file = get_configure_file(virtwho_config.id) + config_file = get_configure_file(virtwho_config_api.id) assert get_configure_option('rhsm_hostname', config_file) == target_sat.hostname assert ( get_configure_option('username', config_file) == settings.virtwho.esx.hypervisor_username ) - virtwho_config.delete() + virtwho_config_api.delete() assert not target_sat.api.VirtWhoConfig().search( - query={'search': f"name={form_data['name']}"} + query={'search': f"name={form_data_api['name']}"} ) # check the hypervisor password contains backtick - form_data['hypervisor_password'] = "my`password" - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() + form_data_api['hypervisor_password'] = "my`password" + virtwho_config = target_sat.api.VirtWhoConfig(**form_data_api).create() assert virtwho_config.status == 'unknown' command = get_configure_command(virtwho_config.id, default_org.name) deploy_status = deploy_configure_by_command_check(command) @@ -372,11 +343,13 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( ) virtwho_config.delete() assert not target_sat.api.VirtWhoConfig().search( - query={'search': f"name={form_data['name']}"} + query={'search': f"name={form_data_api['name']}"} ) @pytest.mark.tier2 - def test_positive_remove_env_option(self, default_org, form_data, virtwho_config, target_sat): + def test_positive_remove_env_option( + self, default_org, form_data_api, virtwho_config_api, target_sat + ): """remove option 'env=' from the virt-who configuration file and without any error :id: 981b6828-a7ed-46d9-9c6c-9fb22af4011e @@ -394,19 +367,19 @@ def test_positive_remove_env_option(self, default_org, form_data, virtwho_config :BZ: 1834897 """ - command = get_configure_command(virtwho_config.id, default_org.name) + command = get_configure_command(virtwho_config_api.id, default_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label + command, form_data_api['hypervisor_type'], debug=True, org=default_org.label ) virt_who_instance = ( target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] + .search(query={'search': f'name={virtwho_config_api.name}'})[0] .status ) assert virt_who_instance == 'ok' # Check the option "env=" should be removed from etc/virt-who.d/virt-who.conf option = "env" - config_file = get_configure_file(virtwho_config.id) + config_file = get_configure_file(virtwho_config_api.id) env_error = ( f"option {{\'{option}\'}} is not exist or not be enabled in {{\'{config_file}\'}}" ) diff --git a/tests/foreman/virtwho/api/test_esx_sca.py b/tests/foreman/virtwho/api/test_esx_sca.py index cd9cc3d3140..3a967cbded1 100644 --- a/tests/foreman/virtwho/api/test_esx_sca.py +++ b/tests/foreman/virtwho/api/test_esx_sca.py @@ -14,7 +14,6 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest from robottelo.config import settings @@ -23,45 +22,18 @@ create_http_proxy, deploy_configure_by_command, deploy_configure_by_command_check, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_option, ) -@pytest.fixture() -def form_data(module_sca_manifest_org, target_sat): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.esx.hypervisor_type, - 'hypervisor_server': settings.virtwho.esx.hypervisor_server, - 'organization_id': module_sca_manifest_org.id, - 'filtering_mode': 'none', - 'satellite_url': target_sat.hostname, - 'hypervisor_username': settings.virtwho.esx.hypervisor_username, - 'hypervisor_password': settings.virtwho.esx.hypervisor_password, - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() - yield virtwho_config - virtwho_config.delete() - assert not target_sat.api.VirtWhoConfig().search(query={'search': f"name={form_data['name']}"}) - - class TestVirtWhoConfigforEsx: @pytest.mark.tier2 @pytest.mark.upgrade - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_api', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat, deploy_type + self, module_sca_manifest_org, target_sat, virtwho_config_api, deploy_type_api ): """Verify "POST /foreman_virt_who_configure/api/v2/configs" @@ -73,30 +45,17 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config.status == 'unknown' - if deploy_type == "id": - command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) - deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=module_sca_manifest_org.label - ) - elif deploy_type == "script": - script = virtwho_config.deploy_script() - deploy_configure_by_script( - script['virt_who_config_script'], - form_data['hypervisor_type'], - debug=True, - org=module_sca_manifest_org.label, - ) + assert virtwho_config_api.status == 'unknown' virt_who_instance = ( target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] + .search(query={'search': f'name={virtwho_config_api.name}'})[0] .status ) assert virt_who_instance == 'ok' @pytest.mark.tier2 def test_positive_debug_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_api, virtwho_config_api, target_sat ): """Verify debug option by "PUT @@ -112,17 +71,17 @@ def test_positive_debug_option( """ options = {'0': '0', '1': '1', 'false': '0', 'true': '1'} for key, value in options.items(): - virtwho_config.debug = key - virtwho_config.update(['debug']) - command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) + virtwho_config_api.debug = key + virtwho_config_api.update(['debug']) + command = get_configure_command(virtwho_config_api.id, module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_api['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('debug', ETC_VIRTWHO_CONFIG) == value @pytest.mark.tier2 def test_positive_interval_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_api, virtwho_config_api, target_sat ): """Verify interval option by "PUT @@ -147,17 +106,17 @@ def test_positive_interval_option( '4320': '259200', } for key, value in options.items(): - virtwho_config.interval = key - virtwho_config.update(['interval']) - command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) + virtwho_config_api.interval = key + virtwho_config_api.update(['interval']) + command = get_configure_command(virtwho_config_api.id, module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_api['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('interval', ETC_VIRTWHO_CONFIG) == value @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_api, virtwho_config_api, target_sat ): """Verify hypervisor_id option by "PUT @@ -172,12 +131,12 @@ def test_positive_hypervisor_id_option( :CaseImportance: Medium """ for value in ['uuid', 'hostname']: - virtwho_config.hypervisor_id = value - virtwho_config.update(['hypervisor_id']) - config_file = get_configure_file(virtwho_config.id) - command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) + virtwho_config_api.hypervisor_id = value + virtwho_config_api.update(['hypervisor_id']) + config_file = get_configure_file(virtwho_config_api.id) + command = get_configure_command(virtwho_config_api.id, module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_api['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('hypervisor_id', config_file) == value @@ -185,7 +144,7 @@ def test_positive_hypervisor_id_option( @pytest.mark.parametrize('filter_type', ['whitelist', 'blacklist']) @pytest.mark.parametrize('option_type', ['edit', 'create']) def test_positive_filter_option( - self, module_sca_manifest_org, form_data, target_sat, filter_type, option_type + self, module_sca_manifest_org, form_data_api, target_sat, filter_type, option_type ): """Verify filter option by "PUT @@ -204,7 +163,7 @@ def test_positive_filter_option( regex = '.*redhat.com' whitelist = {'filtering_mode': '1', 'whitelist': regex} blacklist = {'filtering_mode': '2', 'blacklist': regex} - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() + virtwho_config = target_sat.api.VirtWhoConfig(**form_data_api).create() if option_type == "edit": if filter_type == "whitelist": whitelist['filter_host_parents'] = regex @@ -220,7 +179,7 @@ def test_positive_filter_option( virtwho_config.update(blacklist.keys()) command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_api['hypervisor_type'], org=module_sca_manifest_org.label ) config_file = get_configure_file(virtwho_config.id) result = target_sat.api.VirtWhoConfig().search( @@ -245,20 +204,20 @@ def test_positive_filter_option( elif option_type == "create": virtwho_config.delete() assert not target_sat.api.VirtWhoConfig().search( - query={'search': f"name={form_data['name']}"} + query={'search': f"name={form_data_api['name']}"} ) if filter_type == "whitelist": - form_data['filtering_mode'] = 1 - form_data['whitelist'] = regex - form_data['filter_host_parents'] = regex + form_data_api['filtering_mode'] = 1 + form_data_api['whitelist'] = regex + form_data_api['filter_host_parents'] = regex elif filter_type == "blacklist": - form_data['filtering_mode'] = 2 - form_data['blacklist'] = regex - form_data['exclude_host_parents'] = regex - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() + form_data_api['filtering_mode'] = 2 + form_data_api['blacklist'] = regex + form_data_api['exclude_host_parents'] = regex + virtwho_config = target_sat.api.VirtWhoConfig(**form_data_api).create() command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_api['hypervisor_type'], org=module_sca_manifest_org.label ) config_file = get_configure_file(virtwho_config.id) result = target_sat.api.VirtWhoConfig().search( @@ -283,7 +242,7 @@ def test_positive_filter_option( assert result.exclude_host_parents == regex @pytest.mark.tier2 - def test_positive_proxy_option(self, module_sca_manifest_org, form_data, target_sat): + def test_positive_proxy_option(self, module_sca_manifest_org, form_data_api, target_sat): """Verify http_proxy option by "PUT /foreman_virt_who_configure/api/v2/configs/:id"" @@ -300,10 +259,10 @@ def test_positive_proxy_option(self, module_sca_manifest_org, form_data, target_ :BZ: 1902199 """ - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() + virtwho_config = target_sat.api.VirtWhoConfig(**form_data_api).create() command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_api['hypervisor_type'], org=module_sca_manifest_org.label ) # Check default NO_PROXY option assert get_configure_option('no_proxy', ETC_VIRTWHO_CONFIG) == '*' @@ -317,7 +276,7 @@ def test_positive_proxy_option(self, module_sca_manifest_org, form_data, target_ virtwho_config.update(['http_proxy_id', 'no_proxy']) command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_api['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('http_proxy', ETC_VIRTWHO_CONFIG) == http_proxy_url assert get_configure_option('no_proxy', ETC_VIRTWHO_CONFIG) == no_proxy @@ -332,21 +291,21 @@ def test_positive_proxy_option(self, module_sca_manifest_org, form_data, target_ virtwho_config.http_proxy_id = https_proxy_id virtwho_config.update(['http_proxy_id']) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_api['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('https_proxy', ETC_VIRTWHO_CONFIG) == https_proxy_url virtwho_config.delete() assert not target_sat.api.VirtWhoConfig().search( - query={'search': f"name={form_data['name']}"} + query={'search': f"name={form_data_api['name']}"} ) # Check the http proxy option, create virt-who config via http proxy id - form_data['http_proxy_id'] = http_proxy_id - form_data['no_proxy'] = no_proxy - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() + form_data_api['http_proxy_id'] = http_proxy_id + form_data_api['no_proxy'] = no_proxy + virtwho_config = target_sat.api.VirtWhoConfig(**form_data_api).create() command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_api['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('http_proxy', ETC_VIRTWHO_CONFIG) == http_proxy_url assert get_configure_option('no_proxy', ETC_VIRTWHO_CONFIG) == no_proxy @@ -356,12 +315,12 @@ def test_positive_proxy_option(self, module_sca_manifest_org, form_data, target_ assert result.no_proxy == no_proxy virtwho_config.delete() assert not target_sat.api.VirtWhoConfig().search( - query={'search': f"name={form_data['name']}"} + query={'search': f"name={form_data_api['name']}"} ) @pytest.mark.tier2 def test_positive_configure_organization_list( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_api, virtwho_config_api, target_sat ): """Verify "GET /foreman_virt_who_configure/ @@ -375,16 +334,16 @@ def test_positive_configure_organization_list( :CaseImportance: Medium """ - command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) + command = get_configure_command(virtwho_config_api.id, module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_api['hypervisor_type'], org=module_sca_manifest_org.label ) - search_result = virtwho_config.get_organization_configs(data={'per_page': '1000'}) - assert [item for item in search_result['results'] if item['name'] == form_data['name']] + search_result = virtwho_config_api.get_organization_configs(data={'per_page': '1000'}) + assert [item for item in search_result['results'] if item['name'] == form_data_api['name']] @pytest.mark.tier2 def test_positive_deploy_configure_hypervisor_password_with_special_characters( - self, module_sca_manifest_org, form_data, target_sat + self, module_sca_manifest_org, form_data_api, target_sat ): """Verify "hammer virt-who-config deploy hypervisor with special characters" @@ -401,8 +360,8 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( :customerscenario: true """ # check the hypervisor password contains single quotes - form_data['hypervisor_password'] = "Tes't" - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() + form_data_api['hypervisor_password'] = "Tes't" + virtwho_config = target_sat.api.VirtWhoConfig(**form_data_api).create() assert virtwho_config.status == 'unknown' command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) deploy_status = deploy_configure_by_command_check(command) @@ -415,11 +374,11 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( ) virtwho_config.delete() assert not target_sat.api.VirtWhoConfig().search( - query={'search': f"name={form_data['name']}"} + query={'search': f"name={form_data_api['name']}"} ) # check the hypervisor password contains backtick - form_data['hypervisor_password'] = "my`password" - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() + form_data_api['hypervisor_password'] = "my`password" + virtwho_config = target_sat.api.VirtWhoConfig(**form_data_api).create() assert virtwho_config.status == 'unknown' command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) deploy_status = deploy_configure_by_command_check(command) @@ -432,12 +391,12 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( ) virtwho_config.delete() assert not target_sat.api.VirtWhoConfig().search( - query={'search': f"name={form_data['name']}"} + query={'search': f"name={form_data_api['name']}"} ) @pytest.mark.tier2 def test_positive_remove_env_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_api, virtwho_config_api, target_sat ): """remove option 'env=' from the virt-who configuration file and without any error @@ -456,19 +415,19 @@ def test_positive_remove_env_option( :BZ: 1834897 """ - command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) + command = get_configure_command(virtwho_config_api.id, module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=module_sca_manifest_org.label + command, form_data_api['hypervisor_type'], debug=True, org=module_sca_manifest_org.label ) virt_who_instance = ( target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] + .search(query={'search': f'name={virtwho_config_api.name}'})[0] .status ) assert virt_who_instance == 'ok' # Check the option "env=" should be removed from etc/virt-who.d/virt-who.conf option = "env" - config_file = get_configure_file(virtwho_config.id) + config_file = get_configure_file(virtwho_config_api.id) env_error = ( f"option {{\'{option}\'}} is not exist or not be enabled in {{\'{config_file}\'}}" ) diff --git a/tests/foreman/virtwho/api/test_hyperv.py b/tests/foreman/virtwho/api/test_hyperv.py index ec7c37ae6e7..e76e12f2669 100644 --- a/tests/foreman/virtwho/api/test_hyperv.py +++ b/tests/foreman/virtwho/api/test_hyperv.py @@ -16,50 +16,22 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_option, ) -@pytest.fixture() -def form_data(default_org, target_sat): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.hyperv.hypervisor_type, - 'hypervisor_server': settings.virtwho.hyperv.hypervisor_server, - 'organization_id': default_org.id, - 'filtering_mode': 'none', - 'satellite_url': target_sat.hostname, - 'hypervisor_username': settings.virtwho.hyperv.hypervisor_username, - 'hypervisor_password': settings.virtwho.hyperv.hypervisor_password, - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() - yield virtwho_config - virtwho_config.delete() - assert not target_sat.api.VirtWhoConfig().search(query={'search': f"name={form_data['name']}"}) - - class TestVirtWhoConfigforHyperv: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_api', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, default_org, form_data, virtwho_config, target_sat, deploy_type + self, default_org, virtwho_config_api, target_sat, deploy_type_api ): """Verify "POST /foreman_virt_who_configure/api/v2/configs" @@ -71,23 +43,11 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config.status == 'unknown' - if deploy_type == "id": - command = get_configure_command(virtwho_config.id, default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - elif deploy_type == "script": - script = virtwho_config.deploy_script() - hypervisor_name, guest_name = deploy_configure_by_script( - script['virt_who_config_script'], - form_data['hypervisor_type'], - debug=True, - org=default_org.label, - ) + assert virtwho_config_api.status == 'unknown' + hypervisor_name, guest_name = deploy_type_api virt_who_instance = ( target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] + .search(query={'search': f'name={virtwho_config_api.name}'})[0] .status ) assert virt_who_instance == 'ok' @@ -120,7 +80,7 @@ def test_positive_deploy_configure_by_id_script( @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, default_org, form_data, virtwho_config, target_sat + self, default_org, form_data_api, virtwho_config_api, target_sat ): """Verify hypervisor_id option by "PUT @@ -136,11 +96,11 @@ def test_positive_hypervisor_id_option( """ values = ['uuid', 'hostname'] for value in values: - virtwho_config.hypervisor_id = value - virtwho_config.update(['hypervisor_id']) - config_file = get_configure_file(virtwho_config.id) - command = get_configure_command(virtwho_config.id, default_org.name) + virtwho_config_api.hypervisor_id = value + virtwho_config_api.update(['hypervisor_id']) + config_file = get_configure_file(virtwho_config_api.id) + command = get_configure_command(virtwho_config_api.id, default_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=default_org.label + command, form_data_api['hypervisor_type'], org=default_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/api/test_hyperv_sca.py b/tests/foreman/virtwho/api/test_hyperv_sca.py index 67f18934c8d..68dd7c0e4f1 100644 --- a/tests/foreman/virtwho/api/test_hyperv_sca.py +++ b/tests/foreman/virtwho/api/test_hyperv_sca.py @@ -16,50 +16,21 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest -from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_option, ) -@pytest.fixture() -def form_data(module_sca_manifest_org, target_sat): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.hyperv.hypervisor_type, - 'hypervisor_server': settings.virtwho.hyperv.hypervisor_server, - 'organization_id': module_sca_manifest_org.id, - 'filtering_mode': 'none', - 'satellite_url': target_sat.hostname, - 'hypervisor_username': settings.virtwho.hyperv.hypervisor_username, - 'hypervisor_password': settings.virtwho.hyperv.hypervisor_password, - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() - yield virtwho_config - virtwho_config.delete() - assert not target_sat.api.VirtWhoConfig().search(query={'search': f"name={form_data['name']}"}) - - class TestVirtWhoConfigforHyperv: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_api', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat, deploy_type + self, module_sca_manifest_org, virtwho_config_api, target_sat, deploy_type_api ): """Verify "POST /foreman_virt_who_configure/api/v2/configs" @@ -71,30 +42,17 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config.status == 'unknown' - if deploy_type == "id": - command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) - deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=module_sca_manifest_org.label - ) - elif deploy_type == "script": - script = virtwho_config.deploy_script() - deploy_configure_by_script( - script['virt_who_config_script'], - form_data['hypervisor_type'], - debug=True, - org=module_sca_manifest_org.label, - ) + assert virtwho_config_api.status == 'unknown' virt_who_instance = ( target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] + .search(query={'search': f'name={virtwho_config_api.name}'})[0] .status ) assert virt_who_instance == 'ok' @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_api, virtwho_config_api, target_sat ): """Verify hypervisor_id option by "PUT @@ -109,11 +67,11 @@ def test_positive_hypervisor_id_option( :CaseImportance: Medium """ for value in ['uuid', 'hostname']: - virtwho_config.hypervisor_id = value - virtwho_config.update(['hypervisor_id']) - config_file = get_configure_file(virtwho_config.id) - command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) + virtwho_config_api.hypervisor_id = value + virtwho_config_api.update(['hypervisor_id']) + config_file = get_configure_file(virtwho_config_api.id) + command = get_configure_command(virtwho_config_api.id, module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_api['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/api/test_kubevirt.py b/tests/foreman/virtwho/api/test_kubevirt.py index e2fbe884f9a..88292e772db 100644 --- a/tests/foreman/virtwho/api/test_kubevirt.py +++ b/tests/foreman/virtwho/api/test_kubevirt.py @@ -16,58 +16,23 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_option, - get_guest_info, ) -@pytest.fixture() -def form_data(default_org, target_sat): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.kubevirt.hypervisor_type, - 'organization_id': default_org.id, - 'filtering_mode': 'none', - 'satellite_url': target_sat.hostname, - 'kubeconfig_path': settings.virtwho.kubevirt.hypervisor_config_file, - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() - yield virtwho_config - virtwho_config.delete() - assert not target_sat.api.VirtWhoConfig().search(query={'search': f"name={form_data['name']}"}) - - -@pytest.fixture(autouse=True) -def delete_host(form_data, target_sat): - guest_name, _ = get_guest_info(form_data['hypervisor_type']) - results = target_sat.api.Host().search(query={'search': guest_name}) - if results: - target_sat.api.Host(id=results[0].read_json()['id']).delete() - - -@pytest.mark.usefixtures('delete_host') +@pytest.mark.delete_host class TestVirtWhoConfigforKubevirt: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_api', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, default_org, form_data, virtwho_config, target_sat, deploy_type + self, default_org, virtwho_config_api, target_sat, deploy_type_api ): """Verify "POST /foreman_virt_who_configure/api/v2/configs" @@ -79,80 +44,11 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config.status == 'unknown' - if deploy_type == "id": - command = get_configure_command(virtwho_config.id, default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - elif deploy_type == "script": - script = virtwho_config.deploy_script() - hypervisor_name, guest_name = deploy_configure_by_script( - script['virt_who_config_script'], - form_data['hypervisor_type'], - debug=True, - org=default_org.label, - ) - virt_who_instance = ( - target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] - .status - ) - assert virt_who_instance == 'ok' - hosts = [ - ( - hypervisor_name, - f'product_id={settings.virtwho.sku.vdc_physical} and type=NORMAL', - ), - ( - guest_name, - f'product_id={settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED', - ), - ] - for hostname, sku in hosts: - host = target_sat.cli.Host.list({'search': hostname})[0] - subscriptions = target_sat.cli.Subscription.list( - {'organization': default_org.name, 'search': sku} - ) - vdc_id = subscriptions[0]['id'] - if 'type=STACK_DERIVED' in sku: - for item in subscriptions: - if hypervisor_name.lower() in item['type']: - vdc_id = item['id'] - break - target_sat.api.HostSubscription(host=host['id']).add_subscriptions( - data={'subscriptions': [{'id': vdc_id, 'quantity': 'Automatic'}]} - ) - result = target_sat.api.Host().search(query={'search': hostname})[0].read_json() - assert result['subscription_status_label'] == 'Fully entitled' - - @pytest.mark.tier2 - def test_positive_deploy_configure_by_script( - self, default_org, form_data, virtwho_config, target_sat - ): - """Verify "GET /foreman_virt_who_configure/api/ - - v2/configs/:id/deploy_script" - - :id: 77100dc7-644a-44a4-802a-2da562246cba - - :expectedresults: Config can be created and deployed - - :CaseLevel: Integration - - :CaseImportance: High - """ - assert virtwho_config.status == 'unknown' - script = virtwho_config.deploy_script() - hypervisor_name, guest_name = deploy_configure_by_script( - script['virt_who_config_script'], - form_data['hypervisor_type'], - debug=True, - org=default_org.label, - ) + assert virtwho_config_api.status == 'unknown' + hypervisor_name, guest_name = deploy_type_api virt_who_instance = ( target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] + .search(query={'search': f'name={virtwho_config_api.name}'})[0] .status ) assert virt_who_instance == 'ok' @@ -185,7 +81,7 @@ def test_positive_deploy_configure_by_script( @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, default_org, form_data, virtwho_config, target_sat + self, default_org, form_data_api, virtwho_config_api, target_sat ): """Verify hypervisor_id option by "PUT @@ -201,11 +97,11 @@ def test_positive_hypervisor_id_option( """ values = ['uuid', 'hostname'] for value in values: - virtwho_config.hypervisor_id = value - virtwho_config.update(['hypervisor_id']) - config_file = get_configure_file(virtwho_config.id) - command = get_configure_command(virtwho_config.id, default_org.name) + virtwho_config_api.hypervisor_id = value + virtwho_config_api.update(['hypervisor_id']) + config_file = get_configure_file(virtwho_config_api.id) + command = get_configure_command(virtwho_config_api.id, default_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=default_org.label + command, form_data_api['hypervisor_type'], org=default_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/api/test_kubevirt_sca.py b/tests/foreman/virtwho/api/test_kubevirt_sca.py index f64b719e9c4..364a637be5c 100644 --- a/tests/foreman/virtwho/api/test_kubevirt_sca.py +++ b/tests/foreman/virtwho/api/test_kubevirt_sca.py @@ -14,48 +14,21 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest -from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_option, ) -@pytest.fixture() -def form_data(module_sca_manifest_org, target_sat): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.kubevirt.hypervisor_type, - 'organization_id': module_sca_manifest_org.id, - 'filtering_mode': 'none', - 'satellite_url': target_sat.hostname, - 'kubeconfig_path': settings.virtwho.kubevirt.hypervisor_config_file, - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() - yield virtwho_config - virtwho_config.delete() - assert not target_sat.api.VirtWhoConfig().search(query={'search': f"name={form_data['name']}"}) - - class TestVirtWhoConfigforKubevirt: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_api', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat, deploy_type + self, module_sca_manifest_org, virtwho_config_api, target_sat, deploy_type_api ): """Verify "POST /foreman_virt_who_configure/api/v2/configs" @@ -67,30 +40,17 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config.status == 'unknown' - if deploy_type == "id": - command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) - deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=module_sca_manifest_org.label - ) - elif deploy_type == "script": - script = virtwho_config.deploy_script() - deploy_configure_by_script( - script['virt_who_config_script'], - form_data['hypervisor_type'], - debug=True, - org=module_sca_manifest_org.label, - ) + assert virtwho_config_api.status == 'unknown' virt_who_instance = ( target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] + .search(query={'search': f'name={virtwho_config_api.name}'})[0] .status ) assert virt_who_instance == 'ok' @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_api, virtwho_config_api, target_sat ): """Verify hypervisor_id option by "PUT @@ -105,11 +65,11 @@ def test_positive_hypervisor_id_option( :CaseImportance: Medium """ for value in ['uuid', 'hostname']: - virtwho_config.hypervisor_id = value - virtwho_config.update(['hypervisor_id']) - config_file = get_configure_file(virtwho_config.id) - command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) + virtwho_config_api.hypervisor_id = value + virtwho_config_api.update(['hypervisor_id']) + config_file = get_configure_file(virtwho_config_api.id) + command = get_configure_command(virtwho_config_api.id, module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_api['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/api/test_libvirt.py b/tests/foreman/virtwho/api/test_libvirt.py index eb0806e5753..2d05ebd5e49 100644 --- a/tests/foreman/virtwho/api/test_libvirt.py +++ b/tests/foreman/virtwho/api/test_libvirt.py @@ -16,49 +16,22 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_option, ) -@pytest.fixture() -def form_data(default_org, target_sat): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.libvirt.hypervisor_type, - 'hypervisor_server': settings.virtwho.libvirt.hypervisor_server, - 'organization_id': default_org.id, - 'filtering_mode': 'none', - 'satellite_url': target_sat.hostname, - 'hypervisor_username': settings.virtwho.libvirt.hypervisor_username, - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() - yield virtwho_config - virtwho_config.delete() - assert not target_sat.api.VirtWhoConfig().search(query={'search': f"name={form_data['name']}"}) - - class TestVirtWhoConfigforLibvirt: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_api', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, default_org, form_data, virtwho_config, target_sat, deploy_type + self, default_org, virtwho_config_api, target_sat, deploy_type_api ): """Verify "POST /foreman_virt_who_configure/api/v2/configs" @@ -70,23 +43,11 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config.status == 'unknown' - if deploy_type == "id": - command = get_configure_command(virtwho_config.id, default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - elif deploy_type == "script": - script = virtwho_config.deploy_script() - hypervisor_name, guest_name = deploy_configure_by_script( - script['virt_who_config_script'], - form_data['hypervisor_type'], - debug=True, - org=default_org.label, - ) + assert virtwho_config_api.status == 'unknown' + hypervisor_name, guest_name = deploy_type_api virt_who_instance = ( target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] + .search(query={'search': f'name={virtwho_config_api.name}'})[0] .status ) assert virt_who_instance == 'ok' @@ -119,7 +80,7 @@ def test_positive_deploy_configure_by_id_script( @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, default_org, form_data, virtwho_config, target_sat + self, default_org, form_data_api, virtwho_config_api, target_sat ): """Verify hypervisor_id option by "PUT @@ -135,11 +96,11 @@ def test_positive_hypervisor_id_option( """ values = ['uuid', 'hostname'] for value in values: - virtwho_config.hypervisor_id = value - virtwho_config.update(['hypervisor_id']) - config_file = get_configure_file(virtwho_config.id) - command = get_configure_command(virtwho_config.id, default_org.name) + virtwho_config_api.hypervisor_id = value + virtwho_config_api.update(['hypervisor_id']) + config_file = get_configure_file(virtwho_config_api.id) + command = get_configure_command(virtwho_config_api.id, default_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=default_org.label + command, form_data_api['hypervisor_type'], org=default_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/api/test_libvirt_sca.py b/tests/foreman/virtwho/api/test_libvirt_sca.py index bead5b8e95f..d805b4da5ef 100644 --- a/tests/foreman/virtwho/api/test_libvirt_sca.py +++ b/tests/foreman/virtwho/api/test_libvirt_sca.py @@ -14,49 +14,21 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest -from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_option, ) -@pytest.fixture() -def form_data(module_sca_manifest_org, target_sat): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.libvirt.hypervisor_type, - 'hypervisor_server': settings.virtwho.libvirt.hypervisor_server, - 'organization_id': module_sca_manifest_org.id, - 'filtering_mode': 'none', - 'satellite_url': target_sat.hostname, - 'hypervisor_username': settings.virtwho.libvirt.hypervisor_username, - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() - yield virtwho_config - virtwho_config.delete() - assert not target_sat.api.VirtWhoConfig().search(query={'search': f"name={form_data['name']}"}) - - class TestVirtWhoConfigforLibvirt: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_api', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat, deploy_type + self, module_sca_manifest_org, virtwho_config_api, target_sat, deploy_type_api ): """Verify "POST /foreman_virt_who_configure/api/v2/configs" @@ -68,30 +40,17 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config.status == 'unknown' - if deploy_type == "id": - command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) - deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=module_sca_manifest_org.label - ) - elif deploy_type == "script": - script = virtwho_config.deploy_script() - deploy_configure_by_script( - script['virt_who_config_script'], - form_data['hypervisor_type'], - debug=True, - org=module_sca_manifest_org.label, - ) + assert virtwho_config_api.status == 'unknown' virt_who_instance = ( target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] + .search(query={'search': f'name={virtwho_config_api.name}'})[0] .status ) assert virt_who_instance == 'ok' @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_api, virtwho_config_api, target_sat ): """Verify hypervisor_id option by "PUT /foreman_virt_who_configure/api/v2/configs/:id" @@ -104,11 +63,11 @@ def test_positive_hypervisor_id_option( :CaseImportance: Medium """ for value in ['uuid', 'hostname']: - virtwho_config.hypervisor_id = value - virtwho_config.update(['hypervisor_id']) - config_file = get_configure_file(virtwho_config.id) - command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) + virtwho_config_api.hypervisor_id = value + virtwho_config_api.update(['hypervisor_id']) + config_file = get_configure_file(virtwho_config_api.id) + command = get_configure_command(virtwho_config_api.id, module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_api['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/api/test_nutanix.py b/tests/foreman/virtwho/api/test_nutanix.py index 5e4d4c51f34..d9ddc34938f 100644 --- a/tests/foreman/virtwho/api/test_nutanix.py +++ b/tests/foreman/virtwho/api/test_nutanix.py @@ -16,7 +16,6 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest from robottelo.config import settings @@ -27,53 +26,16 @@ get_configure_command, get_configure_file, get_configure_option, - get_guest_info, get_hypervisor_ahv_mapping, ) -@pytest.fixture() -def form_data(default_org, target_sat): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.ahv.hypervisor_type, - 'hypervisor_server': settings.virtwho.ahv.hypervisor_server, - 'organization_id': default_org.id, - 'filtering_mode': 'none', - 'satellite_url': target_sat.hostname, - 'hypervisor_username': settings.virtwho.ahv.hypervisor_username, - 'hypervisor_password': settings.virtwho.ahv.hypervisor_password, - 'prism_flavor': settings.virtwho.ahv.prism_flavor, - 'ahv_internal_debug': 'false', - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() - yield virtwho_config - virtwho_config.delete() - assert not target_sat.api.VirtWhoConfig().search(query={'search': f"name={form_data['name']}"}) - - -@pytest.fixture(autouse=True) -def delete_host(form_data, target_sat): - guest_name, _ = get_guest_info(form_data['hypervisor_type']) - results = target_sat.api.Host().search(query={'search': guest_name}) - if results: - target_sat.api.Host(id=results[0].read_json()['id']).delete() - - -@pytest.mark.usefixtures('delete_host') +@pytest.mark.delete_host class TestVirtWhoConfigforNutanix: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_api', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, default_org, form_data, virtwho_config, target_sat, deploy_type + self, default_org, virtwho_config_api, target_sat, deploy_type_api ): """Verify "POST /foreman_virt_who_configure/api/v2/configs" @@ -85,23 +47,11 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config.status == 'unknown' - if deploy_type == "id": - command = get_configure_command(virtwho_config.id, default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - elif deploy_type == "script": - script = virtwho_config.deploy_script() - hypervisor_name, guest_name = deploy_configure_by_script( - script['virt_who_config_script'], - form_data['hypervisor_type'], - debug=True, - org=default_org.label, - ) + assert virtwho_config_api.status == 'unknown' + hypervisor_name, guest_name = deploy_type_api virt_who_instance = ( target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] + .search(query={'search': f'name={virtwho_config_api.name}'})[0] .status ) assert virt_who_instance == 'ok' @@ -138,7 +88,7 @@ def test_positive_deploy_configure_by_id_script( @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, default_org, form_data, virtwho_config, target_sat + self, default_org, form_data_api, virtwho_config_api, target_sat ): """Verify hypervisor_id option by "PUT @@ -154,19 +104,19 @@ def test_positive_hypervisor_id_option( """ values = ['uuid', 'hostname'] for value in values: - virtwho_config.hypervisor_id = value - virtwho_config.update(['hypervisor_id']) - config_file = get_configure_file(virtwho_config.id) - command = get_configure_command(virtwho_config.id, default_org.name) + virtwho_config_api.hypervisor_id = value + virtwho_config_api.update(['hypervisor_id']) + config_file = get_configure_file(virtwho_config_api.id) + command = get_configure_command(virtwho_config_api.id, default_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=default_org.label + command, form_data_api['hypervisor_type'], org=default_org.label ) assert get_configure_option('hypervisor_id', config_file) == value @pytest.mark.tier2 @pytest.mark.parametrize('deploy_type', ['id', 'script']) def test_positive_prism_central_deploy_configure_by_id_script( - self, default_org, form_data, target_sat, deploy_type + self, default_org, form_data_api, target_sat, deploy_type ): """Verify "POST /foreman_virt_who_configure/api/v2/configs" on nutanix prism central mode @@ -180,19 +130,19 @@ def test_positive_prism_central_deploy_configure_by_id_script( :CaseImportance: High """ - form_data['prism_flavor'] = "central" - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() + form_data_api['prism_flavor'] = "central" + virtwho_config = target_sat.api.VirtWhoConfig(**form_data_api).create() assert virtwho_config.status == 'unknown' if deploy_type == "id": command = get_configure_command(virtwho_config.id, default_org.name) hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label + command, form_data_api['hypervisor_type'], debug=True, org=default_org.label ) elif deploy_type == "script": script = virtwho_config.deploy_script() hypervisor_name, guest_name = deploy_configure_by_script( script['virt_who_config_script'], - form_data['hypervisor_type'], + form_data_api['hypervisor_type'], debug=True, org=default_org.label, ) @@ -238,7 +188,7 @@ def test_positive_prism_central_deploy_configure_by_id_script( @pytest.mark.tier2 def test_positive_prism_central_prism_central_option( - self, default_org, form_data, virtwho_config, target_sat + self, default_org, form_data_api, virtwho_config_api, target_sat ): """Verify prism_flavor option by "PUT @@ -253,16 +203,18 @@ def test_positive_prism_central_prism_central_option( :CaseImportance: Medium """ value = 'central' - virtwho_config.prism_flavor = value - virtwho_config.update(['prism_flavor']) - config_file = get_configure_file(virtwho_config.id) - command = get_configure_command(virtwho_config.id, default_org.name) - deploy_configure_by_command(command, form_data['hypervisor_type'], org=default_org.label) + virtwho_config_api.prism_flavor = value + virtwho_config_api.update(['prism_flavor']) + config_file = get_configure_file(virtwho_config_api.id) + command = get_configure_command(virtwho_config_api.id, default_org.name) + deploy_configure_by_command( + command, form_data_api['hypervisor_type'], org=default_org.label + ) assert get_configure_option("prism_central", config_file) == 'true' @pytest.mark.tier2 def test_positive_ahv_internal_debug_option( - self, default_org, form_data, virtwho_config, target_sat + self, default_org, form_data_api, virtwho_config_api, target_sat ): """Verify ahv_internal_debug option by hammer virt-who-config" @@ -284,18 +236,18 @@ def test_positive_ahv_internal_debug_option( :customerscenario: true """ - command = get_configure_command(virtwho_config.id, default_org.name) + command = get_configure_command(virtwho_config_api.id, default_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label + command, form_data_api['hypervisor_type'], debug=True, org=default_org.label ) result = ( target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] + .search(query={'search': f'name={virtwho_config_api.name}'})[0] .ahv_internal_debug ) assert str(result) == 'False' # ahv_internal_debug does not set in virt-who-config-X.conf - config_file = get_configure_file(virtwho_config.id) + config_file = get_configure_file(virtwho_config_api.id) option = 'ahv_internal_debug' env_error = f"option {option} is not exist or not be enabled in {config_file}" try: @@ -308,21 +260,23 @@ def test_positive_ahv_internal_debug_option( # Update ahv_internal_debug option to true value = 'true' - virtwho_config.ahv_internal_debug = value - virtwho_config.update(['ahv_internal_debug']) - command = get_configure_command(virtwho_config.id, default_org.name) + virtwho_config_api.ahv_internal_debug = value + virtwho_config_api.update(['ahv_internal_debug']) + command = get_configure_command(virtwho_config_api.id, default_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label + command, form_data_api['hypervisor_type'], debug=True, org=default_org.label + ) + assert ( + get_hypervisor_ahv_mapping(form_data_api['hypervisor_type']) == 'Host UUID found for VM' ) - assert get_hypervisor_ahv_mapping(form_data['hypervisor_type']) == 'Host UUID found for VM' result = ( target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] + .search(query={'search': f'name={virtwho_config_api.name}'})[0] .ahv_internal_debug ) assert str(result) == 'True' # ahv_internal_debug bas been set to true in virt-who-config-X.conf - config_file = get_configure_file(virtwho_config.id) + config_file = get_configure_file(virtwho_config_api.id) assert get_configure_option("ahv_internal_debug", config_file) == 'true' # check message does not exist in log file /var/log/rhsm/rhsm.log message = 'Value for "ahv_internal_debug" not set, using default: False' diff --git a/tests/foreman/virtwho/api/test_nutanix_sca.py b/tests/foreman/virtwho/api/test_nutanix_sca.py index 05287279231..c075db7f795 100644 --- a/tests/foreman/virtwho/api/test_nutanix_sca.py +++ b/tests/foreman/virtwho/api/test_nutanix_sca.py @@ -16,10 +16,8 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest -from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, deploy_configure_by_script, @@ -29,38 +27,11 @@ ) -@pytest.fixture() -def form_data(module_sca_manifest_org, target_sat): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.ahv.hypervisor_type, - 'hypervisor_server': settings.virtwho.ahv.hypervisor_server, - 'organization_id': module_sca_manifest_org.id, - 'filtering_mode': 'none', - 'satellite_url': target_sat.hostname, - 'hypervisor_username': settings.virtwho.ahv.hypervisor_username, - 'hypervisor_password': settings.virtwho.ahv.hypervisor_password, - 'prism_flavor': settings.virtwho.ahv.prism_flavor, - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() - yield virtwho_config - virtwho_config.delete() - assert not target_sat.api.VirtWhoConfig().search(query={'search': f"name={form_data['name']}"}) - - class TestVirtWhoConfigforNutanix: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_api', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat, deploy_type + self, default_org, virtwho_config_api, target_sat, deploy_type_api ): """Verify "POST /foreman_virt_who_configure/api/v2/configs" @@ -72,30 +43,17 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config.status == 'unknown' - if deploy_type == "id": - command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) - deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=module_sca_manifest_org.label - ) - elif deploy_type == "script": - script = virtwho_config.deploy_script() - deploy_configure_by_script( - script['virt_who_config_script'], - form_data['hypervisor_type'], - debug=True, - org=module_sca_manifest_org.label, - ) + assert virtwho_config_api.status == 'unknown' virt_who_instance = ( target_sat.api.VirtWhoConfig() - .search(query={'search': f'name={virtwho_config.name}'})[0] + .search(query={'search': f'name={virtwho_config_api.name}'})[0] .status ) assert virt_who_instance == 'ok' @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_api, virtwho_config_api, target_sat ): """Verify hypervisor_id option by "PUT @@ -110,19 +68,19 @@ def test_positive_hypervisor_id_option( :CaseImportance: Medium """ for value in ['uuid', 'hostname']: - virtwho_config.hypervisor_id = value - virtwho_config.update(['hypervisor_id']) - config_file = get_configure_file(virtwho_config.id) - command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) + virtwho_config_api.hypervisor_id = value + virtwho_config_api.update(['hypervisor_id']) + config_file = get_configure_file(virtwho_config_api.id) + command = get_configure_command(virtwho_config_api.id, module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_api['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('hypervisor_id', config_file) == value @pytest.mark.tier2 @pytest.mark.parametrize('deploy_type', ['id', 'script']) def test_positive_prism_central_deploy_configure_by_id_script( - self, module_sca_manifest_org, form_data, target_sat, deploy_type + self, module_sca_manifest_org, form_data_api, target_sat, deploy_type ): """Verify "POST /foreman_virt_who_configure/api/v2/configs" on nutanix prism central mode @@ -136,19 +94,22 @@ def test_positive_prism_central_deploy_configure_by_id_script( :CaseImportance: High """ - form_data['prism_flavor'] = "central" - virtwho_config = target_sat.api.VirtWhoConfig(**form_data).create() + form_data_api['prism_flavor'] = "central" + virtwho_config = target_sat.api.VirtWhoConfig(**form_data_api).create() assert virtwho_config.status == 'unknown' if deploy_type == "id": command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=module_sca_manifest_org.label + command, + form_data_api['hypervisor_type'], + debug=True, + org=module_sca_manifest_org.label, ) elif deploy_type == "script": script = virtwho_config.deploy_script() deploy_configure_by_script( script['virt_who_config_script'], - form_data['hypervisor_type'], + form_data_api['hypervisor_type'], debug=True, org=module_sca_manifest_org.label, ) @@ -164,7 +125,7 @@ def test_positive_prism_central_deploy_configure_by_id_script( @pytest.mark.tier2 def test_positive_prism_central_prism_central_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_api, virtwho_config_api, target_sat ): """Verify prism_flavor option by "PUT @@ -179,11 +140,11 @@ def test_positive_prism_central_prism_central_option( :CaseImportance: Medium """ value = 'central' - virtwho_config.prism_flavor = value - virtwho_config.update(['prism_flavor']) - config_file = get_configure_file(virtwho_config.id) - command = get_configure_command(virtwho_config.id, module_sca_manifest_org.name) + virtwho_config_api.prism_flavor = value + virtwho_config_api.update(['prism_flavor']) + config_file = get_configure_file(virtwho_config_api.id) + command = get_configure_command(virtwho_config_api.id, module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_api['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option("prism_central", config_file) == 'true' From b61e5fe007de8acc0890c8bdca8439d91779093b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 10 Oct 2023 15:06:08 -0400 Subject: [PATCH 276/586] [6.14.z] Fixing loadbalancer e2e test (#12872) --- .../destructive/test_capsule_loadbalancer.py | 77 +++++++------------ 1 file changed, 27 insertions(+), 50 deletions(-) diff --git a/tests/foreman/destructive/test_capsule_loadbalancer.py b/tests/foreman/destructive/test_capsule_loadbalancer.py index 3d9fdf02604..04ba4614438 100644 --- a/tests/foreman/destructive/test_capsule_loadbalancer.py +++ b/tests/foreman/destructive/test_capsule_loadbalancer.py @@ -17,6 +17,7 @@ :Upstream: No """ import pytest +from wrapanapi import VmState from robottelo.config import settings from robottelo.constants import CLIENT_PORT, DataFile @@ -175,20 +176,20 @@ def loadbalancer_setup( @pytest.mark.e2e @pytest.mark.tier1 -def test_loadbalancer_register_client_using_ak_to_ha_proxy(loadbalancer_setup, rhel7_contenthost): - """Register the client using ak to the haproxy +def test_loadbalancer_install_package( + loadbalancer_setup, setup_capsules, rhel7_contenthost, module_org, module_location, request +): + r"""Install packages on a content host regardless of the registered capsule being available :id: bd3c2e50-18e2-4be7-8a7f-c32472e17c61 :Steps: 1. run `subscription-manager register --org=Your_Organization \ - --activationkey=Your_Activation_Key \ - --serverurl=https://loadbalancer.example.com:8443/rhsm \ - --baseurl=https://loadbalancer.example.com/pulp/content` - 2. Check which capsule the host got registered. - 3. Try package installation - 4. Remove the package and unregister the host - 5. Again register, verify it's the other capsule this time. + --activationkey=Your_Activation_Key \` + 2. Try package installation + 3. Check which capsule the host got registered. + 4. Remove the package + 5. Take down the capsule that the host was registered to 6. Try package installation again :expectedresults: The client should be get the package irrespective of the capsule @@ -196,20 +197,17 @@ def test_loadbalancer_register_client_using_ak_to_ha_proxy(loadbalancer_setup, r :CaseLevel: Integration """ - url = f'https://{loadbalancer_setup["setup_haproxy"]["haproxy"].hostname}' - server_url = f'{url}:8443/rhsm' - base_url = f'{url}/pulp/content' - - result = rhel7_contenthost.download_install_rpm( - repo_url=f'{url}/pub', package_name='katello-ca-consumer-latest.noarch' - ) - assert result.status == 0 - rhel7_contenthost.register_contenthost( - org=loadbalancer_setup['module_org'].label, - activation_key=loadbalancer_setup['content_for_client']['client_ak'].name, - serverurl=server_url, - baseurl=base_url, + # Register content host + result = rhel7_contenthost.register( + org=module_org, + loc=module_location, + activation_keys=loadbalancer_setup['content_for_client']['client_ak'].name, + target=setup_capsules['capsule_1'], + force=True, ) + assert result.status == 0, f'Failed to register host: {result.stderr}' + + # Try package installation result = rhel7_contenthost.execute('yum install -y tree') assert result.status == 0 @@ -227,42 +225,21 @@ def test_loadbalancer_register_client_using_ak_to_ha_proxy(loadbalancer_setup, r if loadbalancer_setup['setup_capsules']['capsule_1'].hostname in result.stdout else loadbalancer_setup['setup_capsules']['capsule_2'] ) - # Find the other capsule - for capsule in loadbalancer_setup['setup_capsules'].values(): - if registered_to_capsule != capsule: - other_capsule = capsule + # Remove the packages from the client result = rhel7_contenthost.execute('yum remove -y tree') assert result.status == 0 - # For other capsule - rhel7_contenthost.remove_katello_ca() - rhel7_contenthost.unregister() - - result = rhel7_contenthost.execute('rm -f katello-ca-consumer-latest.noarch.rpm') - assert result.status == 0 - - result = rhel7_contenthost.download_install_rpm( - repo_url=f'{url}/pub', package_name='katello-ca-consumer-latest.noarch' - ) - assert result.status == 0 - - rhel7_contenthost.register_contenthost( - org=loadbalancer_setup['module_org'].label, - activation_key=loadbalancer_setup['content_for_client']['client_ak'].name, - serverurl=server_url, - baseurl=base_url, - ) - result = rhel7_contenthost.execute('rpm -qa | grep katello-ca-consumer') - assert other_capsule.hostname in result.stdout + # Power off the capsule that the client is registered to + registered_to_capsule.power_control(state=VmState.STOPPED, ensure=True) + # Try package installation again result = rhel7_contenthost.execute('yum install -y tree') assert result.status == 0 - hosts = loadbalancer_setup['module_target_sat'].cli.Host.list( - {'organization-id': loadbalancer_setup['module_org'].id} - ) - assert rhel7_contenthost.hostname in [host['name'] for host in hosts] + @request.addfinalizer + def _finalize(): + registered_to_capsule.power_control(state=VmState.RUNNING, ensure=True) @pytest.mark.rhel_ver_match('[^6]') From cd15d43363a20ad0fc6e8644702eacaf3ece62c1 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 11 Oct 2023 04:36:12 -0400 Subject: [PATCH 277/586] [6.14.z] fixture support for virt-who config ui : data_form deploy_type virtwho_config (#12877) fixture support for virt-who config ui : data_form deploy_type virtwho_config (cherry picked from commit a1dade860f307866900cd533b3663cfb4ed4b6fc) Co-authored-by: yanpliu --- tests/foreman/virtwho/ui/test_esx.py | 346 ++++++++---------- tests/foreman/virtwho/ui/test_esx_sca.py | 289 ++++++--------- tests/foreman/virtwho/ui/test_hyperv.py | 74 +--- tests/foreman/virtwho/ui/test_hyperv_sca.py | 62 +--- tests/foreman/virtwho/ui/test_kubevirt.py | 72 +--- tests/foreman/virtwho/ui/test_kubevirt_sca.py | 60 +-- tests/foreman/virtwho/ui/test_libvirt.py | 73 +--- tests/foreman/virtwho/ui/test_libvirt_sca.py | 61 +-- tests/foreman/virtwho/ui/test_nutanix.py | 148 +++----- tests/foreman/virtwho/ui/test_nutanix_sca.py | 117 ++---- 10 files changed, 457 insertions(+), 845 deletions(-) diff --git a/tests/foreman/virtwho/ui/test_esx.py b/tests/foreman/virtwho/ui/test_esx.py index f38590b8ddf..2715febb34f 100644 --- a/tests/foreman/virtwho/ui/test_esx.py +++ b/tests/foreman/virtwho/ui/test_esx.py @@ -31,57 +31,22 @@ delete_configure_option, deploy_configure_by_command, deploy_configure_by_command_check, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_id, get_configure_option, - get_guest_info, get_virtwho_status, restart_virtwho_service, update_configure_option, ) -@pytest.fixture() -def form_data(): - form = { - 'debug': True, - 'interval': 'Every hour', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.esx.hypervisor_type, - 'hypervisor_content.server': settings.virtwho.esx.hypervisor_server, - 'hypervisor_content.username': settings.virtwho.esx.hypervisor_username, - 'hypervisor_content.password': settings.virtwho.esx.hypervisor_password, - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat, session): - name = gen_string('alpha') - form_data['name'] = name - with session: - session.virtwho_configure.create(form_data) - yield virtwho_config - session.virtwho_configure.delete(name) - assert not session.virtwho_configure.search(name) - - -@pytest.fixture(autouse=True) -def delete_host(form_data, target_sat): - guest_name, _ = get_guest_info(form_data['hypervisor_type']) - results = target_sat.api.Host().search(query={'search': guest_name}) - if results: - target_sat.api.Host(id=results[0].read_json()['id']).delete() - - -@pytest.mark.usefixtures('delete_host') +@pytest.mark.delete_host class TestVirtwhoConfigforEsx: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_ui', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, default_org, virtwho_config, session, form_data, deploy_type + self, default_org, org_session, form_data_ui, deploy_type_ui ): """Verify configure created and deployed with id|script. @@ -98,37 +63,28 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - name = form_data['name'] - values = session.virtwho_configure.read(name) - if deploy_type == "id": - command = values['deploy']['command'] - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - elif deploy_type == "script": - script = values['deploy']['script'] - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' - hypervisor_display_name = session.contenthost.search(hypervisor_name)[0]['Name'] + hypervisor_name, guest_name = deploy_type_ui + assert org_session.virtwho_configure.search(form_data_ui['name'])[0]['Status'] == 'ok' + hypervisor_display_name = org_session.contenthost.search(hypervisor_name)[0]['Name'] vdc_physical = f'product_id = {settings.virtwho.sku.vdc_physical} and type=NORMAL' vdc_virtual = f'product_id = {settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED' assert ( - session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions']['status'] + org_session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions'][ + 'status' + ] == 'Unsubscribed hypervisor' ) - session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) - assert session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' + org_session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) + assert org_session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' assert ( - session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] + org_session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] == 'Unentitled' ) - session.contenthost.add_subscription(guest_name, vdc_virtual) - assert session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' + org_session.contenthost.add_subscription(guest_name, vdc_virtual) + assert org_session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' @pytest.mark.tier2 - def test_positive_debug_option(self, default_org, virtwho_config, session, form_data): + def test_positive_debug_option(self, default_org, virtwho_config_ui, org_session, form_data_ui): """Verify debug checkbox and the value changes of VIRTWHO_DEBUG :id: adb435c4-d02b-47b6-89f5-dce9a4ff7939 @@ -141,23 +97,25 @@ def test_positive_debug_option(self, default_org, virtwho_config, session, form_ :CaseImportance: Medium """ - name = form_data['name'] + name = form_data_ui['name'] config_id = get_configure_id(name) config_command = get_configure_command(config_id, default_org.name) deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=default_org.label + config_command, form_data_ui['hypervisor_type'], org=default_org.label ) assert get_configure_option('debug', ETC_VIRTWHO_CONFIG) == '1' - session.virtwho_configure.edit(name, {'debug': False}) - results = session.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'debug': False}) + results = org_session.virtwho_configure.read(name) assert results['overview']['debug'] is False deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=default_org.label + config_command, form_data_ui['hypervisor_type'], org=default_org.label ) assert get_configure_option('debug', ETC_VIRTWHO_CONFIG) == '0' @pytest.mark.tier2 - def test_positive_interval_option(self, default_org, virtwho_config, session, form_data): + def test_positive_interval_option( + self, default_org, virtwho_config_ui, org_session, form_data_ui + ): """Verify interval dropdown options and the value changes of VIRTWHO_INTERVAL. :id: 731f8361-38d4-40b9-9530-8d785d61eaab @@ -170,7 +128,7 @@ def test_positive_interval_option(self, default_org, virtwho_config, session, fo :CaseImportance: Medium """ - name = form_data['name'] + name = form_data_ui['name'] config_id = get_configure_id(name) config_command = get_configure_command(config_id, default_org.name) intervals = { @@ -184,16 +142,18 @@ def test_positive_interval_option(self, default_org, virtwho_config, session, fo 'Every 3 days': '259200', } for option, value in sorted(intervals.items(), key=lambda item: int(item[1])): - session.virtwho_configure.edit(name, {'interval': option}) - results = session.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'interval': option}) + results = org_session.virtwho_configure.read(name) assert results['overview']['interval'] == option deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=default_org.label + config_command, form_data_ui['hypervisor_type'], org=default_org.label ) assert get_configure_option('interval', ETC_VIRTWHO_CONFIG) == value @pytest.mark.tier2 - def test_positive_hypervisor_id_option(self, default_org, virtwho_config, session, form_data): + def test_positive_hypervisor_id_option( + self, default_org, virtwho_config_ui, org_session, form_data_ui + ): """Verify Hypervisor ID dropdown options. :id: cc494bd9-51d9-452a-bfa9-5cdcafef5197 @@ -206,23 +166,25 @@ def test_positive_hypervisor_id_option(self, default_org, virtwho_config, sessio :CaseImportance: Medium """ - name = form_data['name'] + name = form_data_ui['name'] config_id = get_configure_id(name) config_command = get_configure_command(config_id, default_org.name) config_file = get_configure_file(config_id) # esx and rhevm support hwuuid option values = ['uuid', 'hostname', 'hwuuid'] for value in values: - session.virtwho_configure.edit(name, {'hypervisor_id': value}) - results = session.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'hypervisor_id': value}) + results = org_session.virtwho_configure.read(name) assert results['overview']['hypervisor_id'] == value deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=default_org.label + config_command, form_data_ui['hypervisor_type'], org=default_org.label ) assert get_configure_option('hypervisor_id', config_file) == value @pytest.mark.tier2 - def test_positive_filtering_option(self, default_org, virtwho_config, session, form_data): + def test_positive_filtering_option( + self, default_org, virtwho_config_ui, org_session, form_data_ui + ): """Verify Filtering dropdown options. :id: e17dda14-79cd-4cd2-8f29-60970b24a905 @@ -237,7 +199,7 @@ def test_positive_filtering_option(self, default_org, virtwho_config, session, f :BZ: 1735670 """ - name = form_data['name'] + name = form_data_ui['name'] config_id = get_configure_id(name) config_command = get_configure_command(config_id, default_org.name) config_file = get_configure_file(config_id) @@ -248,28 +210,28 @@ def test_positive_filtering_option(self, default_org, virtwho_config, session, f whitelist['filtering_content.filter_host_parents'] = regex blacklist['filtering_content.exclude_host_parents'] = regex # Update Whitelist and check the result - session.virtwho_configure.edit(name, whitelist) - results = session.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, whitelist) + results = org_session.virtwho_configure.read(name) assert results['overview']['filter_hosts'] == regex assert results['overview']['filter_host_parents'] == regex deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=default_org.label + config_command, form_data_ui['hypervisor_type'], org=default_org.label ) assert regex == get_configure_option('filter_hosts', config_file) assert regex == get_configure_option('filter_host_parents', config_file) # Update Blacklist and check the result - session.virtwho_configure.edit(name, blacklist) - results = session.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, blacklist) + results = org_session.virtwho_configure.read(name) assert results['overview']['exclude_hosts'] == regex assert results['overview']['exclude_host_parents'] == regex deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=default_org.label + config_command, form_data_ui['hypervisor_type'], org=default_org.label ) assert regex == get_configure_option('exclude_hosts', config_file) assert regex == get_configure_option('exclude_host_parents', config_file) @pytest.mark.tier2 - def test_positive_proxy_option(self, default_org, virtwho_config, session, form_data): + def test_positive_proxy_option(self, default_org, virtwho_config_ui, org_session, form_data_ui): """Verify 'HTTP Proxy' and 'Ignore Proxy' options. :id: 6659d577-0135-4bf0-81af-14b930011536 @@ -285,31 +247,31 @@ def test_positive_proxy_option(self, default_org, virtwho_config, session, form_ http_proxy, http_proxy_name, http_proxy_id = create_http_proxy( http_type='http', org=default_org ) - name = form_data['name'] + name = form_data_ui['name'] config_id = get_configure_id(name) config_command = get_configure_command(config_id, default_org.name) no_proxy = 'test.satellite.com' # Check the https proxy and No_PROXY settings - session.virtwho_configure.edit(name, {'proxy': https_proxy, 'no_proxy': no_proxy}) - results = session.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'proxy': https_proxy, 'no_proxy': no_proxy}) + results = org_session.virtwho_configure.read(name) assert results['overview']['proxy'] == https_proxy assert results['overview']['no_proxy'] == no_proxy deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=default_org.label + config_command, form_data_ui['hypervisor_type'], org=default_org.label ) assert get_configure_option('https_proxy', ETC_VIRTWHO_CONFIG) == https_proxy assert get_configure_option('no_proxy', ETC_VIRTWHO_CONFIG) == no_proxy # Check the http proxy setting - session.virtwho_configure.edit(name, {'proxy': http_proxy}) - results = session.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'proxy': http_proxy}) + results = org_session.virtwho_configure.read(name) assert results['overview']['proxy'] == http_proxy deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=default_org.label + config_command, form_data_ui['hypervisor_type'], org=default_org.label ) assert get_configure_option('http_proxy', ETC_VIRTWHO_CONFIG) == http_proxy @pytest.mark.tier2 - def test_positive_virtwho_roles(self, session): + def test_positive_virtwho_roles(self, org_session): """Verify the default roles for virtwho configure :id: cd6a5363-f9ba-4b52-892c-905634168fc5 @@ -337,14 +299,14 @@ def test_positive_virtwho_roles(self, session): }, 'Virt-who Viewer': {'Satellite virt who configure/config': ['view_virt_who_config']}, } - with session: + with org_session: for role_name, role_filters in roles.items(): - assert session.role.search(role_name)[0]['Name'] == role_name - assigned_permissions = session.filter.read_permissions(role_name) + assert org_session.role.search(role_name)[0]['Name'] == role_name + assigned_permissions = org_session.filter.read_permissions(role_name) assert sorted(assigned_permissions) == sorted(role_filters) @pytest.mark.tier2 - def test_positive_virtwho_configs_widget(self, default_org, session, form_data): + def test_positive_virtwho_configs_widget(self, default_org, org_session, form_data_ui): """Check if Virt-who Configurations Status Widget is working in the Dashboard UI :id: 5d61ce00-a640-4823-89d4-7b1d02b50ea6 @@ -363,38 +325,40 @@ def test_positive_virtwho_configs_widget(self, default_org, session, form_data): """ org_name = gen_string('alpha') name = gen_string('alpha') - form_data['name'] = name - with session: - session.organization.create({'name': org_name}) - session.organization.select(org_name) - session.virtwho_configure.create(form_data) + form_data_ui['name'] = name + with org_session: + org_session.organization.create({'name': org_name}) + org_session.organization.select(org_name) + org_session.virtwho_configure.create(form_data_ui) expected_values = [ {'Configuration Status': 'No Reports', 'Count': '1'}, {'Configuration Status': 'No Change', 'Count': '0'}, {'Configuration Status': 'OK', 'Count': '0'}, {'Configuration Status': 'Total Configurations', 'Count': '1'}, ] - values = session.dashboard.read('VirtWhoConfigStatus') + values = org_session.dashboard.read('VirtWhoConfigStatus') assert values['config_status'] == expected_values assert values['latest_config'] == 'No configuration found' # Check the 'Status' changed after deployed the virt-who config config_id = get_configure_id(name) config_command = get_configure_command(config_id, org_name) - deploy_configure_by_command(config_command, form_data['hypervisor_type'], org=org_name) - assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' + deploy_configure_by_command( + config_command, form_data_ui['hypervisor_type'], org=org_name + ) + assert org_session.virtwho_configure.search(name)[0]['Status'] == 'ok' expected_values = [ {'Configuration Status': 'No Reports', 'Count': '0'}, {'Configuration Status': 'No Change', 'Count': '0'}, {'Configuration Status': 'OK', 'Count': '1'}, {'Configuration Status': 'Total Configurations', 'Count': '1'}, ] - values = session.dashboard.read('VirtWhoConfigStatus') + values = org_session.dashboard.read('VirtWhoConfigStatus') assert values['config_status'] == expected_values assert values['latest_config'] == 'No configuration found' - session.organization.select("Default Organization") + org_session.organization.select("Default Organization") @pytest.mark.tier2 - def test_positive_delete_configure(self, default_org, session, form_data): + def test_positive_delete_configure(self, default_org, org_session, form_data_ui): """Verify when a config is deleted the associated user is deleted. :id: 0e66dcf6-dc64-4fb2-b8a9-518f5adfa800 @@ -410,22 +374,24 @@ def test_positive_delete_configure(self, default_org, session, form_data): """ name = gen_string('alpha') - form_data['name'] = name - with session: - session.virtwho_configure.create(form_data) + form_data_ui['name'] = name + with org_session: + org_session.virtwho_configure.create(form_data_ui) config_id = get_configure_id(name) config_command = get_configure_command(config_id, default_org.name) deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=default_org.label + config_command, form_data_ui['hypervisor_type'], org=default_org.label ) - assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' - session.virtwho_configure.delete(name) - assert not session.virtwho_configure.search(name) + assert org_session.virtwho_configure.search(name)[0]['Status'] == 'ok' + org_session.virtwho_configure.delete(name) + assert not org_session.virtwho_configure.search(name) restart_virtwho_service() assert get_virtwho_status() == 'logerror' @pytest.mark.tier2 - def test_positive_virtwho_reporter_role(self, default_org, session, test_name, form_data): + def test_positive_virtwho_reporter_role( + self, default_org, org_session, test_name, form_data_ui + ): """Verify the virt-who reporter role can TRULY work. :id: cd235ab0-d89c-464b-98d6-9d090ac40d8f @@ -438,9 +404,9 @@ def test_positive_virtwho_reporter_role(self, default_org, session, test_name, f username = gen_string('alpha') password = gen_string('alpha') config_name = gen_string('alpha') - with session: + with org_session: # Create an user - session.user.create( + org_session.user.create( { 'user.login': username, 'user.mail': valid_emails_list()[0], @@ -450,14 +416,14 @@ def test_positive_virtwho_reporter_role(self, default_org, session, test_name, f } ) # Create a virt-who config plugin - form_data['name'] = config_name - session.virtwho_configure.create(form_data) - values = session.virtwho_configure.read(config_name) + form_data_ui['name'] = config_name + org_session.virtwho_configure.create(form_data_ui) + values = org_session.virtwho_configure.read(config_name) command = values['deploy']['command'] deploy_configure_by_command( - command, form_data['hypervisor_type'], org=default_org.label + command, form_data_ui['hypervisor_type'], org=default_org.label ) - assert session.virtwho_configure.search(config_name)[0]['Status'] == 'ok' + assert org_session.virtwho_configure.search(config_name)[0]['Status'] == 'ok' # Update the virt-who config file config_id = get_configure_id(config_name) config_file = get_configure_file(config_id) @@ -467,19 +433,19 @@ def test_positive_virtwho_reporter_role(self, default_org, session, test_name, f restart_virtwho_service() assert get_virtwho_status() == 'logerror' # Check the permissioin of Virt-who Reporter - session.user.update(username, {'roles.resources.assigned': ['Virt-who Reporter']}) - assert session.user.search(username)[0]['Username'] == username - user = session.user.read(username) + org_session.user.update(username, {'roles.resources.assigned': ['Virt-who Reporter']}) + assert org_session.user.search(username)[0]['Username'] == username + user = org_session.user.read(username) assert user['roles']['resources']['assigned'] == ['Virt-who Reporter'] restart_virtwho_service() assert get_virtwho_status() == 'running' with Session(test_name, username, password) as newsession: assert not newsession.virtwho_configure.check_create_permission()['can_view'] - session.user.delete(username) - assert not session.user.search(username) + org_session.user.delete(username) + assert not org_session.user.search(username) @pytest.mark.tier2 - def test_positive_virtwho_viewer_role(self, default_org, session, test_name, form_data): + def test_positive_virtwho_viewer_role(self, default_org, org_session, test_name, form_data_ui): """Verify the virt-who viewer role can TRULY work. :id: bf3be2e4-3853-41cc-9b3e-c8677f0b8c5f @@ -492,9 +458,9 @@ def test_positive_virtwho_viewer_role(self, default_org, session, test_name, for username = gen_string('alpha') password = gen_string('alpha') config_name = gen_string('alpha') - with session: + with org_session: # Create an user - session.user.create( + org_session.user.create( { 'user.login': username, 'user.mail': valid_emails_list()[0], @@ -504,17 +470,17 @@ def test_positive_virtwho_viewer_role(self, default_org, session, test_name, for } ) # Create a virt-who config plugin - form_data['name'] = config_name - session.virtwho_configure.create(form_data) - values = session.virtwho_configure.read(config_name) + form_data_ui['name'] = config_name + org_session.virtwho_configure.create(form_data_ui) + values = org_session.virtwho_configure.read(config_name) command = values['deploy']['command'] deploy_configure_by_command( - command, form_data['hypervisor_type'], org=default_org.label + command, form_data_ui['hypervisor_type'], org=default_org.label ) - assert session.virtwho_configure.search(config_name)[0]['Status'] == 'ok' + assert org_session.virtwho_configure.search(config_name)[0]['Status'] == 'ok' # Check the permissioin of Virt-who Viewer - session.user.update(username, {'roles.resources.assigned': ['Virt-who Viewer']}) - user = session.user.read(username) + org_session.user.update(username, {'roles.resources.assigned': ['Virt-who Viewer']}) + user = org_session.user.read(username) assert user['roles']['resources']['assigned'] == ['Virt-who Viewer'] # Update the virt-who config file config_id = get_configure_id(config_name) @@ -535,11 +501,11 @@ def test_positive_virtwho_viewer_role(self, default_org, session, test_name, for assert not update_permission['can_edit'] newsession.virtwho_configure.read(config_name) # Delete the created user - session.user.delete(username) - assert not session.user.search(username) + org_session.user.delete(username) + assert not org_session.user.search(username) @pytest.mark.tier2 - def test_positive_virtwho_manager_role(self, default_org, session, test_name, form_data): + def test_positive_virtwho_manager_role(self, default_org, org_session, test_name, form_data_ui): """Verify the virt-who manager role can TRULY work. :id: a72023fb-7b23-4582-9adc-c5227dc7859c @@ -551,9 +517,9 @@ def test_positive_virtwho_manager_role(self, default_org, session, test_name, fo username = gen_string('alpha') password = gen_string('alpha') config_name = gen_string('alpha') - with session: + with org_session: # Create an user - session.user.create( + org_session.user.create( { 'user.login': username, 'user.mail': valid_emails_list()[0], @@ -563,28 +529,28 @@ def test_positive_virtwho_manager_role(self, default_org, session, test_name, fo } ) # Create a virt-who config plugin - form_data['name'] = config_name - session.virtwho_configure.create(form_data) - values = session.virtwho_configure.read(config_name) + form_data_ui['name'] = config_name + org_session.virtwho_configure.create(form_data_ui) + values = org_session.virtwho_configure.read(config_name) command = values['deploy']['command'] deploy_configure_by_command( - command, form_data['hypervisor_type'], org=default_org.label + command, form_data_ui['hypervisor_type'], org=default_org.label ) - assert session.virtwho_configure.search(config_name)[0]['Status'] == 'ok' + assert org_session.virtwho_configure.search(config_name)[0]['Status'] == 'ok' # Check the permissioin of Virt-who Manager - session.user.update(username, {'roles.resources.assigned': ['Virt-who Manager']}) - user = session.user.read(username) + org_session.user.update(username, {'roles.resources.assigned': ['Virt-who Manager']}) + user = org_session.user.read(username) assert user['roles']['resources']['assigned'] == ['Virt-who Manager'] with Session(test_name, username, password) as newsession: # create_virt_who_config new_virt_who_name = gen_string('alpha') - form_data['name'] = new_virt_who_name - newsession.virtwho_configure.create(form_data) + form_data_ui['name'] = new_virt_who_name + newsession.virtwho_configure.create(form_data_ui) # view_virt_who_config values = newsession.virtwho_configure.read(new_virt_who_name) command = values['deploy']['command'] deploy_configure_by_command( - command, form_data['hypervisor_type'], org=default_org.label + command, form_data_ui['hypervisor_type'], org=default_org.label ) assert newsession.virtwho_configure.search(new_virt_who_name)[0]['Status'] == 'ok' # edit_virt_who_config @@ -595,11 +561,11 @@ def test_positive_virtwho_manager_role(self, default_org, session, test_name, fo newsession.virtwho_configure.delete(modify_name) assert not newsession.virtwho_configure.search(modify_name) # Delete the created user - session.user.delete(username) - assert not session.user.search(username) + org_session.user.delete(username) + assert not org_session.user.search(username) @pytest.mark.tier2 - def test_positive_overview_label_name(self, default_org, form_data, session): + def test_positive_overview_label_name(self, default_org, form_data_ui, org_session): """Verify the label name on virt-who config Overview Page. :id: 21df8175-bb41-422e-a263-8677bc3a9565 @@ -613,20 +579,20 @@ def test_positive_overview_label_name(self, default_org, form_data, session): :CaseImportance: Medium """ name = gen_string('alpha') - form_data['name'] = name - hypervisor_type = form_data['hypervisor_type'] + form_data_ui['name'] = name + hypervisor_type = form_data_ui['hypervisor_type'] http_proxy_url, proxy_name, proxy_id = create_http_proxy(org=default_org) - form_data['proxy'] = http_proxy_url - form_data['no_proxy'] = 'test.satellite.com' + form_data_ui['proxy'] = http_proxy_url + form_data_ui['no_proxy'] = 'test.satellite.com' regex = '.*redhat.com' whitelist = {'filtering': 'Whitelist', 'filtering_content.filter_hosts': regex} blacklist = {'filtering': 'Blacklist', 'filtering_content.exclude_hosts': regex} if hypervisor_type == 'esx': whitelist['filtering_content.filter_host_parents'] = regex blacklist['filtering_content.exclude_host_parents'] = regex - form_data = dict(form_data, **whitelist) - with session: - session.virtwho_configure.create(form_data) + form_data = dict(form_data_ui, **whitelist) + with org_session: + org_session.virtwho_configure.create(form_data) fields = { 'status_label': 'Status', 'hypervisor_type_label': 'Hypervisor Type', @@ -647,11 +613,11 @@ def test_positive_overview_label_name(self, default_org, form_data, session): fields['kubeconfig_path_label'] = 'Kubeconfig Path' if hypervisor_type == 'esx': fields['filter_host_parents_label'] = 'Filter Host Parents' - results = session.virtwho_configure.read(name) + results = org_session.virtwho_configure.read(name) for key, value in fields.items(): assert results['overview'][key] == value - session.virtwho_configure.edit(name, blacklist) - results = session.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, blacklist) + results = org_session.virtwho_configure.read(name) del fields['filter_hosts_label'] if hypervisor_type == 'esx': del fields['filter_host_parents_label'] @@ -661,7 +627,9 @@ def test_positive_overview_label_name(self, default_org, form_data, session): assert results['overview'][key] == value @pytest.mark.tier2 - def test_positive_last_checkin_status(self, default_org, virtwho_config, form_data, session): + def test_positive_last_checkin_status( + self, default_org, virtwho_config_ui, form_data_ui, org_session + ): """Verify the Last Checkin status on Content Hosts Page. :id: 7448d482-d05c-4727-8980-176586e9e4a7 @@ -676,15 +644,15 @@ def test_positive_last_checkin_status(self, default_org, virtwho_config, form_da :CaseImportance: Medium """ - name = form_data['name'] - values = session.virtwho_configure.read(name, widget_names='deploy.command') + name = form_data_ui['name'] + values = org_session.virtwho_configure.read(name, widget_names='deploy.command') command = values['deploy']['command'] hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label + command, form_data_ui['hypervisor_type'], debug=True, org=default_org.label ) - time_now = session.browser.get_client_datetime() - assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' - checkin_time = session.contenthost.search(hypervisor_name)[0]['Last Checkin'] + time_now = org_session.browser.get_client_datetime() + assert org_session.virtwho_configure.search(name)[0]['Status'] == 'ok' + checkin_time = org_session.contenthost.search(hypervisor_name)[0]['Last Checkin'] # 10 mins margin to check the Last Checkin time assert ( abs( @@ -698,7 +666,7 @@ def test_positive_last_checkin_status(self, default_org, virtwho_config, form_da @pytest.mark.tier2 def test_positive_deploy_configure_hypervisor_password_with_special_characters( - self, default_org, form_data, target_sat, session + self, default_org, form_data_ui, target_sat, org_session ): """Verify " hammer virt-who-config deploy hypervisor with special characters" @@ -715,12 +683,12 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( :customerscenario: true """ name = gen_string('alpha') - form_data['name'] = name - with session: + form_data_ui['name'] = name + with org_session: # check the hypervisor password contains single quotes - form_data['hypervisor_content.password'] = "Tes't" - session.virtwho_configure.create(form_data) - values = session.virtwho_configure.read(name) + form_data_ui['hypervisor_content.password'] = "Tes't" + org_session.virtwho_configure.create(form_data_ui) + values = org_session.virtwho_configure.read(name) command = values['deploy']['command'] config_id = get_configure_id(name) deploy_status = deploy_configure_by_command_check(command) @@ -731,12 +699,12 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( get_configure_option('username', config_file) == settings.virtwho.esx.hypervisor_username ) - session.virtwho_configure.delete(name) - assert not session.virtwho_configure.search(name) + org_session.virtwho_configure.delete(name) + assert not org_session.virtwho_configure.search(name) # check the hypervisor password contains backtick - form_data['hypervisor_content.password'] = "my`password" - session.virtwho_configure.create(form_data) - values = session.virtwho_configure.read(name) + form_data_ui['hypervisor_content.password'] = "my`password" + org_session.virtwho_configure.create(form_data_ui) + values = org_session.virtwho_configure.read(name) command = values['deploy']['command'] config_id = get_configure_id(name) deploy_status = deploy_configure_by_command_check(command) @@ -747,12 +715,12 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( get_configure_option('username', config_file) == settings.virtwho.esx.hypervisor_username ) - session.virtwho_configure.delete(name) - assert not session.virtwho_configure.search(name) + org_session.virtwho_configure.delete(name) + assert not org_session.virtwho_configure.search(name) @pytest.mark.tier2 def test_positive_remove_env_option( - self, default_org, virtwho_config, form_data, target_sat, session + self, default_org, virtwho_config_ui, form_data_ui, target_sat, org_session ): """remove option 'env=' from the virt-who configuration file and without any error @@ -770,13 +738,13 @@ def test_positive_remove_env_option( :customerscenario: true """ - name = form_data['name'] - values = session.virtwho_configure.read(name) + name = form_data_ui['name'] + values = org_session.virtwho_configure.read(name) command = values['deploy']['command'] deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label + command, form_data_ui['hypervisor_type'], debug=True, org=default_org.label ) - assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' + assert org_session.virtwho_configure.search(name)[0]['Status'] == 'ok' # Check the option "env=" should be removed from etc/virt-who.d/virt-who.conf option = "env" config_id = get_configure_id(name) diff --git a/tests/foreman/virtwho/ui/test_esx_sca.py b/tests/foreman/virtwho/ui/test_esx_sca.py index 6abda09c6e4..f8eb03fea98 100644 --- a/tests/foreman/virtwho/ui/test_esx_sca.py +++ b/tests/foreman/virtwho/ui/test_esx_sca.py @@ -28,58 +28,23 @@ delete_configure_option, deploy_configure_by_command, deploy_configure_by_command_check, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_id, get_configure_option, - get_guest_info, get_virtwho_status, restart_virtwho_service, update_configure_option, ) -@pytest.fixture() -def form_data(): - form = { - 'debug': True, - 'interval': 'Every hour', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.esx.hypervisor_type, - 'hypervisor_content.server': settings.virtwho.esx.hypervisor_server, - 'hypervisor_content.username': settings.virtwho.esx.hypervisor_username, - 'hypervisor_content.password': settings.virtwho.esx.hypervisor_password, - } - return form - - -@pytest.fixture(autouse=True) -def clean_host(form_data, target_sat): - guest_name, _ = get_guest_info(form_data['hypervisor_type']) - results = target_sat.api.Host().search(query={'search': guest_name}) - if results: - target_sat.api.Host(id=results[0].read_json()['id']).delete() - - -@pytest.fixture() -def virtwho_config(form_data, target_sat, session_sca): - name = gen_string('alpha') - form_data['name'] = name - with session_sca: - session_sca.virtwho_configure.create(form_data) - yield virtwho_config - session_sca.virtwho_configure.delete(name) - assert not session_sca.virtwho_configure.search(name) - - -@pytest.mark.usefixtures('clean_host') +@pytest.mark.delete_host class TestVirtwhoConfigforEsx: @pytest.mark.tier2 @pytest.mark.upgrade - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_ui', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, module_sca_manifest_org, virtwho_config, session_sca, form_data, deploy_type + self, module_sca_manifest_org, org_session, form_data_ui, deploy_type_ui ): """Verify configure created and deployed with id. @@ -96,29 +61,11 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - name = form_data['name'] - values = session_sca.virtwho_configure.read(name) - if deploy_type == "id": - command = values['deploy']['command'] - deploy_configure_by_command( - command, - form_data['hypervisor_type'], - debug=True, - org=module_sca_manifest_org.label, - ) - elif deploy_type == "script": - script = values['deploy']['script'] - deploy_configure_by_script( - script, - form_data['hypervisor_type'], - debug=True, - org=module_sca_manifest_org.label, - ) - assert session_sca.virtwho_configure.search(name)[0]['Status'] == 'ok' + assert org_session.virtwho_configure.search(form_data_ui['name'])[0]['Status'] == 'ok' @pytest.mark.tier2 def test_positive_debug_option( - self, module_sca_manifest_org, virtwho_config, session_sca, form_data + self, module_sca_manifest_org, virtwho_config_ui, org_session, form_data_ui ): """Verify debug checkbox and the value changes of VIRTWHO_DEBUG @@ -132,24 +79,24 @@ def test_positive_debug_option( :CaseImportance: Medium """ - name = form_data['name'] + name = form_data_ui['name'] config_id = get_configure_id(name) config_command = get_configure_command(config_id, module_sca_manifest_org.name) deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + config_command, form_data_ui['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('debug', ETC_VIRTWHO_CONFIG) == '1' - session_sca.virtwho_configure.edit(name, {'debug': False}) - results = session_sca.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'debug': False}) + results = org_session.virtwho_configure.read(name) assert results['overview']['debug'] is False deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + config_command, form_data_ui['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('debug', ETC_VIRTWHO_CONFIG) == '0' @pytest.mark.tier2 def test_positive_interval_option( - self, module_sca_manifest_org, virtwho_config, session_sca, form_data + self, module_sca_manifest_org, virtwho_config_ui, org_session, form_data_ui ): """Verify interval dropdown options and the value changes of VIRTWHO_INTERVAL. @@ -163,7 +110,7 @@ def test_positive_interval_option( :CaseImportance: Medium """ - name = form_data['name'] + name = form_data_ui['name'] config_id = get_configure_id(name) config_command = get_configure_command(config_id, module_sca_manifest_org.name) intervals = { @@ -177,17 +124,17 @@ def test_positive_interval_option( 'Every 3 days': '259200', } for option, value in intervals.items(): - session_sca.virtwho_configure.edit(name, {'interval': option}) - results = session_sca.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'interval': option}) + results = org_session.virtwho_configure.read(name) assert results['overview']['interval'] == option deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + config_command, form_data_ui['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('interval', ETC_VIRTWHO_CONFIG) == value @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, module_sca_manifest_org, virtwho_config, session_sca, form_data + self, module_sca_manifest_org, virtwho_config_ui, org_session, form_data_ui ): """Verify Hypervisor ID dropdown options. @@ -201,17 +148,17 @@ def test_positive_hypervisor_id_option( :CaseImportance: Medium """ - name = form_data['name'] + name = form_data_ui['name'] config_id = get_configure_id(name) config_command = get_configure_command(config_id, module_sca_manifest_org.name) config_file = get_configure_file(config_id) # esx and rhevm support hwuuid option for value in ['uuid', 'hostname', 'hwuuid']: - session_sca.virtwho_configure.edit(name, {'hypervisor_id': value}) - results = session_sca.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'hypervisor_id': value}) + results = org_session.virtwho_configure.read(name) assert results['overview']['hypervisor_id'] == value deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + config_command, form_data_ui['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('hypervisor_id', config_file) == value @@ -219,7 +166,7 @@ def test_positive_hypervisor_id_option( @pytest.mark.parametrize('filter_type', ['whitelist', 'blacklist']) @pytest.mark.parametrize('option_type', ['edit', 'create']) def test_positive_filtering_option( - self, module_sca_manifest_org, session_sca, form_data, filter_type, option_type + self, module_sca_manifest_org, org_session, form_data_ui, filter_type, option_type ): """Verify Filtering dropdown options. @@ -243,11 +190,11 @@ def test_positive_filtering_option( :customerscenario: true """ name = gen_string('alpha') - form_data['name'] = name + form_data_ui['name'] = name regex = '.*redhat.com' - with session_sca: + with org_session: if option_type == "edit": - session_sca.virtwho_configure.create(form_data) + org_session.virtwho_configure.create(form_data_ui) config_id = get_configure_id(name) config_command = get_configure_command(config_id, module_sca_manifest_org.name) config_file = get_configure_file(config_id) @@ -256,19 +203,21 @@ def test_positive_filtering_option( # esx support filter-host-parents and exclude-host-parents options whitelist['filtering_content.filter_host_parents'] = regex # Update Whitelist and check the result - session_sca.virtwho_configure.edit(name, whitelist) - results = session_sca.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, whitelist) + results = org_session.virtwho_configure.read(name) assert results['overview']['filter_hosts'] == regex assert results['overview']['filter_host_parents'] == regex elif filter_type == "blacklist": blacklist = {'filtering': 'Blacklist', 'filtering_content.exclude_hosts': regex} blacklist['filtering_content.exclude_host_parents'] = regex - session_sca.virtwho_configure.edit(name, blacklist) - results = session_sca.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, blacklist) + results = org_session.virtwho_configure.read(name) assert results['overview']['exclude_hosts'] == regex assert results['overview']['exclude_host_parents'] == regex deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + config_command, + form_data_ui['hypervisor_type'], + org=module_sca_manifest_org.label, ) if filter_type == "whitelist": assert regex == get_configure_option('filter_hosts', config_file) @@ -276,25 +225,25 @@ def test_positive_filtering_option( elif filter_type == "blacklist": assert regex == get_configure_option('exclude_hosts', config_file) assert regex == get_configure_option('exclude_host_parents', config_file) - session_sca.virtwho_configure.delete(name) - assert not session_sca.virtwho_configure.search(name) + org_session.virtwho_configure.delete(name) + assert not org_session.virtwho_configure.search(name) elif option_type == "create": if filter_type == "whitelist": - form_data['filtering'] = "Whitelist" - form_data['filtering_content.filter_hosts'] = regex - form_data['filtering_content.filter_host_parents'] = regex + form_data_ui['filtering'] = "Whitelist" + form_data_ui['filtering_content.filter_hosts'] = regex + form_data_ui['filtering_content.filter_host_parents'] = regex elif filter_type == "blacklist": - form_data['filtering'] = "Blacklist" - form_data['filtering_content.exclude_hosts'] = regex - form_data['filtering_content.exclude_host_parents'] = regex - session_sca.virtwho_configure.create(form_data) + form_data_ui['filtering'] = "Blacklist" + form_data_ui['filtering_content.exclude_hosts'] = regex + form_data_ui['filtering_content.exclude_host_parents'] = regex + org_session.virtwho_configure.create(form_data_ui) config_id = get_configure_id(name) command = get_configure_command(config_id, module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_ui['hypervisor_type'], org=module_sca_manifest_org.label ) config_file = get_configure_file(config_id) - results = session_sca.virtwho_configure.read(name) + results = org_session.virtwho_configure.read(name) if filter_type == "whitelist": assert results['overview']['filter_hosts'] == regex assert results['overview']['filter_host_parents'] == regex @@ -308,7 +257,7 @@ def test_positive_filtering_option( @pytest.mark.tier2 def test_positive_last_checkin_status( - self, module_sca_manifest_org, virtwho_config, form_data, session_sca + self, module_sca_manifest_org, virtwho_config_ui, form_data_ui, org_session ): """Verify the Last Checkin status on Content Hosts Page. @@ -324,15 +273,15 @@ def test_positive_last_checkin_status( :CaseImportance: Medium """ - name = form_data['name'] - values = session_sca.virtwho_configure.read(name, widget_names='deploy.command') + name = form_data_ui['name'] + values = org_session.virtwho_configure.read(name, widget_names='deploy.command') command = values['deploy']['command'] hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=module_sca_manifest_org.label + command, form_data_ui['hypervisor_type'], debug=True, org=module_sca_manifest_org.label ) - time_now = session_sca.browser.get_client_datetime() - assert session_sca.virtwho_configure.search(name)[0]['Status'] == 'ok' - checkin_time = session_sca.contenthost.search(hypervisor_name)[0]['Last Checkin'] + time_now = org_session.browser.get_client_datetime() + assert org_session.virtwho_configure.search(name)[0]['Status'] == 'ok' + checkin_time = org_session.contenthost.search(hypervisor_name)[0]['Last Checkin'] # 10 mins margin to check the Last Checkin time assert ( abs( @@ -346,7 +295,7 @@ def test_positive_last_checkin_status( @pytest.mark.tier2 def test_positive_remove_env_option( - self, module_sca_manifest_org, virtwho_config, form_data, target_sat, session_sca + self, module_sca_manifest_org, virtwho_config_ui, form_data_ui, target_sat, org_session ): """remove option 'env=' from the virt-who configuration file and without any error @@ -364,13 +313,13 @@ def test_positive_remove_env_option( :customerscenario: true """ - name = form_data['name'] - values = session_sca.virtwho_configure.read(name) + name = form_data_ui['name'] + values = org_session.virtwho_configure.read(name) command = values['deploy']['command'] deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=module_sca_manifest_org.label + command, form_data_ui['hypervisor_type'], debug=True, org=module_sca_manifest_org.label ) - assert session_sca.virtwho_configure.search(name)[0]['Status'] == 'ok' + assert org_session.virtwho_configure.search(name)[0]['Status'] == 'ok' # Check the option "env=" should be removed from etc/virt-who.d/virt-who.conf option = "env" config_id = get_configure_id(name) @@ -388,7 +337,7 @@ def test_positive_remove_env_option( assert result.status == 1 @pytest.mark.tier2 - def test_positive_virtwho_roles(self, session_sca): + def test_positive_virtwho_roles(self, org_session): """Verify the default roles for virtwho configure :id: 3c2501d5-c122-49f0-baa4-4c0d678cb6fc @@ -416,14 +365,14 @@ def test_positive_virtwho_roles(self, session_sca): }, 'Virt-who Viewer': {'Satellite virt who configure/config': ['view_virt_who_config']}, } - with session_sca: + with org_session: for role_name, role_filters in roles.items(): - assert session_sca.role.search(role_name)[0]['Name'] == role_name - assigned_permissions = session_sca.filter.read_permissions(role_name) + assert org_session.role.search(role_name)[0]['Name'] == role_name + assigned_permissions = org_session.filter.read_permissions(role_name) assert sorted(assigned_permissions) == sorted(role_filters) @pytest.mark.tier2 - def test_positive_delete_configure(self, module_sca_manifest_org, session_sca, form_data): + def test_positive_delete_configure(self, module_sca_manifest_org, org_session, form_data_ui): """Verify when a config is deleted the associated user is deleted. :id: efc7253d-f455-4dc3-ae03-3ed5e215bd11 @@ -442,23 +391,23 @@ def test_positive_delete_configure(self, module_sca_manifest_org, session_sca, f :CaseImportance: Low """ name = gen_string('alpha') - form_data['name'] = name - with session_sca: - session_sca.virtwho_configure.create(form_data) + form_data_ui['name'] = name + with org_session: + org_session.virtwho_configure.create(form_data_ui) config_id = get_configure_id(name) config_command = get_configure_command(config_id, module_sca_manifest_org.name) deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + config_command, form_data_ui['hypervisor_type'], org=module_sca_manifest_org.label ) - assert session_sca.virtwho_configure.search(name)[0]['Status'] == 'ok' - session_sca.virtwho_configure.delete(name) - assert not session_sca.virtwho_configure.search(name) + assert org_session.virtwho_configure.search(name)[0]['Status'] == 'ok' + org_session.virtwho_configure.delete(name) + assert not org_session.virtwho_configure.search(name) restart_virtwho_service() assert get_virtwho_status() == 'logerror' @pytest.mark.tier2 def test_positive_virtwho_reporter_role( - self, module_sca_manifest_org, session_sca, test_name, form_data + self, module_sca_manifest_org, org_session, test_name, form_data_ui ): """Verify the virt-who reporter role can TRULY work. @@ -476,9 +425,9 @@ def test_positive_virtwho_reporter_role( username = gen_string('alpha') password = gen_string('alpha') config_name = gen_string('alpha') - with session_sca: + with org_session: # Create an user - session_sca.user.create( + org_session.user.create( { 'user.login': username, 'user.mail': valid_emails_list()[0], @@ -488,14 +437,14 @@ def test_positive_virtwho_reporter_role( } ) # Create a virt-who config plugin - form_data['name'] = config_name - session_sca.virtwho_configure.create(form_data) - values = session_sca.virtwho_configure.read(config_name) + form_data_ui['name'] = config_name + org_session.virtwho_configure.create(form_data_ui) + values = org_session.virtwho_configure.read(config_name) command = values['deploy']['command'] deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_ui['hypervisor_type'], org=module_sca_manifest_org.label ) - assert session_sca.virtwho_configure.search(config_name)[0]['Status'] == 'ok' + assert org_session.virtwho_configure.search(config_name)[0]['Status'] == 'ok' # Update the virt-who config file config_id = get_configure_id(config_name) config_file = get_configure_file(config_id) @@ -505,20 +454,20 @@ def test_positive_virtwho_reporter_role( restart_virtwho_service() assert get_virtwho_status() == 'logerror' # Check the permissioin of Virt-who Reporter - session_sca.user.update(username, {'roles.resources.assigned': ['Virt-who Reporter']}) - assert session_sca.user.search(username)[0]['Username'] == username - user = session_sca.user.read(username) + org_session.user.update(username, {'roles.resources.assigned': ['Virt-who Reporter']}) + assert org_session.user.search(username)[0]['Username'] == username + user = org_session.user.read(username) assert user['roles']['resources']['assigned'] == ['Virt-who Reporter'] restart_virtwho_service() assert get_virtwho_status() == 'running' with Session(test_name, username, password) as newsession: assert not newsession.virtwho_configure.check_create_permission()['can_view'] - session_sca.user.delete(username) - assert not session_sca.user.search(username) + org_session.user.delete(username) + assert not org_session.user.search(username) @pytest.mark.tier2 def test_positive_virtwho_viewer_role( - self, module_sca_manifest_org, session_sca, test_name, form_data + self, module_sca_manifest_org, org_session, test_name, form_data_ui ): """Verify the virt-who viewer role can TRULY work. @@ -536,9 +485,9 @@ def test_positive_virtwho_viewer_role( username = gen_string('alpha') password = gen_string('alpha') config_name = gen_string('alpha') - with session_sca: + with org_session: # Create an user - session_sca.user.create( + org_session.user.create( { 'user.login': username, 'user.mail': valid_emails_list()[0], @@ -548,17 +497,17 @@ def test_positive_virtwho_viewer_role( } ) # Create a virt-who config plugin - form_data['name'] = config_name - session_sca.virtwho_configure.create(form_data) - values = session_sca.virtwho_configure.read(config_name) + form_data_ui['name'] = config_name + org_session.virtwho_configure.create(form_data_ui) + values = org_session.virtwho_configure.read(config_name) command = values['deploy']['command'] deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_ui['hypervisor_type'], org=module_sca_manifest_org.label ) - assert session_sca.virtwho_configure.search(config_name)[0]['Status'] == 'ok' + assert org_session.virtwho_configure.search(config_name)[0]['Status'] == 'ok' # Check the permissioin of Virt-who Viewer - session_sca.user.update(username, {'roles.resources.assigned': ['Virt-who Viewer']}) - user = session_sca.user.read(username) + org_session.user.update(username, {'roles.resources.assigned': ['Virt-who Viewer']}) + user = org_session.user.read(username) assert user['roles']['resources']['assigned'] == ['Virt-who Viewer'] # Update the virt-who config file config_id = get_configure_id(config_name) @@ -579,12 +528,12 @@ def test_positive_virtwho_viewer_role( assert not update_permission['can_edit'] newsession.virtwho_configure.read(config_name) # Delete the created user - session_sca.user.delete(username) - assert not session_sca.user.search(username) + org_session.user.delete(username) + assert not org_session.user.search(username) @pytest.mark.tier2 def test_positive_virtwho_manager_role( - self, module_sca_manifest_org, session_sca, test_name, form_data + self, module_sca_manifest_org, org_session, test_name, form_data_ui ): """Verify the virt-who manager role can TRULY work. @@ -600,9 +549,9 @@ def test_positive_virtwho_manager_role( username = gen_string('alpha') password = gen_string('alpha') config_name = gen_string('alpha') - with session_sca: + with org_session: # Create an user - session_sca.user.create( + org_session.user.create( { 'user.login': username, 'user.mail': valid_emails_list()[0], @@ -612,28 +561,28 @@ def test_positive_virtwho_manager_role( } ) # Create a virt-who config plugin - form_data['name'] = config_name - session_sca.virtwho_configure.create(form_data) - values = session_sca.virtwho_configure.read(config_name) + form_data_ui['name'] = config_name + org_session.virtwho_configure.create(form_data_ui) + values = org_session.virtwho_configure.read(config_name) command = values['deploy']['command'] deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_ui['hypervisor_type'], org=module_sca_manifest_org.label ) - assert session_sca.virtwho_configure.search(config_name)[0]['Status'] == 'ok' + assert org_session.virtwho_configure.search(config_name)[0]['Status'] == 'ok' # Check the permissioin of Virt-who Manager - session_sca.user.update(username, {'roles.resources.assigned': ['Virt-who Manager']}) - user = session_sca.user.read(username) + org_session.user.update(username, {'roles.resources.assigned': ['Virt-who Manager']}) + user = org_session.user.read(username) assert user['roles']['resources']['assigned'] == ['Virt-who Manager'] with Session(test_name, username, password) as newsession: # create_virt_who_config new_virt_who_name = gen_string('alpha') - form_data['name'] = new_virt_who_name - newsession.virtwho_configure.create(form_data) + form_data_ui['name'] = new_virt_who_name + newsession.virtwho_configure.create(form_data_ui) # view_virt_who_config values = newsession.virtwho_configure.read(new_virt_who_name) command = values['deploy']['command'] deploy_configure_by_command( - command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + command, form_data_ui['hypervisor_type'], org=module_sca_manifest_org.label ) assert newsession.virtwho_configure.search(new_virt_who_name)[0]['Status'] == 'ok' # edit_virt_who_config @@ -644,12 +593,12 @@ def test_positive_virtwho_manager_role( newsession.virtwho_configure.delete(modify_name) assert not newsession.virtwho_configure.search(modify_name) # Delete the created user - session_sca.user.delete(username) - assert not session_sca.user.search(username) + org_session.user.delete(username) + assert not org_session.user.search(username) @pytest.mark.tier2 def test_positive_deploy_configure_hypervisor_password_with_special_characters( - self, module_sca_manifest_org, form_data, target_sat, session_sca + self, module_sca_manifest_org, form_data_ui, target_sat, org_session ): """Verify " hammer virt-who-config deploy hypervisor with special characters" @@ -666,12 +615,12 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( :customerscenario: true """ name = gen_string('alpha') - form_data['name'] = name - with session_sca: + form_data_ui['name'] = name + with org_session: # check the hypervisor password contains single quotes - form_data['hypervisor_content.password'] = "Tes't" - session_sca.virtwho_configure.create(form_data) - values = session_sca.virtwho_configure.read(name) + form_data_ui['hypervisor_content.password'] = "Tes't" + org_session.virtwho_configure.create(form_data_ui) + values = org_session.virtwho_configure.read(name) command = values['deploy']['command'] config_id = get_configure_id(name) deploy_status = deploy_configure_by_command_check(command) @@ -682,12 +631,12 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( get_configure_option('username', config_file) == settings.virtwho.esx.hypervisor_username ) - session_sca.virtwho_configure.delete(name) - assert not session_sca.virtwho_configure.search(name) + org_session.virtwho_configure.delete(name) + assert not org_session.virtwho_configure.search(name) # check the hypervisor password contains backtick - form_data['hypervisor_content.password'] = "my`password" - session_sca.virtwho_configure.create(form_data) - values = session_sca.virtwho_configure.read(name) + form_data_ui['hypervisor_content.password'] = "my`password" + org_session.virtwho_configure.create(form_data_ui) + values = org_session.virtwho_configure.read(name) command = values['deploy']['command'] config_id = get_configure_id(name) deploy_status = deploy_configure_by_command_check(command) @@ -698,5 +647,5 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( get_configure_option('username', config_file) == settings.virtwho.esx.hypervisor_username ) - session_sca.virtwho_configure.delete(name) - assert not session_sca.virtwho_configure.search(name) + org_session.virtwho_configure.delete(name) + assert not org_session.virtwho_configure.search(name) diff --git a/tests/foreman/virtwho/ui/test_hyperv.py b/tests/foreman/virtwho/ui/test_hyperv.py index 56c2bbdb476..a35878e2fc2 100644 --- a/tests/foreman/virtwho/ui/test_hyperv.py +++ b/tests/foreman/virtwho/ui/test_hyperv.py @@ -16,13 +16,11 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_id, @@ -30,36 +28,11 @@ ) -@pytest.fixture() -def form_data(): - form = { - 'debug': True, - 'interval': 'Every hour', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.hyperv.hypervisor_type, - 'hypervisor_content.server': settings.virtwho.hyperv.hypervisor_server, - 'hypervisor_content.username': settings.virtwho.hyperv.hypervisor_username, - 'hypervisor_content.password': settings.virtwho.hyperv.hypervisor_password, - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat, session): - name = gen_string('alpha') - form_data['name'] = name - with session: - session.virtwho_configure.create(form_data) - yield virtwho_config - session.virtwho_configure.delete(name) - assert not session.virtwho_configure.search(name) - - class TestVirtwhoConfigforHyperv: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_ui', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, default_org, virtwho_config, session, form_data, deploy_type + self, default_org, org_session, form_data_ui, deploy_type_ui ): """Verify configure created and deployed with id. @@ -76,37 +49,30 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - name = form_data['name'] - values = session.virtwho_configure.read(name) - if deploy_type == "id": - command = values['deploy']['command'] - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - elif deploy_type == "script": - script = values['deploy']['script'] - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' - hypervisor_display_name = session.contenthost.search(hypervisor_name)[0]['Name'] + hypervisor_name, guest_name = deploy_type_ui + assert org_session.virtwho_configure.search(form_data_ui['name'])[0]['Status'] == 'ok' + hypervisor_display_name = org_session.contenthost.search(hypervisor_name)[0]['Name'] vdc_physical = f'product_id = {settings.virtwho.sku.vdc_physical} and type=NORMAL' vdc_virtual = f'product_id = {settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED' assert ( - session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions']['status'] + org_session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions'][ + 'status' + ] == 'Unsubscribed hypervisor' ) - session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) - assert session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' + org_session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) + assert org_session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' assert ( - session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] + org_session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] == 'Unentitled' ) - session.contenthost.add_subscription(guest_name, vdc_virtual) - assert session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' + org_session.contenthost.add_subscription(guest_name, vdc_virtual) + assert org_session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' @pytest.mark.tier2 - def test_positive_hypervisor_id_option(self, default_org, virtwho_config, session, form_data): + def test_positive_hypervisor_id_option( + self, default_org, virtwho_config_ui, org_session, form_data_ui + ): """Verify Hypervisor ID dropdown options. :id: f2efc018-d57e-4dc5-895e-53af320237de @@ -119,16 +85,16 @@ def test_positive_hypervisor_id_option(self, default_org, virtwho_config, sessio :CaseImportance: Medium """ - name = form_data['name'] + name = form_data_ui['name'] config_id = get_configure_id(name) config_command = get_configure_command(config_id, default_org.name) config_file = get_configure_file(config_id) values = ['uuid', 'hostname'] for value in values: - session.virtwho_configure.edit(name, {'hypervisor_id': value}) - results = session.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'hypervisor_id': value}) + results = org_session.virtwho_configure.read(name) assert results['overview']['hypervisor_id'] == value deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=default_org.label + config_command, form_data_ui['hypervisor_type'], org=default_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/ui/test_hyperv_sca.py b/tests/foreman/virtwho/ui/test_hyperv_sca.py index d58d2c15ebd..3e7b0f01c5e 100644 --- a/tests/foreman/virtwho/ui/test_hyperv_sca.py +++ b/tests/foreman/virtwho/ui/test_hyperv_sca.py @@ -14,13 +14,10 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest -from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_id, @@ -28,36 +25,11 @@ ) -@pytest.fixture() -def form_data(): - form = { - 'debug': True, - 'interval': 'Every hour', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.hyperv.hypervisor_type, - 'hypervisor_content.server': settings.virtwho.hyperv.hypervisor_server, - 'hypervisor_content.username': settings.virtwho.hyperv.hypervisor_username, - 'hypervisor_content.password': settings.virtwho.hyperv.hypervisor_password, - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat, session_sca): - name = gen_string('alpha') - form_data['name'] = name - with session_sca: - session_sca.virtwho_configure.create(form_data) - yield virtwho_config - session_sca.virtwho_configure.delete(name) - assert not session_sca.virtwho_configure.search(name) - - class TestVirtwhoConfigforHyperv: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_ui', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, module_sca_manifest_org, virtwho_config, session_sca, form_data, deploy_type + self, module_sca_manifest_org, org_session, form_data_ui, deploy_type_ui ): """Verify configure created and deployed with id. @@ -74,29 +46,11 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - name = form_data['name'] - values = session_sca.virtwho_configure.read(name) - if deploy_type == "id": - command = values['deploy']['command'] - deploy_configure_by_command( - command, - form_data['hypervisor_type'], - debug=True, - org=module_sca_manifest_org.label, - ) - elif deploy_type == "script": - script = values['deploy']['script'] - deploy_configure_by_script( - script, - form_data['hypervisor_type'], - debug=True, - org=module_sca_manifest_org.label, - ) - assert session_sca.virtwho_configure.search(name)[0]['Status'] == 'ok' + assert org_session.virtwho_configure.search(form_data_ui['name'])[0]['Status'] == 'ok' @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, module_sca_manifest_org, virtwho_config, session_sca, form_data + self, module_sca_manifest_org, virtwho_config_ui, org_session, form_data_ui ): """Verify Hypervisor ID dropdown options. @@ -110,16 +64,16 @@ def test_positive_hypervisor_id_option( :CaseImportance: Medium """ - name = form_data['name'] + name = form_data_ui['name'] config_id = get_configure_id(name) config_command = get_configure_command(config_id, module_sca_manifest_org.name) config_file = get_configure_file(config_id) values = ['uuid', 'hostname'] for value in values: - session_sca.virtwho_configure.edit(name, {'hypervisor_id': value}) - results = session_sca.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'hypervisor_id': value}) + results = org_session.virtwho_configure.read(name) assert results['overview']['hypervisor_id'] == value deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + config_command, form_data_ui['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/ui/test_kubevirt.py b/tests/foreman/virtwho/ui/test_kubevirt.py index 289e4f385f1..19d5bce7f63 100644 --- a/tests/foreman/virtwho/ui/test_kubevirt.py +++ b/tests/foreman/virtwho/ui/test_kubevirt.py @@ -16,13 +16,11 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_id, @@ -30,34 +28,11 @@ ) -@pytest.fixture() -def form_data(): - form = { - 'debug': True, - 'interval': 'Every hour', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.kubevirt.hypervisor_type, - 'hypervisor_content.kubeconfig': settings.virtwho.kubevirt.hypervisor_config_file, - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat, session): - name = gen_string('alpha') - form_data['name'] = name - with session: - session.virtwho_configure.create(form_data) - yield virtwho_config - session.virtwho_configure.delete(name) - assert not session.virtwho_configure.search(name) - - class TestVirtwhoConfigforKubevirt: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_ui', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, default_org, virtwho_config, session, form_data, deploy_type + self, default_org, org_session, form_data_ui, deploy_type_ui ): """Verify configure created and deployed with id. @@ -74,37 +49,30 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - name = form_data['name'] - values = session.virtwho_configure.read(name) - if deploy_type == "id": - command = values['deploy']['command'] - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - elif deploy_type == "script": - script = values['deploy']['script'] - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' - hypervisor_display_name = session.contenthost.search(hypervisor_name)[0]['Name'] + hypervisor_name, guest_name = deploy_type_ui + assert org_session.virtwho_configure.search(form_data_ui['name'])[0]['Status'] == 'ok' + hypervisor_display_name = org_session.contenthost.search(hypervisor_name)[0]['Name'] vdc_physical = f'product_id = {settings.virtwho.sku.vdc_physical} and type=NORMAL' vdc_virtual = f'product_id = {settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED' assert ( - session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions']['status'] + org_session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions'][ + 'status' + ] == 'Unsubscribed hypervisor' ) - session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) - assert session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' + org_session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) + assert org_session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' assert ( - session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] + org_session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] == 'Unentitled' ) - session.contenthost.add_subscription(guest_name, vdc_virtual) - assert session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' + org_session.contenthost.add_subscription(guest_name, vdc_virtual) + assert org_session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' @pytest.mark.tier2 - def test_positive_hypervisor_id_option(self, default_org, virtwho_config, session, form_data): + def test_positive_hypervisor_id_option( + self, default_org, virtwho_config_ui, org_session, form_data_ui + ): """Verify Hypervisor ID dropdown options. :id: 09826cc0-aa49-4355-8980-8097511eb7d7 @@ -117,16 +85,16 @@ def test_positive_hypervisor_id_option(self, default_org, virtwho_config, sessio :CaseImportance: Medium """ - name = form_data['name'] + name = form_data_ui['name'] config_id = get_configure_id(name) config_command = get_configure_command(config_id, default_org.name) config_file = get_configure_file(config_id) values = ['uuid', 'hostname'] for value in values: - session.virtwho_configure.edit(name, {'hypervisor_id': value}) - results = session.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'hypervisor_id': value}) + results = org_session.virtwho_configure.read(name) assert results['overview']['hypervisor_id'] == value deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=default_org.label + config_command, form_data_ui['hypervisor_type'], org=default_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/ui/test_kubevirt_sca.py b/tests/foreman/virtwho/ui/test_kubevirt_sca.py index c4b893947e7..6ad0bdf1f96 100644 --- a/tests/foreman/virtwho/ui/test_kubevirt_sca.py +++ b/tests/foreman/virtwho/ui/test_kubevirt_sca.py @@ -14,13 +14,10 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest -from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_id, @@ -28,34 +25,11 @@ ) -@pytest.fixture() -def form_data(): - form = { - 'debug': True, - 'interval': 'Every hour', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.kubevirt.hypervisor_type, - 'hypervisor_content.kubeconfig': settings.virtwho.kubevirt.hypervisor_config_file, - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat, session_sca): - name = gen_string('alpha') - form_data['name'] = name - with session_sca: - session_sca.virtwho_configure.create(form_data) - yield virtwho_config - session_sca.virtwho_configure.delete(name) - assert not session_sca.virtwho_configure.search(name) - - class TestVirtwhoConfigforKubevirt: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_ui', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, module_sca_manifest_org, virtwho_config, session_sca, form_data, deploy_type + self, module_sca_manifest_org, org_session, form_data_ui, deploy_type_ui ): """Verify configure created and deployed with id. @@ -72,29 +46,11 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - name = form_data['name'] - values = session_sca.virtwho_configure.read(name) - if deploy_type == "id": - command = values['deploy']['command'] - deploy_configure_by_command( - command, - form_data['hypervisor_type'], - debug=True, - org=module_sca_manifest_org.label, - ) - elif deploy_type == "script": - script = values['deploy']['script'] - deploy_configure_by_script( - script, - form_data['hypervisor_type'], - debug=True, - org=module_sca_manifest_org.label, - ) - assert session_sca.virtwho_configure.search(name)[0]['Status'] == 'ok' + assert org_session.virtwho_configure.search(form_data_ui['name'])[0]['Status'] == 'ok' @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, module_sca_manifest_org, virtwho_config, session_sca, form_data + self, module_sca_manifest_org, virtwho_config_ui, org_session, form_data_ui ): """Verify Hypervisor ID dropdown options. @@ -108,16 +64,16 @@ def test_positive_hypervisor_id_option( :CaseImportance: Medium """ - name = form_data['name'] + name = form_data_ui['name'] config_id = get_configure_id(name) config_command = get_configure_command(config_id, module_sca_manifest_org.name) config_file = get_configure_file(config_id) values = ['uuid', 'hostname'] for value in values: - session_sca.virtwho_configure.edit(name, {'hypervisor_id': value}) - results = session_sca.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'hypervisor_id': value}) + results = org_session.virtwho_configure.read(name) assert results['overview']['hypervisor_id'] == value deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + config_command, form_data_ui['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/ui/test_libvirt.py b/tests/foreman/virtwho/ui/test_libvirt.py index 6c6b37fdc30..86b3d3e6532 100644 --- a/tests/foreman/virtwho/ui/test_libvirt.py +++ b/tests/foreman/virtwho/ui/test_libvirt.py @@ -16,13 +16,11 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_id, @@ -30,35 +28,11 @@ ) -@pytest.fixture() -def form_data(): - form = { - 'debug': True, - 'interval': 'Every hour', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.libvirt.hypervisor_type, - 'hypervisor_content.server': settings.virtwho.libvirt.hypervisor_server, - 'hypervisor_content.username': settings.virtwho.libvirt.hypervisor_username, - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat, session): - name = gen_string('alpha') - form_data['name'] = name - with session: - session.virtwho_configure.create(form_data) - yield virtwho_config - session.virtwho_configure.delete(name) - assert not session.virtwho_configure.search(name) - - class TestVirtwhoConfigforLibvirt: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_ui', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, default_org, virtwho_config, session, form_data, deploy_type + self, default_org, org_session, form_data_ui, deploy_type_ui ): """Verify configure created and deployed with id. @@ -75,37 +49,30 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - name = form_data['name'] - values = session.virtwho_configure.read(name) - if deploy_type == "id": - command = values['deploy']['command'] - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - elif deploy_type == "script": - script = values['deploy']['script'] - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' - hypervisor_display_name = session.contenthost.search(hypervisor_name)[0]['Name'] + hypervisor_name, guest_name = deploy_type_ui + assert org_session.virtwho_configure.search(form_data_ui['name'])[0]['Status'] == 'ok' + hypervisor_display_name = org_session.contenthost.search(hypervisor_name)[0]['Name'] vdc_physical = f'product_id = {settings.virtwho.sku.vdc_physical} and type=NORMAL' vdc_virtual = f'product_id = {settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED' assert ( - session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions']['status'] + org_session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions'][ + 'status' + ] == 'Unsubscribed hypervisor' ) - session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) - assert session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' + org_session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) + assert org_session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' assert ( - session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] + org_session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] == 'Unentitled' ) - session.contenthost.add_subscription(guest_name, vdc_virtual) - assert session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' + org_session.contenthost.add_subscription(guest_name, vdc_virtual) + assert org_session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' @pytest.mark.tier2 - def test_positive_hypervisor_id_option(self, default_org, virtwho_config, session, form_data): + def test_positive_hypervisor_id_option( + self, default_org, virtwho_config_ui, org_session, form_data_ui + ): """Verify Hypervisor ID dropdown options. :id: b8b2b272-89f2-45d0-b922-6e988b20808b @@ -118,16 +85,16 @@ def test_positive_hypervisor_id_option(self, default_org, virtwho_config, sessio :CaseImportance: Medium """ - name = form_data['name'] + name = form_data_ui['name'] config_id = get_configure_id(name) config_command = get_configure_command(config_id, default_org.name) config_file = get_configure_file(config_id) values = ['uuid', 'hostname'] for value in values: - session.virtwho_configure.edit(name, {'hypervisor_id': value}) - results = session.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'hypervisor_id': value}) + results = org_session.virtwho_configure.read(name) assert results['overview']['hypervisor_id'] == value deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=default_org.label + config_command, form_data_ui['hypervisor_type'], org=default_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/ui/test_libvirt_sca.py b/tests/foreman/virtwho/ui/test_libvirt_sca.py index 415ff38a37b..b6d33669744 100644 --- a/tests/foreman/virtwho/ui/test_libvirt_sca.py +++ b/tests/foreman/virtwho/ui/test_libvirt_sca.py @@ -14,13 +14,10 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest -from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_id, @@ -28,35 +25,11 @@ ) -@pytest.fixture() -def form_data(): - form = { - 'debug': True, - 'interval': 'Every hour', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.libvirt.hypervisor_type, - 'hypervisor_content.server': settings.virtwho.libvirt.hypervisor_server, - 'hypervisor_content.username': settings.virtwho.libvirt.hypervisor_username, - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat, session_sca): - name = gen_string('alpha') - form_data['name'] = name - with session_sca: - session_sca.virtwho_configure.create(form_data) - yield virtwho_config - session_sca.virtwho_configure.delete(name) - assert not session_sca.virtwho_configure.search(name) - - class TestVirtwhoConfigforLibvirt: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_ui', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, module_sca_manifest_org, virtwho_config, session_sca, form_data, deploy_type + self, module_sca_manifest_org, org_session, form_data_ui, deploy_type_ui ): """Verify configure created and deployed with id. @@ -73,29 +46,11 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - name = form_data['name'] - values = session_sca.virtwho_configure.read(name) - if deploy_type == "id": - command = values['deploy']['command'] - deploy_configure_by_command( - command, - form_data['hypervisor_type'], - debug=True, - org=module_sca_manifest_org.label, - ) - elif deploy_type == "script": - script = values['deploy']['script'] - deploy_configure_by_script( - script, - form_data['hypervisor_type'], - debug=True, - org=module_sca_manifest_org.label, - ) - assert session_sca.virtwho_configure.search(name)[0]['Status'] == 'ok' + assert org_session.virtwho_configure.search(form_data_ui['name'])[0]['Status'] == 'ok' @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, module_sca_manifest_org, virtwho_config, session_sca, form_data + self, module_sca_manifest_org, virtwho_config_ui, org_session, form_data_ui ): """Verify Hypervisor ID dropdown options. @@ -109,16 +64,16 @@ def test_positive_hypervisor_id_option( :CaseImportance: Medium """ - name = form_data['name'] + name = form_data_ui['name'] config_id = get_configure_id(name) config_command = get_configure_command(config_id, module_sca_manifest_org.name) config_file = get_configure_file(config_id) values = ['uuid', 'hostname'] for value in values: - session_sca.virtwho_configure.edit(name, {'hypervisor_id': value}) - results = session_sca.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'hypervisor_id': value}) + results = org_session.virtwho_configure.read(name) assert results['overview']['hypervisor_id'] == value deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + config_command, form_data_ui['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/ui/test_nutanix.py b/tests/foreman/virtwho/ui/test_nutanix.py index d2a77961918..8bd1a3b23c7 100644 --- a/tests/foreman/virtwho/ui/test_nutanix.py +++ b/tests/foreman/virtwho/ui/test_nutanix.py @@ -32,38 +32,11 @@ ) -@pytest.fixture() -def form_data(): - form = { - 'debug': True, - 'interval': 'Every hour', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.ahv.hypervisor_type, - 'hypervisor_content.server': settings.virtwho.ahv.hypervisor_server, - 'hypervisor_content.username': settings.virtwho.ahv.hypervisor_username, - 'hypervisor_content.password': settings.virtwho.ahv.hypervisor_password, - 'hypervisor_content.prism_flavor': "Prism Element", - 'ahv_internal_debug': False, - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat, session): - name = gen_string('alpha') - form_data['name'] = name - with session: - session.virtwho_configure.create(form_data) - yield virtwho_config - session.virtwho_configure.delete(name) - assert not session.virtwho_configure.search(name) - - class TestVirtwhoConfigforNutanix: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_ui', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, default_org, virtwho_config, session, form_data, deploy_type + self, default_org, org_session, form_data_ui, deploy_type_ui ): """Verify configure created and deployed with id. @@ -80,37 +53,30 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - name = form_data['name'] - values = session.virtwho_configure.read(name) - if deploy_type == "id": - command = values['deploy']['command'] - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - elif deploy_type == "script": - script = values['deploy']['script'] - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor_type'], debug=True, org=default_org.label - ) - assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' - hypervisor_display_name = session.contenthost.search(hypervisor_name)[0]['Name'] + hypervisor_name, guest_name = deploy_type_ui + assert org_session.virtwho_configure.search(form_data_ui['name'])[0]['Status'] == 'ok' + hypervisor_display_name = org_session.contenthost.search(hypervisor_name)[0]['Name'] vdc_physical = f'product_id = {settings.virtwho.sku.vdc_physical} and type=NORMAL' vdc_virtual = f'product_id = {settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED' assert ( - session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions']['status'] + org_session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions'][ + 'status' + ] == 'Unsubscribed hypervisor' ) - session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) - assert session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' + org_session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) + assert org_session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' assert ( - session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] + org_session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] == 'Unentitled' ) - session.contenthost.add_subscription(guest_name, vdc_virtual) - assert session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' + org_session.contenthost.add_subscription(guest_name, vdc_virtual) + assert org_session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' @pytest.mark.tier2 - def test_positive_hypervisor_id_option(self, default_org, virtwho_config, session, form_data): + def test_positive_hypervisor_id_option( + self, default_org, virtwho_config_ui, org_session, form_data_ui + ): """Verify Hypervisor ID dropdown options. :id: e076a305-88f4-42fb-8ef2-cb55e38eb912 @@ -123,25 +89,25 @@ def test_positive_hypervisor_id_option(self, default_org, virtwho_config, sessio :CaseImportance: Medium """ - name = form_data['name'] - values = session.virtwho_configure.read(name) + name = form_data_ui['name'] + values = org_session.virtwho_configure.read(name) config_id = get_configure_id(name) config_command = values['deploy']['command'] config_file = get_configure_file(config_id) values = ['uuid', 'hostname'] for value in values: - session.virtwho_configure.edit(name, {'hypervisor_id': value}) - results = session.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'hypervisor_id': value}) + results = org_session.virtwho_configure.read(name) assert results['overview']['hypervisor_id'] == value deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=default_org.label + config_command, form_data_ui['hypervisor_type'], org=default_org.label ) assert get_configure_option('hypervisor_id', config_file) == value @pytest.mark.tier2 @pytest.mark.parametrize('deploy_type', ['id', 'script']) def test_positive_prism_central_deploy_configure_by_id_script( - self, default_org, session, form_data, deploy_type + self, default_org, org_session, form_data_ui, deploy_type ): """Verify configure created and deployed with id on nutanix prism central mode @@ -160,49 +126,51 @@ def test_positive_prism_central_deploy_configure_by_id_script( :CaseImportance: High """ name = gen_string('alpha') - form_data['name'] = name - form_data['hypervisor_content.prism_flavor'] = "Prism Central" - with session: - session.virtwho_configure.create(form_data) - values = session.virtwho_configure.read(name) + form_data_ui['name'] = name + form_data_ui['hypervisor_content.prism_flavor'] = "Prism Central" + with org_session: + org_session.virtwho_configure.create(form_data_ui) + values = org_session.virtwho_configure.read(name) if deploy_type == "id": command = values['deploy']['command'] hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label + command, form_data_ui['hypervisor_type'], debug=True, org=default_org.label ) elif deploy_type == "script": script = values['deploy']['script'] hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor_type'], debug=True, org=default_org.label + script, form_data_ui['hypervisor_type'], debug=True, org=default_org.label ) # Check the option "prism_central=true" should be set in etc/virt-who.d/virt-who.conf config_id = get_configure_id(name) config_file = get_configure_file(config_id) assert get_configure_option("prism_central", config_file) == 'true' - assert session.virtwho_configure.search(name)[0]['Status'] == 'ok' - hypervisor_display_name = session.contenthost.search(hypervisor_name)[0]['Name'] + assert org_session.virtwho_configure.search(name)[0]['Status'] == 'ok' + hypervisor_display_name = org_session.contenthost.search(hypervisor_name)[0]['Name'] vdc_physical = f'product_id = {settings.virtwho.sku.vdc_physical} and type=NORMAL' vdc_virtual = f'product_id = {settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED' assert ( - session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions'][ + org_session.contenthost.read_legacy_ui(hypervisor_display_name)['subscriptions'][ 'status' ] == 'Unsubscribed hypervisor' ) - session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) - assert session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' + org_session.contenthost.add_subscription(hypervisor_display_name, vdc_physical) assert ( - session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] + org_session.contenthost.search(hypervisor_name)[0]['Subscription Status'] == 'green' + ) + assert ( + org_session.contenthost.read_legacy_ui(guest_name)['subscriptions']['status'] == 'Unentitled' ) - session.contenthost.add_subscription(guest_name, vdc_virtual) - assert session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' - session.virtwho_configure.delete(name) - assert not session.virtwho_configure.search(name) + org_session.contenthost.add_subscription(guest_name, vdc_virtual) + assert org_session.contenthost.search(guest_name)[0]['Subscription Status'] == 'green' + org_session.virtwho_configure.delete(name) + assert not org_session.virtwho_configure.search(name) @pytest.mark.tier2 def test_positive_prism_central_prism_flavor_option( - self, default_org, virtwho_config, session, form_data + self, default_org, virtwho_config_ui, org_session, form_data_ui ): """Verify prism_flavor dropdown options. @@ -216,23 +184,25 @@ def test_positive_prism_central_prism_flavor_option( :CaseImportance: Medium """ - name = form_data['name'] - results = session.virtwho_configure.read(name) + name = form_data_ui['name'] + results = org_session.virtwho_configure.read(name) assert results['overview']['prism_flavor'] == "element" config_id = get_configure_id(name) config_command = get_configure_command(config_id, default_org.name) config_file = get_configure_file(config_id) - session.virtwho_configure.edit(name, {'hypervisor_content.prism_flavor': "Prism Central"}) - results = session.virtwho_configure.read(name) + org_session.virtwho_configure.edit( + name, {'hypervisor_content.prism_flavor': "Prism Central"} + ) + results = org_session.virtwho_configure.read(name) assert results['overview']['prism_flavor'] == "central" deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=default_org.label + config_command, form_data_ui['hypervisor_type'], org=default_org.label ) assert get_configure_option('prism_central', config_file) == 'true' @pytest.mark.tier2 def test_positive_ahv_internal_debug_option( - self, default_org, virtwho_config, session, form_data + self, default_org, virtwho_config_ui, org_session, form_data_ui ): """Verify ahv_internal_debug option by hammer virt-who-config" @@ -253,15 +223,15 @@ def test_positive_ahv_internal_debug_option( :BZ: 2141719 :customerscenario: true """ - name = form_data['name'] + name = form_data_ui['name'] config_id = get_configure_id(name) - values = session.virtwho_configure.read(name) + values = org_session.virtwho_configure.read(name) command = values['deploy']['command'] config_file = get_configure_file(config_id) deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label + command, form_data_ui['hypervisor_type'], debug=True, org=default_org.label ) - results = session.virtwho_configure.read(name) + results = org_session.virtwho_configure.read(name) assert str(results['overview']['ahv_internal_debug']) == 'False' # ahv_internal_debug does not set in virt-who-config-X.conf option = 'ahv_internal_debug' @@ -275,14 +245,16 @@ def test_positive_ahv_internal_debug_option( assert check_message_in_rhsm_log(message) == message # Update ahv_internal_debug option to true - session.virtwho_configure.edit(name, {'ahv_internal_debug': True}) - results = session.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'ahv_internal_debug': True}) + results = org_session.virtwho_configure.read(name) command = results['deploy']['command'] assert str(results['overview']['ahv_internal_debug']) == 'True' deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=default_org.label + command, form_data_ui['hypervisor_type'], debug=True, org=default_org.label + ) + assert ( + get_hypervisor_ahv_mapping(form_data_ui['hypervisor_type']) == 'Host UUID found for VM' ) - assert get_hypervisor_ahv_mapping(form_data['hypervisor_type']) == 'Host UUID found for VM' # ahv_internal_debug bas been set to true in virt-who-config-X.conf config_file = get_configure_file(config_id) assert get_configure_option("ahv_internal_debug", config_file) == 'true' diff --git a/tests/foreman/virtwho/ui/test_nutanix_sca.py b/tests/foreman/virtwho/ui/test_nutanix_sca.py index 3b53d038d71..42de668055e 100644 --- a/tests/foreman/virtwho/ui/test_nutanix_sca.py +++ b/tests/foreman/virtwho/ui/test_nutanix_sca.py @@ -17,7 +17,6 @@ from fauxfactory import gen_string import pytest -from robottelo.config import settings from robottelo.utils.virtwho import ( check_message_in_rhsm_log, deploy_configure_by_command, @@ -30,37 +29,11 @@ ) -@pytest.fixture() -def form_data(target_sat, module_sca_manifest_org): - form = { - 'debug': True, - 'interval': 'Every hour', - 'hypervisor_id': 'hostname', - 'hypervisor_type': settings.virtwho.ahv.hypervisor_type, - 'hypervisor_content.server': settings.virtwho.ahv.hypervisor_server, - 'hypervisor_content.username': settings.virtwho.ahv.hypervisor_username, - 'hypervisor_content.password': settings.virtwho.ahv.hypervisor_password, - 'hypervisor_content.prism_flavor': "Prism Element", - } - return form - - -@pytest.fixture() -def virtwho_config(form_data, target_sat, session_sca): - name = gen_string('alpha') - form_data['name'] = name - with session_sca: - session_sca.virtwho_configure.create(form_data) - yield virtwho_config - session_sca.virtwho_configure.delete(name) - assert not session_sca.virtwho_configure.search(name) - - class TestVirtwhoConfigforNutanix: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_ui', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, module_sca_manifest_org, virtwho_config, session_sca, form_data, deploy_type + self, module_sca_manifest_org, org_session, form_data_ui, deploy_type_ui ): """Verify configure created and deployed with id. @@ -76,29 +49,11 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - name = form_data['name'] - values = session_sca.virtwho_configure.read(name) - if deploy_type == "id": - command = values['deploy']['command'] - deploy_configure_by_command( - command, - form_data['hypervisor_type'], - debug=True, - org=module_sca_manifest_org.label, - ) - elif deploy_type == "script": - script = values['deploy']['script'] - deploy_configure_by_script( - script, - form_data['hypervisor_type'], - debug=True, - org=module_sca_manifest_org.label, - ) - assert session_sca.virtwho_configure.search(name)[0]['Status'] == 'ok' + assert org_session.virtwho_configure.search(form_data_ui['name'])[0]['Status'] == 'ok' @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, module_sca_manifest_org, virtwho_config, session_sca, form_data + self, module_sca_manifest_org, virtwho_config_ui, org_session, form_data_ui ): """Verify Hypervisor ID dropdown options. @@ -112,18 +67,18 @@ def test_positive_hypervisor_id_option( :CaseImportance: Medium """ - name = form_data['name'] - values = session_sca.virtwho_configure.read(name) + name = form_data_ui['name'] + values = org_session.virtwho_configure.read(name) config_id = get_configure_id(name) command = values['deploy']['command'] config_file = get_configure_file(config_id) for value in ['uuid', 'hostname']: - session_sca.virtwho_configure.edit(name, {'hypervisor_id': value}) - results = session_sca.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'hypervisor_id': value}) + results = org_session.virtwho_configure.read(name) assert results['overview']['hypervisor_id'] == value deploy_configure_by_command( command, - form_data['hypervisor_type'], + form_data_ui['hypervisor_type'], debug=True, org=module_sca_manifest_org.label, ) @@ -132,7 +87,7 @@ def test_positive_hypervisor_id_option( @pytest.mark.tier2 @pytest.mark.parametrize('deploy_type', ['id', 'script']) def test_positive_prism_central_deploy_configure_by_id_script( - self, module_sca_manifest_org, session_sca, form_data, deploy_type + self, module_sca_manifest_org, org_session, form_data_ui, deploy_type ): """Verify configure created and deployed with id on nutanix prism central mode @@ -151,16 +106,16 @@ def test_positive_prism_central_deploy_configure_by_id_script( :CaseImportance: High """ name = gen_string('alpha') - form_data['name'] = name - form_data['hypervisor_content.prism_flavor'] = "Prism Central" - with session_sca: - session_sca.virtwho_configure.create(form_data) - values = session_sca.virtwho_configure.read(name) + form_data_ui['name'] = name + form_data_ui['hypervisor_content.prism_flavor'] = "Prism Central" + with org_session: + org_session.virtwho_configure.create(form_data_ui) + values = org_session.virtwho_configure.read(name) if deploy_type == "id": command = values['deploy']['command'] deploy_configure_by_command( command, - form_data['hypervisor_type'], + form_data_ui['hypervisor_type'], debug=True, org=module_sca_manifest_org.label, ) @@ -168,7 +123,7 @@ def test_positive_prism_central_deploy_configure_by_id_script( script = values['deploy']['script'] deploy_configure_by_script( script, - form_data['hypervisor_type'], + form_data_ui['hypervisor_type'], debug=True, org=module_sca_manifest_org.label, ) @@ -176,13 +131,13 @@ def test_positive_prism_central_deploy_configure_by_id_script( config_id = get_configure_id(name) config_file = get_configure_file(config_id) assert get_configure_option("prism_central", config_file) == 'true' - assert session_sca.virtwho_configure.search(name)[0]['Status'] == 'ok' - session_sca.virtwho_configure.delete(name) - assert not session_sca.virtwho_configure.search(name) + assert org_session.virtwho_configure.search(name)[0]['Status'] == 'ok' + org_session.virtwho_configure.delete(name) + assert not org_session.virtwho_configure.search(name) @pytest.mark.tier2 def test_positive_prism_central_prism_flavor_option( - self, module_sca_manifest_org, virtwho_config, session_sca, form_data + self, module_sca_manifest_org, virtwho_config_ui, org_session, form_data_ui ): """Verify prism_flavor dropdown options. @@ -196,25 +151,25 @@ def test_positive_prism_central_prism_flavor_option( :CaseImportance: Medium """ - name = form_data['name'] - results = session_sca.virtwho_configure.read(name) + name = form_data_ui['name'] + results = org_session.virtwho_configure.read(name) assert results['overview']['prism_flavor'] == "element" config_id = get_configure_id(name) config_command = get_configure_command(config_id, module_sca_manifest_org.name) config_file = get_configure_file(config_id) - session_sca.virtwho_configure.edit( + org_session.virtwho_configure.edit( name, {'hypervisor_content.prism_flavor': "Prism Central"} ) - results = session_sca.virtwho_configure.read(name) + results = org_session.virtwho_configure.read(name) assert results['overview']['prism_flavor'] == "central" deploy_configure_by_command( - config_command, form_data['hypervisor_type'], org=module_sca_manifest_org.label + config_command, form_data_ui['hypervisor_type'], org=module_sca_manifest_org.label ) assert get_configure_option('prism_central', config_file) == 'true' @pytest.mark.tier2 def test_positive_ahv_internal_debug_option( - self, module_sca_manifest_org, virtwho_config, session_sca, form_data + self, module_sca_manifest_org, virtwho_config_ui, org_session, form_data_ui ): """Verify ahv_internal_debug option by hammer virt-who-config" @@ -237,15 +192,15 @@ def test_positive_ahv_internal_debug_option( :customerscenario: true """ - name = form_data['name'] + name = form_data_ui['name'] config_id = get_configure_id(name) - values = session_sca.virtwho_configure.read(name) + values = org_session.virtwho_configure.read(name) command = values['deploy']['command'] config_file = get_configure_file(config_id) deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=module_sca_manifest_org.label + command, form_data_ui['hypervisor_type'], debug=True, org=module_sca_manifest_org.label ) - results = session_sca.virtwho_configure.read(name) + results = org_session.virtwho_configure.read(name) assert str(results['overview']['ahv_internal_debug']) == 'False' # ahv_internal_debug does not set in virt-who-config-X.conf option = 'ahv_internal_debug' @@ -259,14 +214,16 @@ def test_positive_ahv_internal_debug_option( assert check_message_in_rhsm_log(message) == message # Update ahv_internal_debug option to true - session_sca.virtwho_configure.edit(name, {'ahv_internal_debug': True}) - results = session_sca.virtwho_configure.read(name) + org_session.virtwho_configure.edit(name, {'ahv_internal_debug': True}) + results = org_session.virtwho_configure.read(name) command = results['deploy']['command'] assert str(results['overview']['ahv_internal_debug']) == 'True' deploy_configure_by_command( - command, form_data['hypervisor_type'], debug=True, org=module_sca_manifest_org.label + command, form_data_ui['hypervisor_type'], debug=True, org=module_sca_manifest_org.label + ) + assert ( + get_hypervisor_ahv_mapping(form_data_ui['hypervisor_type']) == 'Host UUID found for VM' ) - assert get_hypervisor_ahv_mapping(form_data['hypervisor_type']) == 'Host UUID found for VM' # ahv_internal_debug bas been set to true in virt-who-config-X.conf config_file = get_configure_file(config_id) assert get_configure_option("ahv_internal_debug", config_file) == 'true' From 284e66f4de1d41bf68eb4e32c729d8e1ad50f1b2 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 11 Oct 2023 07:25:53 -0400 Subject: [PATCH 278/586] [6.14.z] Coverage for autoprovision and reboot of discovered host (#12879) --- conftest.py | 1 + pytest_fixtures/component/discovery.py | 31 +++++++++++ tests/foreman/api/test_discoveredhost.py | 71 +++++++++++++++++++----- 3 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 pytest_fixtures/component/discovery.py diff --git a/conftest.py b/conftest.py index eb8b21a9aa6..efc896f486a 100644 --- a/conftest.py +++ b/conftest.py @@ -38,6 +38,7 @@ 'pytest_fixtures.component.computeprofile', 'pytest_fixtures.component.contentview', 'pytest_fixtures.component.domain', + 'pytest_fixtures.component.discovery', 'pytest_fixtures.component.host', 'pytest_fixtures.component.hostgroup', 'pytest_fixtures.component.http_proxy', diff --git a/pytest_fixtures/component/discovery.py b/pytest_fixtures/component/discovery.py new file mode 100644 index 00000000000..e05b3859f2f --- /dev/null +++ b/pytest_fixtures/component/discovery.py @@ -0,0 +1,31 @@ +from fauxfactory import gen_string +import pytest + + +@pytest.fixture(scope='module') +def module_discovery_hostgroup(module_org, module_location, module_target_sat): + host = module_target_sat.api.Host(organization=module_org, location=module_location).create() + return module_target_sat.api.HostGroup( + organization=[module_org], + location=[module_location], + medium=host.medium, + root_pass=gen_string('alpha'), + operatingsystem=host.operatingsystem, + ptable=host.ptable, + domain=host.domain, + architecture=host.architecture, + ).create() + + +@pytest.fixture(scope='module') +def discovery_org(module_org, module_target_sat): + discovery_org = module_target_sat.update_setting('discovery_organization', module_org.name) + yield module_org + module_target_sat.update_setting('discovery_organization', discovery_org) + + +@pytest.fixture(scope='module') +def discovery_location(module_location, module_target_sat): + discovery_loc = module_target_sat.update_setting('discovery_location', module_location.name) + yield module_location + module_target_sat.update_setting('discovery_location', discovery_loc) diff --git a/tests/foreman/api/test_discoveredhost.py b/tests/foreman/api/test_discoveredhost.py index 9b4a5ae5e22..773175812cf 100644 --- a/tests/foreman/api/test_discoveredhost.py +++ b/tests/foreman/api/test_discoveredhost.py @@ -247,8 +247,6 @@ def test_positive_provision_pxe_less_host( :expectedresults: Host should be provisioned successfully - :CaseAutomation: NotAutomated - :CaseImportance: Critical """ sat = module_discovery_sat.sat @@ -274,9 +272,10 @@ def test_positive_provision_pxe_less_host( assert not sat.api.Host().search(query={"search": f'name={host.name}'}) pxeless_discovery_host.blank = True - @pytest.mark.stubbed @pytest.mark.tier3 - def test_positive_auto_provision_pxe_host(self): + def test_positive_auto_provision_pxe_host( + self, module_discovery_hostgroup, module_target_sat, discovery_org, discovery_location + ): """Auto provision a pxe-based host by executing discovery rules :id: c93fd7c9-41ef-4eb5-8042-f72e87e67e10 @@ -290,14 +289,24 @@ def test_positive_auto_provision_pxe_host(self): :expectedresults: Selected Host should be auto-provisioned successfully - :CaseAutomation: Automated - :CaseImportance: Critical """ + discovered_host = module_target_sat.api_factory.create_discovered_host() + + rule = module_target_sat.api.DiscoveryRule( + max_count=1, + hostgroup=module_discovery_hostgroup, + search_=f'name = {discovered_host["name"]}', + location=[discovery_location], + organization=[discovery_org], + ).create() + result = module_target_sat.api.DiscoveredHost(id=discovered_host['id']).auto_provision() + assert f'provisioned with rule {rule.name}' in result['message'] - @pytest.mark.stubbed @pytest.mark.tier3 - def test_positive_auto_provision_all(self): + def test_positive_auto_provision_all( + self, module_discovery_hostgroup, module_target_sat, discovery_org, discovery_location + ): """Auto provision all host by executing discovery rules :id: 954d3688-62d9-47f7-9106-a4fff8825ffa @@ -314,6 +323,19 @@ def test_positive_auto_provision_all(self): :CaseImportance: High """ + module_target_sat.api.DiscoveryRule( + max_count=25, + hostgroup=module_discovery_hostgroup, + search_=f'location = "{discovery_location.name}"', + location=[discovery_location], + organization=[discovery_org], + ).create() + + for _ in range(2): + module_target_sat.api_factory.create_discovered_host() + + result = module_target_sat.api.DiscoveredHost().auto_provision_all() + assert '2 discovered hosts were provisioned' in result['message'] @pytest.mark.stubbed @pytest.mark.tier3 @@ -337,9 +359,19 @@ def test_positive_refresh_facts_pxe_host(self): :CaseImportance: High """ - @pytest.mark.stubbed + @pytest.mark.on_premises_provisioning + @pytest.mark.parametrize('module_provisioning_sat', ['discovery'], indirect=True) + @pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) + @pytest.mark.rhel_ver_match('9') @pytest.mark.tier3 - def test_positive_reboot_pxe_host(self): + def test_positive_reboot_pxe_host( + self, + module_provisioning_rhel_content, + module_discovery_sat, + provisioning_host, + provisioning_hostgroup, + pxe_loader, + ): """Rebooting a pxe based discovered host :id: 69c807f8-5646-4aa6-8b3c-5ecab69560fc @@ -352,10 +384,23 @@ def test_positive_reboot_pxe_host(self): :expectedresults: Selected host should be rebooted successfully - :CaseAutomation: Automated - :CaseImportance: Medium """ + sat = module_discovery_sat.sat + provisioning_host.power_control(ensure=False) + mac = provisioning_host._broker_args['provisioning_nic_mac_addr'] + wait_for( + lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], + timeout=240, + delay=20, + ) + discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] + discovered_host.hostgroup = provisioning_hostgroup + discovered_host.location = provisioning_hostgroup.location[0] + discovered_host.organization = provisioning_hostgroup.organization[0] + discovered_host.build = True + result = sat.api.DiscoveredHost(id=discovered_host.id).reboot() + assert 'Unable to perform reboot' not in result @pytest.mark.stubbed @pytest.mark.tier3 @@ -381,8 +426,6 @@ def test_positive_reboot_all_pxe_hosts(self): class TestFakeDiscoveryTests: """Tests that use fake discovered host. - :CaseAutomation: Automated - :CaseImportance: High """ From 5dc991cfe49d0ca5291c6dbaf0f58ee2a2e41d6b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 11 Oct 2023 10:15:53 -0400 Subject: [PATCH 279/586] [6.14.z] [Repository Rewrite] Upgrade test for large repo sync (#12882) [Repository Rewrite] Upgrade test for large repo sync (#12710) * fixed failing ak upgrade test * added test for large repo sync after upgrade * added class to large repo sync tests * removed extra test * updated org_id * added customerscenario to docstring (cherry picked from commit cf98270ecb2e99a6665b7ad97346d756c0c63be5) Co-authored-by: Cole Higgins --- tests/upgrades/test_repository.py | 79 ++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/tests/upgrades/test_repository.py b/tests/upgrades/test_repository.py index 2119fa57816..998db385ad4 100644 --- a/tests/upgrades/test_repository.py +++ b/tests/upgrades/test_repository.py @@ -19,7 +19,12 @@ import pytest from robottelo.config import settings -from robottelo.constants import FAKE_0_CUSTOM_PACKAGE_NAME, FAKE_4_CUSTOM_PACKAGE_NAME +from robottelo.constants import ( + DEFAULT_ARCHITECTURE, + FAKE_0_CUSTOM_PACKAGE_NAME, + FAKE_4_CUSTOM_PACKAGE_NAME, + REPOS, +) from robottelo.hosts import ContentHost UPSTREAM_USERNAME = 'rTtest123' @@ -299,3 +304,75 @@ def test_post_scenario_custom_repo_sca_toggle(self, pre_upgrade_data): result = rhel_client.execute('subscription-manager repo-override --list') assert 'enabled: 1' in result.stdout assert f'{org_name}_{product_name}_{repo_name}' in result.stdout + + +class TestScenarioLargeRepoSyncCheck: + """Scenario test to verify that large repositories can be synced without + failure after an upgrade. + + Test Steps: + + 1. Before Satellite upgrade. + 2. Enable and sync large RH repository. + 3. Upgrade Satellite. + 4. Enable and sync a second large repository. + + BZ: 2043144 + + :customerscenario: true + """ + + @pytest.mark.pre_upgrade + def test_pre_scenario_sync_large_repo( + self, target_sat, module_entitlement_manifest_org, save_test_data + ): + """This is a pre-upgrade scenario to verify that users can sync large repositories + before an upgrade + + :id: afb957dc-c509-4009-ac85-4b71b64d3c74 + + :steps: + 1. Enable a large redhat repository + 2. Sync repository and assert sync succeeds + + :expectedresults: Large Repositories should succeed when synced + """ + rh_repo_id = target_sat.api_factory.enable_rhrepo_and_fetchid( + basearch=DEFAULT_ARCHITECTURE, + org_id=module_entitlement_manifest_org.id, + product=REPOS['rhel8_bos']['product'], + repo=REPOS['rhel8_bos']['name'], + reposet=REPOS['rhel8_bos']['reposet'], + releasever=REPOS['rhel8_bos']['releasever'], + ) + repo = target_sat.api.Repository(id=rh_repo_id).read() + res = repo.sync(timeout=2000) + assert res['result'] == 'success' + save_test_data({'org_id': module_entitlement_manifest_org.id}) + + @pytest.mark.post_upgrade(depend_on=test_pre_scenario_sync_large_repo) + def test_post_scenario_sync_large_repo(self, target_sat, pre_upgrade_data): + """This is a post-upgrade scenario to verify that large repositories can be + synced after an upgrade + + :id: 7bdbb2ac-7197-4e1a-8163-5852943eb49b + + :steps: + 1. Sync large repository + 2. Upgrade satellite + 3. Sync a second large repository in that same organization + + :expectedresults: Large repositories should succeed after an upgrade + """ + org_id = pre_upgrade_data.get('org_id') + rh_repo_id = target_sat.api_factory.enable_rhrepo_and_fetchid( + basearch=DEFAULT_ARCHITECTURE, + org_id=org_id, + product=REPOS['rhel8_aps']['product'], + repo=REPOS['rhel8_aps']['name'], + reposet=REPOS['rhel8_aps']['reposet'], + releasever=REPOS['rhel8_aps']['releasever'], + ) + repo = target_sat.api.Repository(id=rh_repo_id).read() + res = repo.sync(timeout=4000) + assert res['result'] == 'success' From eef622dc676d622f8220d4528a1c6d9b33b55338 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 12 Oct 2023 01:55:44 -0400 Subject: [PATCH 280/586] [6.14.z] Fix VMware tests (#12885) --- robottelo/constants/__init__.py | 2 +- tests/foreman/cli/test_computeresource_vmware.py | 2 +- tests/foreman/ui/test_computeresource_vmware.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index 079057b2ebc..635457c88db 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -1726,7 +1726,7 @@ class Colored(Box): VMWARE_CONSTANTS = { 'folder': 'vm', - 'guest_os': 'Red Hat Enterprise Linux 8 (64-bit)', + 'guest_os': 'Red Hat Enterprise Linux 8 (64 bit)', 'scsicontroller': 'LSI Logic Parallel', 'virtualhw_version': 'Default', 'pool': 'Resources', diff --git a/tests/foreman/cli/test_computeresource_vmware.py b/tests/foreman/cli/test_computeresource_vmware.py index 84ebc98af7c..3606b0a077d 100644 --- a/tests/foreman/cli/test_computeresource_vmware.py +++ b/tests/foreman/cli/test_computeresource_vmware.py @@ -56,7 +56,7 @@ def test_positive_vmware_cr_end_to_end(target_sat, module_org, module_location): ) assert vmware_cr['name'] == cr_name assert vmware_cr['locations'][0] == module_location.name - assert vmware_cr['organizations'] == module_org.name + assert vmware_cr['organizations'][0] == module_org.name assert vmware_cr['server'] == settings.vmware.vcenter assert vmware_cr['datacenter'] == settings.vmware.datacenter # List diff --git a/tests/foreman/ui/test_computeresource_vmware.py b/tests/foreman/ui/test_computeresource_vmware.py index 1f93bbe673a..34d0127e983 100644 --- a/tests/foreman/ui/test_computeresource_vmware.py +++ b/tests/foreman/ui/test_computeresource_vmware.py @@ -557,6 +557,7 @@ def test_positive_virt_card(session, target_sat, module_location, module_org): settings.vmware.vm_name, hostgroup_name, module_location.name, + name=settings.vmware.vm_name, ) host_name = '.'.join([settings.vmware.vm_name, domain.name]) power_status = session.computeresource.vm_status(cr_name, settings.vmware.vm_name) From cebd2d9c2ff83964d5c1a49688ae9147fbfdecf3 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 12 Oct 2023 03:26:43 -0400 Subject: [PATCH 281/586] [6.14.z] Validate testimony for upgrade tests (#12889) Validate testimony for upgrade tests (#12878) Signed-off-by: Gaurav Talreja (cherry picked from commit 4352c9bfb3843fd6ea77ca19ae2953b96fa5e4c0) Co-authored-by: Gaurav Talreja --- Makefile | 1 + tests/upgrades/test_activation_key.py | 8 ++++---- tests/upgrades/test_bookmarks.py | 6 ++---- tests/upgrades/test_capsule.py | 3 +-- tests/upgrades/test_errata.py | 14 ++++++-------- tests/upgrades/test_satellitesync.py | 9 ++++----- tests/upgrades/test_syncplan.py | 8 +++----- tests/upgrades/test_usergroup.py | 2 +- 8 files changed, 22 insertions(+), 29 deletions(-) diff --git a/Makefile b/Makefile index 2f73c60d3b4..ff0920c70ee 100644 --- a/Makefile +++ b/Makefile @@ -73,6 +73,7 @@ test-docstrings: uuid-check testimony $(TESTIMONY_OPTIONS) validate tests/foreman/ui testimony $(TESTIMONY_OPTIONS) validate tests/foreman/virtwho testimony $(TESTIMONY_OPTIONS) validate tests/foreman/maintain + testimony $(TESTIMONY_OPTIONS) validate tests/upgrades test-robottelo: $(info "Running robottelo framework unit tests...") diff --git a/tests/upgrades/test_activation_key.py b/tests/upgrades/test_activation_key.py index 982e9e945a7..7e7cdf9de21 100644 --- a/tests/upgrades/test_activation_key.py +++ b/tests/upgrades/test_activation_key.py @@ -60,14 +60,14 @@ def test_pre_create_activation_key(self, activation_key_setup, target_sat): :steps: 1. Create the activation key. 2. Add subscription in the activation key. - 3: Check the subscription id of the activation key and compare it with custom_repos - product id. + 3. Check the subscription id of the activation key and compare it with custom_repos + product id. 4. Update the host collection in the activation key. :parametrized: yes :expectedresults: Activation key should be created successfully and it's subscription id - should be same with custom repos product id. + should be same with custom repos product id. """ ak = activation_key_setup['ak'] org_subscriptions = target_sat.api.Subscription( @@ -95,7 +95,7 @@ def test_post_crud_activation_key(self, dependent_scenario_name, target_sat): 3. Delete activation key. :expectedresults: Activation key's entities should be same after upgrade and activation - key update and delete should work. + key update and delete should work. """ pre_test_name = dependent_scenario_name org = target_sat.api.Organization().search(query={'search': f'name={pre_test_name}_org'}) diff --git a/tests/upgrades/test_bookmarks.py b/tests/upgrades/test_bookmarks.py index dde6b112a3b..a3f330e7569 100644 --- a/tests/upgrades/test_bookmarks.py +++ b/tests/upgrades/test_bookmarks.py @@ -77,7 +77,7 @@ def test_post_create_public_disable_bookmark(self, dependent_scenario_name, targ 2. Remove the bookmark. :expectedresults: Public disabled bookmarks details for all the system entities - should be unchanged after upgrade. + should be unchanged after upgrade. :CaseImportance: Critical """ @@ -106,7 +106,6 @@ def test_pre_create_public_enable_bookmark(self, request, target_sat): :id: preupgrade-93c419db-66b4-4c9a-a82a-a6a68703881f :Steps: - 1. Create public enable bookmarks before the upgrade for all system entities using available bookmark data. 2. Check the bookmark attribute(controller, name, query public) status @@ -142,12 +141,11 @@ def test_post_create_public_enable_bookmark(self, dependent_scenario_name, targe :id: postupgrade-93c419db-66b4-4c9a-a82a-a6a68703881f :Steps: - 1. Check the bookmark status after post-upgrade. 2. Remove the bookmark. :expectedresults: Public disabled bookmarks details for all the system entities - should be unchanged after upgrade. + should be unchanged after upgrade. :CaseImportance: Critical """ diff --git a/tests/upgrades/test_capsule.py b/tests/upgrades/test_capsule.py index 19f8ad87b9e..e4f2d8a0720 100644 --- a/tests/upgrades/test_capsule.py +++ b/tests/upgrades/test_capsule.py @@ -59,8 +59,7 @@ def test_pre_user_scenario_capsule_sync(self, target_sat, default_org, save_test :expectedresults: 1. The repo/rpm should be synced to satellite 2. Activation key's environment id should be available in the content views environment - id's list - + id's list """ ak_name = ( settings.upgrade.capsule_ak[settings.upgrade.os] diff --git a/tests/upgrades/test_errata.py b/tests/upgrades/test_errata.py index 0f4bc84e2f6..caa4cf21fc5 100644 --- a/tests/upgrades/test_errata.py +++ b/tests/upgrades/test_errata.py @@ -115,17 +115,15 @@ def test_pre_scenario_generate_errata_for_client( :id: preupgrade-88fd28e6-b4df-46c0-91d6-784859fd1c21 :steps: - 1. Create Product and Custom Yum Repo 2. Create custom tools, rhel repos and sync them 3. Create content view and publish it 4. Create activation key and add subscription 5. Register RHEL host to Satellite - 7. Generate Errata by installing outdated/older packages - 8. Check that errata applicability generated expected errata list for the given client. + 6. Generate Errata by installing outdated/older packages + 7. Check that errata applicability generated expected errata list for the given client. :expectedresults: - 1. The content host is created 2. errata count, erratum list will be generated to Satellite content host 3. All the expected errata are ready-to-be-applied on the client @@ -203,10 +201,10 @@ def test_post_scenario_errata_count_installation(self, target_sat, pre_upgrade_d 1. Recover pre_upgrade data for post_upgrade verification 2. Verify errata count has not changed on Satellite - 4. Verify the errata_ids - 5. Verify installation of errata is successfull - 6. Verify that the errata application updated packages on client - 7. Verify that all expected erratas were installed on client. + 3. Verify the errata_ids + 4. Verify installation of errata is successfull + 5. Verify that the errata application updated packages on client + 6. Verify that all expected erratas were installed on client. :expectedresults: 1. errata count and erratum list should same after Satellite upgrade diff --git a/tests/upgrades/test_satellitesync.py b/tests/upgrades/test_satellitesync.py index 5024cf93c18..dcbc7866f86 100644 --- a/tests/upgrades/test_satellitesync.py +++ b/tests/upgrades/test_satellitesync.py @@ -33,13 +33,12 @@ def test_pre_version_cv_export_import(self, module_org, target_sat, save_test_da :id: preupgrade-f19e4928-94db-4df6-8ce8-b5e4afe34258 :steps: - - 1. Create a ContentView - 2. Publish and promote the Content View - 3. Check the package count of promoted content view. + 1. Create a ContentView + 2. Publish and promote the Content View + 3. Check the package count of promoted content view. :expectedresults: Before the upgrade, Content view published and promoted, and package - count should be greater than 0. + count should be greater than 0. """ product = target_sat.api.Product(organization=module_org).create() repo = target_sat.api.Repository( diff --git a/tests/upgrades/test_syncplan.py b/tests/upgrades/test_syncplan.py index 63fdb7ee164..ba14f9de831 100644 --- a/tests/upgrades/test_syncplan.py +++ b/tests/upgrades/test_syncplan.py @@ -41,7 +41,6 @@ def test_pre_sync_plan_migration(self, request, target_sat): 3. Assign sync plan to product and sync the repo :expectedresults: Run sync plan create, get, assign and verify it should pass - """ org = target_sat.api.Organization(name=f'{request.node.name}_org').create() sync_plan = target_sat.api.SyncPlan( @@ -71,7 +70,7 @@ def test_pre_disabled_sync_plan_logic(self, request, target_sat): 5. Re enable the sync plan :expectedresults: Sync plan is created and assigned to a product. The associated recurring - logic is cancelled and then the plan is re-enabled so that it gets a new recurring logic. + logic is cancelled and then the plan is re-enabled so that it gets a new recurring logic. :BZ: 1887511 @@ -114,8 +113,7 @@ def test_post_sync_plan_migration(self, request, dependent_scenario_name, target 2. Check the all available sync_interval type update with pre-created sync_plan :expectedresults: After upgrade, the sync plan should remain the same with their all - target_sat.api and sync_interval updated with their all supported sync interval type. - + target_sat.api and sync_interval updated with their all supported sync interval type. """ pre_test_name = dependent_scenario_name org = target_sat.api.Organization().search(query={'search': f'name="{pre_test_name}_org"'})[ @@ -156,7 +154,7 @@ def test_post_disabled_sync_plan_logic(self, request, dependent_scenario_name, t 2. Check the all available sync_interval type update with pre-created sync_plan. :expectedresults: Update proceedes without any errors. After upgrade, the sync plan - should remain the same with all entities + should remain the same with all entities :BZ: 1887511 diff --git a/tests/upgrades/test_usergroup.py b/tests/upgrades/test_usergroup.py index 838a074c363..0420a8a5e8c 100644 --- a/tests/upgrades/test_usergroup.py +++ b/tests/upgrades/test_usergroup.py @@ -101,7 +101,7 @@ def test_post_verify_user_group_membership( 2. Update ldap auth. :expectedresults: After upgrade, user group membership should remain the same and LDAP - auth update should work. + auth update should work. """ ad_data = ad_data() user_group = target_sat.api.UserGroup().search( From bce0c33042ad21b41619b797261b7e610a57d64d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 13 Oct 2023 04:57:54 -0400 Subject: [PATCH 282/586] [6.14.z] Fix Puppet tests (#12898) --- robottelo/hosts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robottelo/hosts.py b/robottelo/hosts.py index ef04881b5e5..2971cdcd79a 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1956,7 +1956,7 @@ def delete_puppet_class(self, puppetclass_name): for hostgroup in puppet_class.read().hostgroup: hostgroup.delete_puppetclass(data={'puppetclass_id': puppet_class.id}) # Search and remove puppet class from affected hosts - for host in self.api.Host().search(query={'search': f'class={puppet_class.name}'}): + for host in self.api.Host(puppetclass=f'{puppet_class.name}').search(): host.delete_puppetclass(data={'puppetclass_id': puppet_class.id}) # Remove puppet class entity puppet_class.delete() From 9957acac2a61522df4b00367ce19f380ef90a0e3 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 13 Oct 2023 08:13:07 -0400 Subject: [PATCH 283/586] [6.14.z] Fix typo for organization field in global registration (#12903) Fix typo for organization field in global registration (#12902) Signed-off-by: Gaurav Talreja (cherry picked from commit 5b7f1cb93292ae3f55045c06e49a105b6126f212) Co-authored-by: Gaurav Talreja --- tests/foreman/ui/test_host.py | 2 +- tests/foreman/ui/test_rhcloud_insights.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index 27ec9ca39cd..08ce12c02ff 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -1391,7 +1391,7 @@ def test_global_registration_with_capsule_host( session.location.select(loc_name=module_location.name) cmd = session.host.get_register_command( { - 'general.orgnization': module_org.name, + 'general.organization': module_org.name, 'general.location': module_location.name, 'general.operating_system': default_os.title, 'general.capsule': capsule_configured.hostname, diff --git a/tests/foreman/ui/test_rhcloud_insights.py b/tests/foreman/ui/test_rhcloud_insights.py index 6ba96238d21..153e2a11876 100644 --- a/tests/foreman/ui/test_rhcloud_insights.py +++ b/tests/foreman/ui/test_rhcloud_insights.py @@ -356,7 +356,7 @@ def test_insights_registration_with_capsule( cmd = session.host_new.get_register_command( { 'general.operating_system': default_os.title, - 'general.orgnization': org.name, + 'general.organization': org.name, 'general.capsule': rhcloud_capsule.hostname, 'general.activation_keys': ak.name, 'general.insecure': True, From 1d440052d2dc18aa545971cb2c2da98f4443ee33 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 16 Oct 2023 00:10:48 -0400 Subject: [PATCH 284/586] [6.14.z] Bump pre-commit from 3.4.0 to 3.5.0 (#12912) --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 6da8ffd5441..8d8abe4d798 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -2,7 +2,7 @@ flake8==6.1.0 pytest-cov==4.1.0 redis==5.0.1 -pre-commit==3.4.0 +pre-commit==3.5.0 # For generating documentation. sphinx==7.2.6 From 37a7bac3767844a59c732f506f3a0395e428008d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 16 Oct 2023 13:13:54 -0400 Subject: [PATCH 285/586] [6.14.z] Fix packages status (#12917) Fix packages status (#12845) (cherry picked from commit 2a013f8a829d7e73032d370bf9723a83f7aa0a99) Co-authored-by: Griffin Sullivan <48397354+Griffin-Sullivan@users.noreply.github.com> --- robottelo/cli/sm_packages.py | 1 + 1 file changed, 1 insertion(+) diff --git a/robottelo/cli/sm_packages.py b/robottelo/cli/sm_packages.py index d56a1156ec9..ece44a773e0 100644 --- a/robottelo/cli/sm_packages.py +++ b/robottelo/cli/sm_packages.py @@ -51,6 +51,7 @@ def is_locked(cls, options=None): def status(cls, options=None): """Build satellite-maintain packages status""" cls.command_sub = 'status' + cls.command_end = None options = options or {} return cls.sm_execute(cls._construct_command(options)) From 962cc4338ce6851fd5640cc59798045411799271 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 18 Oct 2023 00:04:54 -0400 Subject: [PATCH 286/586] [6.14.z] Bump pytest-reportportal from 5.2.2 to 5.3.0 (#12934) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bca8b41f01a..a3aeb89d14f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ python-box==7.1.1 pytest==7.4.2 pytest-services==2.2.1 pytest-mock==3.11.1 -pytest-reportportal==5.2.2 +pytest-reportportal==5.3.0 pytest-xdist==3.3.1 pytest-ibutsu==2.2.4 PyYAML==6.0.1 From 6bc0b7cebfbb4e4956bbd212b001282bb5b32c1a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 18 Oct 2023 03:03:29 -0400 Subject: [PATCH 287/586] [6.14.z] Use sAMAccountName (#12929) Use sAMAccountName (cherry picked from commit ed25cc48b772c07cd307e4dc235508b5954968e4) Co-authored-by: Lukas Hellebrandt --- tests/foreman/cli/test_ldapauthsource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/cli/test_ldapauthsource.py b/tests/foreman/cli/test_ldapauthsource.py index 92d5185cc83..3feed9bad43 100644 --- a/tests/foreman/cli/test_ldapauthsource.py +++ b/tests/foreman/cli/test_ldapauthsource.py @@ -127,7 +127,7 @@ def test_positive_refresh_usergroup_with_ad(self, member_group, ad_data): 'attr-firstname': LDAP_ATTR['firstname'], 'attr-lastname': LDAP_ATTR['surname'], 'attr-mail': LDAP_ATTR['mail'], - 'account': ad_data['ldap_user_name'], + 'account': fr"{ad_data['workgroup']}\{ad_data['ldap_user_name']}", 'account-password': ad_data['ldap_user_passwd'], 'base-dn': ad_data['base_dn'], } From c22e3f18324f1306326ae6a2e11876682414dd2f Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 18 Oct 2023 03:25:25 -0400 Subject: [PATCH 288/586] [6.14.z] Bump deepdiff from 6.6.0 to 6.6.1 (#12938) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a3aeb89d14f..a585d28df3b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ betelgeuse==1.10.0 broker[docker]==0.4.1 cryptography==41.0.4 -deepdiff==6.6.0 +deepdiff==6.6.1 dynaconf[vault]==3.2.3 fauxfactory==3.1.0 jinja2==3.1.2 From 861ab2a53208c45cbfa3bcc0044fa0c59b4206bf Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 18 Oct 2023 06:53:33 -0400 Subject: [PATCH 289/586] [6.14.z] Fixing Discovery tests (#12927) --- tests/foreman/api/test_discoveredhost.py | 12 ++++++------ tests/foreman/cli/test_discoveredhost.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/foreman/api/test_discoveredhost.py b/tests/foreman/api/test_discoveredhost.py index 773175812cf..0db7c486bcf 100644 --- a/tests/foreman/api/test_discoveredhost.py +++ b/tests/foreman/api/test_discoveredhost.py @@ -172,7 +172,7 @@ class TestDiscoveredHost: @pytest.mark.on_premises_provisioning @pytest.mark.parametrize('module_provisioning_sat', ['discovery'], indirect=True) @pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) - @pytest.mark.rhel_ver_match('[^6]') + @pytest.mark.rhel_ver_list([8, 9]) @pytest.mark.tier3 def test_positive_provision_pxe_host( self, @@ -204,8 +204,8 @@ def test_positive_provision_pxe_host( mac = provisioning_host._broker_args['provisioning_nic_mac_addr'] wait_for( lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], - timeout=240, - delay=20, + timeout=600, + delay=40, ) discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] discovered_host.hostgroup = provisioning_hostgroup @@ -227,7 +227,7 @@ def test_positive_provision_pxe_host( @pytest.mark.on_premises_provisioning @pytest.mark.parametrize('module_provisioning_sat', ['discovery'], indirect=True) @pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) - @pytest.mark.rhel_ver_match('[^6]') + @pytest.mark.rhel_ver_list([8, 9]) @pytest.mark.tier3 def test_positive_provision_pxe_less_host( self, @@ -254,8 +254,8 @@ def test_positive_provision_pxe_less_host( mac = pxeless_discovery_host._broker_args['provisioning_nic_mac_addr'] wait_for( lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], - timeout=240, - delay=20, + timeout=600, + delay=40, ) discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] discovered_host.hostgroup = provisioning_hostgroup diff --git a/tests/foreman/cli/test_discoveredhost.py b/tests/foreman/cli/test_discoveredhost.py index 031116a2e09..4ee7015ec03 100644 --- a/tests/foreman/cli/test_discoveredhost.py +++ b/tests/foreman/cli/test_discoveredhost.py @@ -24,7 +24,7 @@ @pytest.mark.on_premises_provisioning @pytest.mark.parametrize('module_provisioning_sat', ['discovery'], indirect=True) @pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) -@pytest.mark.rhel_ver_match('8') +@pytest.mark.rhel_ver_match('7') def test_rhel_pxe_discovery_provisioning( module_provisioning_rhel_content, module_discovery_sat, @@ -57,8 +57,8 @@ def test_rhel_pxe_discovery_provisioning( wait_for( lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], - timeout=240, - delay=20, + timeout=600, + delay=40, ) discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] discovered_host.hostgroup = provisioning_hostgroup @@ -95,7 +95,7 @@ def _finalize(): @pytest.mark.on_premises_provisioning @pytest.mark.parametrize('module_provisioning_sat', ['discovery'], indirect=True) @pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) -@pytest.mark.rhel_ver_match('8') +@pytest.mark.rhel_ver_match('7') def test_rhel_pxeless_discovery_provisioning( module_discovery_sat, pxeless_discovery_host, @@ -120,8 +120,8 @@ def test_rhel_pxeless_discovery_provisioning( wait_for( lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], - timeout=240, - delay=20, + timeout=600, + delay=40, ) discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] discovered_host.hostgroup = provisioning_hostgroup From 70e12f6e128c100b0c56ff5a6fc1b2862fe01767 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 19 Oct 2023 09:22:43 -0400 Subject: [PATCH 290/586] [6.14.z] Fix maintain CLI caching class attributes (#12941) --- robottelo/cli/sm_packages.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/robottelo/cli/sm_packages.py b/robottelo/cli/sm_packages.py index ece44a773e0..d4674279172 100644 --- a/robottelo/cli/sm_packages.py +++ b/robottelo/cli/sm_packages.py @@ -30,6 +30,7 @@ class Packages(Base): def lock(cls, options=None): """Build satellite-maintain packages lock""" cls.command_sub = 'lock' + cls.command_end = None options = options or {} return cls.sm_execute(cls._construct_command(options)) @@ -37,6 +38,7 @@ def lock(cls, options=None): def unlock(cls, options=None): """Build satellite-maintain packages unlock""" cls.command_sub = 'unlock' + cls.command_end = None options = options or {} return cls.sm_execute(cls._construct_command(options)) @@ -44,6 +46,7 @@ def unlock(cls, options=None): def is_locked(cls, options=None): """Build satellite-maintain packages is-locked""" cls.command_sub = 'is-locked' + cls.command_end = None options = options or {} return cls.sm_execute(cls._construct_command(options)) @@ -75,5 +78,6 @@ def update(cls, packages='', options=None): def check_update(cls, options=None): """Build satellite-maintain packages check-update""" cls.command_sub = 'check-update' + cls.command_end = None options = options or {} return cls.sm_execute(cls._construct_command(options)) From e5521ea4f48c39d24598461f26d37f471fd2fba0 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 20 Oct 2023 00:16:45 -0400 Subject: [PATCH 291/586] [6.14.z] Bump pytest-mock from 3.11.1 to 3.12.0 (#12951) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a585d28df3b..98e78baf601 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ pyotp==2.9.0 python-box==7.1.1 pytest==7.4.2 pytest-services==2.2.1 -pytest-mock==3.11.1 +pytest-mock==3.12.0 pytest-reportportal==5.3.0 pytest-xdist==3.3.1 pytest-ibutsu==2.2.4 From 14b9d179f7d0bdf564047694d6df1205cf8b213b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 20 Oct 2023 04:10:19 -0400 Subject: [PATCH 292/586] [6.14.z] Optional pytest Vault login and code nitpicking (#12867) Optional pytest Vault login and code nitpicking (#12822) * Optional pytest Vault login and code formating * Multiple options supported for non-vault pytest session (cherry picked from commit 9e851610c43e50ab8e472c1fd341394c0b16fd3c) Co-authored-by: Jitendra Yejare --- robottelo/utils/vault.py | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/robottelo/utils/vault.py b/robottelo/utils/vault.py index a4b5d48adb4..b0fc77d861e 100644 --- a/robottelo/utils/vault.py +++ b/robottelo/utils/vault.py @@ -19,25 +19,31 @@ class Vault: def __init__(self, env_file='.env'): self.env_path = robottelo_root_dir.joinpath(env_file) + self.envdata = None + self.vault_enabled = None def setup(self): - self.export_vault_addr() + if self.env_path.exists(): + self.envdata = self.env_path.read_text() + is_enabled = re.findall('\nVAULT_ENABLED_FOR_DYNACONF=(.*)', self.envdata) + if is_enabled: + self.vault_enabled = is_enabled[0] + self.export_vault_addr() def teardown(self): del os.environ['VAULT_ADDR'] def export_vault_addr(self): - envdata = self.env_path.read_text() - vaulturl = re.findall('VAULT_URL_FOR_DYNACONF=(.*)', envdata)[0] + vaulturl = re.findall('VAULT_URL_FOR_DYNACONF=(.*)', self.envdata)[0] # Set Vault CLI Env Var os.environ['VAULT_ADDR'] = vaulturl # Dynaconf Vault Env Vars - if re.findall('VAULT_ENABLED_FOR_DYNACONF=(.*)', envdata)[0] == 'true': + if self.vault_enabled and self.vault_enabled in ['True', 'true']: if 'localhost:8200' in vaulturl: raise InvalidVaultURLForOIDC( - f"{vaulturl} doesnt supports OIDC login," + f"{vaulturl} doesn't support OIDC login," "please change url to corp vault in env file!" ) @@ -63,7 +69,11 @@ def exec_vault_command(self, command: str, **kwargs): return vcommand def login(self, **kwargs): - if 'VAULT_SECRET_ID_FOR_DYNACONF' not in os.environ: + if ( + self.vault_enabled + and self.vault_enabled in ['True', 'true'] + and 'VAULT_SECRET_ID_FOR_DYNACONF' not in os.environ + ): if self.status(**kwargs).returncode != 0: logger.warning( "Warning! The browser is about to open for vault OIDC login, " @@ -81,22 +91,22 @@ def login(self, **kwargs): ).stdout token = json.loads(str(token.decode('UTF-8')))['data']['id'] # Setting new token in env file - envdata = self.env_path.read_text() - envdata = re.sub( - '.*VAULT_TOKEN_FOR_DYNACONF=.*', f"VAULT_TOKEN_FOR_DYNACONF={token}", envdata + _envdata = re.sub( + '.*VAULT_TOKEN_FOR_DYNACONF=.*', + f"VAULT_TOKEN_FOR_DYNACONF={token}", + self.envdata, ) - self.env_path.write_text(envdata) + self.env_path.write_text(_envdata) logger.info( "Success! New OIDC token added to .env file to access secrets from vault!" ) def logout(self): # Teardown - Setting dymmy token in env file - envdata = self.env_path.read_text() - envdata = re.sub( - '.*VAULT_TOKEN_FOR_DYNACONF=.*', "# VAULT_TOKEN_FOR_DYNACONF=myroot", envdata + _envdata = re.sub( + '.*VAULT_TOKEN_FOR_DYNACONF=.*', "# VAULT_TOKEN_FOR_DYNACONF=myroot", self.envdata ) - self.env_path.write_text(envdata) + self.env_path.write_text(_envdata) self.exec_vault_command('vault token revoke -self') logger.info("Success! OIDC token removed from Env file successfully!") From aa7a78fb719e3f6543231b5626931e64db6a5e99 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 20 Oct 2023 05:30:04 -0400 Subject: [PATCH 293/586] [6.14.z] Fix Container Image Tag test (#12952) Fix Container Image Tag test (#12848) (cherry picked from commit d1dcab6d531b1c9787aca3e3c0b5d8255615861c) Co-authored-by: vsedmik <46570670+vsedmik@users.noreply.github.com> --- tests/foreman/ui/test_containerimagetag.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/foreman/ui/test_containerimagetag.py b/tests/foreman/ui/test_containerimagetag.py index 681cb1e877d..a1e68bc7e61 100644 --- a/tests/foreman/ui/test_containerimagetag.py +++ b/tests/foreman/ui/test_containerimagetag.py @@ -22,9 +22,11 @@ from robottelo.constants import ( CONTAINER_REGISTRY_HUB, CONTAINER_UPSTREAM_NAME, + DEFAULT_CV, ENVIRONMENT, REPO_TYPE, ) +from robottelo.utils.issue_handlers import is_open @pytest.fixture(scope="module") @@ -49,7 +51,6 @@ def module_repository(module_product): return repo -@pytest.mark.skip_if_open("BZ:2009069") @pytest.mark.tier2 def test_positive_search(session, module_org, module_product, module_repository): """Search for a docker image tag and reads details of it @@ -60,13 +61,22 @@ def test_positive_search(session, module_org, module_product, module_repository) details are read :CaseLevel: Integration + + :BZ: 2009069, 2242515 """ with session: session.organization.select(org_name=module_org.name) search = session.containerimagetag.search('latest') - assert module_product.name in [i['Product Name'] for i in search] - assert module_repository.name in [i['Repository Name'] for i in search] + if not is_open('BZ:2242515'): + assert module_product.name in [i['Product Name'] for i in search] values = session.containerimagetag.read('latest') - assert module_product.name == values['details']['product'] - assert module_repository.name == values['details']['repository'] + if not is_open('BZ:2242515'): + assert module_product.name == values['details']['product'] assert values['lce']['table'][0]['Environment'] == ENVIRONMENT + repo_line = next( + (item for item in values['repos']['table'] if item['Name'] == module_repository.name), + None, + ) + assert module_product.name == repo_line['Product'] + assert DEFAULT_CV == repo_line['Content View'] + assert 'Success' in repo_line['Last Sync'] From d93867eaf68527c6f23d986148765f21e3813ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Va=C5=A1ina?= Date: Fri, 20 Oct 2023 19:30:56 +0200 Subject: [PATCH 294/586] [6.14.z] Acs test coverage (#12957) * Acs test coverage added * Fix docstring * Assert changed * Comments addressed * Precommit ran * Docstring fixed * Address comments and add class_sca_manifest --- pytest_fixtures/component/taxonomy.py | 15 + robottelo/constants/__init__.py | 3 +- tests/foreman/ui/test_acs.py | 463 ++++++++++++++++++++++++++ 3 files changed, 480 insertions(+), 1 deletion(-) create mode 100644 tests/foreman/ui/test_acs.py diff --git a/pytest_fixtures/component/taxonomy.py b/pytest_fixtures/component/taxonomy.py index faad21122e5..c6140cebb63 100644 --- a/pytest_fixtures/component/taxonomy.py +++ b/pytest_fixtures/component/taxonomy.py @@ -103,6 +103,13 @@ def module_sca_manifest_org(module_org, module_sca_manifest, module_target_sat): return module_org +@pytest.fixture(scope='class') +def class_sca_manifest_org(class_org, class_sca_manifest, class_target_sat): + """Creates an organization and uploads an SCA mode manifest generated with manifester""" + class_target_sat.upload_manifest(class_org.id, class_sca_manifest.content) + return class_org + + @pytest.fixture(scope='module') def module_extra_rhel_entitlement_manifest_org( module_target_sat, @@ -197,6 +204,14 @@ def module_sca_manifest(): yield manifest +@pytest.fixture(scope='class') +def class_sca_manifest(): + """Yields a manifest in Simple Content Access mode with subscriptions determined by the + `manifest_category.golden_ticket` setting in conf/manifest.yaml.""" + with Manifester(manifest_category=settings.manifest.golden_ticket) as manifest: + yield manifest + + @pytest.fixture(scope='function') def function_entitlement_manifest(): """Yields a manifest in entitlement mode with subscriptions determined by the diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index 635457c88db..6c07eb49919 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -222,7 +222,8 @@ class Colored(Box): 'yum': "yum", 'ostree': "ostree", 'docker': "docker", - "ansible_collection": "ansible collection", + 'ansible_collection': "ansible collection", + 'file': "file", } DOWNLOAD_POLICIES = { diff --git a/tests/foreman/ui/test_acs.py b/tests/foreman/ui/test_acs.py new file mode 100644 index 00000000000..e1c40911c6e --- /dev/null +++ b/tests/foreman/ui/test_acs.py @@ -0,0 +1,463 @@ +"""Tests for Alternate Content Sources UI + +:Requirement: AlternateContentSources + +:CaseAutomation: Automated + +:CaseLevel: Acceptance + +:CaseComponent: AlternateContentSources + +:Team: Phoenix-content + +:TestType: Functional + +:CaseImportance: High + +:Upstream: No +""" +import pytest + +from robottelo import constants +from robottelo.constants import REPO_TYPE +from robottelo.constants.repos import CUSTOM_FILE_REPO +from robottelo.utils.datafactory import gen_string + +ssl_name, product_name, product_label, product_description, repository_name = ( + gen_string('alpha') for _ in range(5) +) +repos_to_enable = ['rhae2.9_el8'] + + +@pytest.fixture(scope='class') +def acs_setup(class_target_sat, class_sca_manifest_org): + """ + This fixture creates all the necessary data for the test to run. + It creates an organization, content credentials, product and repositories. + """ + class_target_sat.api.ContentCredential( + name=ssl_name, + content=gen_string('alpha'), + organization=class_sca_manifest_org.id, + content_type="cert", + ).create() + + product = class_target_sat.api.Product( + name=product_name, organization=class_sca_manifest_org.id + ).create() + + class_target_sat.api.Repository( + product=product, content_type=REPO_TYPE['file'], url=CUSTOM_FILE_REPO + ).create() + + for repo in repos_to_enable: + class_target_sat.cli.RepositorySet.enable( + { + 'organization-id': class_sca_manifest_org.id, + 'name': constants.REPOS[repo]['reposet'], + 'product': constants.REPOS[repo]['product'], + 'releasever': constants.REPOS[repo]['version'], + 'basearch': constants.DEFAULT_ARCHITECTURE, + } + ) + + return class_target_sat, class_sca_manifest_org + + +class TestAllAcsTypes: + """ + Test class insuring fixture is ran once before + test_check_all_acs_types_can_be_created + """ + + pytestmark = pytest.mark.usefixtures('acs_setup') + + def gen_params(): + """ + This function generates parameters that are used in test_check_all_acs_types_can_be_created. + """ + + parameters_dict = { + '_common': { + 'use_http_proxies': True, + }, + 'custom': { + '_common': { + 'custom_type': True, + 'base_url': 'https://test.com/', + 'subpaths': ['test/'], + 'capsules_to_add': 'class_target_sat.hostname', + }, + 'yum_manual_auth': { + 'content_type': 'yum', + 'name': 'customYumManualAuth', + 'description': 'customYumManualAuthDesc', + 'manual_auth': True, + 'verify_ssl': True, + 'ca_cert': ssl_name, + 'username': 'test', + 'password': 'test', + }, + 'yum_content_auth': { + 'content_type': 'yum', + 'name': 'customYumContentAuth', + 'description': 'customYumContentAuthDesc', + 'content_credentials_auth': True, + 'ssl_client_cert': ssl_name, + 'ssl_client_key': ssl_name, + 'verify_ssl': True, + 'ca_cert': ssl_name, + }, + 'yum_none_auth': { + 'content_type': 'yum', + 'name': 'customYumNoneAuth', + 'description': 'customYumNoneAuthDesc', + 'none_auth': True, + }, + 'file_manual_auth': { + 'content_type': 'file', + 'name': 'customFileManualAuth', + 'description': 'customFileManualAuthDesc', + 'manual_auth': True, + 'verify_ssl': True, + 'ca_cert': ssl_name, + 'username': 'test', + 'password': 'test', + }, + 'file_content_auth': { + 'content_type': 'file', + 'name': 'customFileContentAuth', + 'description': 'customFileContentAuthDesc', + 'content_credentials_auth': True, + 'ssl_client_cert': ssl_name, + 'ssl_client_key': ssl_name, + 'verify_ssl': True, + 'ca_cert': ssl_name, + }, + 'file_none_auth': { + 'content_type': 'file', + 'name': 'customFileNoneAuth', + 'description': 'customFileNoneAuthDesc', + 'none_auth': True, + }, + }, + 'simplified': { + '_common': {'simplified_type': True}, + 'yum': { + 'content_type': 'yum', + 'name': 'simpleYum', + 'description': 'simpleYumDesc', + 'capsules_to_add': 'class_target_sat.hostname', + 'products_to_add': [ + constants.REPOS[repo]['product'] for repo in repos_to_enable + ], + }, + 'file': { + 'content_type': 'file', + 'name': 'simpleFile', + 'description': 'simpleFileDesc', + 'add_all_capsules': True, + 'products_to_add': product_name, + }, + }, + 'rhui': { + '_common': { + 'rhui_type': True, + 'base_url': 'https://test.com/pulp/content', + 'subpaths': ['test/', 'test2/'], + 'verify_ssl': True, + 'ca_cert': ssl_name, + 'capsules_to_add': 'class_target_sat.hostname', + }, + 'yum_none_auth': { + 'name': 'rhuiYumNoneAuth', + 'description': 'rhuiYumNoneAuthDesc', + 'none_auth': True, + }, + 'yum_content_auth': { + 'name': 'rhuiYumContentAuth', + 'description': 'rhuiYumContentAuthDesc', + 'content_credentials_auth': True, + 'ssl_client_cert': ssl_name, + 'ssl_client_key': ssl_name, + }, + }, + } + + ids = [] + vals = [] + # This code creates a list of scenario IDs and values for each scenario. + # It loops through the keys in the parameters dictionary, and uses the keys to create a scenario ID + # and then it uses the scenario ID to access the scenario values from the parameters dictionary. + # The code then adds the scenario values to the list of scenario values. + for acs in parameters_dict.keys(): + if not acs.startswith('_'): + for cnt in parameters_dict[acs]: + if not cnt.startswith('_'): + scenario = ( + parameters_dict[acs][cnt] + | parameters_dict.get('_common', {}) + | parameters_dict[acs].get('_common', {}) + ) + ids.append(f'{acs}_{cnt}') + vals.append(scenario) + return (vals, ids) + + @pytest.mark.parametrize('scenario', gen_params()[0], ids=gen_params()[1]) + def test_check_all_acs_types_can_be_created(session, scenario, acs_setup): + """ + This test creates all possible ACS types. + + :id: 6bfad272-3ff8-4780-b346-1229d70524b1 + + :parametrized: yes + + :steps: + 1. Select an organization + 2. Create ACSes + :expectedresults: + This test should create all Aleternate Content Sources + """ + + class_target_sat, class_sca_manifest_org = acs_setup + vals = scenario + + # Replace the placeholder in 'capsules_to_add' with the hostname of the Satellite under test + for val in vals: + if 'capsules_to_add' in val: + vals['capsules_to_add'] = class_target_sat.hostname + + with class_target_sat.ui_session() as session: + session.organization.select(org_name=class_sca_manifest_org.name) + session.acs.create_new_acs(**vals) + + +class TestAcsE2e: + """ + Test class insuring fixture is ran once before + test_acs_positive_end_to_end + """ + + pytestmark = pytest.mark.usefixtures('acs_setup') + + @pytest.mark.e2e + def test_acs_positive_end_to_end(self, session, acs_setup): + """ + Create, update, delete and refresh ACSes. + + :id: 047452cc-5a9f-4473-96b1-d5b6830b7d6b + + :steps: + 1. Select an organization + 2. Create ACSes (randomly selected ones) + 3. Test refresh + 4. Test renaming and changing description + 5. Test editing capsules + 6. Test editing urls and subpaths + 7. Test editing credentials + 8. Test editing products + 9. Create ACS on which deletion is going to be tested + 10. Test deletion + :expectedresults: + This test should create some + Aleternate Content Sources and asserts that actions + were made correctly on them. + """ + + class_target_sat, class_sca_manifest_org = acs_setup + + with class_target_sat.ui_session() as session: + session.organization.select(org_name=class_sca_manifest_org.name) + + # Create ACS using "Simplified" option with content type of "File" + session.acs.create_new_acs( + simplified_type=True, + content_type='file', + name='simpleFileTest', + description='simpleFileTestDesc', + add_all_capsules=True, + use_http_proxies=True, + products_to_add=product_name, + ) + + # Create ACS using "Simplified" option with content type of "Yum" + session.acs.create_new_acs( + simplified_type=True, + content_type='yum', + name='simpleYumTest', + description='simpleYumTestDesc', + capsules_to_add=class_target_sat.hostname, + use_http_proxies=True, + products_to_add=[constants.REPOS[repo]['product'] for repo in repos_to_enable], + ) + + # Create ACS using "Custom" option with content type of "File" + # and using manual authentication + session.acs.create_new_acs( + custom_type=True, + content_type='file', + name='customFileManualTestAuth', + description='customFileManualTestAuthDesc', + capsules_to_add=class_target_sat.hostname, + use_http_proxies=True, + base_url='https://test.com', + subpaths=['test/'], + manual_auth=True, + username='test', + password='test', + verify_ssl=True, + ca_cert=ssl_name, + ) + + # Create ACS using "Custom" option with content type of "File" + # and using content credentials authentication + session.acs.create_new_acs( + custom_type=True, + content_type='file', + name='customFileContentAuthTest', + description='customFileContentAuthTestDesc', + capsules_to_add=class_target_sat.hostname, + use_http_proxies=True, + base_url='https://test.com', + subpaths=['test/'], + content_credentials_auth=True, + ssl_client_cert=ssl_name, + ssl_client_key=ssl_name, + verify_ssl=True, + ca_cert=ssl_name, + ) + + # Create ACS using "Custom" option with content type of "Yum" + # and using NO authentication + session.acs.create_new_acs( + custom_type=True, + content_type='yum', + name='customYumNoneAuthTest', + description='customYumNoneAuthTestDesc', + capsules_to_add=class_target_sat.hostname, + use_http_proxies=True, + base_url='https://test.com', + subpaths=['test/'], + none_auth=True, + ) + + # Refresh ACS and check that last refresh time is updated + session.acs.refresh_acs(acs_name='simpleFileTest') + simple_file_refreshed = session.acs.get_row_drawer_content(acs_name='simpleFileTest') + assert simple_file_refreshed['details']['last_refresh'] in [ + 'less than a minute ago', + '1 minute ago', + ] + + # Rename and change description of ACS and then check that it was changed + simple_file_renamed = session.acs.edit_acs_details( + acs_name_to_edit='simpleFileTest', + new_acs_name='simpleFileTestRenamed', + new_description='simpleFileTestRenamedDesc', + ) + simple_file_renamed = session.acs.get_row_drawer_content( + acs_name='simpleFileTestRenamed' + ) + assert ( + simple_file_renamed['details']['details_stack_content']['name'] + == 'simpleFileTestRenamed' + ) + assert ( + simple_file_renamed['details']['details_stack_content']['description'] + == 'simpleFileTestRenamedDesc' + ) + + # Edit ACS capsules + custom_file_edited_capsules = session.acs.edit_capsules( + acs_name_to_edit='customFileContentAuthTest', + remove_all=True, + use_http_proxies=False, + ) + custom_file_edited_capsules = session.acs.get_row_drawer_content( + acs_name='customFileContentAuthTest' + ) + assert ( + custom_file_edited_capsules['capsules']['capsules_stack_content']['capsules_list'] + == [] + ) + assert ( + custom_file_edited_capsules['capsules']['capsules_stack_content'][ + 'use_http_proxies' + ] + == 'false' + ) + + # Edit ACS urls and subpaths + custom_yum_edited_url = session.acs.edit_url_subpaths( + acs_name_to_edit='customYumNoneAuthTest', + new_url='https://testNEW.com', + new_subpaths=['test/', 'testNEW/'], + ) + custom_yum_edited_url = session.acs.get_row_drawer_content( + acs_name='customYumNoneAuthTest' + ) + assert ( + custom_yum_edited_url['url_and_subpaths']['url_and_subpaths_stack_content']['url'] + == 'https://testNEW.com' + ) + assert ( + custom_yum_edited_url['url_and_subpaths']['url_and_subpaths_stack_content'][ + 'subpaths' + ] + == 'test/,testNEW/' + ) + + # Edit ACS credentials + custom_file_edited_credentials = session.acs.edit_credentials( + acs_name_to_edit='customFileManualTestAuth', + verify_ssl=False, + manual_auth=True, + username='changedUserName', + ) + custom_file_edited_credentials = session.acs.get_row_drawer_content( + acs_name='customFileManualTestAuth' + ) + assert ( + custom_file_edited_credentials['credentials']['credentials_stack_content'][ + 'verify_ssl' + ] + == 'false' + ) + assert ( + custom_file_edited_credentials['credentials']['credentials_stack_content'][ + 'username' + ] + == 'changedUserName' + ) + + # Edit ACS products + simple_yum_edited_products = session.acs.edit_products( + acs_name_to_edit='simpleYumTest', + remove_all=True, + ) + simple_yum_edited_products = session.acs.get_row_drawer_content( + acs_name='simpleYumTest' + ) + assert ( + simple_yum_edited_products['products']['products_stack_content']['products_list'] + == [] + ) + + # Create ACS on which deletion is going to be tested + session.acs.create_new_acs( + rhui_type=True, + name='testAcsToBeDeleted', + description='testAcsToBeDeleted', + capsules_to_add=class_target_sat.hostname, + use_http_proxies=True, + base_url='https://test.com/pulp/content', + subpaths=['test/', 'test2/'], + none_auth=True, + verify_ssl=True, + ca_cert=ssl_name, + ) + + # Delete ACS and check if trying to read it afterwards fails + session.acs.delete_acs(acs_name='testAcsToBeDeleted') + with pytest.raises(ValueError): + session.acs.get_row_drawer_content(acs_name='testAcsToBeDeleted') From 5ac8f0e76c8ffde5032c3456b24e8997349a100c Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 25 Oct 2023 00:09:38 -0400 Subject: [PATCH 295/586] [6.14.z] Bump cryptography from 41.0.4 to 41.0.5 (#12967) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 98e78baf601..228bfd8a163 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ betelgeuse==1.10.0 broker[docker]==0.4.1 -cryptography==41.0.4 +cryptography==41.0.5 deepdiff==6.6.1 dynaconf[vault]==3.2.3 fauxfactory==3.1.0 From 197d6454495ba9fa527192d56be3b46317bd0b68 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 25 Oct 2023 03:47:23 -0400 Subject: [PATCH 296/586] [6.14.z] Bump pytest from 7.4.2 to 7.4.3 (#12970) Bump pytest from 7.4.2 to 7.4.3 (#12966) (cherry picked from commit 8c8780895b5e86b991d3d441c0473226956a8ad7) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 228bfd8a163..57f6e2756ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ navmazing==1.1.6 productmd==1.37 pyotp==2.9.0 python-box==7.1.1 -pytest==7.4.2 +pytest==7.4.3 pytest-services==2.2.1 pytest-mock==3.12.0 pytest-reportportal==5.3.0 From 3c56df3b84fd512103f8acc7eae736ccfa8ca4d5 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:31:30 -0400 Subject: [PATCH 297/586] [6.14.z] Fix vault makescripts with capture output (#12977) Fix vault makescripts with capture output (#12909) * Fix vault makescripts with capture output * Handle topped Vault enablement in .env file (cherry picked from commit 291698f925eb7016ff7d11764b218e2c7cf995d4) Co-authored-by: Jitendra Yejare --- pytest_plugins/auto_vault.py | 3 +-- robottelo/utils/vault.py | 17 ++++++++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/pytest_plugins/auto_vault.py b/pytest_plugins/auto_vault.py index e63fc7f0835..cb9e1f0c10a 100644 --- a/pytest_plugins/auto_vault.py +++ b/pytest_plugins/auto_vault.py @@ -1,5 +1,4 @@ """Plugin enables pytest to notify and update the requirements""" -import subprocess from robottelo.utils.vault import Vault @@ -7,4 +6,4 @@ def pytest_addoption(parser): """Options to allow user to update the requirements""" with Vault() as vclient: - vclient.login(stdout=subprocess.PIPE, stderr=subprocess.PIPE) + vclient.login() diff --git a/robottelo/utils/vault.py b/robottelo/utils/vault.py index b0fc77d861e..d3a40d1a706 100644 --- a/robottelo/utils/vault.py +++ b/robottelo/utils/vault.py @@ -25,7 +25,7 @@ def __init__(self, env_file='.env'): def setup(self): if self.env_path.exists(): self.envdata = self.env_path.read_text() - is_enabled = re.findall('\nVAULT_ENABLED_FOR_DYNACONF=(.*)', self.envdata) + is_enabled = re.findall('^(?:.*\n)*VAULT_ENABLED_FOR_DYNACONF=(.*)', self.envdata) if is_enabled: self.vault_enabled = is_enabled[0] self.export_vault_addr() @@ -53,7 +53,7 @@ def exec_vault_command(self, command: str, **kwargs): :param comamnd str: The vault CLI command :param kwargs dict: Arguments to the subprocess run command to customize the run behavior """ - vcommand = subprocess.run(command, shell=True, **kwargs) # capture_output=True + vcommand = subprocess.run(command, shell=True, capture_output=True, **kwargs) if vcommand.returncode != 0: verror = str(vcommand.stderr) if vcommand.returncode == 127: @@ -63,7 +63,7 @@ def exec_vault_command(self, command: str, **kwargs): if 'Error revoking token' in verror: logger.info("Token is alredy revoked!") elif 'Error looking up token' in verror: - logger.warning("Warning! Vault not logged in!") + logger.info("Vault is not logged in!") else: logger.error(f"Error! {verror}") return vcommand @@ -75,7 +75,7 @@ def login(self, **kwargs): and 'VAULT_SECRET_ID_FOR_DYNACONF' not in os.environ ): if self.status(**kwargs).returncode != 0: - logger.warning( + logger.info( "Warning! The browser is about to open for vault OIDC login, " "close the tab once the sign-in is done!" ) @@ -86,9 +86,7 @@ def login(self, **kwargs): self.exec_vault_command(command="vault token renew -i 10h", **kwargs) logger.info("Success! Vault OIDC Logged-In and extended for 10 hours!") # Fetching tokens - token = self.exec_vault_command( - "vault token lookup --format json", capture_output=True - ).stdout + token = self.exec_vault_command("vault token lookup --format json").stdout token = json.loads(str(token.decode('UTF-8')))['data']['id'] # Setting new token in env file _envdata = re.sub( @@ -107,8 +105,9 @@ def logout(self): '.*VAULT_TOKEN_FOR_DYNACONF=.*', "# VAULT_TOKEN_FOR_DYNACONF=myroot", self.envdata ) self.env_path.write_text(_envdata) - self.exec_vault_command('vault token revoke -self') - logger.info("Success! OIDC token removed from Env file successfully!") + vstatus = self.exec_vault_command('vault token revoke -self') + if vstatus.returncode == 0: + logger.info("Success! OIDC token removed from Env file successfully!") def status(self, **kwargs): vstatus = self.exec_vault_command('vault token lookup', **kwargs) From 42ee184fa07ad40ce5b21c44d013f7659b295376 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 26 Oct 2023 05:19:14 -0400 Subject: [PATCH 298/586] [6.14.z] Fixture collection splitting restructured (#12981) Fixture collection splitting restructured (#12923) Fixture collection spliting restructured (cherry picked from commit 33f4b323fdbf26a83af37ff1aac635f287b082b2) Co-authored-by: Jitendra Yejare --- pytest_plugins/fixture_collection.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pytest_plugins/fixture_collection.py b/pytest_plugins/fixture_collection.py index 934efa5f56d..6f61f2b3360 100644 --- a/pytest_plugins/fixture_collection.py +++ b/pytest_plugins/fixture_collection.py @@ -13,7 +13,7 @@ def pytest_addoption(parser): example: pytest tests/foreman --uses-fixtures target_sat module_target_sat ''' - parser.addoption("--uses-fixtures", nargs='+', help=help_text) + parser.addoption("--uses-fixtures", nargs='?', help=help_text) def pytest_collection_modifyitems(items, config): @@ -22,17 +22,18 @@ def pytest_collection_modifyitems(items, config): return filter_fixtures = config.getvalue('uses_fixtures') + fixtures_list = filter_fixtures.split(',') if ',' in filter_fixtures else [filter_fixtures] selected = [] deselected = [] for item in items: - if set(item.fixturenames).intersection(set(filter_fixtures)): + if set(item.fixturenames).intersection(set(fixtures_list)): selected.append(item) else: deselected.append(item) logger.debug( f'Selected {len(selected)} and deselected {len(deselected)} ' - f'tests based on given fixtures {filter_fixtures} used by tests' + f'tests based on given fixtures {fixtures_list} used by tests' ) config.hook.pytest_deselected(items=deselected) items[:] = selected From 97f52af0d389dde77dce95fd7fed0f4769c8687d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 26 Oct 2023 05:26:30 -0400 Subject: [PATCH 299/586] [6.14.z] [Upgrades] Add upgrade tests for ProvisioningTemplates (#12954) [Upgrades] Add upgrade tests for ProvisioningTemplates (#12897) Add upgrade tests for ProvisioningTemplates Signed-off-by: Gaurav Talreja (cherry picked from commit f9cec5efe795129617cfe6ae2f1ff0a2343e01e0) Co-authored-by: Gaurav Talreja --- pytest_fixtures/component/domain.py | 10 +- tests/upgrades/test_provisioningtemplate.py | 141 ++++++++++++++++++++ 2 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 tests/upgrades/test_provisioningtemplate.py diff --git a/pytest_fixtures/component/domain.py b/pytest_fixtures/component/domain.py index c62b45f54d5..57a1c265b30 100644 --- a/pytest_fixtures/component/domain.py +++ b/pytest_fixtures/component/domain.py @@ -1,15 +1,15 @@ # Domain Fixtures -from nailgun import entities import pytest @pytest.fixture(scope='session') def default_domain(session_target_sat, default_smart_proxy): domain_name = session_target_sat.hostname.partition('.')[-1] - dom = entities.Domain().search(query={'search': f'name={domain_name}'})[0] - dom.dns = default_smart_proxy - dom.update(['dns']) - return entities.Domain(id=dom.id).read() + dom = session_target_sat.api.Domain().search(query={'search': f'name={domain_name}'})[0] + if 'dns' in session_target_sat.get_features(): + dom.dns = default_smart_proxy + dom.update(['dns']) + return session_target_sat.api.Domain(id=dom.id).read() @pytest.fixture(scope='module') diff --git a/tests/upgrades/test_provisioningtemplate.py b/tests/upgrades/test_provisioningtemplate.py new file mode 100644 index 00000000000..b5bc11af363 --- /dev/null +++ b/tests/upgrades/test_provisioningtemplate.py @@ -0,0 +1,141 @@ +"""Test for ProvisioningTemplates related Upgrade Scenario's + +:Requirement: UpgradedSatellite + +:CaseAutomation: Automated + +:CaseComponent: ProvisioningTemplates + +:Team: Rocket + +:TestType: Functional + +:CaseLevel: Integration + +:CaseImportance: High + +:Upstream: No +""" +from fauxfactory import gen_string +import pytest + +from robottelo.config import settings + +provisioning_template_kinds = ['provision', 'PXEGrub', 'PXEGrub2', 'PXELinux', 'iPXE'] + + +class TestScenarioPositiveProvisioningTemplates: + """Provisioning Templates can be rendered correctly on host created in previous and upgraded versions + + :steps: + 1. Create host on Satellite and trying rendering provisioning templates + 2. Upgrade the Satellite to the next or latest version. + 3. After the upgrade, verify provisioning templates can be rendered on existing host + 4. Create host on upgraded Satellite and trying rendering provisioning templates. + + :expectedresults: + 1. Provisioning templates for host are able to render in previous and upgraded versions + """ + + @pytest.mark.pre_upgrade + @pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) + def test_pre_scenario_provisioning_templates( + self, + module_target_sat, + module_org, + module_location, + default_os, + default_domain, + default_architecture, + default_partitiontable, + pxe_loader, + save_test_data, + ): + """Verify Host created Read the Provision template + + :id: preupgrade-3f338475-fa69-43ef-ac86-f00f4d324b21 + + :steps: + 1. Create host on Satellite and trying rendering provisioning templates + + :expectedresults: + 1. Provisioning templates for host are able to render in before upgrading to new version + + :parametrized: yes + """ + host = module_target_sat.api.Host( + organization=module_org, + location=module_location, + name=gen_string('alpha'), + operatingsystem=default_os, + architecture=default_architecture, + domain=default_domain, + root_pass=settings.provisioning.host_root_password, + ptable=default_partitiontable, + pxe_loader=pxe_loader.pxe_loader, + ).create() + + for kind in provisioning_template_kinds: + assert host.read_template(data={'template_kind': kind}) + + save_test_data( + { + 'provision_host_id': host.id, + 'pxe_loader': pxe_loader.pxe_loader, + } + ) + + @pytest.mark.post_upgrade(depend_on=test_pre_scenario_provisioning_templates) + @pytest.mark.parametrize('pre_upgrade_data', ['bios', 'uefi'], indirect=True) + def test_post_scenario_provisioning_templates( + self, + request, + pre_upgrade_data, + module_target_sat, + ): + """Host provisioned using pre-upgrade GCE CR + + :id: postupgrade-ef82143d-efef-49b2-9702-93d67ef6805e + + :steps: + 1. Postupgrade, verify provisioning templates rendering for host + 2. Create a new host on Satellite and try rendering provisioning templates + + :expectedresults: + 1. Provisioning templates for existing and new host are able to render. + + :parametrized: yes + """ + pxe_loader = pre_upgrade_data.pxe_loader + pre_upgrade_host = module_target_sat.api.Host().search( + query={'search': f'id={pre_upgrade_data.provision_host_id}'} + )[0] + org = module_target_sat.api.Organization(id=pre_upgrade_host.organization.id).read() + loc = module_target_sat.api.Location(id=pre_upgrade_host.location.id).read() + domain = module_target_sat.api.Domain(id=pre_upgrade_host.domain.id).read() + architecture = module_target_sat.api.Architecture( + id=pre_upgrade_host.architecture.id + ).read() + os = module_target_sat.api.OperatingSystem(id=pre_upgrade_host.operatingsystem.id).read() + ptable = module_target_sat.api.PartitionTable(id=pre_upgrade_host.ptable.id).read() + + for kind in provisioning_template_kinds: + assert pre_upgrade_host.read_template(data={'template_kind': kind}) + + new_host_name = gen_string('alpha') + new_host = module_target_sat.api.Host( + name=new_host_name, + organization=org, + location=loc, + architecture=architecture, + domain=domain, + operatingsystem=os, + ptable=ptable, + root_pass=settings.provisioning.host_root_password, + pxe_loader=pxe_loader, + ).create() + request.addfinalizer(pre_upgrade_host.delete) + request.addfinalizer(new_host.delete) + + for kind in provisioning_template_kinds: + assert new_host.read_template(data={'template_kind': kind}) From 09bb01cc3d6fee9f8f79bb3ed595b3f64c6a3ddd Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 26 Oct 2023 06:08:28 -0400 Subject: [PATCH 300/586] [6.14.z] Add workaround for cpu_mode for EL9 Libvirt tests (#12985) --- tests/foreman/cli/test_computeresource_libvirt.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/foreman/cli/test_computeresource_libvirt.py b/tests/foreman/cli/test_computeresource_libvirt.py index e5e1997fa3b..c1144c88733 100644 --- a/tests/foreman/cli/test_computeresource_libvirt.py +++ b/tests/foreman/cli/test_computeresource_libvirt.py @@ -45,6 +45,7 @@ from robottelo.config import settings from robottelo.constants import FOREMAN_PROVIDERS, LIBVIRT_RESOURCE_URL from robottelo.utils.datafactory import parametrized +from robottelo.utils.issue_handlers import is_open LIBVIRT_URL = LIBVIRT_RESOURCE_URL % settings.libvirt.libvirt_hostname @@ -436,6 +437,7 @@ def test_positive_provision_end_to_end( module_sca_manifest_org, module_location, provisioning_hostgroup, + module_provisioning_rhel_content, ): """Provision a host on Libvirt compute resource with the help of hostgroup. @@ -453,10 +455,14 @@ def test_positive_provision_end_to_end( :expectedresults: Host should be provisioned with hostgroup :parametrized: yes + + :BZ: 2236693 """ sat = module_libvirt_provisioning_sat.sat cr_name = gen_string('alpha') hostname = gen_string('alpha').lower() + os_major_ver = module_provisioning_rhel_content.os.major + cpu_mode = 'host-passthrough' if is_open('BZ:2236693') and os_major_ver == '9' else 'default' libvirt_cr = sat.cli.ComputeResource.create( { 'name': cr_name, @@ -476,7 +482,7 @@ def test_positive_provision_end_to_end( 'compute-resource-id': libvirt_cr['id'], 'ip': None, 'mac': None, - 'compute-attributes': 'cpus=1, memory=6442450944, cpu_mode=default, start=1', + 'compute-attributes': f'cpus=1, memory=6442450944, cpu_mode={cpu_mode}, start=1', 'interface': f'compute_type=bridge,compute_bridge=br-{settings.provisioning.vlan_id}', 'volume': 'capacity=10', 'provision-method': 'build', From a31ebb969d26a7f186b70b55e6a71d19bf419041 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 26 Oct 2023 07:11:09 -0400 Subject: [PATCH 301/586] [6.14.z] Remove test_positive_backup_restore_snapshot stub (#12984) Remove test_positive_backup_restore_snapshot stub (#12974) (cherry picked from commit 8b0eca51c1384eb75b514fedd15bdc68bf3dabe5) Co-authored-by: Lukas Pramuk --- tests/foreman/maintain/test_backup_restore.py | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/tests/foreman/maintain/test_backup_restore.py b/tests/foreman/maintain/test_backup_restore.py index 473f768fa74..6f6817730b1 100644 --- a/tests/foreman/maintain/test_backup_restore.py +++ b/tests/foreman/maintain/test_backup_restore.py @@ -675,30 +675,3 @@ def test_positive_backup_restore_incremental( query={'search': f'name="{secondary_repo.name}"'} )[0] assert repo.id == secondary_repo.id - - -@pytest.mark.stubbed -def test_positive_backup_restore_snapshot(): - """Take the snapshot backup of a server, restore it, check for content - - :id: dcf3b815-97ed-4c2e-9f2d-5eedd8591c98 - - :setup: - 1. satellite installed on an LVM-based storage with sufficient free extents - - :steps: - 1. create the snapshot backup (with/without pulp) - 2. check that appropriate files are created - 3. restore the backup (installer --reset-data is run in this step) - 4. check system health - 5. check the content was restored - - :expectedresults: - 1. backup succeeds - 2. expected files are present in the backup - 3. restore succeeds - 4. system health check succeeds - 5. content is present after restore - - :CaseAutomation: NotAutomated - """ From a7491f17f40490e7f917e13dcf8e81a7cbe0c317 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 27 Oct 2023 01:42:15 -0400 Subject: [PATCH 302/586] [6.14.z] Bump wrapanapi from 3.5.18 to 3.6.0 (#12995) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 57f6e2756ff..722de1b3cc2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ requests==2.31.0 tenacity==8.2.3 testimony==2.3.0 wait-for==1.2.0 -wrapanapi==3.5.18 +wrapanapi==3.6.0 # Get airgun, nailgun and upgrade from master git+https://github.com/SatelliteQE/airgun.git@6.14.z#egg=airgun From abe5645c15a7d9c9a1471bf0aba1cdd64ce5480d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Sat, 28 Oct 2023 05:52:32 -0400 Subject: [PATCH 303/586] [6.14.z] Add workaround for cpu_mode for EL9 Libvirt UI tests (#12999) --- tests/foreman/ui/test_computeresource_libvirt.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/foreman/ui/test_computeresource_libvirt.py b/tests/foreman/ui/test_computeresource_libvirt.py index 1cb250d2346..2d05fa45bf3 100644 --- a/tests/foreman/ui/test_computeresource_libvirt.py +++ b/tests/foreman/ui/test_computeresource_libvirt.py @@ -28,6 +28,7 @@ FOREMAN_PROVIDERS, LIBVIRT_RESOURCE_URL, ) +from robottelo.utils.issue_handlers import is_open pytestmark = [pytest.mark.skip_if_not_set('libvirt')] @@ -135,6 +136,7 @@ def test_positive_provision_end_to_end( module_location, provisioning_hostgroup, module_libvirt_provisioning_sat, + module_provisioning_rhel_content, ): """Provision Host on libvirt compute resource, and delete it afterwards @@ -146,12 +148,14 @@ def test_positive_provision_end_to_end( :customerscenario: true - :BZ: 1243223 + :BZ: 1243223, 2236693 :parametrized: yes """ sat = module_libvirt_provisioning_sat.sat hostname = gen_string('alpha').lower() + os_major_ver = module_provisioning_rhel_content.os.major + cpu_mode = 'host-passthrough' if is_open('BZ:2236693') and os_major_ver == '9' else 'default' cr = sat.api.LibvirtComputeResource( provider=FOREMAN_PROVIDERS['libvirt'], url=LIBVIRT_URL, @@ -169,6 +173,7 @@ def test_positive_provision_end_to_end( 'host.inherit_deploy_option': False, 'host.deploy': f'{cr.name} (Libvirt)', 'provider_content.virtual_machine.memory': '6144', + 'provider_content.virtual_machine.cpu_mode': cpu_mode, 'interfaces.interface.network_type': 'Physical (Bridge)', 'interfaces.interface.network': f'br-{settings.provisioning.vlan_id}', 'additional_information.comment': 'Libvirt provision using valid data', From 9f7a4b8a6441c489bf82a6ac8bbfa05d16877309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Strelec?= Date: Fri, 27 Oct 2023 18:41:46 +0200 Subject: [PATCH 304/586] Update `test_positive_all_packages_update` (#12342) * add test_positive_fm_packages_check_update * add capsule marker * add regex to find packages to update * remove duplicate test * update regex to match if there's multiple packages (cherry picked from commit b2d4f9832ac250848a17959f47c2736a648b15e5) --- tests/foreman/destructive/test_packages.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/foreman/destructive/test_packages.py b/tests/foreman/destructive/test_packages.py index 3a31e354e46..05c44bf1501 100644 --- a/tests/foreman/destructive/test_packages.py +++ b/tests/foreman/destructive/test_packages.py @@ -16,11 +16,14 @@ :Upstream: No """ +import re + import pytest pytestmark = pytest.mark.destructive +@pytest.mark.include_capsule def test_positive_all_packages_update(target_sat): """Verify update and check-update work as expected. @@ -48,5 +51,10 @@ def test_positive_all_packages_update(target_sat): target_sat.power_control(state='reboot') # Run check-update again to verify there are no more packages available to update result = target_sat.cli.Packages.check_update() + # Regex to match if there are packages available to update + # Matches lines like '\n\nwalrus.noarch 5.21-1 custom_repo\n' + pattern = '(\\n){1,2}(\\S+)(\\s+)(\\S+)(\\s+)(\\S+)(\\n)' + matches = re.search(pattern, result.stdout) + assert matches is None # No packages available to update assert 'FAIL' not in result.stdout assert result.status == 0 From 27d218f71a8a8ec83322302b228337764b8f359d Mon Sep 17 00:00:00 2001 From: vsedmik <46570670+vsedmik@users.noreply.github.com> Date: Tue, 31 Oct 2023 10:50:14 +0100 Subject: [PATCH 305/586] [6.14.z] Add test to ensure katello-agent availability in Client repos (#12988) * Fix Client repos in constants * Add test to ensure katello-agent availability Katello-agent was deprecated and removed from 6.15 and upstream repos, but we still need to have it available in Client repos for older Satellite versions which support it. This test just verifies that. --- robottelo/constants/__init__.py | 10 ++++---- tests/foreman/api/test_repository.py | 36 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index 6c07eb49919..05b675454fa 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -289,9 +289,9 @@ class Colored(Box): 'rhsc7': 'Red Hat Satellite Capsule 6.11 (for RHEL 7 Server) (RPMs)', 'rhsc8': 'Red Hat Satellite Capsule 6.13 for RHEL 8 x86_64 (RPMs)', 'rhsc7_iso': 'Red Hat Satellite Capsule 6.4 (for RHEL 7 Server) (ISOs)', - 'rhsclient7': 'Red Hat Satellite Client 6 for RHEL 7 Server RPMs x86_64', - 'rhsclient8': 'Red Hat Satellite Client 6 for RHEL 8 x86_64 RPMs', - 'rhsclient9': 'Red Hat Satellite Client 6 for RHEL 9 x86_64 RPMs', + 'rhsclient7': 'Red Hat Satellite Client 6 (for RHEL 7 Server) (RPMs)', + 'rhsclient8': 'Red Hat Satellite Client 6 for RHEL 8 x86_64 (RPMs)', + 'rhsclient9': 'Red Hat Satellite Client 6 for RHEL 9 x86_64 (RPMs)', 'rhst7': 'Red Hat Satellite Tools 6.9 (for RHEL 7 Server) (RPMs)', 'rhst7_610': 'Red Hat Satellite Tools 6.10 (for RHEL 7 Server) (RPMs)', 'rhst6': 'Red Hat Satellite Tools 6.9 (for RHEL 6 Server) (RPMs)', @@ -408,7 +408,7 @@ class Colored(Box): 'name': ('Red Hat Satellite Client 6 for RHEL 8 x86_64 RPMs'), 'version': '6', 'reposet': REPOSET['rhsclient8'], - 'product': PRDS['rhel'], + 'product': PRDS['rhel8'], 'distro': 'rhel8', 'key': PRODUCT_KEY_SAT_CLIENT, }, @@ -417,7 +417,7 @@ class Colored(Box): 'name': ('Red Hat Satellite Client 6 for RHEL 9 x86_64 RPMs'), 'version': '6', 'reposet': REPOSET['rhsclient9'], - 'product': PRDS['rhel'], + 'product': PRDS['rhel9'], 'distro': 'rhel9', 'key': PRODUCT_KEY_SAT_CLIENT, }, diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index e6f03383dc2..bfe85d460a4 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -1295,6 +1295,42 @@ def test_positive_sync_with_treeinfo_ignore( f'{repo.full_path}.treeinfo' ), 'The treeinfo file is missing in the KS repo but it should be there.' + @pytest.mark.tier3 + @pytest.mark.parametrize('client_repo', ['rhsclient7', 'rhsclient8', 'rhsclient9']) + def test_positive_katello_agent_availability( + self, target_sat, function_sca_manifest_org, client_repo + ): + """Verify katello-agent package remains available in the RH Satellite Client repos + for older Satellite versions (Sat 6.14 is the last one supporting it). + + :id: cd5b4e5b-e4f1-4d00-b171-30cd5b1e7ce8 + + :parametrized: yes + + :steps: + 1. Enable RH Satellite Client repo and sync it. + 2. Read the repo packages. + + :expectedresults: + 1. Katello-agent remains available in the Client repos. + + """ + # Enable RH Satellite Client repo and sync it. + repo_id = target_sat.api_factory.enable_rhrepo_and_fetchid( + basearch='x86_64', + org_id=function_sca_manifest_org.id, + product=constants.REPOS[client_repo]['product'], + reposet=constants.REPOS[client_repo]['reposet'], + repo=constants.REPOS[client_repo]['name'], + releasever=constants.REPOS[client_repo]['version'], + ) + repo = target_sat.api.Repository(id=repo_id).read() + repo.sync() + # Make sure katello-agent lives in the repo happily. + repo_pkgs = target_sat.api.Repository(id=repo_id).packages()['results'] + agent_pkgs = [pkg['filename'] for pkg in repo_pkgs if 'katello-agent' in pkg['name']] + assert len(agent_pkgs), 'Katello-agent package is missing from the RH Client repo!' + @pytest.mark.run_in_one_thread class TestRepositorySync: From 93155880afdca3bf2ee1980848455f9ac82e735e Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 31 Oct 2023 06:47:58 -0400 Subject: [PATCH 306/586] [6.14.z] sync RHOSP repos to capsule (#13007) sync RHOSP repos to capsule (#12990) (cherry picked from commit ad951328f74cb3a6af418a3292212a346c1b3348) Co-authored-by: vijay sawant --- tests/foreman/api/test_capsulecontent.py | 69 ++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/foreman/api/test_capsulecontent.py b/tests/foreman/api/test_capsulecontent.py index cdc388fd149..900d0803aed 100644 --- a/tests/foreman/api/test_capsulecontent.py +++ b/tests/foreman/api/test_capsulecontent.py @@ -1353,3 +1353,72 @@ def test_positive_remove_capsule_orphans( 'ls /var/lib/pulp/media/artifact/*/* | xargs file | grep RPM' ) assert result.status, 'RPM artifacts are still present. They should be gone.' + + @pytest.mark.skip_if_not_set('capsule') + def test_positive_capsule_sync_openstack_container_repos( + self, + module_target_sat, + module_capsule_configured, + function_org, + function_product, + function_lce, + ): + """Synchronize openstack container repositories to capsule + + :id: 23e64385-7f34-4ab9-bd63-72306e5a4de0 + + :setup: + 1. A blank external capsule that has not been synced yet. + + :steps: + 1. Enable and sync openstack container repos. + + :expectedresults: + 1. container repos should sync on capsule. + + :customerscenario: true + + :BZ: 2154734 + + """ + upstream_names = [ + 'rhosp13/openstack-cinder-api', + 'rhosp13/openstack-neutron-server', + 'rhosp13/openstack-neutron-dhcp-agent', + 'rhosp13/openstack-nova-api', + ] + repos = [] + + for ups_name in upstream_names: + repo = module_target_sat.api.Repository( + content_type='docker', + docker_upstream_name=ups_name, + product=function_product, + url=constants.RH_CONTAINER_REGISTRY_HUB, + upstream_username=settings.subscription.rhn_username, + upstream_password=settings.subscription.rhn_password, + ).create() + repo.sync(timeout=1800) + repos.append(repo) + + # Associate LCE with the capsule + module_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( + data={'environment_id': function_lce.id} + ) + result = module_capsule_configured.nailgun_capsule.content_lifecycle_environments() + assert len(result['results']) + assert function_lce.id in [capsule_lce['id'] for capsule_lce in result['results']] + + # Create and publish a content view with all repositories + cv = module_target_sat.api.ContentView(organization=function_org, repository=repos).create() + cv.publish() + cv = cv.read() + assert len(cv.version) == 1 + + # Promote the latest CV version into capsule's LCE + cvv = cv.version[-1].read() + cvv.promote(data={'environment_ids': function_lce.id}) + cvv = cvv.read() + assert len(cvv.environment) == 2 + + module_capsule_configured.wait_for_sync() From 6c2dbb8c5831416cd0395072a8a5c8ce5a58970c Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 31 Oct 2023 06:56:04 -0400 Subject: [PATCH 307/586] [6.14.z] Discovery coverage for rule priority/limit/provisioning (#13012) Discovery coverage for rule priority/limit/provisioning (#12962) * Discovery coverage for rule priority/limit/provisioning * Adding parametrization for broker workflow (cherry picked from commit 176f5abe3caac419370d5a66539e55b1c6bc4dd0) Co-authored-by: Adarsh dubey --- pytest_fixtures/component/provision_pxe.py | 27 +++++ tests/foreman/api/test_discoveredhost.py | 45 ++++++-- tests/foreman/api/test_discoveryrule.py | 122 ++++++++++----------- 3 files changed, 118 insertions(+), 76 deletions(-) diff --git a/pytest_fixtures/component/provision_pxe.py b/pytest_fixtures/component/provision_pxe.py index 1fc0e47397b..90e7c1798ff 100644 --- a/pytest_fixtures/component/provision_pxe.py +++ b/pytest_fixtures/component/provision_pxe.py @@ -237,6 +237,33 @@ def provisioning_host(module_ssh_key_file, pxe_loader): prov_host.blank = getattr(prov_host, 'blank', False) +@pytest.fixture +def provision_multiple_hosts(module_ssh_key_file, pxe_loader, request): + """Fixture to check out two blank VMs""" + vlan_id = settings.provisioning.vlan_id + cd_iso = ( + "" # TODO: Make this an optional fixture parameter (update vm_firmware when adding this) + ) + # Keeping the default value to 2 + count = request.param if request.param is not None else 2 + + with Broker( + workflow="deploy-configure-pxe-provisioning-host-rhv", + host_class=ContentHost, + _count=count, + target_vlan_id=vlan_id, + target_vm_firmware=pxe_loader.vm_firmware, + target_vm_cd_iso=cd_iso, + blank=True, + target_memory='6GiB', + auth=module_ssh_key_file, + ) as hosts: + yield hosts + + for prov_host in hosts: + prov_host.blank = getattr(prov_host, 'blank', False) + + @pytest.fixture def provisioning_hostgroup( module_provisioning_sat, diff --git a/tests/foreman/api/test_discoveredhost.py b/tests/foreman/api/test_discoveredhost.py index 0db7c486bcf..e61bf069b0a 100644 --- a/tests/foreman/api/test_discoveredhost.py +++ b/tests/foreman/api/test_discoveredhost.py @@ -339,7 +339,7 @@ def test_positive_auto_provision_all( @pytest.mark.stubbed @pytest.mark.tier3 - def test_positive_refresh_facts_pxe_host(self): + def test_positive_refresh_facts_pxe_host(self, module_target_sat): """Refresh the facts of pxe based discovered hosts by adding a new NIC :id: 413fb608-cd5c-441d-af86-fd2d40346d96 @@ -354,14 +354,12 @@ def test_positive_refresh_facts_pxe_host(self): :expectedresults: Added Fact should be displayed on refreshing the facts - :CaseAutomation: NotAutomated - :CaseImportance: High """ @pytest.mark.on_premises_provisioning @pytest.mark.parametrize('module_provisioning_sat', ['discovery'], indirect=True) - @pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) + @pytest.mark.parametrize('pxe_loader', ['uefi'], indirect=True) @pytest.mark.rhel_ver_match('9') @pytest.mark.tier3 def test_positive_reboot_pxe_host( @@ -394,6 +392,7 @@ def test_positive_reboot_pxe_host( timeout=240, delay=20, ) + discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] discovered_host.hostgroup = provisioning_hostgroup discovered_host.location = provisioning_hostgroup.location[0] @@ -402,25 +401,51 @@ def test_positive_reboot_pxe_host( result = sat.api.DiscoveredHost(id=discovered_host.id).reboot() assert 'Unable to perform reboot' not in result - @pytest.mark.stubbed + @pytest.mark.on_premises_provisioning + @pytest.mark.parametrize('module_provisioning_sat', ['discovery'], indirect=True) + @pytest.mark.parametrize('pxe_loader', ['bios'], indirect=True) + @pytest.mark.rhel_ver_match('9') + @pytest.mark.parametrize('provision_multiple_hosts', [2]) @pytest.mark.tier3 - def test_positive_reboot_all_pxe_hosts(self): + def test_positive_reboot_all_pxe_hosts( + self, + module_provisioning_rhel_content, + module_discovery_sat, + provision_multiple_hosts, + provisioning_hostgroup, + pxe_loader, + count, + ): """Rebooting all pxe-based discovered hosts :id: 69c807f8-5646-4aa6-8b3c-5ecdb69560ed :parametrized: yes - :Setup: Provisioning should be configured and a hosts should be discovered via PXE boot. + :Setup: Provisioning should be configured and hosts should be discovered via PXE boot. :Steps: PUT /api/v2/discovered_hosts/reboot_all - :expectedresults: All disdcovered host should be rebooted successfully - - :CaseAutomation: Automated + :expectedresults: All discovered hosst should be rebooted successfully :CaseImportance: Medium """ + sat = module_discovery_sat.sat + for host in provision_multiple_hosts: + host.power_control(ensure=False) + mac = host._broker_args['provisioning_nic_mac_addr'] + wait_for( + lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], + timeout=240, + delay=20, + ) + discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] + discovered_host.hostgroup = provisioning_hostgroup + discovered_host.location = provisioning_hostgroup.location[0] + discovered_host.organization = provisioning_hostgroup.organization[0] + discovered_host.build = True + result = sat.api.DiscoveredHost().reboot_all() + assert 'Discovered hosts are rebooting now' in result['message'] class TestFakeDiscoveryTests: diff --git a/tests/foreman/api/test_discoveryrule.py b/tests/foreman/api/test_discoveryrule.py index f55a287d675..51ac5fec162 100644 --- a/tests/foreman/api/test_discoveryrule.py +++ b/tests/foreman/api/test_discoveryrule.py @@ -16,36 +16,16 @@ :Upstream: No """ -from fauxfactory import gen_choice, gen_integer, gen_string -from nailgun import entities +from fauxfactory import gen_choice, gen_integer import pytest from requests.exceptions import HTTPError from robottelo.utils.datafactory import valid_data_list -@pytest.fixture(scope="module") -def module_hostgroup(module_org): - module_hostgroup = entities.HostGroup(organization=[module_org]).create() - yield module_hostgroup - module_hostgroup.delete() - - -@pytest.fixture(scope="module") -def module_location(module_location): - yield module_location - module_location.delete() - - -@pytest.fixture(scope="module") -def module_org(module_org): - yield module_org - module_org.delete() - - @pytest.mark.tier1 @pytest.mark.e2e -def test_positive_end_to_end_crud(module_org, module_location, module_hostgroup): +def test_positive_end_to_end_crud(module_org, module_location, module_hostgroup, target_sat): """Create a new discovery rule with several attributes, update them and delete the rule itself. @@ -67,7 +47,7 @@ def test_positive_end_to_end_crud(module_org, module_location, module_hostgroup) name = gen_choice(list(valid_data_list().values())) search = gen_choice(searches) hostname = 'myhost-<%= rand(99999) %>' - discovery_rule = entities.DiscoveryRule( + discovery_rule = target_sat.api.DiscoveryRule( name=name, search_=search, hostname=hostname, @@ -103,23 +83,10 @@ def test_positive_end_to_end_crud(module_org, module_location, module_hostgroup) discovery_rule.read() -@pytest.mark.tier1 -def test_negative_create_with_invalid_host_limit_and_priority(): - """Create a discovery rule with invalid host limit and priority - - :id: e3c7acb1-ac56-496b-ac04-2a83f66ec290 - - :expectedresults: Validation error should be raised - """ - with pytest.raises(HTTPError): - entities.DiscoveryRule(max_count=gen_string('alpha')).create() - with pytest.raises(HTTPError): - entities.DiscoveryRule(priority=gen_string('alpha')).create() - - -@pytest.mark.stubbed @pytest.mark.tier3 -def test_positive_provision_with_rule_priority(): +def test_positive_update_and_provision_with_rule_priority( + module_target_sat, module_discovery_hostgroup, discovery_location, discovery_org +): """Create multiple discovery rules with different priority and check rule with highest priority executed first @@ -130,44 +97,67 @@ def test_positive_provision_with_rule_priority(): :expectedresults: Host with lower count have higher priority and that rule should be executed first - :CaseAutomation: NotAutomated - :CaseImportance: High """ + discovered_host = module_target_sat.api_factory.create_discovered_host() + + prio_rule = module_target_sat.api.DiscoveryRule( + max_count=5, + hostgroup=module_discovery_hostgroup, + search_=f'name = {discovered_host["name"]}', + location=[discovery_location], + organization=[discovery_org], + priority=1, + ).create() + rule = module_target_sat.api.DiscoveryRule( + max_count=5, + hostgroup=module_discovery_hostgroup, + search_=f'name = {discovered_host["name"]}', + location=[discovery_location], + organization=[discovery_org], + priority=10, + ).create() -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_positive_multi_provision_with_rule_limit(): - """Create a discovery rule (CPU_COUNT = 2) with host limit 1 and - provision more than 2 hosts with same rule - - :id: 553c8ebf-d1c1-4ac2-7948-d3664a5b450b - - :Setup: Hosts with two CPUs should already be discovered - - :expectedresults: Rule should only be applied to 2 discovered hosts - and the rule should already be skipped for the 3rd one. - - :CaseAutomation: NotAutomated + result = module_target_sat.api.DiscoveredHost(id=discovered_host['id']).auto_provision() + assert f'provisioned with rule {prio_rule.name}' in result['message'] - :CaseImportance: High - """ + # Delete discovery rule + for _ in rule, prio_rule: + _.delete() + with pytest.raises(HTTPError): + _.read() -@pytest.mark.stubbed @pytest.mark.tier3 -def test_positive_provision_with_updated_discovery_rule(): - """Update an existing rule and provision a host with it. +def test_positive_multi_provision_with_rule_limit( + module_target_sat, module_discovery_hostgroup, discovery_location, discovery_org +): + """Create a discovery rule with certain host limit and try to provision more than the passed limit - :id: 3fb20f0f-02e9-4158-9744-f583308c4e89 - - :Setup: Host should already be discovered + :id: 553c8ebf-d1c1-4ac2-7948-d3664a5b450b - :expectedresults: User should be able to update the rule and it should - be applied on discovered host + :Setup: Hosts should already be discovered - :CaseAutomation: NotAutomated + :expectedresults: Rule should only be applied to the number of the hosts passed as limit in the rule :CaseImportance: High """ + for _ in range(2): + discovered_host = module_target_sat.api_factory.create_discovered_host() + + rule = module_target_sat.api.DiscoveryRule( + max_count=1, + hostgroup=module_discovery_hostgroup, + search_=f'name = {discovered_host["name"]}', + location=[discovery_location], + organization=[discovery_org], + priority=1000, + ).create() + result = module_target_sat.api.DiscoveredHost().auto_provision_all() + assert '1 discovered hosts were provisioned' in result['message'] + + # Delete discovery rule + rule.delete() + with pytest.raises(HTTPError): + rule.read() From 110f383328a6d0511802ec46b74e866b16454f1a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 31 Oct 2023 06:58:22 -0400 Subject: [PATCH 308/586] [6.14.z] Remove skip marker from test (#13013) --- tests/foreman/api/test_registration.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/foreman/api/test_registration.py b/tests/foreman/api/test_registration.py index 9d184f029f5..d9267bb5d2f 100644 --- a/tests/foreman/api/test_registration.py +++ b/tests/foreman/api/test_registration.py @@ -89,7 +89,6 @@ def test_host_registration_end_to_end( @pytest.mark.tier3 @pytest.mark.rhel_ver_match('[^6]') -@pytest.mark.skip_if_open("BZ:2229112") def test_positive_allow_reregistration_when_dmi_uuid_changed( module_org, rhel_contenthost, target_sat, module_ak_with_synced_repo, module_location ): From 15177082e87d105ea0b73461155fbf34b1ede745 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 31 Oct 2023 19:42:55 -0400 Subject: [PATCH 309/586] [6.14.z] ISS refactor - batch 3 (#13018) --- tests/foreman/cli/test_satellitesync.py | 240 ++++++++++++++++++++---- 1 file changed, 199 insertions(+), 41 deletions(-) diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index d5a5cfdf9f6..a4c71f13d80 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -17,10 +17,12 @@ :Upstream: No """ import os +from time import sleep from fauxfactory import gen_string from manifester import Manifester import pytest +from wait_for import wait_for from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.content_export import ContentExport @@ -46,6 +48,7 @@ PULP_IMPORT_DIR, REPO_TYPE, REPOS, + DataFile, ) from robottelo.constants.repos import ANSIBLE_GALAXY @@ -486,41 +489,6 @@ def _create_cv(cv_name, repo, module_org, publish=True): return content_view, cvv_id -def _import_entities(product, repo, cv, mos='no'): - """Sets same CV, product and repository in importing organization as - exporting organization - - :param str product: The product name same as exporting product - :param str repo: The repo name same as exporting repo - :param str cv: The cv name same as exporting cv - :param str mos: Mirror on Sync repo, by default 'no' can override to 'yes' - :returns dictionary with CLI entities created in this function - """ - importing_org = make_org() - importing_prod = make_product({'organization-id': importing_org['id'], 'name': product}) - importing_repo = make_repository( - { - 'name': repo, - 'mirror-on-sync': mos, - 'download-policy': 'immediate', - 'product-id': importing_prod['id'], - } - ) - importing_cv = make_content_view({'name': cv, 'organization-id': importing_org['id']}) - ContentView.add_repository( - { - 'id': importing_cv['id'], - 'organization-id': importing_org['id'], - 'repository-id': importing_repo['id'], - } - ) - return { - 'importing_org': importing_org, - 'importing_repo': importing_repo, - 'importing_cv': importing_cv, - } - - class TestContentViewSync: """Implements Content View Export Import tests in CLI""" @@ -1333,6 +1301,107 @@ def test_postive_export_import_cv_with_file_content( assert len(imported_files) assert len(exported_files) == len(imported_files) + @pytest.mark.tier2 + @pytest.mark.parametrize( + 'function_synced_rhel_repo', + ['rhae2'], + indirect=True, + ) + def test_positive_export_rerun_failed_import( + self, + target_sat, + config_export_import_settings, + export_import_cleanup_function, + function_synced_rhel_repo, + function_sca_manifest_org, + function_import_org_with_manifest, + ): + """Verify that import can be rerun successfully after failed import. + + :id: 73e7cece-9a93-4203-9c2c-813d5a8d7700 + + :parametrized: yes + + :setup: + 1. Enabled and synced RH repository. + + :steps: + 1. Create CV, add repo from the setup, publish it and run export. + 2. Start import of the CV into another organization and kill it before it's done. + 3. Rerun the import again, let it finish and check the CVV was imported. + + :expectedresults: + 1. First import should fail, no CVV should be added. + 2. Second import should succeed without errors and should contain the CVV. + + :CaseImportance: Medium + + :BZ: 2058905 + + :customerscenario: true + """ + # Create CV and publish + cv_name = gen_string('alpha') + cv = target_sat.cli_factory.make_content_view( + {'name': cv_name, 'organization-id': function_sca_manifest_org.id} + ) + target_sat.cli.ContentView.add_repository( + { + 'id': cv['id'], + 'organization-id': function_sca_manifest_org.id, + 'repository-id': function_synced_rhel_repo['id'], + } + ) + target_sat.cli.ContentView.publish({'id': cv['id']}) + cv = target_sat.cli.ContentView.info({'id': cv['id']}) + assert len(cv['versions']) == 1 + cvv = cv['versions'][0] + # Verify export directory is empty + assert target_sat.validate_pulp_filepath(function_sca_manifest_org, PULP_EXPORT_DIR) == '' + # Export the CV + export = target_sat.cli.ContentExport.completeVersion( + {'id': cvv['id'], 'organization-id': function_sca_manifest_org.id} + ) + import_path = target_sat.move_pulp_archive(function_sca_manifest_org, export['message']) + assert target_sat.execute(f'ls {import_path}').stdout != '' + # Run the import asynchronously + task_id = target_sat.cli.ContentImport.version( + { + 'organization-id': function_import_org_with_manifest.id, + 'path': import_path, + 'async': True, + } + )['id'] + # Wait for the CV creation on import and make the import fail + wait_for( + lambda: target_sat.cli.ContentView.info( + {'name': cv_name, 'organization-id': function_import_org_with_manifest.id} + ) + ) + target_sat.cli.Service.restart() + sleep(30) + # Assert that the initial import task did not succeed and CVV was removed + assert ( + target_sat.api.ForemanTask() + .search( + query={'search': f'Actions::Katello::ContentViewVersion::Import and id = {task_id}'} + )[0] + .result + != 'success' + ) + importing_cvv = target_sat.cli.ContentView.info( + {'name': cv_name, 'organization-id': function_import_org_with_manifest.id} + )['versions'] + assert len(importing_cvv) == 0 + # Rerun the import and let it finish + target_sat.cli.ContentImport.version( + {'organization-id': function_import_org_with_manifest.id, 'path': import_path} + ) + importing_cvv = target_sat.cli.ContentView.info( + {'name': cv_name, 'organization-id': function_import_org_with_manifest.id} + )['versions'] + assert len(importing_cvv) == 1 + @pytest.mark.tier3 def test_postive_export_import_ansible_collection_repo( self, @@ -1390,6 +1459,73 @@ def test_postive_export_import_ansible_collection_repo( assert len(import_product['content']) == 1 assert import_product['content'][0]['content-type'] == "ansible_collection" + @pytest.mark.tier3 + def test_postive_export_import_repo_with_GPG( + self, + target_sat, + config_export_import_settings, + export_import_cleanup_function, + function_org, + function_synced_custom_repo, + function_import_org, + ): + """Test export and import of a repository with GPG key + + :id: a5b455aa-e87e-4ae5-a1c7-4c8e6c7f7af5 + + :setup: + 1. Product with synced custom repository. + + :steps: + 1. Create a GPG key and add it to the setup repository. + 2. Export the repository and import it into another organization. + + :expectedresults: + 1. Export and import succeeds without any errors. + 2. GPG key is imported to the importing org too. + + :CaseImportance: Medium + + :BZ: 2178645, 2090390 + + :customerscenario: true + """ + # Create a GPG key and add it to the setup repository. + gpg_key = target_sat.api.GPGKey( + organization=function_org, + content=DataFile.VALID_GPG_KEY_FILE.read_text(), + ).create() + target_sat.cli.Repository.update( + {'id': function_synced_custom_repo.id, 'gpg-key-id': gpg_key.id} + ) + # Export the repository and import it into another organization. + export = target_sat.cli.ContentExport.completeRepository( + {'id': function_synced_custom_repo.id} + ) + import_path = target_sat.move_pulp_archive(function_org, export['message']) + target_sat.cli.ContentImport.repository( + { + 'organization-id': function_import_org.id, + 'path': import_path, + } + ) + # Check the imported repo has the GPG key assigned. + imported_repo = target_sat.cli.Repository.info( + { + 'name': function_synced_custom_repo.name, + 'product': function_synced_custom_repo.product.name, + 'organization-id': function_import_org.id, + } + ) + assert int(imported_repo['content-counts']['packages']) + assert imported_repo['gpg-key']['name'] == gpg_key.name + # Check the GPG key is imported to the importing org too. + imported_gpg = target_sat.cli.ContentCredential.info( + {'organization-id': function_import_org.id, 'name': gpg_key.name} + ) + assert imported_gpg + assert imported_gpg['content'] == gpg_key.content + @pytest.mark.tier3 @pytest.mark.parametrize( 'function_synced_rhel_repo', @@ -1480,13 +1616,15 @@ def test_positive_import_content_for_disconnected_sat_with_existing_content( 1. Product with synced custom repository, published in a CV. :steps: - 1. Run complete export of the CV. - 2. On Disconnected satellite, create a cv with same name as cv on 2 and with - 'import-only' selected. - 3. Run the import command. + 1. Run complete export of the CV from setup. + 2. On Disconnected satellite, create a CV with the same name as setup CV and with + 'import-only' set to False and run the import command. + 3. On Disconnected satellite, create a CV with the same name as setup CV and with + 'import-only' set to True and run the import command. :expectedresults: - 1. Import should run successfully + 1. Import should fail with correct message when existing CV has 'import-only' set False. + 2. Import should succeed when existing CV has 'import-only' set True. :bz: 2030101 @@ -1505,7 +1643,27 @@ def test_positive_import_content_for_disconnected_sat_with_existing_content( result = target_sat.execute(f'ls {import_path}') assert result.stdout != '' # Import section - # Create cv with 'import-only' set to true + # Create cv with 'import-only' set to False + cv = target_sat.cli_factory.make_content_view( + { + 'name': export_cv_name, + 'import-only': False, + 'organization-id': function_import_org.id, + } + ) + with pytest.raises(CLIReturnCodeError) as error: + target_sat.cli.ContentImport.version( + {'organization-id': function_import_org.id, 'path': import_path} + ) + assert ( + f"Unable to import in to Content View specified in the metadata - '{export_cv_name}'. " + "The 'import_only' attribute for the content view is set to false. To mark this " + "Content View as importable, have your system administrator run the following command " + f"on the server. \n foreman-rake katello:set_content_view_import_only ID={cv.id}" + ) in error.value.message + target_sat.cli.ContentView.remove({'id': cv.id, 'destroy-content-view': 'yes'}) + + # Create cv with 'import-only' set to True target_sat.cli_factory.make_content_view( {'name': export_cv_name, 'import-only': True, 'organization-id': function_import_org.id} ) From e325177d6145a9062f51db67746ce2c8d73fbba1 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 31 Oct 2023 23:40:48 -0400 Subject: [PATCH 310/586] [6.14.z] Bump dynaconf[vault] from 3.2.3 to 3.2.4 (#13023) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 722de1b3cc2..af138486ae6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ betelgeuse==1.10.0 broker[docker]==0.4.1 cryptography==41.0.5 deepdiff==6.6.1 -dynaconf[vault]==3.2.3 +dynaconf[vault]==3.2.4 fauxfactory==3.1.0 jinja2==3.1.2 manifester==0.0.14 From 18771e5a4429ecb575589c64136b536056f127a7 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 1 Nov 2023 16:53:05 -0400 Subject: [PATCH 311/586] [6.14.z] ISS refactor - batch 4 (#13028) --- tests/foreman/cli/test_satellitesync.py | 258 ++++++++++++------------ 1 file changed, 134 insertions(+), 124 deletions(-) diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index a4c71f13d80..19e3b8a178a 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -41,6 +41,7 @@ from robottelo.config import settings from robottelo.constants import ( CONTAINER_REGISTRY_HUB, + CONTAINER_UPSTREAM_NAME, DEFAULT_ARCHITECTURE, DEFAULT_CV, EXPORT_LIBRARY_NAME, @@ -70,9 +71,8 @@ def config_export_import_settings(): def export_import_cleanup_function(target_sat, function_org): """Deletes export/import dirs of function org""" yield - # Deletes directories created for export/import test target_sat.execute( - f'rm -rf {PULP_EXPORT_DIR}/{function_org.name} {PULP_IMPORT_DIR}/{function_org.name}', + f'rm -rf {PULP_EXPORT_DIR}/{function_org.name} {PULP_IMPORT_DIR}/{function_org.name}' ) @@ -80,7 +80,6 @@ def export_import_cleanup_function(target_sat, function_org): def export_import_cleanup_module(target_sat, module_org): """Deletes export/import dirs of module_org""" yield - # Deletes directories created for export/import test target_sat.execute( f'rm -rf {PULP_EXPORT_DIR}/{module_org.name} {PULP_IMPORT_DIR}/{module_org.name}' ) @@ -101,23 +100,6 @@ def function_import_org_with_manifest(target_sat, function_import_org): yield function_import_org -@pytest.fixture(scope='class') -def docker_repo(module_target_sat, module_org): - product = make_product({'organization-id': module_org.id}) - repo = make_repository( - { - 'organization-id': module_org.id, - 'product-id': product['id'], - 'content-type': REPO_TYPE['docker'], - 'download-policy': 'immediate', - 'url': 'https://quay.io', - 'docker-upstream-name': 'quay/busybox', - } - ) - Repository.synchronize({'id': repo['id']}) - yield repo - - @pytest.fixture(scope='module') def module_synced_custom_repo(module_target_sat, module_org, module_product): repo = module_target_sat.cli_factory.make_repository( @@ -183,6 +165,54 @@ def function_synced_rhel_repo(request, target_sat, function_sca_manifest_org): return repo +@pytest.fixture(scope='function') +def function_synced_file_repo(target_sat, function_org, function_product): + repo = target_sat.cli_factory.make_repository( + { + 'organization-id': function_org.id, + 'product-id': function_product.id, + 'content-type': 'file', + 'url': settings.repos.file_type_repo.url, + } + ) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + yield repo + + +@pytest.fixture(scope='function') +def function_synced_docker_repo(target_sat, function_org): + product = target_sat.cli_factory.make_product({'organization-id': function_org.id}) + repo = target_sat.cli_factory.make_repository( + { + 'organization-id': function_org.id, + 'product-id': product['id'], + 'content-type': REPO_TYPE['docker'], + 'download-policy': 'immediate', + 'url': CONTAINER_REGISTRY_HUB, + 'docker-upstream-name': CONTAINER_UPSTREAM_NAME, + } + ) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + yield repo + + +@pytest.fixture(scope='function') +def function_synced_AC_repo(target_sat, function_org, function_product): + repo = target_sat.cli_factory.make_repository( + { + 'organization-id': function_org.id, + 'product-id': function_product.id, + 'content-type': 'ansible_collection', + 'url': ANSIBLE_GALAXY, + 'ansible-collection-requirements': '{collections: [ \ + { name: theforeman.foreman, version: "2.1.0" }, \ + { name: theforeman.operations, version: "0.1.0"} ]}', + } + ) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + yield repo + + @pytest.mark.run_in_one_thread class TestRepositoryExport: """Tests for exporting a repository via CLI""" @@ -241,7 +271,7 @@ def test_positive_export_version_custom_repo( target_sat.cli.ContentView.publish({'id': cv['id']}) cv = target_sat.cli.ContentView.info({'id': cv['id']}) assert len(cv['versions']) == 2 - cvv = cv['versions'][1] + cvv = max(cv['versions'], key=lambda x: int(x['id'])) target_sat.cli.ContentExport.incrementalVersion( {'id': cvv['id'], 'organization-id': module_org.id} ) @@ -344,7 +374,7 @@ def test_positive_export_complete_library_rh_repo( @pytest.mark.tier3 @pytest.mark.upgrade def test_positive_export_repository_docker( - self, target_sat, export_import_cleanup_module, module_org, docker_repo + self, target_sat, export_import_cleanup_function, function_org, function_synced_docker_repo ): """Export docker repo via complete and incremental repository. @@ -366,18 +396,20 @@ def test_positive_export_repository_docker( :customerscenario: true """ # Verify export directory is empty - assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) == '' + assert target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) == '' # Export complete and check the export directory - target_sat.cli.ContentExport.completeRepository({'id': docker_repo['id']}) - assert '1.0' in target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) + target_sat.cli.ContentExport.completeRepository({'id': function_synced_docker_repo['id']}) + assert '1.0' in target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) # Export incremental and check the export directory - target_sat.cli.ContentExport.incrementalRepository({'id': docker_repo['id']}) - assert '2.0' in target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) + target_sat.cli.ContentExport.incrementalRepository( + {'id': function_synced_docker_repo['id']} + ) + assert '2.0' in target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) @pytest.mark.tier3 @pytest.mark.upgrade def test_positive_export_version_docker( - self, target_sat, export_import_cleanup_module, module_org, docker_repo + self, target_sat, export_import_cleanup_function, function_org, function_synced_docker_repo ): """Export CV with docker repo via complete and incremental version. @@ -402,12 +434,12 @@ def test_positive_export_version_docker( """ # Create CV and publish cv_name = gen_string('alpha') - cv = make_content_view({'name': cv_name, 'organization-id': module_org.id}) + cv = make_content_view({'name': cv_name, 'organization-id': function_org.id}) target_sat.cli.ContentView.add_repository( { 'id': cv['id'], - 'organization-id': module_org.id, - 'repository-id': docker_repo['id'], + 'organization-id': function_org.id, + 'repository-id': function_synced_docker_repo['id'], } ) target_sat.cli.ContentView.publish({'id': cv['id']}) @@ -415,21 +447,21 @@ def test_positive_export_version_docker( assert len(cv['versions']) == 1 cvv = cv['versions'][0] # Verify export directory is empty - assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) == '' + assert target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) == '' # Export complete and check the export directory target_sat.cli.ContentExport.completeVersion( - {'id': cvv['id'], 'organization-id': module_org.id} + {'id': cvv['id'], 'organization-id': function_org.id} ) - assert '1.0' in target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) + assert '1.0' in target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) # Publish new CVV, export incremental and check the export directory target_sat.cli.ContentView.publish({'id': cv['id']}) cv = target_sat.cli.ContentView.info({'id': cv['id']}) assert len(cv['versions']) == 2 - cvv = cv['versions'][1] + cvv = max(cv['versions'], key=lambda x: int(x['id'])) target_sat.cli.ContentExport.incrementalVersion( - {'id': cvv['id'], 'organization-id': module_org.id} + {'id': cvv['id'], 'organization-id': function_org.id} ) - assert '2.0' in target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) + assert '2.0' in target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) @pytest.fixture(scope='class') @@ -1141,149 +1173,127 @@ def test_negative_import_invalid_path(self, module_org): @pytest.mark.tier3 def test_postive_export_cv_with_mixed_content_repos( - self, class_export_entities, export_import_cleanup_module, target_sat, module_org + self, + export_import_cleanup_function, + target_sat, + function_org, + function_synced_custom_repo, + function_synced_file_repo, + function_synced_docker_repo, + function_synced_AC_repo, ): """Exporting CV version having yum and non-yum(docker) is successful :id: ffcdbbc6-f787-4978-80a7-4b44c389bf49 - :steps: + :setup: + 1. Synced repositories of each content type: yum, file, docker, AC - 1. Create product with yum and non-yum(docker) repos - 2. Sync the repositories - 3. Create CV with above product and publish - 4. Export CV version contents to a directory + :steps: + 1. Create CV, add all setup repos and publish. + 2. Export CV version contents to a directory. :expectedresults: - 1. Export will succeed, however the export wont contain non-yum repo. - No warning is printed (see BZ 1775383) + 1. Export succeeds and content is exported. :BZ: 1726457 :customerscenario: true """ - product = make_product( - { - 'organization-id': module_org.id, - 'name': gen_string('alpha'), - } - ) - nonyum_repo = make_repository( - { - 'content-type': 'docker', - 'docker-upstream-name': 'quay/busybox', - 'organization-id': module_org.id, - 'product-id': product['id'], - 'url': CONTAINER_REGISTRY_HUB, - }, - ) - Repository.synchronize({'id': nonyum_repo['id']}) - yum_repo = make_repository( - { - 'name': gen_string('alpha'), - 'download-policy': 'immediate', - 'mirror-on-sync': 'no', - 'product-id': product['id'], - } - ) - Repository.synchronize({'id': yum_repo['id']}) - content_view = make_content_view({'organization-id': module_org.id}) - # Add docker and yum repo - ContentView.add_repository( - { - 'id': content_view['id'], - 'organization-id': module_org.id, - 'repository-id': nonyum_repo['id'], - } - ) - ContentView.add_repository( - { - 'id': content_view['id'], - 'organization-id': module_org.id, - 'repository-id': yum_repo['id'], - } + content_view = target_sat.cli_factory.make_content_view( + {'organization-id': function_org.id} + ) + repos = [ + function_synced_custom_repo, + function_synced_file_repo, + function_synced_docker_repo, + function_synced_AC_repo, + ] + for repo in repos: + target_sat.cli.ContentView.add_repository( + { + 'id': content_view['id'], + 'organization-id': function_org.id, + 'repository-id': repo['id'], + } + ) + target_sat.cli.ContentView.publish({'id': content_view['id']}) + exporting_cv = target_sat.cli.ContentView.info({'id': content_view['id']}) + assert len(exporting_cv['versions']) == 1 + exporting_cvv = target_sat.cli.ContentView.version_info( + {'id': exporting_cv['versions'][0]['id']} ) - ContentView.publish({'id': content_view['id']}) - exporting_cv_id = ContentView.info({'id': content_view['id']}) - assert len(exporting_cv_id['versions']) == 1 - exporting_cvv_id = exporting_cv_id['versions'][0] + assert len(exporting_cvv['repositories']) == len(repos) # check packages - exported_packages = Package.list({'content-view-version-id': exporting_cvv_id['id']}) + exported_packages = target_sat.cli.Package.list( + {'content-view-version-id': exporting_cvv['id']} + ) assert len(exported_packages) # Verify export directory is empty - assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) == '' + assert target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) == '' # Export cv - ContentExport.completeVersion( - {'id': exporting_cvv_id['id'], 'organization-id': module_org.id} + target_sat.cli.ContentExport.completeVersion( + {'id': exporting_cvv['id'], 'organization-id': function_org.id} ) # Verify export directory is not empty - assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) != '' + assert target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) != '' @pytest.mark.tier3 def test_postive_export_import_cv_with_file_content( self, target_sat, config_export_import_settings, - export_import_cleanup_module, - module_org, + export_import_cleanup_function, + function_org, + function_synced_file_repo, function_import_org, ): """Exporting and Importing cv with file content :id: d00739f0-dedf-4303-8929-889dc23260a4 + :setup: + 1. Product with synced file-type repository. + :steps: - 1. Create custom product and custom repo with file type - 2. Sync repo - 3. Create cv and add file repo created in step 1 and publish - 4. Export cv and import cv into another satellite. - 5. Check imported cv has files in it. + 3. Create CV, add the file repo and publish. + 4. Export the CV and import it into another organization. + 5. Check the imported CV has files in it. :expectedresults: - 1. Imported cv should have the files present in the cv of the imported system. + 1. Imported CV should have the files present. :BZ: 1995827 :customerscenario: true """ - # setup custom repo + # Create CV, add the file repo and publish. cv_name = import_cv_name = gen_string('alpha') - product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) - file_repo = target_sat.cli_factory.make_repository( - { - 'organization-id': module_org.id, - 'product-id': product['id'], - 'content-type': 'file', - 'url': settings.repos.file_type_repo.url, - } - ) - target_sat.cli.Repository.synchronize({'id': file_repo['id']}) - # create cv and publish cv = target_sat.cli_factory.make_content_view( - {'name': cv_name, 'organization-id': module_org.id} + {'name': cv_name, 'organization-id': function_org.id} ) target_sat.cli.ContentView.add_repository( { 'id': cv['id'], - 'organization-id': module_org.id, - 'repository-id': file_repo['id'], + 'organization-id': function_org.id, + 'repository-id': function_synced_file_repo['id'], } ) target_sat.cli.ContentView.publish({'id': cv['id']}) - exporting_cv_id = target_sat.cli.ContentView.info({'id': cv['id']}) - assert len(exporting_cv_id['versions']) == 1 - exporting_cvv_id = exporting_cv_id['versions'][0]['id'] + exporting_cv = target_sat.cli.ContentView.info({'id': cv['id']}) + assert len(exporting_cv['versions']) == 1 + exporting_cvv_id = exporting_cv['versions'][0]['id'] # check files exported_files = target_sat.cli.File.list({'content-view-version-id': exporting_cvv_id}) assert len(exported_files) # Verify export directory is empty - assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) == '' - # Export cv + assert target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) == '' + # Export the CV export = target_sat.cli.ContentExport.completeVersion( - {'id': exporting_cvv_id, 'organization-id': module_org.id} + {'id': exporting_cvv_id, 'organization-id': function_org.id} ) - import_path = target_sat.move_pulp_archive(module_org, export['message']) + import_path = target_sat.move_pulp_archive(function_org, export['message']) # Check that files are present in import_path result = target_sat.execute(f'ls {import_path}') assert result.stdout != '' From 57436c0ddb8668b1102f309eb684764cecbc9f80 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 2 Nov 2023 18:33:01 -0400 Subject: [PATCH 312/586] [6.14.z] Check if VAULT_ADDR env var exists before deleting it (#13032) Check if VAULT_ADDR env var exists before deleting it (#13016) Co-authored-by: dosas (cherry picked from commit c1d7b42971f61d831e4afd635ebf0c7a27bd643a) Co-authored-by: dosas --- robottelo/utils/vault.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/robottelo/utils/vault.py b/robottelo/utils/vault.py index d3a40d1a706..d447331ac15 100644 --- a/robottelo/utils/vault.py +++ b/robottelo/utils/vault.py @@ -31,7 +31,8 @@ def setup(self): self.export_vault_addr() def teardown(self): - del os.environ['VAULT_ADDR'] + if os.environ.get('VAULT_ADDR') is not None: + del os.environ['VAULT_ADDR'] def export_vault_addr(self): vaulturl = re.findall('VAULT_URL_FOR_DYNACONF=(.*)', self.envdata)[0] From d9711958cb1ba8a2d435ef81f844cb452b2bce2e Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 3 Nov 2023 16:51:48 -0400 Subject: [PATCH 313/586] [6.14.z] Adding the image config details in the docker config (#13041) Adding the image config details in the docker config (#12958) (cherry picked from commit ee1ee6a3e56bcc7a3e30083b2760c916438a2bca) Co-authored-by: Devendra --- conf/docker.yaml.template | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/conf/docker.yaml.template b/conf/docker.yaml.template index 81fc6a84dd0..4176b38ee8a 100644 --- a/conf/docker.yaml.template +++ b/conf/docker.yaml.template @@ -9,3 +9,23 @@ DOCKER: PRIVATE_REGISTRY_USERNAME: # Private docker registry password PRIVATE_REGISTRY_PASSWORD: + # Image Pass Registry + IMAGE_REGISTRY: + # image repository URL + URL: + # Pull a non-namespace image using the image pass registry proxy + NON_NAMESPACE: + # Proxy for the non-namespace image + PROXY: + # Username for the non-namespace image pass registry proxy + USERNAME: + # Password for the non-namespace image pass registry proxy + PASSWORD: + # Pull a namespace image using the image pass registry proxy + NAMESPACE: + # proxy for the namespace image + PROXY: + # Username for the namespace image pass registry proxy + USERNAME: + # Password for the namespace image pass registry proxy + PASSWORD: From 016e19aa662fb9b4220ef1dd750577e3dc8dde1e Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 8 Nov 2023 03:03:29 -0500 Subject: [PATCH 314/586] [6.14.z] Bump deepdiff from 6.6.1 to 6.7.0 (#13057) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index af138486ae6..750aba55730 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ betelgeuse==1.10.0 broker[docker]==0.4.1 cryptography==41.0.5 -deepdiff==6.6.1 +deepdiff==6.7.0 dynaconf[vault]==3.2.4 fauxfactory==3.1.0 jinja2==3.1.2 From 9a69c8fe16123857c82f92aeb1c648836baa5e09 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 10 Nov 2023 10:10:57 -0500 Subject: [PATCH 315/586] [6.14.z] updating the fedora image to 38 as latest using python 3.12 (#13075) updating the fedora image to 38 as latest using python 3.12 (cherry picked from commit 5f0c2b0520d54a02cbae97a756b3a6c287cb85a6) Co-authored-by: omkarkhatavkar --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index de7b12ac71d..2b88a306431 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM fedora +FROM fedora:38 MAINTAINER https://github.com/SatelliteQE RUN dnf install -y gcc git make cmake libffi-devel openssl-devel python3-devel \ @@ -6,7 +6,6 @@ RUN dnf install -y gcc git make cmake libffi-devel openssl-devel python3-devel \ COPY / /robottelo/ WORKDIR /robottelo -RUN curl https://raw.githubusercontent.com/SatelliteQE/broker/master/broker_settings.yaml.example -o broker_settings.yaml ENV PYCURL_SSL_LIBRARY=openssl RUN pip install -r requirements.txt From 285d7a6cce9890e65fb6c90f5602e5d810170d15 Mon Sep 17 00:00:00 2001 From: omkarkhatavkar Date: Wed, 18 Oct 2023 16:14:10 +0530 Subject: [PATCH 316/586] removing the import entities from nailgun and moving target_sat --- tests/foreman/api/test_activationkey.py | 108 +++-- tests/foreman/api/test_architecture.py | 17 +- tests/foreman/api/test_audit.py | 89 ++-- tests/foreman/api/test_bookmarks.py | 65 ++- tests/foreman/api/test_capsulecontent.py | 99 ++-- tests/foreman/api/test_computeprofile.py | 31 +- tests/foreman/api/test_contentcredentials.py | 49 +- tests/foreman/api/test_contentview.py | 201 ++++---- tests/foreman/api/test_contentviewfilter.py | 226 +++++---- tests/foreman/api/test_contentviewversion.py | 209 +++++--- tests/foreman/api/test_discoveryrule.py | 39 +- tests/foreman/api/test_docker.py | 305 +++++++----- tests/foreman/api/test_errata.py | 72 +-- tests/foreman/api/test_filter.py | 20 +- tests/foreman/api/test_foremantask.py | 11 +- tests/foreman/api/test_host.py | 246 +++++----- tests/foreman/api/test_hostcollection.py | 115 +++-- tests/foreman/api/test_hostgroup.py | 76 +-- tests/foreman/api/test_http_proxy.py | 23 +- tests/foreman/api/test_ldapauthsource.py | 7 +- .../foreman/api/test_lifecycleenvironment.py | 43 +- tests/foreman/api/test_media.py | 61 +-- tests/foreman/api/test_multiple_paths.py | 38 +- tests/foreman/api/test_organization.py | 58 +-- .../foreman/api/test_oscap_tailoringfiles.py | 21 +- tests/foreman/api/test_oscappolicy.py | 19 +- tests/foreman/api/test_permission.py | 24 +- tests/foreman/api/test_product.py | 97 ++-- tests/foreman/api/test_reporttemplates.py | 85 ++-- tests/foreman/api/test_repositories.py | 3 +- tests/foreman/api/test_repository.py | 202 ++++---- tests/foreman/api/test_repository_set.py | 9 +- tests/foreman/api/test_role.py | 457 ++++++++++-------- tests/foreman/api/test_settings.py | 9 +- tests/foreman/api/test_subnet.py | 75 ++- tests/foreman/api/test_subscription.py | 80 +-- tests/foreman/api/test_syncplan.py | 130 ++--- tests/foreman/api/test_templatesync.py | 131 ++--- tests/foreman/api/test_user.py | 205 ++++---- tests/foreman/api/test_usergroup.py | 89 ++-- tests/foreman/api/test_webhook.py | 29 +- 41 files changed, 2120 insertions(+), 1753 deletions(-) diff --git a/tests/foreman/api/test_activationkey.py b/tests/foreman/api/test_activationkey.py index 96da8a8f4c8..8c6f9ea194d 100644 --- a/tests/foreman/api/test_activationkey.py +++ b/tests/foreman/api/test_activationkey.py @@ -19,7 +19,7 @@ import http from fauxfactory import gen_integer, gen_string -from nailgun import client, entities +from nailgun import client import pytest from requests.exceptions import HTTPError @@ -46,7 +46,7 @@ def _bad_max_hosts(): @pytest.mark.tier1 -def test_positive_create_unlimited_hosts(): +def test_positive_create_unlimited_hosts(target_sat): """Create a plain vanilla activation key. :id: 1d73b8cc-a754-4637-8bae-d9d2aaf89003 @@ -56,12 +56,12 @@ def test_positive_create_unlimited_hosts(): :CaseImportance: Critical """ - assert entities.ActivationKey().create().unlimited_hosts is True + assert target_sat.api.ActivationKey().create().unlimited_hosts is True @pytest.mark.tier1 @pytest.mark.parametrize('max_host', **parametrized(_good_max_hosts())) -def test_positive_create_limited_hosts(max_host): +def test_positive_create_limited_hosts(max_host, target_sat): """Create an activation key with limited hosts. :id: 9bbba620-fd98-4139-a44b-af8ce330c7a4 @@ -73,14 +73,14 @@ def test_positive_create_limited_hosts(max_host): :parametrized: yes """ - act_key = entities.ActivationKey(max_hosts=max_host, unlimited_hosts=False).create() + act_key = target_sat.api.ActivationKey(max_hosts=max_host, unlimited_hosts=False).create() assert act_key.max_hosts == max_host assert act_key.unlimited_hosts is False @pytest.mark.tier1 @pytest.mark.parametrize('key_name', **parametrized(valid_data_list())) -def test_positive_create_with_name(key_name): +def test_positive_create_with_name(key_name, target_sat): """Create an activation key providing the initial name. :id: 749e0d28-640e-41e5-89d6-b92411ce73a3 @@ -91,13 +91,13 @@ def test_positive_create_with_name(key_name): :parametrized: yes """ - act_key = entities.ActivationKey(name=key_name).create() + act_key = target_sat.api.ActivationKey(name=key_name).create() assert key_name == act_key.name @pytest.mark.tier2 @pytest.mark.parametrize('desc', **parametrized(valid_data_list())) -def test_positive_create_with_description(desc): +def test_positive_create_with_description(desc, target_sat): """Create an activation key and provide a description. :id: 64d93726-6f96-4a2e-ab29-eb5bfa2ff8ff @@ -106,12 +106,12 @@ def test_positive_create_with_description(desc): :parametrized: yes """ - act_key = entities.ActivationKey(description=desc).create() + act_key = target_sat.api.ActivationKey(description=desc).create() assert desc == act_key.description @pytest.mark.tier2 -def test_negative_create_with_no_host_limit(): +def test_negative_create_with_no_host_limit(target_sat): """Create activation key without providing limitation for hosts number :id: a9e756e1-886d-4f0d-b685-36ce4247517d @@ -121,12 +121,12 @@ def test_negative_create_with_no_host_limit(): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.ActivationKey(unlimited_hosts=False).create() + target_sat.api.ActivationKey(unlimited_hosts=False).create() @pytest.mark.tier3 @pytest.mark.parametrize('max_host', **parametrized(_bad_max_hosts())) -def test_negative_create_with_invalid_host_limit(max_host): +def test_negative_create_with_invalid_host_limit(max_host, target_sat): """Create activation key with invalid limit values for hosts number. :id: c018b177-2074-4f1a-a7e0-9f38d6c9a1a6 @@ -138,12 +138,12 @@ def test_negative_create_with_invalid_host_limit(max_host): :parametrized: yes """ with pytest.raises(HTTPError): - entities.ActivationKey(max_hosts=max_host, unlimited_hosts=False).create() + target_sat.api.ActivationKey(max_hosts=max_host, unlimited_hosts=False).create() @pytest.mark.tier3 @pytest.mark.parametrize('name', **parametrized(invalid_names_list())) -def test_negative_create_with_invalid_name(name): +def test_negative_create_with_invalid_name(name, target_sat): """Create activation key providing an invalid name. :id: 5f7051be-0320-4d37-9085-6904025ad909 @@ -155,12 +155,12 @@ def test_negative_create_with_invalid_name(name): :parametrized: yes """ with pytest.raises(HTTPError): - entities.ActivationKey(name=name).create() + target_sat.api.ActivationKey(name=name).create() @pytest.mark.tier2 @pytest.mark.parametrize('max_host', **parametrized(_good_max_hosts())) -def test_positive_update_limited_host(max_host): +def test_positive_update_limited_host(max_host, target_sat): """Create activation key then update it to limited hosts. :id: 34ca8303-8135-4694-9cf7-b20f8b4b0a1e @@ -170,7 +170,7 @@ def test_positive_update_limited_host(max_host): :parametrized: yes """ # unlimited_hosts defaults to True. - act_key = entities.ActivationKey().create() + act_key = target_sat.api.ActivationKey().create() want = {'max_hosts': max_host, 'unlimited_hosts': False} for key, value in want.items(): setattr(act_key, key, value) @@ -181,7 +181,7 @@ def test_positive_update_limited_host(max_host): @pytest.mark.tier2 @pytest.mark.parametrize('new_name', **parametrized(valid_data_list())) -def test_positive_update_name(new_name): +def test_positive_update_name(new_name, target_sat): """Create activation key providing the initial name, then update its name to another valid name. @@ -192,14 +192,14 @@ def test_positive_update_name(new_name): :parametrized: yes """ - act_key = entities.ActivationKey().create() - updated = entities.ActivationKey(id=act_key.id, name=new_name).update(['name']) + act_key = target_sat.api.ActivationKey().create() + updated = target_sat.api.ActivationKey(id=act_key.id, name=new_name).update(['name']) assert new_name == updated.name @pytest.mark.tier3 @pytest.mark.parametrize('max_host', **parametrized(_bad_max_hosts())) -def test_negative_update_limit(max_host): +def test_negative_update_limit(max_host, target_sat): """Create activation key then update its limit to invalid value. :id: 0f857d2f-81ed-4b8b-b26e-34b4f294edbc @@ -214,7 +214,7 @@ def test_negative_update_limit(max_host): :parametrized: yes """ - act_key = entities.ActivationKey().create() + act_key = target_sat.api.ActivationKey().create() want = {'max_hosts': act_key.max_hosts, 'unlimited_hosts': act_key.unlimited_hosts} act_key.max_hosts = max_host act_key.unlimited_hosts = False @@ -227,7 +227,7 @@ def test_negative_update_limit(max_host): @pytest.mark.tier3 @pytest.mark.parametrize('new_name', **parametrized(invalid_names_list())) -def test_negative_update_name(new_name): +def test_negative_update_name(new_name, target_sat): """Create activation key then update its name to an invalid name. :id: da85a32c-942b-4ab8-a133-36b028208c4d @@ -239,16 +239,16 @@ def test_negative_update_name(new_name): :parametrized: yes """ - act_key = entities.ActivationKey().create() + act_key = target_sat.api.ActivationKey().create() with pytest.raises(HTTPError): - entities.ActivationKey(id=act_key.id, name=new_name).update(['name']) - new_key = entities.ActivationKey(id=act_key.id).read() + target_sat.api.ActivationKey(id=act_key.id, name=new_name).update(['name']) + new_key = target_sat.api.ActivationKey(id=act_key.id).read() assert new_key.name != new_name assert new_key.name == act_key.name @pytest.mark.tier3 -def test_negative_update_max_hosts(): +def test_negative_update_max_hosts(target_sat): """Create an activation key with ``max_hosts == 1``, then update that field with a string value. @@ -258,14 +258,14 @@ def test_negative_update_max_hosts(): :CaseImportance: Low """ - act_key = entities.ActivationKey(max_hosts=1).create() + act_key = target_sat.api.ActivationKey(max_hosts=1).create() with pytest.raises(HTTPError): - entities.ActivationKey(id=act_key.id, max_hosts='foo').update(['max_hosts']) + target_sat.api.ActivationKey(id=act_key.id, max_hosts='foo').update(['max_hosts']) assert act_key.read().max_hosts == 1 @pytest.mark.tier2 -def test_positive_get_releases_status_code(): +def test_positive_get_releases_status_code(target_sat): """Get an activation key's releases. Check response format. :id: e1ea4797-8d92-4bec-ae6b-7a26599825ab @@ -275,7 +275,7 @@ def test_positive_get_releases_status_code(): :CaseLevel: Integration """ - act_key = entities.ActivationKey().create() + act_key = target_sat.api.ActivationKey().create() path = act_key.path('releases') response = client.get(path, auth=get_credentials(), verify=False) status_code = http.client.OK @@ -284,7 +284,7 @@ def test_positive_get_releases_status_code(): @pytest.mark.tier2 -def test_positive_get_releases_content(): +def test_positive_get_releases_content(target_sat): """Get an activation key's releases. Check response contents. :id: 2fec3d71-33e9-40e5-b934-90b03afc26a1 @@ -293,14 +293,14 @@ def test_positive_get_releases_content(): :CaseLevel: Integration """ - act_key = entities.ActivationKey().create() + act_key = target_sat.api.ActivationKey().create() response = client.get(act_key.path('releases'), auth=get_credentials(), verify=False).json() assert 'results' in response.keys() assert type(response['results']) == list @pytest.mark.tier2 -def test_positive_add_host_collections(module_org): +def test_positive_add_host_collections(module_org, module_target_sat): """Associate an activation key with several host collections. :id: 1538808c-621e-4cf9-9b9b-840c5dd54644 @@ -318,23 +318,27 @@ def test_positive_add_host_collections(module_org): :CaseImportance: Critical """ # An activation key has no host collections by default. - act_key = entities.ActivationKey(organization=module_org).create() + act_key = module_target_sat.api.ActivationKey(organization=module_org).create() assert len(act_key.host_collection) == 0 # Give activation key one host collection. - act_key.host_collection.append(entities.HostCollection(organization=module_org).create()) + act_key.host_collection.append( + module_target_sat.api.HostCollection(organization=module_org).create() + ) act_key = act_key.update(['host_collection']) assert len(act_key.host_collection) == 1 # Give activation key second host collection. - act_key.host_collection.append(entities.HostCollection(organization=module_org).create()) + act_key.host_collection.append( + module_target_sat.api.HostCollection(organization=module_org).create() + ) act_key = act_key.update(['host_collection']) assert len(act_key.host_collection) == 2 @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_remove_host_collection(module_org): +def test_positive_remove_host_collection(module_org, module_target_sat): """Disassociate host collection from the activation key :id: 31992ac4-fe55-45bb-bd17-a191928ec2ab @@ -353,10 +357,10 @@ def test_positive_remove_host_collection(module_org): :CaseImportance: Critical """ # An activation key has no host collections by default. - act_key = entities.ActivationKey(organization=module_org).create() + act_key = module_target_sat.api.ActivationKey(organization=module_org).create() assert len(act_key.host_collection) == 0 - host_collection = entities.HostCollection(organization=module_org).create() + host_collection = module_target_sat.api.HostCollection(organization=module_org).create() # Associate host collection with activation key. act_key.add_host_collection(data={'host_collection_ids': [host_collection.id]}) @@ -368,7 +372,7 @@ def test_positive_remove_host_collection(module_org): @pytest.mark.tier1 -def test_positive_update_auto_attach(): +def test_positive_update_auto_attach(target_sat): """Create an activation key, then update the auto_attach field with the inverse boolean value. @@ -378,17 +382,17 @@ def test_positive_update_auto_attach(): :CaseImportance: Critical """ - act_key = entities.ActivationKey().create() - act_key_2 = entities.ActivationKey(id=act_key.id, auto_attach=(not act_key.auto_attach)).update( - ['auto_attach'] - ) + act_key = target_sat.api.ActivationKey().create() + act_key_2 = target_sat.api.ActivationKey( + id=act_key.id, auto_attach=(not act_key.auto_attach) + ).update(['auto_attach']) assert act_key.auto_attach != act_key_2.auto_attach @pytest.mark.tier1 @pytest.mark.upgrade @pytest.mark.parametrize('name', **parametrized(valid_data_list())) -def test_positive_delete(name): +def test_positive_delete(name, target_sat): """Create activation key and then delete it. :id: aa28d8fb-e07d-45fa-b43a-fc90c706d633 @@ -399,10 +403,10 @@ def test_positive_delete(name): :parametrized: yes """ - act_key = entities.ActivationKey(name=name).create() + act_key = target_sat.api.ActivationKey(name=name).create() act_key.delete() with pytest.raises(HTTPError): - entities.ActivationKey(id=act_key.id).read() + target_sat.api.ActivationKey(id=act_key.id).read() @pytest.mark.tier2 @@ -503,7 +507,7 @@ def test_positive_add_future_subscription(): @pytest.mark.tier1 -def test_positive_search_by_org(): +def test_positive_search_by_org(target_sat): """Search for all activation keys in an organization. :id: aedba598-2e47-44a8-826c-4dc304ba00be @@ -513,8 +517,8 @@ def test_positive_search_by_org(): :CaseImportance: Critical """ - org = entities.Organization().create() - act_key = entities.ActivationKey(organization=org).create() - keys = entities.ActivationKey(organization=org).search() + org = target_sat.api.Organization().create() + act_key = target_sat.api.ActivationKey(organization=org).create() + keys = target_sat.api.ActivationKey(organization=org).search() assert len(keys) == 1 assert act_key.id == keys[0].id diff --git a/tests/foreman/api/test_architecture.py b/tests/foreman/api/test_architecture.py index f88c8adba18..f0b7c428b0f 100644 --- a/tests/foreman/api/test_architecture.py +++ b/tests/foreman/api/test_architecture.py @@ -17,7 +17,6 @@ :Upstream: No """ from fauxfactory import gen_choice -from nailgun import entities import pytest from requests.exceptions import HTTPError @@ -29,7 +28,7 @@ @pytest.mark.tier1 -def test_positive_CRUD(default_os): +def test_positive_CRUD(default_os, target_sat): """Create a new Architecture with several attributes, update the name and delete the Architecture itself. @@ -43,13 +42,13 @@ def test_positive_CRUD(default_os): # Create name = gen_choice(list(valid_data_list().values())) - arch = entities.Architecture(name=name, operatingsystem=[default_os]).create() + arch = target_sat.api.Architecture(name=name, operatingsystem=[default_os]).create() assert {default_os.id} == {os.id for os in arch.operatingsystem} assert name == arch.name # Update name = gen_choice(list(valid_data_list().values())) - arch = entities.Architecture(id=arch.id, name=name).update(['name']) + arch = target_sat.api.Architecture(id=arch.id, name=name).update(['name']) assert name == arch.name # Delete @@ -60,7 +59,7 @@ def test_positive_CRUD(default_os): @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(invalid_names_list())) -def test_negative_create_with_invalid_name(name): +def test_negative_create_with_invalid_name(name, target_sat): """Create architecture providing an invalid initial name. :id: 0fa6377d-063a-4e24-b606-b342e0d9108b @@ -74,12 +73,12 @@ def test_negative_create_with_invalid_name(name): :BZ: 1401519 """ with pytest.raises(HTTPError): - entities.Architecture(name=name).create() + target_sat.api.Architecture(name=name).create() @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(invalid_names_list())) -def test_negative_update_with_invalid_name(name, module_architecture): +def test_negative_update_with_invalid_name(name, module_architecture, module_target_sat): """Update architecture's name to an invalid name. :id: cb27b69b-14e0-42d0-9e44-e09d68324803 @@ -91,6 +90,6 @@ def test_negative_update_with_invalid_name(name, module_architecture): :CaseImportance: Medium """ with pytest.raises(HTTPError): - entities.Architecture(id=module_architecture.id, name=name).update(['name']) - arch = entities.Architecture(id=module_architecture.id).read() + module_target_sat.api.Architecture(id=module_architecture.id, name=name).update(['name']) + arch = module_target_sat.api.Architecture(id=module_architecture.id).read() assert arch.name != name diff --git a/tests/foreman/api/test_audit.py b/tests/foreman/api/test_audit.py index 59b19d0806c..1aa9c28cc41 100644 --- a/tests/foreman/api/test_audit.py +++ b/tests/foreman/api/test_audit.py @@ -16,7 +16,6 @@ :Upstream: No """ -from nailgun import entities import pytest from robottelo.utils.datafactory import gen_string @@ -24,7 +23,7 @@ @pytest.mark.e2e @pytest.mark.tier1 -def test_positive_create_by_type(): +def test_positive_create_by_type(target_sat): """Create entities of different types and check audit logs for these events using entity type as search criteria @@ -40,45 +39,49 @@ def test_positive_create_by_type(): :CaseImportance: Medium """ for entity_item in [ - {'entity': entities.Architecture()}, + {'entity': target_sat.api.Architecture()}, { - 'entity': entities.AuthSourceLDAP(), + 'entity': target_sat.api.AuthSourceLDAP(), 'entity_type': 'auth_source', 'value_template': 'LDAP-{entity.name}', }, - {'entity': entities.ComputeProfile(), 'entity_type': 'compute_profile'}, + {'entity': target_sat.api.ComputeProfile(), 'entity_type': 'compute_profile'}, { - 'entity': entities.LibvirtComputeResource(), + 'entity': target_sat.api.LibvirtComputeResource(), 'entity_type': 'compute_resource', 'value_template': '{entity.name} (Libvirt)', }, - {'entity': entities.Domain()}, - {'entity': entities.Host()}, - {'entity': entities.HostGroup()}, - {'entity': entities.Image(compute_resource=entities.LibvirtComputeResource().create())}, - {'entity': entities.Location()}, - {'entity': entities.Media(), 'entity_type': 'medium'}, + {'entity': target_sat.api.Domain()}, + {'entity': target_sat.api.Host()}, + {'entity': target_sat.api.HostGroup()}, { - 'entity': entities.OperatingSystem(), + 'entity': target_sat.api.Image( + compute_resource=target_sat.api.LibvirtComputeResource().create() + ) + }, + {'entity': target_sat.api.Location()}, + {'entity': target_sat.api.Media(), 'entity_type': 'medium'}, + { + 'entity': target_sat.api.OperatingSystem(), 'entity_type': 'os', 'value_template': '{entity.name} {entity.major}', }, - {'entity': entities.PartitionTable(), 'entity_type': 'ptable'}, - {'entity': entities.Role()}, + {'entity': target_sat.api.PartitionTable(), 'entity_type': 'ptable'}, + {'entity': target_sat.api.Role()}, { - 'entity': entities.Subnet(), + 'entity': target_sat.api.Subnet(), 'value_template': '{entity.name} ({entity.network}/{entity.cidr})', }, - {'entity': entities.ProvisioningTemplate(), 'entity_type': 'provisioning_template'}, - {'entity': entities.User(), 'value_template': '{entity.login}'}, - {'entity': entities.UserGroup()}, - {'entity': entities.ContentView(), 'entity_type': 'katello/content_view'}, - {'entity': entities.LifecycleEnvironment(), 'entity_type': 'katello/kt_environment'}, - {'entity': entities.ActivationKey(), 'entity_type': 'katello/activation_key'}, - {'entity': entities.HostCollection(), 'entity_type': 'katello/host_collection'}, - {'entity': entities.Product(), 'entity_type': 'katello/product'}, + {'entity': target_sat.api.ProvisioningTemplate(), 'entity_type': 'provisioning_template'}, + {'entity': target_sat.api.User(), 'value_template': '{entity.login}'}, + {'entity': target_sat.api.UserGroup()}, + {'entity': target_sat.api.ContentView(), 'entity_type': 'katello/content_view'}, + {'entity': target_sat.api.LifecycleEnvironment(), 'entity_type': 'katello/kt_environment'}, + {'entity': target_sat.api.ActivationKey(), 'entity_type': 'katello/activation_key'}, + {'entity': target_sat.api.HostCollection(), 'entity_type': 'katello/host_collection'}, + {'entity': target_sat.api.Product(), 'entity_type': 'katello/product'}, { - 'entity': entities.SyncPlan(organization=entities.Organization(id=1)), + 'entity': target_sat.api.SyncPlan(organization=target_sat.api.Organization(id=1)), 'entity_type': 'katello/sync_plan', }, ]: @@ -86,7 +89,7 @@ def test_positive_create_by_type(): entity_type = entity_item.get('entity_type', created_entity.__class__.__name__.lower()) value_template = entity_item.get('value_template', '{entity.name}') entity_value = value_template.format(entity=created_entity) - audits = entities.Audit().search(query={'search': f'type={entity_type}'}) + audits = target_sat.api.Audit().search(query={'search': f'type={entity_type}'}) entity_audits = [entry for entry in audits if entry.auditable_name == entity_value] assert entity_audits, ( f'audit not found by name "{entity_value}" for entity: ' @@ -99,7 +102,7 @@ def test_positive_create_by_type(): @pytest.mark.tier1 -def test_positive_update_by_type(): +def test_positive_update_by_type(target_sat): """Update some entities of different types and check audit logs for these events using entity type as search criteria @@ -111,19 +114,19 @@ def test_positive_update_by_type(): :CaseImportance: Medium """ for entity in [ - entities.Architecture(), - entities.Domain(), - entities.HostGroup(), - entities.Location(), - entities.Role(), - entities.UserGroup(), + target_sat.api.Architecture(), + target_sat.api.Domain(), + target_sat.api.HostGroup(), + target_sat.api.Location(), + target_sat.api.Role(), + target_sat.api.UserGroup(), ]: created_entity = entity.create() name = created_entity.name new_name = gen_string('alpha') created_entity.name = new_name created_entity = created_entity.update(['name']) - audits = entities.Audit().search( + audits = target_sat.api.Audit().search( query={'search': f'type={created_entity.__class__.__name__.lower()}'} ) entity_audits = [entry for entry in audits if entry.auditable_name == name] @@ -136,7 +139,7 @@ def test_positive_update_by_type(): @pytest.mark.tier1 -def test_positive_delete_by_type(): +def test_positive_delete_by_type(target_sat): """Delete some entities of different types and check audit logs for these events using entity type as search criteria @@ -148,17 +151,17 @@ def test_positive_delete_by_type(): :CaseImportance: Medium """ for entity in [ - entities.Architecture(), - entities.Domain(), - entities.Host(), - entities.HostGroup(), - entities.Location(), - entities.Role(), - entities.UserGroup(), + target_sat.api.Architecture(), + target_sat.api.Domain(), + target_sat.api.Host(), + target_sat.api.HostGroup(), + target_sat.api.Location(), + target_sat.api.Role(), + target_sat.api.UserGroup(), ]: created_entity = entity.create() created_entity.delete() - audits = entities.Audit().search( + audits = target_sat.api.Audit().search( query={'search': f'type={created_entity.__class__.__name__.lower()}'} ) entity_audits = [entry for entry in audits if entry.auditable_name == created_entity.name] diff --git a/tests/foreman/api/test_bookmarks.py b/tests/foreman/api/test_bookmarks.py index 71ae30391ab..6ae674f7699 100644 --- a/tests/foreman/api/test_bookmarks.py +++ b/tests/foreman/api/test_bookmarks.py @@ -19,7 +19,6 @@ import random from fauxfactory import gen_string -from nailgun import entities import pytest from requests.exceptions import HTTPError @@ -32,7 +31,7 @@ @pytest.mark.tier1 @pytest.mark.parametrize('controller', CONTROLLERS) -def test_positive_create_with_name(controller): +def test_positive_create_with_name(controller, target_sat): """Create a bookmark :id: aeef0944-379a-4a27-902d-aa5969dbd441 @@ -51,14 +50,14 @@ def test_positive_create_with_name(controller): :CaseImportance: Critical """ name = random.choice(list(valid_data_list().values())) - bm = entities.Bookmark(controller=controller, name=name, public=False).create() + bm = target_sat.api.Bookmark(controller=controller, name=name, public=False).create() assert bm.controller == controller assert bm.name == name @pytest.mark.tier1 @pytest.mark.parametrize('controller', CONTROLLERS) -def test_positive_create_with_query(controller): +def test_positive_create_with_query(controller, target_sat): """Create a bookmark :id: 9fb6d485-92b5-43ea-b776-012c13734100 @@ -77,7 +76,7 @@ def test_positive_create_with_query(controller): :CaseImportance: Critical """ query = random.choice(list(valid_data_list().values())) - bm = entities.Bookmark(controller=controller, query=query).create() + bm = target_sat.api.Bookmark(controller=controller, query=query).create() assert bm.controller == controller assert bm.query == query @@ -85,7 +84,7 @@ def test_positive_create_with_query(controller): @pytest.mark.tier1 @pytest.mark.parametrize('public', (True, False)) @pytest.mark.parametrize('controller', CONTROLLERS) -def test_positive_create_public(controller, public): +def test_positive_create_public(controller, public, target_sat): """Create a public bookmark :id: 511b9bcf-0661-4e44-b1bc-475a1c207aa9 @@ -103,14 +102,14 @@ def test_positive_create_public(controller, public): :CaseImportance: Critical """ - bm = entities.Bookmark(controller=controller, public=public).create() + bm = target_sat.api.Bookmark(controller=controller, public=public).create() assert bm.controller == controller assert bm.public == public @pytest.mark.tier1 @pytest.mark.parametrize('controller', CONTROLLERS) -def test_negative_create_with_invalid_name(controller): +def test_negative_create_with_invalid_name(controller, target_sat): """Create a bookmark with invalid name :id: 9a79c561-8225-43fc-8ec7-b6858e9665e2 @@ -131,14 +130,14 @@ def test_negative_create_with_invalid_name(controller): """ name = random.choice(invalid_values_list()) with pytest.raises(HTTPError): - entities.Bookmark(controller=controller, name=name, public=False).create() - result = entities.Bookmark().search(query={'search': f'name="{name}"'}) + target_sat.api.Bookmark(controller=controller, name=name, public=False).create() + result = target_sat.api.Bookmark().search(query={'search': f'name="{name}"'}) assert len(result) == 0 @pytest.mark.tier1 @pytest.mark.parametrize('controller', CONTROLLERS) -def test_negative_create_empty_query(controller): +def test_negative_create_empty_query(controller, target_sat): """Create a bookmark with empty query :id: 674d569f-6f86-43ba-b9cc-f43e05e8ab1c @@ -159,14 +158,14 @@ def test_negative_create_empty_query(controller): """ name = gen_string('alpha') with pytest.raises(HTTPError): - entities.Bookmark(controller=controller, name=name, query='').create() - result = entities.Bookmark().search(query={'search': f'name="{name}"'}) + target_sat.api.Bookmark(controller=controller, name=name, query='').create() + result = target_sat.api.Bookmark().search(query={'search': f'name="{name}"'}) assert len(result) == 0 @pytest.mark.tier1 @pytest.mark.parametrize('controller', CONTROLLERS) -def test_negative_create_same_name(controller): +def test_negative_create_same_name(controller, target_sat): """Create bookmarks with the same names :id: f78f6e97-da77-4a61-95c2-622c439d325d @@ -187,16 +186,16 @@ def test_negative_create_same_name(controller): :CaseImportance: Critical """ name = gen_string('alphanumeric') - entities.Bookmark(controller=controller, name=name).create() + target_sat.api.Bookmark(controller=controller, name=name).create() with pytest.raises(HTTPError): - entities.Bookmark(controller=controller, name=name).create() - result = entities.Bookmark().search(query={'search': f'name="{name}"'}) + target_sat.api.Bookmark(controller=controller, name=name).create() + result = target_sat.api.Bookmark().search(query={'search': f'name="{name}"'}) assert len(result) == 1 @pytest.mark.tier1 @pytest.mark.parametrize('controller', CONTROLLERS) -def test_negative_create_null_public(controller): +def test_negative_create_null_public(controller, target_sat): """Create a bookmark omitting the public parameter :id: 0a4cb5ea-912b-445e-a874-b345e43d3eac @@ -220,14 +219,14 @@ def test_negative_create_null_public(controller): """ name = gen_string('alphanumeric') with pytest.raises(HTTPError): - entities.Bookmark(controller=controller, name=name, public=None).create() - result = entities.Bookmark().search(query={'search': f'name="{name}"'}) + target_sat.api.Bookmark(controller=controller, name=name, public=None).create() + result = target_sat.api.Bookmark().search(query={'search': f'name="{name}"'}) assert len(result) == 0 @pytest.mark.tier1 @pytest.mark.parametrize('controller', CONTROLLERS) -def test_positive_update_name(controller): +def test_positive_update_name(controller, target_sat): """Update a bookmark :id: 1cde270a-26fb-4cff-bdff-89fef17a7624 @@ -246,7 +245,7 @@ def test_positive_update_name(controller): :CaseImportance: Critical """ new_name = random.choice(list(valid_data_list().values())) - bm = entities.Bookmark(controller=controller, public=False).create() + bm = target_sat.api.Bookmark(controller=controller, public=False).create() bm.name = new_name bm = bm.update(['name']) assert bm.name == new_name @@ -254,7 +253,7 @@ def test_positive_update_name(controller): @pytest.mark.tier1 @pytest.mark.parametrize('controller', CONTROLLERS) -def test_negative_update_same_name(controller): +def test_negative_update_same_name(controller, target_sat): """Update a bookmark with name already taken :id: 6becf121-2bea-4f7e-98f4-338bd88b8f4b @@ -274,8 +273,8 @@ def test_negative_update_same_name(controller): :CaseImportance: Critical """ name = gen_string('alphanumeric') - entities.Bookmark(controller=controller, name=name).create() - bm = entities.Bookmark(controller=controller).create() + target_sat.api.Bookmark(controller=controller, name=name).create() + bm = target_sat.api.Bookmark(controller=controller).create() bm.name = name with pytest.raises(HTTPError): bm.update(['name']) @@ -285,7 +284,7 @@ def test_negative_update_same_name(controller): @pytest.mark.tier1 @pytest.mark.parametrize('controller', CONTROLLERS) -def test_negative_update_invalid_name(controller): +def test_negative_update_invalid_name(controller, target_sat): """Update a bookmark with an invalid name :id: 479795bb-aeed-45b3-a7e3-d3449c808087 @@ -304,7 +303,7 @@ def test_negative_update_invalid_name(controller): :CaseImportance: Critical """ new_name = random.choice(invalid_values_list()) - bm = entities.Bookmark(controller=controller, public=False).create() + bm = target_sat.api.Bookmark(controller=controller, public=False).create() bm.name = new_name with pytest.raises(HTTPError): bm.update(['name']) @@ -314,7 +313,7 @@ def test_negative_update_invalid_name(controller): @pytest.mark.tier1 @pytest.mark.parametrize('controller', CONTROLLERS) -def test_positive_update_query(controller): +def test_positive_update_query(controller, target_sat): """Update a bookmark query :id: 92a31de2-bebf-4396-94f5-adf59f8d66a5 @@ -333,7 +332,7 @@ def test_positive_update_query(controller): :CaseImportance: Critical """ new_query = random.choice(list(valid_data_list().values())) - bm = entities.Bookmark(controller=controller).create() + bm = target_sat.api.Bookmark(controller=controller).create() bm.query = new_query bm = bm.update(['query']) assert bm.query == new_query @@ -341,7 +340,7 @@ def test_positive_update_query(controller): @pytest.mark.tier1 @pytest.mark.parametrize('controller', CONTROLLERS) -def test_negative_update_empty_query(controller): +def test_negative_update_empty_query(controller, target_sat): """Update a bookmark with an empty query :id: 948602d3-532a-47fe-b313-91e3fab809bf @@ -359,7 +358,7 @@ def test_negative_update_empty_query(controller): :CaseImportance: Critical """ - bm = entities.Bookmark(controller=controller).create() + bm = target_sat.api.Bookmark(controller=controller).create() bm.query = '' with pytest.raises(HTTPError): bm.update(['query']) @@ -370,7 +369,7 @@ def test_negative_update_empty_query(controller): @pytest.mark.tier1 @pytest.mark.parametrize('public', (True, False)) @pytest.mark.parametrize('controller', CONTROLLERS) -def test_positive_update_public(controller, public): +def test_positive_update_public(controller, public, target_sat): """Update a bookmark public state to private and vice versa :id: 2717360d-37c4-4bb9-bce1-b1edabdf11b3 @@ -389,7 +388,7 @@ def test_positive_update_public(controller, public): :CaseImportance: Critical """ - bm = entities.Bookmark(controller=controller, public=not public).create() + bm = target_sat.api.Bookmark(controller=controller, public=not public).create() assert bm.public != public bm.public = public bm = bm.update(['public']) diff --git a/tests/foreman/api/test_capsulecontent.py b/tests/foreman/api/test_capsulecontent.py index 900d0803aed..2a48b2a434c 100644 --- a/tests/foreman/api/test_capsulecontent.py +++ b/tests/foreman/api/test_capsulecontent.py @@ -21,7 +21,7 @@ import re from time import sleep -from nailgun import client, entities +from nailgun import client from nailgun.entity_mixins import call_entity_method_with_timeout import pytest @@ -43,9 +43,13 @@ class TestCapsuleContentManagement: interactions and use capsule. """ - def update_capsule_download_policy(self, module_capsule_configured, download_policy): + def update_capsule_download_policy( + self, module_capsule_configured, download_policy, module_target_sat + ): """Updates capsule's download policy to desired value""" - proxy = entities.SmartProxy(id=module_capsule_configured.nailgun_capsule.id).read() + proxy = module_target_sat.api.SmartProxy( + id=module_capsule_configured.nailgun_capsule.id + ).read() proxy.download_policy = download_policy proxy.update(['download_policy']) @@ -78,7 +82,12 @@ def test_positive_insights_puppet_package_availability(self, module_capsule_conf @pytest.mark.tier4 @pytest.mark.skip_if_not_set('capsule', 'clients', 'fake_manifest') def test_positive_uploaded_content_library_sync( - self, module_capsule_configured, function_org, function_product, function_lce_library + self, + module_capsule_configured, + function_org, + function_product, + function_lce_library, + target_sat, ): """Ensure custom repo with no upstream url and manually uploaded content after publishing to Library is synchronized to capsule @@ -92,7 +101,7 @@ def test_positive_uploaded_content_library_sync( :expectedresults: custom content is present on external capsule """ - repo = entities.Repository(product=function_product, url=None).create() + repo = target_sat.api.Repository(product=function_product, url=None).create() # Associate the lifecycle environment with the capsule module_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( data={'environment_id': function_lce_library.id} @@ -103,7 +112,7 @@ def test_positive_uploaded_content_library_sync( assert function_lce_library.id in [capsule_lce['id'] for capsule_lce in result['results']] # Create a content view with the repository - cv = entities.ContentView(organization=function_org, repository=[repo]).create() + cv = target_sat.api.ContentView(organization=function_org, repository=[repo]).create() # Upload custom content into the repo with open(DataFile.RPM_TO_UPLOAD, 'rb') as handle: @@ -134,7 +143,7 @@ def test_positive_uploaded_content_library_sync( @pytest.mark.tier4 @pytest.mark.skip_if_not_set('capsule', 'clients', 'fake_manifest') def test_positive_checksum_sync( - self, module_capsule_configured, function_org, function_product, function_lce + self, module_capsule_configured, function_org, function_product, function_lce, target_sat ): """Synchronize repository to capsule, update repository's checksum type, trigger capsule sync and make sure checksum type was updated on @@ -152,7 +161,7 @@ def test_positive_checksum_sync( :CaseImportance: Critical """ # Create repository with sha256 checksum type - repo = entities.Repository( + repo = target_sat.api.Repository( product=function_product, checksum_type='sha256', mirroring_policy='additive', @@ -168,7 +177,7 @@ def test_positive_checksum_sync( assert function_lce.id in [capsule_lce['id'] for capsule_lce in result['results']] # Sync, publish and promote a repo - cv = entities.ContentView(organization=function_org, repository=[repo]).create() + cv = target_sat.api.ContentView(organization=function_org, repository=[repo]).create() repo.sync() repo = repo.read() cv.publish() @@ -228,7 +237,12 @@ def test_positive_checksum_sync( @pytest.mark.tier4 @pytest.mark.skip_if_not_set('capsule') def test_positive_sync_updated_repo( - self, target_sat, module_capsule_configured, function_org, function_product, function_lce + self, + target_sat, + module_capsule_configured, + function_org, + function_product, + function_lce, ): """Sync a custom repo with no upstream url but uploaded content to the Capsule via promoted CV, update content of the repo, publish and promote the CV again, resync @@ -256,7 +270,7 @@ def test_positive_sync_updated_repo( :BZ: 2025494 """ - repo = entities.Repository(url=None, product=function_product).create() + repo = target_sat.api.Repository(url=None, product=function_product).create() # Associate the lifecycle environment with the capsule module_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( @@ -274,7 +288,7 @@ def test_positive_sync_updated_repo( assert repo.read().content_counts['rpm'] == 1 # Create, publish and promote CV with the repository to the Capsule's LCE - cv = entities.ContentView(organization=function_org, repository=[repo]).create() + cv = target_sat.api.ContentView(organization=function_org, repository=[repo]).create() cv.publish() cv = cv.read() assert len(cv.version) == 1 @@ -329,7 +343,12 @@ def test_positive_sync_updated_repo( @pytest.mark.tier4 @pytest.mark.skip_if_not_set('capsule', 'clients', 'fake_manifest') def test_positive_capsule_sync( - self, target_sat, module_capsule_configured, function_org, function_product, function_lce + self, + target_sat, + module_capsule_configured, + function_org, + function_product, + function_lce, ): """Create repository, add it to lifecycle environment, assign lifecycle environment with a capsule, sync repository, sync it once again, update @@ -353,7 +372,7 @@ def test_positive_capsule_sync( capsule """ repo_url = settings.repos.yum_1.url - repo = entities.Repository(product=function_product, url=repo_url).create() + repo = target_sat.api.Repository(product=function_product, url=repo_url).create() # Associate the lifecycle environment with the capsule module_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( data={'environment_id': function_lce.id} @@ -364,7 +383,7 @@ def test_positive_capsule_sync( assert function_lce.id in [capsule_lce['id'] for capsule_lce in result['results']] # Create a content view with the repository - cv = entities.ContentView(organization=function_org, repository=[repo]).create() + cv = target_sat.api.ContentView(organization=function_org, repository=[repo]).create() # Sync repository repo.sync() repo = repo.read() @@ -491,12 +510,12 @@ def test_positive_iso_library_sync( reposet=constants.REPOSET['rhsc7_iso'], releasever=None, ) - rh_repo = entities.Repository(id=rh_repo_id).read() + rh_repo = module_target_sat.api.Repository(id=rh_repo_id).read() call_entity_method_with_timeout(rh_repo.sync, timeout=2500) # Find "Library" lifecycle env for specific organization - lce = entities.LifecycleEnvironment(organization=module_entitlement_manifest_org).search( - query={'search': f'name={constants.ENVIRONMENT}'} - )[0] + lce = module_target_sat.api.LifecycleEnvironment( + organization=module_entitlement_manifest_org + ).search(query={'search': f'name={constants.ENVIRONMENT}'})[0] # Associate the lifecycle environment with the capsule module_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( @@ -508,7 +527,7 @@ def test_positive_iso_library_sync( assert lce.id in [capsule_lce['id'] for capsule_lce in result['results']] # Create a content view with the repository - cv = entities.ContentView( + cv = module_target_sat.api.ContentView( organization=module_entitlement_manifest_org, repository=[rh_repo] ).create() # Publish new version of the content view @@ -536,7 +555,12 @@ def test_positive_iso_library_sync( @pytest.mark.tier4 @pytest.mark.skip_if_not_set('capsule', 'clients', 'fake_manifest') def test_positive_on_demand_sync( - self, target_sat, module_capsule_configured, function_org, function_product, function_lce + self, + target_sat, + module_capsule_configured, + function_org, + function_product, + function_lce, ): """Create a repository with 'on_demand' policy, add it to a CV, promote to an 'on_demand' Capsule's LCE, download a published package, @@ -555,7 +579,7 @@ def test_positive_on_demand_sync( repo_url = settings.repos.yum_3.url packages_count = constants.FAKE_3_YUM_REPOS_COUNT package = constants.FAKE_3_YUM_REPO_RPMS[0] - repo = entities.Repository( + repo = target_sat.api.Repository( download_policy='on_demand', mirroring_policy='mirror_complete', product=function_product, @@ -574,7 +598,7 @@ def test_positive_on_demand_sync( self.update_capsule_download_policy(module_capsule_configured, 'on_demand') # Create a content view with the repository - cv = entities.ContentView(organization=function_org, repository=[repo]).create() + cv = target_sat.api.ContentView(organization=function_org, repository=[repo]).create() # Sync repository repo.sync() repo = repo.read() @@ -616,7 +640,12 @@ def test_positive_on_demand_sync( @pytest.mark.tier4 @pytest.mark.skip_if_not_set('capsule', 'clients', 'fake_manifest') def test_positive_update_with_immediate_sync( - self, target_sat, module_capsule_configured, function_org, function_product, function_lce + self, + target_sat, + module_capsule_configured, + function_org, + function_product, + function_lce, ): """Create a repository with on_demand download policy, associate it with capsule, sync repo, update download policy to immediate, sync once @@ -633,7 +662,7 @@ def test_positive_update_with_immediate_sync( """ repo_url = settings.repos.yum_1.url packages_count = constants.FAKE_1_YUM_REPOS_COUNT - repo = entities.Repository( + repo = target_sat.api.Repository( download_policy='on_demand', mirroring_policy='mirror_complete', product=function_product, @@ -651,7 +680,7 @@ def test_positive_update_with_immediate_sync( assert function_lce.id in [capsule_lce['id'] for capsule_lce in result['results']] # Create a content view with the repository - cv = entities.ContentView(organization=function_org, repository=[repo]).create() + cv = target_sat.api.ContentView(organization=function_org, repository=[repo]).create() # Sync repository repo.sync() repo = repo.read() @@ -765,8 +794,10 @@ def test_positive_sync_kickstart_repo( repo=constants.REPOS['kickstart'][distro]['name'], releasever=constants.REPOS['kickstart'][distro]['version'], ) - repo = entities.Repository(id=repo_id).read() - lce = entities.LifecycleEnvironment(organization=function_entitlement_manifest_org).create() + repo = target_sat.api.Repository(id=repo_id).read() + lce = target_sat.api.LifecycleEnvironment( + organization=function_entitlement_manifest_org + ).create() # Associate the lifecycle environment with the capsule module_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( data={'environment_id': lce.id} @@ -780,7 +811,7 @@ def test_positive_sync_kickstart_repo( self.update_capsule_download_policy(module_capsule_configured, 'on_demand') # Create a content view with the repository - cv = entities.ContentView( + cv = target_sat.api.ContentView( organization=function_entitlement_manifest_org, repository=[repo] ).create() # Sync repository @@ -868,7 +899,7 @@ def test_positive_sync_container_repo_end_to_end( repos = [] for ups_name in upstream_names: - repo = entities.Repository( + repo = target_sat.api.Repository( content_type='docker', docker_upstream_name=ups_name, product=function_product, @@ -886,7 +917,7 @@ def test_positive_sync_container_repo_end_to_end( assert function_lce.id in [capsule_lce['id'] for capsule_lce in result['results']] # Create and publish a content view with all repositories - cv = entities.ContentView(organization=function_org, repository=repos).create() + cv = target_sat.api.ContentView(organization=function_org, repository=repos).create() cv.publish() cv = cv.read() assert len(cv.version) == 1 @@ -995,7 +1026,7 @@ def test_positive_sync_collection_repo( - name: theforeman.operations version: "0.1.0" ''' - repo = entities.Repository( + repo = target_sat.api.Repository( content_type='ansible_collection', ansible_collection_requirements=requirements, product=function_product, @@ -1066,7 +1097,7 @@ def test_positive_sync_file_repo( :BZ: 1985122 """ - repo = entities.Repository( + repo = target_sat.api.Repository( content_type='file', product=function_product, url=constants.FAKE_FILE_LARGE_URL, @@ -1086,7 +1117,7 @@ def test_positive_sync_file_repo( assert function_lce.id in [capsule_lce['id'] for capsule_lce in result['results']] # Create and publish a content view with all repositories - cv = entities.ContentView(organization=function_org, repository=[repo]).create() + cv = target_sat.api.ContentView(organization=function_org, repository=[repo]).create() cv.publish() cv = cv.read() assert len(cv.version) == 1 diff --git a/tests/foreman/api/test_computeprofile.py b/tests/foreman/api/test_computeprofile.py index d0ee003c7a3..dffe47620b4 100644 --- a/tests/foreman/api/test_computeprofile.py +++ b/tests/foreman/api/test_computeprofile.py @@ -16,7 +16,6 @@ :Upstream: No """ -from nailgun import entities import pytest from requests.exceptions import HTTPError @@ -29,7 +28,7 @@ @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_create_with_name(name): +def test_positive_create_with_name(name, target_sat): """Create new Compute Profile using different names :id: 97d04911-9368-4674-92c7-1e3ff114bc18 @@ -42,13 +41,13 @@ def test_positive_create_with_name(name): :parametrized: yes """ - profile = entities.ComputeProfile(name=name).create() + profile = target_sat.api.ComputeProfile(name=name).create() assert name == profile.name @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) @pytest.mark.tier1 -def test_negative_create(name): +def test_negative_create(name, target_sat): """Attempt to create Compute Profile using invalid names only :id: 2d34a1fd-70a5-4e59-b2e2-86fbfe8e31ab @@ -62,12 +61,12 @@ def test_negative_create(name): :parametrized: yes """ with pytest.raises(HTTPError): - entities.ComputeProfile(name=name).create() + target_sat.api.ComputeProfile(name=name).create() @pytest.mark.parametrize('new_name', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_update_name(new_name): +def test_positive_update_name(new_name, target_sat): """Update selected Compute Profile entity using proper names :id: c79193d7-2e0f-4ed9-b947-05feeddabfda @@ -80,15 +79,15 @@ def test_positive_update_name(new_name): :parametrized: yes """ - profile = entities.ComputeProfile().create() - entities.ComputeProfile(id=profile.id, name=new_name).update(['name']) - updated_profile = entities.ComputeProfile(id=profile.id).read() + profile = target_sat.api.ComputeProfile().create() + target_sat.api.ComputeProfile(id=profile.id, name=new_name).update(['name']) + updated_profile = target_sat.api.ComputeProfile(id=profile.id).read() assert new_name == updated_profile.name @pytest.mark.parametrize('new_name', **parametrized(invalid_values_list())) @pytest.mark.tier1 -def test_negative_update_name(new_name): +def test_negative_update_name(new_name, target_sat): """Attempt to update Compute Profile entity using invalid names only :id: 042b40d5-a78b-4e65-b5cb-5b270b800b37 @@ -101,16 +100,16 @@ def test_negative_update_name(new_name): :parametrized: yes """ - profile = entities.ComputeProfile().create() + profile = target_sat.api.ComputeProfile().create() with pytest.raises(HTTPError): - entities.ComputeProfile(id=profile.id, name=new_name).update(['name']) - updated_profile = entities.ComputeProfile(id=profile.id).read() + target_sat.api.ComputeProfile(id=profile.id, name=new_name).update(['name']) + updated_profile = target_sat.api.ComputeProfile(id=profile.id).read() assert new_name != updated_profile.name @pytest.mark.parametrize('new_name', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_delete(new_name): +def test_positive_delete(new_name, target_sat): """Delete Compute Profile entity :id: 0a620e23-7ba6-4178-af7a-fd1e332f478f @@ -123,7 +122,7 @@ def test_positive_delete(new_name): :parametrized: yes """ - profile = entities.ComputeProfile(name=new_name).create() + profile = target_sat.api.ComputeProfile(name=new_name).create() profile.delete() with pytest.raises(HTTPError): - entities.ComputeProfile(id=profile.id).read() + target_sat.api.ComputeProfile(id=profile.id).read() diff --git a/tests/foreman/api/test_contentcredentials.py b/tests/foreman/api/test_contentcredentials.py index 81602c953e4..aaad0322976 100644 --- a/tests/foreman/api/test_contentcredentials.py +++ b/tests/foreman/api/test_contentcredentials.py @@ -19,7 +19,6 @@ from copy import copy from fauxfactory import gen_string -from nailgun import entities import pytest from requests import HTTPError @@ -35,7 +34,7 @@ @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_create_with_name(module_org, name): +def test_positive_create_with_name(module_org, name, module_target_sat): """Create a GPG key with valid name. :id: 741d969b-28ef-481f-bcf7-ed4cd920b030 @@ -46,12 +45,12 @@ def test_positive_create_with_name(module_org, name): :CaseImportance: Critical """ - gpg_key = entities.GPGKey(organization=module_org, name=name).create() + gpg_key = module_target_sat.api.GPGKey(organization=module_org, name=name).create() assert name == gpg_key.name @pytest.mark.tier1 -def test_positive_create_with_content(module_org): +def test_positive_create_with_content(module_org, module_target_sat): """Create a GPG key with valid name and valid gpg key text. :id: cfa6690e-fed7-49cf-94f9-fd2deed941c0 @@ -60,13 +59,13 @@ def test_positive_create_with_content(module_org): :CaseImportance: Critical """ - gpg_key = entities.GPGKey(organization=module_org, content=key_content).create() + gpg_key = module_target_sat.api.GPGKey(organization=module_org, content=key_content).create() assert key_content == gpg_key.content @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) @pytest.mark.tier1 -def test_negative_create_name(module_org, name): +def test_negative_create_name(module_org, name, module_target_sat): """Attempt to create GPG key with invalid names only. :id: 904a3ed0-7d50-495e-a700-b4f1ae913599 @@ -78,13 +77,13 @@ def test_negative_create_name(module_org, name): :CaseImportance: Critical """ with pytest.raises(HTTPError) as error: - entities.GPGKey(organization=module_org, name=name).create() + module_target_sat.api.GPGKey(organization=module_org, name=name).create() assert error.value.response.status_code == 422 assert 'Validation failed:' in error.value.response.text @pytest.mark.tier1 -def test_negative_create_with_same_name(module_org): +def test_negative_create_with_same_name(module_org, module_target_sat): """Attempt to create a GPG key providing a name of already existent entity @@ -95,15 +94,15 @@ def test_negative_create_with_same_name(module_org): :CaseImportance: Critical """ name = gen_string('alphanumeric') - entities.GPGKey(organization=module_org, name=name).create() + module_target_sat.api.GPGKey(organization=module_org, name=name).create() with pytest.raises(HTTPError) as error: - entities.GPGKey(organization=module_org, name=name).create() + module_target_sat.api.GPGKey(organization=module_org, name=name).create() assert error.value.response.status_code == 422 assert 'Validation failed:' in error.value.response.text @pytest.mark.tier1 -def test_negative_create_with_content(module_org): +def test_negative_create_with_content(module_org, module_target_sat): """Attempt to create GPG key with empty content. :id: fc79c840-6bcb-4d97-9145-c0008d5b028d @@ -113,14 +112,14 @@ def test_negative_create_with_content(module_org): :CaseImportance: Critical """ with pytest.raises(HTTPError) as error: - entities.GPGKey(organization=module_org, content='').create() + module_target_sat.api.GPGKey(organization=module_org, content='').create() assert error.value.response.status_code == 422 assert 'Validation failed:' in error.value.response.text @pytest.mark.parametrize('new_name', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_update_name(module_org, new_name): +def test_positive_update_name(module_org, new_name, module_target_sat): """Update GPG key name to another valid name. :id: 9868025d-5346-42c9-b850-916ce37a9541 @@ -131,14 +130,14 @@ def test_positive_update_name(module_org, new_name): :CaseImportance: Critical """ - gpg_key = entities.GPGKey(organization=module_org).create() + gpg_key = module_target_sat.api.GPGKey(organization=module_org).create() gpg_key.name = new_name gpg_key = gpg_key.update(['name']) assert new_name == gpg_key.name @pytest.mark.tier1 -def test_positive_update_content(module_org): +def test_positive_update_content(module_org, module_target_sat): """Update GPG key content text to another valid one. :id: 62fdaf55-c931-4be6-9857-68cc816046ad @@ -147,7 +146,7 @@ def test_positive_update_content(module_org): :CaseImportance: Critical """ - gpg_key = entities.GPGKey( + gpg_key = module_target_sat.api.GPGKey( organization=module_org, content=DataFile.VALID_GPG_KEY_BETA_FILE.read_text(), ).create() @@ -158,7 +157,7 @@ def test_positive_update_content(module_org): @pytest.mark.parametrize('new_name', **parametrized(invalid_values_list())) @pytest.mark.tier1 -def test_negative_update_name(module_org, new_name): +def test_negative_update_name(module_org, new_name, module_target_sat): """Attempt to update GPG key name to invalid one :id: 1a43f610-8969-4f08-967f-fb6af0fca31b @@ -169,7 +168,7 @@ def test_negative_update_name(module_org, new_name): :CaseImportance: Critical """ - gpg_key = entities.GPGKey(organization=module_org).create() + gpg_key = module_target_sat.api.GPGKey(organization=module_org).create() gpg_key.name = new_name with pytest.raises(HTTPError) as error: gpg_key.update(['name']) @@ -178,7 +177,7 @@ def test_negative_update_name(module_org, new_name): @pytest.mark.tier1 -def test_negative_update_same_name(module_org): +def test_negative_update_same_name(module_org, module_target_sat): """Attempt to update GPG key name to the name of existing GPG key entity @@ -189,8 +188,8 @@ def test_negative_update_same_name(module_org): :CaseImportance: Critical """ name = gen_string('alpha') - entities.GPGKey(organization=module_org, name=name).create() - new_gpg_key = entities.GPGKey(organization=module_org).create() + module_target_sat.api.GPGKey(organization=module_org, name=name).create() + new_gpg_key = module_target_sat.api.GPGKey(organization=module_org).create() new_gpg_key.name = name with pytest.raises(HTTPError) as error: new_gpg_key.update(['name']) @@ -199,7 +198,7 @@ def test_negative_update_same_name(module_org): @pytest.mark.tier1 -def test_negative_update_content(module_org): +def test_negative_update_content(module_org, module_target_sat): """Attempt to update GPG key content to invalid one :id: fee30ef8-370a-4fdd-9e45-e7ab95dade8b @@ -208,7 +207,7 @@ def test_negative_update_content(module_org): :CaseImportance: Critical """ - gpg_key = entities.GPGKey(organization=module_org, content=key_content).create() + gpg_key = module_target_sat.api.GPGKey(organization=module_org, content=key_content).create() gpg_key.content = '' with pytest.raises(HTTPError) as error: gpg_key.update(['content']) @@ -218,7 +217,7 @@ def test_negative_update_content(module_org): @pytest.mark.tier1 -def test_positive_delete(module_org): +def test_positive_delete(module_org, module_target_sat): """Create a GPG key with different names and then delete it. :id: b06d211f-2827-40f7-b627-8b1fbaee2eb4 @@ -227,7 +226,7 @@ def test_positive_delete(module_org): :CaseImportance: Critical """ - gpg_key = entities.GPGKey(organization=module_org).create() + gpg_key = module_target_sat.api.GPGKey(organization=module_org).create() gpg_key.delete() with pytest.raises(HTTPError): gpg_key.read() diff --git a/tests/foreman/api/test_contentview.py b/tests/foreman/api/test_contentview.py index 0e1e94c88d4..bab306e29e0 100644 --- a/tests/foreman/api/test_contentview.py +++ b/tests/foreman/api/test_contentview.py @@ -19,7 +19,6 @@ import random from fauxfactory import gen_integer, gen_string, gen_utf8 -from nailgun import entities import pytest from requests.exceptions import HTTPError @@ -48,8 +47,8 @@ @pytest.fixture(scope='class') -def class_cv(module_org): - return entities.ContentView(organization=module_org).create() +def class_cv(module_org, class_target_sat): + return class_target_sat.api.ContentView(organization=module_org).create() @pytest.fixture(scope='class') @@ -65,22 +64,22 @@ def class_promoted_cv(class_published_cv, module_lce): @pytest.fixture(scope='class') -def class_cloned_cv(class_cv): - copied_cv_id = entities.ContentView(id=class_cv.id).copy( +def class_cloned_cv(class_cv, class_target_sat): + copied_cv_id = class_target_sat.api.ContentView(id=class_cv.id).copy( data={'name': gen_string('alpha', gen_integer(3, 30))} )['id'] - return entities.ContentView(id=copied_cv_id).read() + return class_target_sat.api.ContentView(id=copied_cv_id).read() @pytest.fixture(scope='class') -def class_published_cloned_cv(class_cloned_cv): +def class_published_cloned_cv(class_cloned_cv, class_target_sat): class_cloned_cv.publish() - return entities.ContentView(id=class_cloned_cv.id).read() + return class_target_sat.api.ContentView(id=class_cloned_cv.id).read() @pytest.fixture -def content_view(module_org): - return entities.ContentView(organization=module_org).create() +def content_view(module_org, module_target_sat): + return module_target_sat.api.ContentView(organization=module_org).create() def apply_package_filter(content_view, repo, package, target_sat, inclusion=True): @@ -93,7 +92,7 @@ def apply_package_filter(content_view, repo, package, target_sat, inclusion=True :return list : list of content view versions """ - cv_filter = entities.RPMContentViewFilter( + cv_filter = target_sat.api.RPMContentViewFilter( content_view=content_view, inclusion=inclusion, repository=[repo] ).create() cv_filter_rule = target_sat.api.ContentViewFilterRule( @@ -109,7 +108,9 @@ def apply_package_filter(content_view, repo, package, target_sat, inclusion=True class TestContentView: @pytest.mark.upgrade @pytest.mark.tier3 - def test_positive_subscribe_host(self, class_cv, class_promoted_cv, module_lce, module_org): + def test_positive_subscribe_host( + self, class_cv, class_promoted_cv, module_lce, module_org, module_target_sat + ): """Subscribe a host to a content view :id: b5a08369-bf92-48ab-b9aa-10f5b9774b79 @@ -130,7 +131,7 @@ def test_positive_subscribe_host(self, class_cv, class_promoted_cv, module_lce, # Check that no host associated to just created content view assert class_cv.content_host_count == 0 assert len(class_promoted_cv.version) == 1 - host = entities.Host( + host = module_target_sat.api.Host( content_facet_attributes={ 'content_view_id': class_cv.id, 'lifecycle_environment_id': module_lce.id, @@ -160,7 +161,9 @@ def test_positive_clone_within_same_env(self, class_published_cloned_cv, module_ @pytest.mark.upgrade @pytest.mark.tier2 - def test_positive_clone_with_diff_env(self, module_org, class_published_cloned_cv): + def test_positive_clone_with_diff_env( + self, module_org, class_published_cloned_cv, module_target_sat + ): """attempt to create, publish and promote new content view based on existing view but promoted to a different environment @@ -174,11 +177,11 @@ def test_positive_clone_with_diff_env(self, module_org, class_published_cloned_c :CaseImportance: Medium """ - le_clone = entities.LifecycleEnvironment(organization=module_org).create() + le_clone = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() class_published_cloned_cv.read().version[0].promote(data={'environment_ids': le_clone.id}) @pytest.mark.tier2 - def test_positive_add_custom_content(self, module_product, module_org): + def test_positive_add_custom_content(self, module_product, module_org, module_target_sat): """Associate custom content in a view :id: db452e0c-0c17-40f2-bab4-8467e7a875f1 @@ -189,9 +192,9 @@ def test_positive_add_custom_content(self, module_product, module_org): :CaseImportance: Critical """ - yum_repo = entities.Repository(product=module_product).create() + yum_repo = module_target_sat.api.Repository(product=module_product).create() yum_repo.sync() - content_view = entities.ContentView(organization=module_org.id).create() + content_view = module_target_sat.api.ContentView(organization=module_org.id).create() assert len(content_view.repository) == 0 content_view.repository = [yum_repo] content_view = content_view.update(['repository']) @@ -202,7 +205,9 @@ def test_positive_add_custom_content(self, module_product, module_org): @pytest.mark.skipif( (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) - def test_positive_add_custom_module_streams(self, content_view, module_product, module_org): + def test_positive_add_custom_module_streams( + self, content_view, module_product, module_org, module_target_sat + ): """Associate custom content (module streams) in a view :id: 9e4821cb-293a-4d84-bd1f-bb9fff36b143 @@ -213,7 +218,7 @@ def test_positive_add_custom_module_streams(self, content_view, module_product, :CaseImportance: High """ - yum_repo = entities.Repository( + yum_repo = module_target_sat.api.Repository( product=module_product, url=settings.repos.module_stream_1.url ).create() yum_repo.sync() @@ -226,7 +231,9 @@ def test_positive_add_custom_module_streams(self, content_view, module_product, assert repo.content_counts['module_stream'] == 7 @pytest.mark.tier2 - def test_negative_add_dupe_repos(self, content_view, module_product, module_org): + def test_negative_add_dupe_repos( + self, content_view, module_product, module_org, module_target_sat + ): """Attempt to associate the same repo multiple times within a content view @@ -238,7 +245,7 @@ def test_negative_add_dupe_repos(self, content_view, module_product, module_org) :CaseImportance: Low """ - yum_repo = entities.Repository(product=module_product).create() + yum_repo = module_target_sat.api.Repository(product=module_product).create() yum_repo.sync() assert len(content_view.repository) == 0 with pytest.raises(HTTPError): @@ -251,7 +258,7 @@ def test_negative_add_dupe_repos(self, content_view, module_product, module_org) @pytest.mark.skipif( (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) - def test_positive_add_sha512_rpm(self, content_view, module_org): + def test_positive_add_sha512_rpm(self, content_view, module_org, module_target_sat): """Associate sha512 RPM content in a view :id: 1f473b02-5e2b-41ff-a706-c0635abc2476 @@ -270,8 +277,10 @@ def test_positive_add_sha512_rpm(self, content_view, module_org): :BZ: 1639406 """ - product = entities.Product(organization=module_org).create() - yum_sha512_repo = entities.Repository(product=product, url=CUSTOM_RPM_SHA_512).create() + product = module_target_sat.api.Product(organization=module_org).create() + yum_sha512_repo = module_target_sat.api.Repository( + product=product, url=CUSTOM_RPM_SHA_512 + ).create() yum_sha512_repo.sync() repo_content = yum_sha512_repo.read() # Assert that the repository content was properly synced @@ -295,7 +304,7 @@ class TestContentViewCreate: @pytest.mark.parametrize('composite', [True, False]) @pytest.mark.tier1 - def test_positive_create_composite(self, composite): + def test_positive_create_composite(self, composite, target_sat): """Create composite and non-composite content views. :id: 4a3b616d-53ab-4396-9a50-916d6c42a401 @@ -307,11 +316,11 @@ def test_positive_create_composite(self, composite): :CaseImportance: Critical """ - assert entities.ContentView(composite=composite).create().composite == composite + assert target_sat.api.ContentView(composite=composite).create().composite == composite @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 - def test_positive_create_with_name(self, name): + def test_positive_create_with_name(self, name, target_sat): """Create empty content-view with random names. :id: 80d36498-2e71-4aa9-b696-f0a45e86267f @@ -322,11 +331,11 @@ def test_positive_create_with_name(self, name): :CaseImportance: Critical """ - assert entities.ContentView(name=name).create().name == name + assert target_sat.api.ContentView(name=name).create().name == name @pytest.mark.parametrize('desc', **parametrized(valid_data_list())) @pytest.mark.tier1 - def test_positive_create_with_description(self, desc): + def test_positive_create_with_description(self, desc, target_sat): """Create empty content view with random description. :id: 068e3e7c-34ac-47cb-a1bb-904d12c74cc7 @@ -337,10 +346,10 @@ def test_positive_create_with_description(self, desc): :CaseImportance: High """ - assert entities.ContentView(description=desc).create().description == desc + assert target_sat.api.ContentView(description=desc).create().description == desc @pytest.mark.tier1 - def test_positive_clone(self, content_view, module_org): + def test_positive_clone(self, content_view, module_org, module_target_sat): """Create a content view by copying an existing one :id: ee03dc63-e2b0-4a89-a828-2910405279ff @@ -349,7 +358,7 @@ def test_positive_clone(self, content_view, module_org): :CaseImportance: Critical """ - cloned_cv = entities.ContentView( + cloned_cv = module_target_sat.api.ContentView( id=content_view.copy(data={'name': gen_string('alpha', gen_integer(3, 30))})['id'] ).read_json() cv_origin = content_view.read_json() @@ -361,7 +370,7 @@ def test_positive_clone(self, content_view, module_org): @pytest.mark.parametrize('name', **parametrized(invalid_names_list())) @pytest.mark.tier1 - def test_negative_create_with_invalid_name(self, name): + def test_negative_create_with_invalid_name(self, name, target_sat): """Create content view providing an invalid name. :id: 261376ca-7d12-41b6-9c36-5f284865243e @@ -373,7 +382,7 @@ def test_negative_create_with_invalid_name(self, name): :CaseImportance: High """ with pytest.raises(HTTPError): - entities.ContentView(name=name).create() + target_sat.api.ContentView(name=name).create() class TestContentViewPublishPromote: @@ -383,16 +392,18 @@ class TestContentViewPublishPromote: (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) @pytest.fixture(scope='class', autouse=True) - def class_setup(self, request, module_product): + def class_setup(self, request, module_product, class_target_sat): """Set up organization, product and repositories for tests.""" - request.cls.yum_repo = entities.Repository(product=module_product).create() + request.cls.yum_repo = class_target_sat.api.Repository(product=module_product).create() self.yum_repo.sync() - request.cls.swid_repo = entities.Repository( + request.cls.swid_repo = class_target_sat.api.Repository( product=module_product, url=settings.repos.swid_tag.url ).create() self.swid_repo.sync() - def add_content_views_to_composite(self, composite_cv, module_org, cv_amount=1): + def add_content_views_to_composite( + self, module_target_sat, composite_cv, module_org, cv_amount=1 + ): """Add necessary number of content views to the composite one :param composite_cv: Composite content view object @@ -400,7 +411,7 @@ def add_content_views_to_composite(self, composite_cv, module_org, cv_amount=1): """ cv_versions = [] for _ in range(cv_amount): - content_view = entities.ContentView(organization=module_org).create() + content_view = module_target_sat.api.ContentView(organization=module_org).create() content_view.publish() cv_versions.append(content_view.read().version[0]) composite_cv.component = cv_versions @@ -435,7 +446,7 @@ def test_positive_publish_with_content_multiple(self, content_view, module_org): assert cvv.read_json()['package_count'] > 0 @pytest.mark.tier2 - def test_positive_publish_composite_multiple_content_once(self, module_org): + def test_positive_publish_composite_multiple_content_once(self, module_org, module_target_sat): """Create empty composite view and assign random number of normal content views to it. After that publish that composite content view once. @@ -449,16 +460,20 @@ def test_positive_publish_composite_multiple_content_once(self, module_org): :CaseImportance: Critical """ - composite_cv = entities.ContentView( + composite_cv = module_target_sat.api.ContentView( composite=True, organization=module_org, ).create() - self.add_content_views_to_composite(composite_cv, module_org, random.randint(2, 3)) + self.add_content_views_to_composite( + module_target_sat, composite_cv, module_org, random.randint(2, 3) + ) composite_cv.publish() assert len(composite_cv.read().version) == 1 @pytest.mark.tier2 - def test_positive_publish_composite_multiple_content_multiple(self, module_org): + def test_positive_publish_composite_multiple_content_multiple( + self, module_org, module_target_sat + ): """Create empty composite view and assign random number of normal content views to it. After that publish that composite content view several times. @@ -472,7 +487,7 @@ def test_positive_publish_composite_multiple_content_multiple(self, module_org): :CaseImportance: High """ - composite_cv = entities.ContentView( + composite_cv = module_target_sat.api.ContentView( composite=True, organization=module_org, ).create() @@ -483,7 +498,7 @@ def test_positive_publish_composite_multiple_content_multiple(self, module_org): assert len(composite_cv.read().version) == i + 1 @pytest.mark.tier2 - def test_positive_promote_with_yum_multiple(self, content_view, module_org): + def test_positive_promote_with_yum_multiple(self, content_view, module_org, module_target_sat): """Give a content view a yum repo, publish it once and promote the content view version ``REPEAT + 1`` times. @@ -504,7 +519,7 @@ def test_positive_promote_with_yum_multiple(self, content_view, module_org): # Promote the content view version. for _ in range(REPEAT): - lce = entities.LifecycleEnvironment(organization=module_org).create() + lce = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() content_view.version[0].promote(data={'environment_ids': lce.id}) # Everything's done - check some content view attributes... @@ -518,7 +533,7 @@ def test_positive_promote_with_yum_multiple(self, content_view, module_org): assert cvv_attrs['package_count'] > 0 @pytest.mark.tier2 - def test_positive_add_to_composite(self, content_view, module_org): + def test_positive_add_to_composite(self, content_view, module_org, module_target_sat): """Create normal content view, publish and add it to a new composite content view @@ -536,7 +551,7 @@ def test_positive_add_to_composite(self, content_view, module_org): content_view.publish() content_view = content_view.read() - composite_cv = entities.ContentView( + composite_cv = module_target_sat.api.ContentView( composite=True, organization=module_org, ).create() @@ -549,7 +564,9 @@ def test_positive_add_to_composite(self, content_view, module_org): assert composite_cv.component[0].read().content_view.id == content_view.id @pytest.mark.tier2 - def test_negative_add_components_to_composite(self, content_view, module_org): + def test_negative_add_components_to_composite( + self, content_view, module_org, module_target_sat + ): """Attempt to associate components in a non-composite content view @@ -565,7 +582,7 @@ def test_negative_add_components_to_composite(self, content_view, module_org): content_view.update(['repository']) content_view.publish() content_view = content_view.read() - non_composite_cv = entities.ContentView( + non_composite_cv = module_target_sat.api.ContentView( composite=False, organization=module_org, ).create() @@ -576,7 +593,9 @@ def test_negative_add_components_to_composite(self, content_view, module_org): @pytest.mark.upgrade @pytest.mark.tier2 - def test_positive_promote_composite_multiple_content_once(self, module_lce, module_org): + def test_positive_promote_composite_multiple_content_once( + self, module_lce, module_org, module_target_sat + ): """Create empty composite view and assign random number of normal content views to it. After that promote that composite content view once. @@ -590,7 +609,7 @@ def test_positive_promote_composite_multiple_content_once(self, module_lce, modu :CaseImportance: High """ - composite_cv = entities.ContentView( + composite_cv = module_target_sat.api.ContentView( composite=True, organization=module_org, ).create() @@ -603,7 +622,9 @@ def test_positive_promote_composite_multiple_content_once(self, module_lce, modu @pytest.mark.upgrade @pytest.mark.tier2 - def test_positive_promote_composite_multiple_content_multiple(self, module_org): + def test_positive_promote_composite_multiple_content_multiple( + self, module_org, module_target_sat + ): """Create empty composite view and assign random number of normal content views to it. After that promote that composite content view ``Library + random`` times. @@ -617,7 +638,7 @@ def test_positive_promote_composite_multiple_content_multiple(self, module_org): :CaseImportance: High """ - composite_cv = entities.ContentView( + composite_cv = module_target_sat.api.ContentView( composite=True, organization=module_org, ).create() @@ -627,7 +648,7 @@ def test_positive_promote_composite_multiple_content_multiple(self, module_org): envs_amount = random.randint(2, 3) for _ in range(envs_amount): - lce = entities.LifecycleEnvironment(organization=module_org).create() + lce = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() composite_cv.version[0].promote(data={'environment_ids': lce.id}) composite_cv = composite_cv.read() assert len(composite_cv.version) == 1 @@ -671,7 +692,7 @@ def test_positive_promote_out_of_sequence(self, content_view, module_org): @pytest.mark.tier3 @pytest.mark.pit_server - def test_positive_publish_multiple_repos(self, content_view, module_org): + def test_positive_publish_multiple_repos(self, content_view, module_org, module_target_sat): """Attempt to publish a content view with multiple YUM repos. :id: 5557a33b-7a6f-45f5-9fe4-23a704ed9e21 @@ -690,9 +711,9 @@ def test_positive_publish_multiple_repos(self, content_view, module_org): :BZ: 1651930 """ - product = entities.Product(organization=module_org).create() + product = module_target_sat.api.Product(organization=module_org).create() for _ in range(10): - repo = entities.Repository(product=product).create() + repo = module_target_sat.api.Repository(product=product).create() repo.sync() content_view.repository.append(repo) content_view = content_view.update(['repository']) @@ -721,13 +742,13 @@ def test_composite_content_view_with_same_repos(self, module_org, target_sat): :CaseImportance: Medium """ - product = entities.Product(organization=module_org).create() - repo = entities.Repository( + product = target_sat.api.Product(organization=module_org).create() + repo = target_sat.api.Repository( content_type='yum', product=product, url=settings.repos.module_stream_1.url ).create() repo.sync() - content_view_1 = entities.ContentView(organization=module_org).create() - content_view_2 = entities.ContentView(organization=module_org).create() + content_view_1 = target_sat.api.ContentView(organization=module_org).create() + content_view_2 = target_sat.api.ContentView(organization=module_org).create() # create content views with same repo and different filter for content_view, package in [(content_view_1, 'camel'), (content_view_2, 'cow')]: @@ -737,7 +758,7 @@ def test_composite_content_view_with_same_repos(self, module_org, target_sat): assert content_view_info.package_count == 35 # create composite content view with these two published content views - comp_content_view = entities.ContentView( + comp_content_view = target_sat.api.ContentView( composite=True, organization=module_org, ).create() @@ -877,7 +898,7 @@ def test_positive_update_attributes(self, module_cv, key, value): @pytest.mark.parametrize('new_name', **parametrized(valid_data_list())) @pytest.mark.tier1 - def test_positive_update_name(self, module_cv, new_name): + def test_positive_update_name(self, module_cv, new_name, module_target_sat): """Create content view providing the initial name, then update its name to another valid name. @@ -891,12 +912,12 @@ def test_positive_update_name(self, module_cv, new_name): """ module_cv.name = new_name module_cv.update(['name']) - updated = entities.ContentView(id=module_cv.id).read() + updated = module_target_sat.api.ContentView(id=module_cv.id).read() assert new_name == updated.name @pytest.mark.parametrize('new_name', **parametrized(invalid_names_list())) @pytest.mark.tier1 - def test_negative_update_name(self, module_cv, new_name): + def test_negative_update_name(self, module_cv, new_name, module_target_sat): """Create content view then update its name to an invalid name. @@ -920,7 +941,7 @@ class TestContentViewDelete: @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 - def test_positive_delete(self, content_view, name): + def test_positive_delete(self, content_view, name, target_sat): """Create content view and then delete it. :id: d582f1b3-8118-4e78-a639-237c6f9d27c6 @@ -933,7 +954,7 @@ def test_positive_delete(self, content_view, name): """ content_view.delete() with pytest.raises(HTTPError): - entities.ContentView(id=content_view.id).read() + target_sat.api.ContentView(id=content_view.id).read() @pytest.mark.run_in_one_thread @@ -942,11 +963,11 @@ class TestContentViewRedHatContent: @pytest.fixture(scope='class', autouse=True) def initiate_testclass( - self, request, module_cv, module_entitlement_manifest_org, module_target_sat + self, request, module_cv, module_entitlement_manifest_org, class_target_sat ): """Set up organization, product and repositories for tests.""" - repo_id = module_target_sat.api_factory.enable_rhrepo_and_fetchid( + repo_id = class_target_sat.api_factory.enable_rhrepo_and_fetchid( basearch='x86_64', org_id=module_entitlement_manifest_org.id, product=PRDS['rhel'], @@ -954,7 +975,7 @@ def initiate_testclass( reposet=REPOSET['rhst7'], releasever=None, ) - request.cls.repo = entities.Repository(id=repo_id) + request.cls.repo = class_target_sat.api.Repository(id=repo_id) self.repo.sync() module_cv.repository = [self.repo] module_cv.update(['repository']) @@ -976,7 +997,7 @@ def test_positive_add_rh(self): assert self.yumcv.repository[0].read().name == REPOS['rhst7']['name'] @pytest.mark.tier2 - def test_positive_add_rh_custom_spin(self): + def test_positive_add_rh_custom_spin(self, target_sat): """Associate Red Hat content in a view and filter it using rule :id: 30c3103d-9503-4501-8117-1f2d25353215 @@ -989,7 +1010,7 @@ def test_positive_add_rh_custom_spin(self): :CaseImportance: High """ # content_view ← cv_filter - cv_filter = entities.RPMContentViewFilter( + cv_filter = target_sat.api.RPMContentViewFilter( content_view=self.yumcv, inclusion='true', name=gen_string('alphanumeric'), @@ -997,13 +1018,13 @@ def test_positive_add_rh_custom_spin(self): assert self.yumcv.id == cv_filter.content_view.id # content_view ← cv_filter ← cv_filter_rule - cv_filter_rule = entities.ContentViewFilterRule( + cv_filter_rule = target_sat.api.ContentViewFilterRule( content_view_filter=cv_filter, name=gen_string('alphanumeric'), version='1.0' ).create() assert cv_filter.id == cv_filter_rule.content_view_filter.id @pytest.mark.tier2 - def test_positive_update_rh_custom_spin(self): + def test_positive_update_rh_custom_spin(self, target_sat): """Edit content views for a custom rh spin. For example, modify a filter @@ -1016,12 +1037,12 @@ def test_positive_update_rh_custom_spin(self): :CaseImportance: High """ - cvf = entities.ErratumContentViewFilter( + cvf = target_sat.api.ErratumContentViewFilter( content_view=self.yumcv, ).create() assert self.yumcv.id == cvf.content_view.id - cv_filter_rule = entities.ContentViewFilterRule( + cv_filter_rule = target_sat.api.ContentViewFilterRule( content_view_filter=cvf, types=[FILTER_ERRATA_TYPE['enhancement']] ).create() assert cv_filter_rule.types == [FILTER_ERRATA_TYPE['enhancement']] @@ -1048,7 +1069,7 @@ def test_positive_publish_rh(self, module_org, content_view): assert len(content_view.read().version) == 1 @pytest.mark.tier2 - def test_positive_publish_rh_custom_spin(self, module_org, content_view): + def test_positive_publish_rh_custom_spin(self, module_org, content_view, module_target_sat): """Attempt to publish a content view containing Red Hat spin - i.e., contains filters. @@ -1062,7 +1083,7 @@ def test_positive_publish_rh_custom_spin(self, module_org, content_view): """ content_view.repository = [self.repo] content_view = content_view.update(['repository']) - entities.RPMContentViewFilter( + module_target_sat.api.RPMContentViewFilter( content_view=content_view, inclusion='true', name=gen_string('alphanumeric') ).create() content_view.publish() @@ -1093,7 +1114,7 @@ def test_positive_promote_rh(self, module_org, content_view, module_lce): @pytest.mark.upgrade @pytest.mark.tier2 - def test_positive_promote_rh_custom_spin(self, content_view, module_lce): + def test_positive_promote_rh_custom_spin(self, content_view, module_lce, module_target_sat): """Attempt to promote a content view containing Red Hat spin - i.e., contains filters. @@ -1107,7 +1128,7 @@ def test_positive_promote_rh_custom_spin(self, content_view, module_lce): """ content_view.repository = [self.repo] content_view = content_view.update(['repository']) - entities.RPMContentViewFilter( + module_target_sat.api.RPMContentViewFilter( content_view=content_view, inclusion='true', name=gen_string('alphanumeric') ).create() content_view.publish() @@ -1357,7 +1378,7 @@ def test_negative_readonly_user_actions( # create a role with content views read only permissions target_sat.api.Filter( organization=[module_org], - permission=entities.Permission().search( + permission=target_sat.api.Permission().search( filters={'name': 'view_content_views'}, query={'search': 'resource_type="Katello::ContentView"'}, ), @@ -1366,7 +1387,7 @@ def test_negative_readonly_user_actions( # create environment permissions and assign it to our role target_sat.api.Filter( organization=[module_org], - permission=entities.Permission().search( + permission=target_sat.api.Permission().search( query={'search': 'resource_type="Katello::KTEnvironment"'} ), role=function_role, @@ -1471,9 +1492,9 @@ class TestOstreeContentView: (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) @pytest.fixture(scope='class', autouse=True) - def initiate_testclass(self, request, module_product): + def initiate_testclass(self, request, module_product, class_target_sat): """Set up organization, product and repositories for tests.""" - request.cls.ostree_repo = entities.Repository( + request.cls.ostree_repo = class_target_sat.api.Repository( product=module_product, content_type='ostree', url=FEDORA_OSTREE_REPO, @@ -1481,13 +1502,13 @@ def initiate_testclass(self, request, module_product): ).create() self.ostree_repo.sync() # Create new yum repository - request.cls.yum_repo = entities.Repository( + request.cls.yum_repo = class_target_sat.api.Repository( url=settings.repos.yum_1.url, product=module_product, ).create() self.yum_repo.sync() # Create new docker repository - request.cls.docker_repo = entities.Repository( + request.cls.docker_repo = class_target_sat.api.Repository( content_type='docker', docker_upstream_name='busybox', product=module_product, @@ -1595,7 +1616,7 @@ def initiate_testclass(self, request, module_entitlement_manifest_org, class_tar reposet=REPOSET['rhaht'], releasever=None, ) - request.cls.repo = entities.Repository(id=repo_id) + request.cls.repo = class_target_sat.api.Repository(id=repo_id) self.repo.sync() @pytest.mark.tier2 @@ -1681,7 +1702,7 @@ def test_positive_publish_promote_with_RH_ostree_and_other( releasever=None, ) # Sync repository - rpm_repo = entities.Repository(id=repo_id) + rpm_repo = module_target_sat.api.Repository(id=repo_id) rpm_repo.sync() content_view.repository = [self.repo, rpm_repo] content_view = content_view.update(['repository']) diff --git a/tests/foreman/api/test_contentviewfilter.py b/tests/foreman/api/test_contentviewfilter.py index b0325ceecec..8e4ac95c75d 100644 --- a/tests/foreman/api/test_contentviewfilter.py +++ b/tests/foreman/api/test_contentviewfilter.py @@ -24,7 +24,7 @@ from random import randint from fauxfactory import gen_integer, gen_string -from nailgun import client, entities +from nailgun import client import pytest from requests.exceptions import HTTPError @@ -38,15 +38,15 @@ @pytest.fixture(scope='module') -def sync_repo(module_product): - repo = entities.Repository(product=module_product).create() +def sync_repo(module_product, module_target_sat): + repo = module_target_sat.api.Repository(product=module_product).create() repo.sync() return repo @pytest.fixture(scope='module') -def sync_repo_module_stream(module_product): - repo = entities.Repository( +def sync_repo_module_stream(module_product, module_target_sat): + repo = module_target_sat.api.Repository( content_type='yum', product=module_product, url=settings.repos.module_stream_1.url ).create() repo.sync() @@ -54,13 +54,15 @@ def sync_repo_module_stream(module_product): @pytest.fixture -def content_view(module_org, sync_repo): - return entities.ContentView(organization=module_org, repository=[sync_repo]).create() +def content_view(module_org, sync_repo, module_target_sat): + return module_target_sat.api.ContentView( + organization=module_org, repository=[sync_repo] + ).create() @pytest.fixture -def content_view_module_stream(module_org, sync_repo_module_stream): - return entities.ContentView( +def content_view_module_stream(module_org, sync_repo_module_stream, module_target_sat): + return module_target_sat.api.ContentView( organization=module_org, repository=[sync_repo_module_stream] ).create() @@ -69,7 +71,7 @@ class TestContentViewFilter: """Tests for content view filters.""" @pytest.mark.tier2 - def test_negative_get_with_no_args(self): + def test_negative_get_with_no_args(self, target_sat): """Issue an HTTP GET to the base content view filters path. :id: da29fd90-cd96-49f9-b94e-71d4e3a35a57 @@ -82,14 +84,14 @@ def test_negative_get_with_no_args(self): :CaseImportance: Low """ response = client.get( - entities.AbstractContentViewFilter().path(), + target_sat.api.AbstractContentViewFilter().path(), auth=get_credentials(), verify=False, ) assert response.status_code == http.client.OK @pytest.mark.tier2 - def test_negative_get_with_bad_args(self): + def test_negative_get_with_bad_args(self, target_sat): """Issue an HTTP GET to the base content view filters path. :id: e6fea726-930b-4b74-b784-41528811994f @@ -102,7 +104,7 @@ def test_negative_get_with_bad_args(self): :CaseImportance: Low """ response = client.get( - entities.AbstractContentViewFilter().path(), + target_sat.api.AbstractContentViewFilter().path(), auth=get_credentials(), verify=False, data={'foo': 'bar'}, @@ -111,7 +113,7 @@ def test_negative_get_with_bad_args(self): @pytest.mark.tier2 @pytest.mark.parametrize('name', **parametrized(valid_data_list())) - def test_positive_create_erratum_with_name(self, name, content_view): + def test_positive_create_erratum_with_name(self, name, content_view, target_sat): """Create new erratum content filter using different inputs as a name :id: f78a133f-441f-4fcc-b292-b9eed228d755 @@ -123,13 +125,13 @@ def test_positive_create_erratum_with_name(self, name, content_view): :CaseLevel: Integration """ - cvf = entities.ErratumContentViewFilter(content_view=content_view, name=name).create() + cvf = target_sat.api.ErratumContentViewFilter(content_view=content_view, name=name).create() assert cvf.name == name assert cvf.type == 'erratum' @pytest.mark.tier2 @pytest.mark.parametrize('name', **parametrized(valid_data_list())) - def test_positive_create_pkg_group_with_name(self, name, content_view): + def test_positive_create_pkg_group_with_name(self, name, content_view, target_sat): """Create new package group content filter using different inputs as a name :id: f9bfb6bf-a879-4f1a-970d-8f4df533cd59 @@ -143,7 +145,7 @@ def test_positive_create_pkg_group_with_name(self, name, content_view): :CaseImportance: Medium """ - cvf = entities.PackageGroupContentViewFilter( + cvf = target_sat.api.PackageGroupContentViewFilter( content_view=content_view, name=name, ).create() @@ -152,7 +154,7 @@ def test_positive_create_pkg_group_with_name(self, name, content_view): @pytest.mark.tier2 @pytest.mark.parametrize('name', **parametrized(valid_data_list())) - def test_positive_create_rpm_with_name(self, name, content_view): + def test_positive_create_rpm_with_name(self, name, content_view, target_sat): """Create new RPM content filter using different inputs as a name :id: f1c88e72-7993-47ac-8fbc-c749d32bc768 @@ -166,13 +168,13 @@ def test_positive_create_rpm_with_name(self, name, content_view): :CaseImportance: Medium """ - cvf = entities.RPMContentViewFilter(content_view=content_view, name=name).create() + cvf = target_sat.api.RPMContentViewFilter(content_view=content_view, name=name).create() assert cvf.name == name assert cvf.type == 'rpm' @pytest.mark.tier2 @pytest.mark.parametrize('inclusion', [True, False]) - def test_positive_create_with_inclusion(self, inclusion, content_view): + def test_positive_create_with_inclusion(self, inclusion, content_view, target_sat): """Create new content view filter with different inclusion values :id: 81130dc9-ae33-48bc-96a7-d54d3e99448e @@ -184,12 +186,14 @@ def test_positive_create_with_inclusion(self, inclusion, content_view): :CaseLevel: Integration """ - cvf = entities.RPMContentViewFilter(content_view=content_view, inclusion=inclusion).create() + cvf = target_sat.api.RPMContentViewFilter( + content_view=content_view, inclusion=inclusion + ).create() assert cvf.inclusion == inclusion @pytest.mark.tier2 @pytest.mark.parametrize('description', **parametrized(valid_data_list())) - def test_positive_create_with_description(self, description, content_view): + def test_positive_create_with_description(self, description, content_view, target_sat): """Create new content filter using different inputs as a description :id: e057083f-e69d-46e7-b336-45faaf67fa52 @@ -203,14 +207,14 @@ def test_positive_create_with_description(self, description, content_view): :CaseImportance: Low """ - cvf = entities.RPMContentViewFilter( + cvf = target_sat.api.RPMContentViewFilter( content_view=content_view, description=description, ).create() assert cvf.description == description @pytest.mark.tier2 - def test_positive_create_with_repo(self, content_view, sync_repo): + def test_positive_create_with_repo(self, content_view, sync_repo, target_sat): """Create new content filter with repository assigned :id: 7207d4cf-3ccf-4d63-a50a-1373b16062e2 @@ -220,7 +224,7 @@ def test_positive_create_with_repo(self, content_view, sync_repo): :CaseLevel: Integration """ - cvf = entities.RPMContentViewFilter( + cvf = target_sat.api.RPMContentViewFilter( content_view=content_view, inclusion=True, repository=[sync_repo], @@ -230,7 +234,7 @@ def test_positive_create_with_repo(self, content_view, sync_repo): @pytest.mark.tier2 @pytest.mark.parametrize('original_packages', [True, False]) def test_positive_create_with_original_packages( - self, original_packages, content_view, sync_repo + self, original_packages, content_view, sync_repo, target_sat ): """Create new content view filter with different 'original packages' option values @@ -246,7 +250,7 @@ def test_positive_create_with_original_packages( :CaseImportance: Medium """ - cvf = entities.RPMContentViewFilter( + cvf = target_sat.api.RPMContentViewFilter( content_view=content_view, inclusion=True, repository=[sync_repo], @@ -255,7 +259,9 @@ def test_positive_create_with_original_packages( assert cvf.original_packages == original_packages @pytest.mark.tier2 - def test_positive_create_with_docker_repos(self, module_product, sync_repo, content_view): + def test_positive_create_with_docker_repos( + self, module_product, sync_repo, content_view, module_target_sat + ): """Create new docker repository and add to content view that has yum repo already assigned to it. Create new content view filter and assign it to the content view. @@ -267,7 +273,7 @@ def test_positive_create_with_docker_repos(self, module_product, sync_repo, cont :CaseLevel: Integration """ - docker_repository = entities.Repository( + docker_repository = module_target_sat.api.Repository( content_type='docker', docker_upstream_name='busybox', product=module_product.id, @@ -276,7 +282,7 @@ def test_positive_create_with_docker_repos(self, module_product, sync_repo, cont content_view.repository = [sync_repo, docker_repository] content_view.update(['repository']) - cvf = entities.RPMContentViewFilter( + cvf = module_target_sat.api.RPMContentViewFilter( content_view=content_view, inclusion=True, repository=[sync_repo, docker_repository], @@ -290,7 +296,7 @@ def test_positive_create_with_docker_repos(self, module_product, sync_repo, cont (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) def test_positive_create_with_module_streams( - self, module_product, sync_repo, sync_repo_module_stream, content_view + self, module_product, sync_repo, sync_repo_module_stream, content_view, target_sat ): """Verify Include and Exclude Filters creation for modulemd (module streams) @@ -304,7 +310,7 @@ def test_positive_create_with_module_streams( content_view.repository += [sync_repo_module_stream] content_view.update(['repository']) for inclusion in (True, False): - cvf = entities.ModuleStreamContentViewFilter( + cvf = target_sat.api.ModuleStreamContentViewFilter( content_view=content_view, inclusion=inclusion, repository=[sync_repo, sync_repo_module_stream], @@ -316,7 +322,7 @@ def test_positive_create_with_module_streams( @pytest.mark.tier2 @pytest.mark.parametrize('name', **parametrized(invalid_names_list())) - def test_negative_create_with_invalid_name(self, name, content_view): + def test_negative_create_with_invalid_name(self, name, content_view, target_sat): """Try to create content view filter using invalid names only :id: 8cf4227b-75c4-4d6f-b94f-88e4eb586435 @@ -330,10 +336,10 @@ def test_negative_create_with_invalid_name(self, name, content_view): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.RPMContentViewFilter(content_view=content_view, name=name).create() + target_sat.api.RPMContentViewFilter(content_view=content_view, name=name).create() @pytest.mark.tier2 - def test_negative_create_with_same_name(self, content_view): + def test_negative_create_with_same_name(self, content_view, target_sat): """Try to create content view filter using same name twice :id: 73a64ca7-07a3-49ee-8921-0474a16a23ff @@ -345,12 +351,12 @@ def test_negative_create_with_same_name(self, content_view): :CaseImportance: Low """ kwargs = {'content_view': content_view, 'name': gen_string('alpha')} - entities.RPMContentViewFilter(**kwargs).create() + target_sat.api.RPMContentViewFilter(**kwargs).create() with pytest.raises(HTTPError): - entities.RPMContentViewFilter(**kwargs).create() + target_sat.api.RPMContentViewFilter(**kwargs).create() @pytest.mark.tier2 - def test_negative_create_without_cv(self): + def test_negative_create_without_cv(self, target_sat): """Try to create content view filter without providing content view @@ -363,10 +369,10 @@ def test_negative_create_without_cv(self): :CaseImportance: Low """ with pytest.raises(HTTPError): - entities.RPMContentViewFilter(content_view=None).create() + target_sat.api.RPMContentViewFilter(content_view=None).create() @pytest.mark.tier2 - def test_negative_create_with_invalid_repo_id(self, content_view): + def test_negative_create_with_invalid_repo_id(self, content_view, target_sat): """Try to create content view filter using incorrect repository id @@ -379,13 +385,13 @@ def test_negative_create_with_invalid_repo_id(self, content_view): :CaseImportance: Low """ with pytest.raises(HTTPError): - entities.RPMContentViewFilter( + target_sat.api.RPMContentViewFilter( content_view=content_view, repository=[gen_integer(10000, 99999)], ).create() @pytest.mark.tier2 - def test_positive_delete_by_id(self, content_view): + def test_positive_delete_by_id(self, content_view, target_sat): """Delete content view filter :id: 07caeb9d-419d-43f8-996b-456b0cc0f70d @@ -396,14 +402,14 @@ def test_positive_delete_by_id(self, content_view): :CaseImportance: Critical """ - cvf = entities.RPMContentViewFilter(content_view=content_view).create() + cvf = target_sat.api.RPMContentViewFilter(content_view=content_view).create() cvf.delete() with pytest.raises(HTTPError): cvf.read() @pytest.mark.tier2 @pytest.mark.parametrize('name', **parametrized(valid_data_list())) - def test_positive_update_name(self, name, content_view): + def test_positive_update_name(self, name, content_view, target_sat): """Update content view filter with new name :id: f310c161-00d2-4281-9721-6e45cbc5e4ec @@ -415,13 +421,13 @@ def test_positive_update_name(self, name, content_view): :CaseLevel: Integration """ - cvf = entities.RPMContentViewFilter(content_view=content_view).create() + cvf = target_sat.api.RPMContentViewFilter(content_view=content_view).create() cvf.name = name assert cvf.update(['name']).name == name @pytest.mark.tier2 @pytest.mark.parametrize('description', **parametrized(valid_data_list())) - def test_positive_update_description(self, description, content_view): + def test_positive_update_description(self, description, content_view, target_sat): """Update content view filter with new description :id: f2c5db28-0163-4cf3-929a-16ba1cb98c34 @@ -435,14 +441,14 @@ def test_positive_update_description(self, description, content_view): :CaseImportance: Low """ - cvf = entities.RPMContentViewFilter(content_view=content_view).create() + cvf = target_sat.api.RPMContentViewFilter(content_view=content_view).create() cvf.description = description cvf = cvf.update(['description']) assert cvf.description == description @pytest.mark.tier2 @pytest.mark.parametrize('inclusion', [True, False]) - def test_positive_update_inclusion(self, inclusion, content_view): + def test_positive_update_inclusion(self, inclusion, content_view, target_sat): """Update content view filter with new inclusion value :id: 0aedd2d6-d020-4a90-adcd-01694b47c0b0 @@ -454,13 +460,13 @@ def test_positive_update_inclusion(self, inclusion, content_view): :CaseLevel: Integration """ - cvf = entities.RPMContentViewFilter(content_view=content_view).create() + cvf = target_sat.api.RPMContentViewFilter(content_view=content_view).create() cvf.inclusion = inclusion cvf = cvf.update(['inclusion']) assert cvf.inclusion == inclusion @pytest.mark.tier2 - def test_positive_update_repo(self, module_product, sync_repo, content_view): + def test_positive_update_repo(self, module_product, sync_repo, content_view, target_sat): """Update content view filter with new repository :id: 329ef155-c2d0-4aa2-bac3-79087ae49bdf @@ -470,12 +476,12 @@ def test_positive_update_repo(self, module_product, sync_repo, content_view): :CaseLevel: Integration """ - cvf = entities.RPMContentViewFilter( + cvf = target_sat.api.RPMContentViewFilter( content_view=content_view, inclusion=True, repository=[sync_repo], ).create() - new_repo = entities.Repository(product=module_product).create() + new_repo = target_sat.api.Repository(product=module_product).create() new_repo.sync() content_view.repository = [new_repo] content_view.update(['repository']) @@ -485,7 +491,7 @@ def test_positive_update_repo(self, module_product, sync_repo, content_view): assert cvf.repository[0].id == new_repo.id @pytest.mark.tier2 - def test_positive_update_repos(self, module_product, sync_repo, content_view): + def test_positive_update_repos(self, module_product, sync_repo, content_view, target_sat): """Update content view filter with multiple repositories :id: 478fbb1c-fa1d-4fcd-93d6-3a7f47092ed3 @@ -497,12 +503,14 @@ def test_positive_update_repos(self, module_product, sync_repo, content_view): :CaseImportance: Low """ - cvf = entities.RPMContentViewFilter( + cvf = target_sat.api.RPMContentViewFilter( content_view=content_view, inclusion=True, repository=[sync_repo], ).create() - repos = [entities.Repository(product=module_product).create() for _ in range(randint(3, 5))] + repos = [ + target_sat.api.Repository(product=module_product).create() for _ in range(randint(3, 5)) + ] for repo in repos: repo.sync() content_view.repository = repos @@ -513,7 +521,9 @@ def test_positive_update_repos(self, module_product, sync_repo, content_view): @pytest.mark.tier2 @pytest.mark.parametrize('original_packages', [True, False]) - def test_positive_update_original_packages(self, original_packages, sync_repo, content_view): + def test_positive_update_original_packages( + self, original_packages, sync_repo, content_view, target_sat + ): """Update content view filter with new 'original packages' option value :id: 0c41e57a-afa3-479e-83ba-01f09f0fd2b6 @@ -525,7 +535,7 @@ def test_positive_update_original_packages(self, original_packages, sync_repo, c :CaseLevel: Integration """ - cvf = entities.RPMContentViewFilter( + cvf = target_sat.api.RPMContentViewFilter( content_view=content_view, inclusion=True, repository=[sync_repo], @@ -535,7 +545,9 @@ def test_positive_update_original_packages(self, original_packages, sync_repo, c assert cvf.original_packages == original_packages @pytest.mark.tier2 - def test_positive_update_repo_with_docker(self, module_product, sync_repo, content_view): + def test_positive_update_repo_with_docker( + self, module_product, sync_repo, content_view, target_sat + ): """Update existing content view filter which has yum repository assigned with new docker repository @@ -546,12 +558,12 @@ def test_positive_update_repo_with_docker(self, module_product, sync_repo, conte :CaseLevel: Integration """ - cvf = entities.RPMContentViewFilter( + cvf = target_sat.api.RPMContentViewFilter( content_view=content_view, inclusion=True, repository=[sync_repo], ).create() - docker_repository = entities.Repository( + docker_repository = target_sat.api.Repository( content_type='docker', docker_upstream_name='busybox', product=module_product.id, @@ -567,7 +579,7 @@ def test_positive_update_repo_with_docker(self, module_product, sync_repo, conte @pytest.mark.tier2 @pytest.mark.parametrize('name', **parametrized(invalid_names_list())) - def test_negative_update_name(self, name, content_view): + def test_negative_update_name(self, name, content_view, target_sat): """Try to update content view filter using invalid names only :id: 9799648a-3900-4186-8271-6b2dedb547ab @@ -580,13 +592,13 @@ def test_negative_update_name(self, name, content_view): :CaseImportance: Low """ - cvf = entities.RPMContentViewFilter(content_view=content_view).create() + cvf = target_sat.api.RPMContentViewFilter(content_view=content_view).create() cvf.name = name with pytest.raises(HTTPError): cvf.update(['name']) @pytest.mark.tier2 - def test_negative_update_same_name(self, content_view): + def test_negative_update_same_name(self, content_view, target_sat): """Try to update content view filter's name to already used one :id: b68569f1-9f7b-4a95-9e2a-a5da348abff7 @@ -598,14 +610,14 @@ def test_negative_update_same_name(self, content_view): :CaseImportance: Low """ name = gen_string('alpha', 8) - entities.RPMContentViewFilter(content_view=content_view, name=name).create() - cvf = entities.RPMContentViewFilter(content_view=content_view).create() + target_sat.api.RPMContentViewFilter(content_view=content_view, name=name).create() + cvf = target_sat.api.RPMContentViewFilter(content_view=content_view).create() cvf.name = name with pytest.raises(HTTPError): cvf.update(['name']) @pytest.mark.tier2 - def test_negative_update_cv_by_id(self, content_view): + def test_negative_update_cv_by_id(self, content_view, target_sat): """Try to update content view filter using incorrect content view ID @@ -615,13 +627,13 @@ def test_negative_update_cv_by_id(self, content_view): :CaseLevel: Integration """ - cvf = entities.RPMContentViewFilter(content_view=content_view).create() + cvf = target_sat.api.RPMContentViewFilter(content_view=content_view).create() cvf.content_view.id = gen_integer(10000, 99999) with pytest.raises(HTTPError): cvf.update(['content_view']) @pytest.mark.tier2 - def test_negative_update_repo_by_id(self, sync_repo, content_view): + def test_negative_update_repo_by_id(self, sync_repo, content_view, target_sat): """Try to update content view filter using incorrect repository ID @@ -631,7 +643,7 @@ def test_negative_update_repo_by_id(self, sync_repo, content_view): :CaseLevel: Integration """ - cvf = entities.RPMContentViewFilter( + cvf = target_sat.api.RPMContentViewFilter( content_view=content_view, repository=[sync_repo], ).create() @@ -640,7 +652,7 @@ def test_negative_update_repo_by_id(self, sync_repo, content_view): cvf.update(['repository']) @pytest.mark.tier2 - def test_negative_update_repo(self, module_product, sync_repo, content_view): + def test_negative_update_repo(self, module_product, sync_repo, content_view, target_sat): """Try to update content view filter with new repository which doesn't belong to filter's content view @@ -652,12 +664,12 @@ def test_negative_update_repo(self, module_product, sync_repo, content_view): :CaseImportance: Low """ - cvf = entities.RPMContentViewFilter( + cvf = target_sat.api.RPMContentViewFilter( content_view=content_view, inclusion=True, repository=[sync_repo], ).create() - new_repo = entities.Repository(product=module_product).create() + new_repo = target_sat.api.Repository(product=module_product).create() new_repo.sync() cvf.repository = [new_repo] with pytest.raises(HTTPError): @@ -713,7 +725,7 @@ class TestContentViewFilterSearch: """Tests that search through content view filters.""" @pytest.mark.tier1 - def test_positive_search_erratum(self, content_view): + def test_positive_search_erratum(self, content_view, target_sat): """Search for an erratum content view filter's rules. :id: 6a86060f-6b4f-4688-8ea9-c198e0aeb3f6 @@ -724,11 +736,11 @@ def test_positive_search_erratum(self, content_view): :BZ: 1242534 """ - cv_filter = entities.ErratumContentViewFilter(content_view=content_view).create() - entities.ContentViewFilterRule(content_view_filter=cv_filter).search() + cv_filter = target_sat.api.ErratumContentViewFilter(content_view=content_view).create() + target_sat.api.ContentViewFilterRule(content_view_filter=cv_filter).search() @pytest.mark.tier1 - def test_positive_search_package_group(self, content_view): + def test_positive_search_package_group(self, content_view, target_sat): """Search for an package group content view filter's rules. :id: 832c50cc-c2c8-48c9-9a23-80956baf5f3c @@ -737,11 +749,11 @@ def test_positive_search_package_group(self, content_view): :CaseImportance: Critical """ - cv_filter = entities.PackageGroupContentViewFilter(content_view=content_view).create() - entities.ContentViewFilterRule(content_view_filter=cv_filter).search() + cv_filter = target_sat.api.PackageGroupContentViewFilter(content_view=content_view).create() + target_sat.api.ContentViewFilterRule(content_view_filter=cv_filter).search() @pytest.mark.tier1 - def test_positive_search_rpm(self, content_view): + def test_positive_search_rpm(self, content_view, target_sat): """Search for an rpm content view filter's rules. :id: 1c9058f1-35c4-46f2-9b21-155ef988564a @@ -750,8 +762,8 @@ def test_positive_search_rpm(self, content_view): :CaseImportance: Critical """ - cv_filter = entities.RPMContentViewFilter(content_view=content_view).create() - entities.ContentViewFilterRule(content_view_filter=cv_filter).search() + cv_filter = target_sat.api.RPMContentViewFilter(content_view=content_view).create() + target_sat.api.ContentViewFilterRule(content_view_filter=cv_filter).search() class TestContentViewFilterRule: @@ -761,7 +773,9 @@ class TestContentViewFilterRule: (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) @pytest.mark.tier2 - def test_positive_promote_module_stream_filter(self, module_org, content_view_module_stream): + def test_positive_promote_module_stream_filter( + self, module_org, content_view_module_stream, target_sat + ): """Verify Module Stream, Errata Count after Promote, Publish for Content View with Module Stream Exclude Filter @@ -774,14 +788,14 @@ def test_positive_promote_module_stream_filter(self, module_org, content_view_mo """ # Exclude module stream filter content_view = content_view_module_stream - cv_filter = entities.ModuleStreamContentViewFilter( + cv_filter = target_sat.api.ModuleStreamContentViewFilter( content_view=content_view, inclusion=False, ).create() - module_streams = entities.ModuleStream().search( + module_streams = target_sat.api.ModuleStream().search( query={'search': 'name="{}"'.format('duck')} ) - entities.ContentViewFilterRule( + target_sat.api.ContentViewFilterRule( content_view_filter=cv_filter, module_stream=module_streams ).create() content_view.publish() @@ -795,7 +809,7 @@ def test_positive_promote_module_stream_filter(self, module_org, content_view_mo assert content_view_version_info.errata_counts['total'] == 3 # Promote Content View - lce = entities.LifecycleEnvironment(organization=module_org).create() + lce = target_sat.api.LifecycleEnvironment(organization=module_org).create() content_view.version[0].promote(data={'environment_ids': lce.id, 'force': False}) content_view = content_view.read() content_view_version_info = content_view.version[0].read() @@ -808,7 +822,9 @@ def test_positive_promote_module_stream_filter(self, module_org, content_view_mo (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) @pytest.mark.tier2 - def test_positive_include_exclude_module_stream_filter(self, content_view_module_stream): + def test_positive_include_exclude_module_stream_filter( + self, content_view_module_stream, target_sat + ): """Verify Include and Exclude Errata filter(modular errata) automatically force the copy of the module streams associated to it. @@ -827,14 +843,14 @@ def test_positive_include_exclude_module_stream_filter(self, content_view_module :CaseLevel: Integration """ content_view = content_view_module_stream - cv_filter = entities.ErratumContentViewFilter( + cv_filter = target_sat.api.ErratumContentViewFilter( content_view=content_view, inclusion=True, ).create() - errata = entities.Errata().search( + errata = target_sat.api.Errata().search( query={'search': f'errata_id="{settings.repos.module_stream_0.errata[2]}"'} )[0] - entities.ContentViewFilterRule(content_view_filter=cv_filter, errata=errata).create() + target_sat.api.ContentViewFilterRule(content_view_filter=cv_filter, errata=errata).create() content_view.publish() content_view = content_view.read() @@ -846,14 +862,14 @@ def test_positive_include_exclude_module_stream_filter(self, content_view_module # delete the previous content_view_filter cv_filter.delete() - cv_filter = entities.ErratumContentViewFilter( + cv_filter = target_sat.api.ErratumContentViewFilter( content_view=content_view, inclusion=False, ).create() - errata = entities.Errata().search( + errata = target_sat.api.Errata().search( query={'search': f'errata_id="{settings.repos.module_stream_0.errata[2]}"'} )[0] - entities.ContentViewFilterRule(content_view_filter=cv_filter, errata=errata).create() + target_sat.api.ContentViewFilterRule(content_view_filter=cv_filter, errata=errata).create() content_view.publish() content_view_version_info = content_view.read().version[1].read() @@ -866,7 +882,7 @@ def test_positive_include_exclude_module_stream_filter(self, content_view_module (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) @pytest.mark.tier2 - def test_positive_multi_level_filters(self, content_view_module_stream): + def test_positive_multi_level_filters(self, content_view_module_stream, target_sat): """Verify promotion of Content View and Verify count after applying multi_filters (errata and module stream) @@ -878,24 +894,24 @@ def test_positive_multi_level_filters(self, content_view_module_stream): """ content_view = content_view_module_stream # apply include errata filter - cv_filter = entities.ErratumContentViewFilter( + cv_filter = target_sat.api.ErratumContentViewFilter( content_view=content_view, inclusion=True, ).create() - errata = entities.Errata().search( + errata = target_sat.api.Errata().search( query={'search': f'errata_id="{settings.repos.module_stream_0.errata[2]}"'} )[0] - entities.ContentViewFilterRule(content_view_filter=cv_filter, errata=errata).create() + target_sat.api.ContentViewFilterRule(content_view_filter=cv_filter, errata=errata).create() # apply exclude module filter - cv_filter = entities.ModuleStreamContentViewFilter( + cv_filter = target_sat.api.ModuleStreamContentViewFilter( content_view=content_view, inclusion=False, ).create() - module_streams = entities.ModuleStream().search( + module_streams = target_sat.api.ModuleStream().search( query={'search': 'name="{}"'.format('walrus')} ) - entities.ContentViewFilterRule( + target_sat.api.ContentViewFilterRule( content_view_filter=cv_filter, module_stream=module_streams ).create() content_view.publish() @@ -909,7 +925,9 @@ def test_positive_multi_level_filters(self, content_view_module_stream): (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) @pytest.mark.tier2 - def test_positive_dependency_solving_module_stream_filter(self, content_view_module_stream): + def test_positive_dependency_solving_module_stream_filter( + self, content_view_module_stream, target_sat + ): """Verify Module Stream Content View Filter's with Dependency Solve 'Yes'. If dependency solving enabled then module streams with deps will not get fetched over even if the exclude filter has been applied. @@ -927,14 +945,14 @@ def test_positive_dependency_solving_module_stream_filter(self, content_view_mod content_view = content_view_module_stream content_view.solve_dependencies = True content_view = content_view.update(['solve_dependencies']) - cv_filter = entities.ModuleStreamContentViewFilter( + cv_filter = target_sat.api.ModuleStreamContentViewFilter( content_view=content_view, inclusion=False, ).create() - module_streams = entities.ModuleStream().search( + module_streams = target_sat.api.ModuleStream().search( query={'search': 'name="{}" and version="{}'.format('duck', '20180704244205')} ) - entities.ContentViewFilterRule( + target_sat.api.ContentViewFilterRule( content_view_filter=cv_filter, module_stream=module_streams ).create() content_view.publish() diff --git a/tests/foreman/api/test_contentviewversion.py b/tests/foreman/api/test_contentviewversion.py index ca0e5e7cf36..5953edeed42 100644 --- a/tests/foreman/api/test_contentviewversion.py +++ b/tests/foreman/api/test_contentviewversion.py @@ -17,7 +17,6 @@ :Upstream: No """ from fauxfactory import gen_string -from nailgun import entities import pytest from requests.exceptions import HTTPError @@ -31,11 +30,13 @@ @pytest.fixture(scope='module') -def module_lce_cv(module_org): +def module_lce_cv(module_org, module_target_sat): """Create some entities for all tests.""" - lce1 = entities.LifecycleEnvironment(organization=module_org).create() - lce2 = entities.LifecycleEnvironment(organization=module_org, prior=lce1).create() - default_cv = entities.ContentView(organization=module_org, name=DEFAULT_CV).search() + lce1 = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() + lce2 = module_target_sat.api.LifecycleEnvironment(organization=module_org, prior=lce1).create() + default_cv = module_target_sat.api.ContentView( + organization=module_org, name=DEFAULT_CV + ).search() default_cvv = default_cv[0].version[0] return lce1, lce2, default_cvv @@ -68,7 +69,7 @@ def test_positive_create(module_cv): @pytest.mark.tier2 -def test_negative_create(module_org): +def test_negative_create(module_org, module_target_sat): """Attempt to create content view version using the 'Default Content View'. :id: 0afd49c6-f3a4-403e-9929-849f51ffa922 @@ -80,7 +81,7 @@ def test_negative_create(module_org): :CaseImportance: Critical """ # The default content view cannot be published - cv = entities.ContentView(organization=module_org.id, name=DEFAULT_CV).search() + cv = module_target_sat.api.ContentView(organization=module_org.id, name=DEFAULT_CV).search() # There should be only 1 record returned assert len(cv) == 1 with pytest.raises(HTTPError): @@ -91,8 +92,8 @@ def test_negative_create(module_org): @pytest.mark.tier2 -def test_positive_promote_valid_environment(module_lce_cv, module_org): - """Promote a content view version to 'next in sequence lifecycle environment. +def test_positive_promote_valid_environment(module_lce_cv, module_org, module_target_sat): + """Promote a content view version to next in sequence lifecycle environment. :id: f205ca06-8ab5-4546-83bd-deac4363d487 @@ -103,7 +104,7 @@ def test_positive_promote_valid_environment(module_lce_cv, module_org): :CaseImportance: Critical """ # Create a new content view... - cv = entities.ContentView(organization=module_org).create() + cv = module_target_sat.api.ContentView(organization=module_org).create() # ... and promote it. cv.publish() # Refresh the entity @@ -123,7 +124,7 @@ def test_positive_promote_valid_environment(module_lce_cv, module_org): @pytest.mark.tier2 -def test_positive_promote_out_of_sequence_environment(module_org, module_lce_cv): +def test_positive_promote_out_of_sequence_environment(module_org, module_lce_cv, module_target_sat): """Promote a content view version to a lifecycle environment that is 'out of sequence'. @@ -134,7 +135,7 @@ def test_positive_promote_out_of_sequence_environment(module_org, module_lce_cv) :CaseLevel: Integration """ # Create a new content view... - cv = entities.ContentView(organization=module_org).create() + cv = module_target_sat.api.ContentView(organization=module_org).create() # ... and publish it. cv.publish() # Refresh the entity @@ -168,7 +169,7 @@ def test_negative_promote_valid_environment(module_lce_cv): @pytest.mark.tier2 -def test_negative_promote_out_of_sequence_environment(module_lce_cv, module_org): +def test_negative_promote_out_of_sequence_environment(module_lce_cv, module_org, module_target_sat): """Attempt to promote a content view version to a Lifecycle environment that is 'out of sequence'. @@ -179,7 +180,7 @@ def test_negative_promote_out_of_sequence_environment(module_lce_cv, module_org) :CaseLevel: Integration """ # Create a new content view... - cv = entities.ContentView(organization=module_org).create() + cv = module_target_sat.api.ContentView(organization=module_org).create() # ... and publish it. cv.publish() # Refresh the entity @@ -197,7 +198,7 @@ def test_negative_promote_out_of_sequence_environment(module_lce_cv, module_org) @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_delete(module_org, module_product): +def test_positive_delete(module_org, module_product, module_target_sat): """Create content view and publish it. After that try to disassociate content view from 'Library' environment through 'delete_from_environment' command and delete content view version from @@ -213,15 +214,15 @@ def test_positive_delete(module_org, module_product): :CaseImportance: Critical """ key_content = DataFile.ZOO_CUSTOM_GPG_KEY.read_text() - gpgkey = entities.GPGKey(content=key_content, organization=module_org).create() + gpgkey = module_target_sat.api.GPGKey(content=key_content, organization=module_org).create() # Creates new repository with GPGKey - repo = entities.Repository( + repo = module_target_sat.api.Repository( gpg_key=gpgkey, product=module_product, url=settings.repos.yum_1.url ).create() # sync repository repo.sync() # Create content view - content_view = entities.ContentView(organization=module_org).create() + content_view = module_target_sat.api.ContentView(organization=module_org).create() # Associate repository to new content view content_view.repository = [repo] content_view = content_view.update(['repository']) @@ -242,7 +243,7 @@ def test_positive_delete(module_org, module_product): @pytest.mark.upgrade @pytest.mark.tier2 -def test_positive_delete_non_default(module_org): +def test_positive_delete_non_default(module_org, module_target_sat): """Create content view and publish and promote it to new environment. After that try to disassociate content view from 'Library' and one more non-default environment through 'delete_from_environment' @@ -256,13 +257,13 @@ def test_positive_delete_non_default(module_org): :CaseImportance: Critical """ - content_view = entities.ContentView(organization=module_org).create() + content_view = module_target_sat.api.ContentView(organization=module_org).create() # Publish content view content_view.publish() content_view = content_view.read() assert len(content_view.version) == 1 assert len(content_view.version[0].read().environment) == 1 - lce = entities.LifecycleEnvironment(organization=module_org).create() + lce = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() content_view.version[0].promote(data={'environment_ids': lce.id, 'force': False}) cvv = content_view.version[0].read() assert len(cvv.environment) == 2 @@ -277,7 +278,7 @@ def test_positive_delete_non_default(module_org): @pytest.mark.upgrade @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_delete_composite_version(module_org): +def test_positive_delete_composite_version(module_org, module_target_sat): """Create composite content view and publish it. After that try to disassociate content view from 'Library' environment through 'delete_from_environment' command and delete content view version from @@ -293,14 +294,16 @@ def test_positive_delete_composite_version(module_org): :BZ: 1276479 """ # Create product with repository and publish it - product = entities.Product(organization=module_org).create() - repo = entities.Repository(product=product, url=settings.repos.yum_1.url).create() + product = module_target_sat.api.Product(organization=module_org).create() + repo = module_target_sat.api.Repository(product=product, url=settings.repos.yum_1.url).create() repo.sync() # Create and publish content views - content_view = entities.ContentView(organization=module_org, repository=[repo]).create() + content_view = module_target_sat.api.ContentView( + organization=module_org, repository=[repo] + ).create() content_view.publish() # Create and publish composite content view - composite_cv = entities.ContentView( + composite_cv = module_target_sat.api.ContentView( organization=module_org, composite=True, component=[content_view.read().version[0]] ).create() composite_cv.publish() @@ -318,7 +321,7 @@ def test_positive_delete_composite_version(module_org): @pytest.mark.tier2 -def test_negative_delete(module_org): +def test_negative_delete(module_org, module_target_sat): """Create content view and publish it. Try to delete content view version while content view is still associated with lifecycle environment @@ -331,7 +334,7 @@ def test_negative_delete(module_org): :CaseImportance: Critical """ - content_view = entities.ContentView(organization=module_org).create() + content_view = module_target_sat.api.ContentView(organization=module_org).create() # Publish content view content_view.publish() content_view = content_view.read() @@ -344,7 +347,7 @@ def test_negative_delete(module_org): @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_remove_renamed_cv_version_from_default_env(module_org): +def test_positive_remove_renamed_cv_version_from_default_env(module_org, module_target_sat): """Remove version of renamed content view from Library environment :id: 7d5961d0-6a9a-4610-979e-cbc4ddbc50ca @@ -364,11 +367,13 @@ def test_positive_remove_renamed_cv_version_from_default_env(module_org): """ new_name = gen_string('alpha') # create yum product and repo - product = entities.Product(organization=module_org).create() - yum_repo = entities.Repository(url=settings.repos.yum_1.url, product=product).create() + product = module_target_sat.api.Product(organization=module_org).create() + yum_repo = module_target_sat.api.Repository( + url=settings.repos.yum_1.url, product=product + ).create() yum_repo.sync() # create a content view and add the yum repo to it - content_view = entities.ContentView(organization=module_org).create() + content_view = module_target_sat.api.ContentView(organization=module_org).create() content_view.repository = [yum_repo] content_view = content_view.update(['repository']) # publish the content view @@ -377,7 +382,9 @@ def test_positive_remove_renamed_cv_version_from_default_env(module_org): assert len(content_view.version) == 1 content_view_version = content_view.version[0].read() assert len(content_view_version.environment) == 1 - lce_library = entities.LifecycleEnvironment(id=content_view_version.environment[0].id).read() + lce_library = module_target_sat.api.LifecycleEnvironment( + id=content_view_version.environment[0].id + ).read() # ensure that the content view version is promoted to the Library # lifecycle environment assert lce_library.name == ENVIRONMENT @@ -393,7 +400,7 @@ def test_positive_remove_renamed_cv_version_from_default_env(module_org): @pytest.mark.tier2 -def test_positive_remove_qe_promoted_cv_version_from_default_env(module_org): +def test_positive_remove_qe_promoted_cv_version_from_default_env(module_org, module_target_sat): """Remove QE promoted content view version from Library environment :id: c7795762-93bd-419c-ac49-d10dc26b842b @@ -412,10 +419,12 @@ def test_positive_remove_qe_promoted_cv_version_from_default_env(module_org): :CaseLevel: Integration """ - lce_dev = entities.LifecycleEnvironment(organization=module_org).create() - lce_qe = entities.LifecycleEnvironment(organization=module_org, prior=lce_dev).create() - product = entities.Product(organization=module_org).create() - docker_repo = entities.Repository( + lce_dev = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() + lce_qe = module_target_sat.api.LifecycleEnvironment( + organization=module_org, prior=lce_dev + ).create() + product = module_target_sat.api.Product(organization=module_org).create() + docker_repo = module_target_sat.api.Repository( content_type='docker', docker_upstream_name='busybox', product=product, @@ -423,7 +432,7 @@ def test_positive_remove_qe_promoted_cv_version_from_default_env(module_org): ).create() docker_repo.sync() # create a content view and add to it the docker repo - content_view = entities.ContentView(organization=module_org).create() + content_view = module_target_sat.api.ContentView(organization=module_org).create() content_view.repository = [docker_repo] content_view = content_view.update(['repository']) # publish the content view @@ -432,7 +441,9 @@ def test_positive_remove_qe_promoted_cv_version_from_default_env(module_org): assert len(content_view.version) == 1 content_view_version = content_view.version[0].read() assert len(content_view_version.environment) == 1 - lce_library = entities.LifecycleEnvironment(id=content_view_version.environment[0].id).read() + lce_library = module_target_sat.api.LifecycleEnvironment( + id=content_view_version.environment[0].id + ).read() assert lce_library.name == ENVIRONMENT # promote content view version to DEV and QE lifecycle environments for lce in [lce_dev, lce_qe]: @@ -449,7 +460,7 @@ def test_positive_remove_qe_promoted_cv_version_from_default_env(module_org): @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_remove_prod_promoted_cv_version_from_default_env(module_org): +def test_positive_remove_prod_promoted_cv_version_from_default_env(module_org, module_target_sat): """Remove PROD promoted content view version from Library environment :id: 24911876-7c2a-4a12-a3aa-98051dfda29d @@ -468,13 +479,19 @@ def test_positive_remove_prod_promoted_cv_version_from_default_env(module_org): :CaseLevel: Integration """ - lce_dev = entities.LifecycleEnvironment(organization=module_org).create() - lce_qe = entities.LifecycleEnvironment(organization=module_org, prior=lce_dev).create() - lce_prod = entities.LifecycleEnvironment(organization=module_org, prior=lce_qe).create() - product = entities.Product(organization=module_org).create() - yum_repo = entities.Repository(url=settings.repos.yum_1.url, product=product).create() + lce_dev = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() + lce_qe = module_target_sat.api.LifecycleEnvironment( + organization=module_org, prior=lce_dev + ).create() + lce_prod = module_target_sat.api.LifecycleEnvironment( + organization=module_org, prior=lce_qe + ).create() + product = module_target_sat.api.Product(organization=module_org).create() + yum_repo = module_target_sat.api.Repository( + url=settings.repos.yum_1.url, product=product + ).create() yum_repo.sync() - docker_repo = entities.Repository( + docker_repo = module_target_sat.api.Repository( content_type='docker', docker_upstream_name='busybox', product=product, @@ -482,7 +499,7 @@ def test_positive_remove_prod_promoted_cv_version_from_default_env(module_org): ).create() docker_repo.sync() # create a content view and add to it the yum and docker repos - content_view = entities.ContentView(organization=module_org).create() + content_view = module_target_sat.api.ContentView(organization=module_org).create() content_view.repository = [yum_repo, docker_repo] content_view = content_view.update(['repository']) # publish the content view @@ -491,7 +508,9 @@ def test_positive_remove_prod_promoted_cv_version_from_default_env(module_org): assert len(content_view.version) == 1 content_view_version = content_view.version[0].read() assert len(content_view_version.environment) == 1 - lce_library = entities.LifecycleEnvironment(id=content_view_version.environment[0].id).read() + lce_library = module_target_sat.api.LifecycleEnvironment( + id=content_view_version.environment[0].id + ).read() assert lce_library.name == ENVIRONMENT # promote content view version to DEV QE PROD lifecycle environments for lce in [lce_dev, lce_qe, lce_prod]: @@ -510,7 +529,7 @@ def test_positive_remove_prod_promoted_cv_version_from_default_env(module_org): @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_remove_cv_version_from_env(module_org): +def test_positive_remove_cv_version_from_env(module_org, module_target_sat): """Remove promoted content view version from environment :id: 17cf18bf-09d5-4641-b0e0-c50e628fa6c8 @@ -532,15 +551,23 @@ def test_positive_remove_cv_version_from_env(module_org): :CaseLevel: Integration """ - lce_dev = entities.LifecycleEnvironment(organization=module_org).create() - lce_qe = entities.LifecycleEnvironment(organization=module_org, prior=lce_dev).create() - lce_stage = entities.LifecycleEnvironment(organization=module_org, prior=lce_qe).create() - lce_prod = entities.LifecycleEnvironment(organization=module_org, prior=lce_stage).create() - product = entities.Product(organization=module_org).create() - yum_repo = entities.Repository(url=settings.repos.yum_1.url, product=product).create() + lce_dev = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() + lce_qe = module_target_sat.api.LifecycleEnvironment( + organization=module_org, prior=lce_dev + ).create() + lce_stage = module_target_sat.api.LifecycleEnvironment( + organization=module_org, prior=lce_qe + ).create() + lce_prod = module_target_sat.api.LifecycleEnvironment( + organization=module_org, prior=lce_stage + ).create() + product = module_target_sat.api.Product(organization=module_org).create() + yum_repo = module_target_sat.api.Repository( + url=settings.repos.yum_1.url, product=product + ).create() yum_repo.sync() # docker repo - docker_repo = entities.Repository( + docker_repo = module_target_sat.api.Repository( content_type='docker', docker_upstream_name='busybox', product=product, @@ -548,7 +575,7 @@ def test_positive_remove_cv_version_from_env(module_org): ).create() docker_repo.sync() # create a content view and add the yum and docker repo to it - content_view = entities.ContentView(organization=module_org).create() + content_view = module_target_sat.api.ContentView(organization=module_org).create() content_view.repository = [yum_repo, docker_repo] content_view = content_view.update(['repository']) # publish the content view @@ -557,7 +584,9 @@ def test_positive_remove_cv_version_from_env(module_org): assert len(content_view.version) == 1 content_view_version = content_view.version[0].read() assert len(content_view_version.environment) == 1 - lce_library = entities.LifecycleEnvironment(id=content_view_version.environment[0].id).read() + lce_library = module_target_sat.api.LifecycleEnvironment( + id=content_view_version.environment[0].id + ).read() assert lce_library.name == ENVIRONMENT # promote content view version to DEV QE STAGE PROD lifecycle # environments @@ -582,7 +611,7 @@ def test_positive_remove_cv_version_from_env(module_org): @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_remove_cv_version_from_multi_env(module_org): +def test_positive_remove_cv_version_from_multi_env(module_org, module_target_sat): """Remove promoted content view version from multiple environments :id: 18b86a68-8e6a-43ea-b95e-188fba125a26 @@ -602,14 +631,22 @@ def test_positive_remove_cv_version_from_multi_env(module_org): :CaseImportance: Low """ - lce_dev = entities.LifecycleEnvironment(organization=module_org).create() - lce_qe = entities.LifecycleEnvironment(organization=module_org, prior=lce_dev).create() - lce_stage = entities.LifecycleEnvironment(organization=module_org, prior=lce_qe).create() - lce_prod = entities.LifecycleEnvironment(organization=module_org, prior=lce_stage).create() - product = entities.Product(organization=module_org).create() - yum_repo = entities.Repository(url=settings.repos.yum_1.url, product=product).create() + lce_dev = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() + lce_qe = module_target_sat.api.LifecycleEnvironment( + organization=module_org, prior=lce_dev + ).create() + lce_stage = module_target_sat.api.LifecycleEnvironment( + organization=module_org, prior=lce_qe + ).create() + lce_prod = module_target_sat.api.LifecycleEnvironment( + organization=module_org, prior=lce_stage + ).create() + product = module_target_sat.api.Product(organization=module_org).create() + yum_repo = module_target_sat.api.Repository( + url=settings.repos.yum_1.url, product=product + ).create() yum_repo.sync() - docker_repo = entities.Repository( + docker_repo = module_target_sat.api.Repository( content_type='docker', docker_upstream_name='busybox', product=product, @@ -617,7 +654,7 @@ def test_positive_remove_cv_version_from_multi_env(module_org): ).create() docker_repo.sync() # create a content view and add the yum repo to it - content_view = entities.ContentView(organization=module_org).create() + content_view = module_target_sat.api.ContentView(organization=module_org).create() content_view.repository = [yum_repo, docker_repo] content_view = content_view.update(['repository']) # publish the content view @@ -626,7 +663,9 @@ def test_positive_remove_cv_version_from_multi_env(module_org): assert len(content_view.version) == 1 content_view_version = content_view.version[0].read() assert len(content_view_version.environment) == 1 - lce_library = entities.LifecycleEnvironment(id=content_view_version.environment[0].id).read() + lce_library = module_target_sat.api.LifecycleEnvironment( + id=content_view_version.environment[0].id + ).read() assert lce_library.name == ENVIRONMENT # promote content view version to DEV QE STAGE PROD lifecycle # environments @@ -648,7 +687,7 @@ def test_positive_remove_cv_version_from_multi_env(module_org): @pytest.mark.upgrade @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_delete_cv_promoted_to_multi_env(module_org): +def test_positive_delete_cv_promoted_to_multi_env(module_org, module_target_sat): """Delete published content view with version promoted to multiple environments @@ -664,20 +703,28 @@ def test_positive_delete_cv_promoted_to_multi_env(module_org): 5. Delete the content view, this should delete the content with all it's published/promoted versions from all environments - :expectedresults: The content view doesn't exists + :expectedresults: The content view doesn't exist :CaseLevel: Integration :CaseImportance: Critical """ - lce_dev = entities.LifecycleEnvironment(organization=module_org).create() - lce_qe = entities.LifecycleEnvironment(organization=module_org, prior=lce_dev).create() - lce_stage = entities.LifecycleEnvironment(organization=module_org, prior=lce_qe).create() - lce_prod = entities.LifecycleEnvironment(organization=module_org, prior=lce_stage).create() - product = entities.Product(organization=module_org).create() - yum_repo = entities.Repository(url=settings.repos.yum_1.url, product=product).create() + lce_dev = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() + lce_qe = module_target_sat.api.LifecycleEnvironment( + organization=module_org, prior=lce_dev + ).create() + lce_stage = module_target_sat.api.LifecycleEnvironment( + organization=module_org, prior=lce_qe + ).create() + lce_prod = module_target_sat.api.LifecycleEnvironment( + organization=module_org, prior=lce_stage + ).create() + product = module_target_sat.api.Product(organization=module_org).create() + yum_repo = module_target_sat.api.Repository( + url=settings.repos.yum_1.url, product=product + ).create() yum_repo.sync() - docker_repo = entities.Repository( + docker_repo = module_target_sat.api.Repository( content_type='docker', docker_upstream_name='busybox', product=product, @@ -685,7 +732,7 @@ def test_positive_delete_cv_promoted_to_multi_env(module_org): ).create() docker_repo.sync() # create a content view and add the yum repo to it - content_view = entities.ContentView(organization=module_org).create() + content_view = module_target_sat.api.ContentView(organization=module_org).create() content_view.repository = [yum_repo, docker_repo] content_view = content_view.update(['repository']) # publish the content view @@ -694,7 +741,9 @@ def test_positive_delete_cv_promoted_to_multi_env(module_org): assert len(content_view.version) == 1 content_view_version = content_view.version[0].read() assert len(content_view_version.environment) == 1 - lce_library = entities.LifecycleEnvironment(id=content_view_version.environment[0].id).read() + lce_library = module_target_sat.api.LifecycleEnvironment( + id=content_view_version.environment[0].id + ).read() assert lce_library.name == ENVIRONMENT # promote content view version to DEV QE STAGE PROD lifecycle # environments diff --git a/tests/foreman/api/test_discoveryrule.py b/tests/foreman/api/test_discoveryrule.py index 51ac5fec162..528647a3341 100644 --- a/tests/foreman/api/test_discoveryrule.py +++ b/tests/foreman/api/test_discoveryrule.py @@ -16,16 +16,34 @@ :Upstream: No """ -from fauxfactory import gen_choice, gen_integer +from fauxfactory import gen_choice, gen_integer, gen_string import pytest from requests.exceptions import HTTPError from robottelo.utils.datafactory import valid_data_list +@pytest.fixture(scope='module') +def module_hostgroup(module_org, module_target_sat): + module_hostgroup = module_target_sat.api.HostGroup(organization=[module_org]).create() + module_hostgroup.delete() + + +@pytest.fixture(scope='module') +def module_location(module_location): + yield module_location + module_location.delete() + + +@pytest.fixture(scope='module') +def module_org(module_org): + yield module_org + module_org.delete() + + @pytest.mark.tier1 @pytest.mark.e2e -def test_positive_end_to_end_crud(module_org, module_location, module_hostgroup, target_sat): +def test_positive_end_to_end_crud(module_org, module_location, module_hostgroup, module_target_sat): """Create a new discovery rule with several attributes, update them and delete the rule itself. @@ -47,7 +65,7 @@ def test_positive_end_to_end_crud(module_org, module_location, module_hostgroup, name = gen_choice(list(valid_data_list().values())) search = gen_choice(searches) hostname = 'myhost-<%= rand(99999) %>' - discovery_rule = target_sat.api.DiscoveryRule( + discovery_rule = module_target_sat.api.DiscoveryRule( name=name, search_=search, hostname=hostname, @@ -83,6 +101,21 @@ def test_positive_end_to_end_crud(module_org, module_location, module_hostgroup, discovery_rule.read() +@pytest.mark.tier1 +def test_negative_create_with_invalid_host_limit_and_priority(module_target_sat): + """Create a discovery rule with invalid host limit and priority + + :id: e3c7acb1-ac56-496b-ac04-2a83f66ec290 + + :expectedresults: Validation error should be raised + """ + with pytest.raises(HTTPError): + module_target_sat.api.DiscoveryRule(max_count=gen_string('alpha')).create() + with pytest.raises(HTTPError): + module_target_sat.api.DiscoveryRule(priority=gen_string('alpha')).create() + + +@pytest.mark.stubbed @pytest.mark.tier3 def test_positive_update_and_provision_with_rule_priority( module_target_sat, module_discovery_hostgroup, discovery_location, discovery_org diff --git a/tests/foreman/api/test_docker.py b/tests/foreman/api/test_docker.py index e87dac66fb6..4b6dc88537c 100644 --- a/tests/foreman/api/test_docker.py +++ b/tests/foreman/api/test_docker.py @@ -15,7 +15,6 @@ from random import choice, randint, shuffle from fauxfactory import gen_string, gen_url -from nailgun import entities import pytest from requests.exceptions import HTTPError @@ -31,7 +30,7 @@ DOCKER_PROVIDER = 'Docker' -def _create_repository(product, name=None, upstream_name=None): +def _create_repository(module_target_sat, product, name=None, upstream_name=None): """Create a Docker-based repository. :param product: A ``Product`` object. @@ -45,7 +44,7 @@ def _create_repository(product, name=None, upstream_name=None): name = choice(generate_strings_list(15, ['numeric', 'html'])) if upstream_name is None: upstream_name = CONTAINER_UPSTREAM_NAME - return entities.Repository( + return module_target_sat.api.Repository( content_type='docker', docker_upstream_name=upstream_name, name=name, @@ -55,21 +54,21 @@ def _create_repository(product, name=None, upstream_name=None): @pytest.fixture -def repo(module_product): +def repo(module_product, module_target_sat): """Create a single repository.""" - return _create_repository(module_product) + return _create_repository(module_target_sat, module_product) @pytest.fixture -def repos(module_product): +def repos(module_product, module_target_sat): """Create and return a list of repositories.""" - return [_create_repository(module_product) for _ in range(randint(2, 5))] + return [_create_repository(module_target_sat, module_product) for _ in range(randint(2, 5))] @pytest.fixture -def content_view(module_org): +def content_view(module_org, module_target_sat): """Create a content view.""" - return entities.ContentView(composite=False, organization=module_org).create() + return module_target_sat.api.ContentView(composite=False, organization=module_org).create() @pytest.fixture @@ -107,7 +106,7 @@ class TestDockerRepository: @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(valid_docker_repository_names())) - def test_positive_create_with_name(self, module_product, name): + def test_positive_create_with_name(self, module_product, name, module_target_sat): """Create one Docker-type repository :id: 3360aab2-74f3-4f6e-a083-46498ceacad2 @@ -119,14 +118,16 @@ def test_positive_create_with_name(self, module_product, name): :CaseImportance: Critical """ - repo = _create_repository(module_product, name) + repo = _create_repository(module_target_sat, module_product, name) assert repo.name == name assert repo.docker_upstream_name == CONTAINER_UPSTREAM_NAME assert repo.content_type == 'docker' @pytest.mark.tier1 @pytest.mark.parametrize('upstream_name', **parametrized(valid_docker_upstream_names())) - def test_positive_create_with_upstream_name(self, module_product, upstream_name): + def test_positive_create_with_upstream_name( + self, module_product, upstream_name, module_target_sat + ): """Create a Docker-type repository with a valid docker upstream name @@ -139,13 +140,15 @@ def test_positive_create_with_upstream_name(self, module_product, upstream_name) :CaseImportance: Critical """ - repo = _create_repository(module_product, upstream_name=upstream_name) + repo = _create_repository(module_target_sat, module_product, upstream_name=upstream_name) assert repo.docker_upstream_name == upstream_name assert repo.content_type == 'docker' @pytest.mark.tier1 @pytest.mark.parametrize('upstream_name', **parametrized(invalid_docker_upstream_names())) - def test_negative_create_with_invalid_upstream_name(self, module_product, upstream_name): + def test_negative_create_with_invalid_upstream_name( + self, module_product, upstream_name, module_target_sat + ): """Create a Docker-type repository with a invalid docker upstream name. @@ -159,25 +162,25 @@ def test_negative_create_with_invalid_upstream_name(self, module_product, upstre :CaseImportance: Critical """ with pytest.raises(HTTPError): - _create_repository(module_product, upstream_name=upstream_name) + _create_repository(module_target_sat, module_product, upstream_name=upstream_name) @pytest.mark.tier2 - def test_positive_create_repos_using_same_product(self, module_product): + def test_positive_create_repos_using_same_product(self, module_product, module_target_sat): """Create multiple Docker-type repositories :id: 4a6929fc-5111-43ff-940c-07a754828630 :expectedresults: Multiple docker repositories are created with a - Docker usptream repository and they all belong to the same product. + Docker upstream repository and they all belong to the same product. :CaseLevel: Integration """ for _ in range(randint(2, 5)): - repo = _create_repository(module_product) + repo = _create_repository(module_target_sat, module_product) assert repo.id in [repo_.id for repo_ in module_product.read().repository] @pytest.mark.tier2 - def test_positive_create_repos_using_multiple_products(self, module_org): + def test_positive_create_repos_using_multiple_products(self, module_org, module_target_sat): """Create multiple Docker-type repositories on multiple products :id: 5a65d20b-d3b5-4bd7-9c8f-19c8af190558 @@ -189,14 +192,14 @@ def test_positive_create_repos_using_multiple_products(self, module_org): :CaseLevel: Integration """ for _ in range(randint(2, 5)): - product = entities.Product(organization=module_org).create() + product = module_target_sat.api.Product(organization=module_org).create() for _ in range(randint(2, 3)): - repo = _create_repository(product) + repo = _create_repository(module_target_sat, product) product = product.read() assert repo.id in [repo_.id for repo_ in product.repository] @pytest.mark.tier1 - def test_positive_sync(self, module_product): + def test_positive_sync(self, module_product, module_target_sat): """Create and sync a Docker-type repository :id: 80fbcd84-1c6f-444f-a44e-7d2738a0cba2 @@ -206,14 +209,14 @@ def test_positive_sync(self, module_product): :CaseImportance: Critical """ - repo = _create_repository(module_product) + repo = _create_repository(module_target_sat, module_product) repo.sync(timeout=600) repo = repo.read() assert repo.content_counts['docker_manifest'] >= 1 @pytest.mark.tier1 @pytest.mark.parametrize('new_name', **parametrized(valid_docker_repository_names())) - def test_positive_update_name(self, module_product, repo, new_name): + def test_positive_update_name(self, repo, new_name): """Create a Docker-type repository and update its name. :id: 7967e6b5-c206-4ad0-bcf5-64a7ce85233b @@ -249,7 +252,7 @@ def test_positive_update_upstream_name(self, repo): assert repo.docker_upstream_name == new_upstream_name @pytest.mark.tier2 - def test_positive_update_url(self, module_product, repo): + def test_positive_update_url(self, repo): """Create a Docker-type repository and update its URL. :id: 6a588e65-bf1d-4ca9-82ce-591f9070215f @@ -285,7 +288,7 @@ def test_positive_delete(self, repo): repo.read() @pytest.mark.tier2 - def test_positive_delete_random_repo(self, module_org): + def test_positive_delete_random_repo(self, module_org, module_target_sat): """Create Docker-type repositories on multiple products and delete a random repository from a random product. @@ -296,10 +299,11 @@ def test_positive_delete_random_repo(self, module_org): """ repos = [] products = [ - entities.Product(organization=module_org).create() for _ in range(randint(2, 5)) + module_target_sat.api.Product(organization=module_org).create() + for _ in range(randint(2, 5)) ] for product in products: - repo = _create_repository(product) + repo = _create_repository(module_target_sat, product) assert repo.content_type == 'docker' repos.append(repo) @@ -341,7 +345,7 @@ def test_positive_add_docker_repo(self, repo, content_view): assert repo.id in [repo_.id for repo_ in content_view.repository] @pytest.mark.tier2 - def test_positive_add_docker_repos(self, module_org, module_product, content_view): + def test_positive_add_docker_repos(self, module_target_sat, module_product, content_view): """Add multiple Docker-type repositories to a non-composite content view. @@ -351,7 +355,7 @@ def test_positive_add_docker_repos(self, module_org, module_product, content_vie and the product is added to a non-composite content view. """ repos = [ - _create_repository(module_product, name=gen_string('alpha')) + _create_repository(module_target_sat, module_product, name=gen_string('alpha')) for _ in range(randint(2, 5)) ] repo_ids = {r.id for r in repos} @@ -369,7 +373,7 @@ def test_positive_add_docker_repos(self, module_org, module_product, content_vie assert r.docker_upstream_name == CONTAINER_UPSTREAM_NAME @pytest.mark.tier2 - def test_positive_add_synced_docker_repo(self, module_org, module_product): + def test_positive_add_synced_docker_repo(self, module_org, module_product, module_target_sat): """Create and sync a Docker-type repository :id: 3c7d6f17-266e-43d3-99f8-13bf0251eca6 @@ -377,19 +381,21 @@ def test_positive_add_synced_docker_repo(self, module_org, module_product): :expectedresults: A repository is created with a Docker repository and it is synchronized. """ - repo = _create_repository(module_product) + repo = _create_repository(module_target_sat, module_product) repo.sync(timeout=600) repo = repo.read() assert repo.content_counts['docker_manifest'] > 0 # Create content view and associate docker repo - content_view = entities.ContentView(composite=False, organization=module_org).create() + content_view = module_target_sat.api.ContentView( + composite=False, organization=module_org + ).create() content_view.repository = [repo] content_view = content_view.update(['repository']) assert repo.id in [repo_.id for repo_ in content_view.repository] @pytest.mark.tier2 - def test_positive_add_docker_repo_to_ccv(self, module_org): + def test_positive_add_docker_repo_to_ccv(self, module_org, module_target_sat): """Add one Docker-type repository to a composite content view :id: fe278275-2bb2-4d68-8624-f0cfd63ecb57 @@ -398,10 +404,14 @@ def test_positive_add_docker_repo_to_ccv(self, module_org): the product is added to a content view which is then added to a composite content view. """ - repo = _create_repository(entities.Product(organization=module_org).create()) + repo = _create_repository( + module_target_sat, module_target_sat.api.Product(organization=module_org).create() + ) # Create content view and associate docker repo - content_view = entities.ContentView(composite=False, organization=module_org).create() + content_view = module_target_sat.api.ContentView( + composite=False, organization=module_org + ).create() content_view.repository = [repo] content_view = content_view.update(['repository']) assert repo.id in [repo_.id for repo_ in content_view.repository] @@ -412,7 +422,9 @@ def test_positive_add_docker_repo_to_ccv(self, module_org): assert len(content_view.version) == 1 # Create composite content view and associate content view to it - comp_content_view = entities.ContentView(composite=True, organization=module_org).create() + comp_content_view = module_target_sat.api.ContentView( + composite=True, organization=module_org + ).create() comp_content_view.component = content_view.version comp_content_view = comp_content_view.update(['component']) assert content_view.version[0].id in [ @@ -420,7 +432,7 @@ def test_positive_add_docker_repo_to_ccv(self, module_org): ] @pytest.mark.tier2 - def test_positive_add_docker_repos_to_ccv(self, module_org): + def test_positive_add_docker_repos_to_ccv(self, module_org, module_target_sat): """Add multiple Docker-type repositories to a composite content view. @@ -431,11 +443,13 @@ def test_positive_add_docker_repos_to_ccv(self, module_org): views which are then added to a composite content view. """ cv_versions = [] - product = entities.Product(organization=module_org).create() + product = module_target_sat.api.Product(organization=module_org).create() for _ in range(randint(2, 5)): # Create content view and associate docker repo - content_view = entities.ContentView(composite=False, organization=module_org).create() - repo = _create_repository(product) + content_view = module_target_sat.api.ContentView( + composite=False, organization=module_org + ).create() + repo = _create_repository(module_target_sat, product) content_view.repository = [repo] content_view = content_view.update(['repository']) assert repo.id in [repo_.id for repo_ in content_view.repository] @@ -446,14 +460,16 @@ def test_positive_add_docker_repos_to_ccv(self, module_org): cv_versions.append(content_view.version[0]) # Create composite content view and associate content view to it - comp_content_view = entities.ContentView(composite=True, organization=module_org).create() + comp_content_view = module_target_sat.api.ContentView( + composite=True, organization=module_org + ).create() for cv_version in cv_versions: comp_content_view.component.append(cv_version) comp_content_view = comp_content_view.update(['component']) assert cv_version.id in [component.id for component in comp_content_view.component] @pytest.mark.tier2 - def test_positive_publish_with_docker_repo(self, module_org): + def test_positive_publish_with_docker_repo(self, module_org, module_target_sat): """Add Docker-type repository to content view and publish it once. :id: 86a73e96-ead6-41fb-8095-154a0b83e344 @@ -462,9 +478,13 @@ def test_positive_publish_with_docker_repo(self, module_org): repository and the product is added to a content view which is then published only once. """ - repo = _create_repository(entities.Product(organization=module_org).create()) + repo = _create_repository( + module_target_sat, module_target_sat.api.Product(organization=module_org).create() + ) - content_view = entities.ContentView(composite=False, organization=module_org).create() + content_view = module_target_sat.api.ContentView( + composite=False, organization=module_org + ).create() content_view.repository = [repo] content_view = content_view.update(['repository']) assert repo.id in [repo_.id for repo_ in content_view.repository] @@ -481,7 +501,7 @@ def test_positive_publish_with_docker_repo(self, module_org): assert float(content_view.next_version) > 1.0 @pytest.mark.tier2 - def test_positive_publish_with_docker_repo_composite(self, module_org): + def test_positive_publish_with_docker_repo_composite(self, module_org, module_target_sat): """Add Docker-type repository to composite content view and publish it once. @@ -494,8 +514,12 @@ def test_positive_publish_with_docker_repo_composite(self, module_org): :BZ: 1217635 """ - repo = _create_repository(entities.Product(organization=module_org).create()) - content_view = entities.ContentView(composite=False, organization=module_org).create() + repo = _create_repository( + module_target_sat, module_target_sat.api.Product(organization=module_org).create() + ) + content_view = module_target_sat.api.ContentView( + composite=False, organization=module_org + ).create() content_view.repository = [repo] content_view = content_view.update(['repository']) assert repo.id in [repo_.id for repo_ in content_view.repository] @@ -512,7 +536,9 @@ def test_positive_publish_with_docker_repo_composite(self, module_org): assert float(content_view.next_version) > 1.0 # Create composite content view… - comp_content_view = entities.ContentView(composite=True, organization=module_org).create() + comp_content_view = module_target_sat.api.ContentView( + composite=True, organization=module_org + ).create() comp_content_view.component = [content_view.version[0]] comp_content_view = comp_content_view.update(['component']) assert content_view.version[0].id in [ @@ -526,7 +552,7 @@ def test_positive_publish_with_docker_repo_composite(self, module_org): assert float(comp_content_view.next_version) > 1.0 @pytest.mark.tier2 - def test_positive_publish_multiple_with_docker_repo(self, module_org): + def test_positive_publish_multiple_with_docker_repo(self, module_org, module_target_sat): """Add Docker-type repository to content view and publish it multiple times. @@ -536,8 +562,12 @@ def test_positive_publish_multiple_with_docker_repo(self, module_org): repository and the product is added to a content view which is then published multiple times. """ - repo = _create_repository(entities.Product(organization=module_org).create()) - content_view = entities.ContentView(composite=False, organization=module_org).create() + repo = _create_repository( + module_target_sat, module_target_sat.api.Product(organization=module_org).create() + ) + content_view = module_target_sat.api.ContentView( + composite=False, organization=module_org + ).create() content_view.repository = [repo] content_view = content_view.update(['repository']) assert [repo.id] == [repo_.id for repo_ in content_view.repository] @@ -551,7 +581,9 @@ def test_positive_publish_multiple_with_docker_repo(self, module_org): assert len(content_view.version) == publish_amount @pytest.mark.tier2 - def test_positive_publish_multiple_with_docker_repo_composite(self, module_org): + def test_positive_publish_multiple_with_docker_repo_composite( + self, module_org, module_target_sat + ): """Add Docker-type repository to content view and publish it multiple times. @@ -562,8 +594,12 @@ def test_positive_publish_multiple_with_docker_repo_composite(self, module_org): added to a composite content view which is then published multiple times. """ - repo = _create_repository(entities.Product(organization=module_org).create()) - content_view = entities.ContentView(composite=False, organization=module_org).create() + repo = _create_repository( + module_target_sat, module_target_sat.api.Product(organization=module_org).create() + ) + content_view = module_target_sat.api.ContentView( + composite=False, organization=module_org + ).create() content_view.repository = [repo] content_view = content_view.update(['repository']) assert [repo.id] == [repo_.id for repo_ in content_view.repository] @@ -573,7 +609,9 @@ def test_positive_publish_multiple_with_docker_repo_composite(self, module_org): content_view = content_view.read() assert content_view.last_published is not None - comp_content_view = entities.ContentView(composite=True, organization=module_org).create() + comp_content_view = module_target_sat.api.ContentView( + composite=True, organization=module_org + ).create() comp_content_view.component = [content_view.version[0]] comp_content_view = comp_content_view.update(['component']) assert [content_view.version[0].id] == [comp.id for comp in comp_content_view.component] @@ -587,7 +625,7 @@ def test_positive_publish_multiple_with_docker_repo_composite(self, module_org): assert len(comp_content_view.version) == publish_amount @pytest.mark.tier2 - def test_positive_promote_with_docker_repo(self, module_org): + def test_positive_promote_with_docker_repo(self, module_org, module_target_sat): """Add Docker-type repository to content view and publish it. Then promote it to the next available lifecycle-environment. @@ -596,10 +634,14 @@ def test_positive_promote_with_docker_repo(self, module_org): :expectedresults: Docker-type repository is promoted to content view found in the specific lifecycle-environment. """ - lce = entities.LifecycleEnvironment(organization=module_org).create() - repo = _create_repository(entities.Product(organization=module_org).create()) + lce = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() + repo = _create_repository( + module_target_sat, module_target_sat.api.Product(organization=module_org).create() + ) - content_view = entities.ContentView(composite=False, organization=module_org).create() + content_view = module_target_sat.api.ContentView( + composite=False, organization=module_org + ).create() content_view.repository = [repo] content_view = content_view.update(['repository']) assert [repo.id] == [repo_.id for repo_ in content_view.repository] @@ -613,7 +655,7 @@ def test_positive_promote_with_docker_repo(self, module_org): assert len(cvv.read().environment) == 2 @pytest.mark.tier2 - def test_positive_promote_multiple_with_docker_repo(self, module_org): + def test_positive_promote_multiple_with_docker_repo(self, module_org, module_target_sat): """Add Docker-type repository to content view and publish it. Then promote it to multiple available lifecycle-environments. @@ -622,9 +664,13 @@ def test_positive_promote_multiple_with_docker_repo(self, module_org): :expectedresults: Docker-type repository is promoted to content view found in the specific lifecycle-environments. """ - repo = _create_repository(entities.Product(organization=module_org).create()) + repo = _create_repository( + module_target_sat, module_target_sat.api.Product(organization=module_org).create() + ) - content_view = entities.ContentView(composite=False, organization=module_org).create() + content_view = module_target_sat.api.ContentView( + composite=False, organization=module_org + ).create() content_view.repository = [repo] content_view = content_view.update(['repository']) assert [repo.id] == [repo_.id for repo_ in content_view.repository] @@ -634,12 +680,12 @@ def test_positive_promote_multiple_with_docker_repo(self, module_org): assert len(cvv.read().environment) == 1 for i in range(1, randint(3, 6)): - lce = entities.LifecycleEnvironment(organization=module_org).create() + lce = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() cvv.promote(data={'environment_ids': lce.id, 'force': False}) assert len(cvv.read().environment) == i + 1 @pytest.mark.tier2 - def test_positive_promote_with_docker_repo_composite(self, module_org): + def test_positive_promote_with_docker_repo_composite(self, module_org, module_target_sat): """Add Docker-type repository to content view and publish it. Then add that content view to composite one. Publish and promote that composite content view to the next available lifecycle-environment. @@ -649,9 +695,13 @@ def test_positive_promote_with_docker_repo_composite(self, module_org): :expectedresults: Docker-type repository is promoted to content view found in the specific lifecycle-environment. """ - lce = entities.LifecycleEnvironment(organization=module_org).create() - repo = _create_repository(entities.Product(organization=module_org).create()) - content_view = entities.ContentView(composite=False, organization=module_org).create() + lce = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() + repo = _create_repository( + module_target_sat, module_target_sat.api.Product(organization=module_org).create() + ) + content_view = module_target_sat.api.ContentView( + composite=False, organization=module_org + ).create() content_view.repository = [repo] content_view = content_view.update(['repository']) assert [repo.id] == [repo_.id for repo_ in content_view.repository] @@ -659,7 +709,9 @@ def test_positive_promote_with_docker_repo_composite(self, module_org): content_view.publish() cvv = content_view.read().version[0].read() - comp_content_view = entities.ContentView(composite=True, organization=module_org).create() + comp_content_view = module_target_sat.api.ContentView( + composite=True, organization=module_org + ).create() comp_content_view.component = [cvv] comp_content_view = comp_content_view.update(['component']) assert cvv.id == comp_content_view.component[0].id @@ -673,7 +725,9 @@ def test_positive_promote_with_docker_repo_composite(self, module_org): @pytest.mark.upgrade @pytest.mark.tier2 - def test_positive_promote_multiple_with_docker_repo_composite(self, module_org): + def test_positive_promote_multiple_with_docker_repo_composite( + self, module_org, module_target_sat + ): """Add Docker-type repository to content view and publish it. Then add that content view to composite one. Publish and promote that composite content view to the multiple available lifecycle-environments @@ -683,8 +737,12 @@ def test_positive_promote_multiple_with_docker_repo_composite(self, module_org): :expectedresults: Docker-type repository is promoted to content view found in the specific lifecycle-environments. """ - repo = _create_repository(entities.Product(organization=module_org).create()) - content_view = entities.ContentView(composite=False, organization=module_org).create() + repo = _create_repository( + module_target_sat, module_target_sat.api.Product(organization=module_org).create() + ) + content_view = module_target_sat.api.ContentView( + composite=False, organization=module_org + ).create() content_view.repository = [repo] content_view = content_view.update(['repository']) assert [repo.id] == [repo_.id for repo_ in content_view.repository] @@ -692,7 +750,9 @@ def test_positive_promote_multiple_with_docker_repo_composite(self, module_org): content_view.publish() cvv = content_view.read().version[0].read() - comp_content_view = entities.ContentView(composite=True, organization=module_org).create() + comp_content_view = module_target_sat.api.ContentView( + composite=True, organization=module_org + ).create() comp_content_view.component = [cvv] comp_content_view = comp_content_view.update(['component']) assert cvv.id == comp_content_view.component[0].id @@ -702,13 +762,13 @@ def test_positive_promote_multiple_with_docker_repo_composite(self, module_org): assert len(comp_cvv.read().environment) == 1 for i in range(1, randint(3, 6)): - lce = entities.LifecycleEnvironment(organization=module_org).create() + lce = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() comp_cvv.promote(data={'environment_ids': lce.id, 'force': False}) assert len(comp_cvv.read().environment) == i + 1 @pytest.mark.tier2 @pytest.mark.upgrade - def test_positive_name_pattern_change(self, module_org): + def test_positive_name_pattern_change(self, module_org, module_target_sat): """Promote content view with Docker repository to lifecycle environment. Change registry name pattern for that environment. Verify that repository name on product changed according to new pattern. @@ -725,19 +785,23 @@ def test_positive_name_pattern_change(self, module_org): ) repo = _create_repository( - entities.Product(organization=module_org).create(), upstream_name=docker_upstream_name + module_target_sat, + module_target_sat.api.Product(organization=module_org).create(), + upstream_name=docker_upstream_name, ) repo.sync(timeout=600) - content_view = entities.ContentView(composite=False, organization=module_org).create() + content_view = module_target_sat.api.ContentView( + composite=False, organization=module_org + ).create() content_view.repository = [repo] content_view = content_view.update(['repository']) content_view.publish() cvv = content_view.read().version[0] - lce = entities.LifecycleEnvironment(organization=module_org).create() + lce = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() cvv.promote(data={'environment_ids': lce.id, 'force': False}) lce.registry_name_pattern = new_pattern lce = lce.update(['registry_name_pattern']) - repos = entities.Repository(organization=module_org).search( + repos = module_target_sat.api.Repository(organization=module_org).search( query={'environment_id': lce.id} ) @@ -746,7 +810,7 @@ def test_positive_name_pattern_change(self, module_org): assert repos[0].container_repository_name == expected_pattern @pytest.mark.tier2 - def test_positive_product_name_change_after_promotion(self, module_org): + def test_positive_product_name_change_after_promotion(self, module_org, module_target_sat): """Promote content view with Docker repository to lifecycle environment. Change product name. Verify that repository name on product changed according to new pattern. @@ -761,21 +825,23 @@ def test_positive_product_name_change_after_promotion(self, module_org): docker_upstream_name = 'hello-world' new_pattern = "<%= organization.label %>/<%= product.name %>" - prod = entities.Product(organization=module_org, name=old_prod_name).create() - repo = _create_repository(prod, upstream_name=docker_upstream_name) + prod = module_target_sat.api.Product(organization=module_org, name=old_prod_name).create() + repo = _create_repository(module_target_sat, prod, upstream_name=docker_upstream_name) repo.sync(timeout=600) - content_view = entities.ContentView(composite=False, organization=module_org).create() + content_view = module_target_sat.api.ContentView( + composite=False, organization=module_org + ).create() content_view.repository = [repo] content_view = content_view.update(['repository']) content_view.publish() cvv = content_view.read().version[0] - lce = entities.LifecycleEnvironment(organization=module_org).create() + lce = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() lce.registry_name_pattern = new_pattern lce = lce.update(['registry_name_pattern']) cvv.promote(data={'environment_ids': lce.id, 'force': False}) prod.name = new_prod_name prod.update(['name']) - repos = entities.Repository(organization=module_org).search( + repos = module_target_sat.api.Repository(organization=module_org).search( query={'environment_id': lce.id} ) @@ -785,7 +851,7 @@ def test_positive_product_name_change_after_promotion(self, module_org): content_view.publish() cvv = content_view.read().version[-1] cvv.promote(data={'environment_ids': lce.id, 'force': False}) - repos = entities.Repository(organization=module_org).search( + repos = module_target_sat.api.Repository(organization=module_org).search( query={'environment_id': lce.id} ) @@ -793,7 +859,7 @@ def test_positive_product_name_change_after_promotion(self, module_org): assert repos[0].container_repository_name == expected_pattern @pytest.mark.tier2 - def test_positive_repo_name_change_after_promotion(self, module_org): + def test_positive_repo_name_change_after_promotion(self, module_org, module_target_sat): """Promote content view with Docker repository to lifecycle environment. Change repository name. Verify that Docker repository name on product changed according to new pattern. @@ -809,23 +875,26 @@ def test_positive_repo_name_change_after_promotion(self, module_org): new_pattern = "<%= organization.label %>/<%= repository.name %>" repo = _create_repository( - entities.Product(organization=module_org).create(), + module_target_sat, + module_target_sat.api.Product(organization=module_org).create(), name=old_repo_name, upstream_name=docker_upstream_name, ) repo.sync(timeout=600) - content_view = entities.ContentView(composite=False, organization=module_org).create() + content_view = module_target_sat.api.ContentView( + composite=False, organization=module_org + ).create() content_view.repository = [repo] content_view = content_view.update(['repository']) content_view.publish() cvv = content_view.read().version[0] - lce = entities.LifecycleEnvironment(organization=module_org).create() + lce = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() lce.registry_name_pattern = new_pattern lce = lce.update(['registry_name_pattern']) cvv.promote(data={'environment_ids': lce.id, 'force': False}) repo.name = new_repo_name repo.update(['name']) - repos = entities.Repository(organization=module_org).search( + repos = module_target_sat.api.Repository(organization=module_org).search( query={'environment_id': lce.id} ) @@ -835,7 +904,7 @@ def test_positive_repo_name_change_after_promotion(self, module_org): content_view.publish() cvv = content_view.read().version[-1] cvv.promote(data={'environment_ids': lce.id, 'force': False}) - repos = entities.Repository(organization=module_org).search( + repos = module_target_sat.api.Repository(organization=module_org).search( query={'environment_id': lce.id} ) @@ -843,7 +912,7 @@ def test_positive_repo_name_change_after_promotion(self, module_org): assert repos[0].container_repository_name == expected_pattern @pytest.mark.tier2 - def test_negative_set_non_unique_name_pattern_and_promote(self, module_org, module_lce): + def test_negative_set_non_unique_name_pattern_and_promote(self, module_org, module_target_sat): """Set registry name pattern to one that does not guarantee uniqueness. Try to promote content view with multiple Docker repositories to lifecycle environment. Verify that content has not been promoted. @@ -855,16 +924,18 @@ def test_negative_set_non_unique_name_pattern_and_promote(self, module_org, modu docker_upstream_names = ['hello-world', 'alpine'] new_pattern = "<%= organization.label %>" - lce = entities.LifecycleEnvironment(organization=module_org).create() + lce = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() lce.registry_name_pattern = new_pattern lce = lce.update(['registry_name_pattern']) - prod = entities.Product(organization=module_org).create() + prod = module_target_sat.api.Product(organization=module_org).create() repos = [] for docker_name in docker_upstream_names: - repo = _create_repository(prod, upstream_name=docker_name) + repo = _create_repository(module_target_sat, prod, upstream_name=docker_name) repo.sync(timeout=600) repos.append(repo) - content_view = entities.ContentView(composite=False, organization=module_org).create() + content_view = module_target_sat.api.ContentView( + composite=False, organization=module_org + ).create() content_view.repository = repos content_view = content_view.update(['repository']) content_view.publish() @@ -873,7 +944,7 @@ def test_negative_set_non_unique_name_pattern_and_promote(self, module_org, modu cvv.promote(data={'environment_ids': lce.id, 'force': False}) @pytest.mark.tier2 - def test_negative_promote_and_set_non_unique_name_pattern(self, module_org): + def test_negative_promote_and_set_non_unique_name_pattern(self, module_org, module_target_sat): """Promote content view with multiple Docker repositories to lifecycle environment. Set registry name pattern to one that does not guarantee uniqueness. Verify that pattern has not been @@ -886,18 +957,20 @@ def test_negative_promote_and_set_non_unique_name_pattern(self, module_org): docker_upstream_names = ['hello-world', 'alpine'] new_pattern = "<%= organization.label %>" - prod = entities.Product(organization=module_org).create() + prod = module_target_sat.api.Product(organization=module_org).create() repos = [] for docker_name in docker_upstream_names: - repo = _create_repository(prod, upstream_name=docker_name) + repo = _create_repository(module_target_sat, prod, upstream_name=docker_name) repo.sync(timeout=600) repos.append(repo) - content_view = entities.ContentView(composite=False, organization=module_org).create() + content_view = module_target_sat.api.ContentView( + composite=False, organization=module_org + ).create() content_view.repository = repos content_view = content_view.update(['repository']) content_view.publish() cvv = content_view.read().version[0] - lce = entities.LifecycleEnvironment(organization=module_org).create() + lce = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() cvv.promote(data={'environment_ids': lce.id, 'force': False}) with pytest.raises(HTTPError): lce.registry_name_pattern = new_pattern @@ -916,7 +989,7 @@ class TestDockerActivationKey: @pytest.mark.tier2 def test_positive_add_docker_repo_cv( - self, module_lce, module_org, repo, content_view_publish_promote + self, module_lce, module_org, repo, content_view_publish_promote, module_target_sat ): """Add Docker-type repository to a non-composite content view and publish it. Then create an activation key and associate it with the @@ -928,7 +1001,7 @@ def test_positive_add_docker_repo_cv( key """ content_view = content_view_publish_promote - ak = entities.ActivationKey( + ak = module_target_sat.api.ActivationKey( content_view=content_view, environment=module_lce, organization=module_org ).create() assert ak.content_view.id == content_view.id @@ -936,7 +1009,7 @@ def test_positive_add_docker_repo_cv( @pytest.mark.tier2 def test_positive_remove_docker_repo_cv( - self, module_org, module_lce, content_view_publish_promote + self, module_org, module_lce, content_view_publish_promote, module_target_sat ): """Create an activation key and associate it with the Docker content view. Then remove this content view from the activation key. @@ -949,7 +1022,7 @@ def test_positive_remove_docker_repo_cv( :CaseLevel: Integration """ content_view = content_view_publish_promote - ak = entities.ActivationKey( + ak = module_target_sat.api.ActivationKey( content_view=content_view, environment=module_lce, organization=module_org ).create() assert ak.content_view.id == content_view.id @@ -957,7 +1030,9 @@ def test_positive_remove_docker_repo_cv( assert ak.update(['content_view']).content_view is None @pytest.mark.tier2 - def test_positive_add_docker_repo_ccv(self, content_view_version, module_lce, module_org): + def test_positive_add_docker_repo_ccv( + self, content_view_version, module_lce, module_org, module_target_sat + ): """Add Docker-type repository to a non-composite content view and publish it. Then add this content view to a composite content view and publish it. Create an activation key and associate it with the @@ -969,7 +1044,9 @@ def test_positive_add_docker_repo_ccv(self, content_view_version, module_lce, mo key """ cvv = content_view_version - comp_content_view = entities.ContentView(composite=True, organization=module_org).create() + comp_content_view = module_target_sat.api.ContentView( + composite=True, organization=module_org + ).create() comp_content_view.component = [cvv] comp_content_view = comp_content_view.update(['component']) assert cvv.id == comp_content_view.component[0].id @@ -978,13 +1055,15 @@ def test_positive_add_docker_repo_ccv(self, content_view_version, module_lce, mo comp_cvv = comp_content_view.read().version[0].read() comp_cvv.promote(data={'environment_ids': module_lce.id, 'force': False}) - ak = entities.ActivationKey( + ak = module_target_sat.api.ActivationKey( content_view=comp_content_view, environment=module_lce, organization=module_org ).create() assert ak.content_view.id == comp_content_view.id @pytest.mark.tier2 - def test_positive_remove_docker_repo_ccv(self, module_lce, module_org, content_view_version): + def test_positive_remove_docker_repo_ccv( + self, module_lce, module_org, content_view_version, module_target_sat + ): """Add Docker-type repository to a non-composite content view and publish it. Then add this content view to a composite content view and publish it. Create an activation key and associate it with the @@ -997,7 +1076,9 @@ def test_positive_remove_docker_repo_ccv(self, module_lce, module_org, content_v then removed from the activation key. """ cvv = content_view_version - comp_content_view = entities.ContentView(composite=True, organization=module_org).create() + comp_content_view = module_target_sat.api.ContentView( + composite=True, organization=module_org + ).create() comp_content_view.component = [cvv] comp_content_view = comp_content_view.update(['component']) assert cvv.id == comp_content_view.component[0].id @@ -1006,7 +1087,7 @@ def test_positive_remove_docker_repo_ccv(self, module_lce, module_org, content_v comp_cvv = comp_content_view.read().version[0].read() comp_cvv.promote(data={'environment_ids': module_lce.id, 'force': False}) - ak = entities.ActivationKey( + ak = module_target_sat.api.ActivationKey( content_view=comp_content_view, environment=module_lce, organization=module_org ).create() assert ak.content_view.id == comp_content_view.id diff --git a/tests/foreman/api/test_errata.py b/tests/foreman/api/test_errata.py index f33931fe833..d88b27411bc 100644 --- a/tests/foreman/api/test_errata.py +++ b/tests/foreman/api/test_errata.py @@ -40,8 +40,8 @@ @pytest.fixture(scope='module') -def activation_key(module_org, module_lce): - activation_key = entities.ActivationKey( +def activation_key(module_org, module_lce, module_target_sat): + activation_key = module_target_sat.api.ActivationKey( environment=module_lce, organization=module_org ).create() return activation_key @@ -339,7 +339,7 @@ def test_positive_sorted_issue_date_and_filter_by_cve(module_org, custom_repo, t :CaseLevel: System """ # Errata is sorted by issued date. - erratum_list = entities.Errata(repository=custom_repo['repository-id']).search( + erratum_list = target_sat.api.Errata(repository=custom_repo['repository-id']).search( query={'order': 'issued ASC', 'per_page': '1000'} ) issued = [errata.issued for errata in erratum_list] @@ -374,28 +374,28 @@ def setup_content_rhel6(module_entitlement_manifest_org, module_target_sat): reposet=constants.REPOSET['rhva6'], releasever=constants.DEFAULT_RELEASE_VERSION, ) - rh_repo = entities.Repository(id=rh_repo_id_rhva).read() + rh_repo = module_target_sat.api.Repository(id=rh_repo_id_rhva).read() rh_repo.sync() - host_tools_product = entities.Product(organization=org).create() - host_tools_repo = entities.Repository( + host_tools_product = module_target_sat.api.Product(organization=org).create() + host_tools_repo = module_target_sat.api.Repository( product=host_tools_product, ).create() host_tools_repo.url = settings.repos.SATCLIENT_REPO.RHEL6 host_tools_repo = host_tools_repo.update(['url']) host_tools_repo.sync() - custom_product = entities.Product(organization=org).create() - custom_repo = entities.Repository( + custom_product = module_target_sat.api.Product(organization=org).create() + custom_repo = module_target_sat.api.Repository( product=custom_product, ).create() custom_repo.url = CUSTOM_REPO_URL custom_repo = custom_repo.update(['url']) custom_repo.sync() - lce = entities.LifecycleEnvironment(organization=org).create() + lce = module_target_sat.api.LifecycleEnvironment(organization=org).create() - cv = entities.ContentView( + cv = module_target_sat.api.ContentView( organization=org, repository=[rh_repo_id_rhva, host_tools_repo.id, custom_repo.id], ).create() @@ -403,11 +403,13 @@ def setup_content_rhel6(module_entitlement_manifest_org, module_target_sat): cvv = cv.read().version[0].read() cvv.promote(data={'environment_ids': lce.id, 'force': False}) - ak = entities.ActivationKey(content_view=cv, organization=org, environment=lce).create() + ak = module_target_sat.api.ActivationKey( + content_view=cv, organization=org, environment=lce + ).create() sub_list = [DEFAULT_SUBSCRIPTION_NAME, host_tools_product.name, custom_product.name] for sub_name in sub_list: - subscription = entities.Subscription(organization=org).search( + subscription = module_target_sat.api.Subscription(organization=org).search( query={'search': f'name="{sub_name}"'} )[0] ak.add_subscriptions(data={'subscription_id': subscription.id}) @@ -504,7 +506,7 @@ def test_positive_get_applicable_for_host(setup_content_rhel6, rhel6_contenthost @pytest.mark.tier3 -def test_positive_get_diff_for_cv_envs(): +def test_positive_get_diff_for_cv_envs(target_sat): """Generate a difference in errata between a set of environments for a content view @@ -522,10 +524,10 @@ def test_positive_get_diff_for_cv_envs(): :CaseLevel: System """ - org = entities.Organization().create() - env = entities.LifecycleEnvironment(organization=org).create() - content_view = entities.ContentView(organization=org).create() - activation_key = entities.ActivationKey(environment=env, organization=org).create() + org = target_sat.api.Organization().create() + env = target_sat.api.LifecycleEnvironment(organization=org).create() + content_view = target_sat.api.ContentView(organization=org).create() + activation_key = target_sat.api.ActivationKey(environment=env, organization=org).create() for repo_url in [settings.repos.yum_9.url, CUSTOM_REPO_URL]: setup_org_for_a_custom_repo( { @@ -536,10 +538,10 @@ def test_positive_get_diff_for_cv_envs(): 'activationkey-id': activation_key.id, } ) - new_env = entities.LifecycleEnvironment(organization=org, prior=env).create() + new_env = target_sat.api.LifecycleEnvironment(organization=org, prior=env).create() cvvs = content_view.read().version[-2:] cvvs[-1].promote(data={'environment_ids': new_env.id, 'force': False}) - result = entities.Errata().compare( + result = target_sat.api.Errata().compare( data={'content_view_version_ids': [cvv.id for cvv in cvvs], 'per_page': '9999'} ) cvv2_only_errata = next( @@ -611,7 +613,7 @@ def test_positive_incremental_update_required( rpm_package_name=constants.FAKE_1_CUSTOM_PACKAGE, ) # Call nailgun to make the API POST to see if any incremental updates are required - response = entities.Host().bulk_available_incremental_updates( + response = target_sat.api.Host().bulk_available_incremental_updates( data={ 'organization_id': module_org.id, 'included': {'ids': [host.id]}, @@ -621,7 +623,7 @@ def test_positive_incremental_update_required( assert not response, 'Incremental update should not be required at this point' # Add filter of type include but do not include anything # this will hide all RPMs from selected erratum before publishing - entities.RPMContentViewFilter( + target_sat.api.RPMContentViewFilter( content_view=module_cv, inclusion=True, name='Include Nothing' ).create() module_cv.publish() @@ -631,7 +633,7 @@ def test_positive_incremental_update_required( CV1V.promote(data={'environment_ids': module_lce.id, 'force': False}) module_cv = module_cv.read() # Call nailgun to make the API POST to ensure an incremental update is required - response = entities.Host().bulk_available_incremental_updates( + response = target_sat.api.Host().bulk_available_incremental_updates( data={ 'organization_id': module_org.id, 'included': {'ids': [host.id]}, @@ -773,7 +775,7 @@ def rh_repo_module_manifest(module_entitlement_manifest_org, module_target_sat): releasever='None', ) # Sync step because repo is not synced by default - rh_repo = entities.Repository(id=rh_repo_id).read() + rh_repo = module_target_sat.api.Repository(id=rh_repo_id).read() rh_repo.sync() return rh_repo @@ -791,11 +793,17 @@ def rhel8_custom_repo_cv(module_entitlement_manifest_org): @pytest.fixture(scope='module') def rhel8_module_ak( - module_entitlement_manifest_org, default_lce, rh_repo_module_manifest, rhel8_custom_repo_cv + module_entitlement_manifest_org, + default_lce, + rh_repo_module_manifest, + rhel8_custom_repo_cv, + module_target_sat, ): - rhel8_module_ak = entities.ActivationKey( + rhel8_module_ak = module_target_sat.api.ActivationKey( content_view=module_entitlement_manifest_org.default_content_view, - environment=entities.LifecycleEnvironment(id=module_entitlement_manifest_org.library.id), + environment=module_target_sat.api.LifecycleEnvironment( + id=module_entitlement_manifest_org.library.id + ), organization=module_entitlement_manifest_org, ).create() # Ensure tools repo is enabled in the activation key @@ -805,19 +813,19 @@ def rhel8_module_ak( } ) # Fetch available subscriptions - subs = entities.Subscription(organization=module_entitlement_manifest_org).search( + subs = module_target_sat.api.Subscription(organization=module_entitlement_manifest_org).search( query={'search': f'{constants.DEFAULT_SUBSCRIPTION_NAME}'} ) assert subs # Add default subscription to activation key rhel8_module_ak.add_subscriptions(data={'subscription_id': subs[0].id}) # Add custom subscription to activation key - product = entities.Product(organization=module_entitlement_manifest_org).search( - query={'search': "redhat=false"} - ) - custom_sub = entities.Subscription(organization=module_entitlement_manifest_org).search( - query={'search': f"name={product[0].name}"} + product = module_target_sat.api.Product(organization=module_entitlement_manifest_org).search( + query={'search': 'redhat=false'} ) + custom_sub = module_target_sat.api.Subscription( + organization=module_entitlement_manifest_org + ).search(query={'search': f'name={product[0].name}'}) rhel8_module_ak.add_subscriptions(data={'subscription_id': custom_sub[0].id}) return rhel8_module_ak diff --git a/tests/foreman/api/test_filter.py b/tests/foreman/api/test_filter.py index 75d42707e4a..bf04a011276 100644 --- a/tests/foreman/api/test_filter.py +++ b/tests/foreman/api/test_filter.py @@ -20,22 +20,22 @@ :Upstream: No """ -from nailgun import entities + import pytest from requests.exceptions import HTTPError @pytest.fixture(scope='module') -def module_perms(): +def module_perms(module_target_sat): """Search for provisioning template permissions. Set ``cls.ct_perms``.""" - ct_perms = entities.Permission().search( + ct_perms = module_target_sat.api.Permission().search( query={'search': 'resource_type="ProvisioningTemplate"'} ) return ct_perms @pytest.mark.tier1 -def test_positive_create_with_permission(module_perms): +def test_positive_create_with_permission(module_perms, module_target_sat): """Create a filter and assign it some permissions. :id: b8631d0a-a71a-41aa-9f9a-d12d62adc496 @@ -45,14 +45,14 @@ def test_positive_create_with_permission(module_perms): :CaseImportance: Critical """ # Create a filter and assign all ProvisioningTemplate permissions to it - filter_ = entities.Filter(permission=module_perms).create() + filter_ = module_target_sat.api.Filter(permission=module_perms).create() filter_perms = [perm.id for perm in filter_.permission] perms = [perm.id for perm in module_perms] assert filter_perms == perms @pytest.mark.tier1 -def test_positive_delete(module_perms): +def test_positive_delete(module_perms, module_target_sat): """Create a filter and delete it afterwards. :id: f0c56fd8-c91d-48c3-ad21-f538313b17eb @@ -61,14 +61,14 @@ def test_positive_delete(module_perms): :CaseImportance: Critical """ - filter_ = entities.Filter(permission=module_perms).create() + filter_ = module_target_sat.api.Filter(permission=module_perms).create() filter_.delete() with pytest.raises(HTTPError): filter_.read() @pytest.mark.tier1 -def test_positive_delete_role(module_perms): +def test_positive_delete_role(module_perms, module_target_sat): """Create a filter and delete the role it points at. :id: b129642d-926d-486a-84d9-5952b44ac446 @@ -77,8 +77,8 @@ def test_positive_delete_role(module_perms): :CaseImportance: Critical """ - role = entities.Role().create() - filter_ = entities.Filter(permission=module_perms, role=role).create() + role = module_target_sat.api.Role().create() + filter_ = module_target_sat.api.Filter(permission=module_perms, role=role).create() # A filter depends on a role. Deleting a role implicitly deletes the # filter pointing at it. diff --git a/tests/foreman/api/test_foremantask.py b/tests/foreman/api/test_foremantask.py index f7e8377e82f..65918863cac 100644 --- a/tests/foreman/api/test_foremantask.py +++ b/tests/foreman/api/test_foremantask.py @@ -16,13 +16,12 @@ :Upstream: No """ -from nailgun import entities import pytest from requests.exceptions import HTTPError @pytest.mark.tier1 -def test_negative_fetch_non_existent_task(): +def test_negative_fetch_non_existent_task(target_sat): """Fetch a non-existent task. :id: a2a81ca2-63c4-47f5-9314-5852f5e2617f @@ -32,13 +31,13 @@ def test_negative_fetch_non_existent_task(): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.ForemanTask(id='abc123').read() + target_sat.api.ForemanTask(id='abc123').read() @pytest.mark.tier1 @pytest.mark.upgrade @pytest.mark.e2e -def test_positive_get_summary(): +def test_positive_get_summary(target_sat): """Get a summary of foreman tasks. :id: bdcab413-a25d-4fe1-9db4-b50b5c31ebce @@ -47,7 +46,7 @@ def test_positive_get_summary(): :CaseImportance: Critical """ - summary = entities.ForemanTask().summary() - assert type(summary) is list + summary = target_sat.api.ForemanTask().summary() + assert isinstance(summary, list) for item in summary: assert type(item) is dict diff --git a/tests/foreman/api/test_host.py b/tests/foreman/api/test_host.py index 8393dd5ecf0..15411a4741b 100644 --- a/tests/foreman/api/test_host.py +++ b/tests/foreman/api/test_host.py @@ -23,7 +23,7 @@ import http from fauxfactory import gen_choice, gen_integer, gen_ipaddr, gen_mac, gen_string -from nailgun import client, entities +from nailgun import client import pytest from requests.exceptions import HTTPError @@ -32,14 +32,14 @@ from robottelo.utils import datafactory -def update_smart_proxy(location, smart_proxy): - if location.id not in [location.id for location in smart_proxy.location]: - smart_proxy.location.append(entities.Location(id=location.id)) - smart_proxy.update(['location']) +def update_smart_proxy(smart_proxy_location, smart_proxy): + if smart_proxy_location.id not in [location.id for location in smart_proxy.location]: + smart_proxy.location.append(smart_proxy_location) + smart_proxy.update(['location']) @pytest.mark.tier1 -def test_positive_get_search(): +def test_positive_get_search(target_sat): """GET ``api/v2/hosts`` and specify the ``search`` parameter. :id: d63f87e5-66e6-4886-8b44-4129259493a6 @@ -50,7 +50,7 @@ def test_positive_get_search(): """ query = gen_string('utf8', gen_integer(1, 100)) response = client.get( - entities.Host().path(), + target_sat.api.Host().path(), auth=get_credentials(), data={'search': query}, verify=False, @@ -60,7 +60,7 @@ def test_positive_get_search(): @pytest.mark.tier1 -def test_positive_get_per_page(): +def test_positive_get_per_page(target_sat): """GET ``api/v2/hosts`` and specify the ``per_page`` parameter. :id: 9086f41c-b3b9-4af2-b6c4-46b80b4d1cfd @@ -72,7 +72,7 @@ def test_positive_get_per_page(): """ per_page = gen_integer(1, 1000) response = client.get( - entities.Host().path(), + target_sat.api.Host().path(), auth=get_credentials(), data={'per_page': str(per_page)}, verify=False, @@ -82,7 +82,7 @@ def test_positive_get_per_page(): @pytest.mark.tier2 -def test_positive_search_by_org_id(): +def test_positive_search_by_org_id(target_sat): """Search for host by specifying host's organization id :id: 56353f7c-b77e-4b6c-9ec3-51b58f9a18d8 @@ -96,9 +96,9 @@ def test_positive_search_by_org_id(): :CaseLevel: Integration """ - host = entities.Host().create() + host = target_sat.api.Host().create() # adding org id as GET parameter for correspondence with BZ - query = entities.Host() + query = target_sat.api.Host() query._meta['api_path'] += f'?organization_id={host.organization.id}' results = query.search() assert len(results) == 1 @@ -107,7 +107,7 @@ def test_positive_search_by_org_id(): @pytest.mark.tier1 @pytest.mark.parametrize('owner_type', ['User', 'Usergroup']) -def test_negative_create_with_owner_type(owner_type): +def test_negative_create_with_owner_type(owner_type, target_sat): """Create a host and specify only ``owner_type``. :id: cdf9d16f-1c47-498a-be48-901355385dde @@ -119,13 +119,15 @@ def test_negative_create_with_owner_type(owner_type): :CaseImportance: Critical """ with pytest.raises(HTTPError) as error: - entities.Host(owner_type=owner_type).create() + target_sat.api.Host(owner_type=owner_type).create() assert str(422) in str(error) @pytest.mark.tier1 @pytest.mark.parametrize('owner_type', ['User', 'Usergroup']) -def test_positive_update_owner_type(owner_type, module_org, module_location, module_user): +def test_positive_update_owner_type( + owner_type, module_org, module_location, module_user, module_target_sat +): """Update a host's ``owner_type``. :id: b72cd8ef-3a0b-4d2d-94f9-9b64908d699a @@ -141,9 +143,9 @@ def test_positive_update_owner_type(owner_type, module_org, module_location, mod """ owners = { 'User': module_user, - 'Usergroup': entities.UserGroup().create(), + 'Usergroup': module_target_sat.api.UserGroup().create(), } - host = entities.Host(organization=module_org, location=module_location).create() + host = module_target_sat.api.Host(organization=module_org, location=module_location).create() host.owner_type = owner_type host.owner = owners[owner_type] host = host.update(['owner_type', 'owner']) @@ -152,7 +154,7 @@ def test_positive_update_owner_type(owner_type, module_org, module_location, mod @pytest.mark.tier1 -def test_positive_create_and_update_with_name(): +def test_positive_create_and_update_with_name(target_sat): """Create and update a host with different names and minimal input parameters :id: a7c0e8ec-3816-4092-88b1-0324cb271752 @@ -162,7 +164,7 @@ def test_positive_create_and_update_with_name(): :CaseImportance: Critical """ name = gen_choice(datafactory.valid_hosts_list()) - host = entities.Host(name=name).create() + host = target_sat.api.Host(name=name).create() assert host.name == f'{name}.{host.domain.read().name}' new_name = gen_choice(datafactory.valid_hosts_list()) host.name = new_name @@ -171,7 +173,7 @@ def test_positive_create_and_update_with_name(): @pytest.mark.tier1 -def test_positive_create_and_update_with_ip(): +def test_positive_create_and_update_with_ip(target_sat): """Create and update host with IP address specified :id: 3f266906-c509-42ce-9b20-def448bf8d86 @@ -181,7 +183,7 @@ def test_positive_create_and_update_with_ip(): :CaseImportance: Critical """ ip_addr = gen_ipaddr() - host = entities.Host(ip=ip_addr).create() + host = target_sat.api.Host(ip=ip_addr).create() assert host.ip == ip_addr new_ip_addr = gen_ipaddr() host.ip = new_ip_addr @@ -190,7 +192,7 @@ def test_positive_create_and_update_with_ip(): @pytest.mark.tier1 -def test_positive_create_and_update_mac(): +def test_positive_create_and_update_mac(target_sat): """Create host with MAC address and update it :id: 72e3b020-7347-4500-8669-c6ddf6dfd0b6 @@ -201,7 +203,7 @@ def test_positive_create_and_update_mac(): """ mac = gen_mac(multicast=False) - host = entities.Host(mac=mac).create() + host = target_sat.api.Host(mac=mac).create() assert host.mac == mac new_mac = gen_mac(multicast=False) host.mac = new_mac @@ -211,7 +213,7 @@ def test_positive_create_and_update_mac(): @pytest.mark.tier2 def test_positive_create_and_update_with_hostgroup( - module_org, module_location, module_lce, module_published_cv + module_org, module_location, module_lce, module_published_cv, module_target_sat ): """Create and update host with hostgroup specified @@ -222,8 +224,10 @@ def test_positive_create_and_update_with_hostgroup( :CaseLevel: Integration """ module_published_cv.version[0].promote(data={'environment_ids': module_lce.id, 'force': False}) - hostgroup = entities.HostGroup(location=[module_location], organization=[module_org]).create() - host = entities.Host( + hostgroup = module_target_sat.api.HostGroup( + location=[module_location], organization=[module_org] + ).create() + host = module_target_sat.api.Host( hostgroup=hostgroup, location=module_location, organization=module_org, @@ -233,7 +237,7 @@ def test_positive_create_and_update_with_hostgroup( }, ).create() assert host.hostgroup.read().name == hostgroup.name - new_hostgroup = entities.HostGroup( + new_hostgroup = module_target_sat.api.HostGroup( location=[host.location], organization=[host.organization] ).create() host.hostgroup = new_hostgroup @@ -246,7 +250,9 @@ def test_positive_create_and_update_with_hostgroup( @pytest.mark.tier2 -def test_positive_create_inherit_lce_cv(module_default_org_view, module_lce_library, module_org): +def test_positive_create_inherit_lce_cv( + module_default_org_view, module_lce_library, module_org, module_target_sat +): """Create a host with hostgroup specified. Make sure host inherited hostgroup's lifecycle environment and content-view @@ -259,12 +265,12 @@ def test_positive_create_inherit_lce_cv(module_default_org_view, module_lce_libr :BZ: 1391656 """ - hostgroup = entities.HostGroup( + hostgroup = module_target_sat.api.HostGroup( content_view=module_default_org_view, lifecycle_environment=module_lce_library, organization=[module_org], ).create() - host = entities.Host(hostgroup=hostgroup, organization=module_org).create() + host = module_target_sat.api.Host(hostgroup=hostgroup, organization=module_org).create() assert ( host.content_facet_attributes['lifecycle_environment_id'] == hostgroup.lifecycle_environment.id @@ -273,7 +279,7 @@ def test_positive_create_inherit_lce_cv(module_default_org_view, module_lce_libr @pytest.mark.tier2 -def test_positive_create_with_inherited_params(module_org, module_location): +def test_positive_create_with_inherited_params(module_org, module_location, module_target_sat): """Create a new Host in organization and location with parameters :BZ: 1287223 @@ -287,18 +293,20 @@ def test_positive_create_with_inherited_params(module_org, module_location): :CaseImportance: High """ - org_param = entities.Parameter(organization=module_org).create() - loc_param = entities.Parameter(location=module_location).create() - host = entities.Host(location=module_location, organization=module_org).create() + org_param = module_target_sat.api.Parameter(organization=module_org).create() + loc_param = module_target_sat.api.Parameter(location=module_location).create() + host = module_target_sat.api.Host(location=module_location, organization=module_org).create() # get global parameters - glob_param_list = {(param.name, param.value) for param in entities.CommonParameter().search()} + glob_param_list = { + (param.name, param.value) for param in module_target_sat.api.CommonParameter().search() + } # if there are no global parameters, create one if len(glob_param_list) == 0: param_name = gen_string('alpha') param_global_value = gen_string('numeric') - entities.CommonParameter(name=param_name, value=param_global_value).create() + module_target_sat.api.CommonParameter(name=param_name, value=param_global_value).create() glob_param_list = { - (param.name, param.value) for param in entities.CommonParameter().search() + (param.name, param.value) for param in module_target_sat.api.CommonParameter().search() } assert len(host.all_parameters) == 2 + len(glob_param_list) innerited_params = {(org_param.name, org_param.value), (loc_param.name, loc_param.value)} @@ -397,7 +405,9 @@ def test_positive_end_to_end_with_puppet_class( @pytest.mark.tier2 -def test_positive_create_and_update_with_subnet(module_location, module_org, module_default_subnet): +def test_positive_create_and_update_with_subnet( + module_location, module_org, module_default_subnet, module_target_sat +): """Create and update a host with subnet specified :id: 9aa97aff-8439-4027-89ee-01c643fbf7d1 @@ -406,11 +416,13 @@ def test_positive_create_and_update_with_subnet(module_location, module_org, mod :CaseLevel: Integration """ - host = entities.Host( + host = module_target_sat.api.Host( location=module_location, organization=module_org, subnet=module_default_subnet ).create() assert host.subnet.read().name == module_default_subnet.name - new_subnet = entities.Subnet(location=[module_location], organization=[module_org]).create() + new_subnet = module_target_sat.api.Subnet( + location=[module_location], organization=[module_org] + ).create() host.subnet = new_subnet host = host.update(['subnet']) assert host.subnet.read().name == new_subnet.name @@ -418,7 +430,7 @@ def test_positive_create_and_update_with_subnet(module_location, module_org, mod @pytest.mark.tier2 def test_positive_create_and_update_with_compresource( - module_org, module_location, module_cr_libvirt + module_org, module_location, module_cr_libvirt, module_target_sat ): """Create and update a host with compute resource specified @@ -429,11 +441,11 @@ def test_positive_create_and_update_with_compresource( :CaseLevel: Integration """ - host = entities.Host( + host = module_target_sat.api.Host( compute_resource=module_cr_libvirt, location=module_location, organization=module_org ).create() assert host.compute_resource.read().name == module_cr_libvirt.name - new_compresource = entities.LibvirtComputeResource( + new_compresource = module_target_sat.api.LibvirtComputeResource( location=[host.location], organization=[host.organization] ).create() host.compute_resource = new_compresource @@ -442,7 +454,7 @@ def test_positive_create_and_update_with_compresource( @pytest.mark.tier2 -def test_positive_create_and_update_with_model(module_model): +def test_positive_create_and_update_with_model(module_model, module_target_sat): """Create and update a host with model specified :id: 7a912a19-71e4-4843-87fd-bab98c156f4a @@ -451,16 +463,18 @@ def test_positive_create_and_update_with_model(module_model): :CaseLevel: Integration """ - host = entities.Host(model=module_model).create() + host = module_target_sat.api.Host(model=module_model).create() assert host.model.read().name == module_model.name - new_model = entities.Model().create() + new_model = module_target_sat.api.Model().create() host.model = new_model host = host.update(['model']) assert host.model.read().name == new_model.name @pytest.mark.tier2 -def test_positive_create_and_update_with_user(module_org, module_location, module_user): +def test_positive_create_and_update_with_user( + module_org, module_location, module_user, module_target_sat +): """Create and update host with user specified :id: 72e20f8f-17dc-4e38-8ac1-d08df8758f56 @@ -469,18 +483,22 @@ def test_positive_create_and_update_with_user(module_org, module_location, modul :CaseLevel: Integration """ - host = entities.Host( + host = module_target_sat.api.Host( owner=module_user, owner_type='User', organization=module_org, location=module_location ).create() assert host.owner.read() == module_user - new_user = entities.User(organization=[module_org], location=[module_location]).create() + new_user = module_target_sat.api.User( + organization=[module_org], location=[module_location] + ).create() host.owner = new_user host = host.update(['owner']) assert host.owner.read() == new_user @pytest.mark.tier2 -def test_positive_create_and_update_with_usergroup(module_org, module_location, function_role): +def test_positive_create_and_update_with_usergroup( + module_org, module_location, function_role, module_target_sat +): """Create and update host with user group specified :id: 706e860c-8c05-4ddc-be20-0ecd9f0da813 @@ -489,18 +507,18 @@ def test_positive_create_and_update_with_usergroup(module_org, module_location, :CaseLevel: Integration """ - user = entities.User( + user = module_target_sat.api.User( location=[module_location], organization=[module_org], role=[function_role] ).create() - usergroup = entities.UserGroup(role=[function_role], user=[user]).create() - host = entities.Host( + usergroup = module_target_sat.api.UserGroup(role=[function_role], user=[user]).create() + host = module_target_sat.api.Host( location=module_location, organization=module_org, owner=usergroup, owner_type='Usergroup', ).create() assert host.owner.read().name == usergroup.name - new_usergroup = entities.UserGroup(role=[function_role], user=[user]).create() + new_usergroup = module_target_sat.api.UserGroup(role=[function_role], user=[user]).create() host.owner = new_usergroup host = host.update(['owner']) assert host.owner.read().name == new_usergroup.name @@ -508,7 +526,7 @@ def test_positive_create_and_update_with_usergroup(module_org, module_location, @pytest.mark.tier1 @pytest.mark.parametrize('build', [True, False]) -def test_positive_create_and_update_with_build_parameter(build): +def test_positive_create_and_update_with_build_parameter(build, target_sat): """Create and update a host with 'build' parameter specified. Build parameter determines whether to enable the host for provisioning @@ -521,7 +539,7 @@ def test_positive_create_and_update_with_build_parameter(build): :CaseImportance: Critical """ - host = entities.Host(build=build).create() + host = target_sat.api.Host(build=build).create() assert host.build == build host.build = not build host = host.update(['build']) @@ -530,7 +548,7 @@ def test_positive_create_and_update_with_build_parameter(build): @pytest.mark.tier1 @pytest.mark.parametrize('enabled', [True, False], ids=['enabled', 'disabled']) -def test_positive_create_and_update_with_enabled_parameter(enabled): +def test_positive_create_and_update_with_enabled_parameter(enabled, target_sat): """Create and update a host with 'enabled' parameter specified. Enabled parameter determines whether to include the host within Satellite 6 reporting @@ -544,7 +562,7 @@ def test_positive_create_and_update_with_enabled_parameter(enabled): :CaseImportance: Critical """ - host = entities.Host(enabled=enabled).create() + host = target_sat.api.Host(enabled=enabled).create() assert host.enabled == enabled host.enabled = not enabled host = host.update(['enabled']) @@ -553,7 +571,7 @@ def test_positive_create_and_update_with_enabled_parameter(enabled): @pytest.mark.tier1 @pytest.mark.parametrize('managed', [True, False], ids=['managed', 'unmanaged']) -def test_positive_create_and_update_with_managed_parameter(managed): +def test_positive_create_and_update_with_managed_parameter(managed, target_sat): """Create and update a host with managed parameter specified. Managed flag shows whether the host is managed or unmanaged and determines whether some extra parameters are required @@ -567,7 +585,7 @@ def test_positive_create_and_update_with_managed_parameter(managed): :CaseImportance: Critical """ - host = entities.Host(managed=managed).create() + host = target_sat.api.Host(managed=managed).create() assert host.managed == managed host.managed = not managed host = host.update(['managed']) @@ -575,7 +593,7 @@ def test_positive_create_and_update_with_managed_parameter(managed): @pytest.mark.tier1 -def test_positive_create_and_update_with_comment(): +def test_positive_create_and_update_with_comment(target_sat): """Create and update a host with a comment :id: 9b78663f-139c-4d0b-9115-180624b0d41b @@ -585,7 +603,7 @@ def test_positive_create_and_update_with_comment(): :CaseImportance: Critical """ comment = gen_choice(list(datafactory.valid_data_list().values())) - host = entities.Host(comment=comment).create() + host = target_sat.api.Host(comment=comment).create() assert host.comment == comment new_comment = gen_choice(list(datafactory.valid_data_list().values())) host.comment = new_comment @@ -594,7 +612,7 @@ def test_positive_create_and_update_with_comment(): @pytest.mark.tier2 -def test_positive_create_and_update_with_compute_profile(module_compute_profile): +def test_positive_create_and_update_with_compute_profile(module_compute_profile, module_target_sat): """Create and update a host with a compute profile specified :id: 94be25e8-035d-42c5-b1f3-3aa20030410d @@ -604,9 +622,9 @@ def test_positive_create_and_update_with_compute_profile(module_compute_profile) :CaseLevel: Integration """ - host = entities.Host(compute_profile=module_compute_profile).create() + host = module_target_sat.api.Host(compute_profile=module_compute_profile).create() assert host.compute_profile.read().name == module_compute_profile.name - new_cprofile = entities.ComputeProfile().create() + new_cprofile = module_target_sat.api.ComputeProfile().create() host.compute_profile = new_cprofile host = host.update(['compute_profile']) assert host.compute_profile.read().name == new_cprofile.name @@ -614,7 +632,7 @@ def test_positive_create_and_update_with_compute_profile(module_compute_profile) @pytest.mark.tier2 def test_positive_create_and_update_with_content_view( - module_org, module_location, module_default_org_view, module_lce_library + module_org, module_location, module_default_org_view, module_lce_library, module_target_sat ): """Create and update host with a content view specified @@ -624,7 +642,7 @@ def test_positive_create_and_update_with_content_view( :CaseLevel: Integration """ - host = entities.Host( + host = module_target_sat.api.Host( organization=module_org, location=module_location, content_facet_attributes={ @@ -646,7 +664,7 @@ def test_positive_create_and_update_with_content_view( @pytest.mark.tier1 @pytest.mark.e2e -def test_positive_end_to_end_with_host_parameters(module_org, module_location): +def test_positive_end_to_end_with_host_parameters(module_org, module_location, module_target_sat): """Create a host with a host parameters specified then remove and update with the newly specified parameters @@ -658,7 +676,7 @@ def test_positive_end_to_end_with_host_parameters(module_org, module_location): :CaseImportance: Critical """ parameters = [{'name': gen_string('alpha'), 'value': gen_string('alpha')}] - host = entities.Host( + host = module_target_sat.api.Host( organization=module_org, location=module_location, host_parameters_attributes=parameters, @@ -683,7 +701,7 @@ def test_positive_end_to_end_with_host_parameters(module_org, module_location): @pytest.mark.tier2 @pytest.mark.e2e def test_positive_end_to_end_with_image( - module_org, module_location, module_cr_libvirt, module_libvirt_image + module_org, module_location, module_cr_libvirt, module_libvirt_image, module_target_sat ): """Create a host with an image specified then remove it and update the host with the same image afterwards @@ -695,7 +713,7 @@ def test_positive_end_to_end_with_image( :CaseLevel: Integration """ - host = entities.Host( + host = module_target_sat.api.Host( organization=module_org, location=module_location, compute_resource=module_cr_libvirt, @@ -715,7 +733,7 @@ def test_positive_end_to_end_with_image( @pytest.mark.tier1 @pytest.mark.parametrize('method', ['build', 'image']) def test_positive_create_with_provision_method( - method, module_org, module_location, module_cr_libvirt + method, module_org, module_location, module_cr_libvirt, module_target_sat ): """Create a host with provision method specified @@ -728,7 +746,7 @@ def test_positive_create_with_provision_method( :CaseImportance: Critical """ # Compute resource is required for 'image' method - host = entities.Host( + host = module_target_sat.api.Host( organization=module_org, location=module_location, compute_resource=module_cr_libvirt, @@ -738,7 +756,7 @@ def test_positive_create_with_provision_method( @pytest.mark.tier1 -def test_positive_delete(): +def test_positive_delete(target_sat): """Delete a host :id: ec725359-a75e-498c-9da8-f5abd2343dd3 @@ -747,14 +765,16 @@ def test_positive_delete(): :CaseImportance: Critical """ - host = entities.Host().create() + host = target_sat.api.Host().create() host.delete() with pytest.raises(HTTPError): host.read() @pytest.mark.tier2 -def test_positive_create_and_update_domain(module_org, module_location, module_domain): +def test_positive_create_and_update_domain( + module_org, module_location, module_domain, module_target_sat +): """Create and update a host with a domain :id: 8ca9f67c-4c11-40f9-b434-4f200bad000f @@ -763,12 +783,14 @@ def test_positive_create_and_update_domain(module_org, module_location, module_d :CaseLevel: Integration """ - host = entities.Host( + host = module_target_sat.api.Host( organization=module_org, location=module_location, domain=module_domain ).create() assert host.domain.read().name == module_domain.name - new_domain = entities.Domain(organization=[module_org], location=[module_location]).create() + new_domain = module_target_sat.api.Domain( + organization=[module_org], location=[module_location] + ).create() host.domain = new_domain host = host.update(['domain']) assert host.domain.read().name == new_domain.name @@ -802,7 +824,7 @@ def test_positive_create_and_update_env( @pytest.mark.tier2 -def test_positive_create_and_update_arch(module_architecture): +def test_positive_create_and_update_arch(module_architecture, module_target_sat): """Create and update a host with an architecture :id: 5f190b14-e6db-46e1-8cd1-e94e048e6a77 @@ -811,17 +833,17 @@ def test_positive_create_and_update_arch(module_architecture): :CaseLevel: Integration """ - host = entities.Host(architecture=module_architecture).create() + host = module_target_sat.api.Host(architecture=module_architecture).create() assert host.architecture.read().name == module_architecture.name - new_arch = entities.Architecture(operatingsystem=[host.operatingsystem]).create() + new_arch = module_target_sat.api.Architecture(operatingsystem=[host.operatingsystem]).create() host.architecture = new_arch host = host.update(['architecture']) assert host.architecture.read().name == new_arch.name @pytest.mark.tier2 -def test_positive_create_and_update_os(module_os): +def test_positive_create_and_update_os(module_os, module_target_sat): """Create and update a host with an operating system :id: 46edced1-8909-4066-b196-b8e22512341f @@ -830,13 +852,13 @@ def test_positive_create_and_update_os(module_os): :CaseLevel: Integration """ - host = entities.Host(operatingsystem=module_os).create() + host = module_target_sat.api.Host(operatingsystem=module_os).create() assert host.operatingsystem.read().name == module_os.name - new_os = entities.OperatingSystem( + new_os = module_target_sat.api.OperatingSystem( architecture=[host.architecture], ptable=[host.ptable] ).create() - medium = entities.Media(id=host.medium.id).read() + medium = module_target_sat.api.Media(id=host.medium.id).read() medium.operatingsystem.append(new_os) medium.update(['operatingsystem']) host.operatingsystem = new_os @@ -845,7 +867,7 @@ def test_positive_create_and_update_os(module_os): @pytest.mark.tier2 -def test_positive_create_and_update_medium(module_org, module_location): +def test_positive_create_and_update_medium(module_org, module_location, module_target_sat): """Create and update a host with a medium :id: d81cb65c-48b3-4ce3-971e-51b9dd123697 @@ -854,11 +876,13 @@ def test_positive_create_and_update_medium(module_org, module_location): :CaseLevel: Integration """ - medium = entities.Media(organization=[module_org], location=[module_location]).create() - host = entities.Host(medium=medium).create() + medium = module_target_sat.api.Media( + organization=[module_org], location=[module_location] + ).create() + host = module_target_sat.api.Host(medium=medium).create() assert host.medium.read().name == medium.name - new_medium = entities.Media( + new_medium = module_target_sat.api.Media( operatingsystem=[host.operatingsystem], location=[host.location], organization=[host.organization], @@ -907,7 +931,7 @@ def test_negative_update_mac(module_host): @pytest.mark.tier2 -def test_negative_update_arch(module_architecture): +def test_negative_update_arch(module_architecture, module_target_sat): """Attempt to update a host with an architecture, which does not belong to host's operating system @@ -917,7 +941,7 @@ def test_negative_update_arch(module_architecture): :CaseLevel: Integration """ - host = entities.Host().create() + host = module_target_sat.api.Host().create() host.architecture = module_architecture with pytest.raises(HTTPError): host = host.update(['architecture']) @@ -925,7 +949,7 @@ def test_negative_update_arch(module_architecture): @pytest.mark.tier2 -def test_negative_update_os(): +def test_negative_update_os(target_sat): """Attempt to update a host with an operating system, which is not associated with host's medium @@ -935,8 +959,8 @@ def test_negative_update_os(): :CaseLevel: Integration """ - host = entities.Host().create() - new_os = entities.OperatingSystem( + host = target_sat.api.Host().create() + new_os = target_sat.api.OperatingSystem( architecture=[host.architecture], ptable=[host.ptable] ).create() host.operatingsystem = new_os @@ -963,9 +987,9 @@ def test_positive_read_content_source_id( :CaseLevel: System """ - proxy = entities.SmartProxy().search(query={'url': f'{target_sat.url}:9090'})[0].read() + proxy = target_sat.api.SmartProxy().search(query={'url': f'{target_sat.url}:9090'})[0].read() module_published_cv.version[0].promote(data={'environment_ids': module_lce.id, 'force': False}) - host = entities.Host( + host = target_sat.api.Host( organization=module_org, location=module_location, content_facet_attributes={ @@ -1407,7 +1431,7 @@ class TestHostInterface: @pytest.mark.tier1 @pytest.mark.e2e - def test_positive_create_end_to_end(self, module_host): + def test_positive_create_end_to_end(self, module_host, target_sat): """Create update and delete an interface with different names and minimal input parameters @@ -1418,7 +1442,7 @@ def test_positive_create_end_to_end(self, module_host): :CaseImportance: Critical """ name = gen_choice(datafactory.valid_interfaces_list()) - interface = entities.Interface(host=module_host, name=name).create() + interface = target_sat.api.Interface(host=module_host, name=name).create() assert interface.name == name new_name = gen_choice(datafactory.valid_interfaces_list()) interface.name = new_name @@ -1429,7 +1453,7 @@ def test_positive_create_end_to_end(self, module_host): interface.read() @pytest.mark.tier1 - def test_negative_end_to_end(self, module_host): + def test_negative_end_to_end(self, module_host, target_sat): """Attempt to create and update an interface with different invalid entries as names (>255 chars, unsupported string types), at the end attempt to remove primary interface @@ -1442,9 +1466,9 @@ def test_negative_end_to_end(self, module_host): """ name = gen_choice(datafactory.invalid_interfaces_list()) with pytest.raises(HTTPError) as error: - entities.Interface(host=module_host, name=name).create() + target_sat.api.Interface(host=module_host, name=name).create() assert str(422) in str(error) - interface = entities.Interface(host=module_host).create() + interface = target_sat.api.Interface(host=module_host).create() interface.name = name with pytest.raises(HTTPError) as error: interface.update(['name']) @@ -1463,7 +1487,7 @@ def test_negative_end_to_end(self, module_host): @pytest.mark.upgrade @pytest.mark.tier1 - def test_positive_delete_and_check_host(self): + def test_positive_delete_and_check_host(self, target_sat): """Delete host's interface (not primary) and make sure the host was not accidentally removed altogether with the interface @@ -1476,8 +1500,8 @@ def test_positive_delete_and_check_host(self): :CaseImportance: Critical """ - host = entities.Host().create() - interface = entities.Interface(host=host, primary=False).create() + host = target_sat.api.Host().create() + interface = target_sat.api.Interface(host=host, primary=False).create() interface.delete() with pytest.raises(HTTPError): interface.read() @@ -1491,7 +1515,7 @@ class TestHostBulkAction: """Tests for host bulk actions.""" @pytest.mark.tier2 - def test_positive_bulk_destroy(self, module_org): + def test_positive_bulk_destroy(self, module_org, module_target_sat): """Destroy multiple hosts make sure that hosts were removed, or were not removed when host is excluded from the list. @@ -1506,10 +1530,10 @@ def test_positive_bulk_destroy(self, module_org): host_ids = [] for _ in range(3): name = gen_choice(datafactory.valid_hosts_list()) - host = entities.Host(name=name, organization=module_org).create() + host = module_target_sat.api.Host(name=name, organization=module_org).create() host_ids.append(host.id) - entities.Host().bulk_destroy( + module_target_sat.api.Host().bulk_destroy( data={ 'organization_id': module_org.id, 'included': {'ids': host_ids}, @@ -1517,15 +1541,15 @@ def test_positive_bulk_destroy(self, module_org): } ) for host_id in host_ids[:-1]: - result = entities.Host(id=host_id).read() + result = module_target_sat.api.Host(id=host_id).read() assert result.id == host_id with pytest.raises(HTTPError): - entities.Host(id=host_ids[-1]).read() + module_target_sat.api.Host(id=host_ids[-1]).read() - entities.Host().bulk_destroy( + module_target_sat.api.Host().bulk_destroy( data={'organization_id': module_org.id, 'included': {'ids': host_ids[:-1]}} ) for host_id in host_ids[:-1]: with pytest.raises(HTTPError): - entities.Host(id=host_id).read() + module_target_sat.api.Host(id=host_id).read() diff --git a/tests/foreman/api/test_hostcollection.py b/tests/foreman/api/test_hostcollection.py index 0b5c0147ead..8935779101c 100644 --- a/tests/foreman/api/test_hostcollection.py +++ b/tests/foreman/api/test_hostcollection.py @@ -19,7 +19,6 @@ from random import choice, randint from broker import Broker -from nailgun import entities import pytest from requests.exceptions import HTTPError @@ -32,15 +31,15 @@ @pytest.fixture(scope='module') -def fake_hosts(module_org): +def fake_hosts(module_org, module_target_sat): """Create content hosts that can be shared by tests.""" - hosts = [entities.Host(organization=module_org).create() for _ in range(2)] + hosts = [module_target_sat.api.Host(organization=module_org).create() for _ in range(2)] return hosts @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_create_with_name(module_org, name): +def test_positive_create_with_name(module_org, name, module_target_sat): """Create host collections with different names. :id: 8f2b9223-f5be-4cb1-8316-01ea747cae14 @@ -52,12 +51,14 @@ def test_positive_create_with_name(module_org, name): :CaseImportance: Critical """ - host_collection = entities.HostCollection(name=name, organization=module_org).create() + host_collection = module_target_sat.api.HostCollection( + name=name, organization=module_org + ).create() assert host_collection.name == name @pytest.mark.tier1 -def test_positive_list(module_org): +def test_positive_list(module_org, module_target_sat): """Create new host collection and then retrieve list of all existing host collections @@ -72,13 +73,13 @@ def test_positive_list(module_org): :CaseImportance: Critical """ - entities.HostCollection(organization=module_org).create() - hc_list = entities.HostCollection().search() + module_target_sat.api.HostCollection(organization=module_org).create() + hc_list = module_target_sat.api.HostCollection().search() assert len(hc_list) >= 1 @pytest.mark.tier1 -def test_positive_list_for_organization(): +def test_positive_list_for_organization(target_sat): """Create host collection for specific organization. Retrieve list of host collections for that organization @@ -89,16 +90,16 @@ def test_positive_list_for_organization(): :CaseImportance: Critical """ - org = entities.Organization().create() - hc = entities.HostCollection(organization=org).create() - hc_list = entities.HostCollection(organization=org).search() + org = target_sat.api.Organization().create() + hc = target_sat.api.HostCollection(organization=org).create() + hc_list = target_sat.api.HostCollection(organization=org).search() assert len(hc_list) == 1 assert hc_list[0].id == hc.id @pytest.mark.parametrize('desc', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_create_with_description(module_org, desc): +def test_positive_create_with_description(module_org, desc, module_target_sat): """Create host collections with different descriptions. :id: 9d13392f-8d9d-4ff1-8909-4233e4691055 @@ -110,12 +111,14 @@ def test_positive_create_with_description(module_org, desc): :CaseImportance: Critical """ - host_collection = entities.HostCollection(description=desc, organization=module_org).create() + host_collection = module_target_sat.api.HostCollection( + description=desc, organization=module_org + ).create() assert host_collection.description == desc @pytest.mark.tier1 -def test_positive_create_with_limit(module_org): +def test_positive_create_with_limit(module_org, module_target_sat): """Create host collections with different limits. :id: 86d9387b-7036-4794-96fd-5a3472dd9160 @@ -127,13 +130,15 @@ def test_positive_create_with_limit(module_org): """ for _ in range(5): limit = randint(1, 30) - host_collection = entities.HostCollection(max_hosts=limit, organization=module_org).create() + host_collection = module_target_sat.api.HostCollection( + max_hosts=limit, organization=module_org + ).create() assert host_collection.max_hosts == limit @pytest.mark.parametrize("unlimited", [False, True]) @pytest.mark.tier1 -def test_positive_create_with_unlimited_hosts(module_org, unlimited): +def test_positive_create_with_unlimited_hosts(module_org, unlimited, module_target_sat): """Create host collection with different values of 'unlimited hosts' parameter. @@ -146,7 +151,7 @@ def test_positive_create_with_unlimited_hosts(module_org, unlimited): :CaseImportance: Critical """ - host_collection = entities.HostCollection( + host_collection = module_target_sat.api.HostCollection( max_hosts=None if unlimited else 1, organization=module_org, unlimited_hosts=unlimited, @@ -155,7 +160,7 @@ def test_positive_create_with_unlimited_hosts(module_org, unlimited): @pytest.mark.tier1 -def test_positive_create_with_host(module_org, fake_hosts): +def test_positive_create_with_host(module_org, fake_hosts, module_target_sat): """Create a host collection that contains a host. :id: 9dc0ad72-58c2-4079-b1ca-2c4373472f0f @@ -167,14 +172,14 @@ def test_positive_create_with_host(module_org, fake_hosts): :BZ: 1325989 """ - host_collection = entities.HostCollection( + host_collection = module_target_sat.api.HostCollection( host=[fake_hosts[0]], organization=module_org ).create() assert len(host_collection.host) == 1 @pytest.mark.tier1 -def test_positive_create_with_hosts(module_org, fake_hosts): +def test_positive_create_with_hosts(module_org, fake_hosts, module_target_sat): """Create a host collection that contains hosts. :id: bb8d2b42-9a8b-4c4f-ba0c-c56ae5a7eb1d @@ -186,12 +191,14 @@ def test_positive_create_with_hosts(module_org, fake_hosts): :BZ: 1325989 """ - host_collection = entities.HostCollection(host=fake_hosts, organization=module_org).create() + host_collection = module_target_sat.api.HostCollection( + host=fake_hosts, organization=module_org + ).create() assert len(host_collection.host) == len(fake_hosts) @pytest.mark.tier2 -def test_positive_add_host(module_org, fake_hosts): +def test_positive_add_host(module_org, fake_hosts, module_target_sat): """Add a host to host collection. :id: da8bc901-7ac8-4029-bb62-af21aa4d3a88 @@ -202,7 +209,7 @@ def test_positive_add_host(module_org, fake_hosts): :BZ:1325989 """ - host_collection = entities.HostCollection(organization=module_org).create() + host_collection = module_target_sat.api.HostCollection(organization=module_org).create() host_collection.host_ids = [fake_hosts[0].id] host_collection = host_collection.update(['host_ids']) assert len(host_collection.host) == 1 @@ -210,7 +217,7 @@ def test_positive_add_host(module_org, fake_hosts): @pytest.mark.upgrade @pytest.mark.tier2 -def test_positive_add_hosts(module_org, fake_hosts): +def test_positive_add_hosts(module_org, fake_hosts, module_target_sat): """Add hosts to host collection. :id: f76b4db1-ccd5-47ab-be15-8c7d91d03b22 @@ -221,7 +228,7 @@ def test_positive_add_hosts(module_org, fake_hosts): :BZ: 1325989 """ - host_collection = entities.HostCollection(organization=module_org).create() + host_collection = module_target_sat.api.HostCollection(organization=module_org).create() host_ids = [str(host.id) for host in fake_hosts] host_collection.host_ids = host_ids host_collection = host_collection.update(['host_ids']) @@ -229,7 +236,7 @@ def test_positive_add_hosts(module_org, fake_hosts): @pytest.mark.tier1 -def test_positive_read_host_ids(module_org, fake_hosts): +def test_positive_read_host_ids(module_org, fake_hosts, module_target_sat): """Read a host collection and look at the ``host_ids`` field. :id: 444a1528-64c8-41b6-ba2b-6c49799d5980 @@ -241,7 +248,9 @@ def test_positive_read_host_ids(module_org, fake_hosts): :BZ:1325989 """ - host_collection = entities.HostCollection(host=fake_hosts, organization=module_org).create() + host_collection = module_target_sat.api.HostCollection( + host=fake_hosts, organization=module_org + ).create() assert frozenset(host.id for host in host_collection.host) == frozenset( host.id for host in fake_hosts ) @@ -249,7 +258,7 @@ def test_positive_read_host_ids(module_org, fake_hosts): @pytest.mark.parametrize('new_name', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_update_name(module_org, new_name): +def test_positive_update_name(module_org, new_name, module_target_sat): """Check if host collection name can be updated :id: b2dedb99-6dd7-41be-8aaa-74065c820ac6 @@ -260,14 +269,14 @@ def test_positive_update_name(module_org, new_name): :CaseImportance: Critical """ - host_collection = entities.HostCollection(organization=module_org).create() + host_collection = module_target_sat.api.HostCollection(organization=module_org).create() host_collection.name = new_name assert host_collection.update().name == new_name @pytest.mark.parametrize('new_desc', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_update_description(module_org, new_desc): +def test_positive_update_description(module_org, new_desc, module_target_sat): """Check if host collection description can be updated :id: f8e9bd1c-1525-4b5f-a07c-eb6b6e7aa628 @@ -278,13 +287,13 @@ def test_positive_update_description(module_org, new_desc): :CaseImportance: Critical """ - host_collection = entities.HostCollection(organization=module_org).create() + host_collection = module_target_sat.api.HostCollection(organization=module_org).create() host_collection.description = new_desc assert host_collection.update().description == new_desc @pytest.mark.tier1 -def test_positive_update_limit(module_org): +def test_positive_update_limit(module_org, module_target_sat): """Check if host collection limit can be updated :id: 4eda7796-cd81-453b-9b72-4ef84b2c1d8c @@ -293,7 +302,7 @@ def test_positive_update_limit(module_org): :CaseImportance: Critical """ - host_collection = entities.HostCollection( + host_collection = module_target_sat.api.HostCollection( max_hosts=1, organization=module_org, unlimited_hosts=False ).create() for limit in (1, 3, 5, 10, 20): @@ -302,7 +311,7 @@ def test_positive_update_limit(module_org): @pytest.mark.tier1 -def test_positive_update_unlimited_hosts(module_org): +def test_positive_update_unlimited_hosts(module_org, module_target_sat): """Check if host collection 'unlimited hosts' parameter can be updated :id: 09a3973d-9832-4255-87bf-f9eaeab4aee8 @@ -313,7 +322,7 @@ def test_positive_update_unlimited_hosts(module_org): :CaseImportance: Critical """ random_unlimited = choice([True, False]) - host_collection = entities.HostCollection( + host_collection = module_target_sat.api.HostCollection( max_hosts=1 if not random_unlimited else None, organization=module_org, unlimited_hosts=random_unlimited, @@ -326,7 +335,7 @@ def test_positive_update_unlimited_hosts(module_org): @pytest.mark.tier1 -def test_positive_update_host(module_org, fake_hosts): +def test_positive_update_host(module_org, fake_hosts, module_target_sat): """Update host collection's host. :id: 23082854-abcf-4085-be9c-a5d155446acb @@ -335,7 +344,7 @@ def test_positive_update_host(module_org, fake_hosts): :CaseImportance: Critical """ - host_collection = entities.HostCollection( + host_collection = module_target_sat.api.HostCollection( host=[fake_hosts[0]], organization=module_org ).create() host_collection.host_ids = [fake_hosts[1].id] @@ -345,7 +354,7 @@ def test_positive_update_host(module_org, fake_hosts): @pytest.mark.upgrade @pytest.mark.tier1 -def test_positive_update_hosts(module_org, fake_hosts): +def test_positive_update_hosts(module_org, fake_hosts, module_target_sat): """Update host collection's hosts. :id: 0433b37d-ae16-456f-a51d-c7b800334861 @@ -354,8 +363,10 @@ def test_positive_update_hosts(module_org, fake_hosts): :CaseImportance: Critical """ - host_collection = entities.HostCollection(host=fake_hosts, organization=module_org).create() - new_hosts = [entities.Host(organization=module_org).create() for _ in range(2)] + host_collection = module_target_sat.api.HostCollection( + host=fake_hosts, organization=module_org + ).create() + new_hosts = [module_target_sat.api.Host(organization=module_org).create() for _ in range(2)] host_ids = [str(host.id) for host in new_hosts] host_collection.host_ids = host_ids host_collection = host_collection.update(['host_ids']) @@ -364,7 +375,7 @@ def test_positive_update_hosts(module_org, fake_hosts): @pytest.mark.upgrade @pytest.mark.tier1 -def test_positive_delete(module_org): +def test_positive_delete(module_org, module_target_sat): """Check if host collection can be deleted :id: 13a16cd2-16ce-4966-8c03-5d821edf963b @@ -373,7 +384,7 @@ def test_positive_delete(module_org): :CaseImportance: Critical """ - host_collection = entities.HostCollection(organization=module_org).create() + host_collection = module_target_sat.api.HostCollection(organization=module_org).create() host_collection.delete() with pytest.raises(HTTPError): host_collection.read() @@ -381,7 +392,7 @@ def test_positive_delete(module_org): @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) @pytest.mark.tier1 -def test_negative_create_with_invalid_name(module_org, name): +def test_negative_create_with_invalid_name(module_org, name, module_target_sat): """Try to create host collections with different invalid names :id: 38f67d04-a19d-4eab-a577-21b8d62c7389 @@ -393,7 +404,7 @@ def test_negative_create_with_invalid_name(module_org, name): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.HostCollection(name=name, organization=module_org).create() + module_target_sat.api.HostCollection(name=name, organization=module_org).create() @pytest.mark.tier1 @@ -418,14 +429,14 @@ def test_positive_add_remove_subscription(module_org, module_ak_cv_lce, target_s """ # this command creates a host collection and "appends", makes available, to the AK module_ak_cv_lce.host_collection.append( - entities.HostCollection(organization=module_org).create() + target_sat.api.HostCollection(organization=module_org).create() ) # Move HC from Add tab to List tab on AK view module_ak_cv_lce = module_ak_cv_lce.update(['host_collection']) # Create a product so we have a subscription to use - product = entities.Product(organization=module_org).create() + product = target_sat.api.Product(organization=module_org).create() prod_name = product.name - product_subscription = entities.Subscription(organization=module_org).search( + product_subscription = target_sat.api.Subscription(organization=module_org).search( query={'search': f'name={prod_name}'} )[0] # Create and register VMs as members of Host Collection @@ -438,7 +449,7 @@ def test_positive_add_remove_subscription(module_org, module_ak_cv_lce, target_s host_ids = [host.id for host in host_collection.host] # Add subscription # Call nailgun to make the API PUT to members of Host Collection - entities.Host().bulk_add_subscriptions( + target_sat.api.Host().bulk_add_subscriptions( data={ "organization_id": module_org.id, "included": {"ids": host_ids}, @@ -447,13 +458,13 @@ def test_positive_add_remove_subscription(module_org, module_ak_cv_lce, target_s ) # GET the subscriptions from hosts and assert they are there for host_id in host_ids: - req = entities.HostSubscription(host=host_id).subscriptions() + req = target_sat.api.HostSubscription(host=host_id).subscriptions() assert ( prod_name in req['results'][0]['product_name'] ), 'Subscription not applied to HC members' # Remove the subscription # Call nailgun to make the API PUT to members of Host Collection - entities.Host().bulk_remove_subscriptions( + target_sat.api.Host().bulk_remove_subscriptions( data={ "organization_id": module_org.id, "included": {"ids": host_ids}, @@ -462,5 +473,5 @@ def test_positive_add_remove_subscription(module_org, module_ak_cv_lce, target_s ) # GET the subscriptions from hosts and assert they are gone for host_id in host_ids: - req = entities.HostSubscription(host=host_id).subscriptions() + req = target_sat.api.HostSubscription(host=host_id).subscriptions() assert not req['results'], 'Subscription not removed from HC members' diff --git a/tests/foreman/api/test_hostgroup.py b/tests/foreman/api/test_hostgroup.py index 9e3c3aa0c21..784b404e5dd 100644 --- a/tests/foreman/api/test_hostgroup.py +++ b/tests/foreman/api/test_hostgroup.py @@ -19,7 +19,7 @@ from random import randint from fauxfactory import gen_string -from nailgun import client, entities, entity_fields +from nailgun import client, entity_fields import pytest from requests.exceptions import HTTPError @@ -32,8 +32,10 @@ @pytest.fixture -def hostgroup(module_org, module_location): - return entities.HostGroup(location=[module_location], organization=[module_org]).create() +def hostgroup(module_org, module_location, module_target_sat): + return module_target_sat.api.HostGroup( + location=[module_location], organization=[module_org] + ).create() @pytest.fixture @@ -158,7 +160,7 @@ def test_inherit_puppetclass(self, session_puppet_enabled_sat): @pytest.mark.upgrade @pytest.mark.tier3 - def test_rebuild_config(self, module_org, module_location, hostgroup): + def test_rebuild_config(self, module_org, module_location, hostgroup, module_target_sat): """'Rebuild orchestration config' of an existing host group :id: 58bf7015-18fc-4d25-9b64-7f2dd6dde425 @@ -169,12 +171,12 @@ def test_rebuild_config(self, module_org, module_location, hostgroup): :CaseLevel: System """ - lce = entities.LifecycleEnvironment(organization=module_org).create() - content_view = entities.ContentView(organization=module_org).create() + lce = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() + content_view = module_target_sat.api.ContentView(organization=module_org).create() content_view.publish() content_view = content_view.read() content_view.version[0].promote(data={'environment_ids': lce.id, 'force': False}) - entities.Host( + module_target_sat.api.Host( hostgroup=hostgroup, location=module_location, organization=module_org, @@ -193,7 +195,7 @@ def test_rebuild_config(self, module_org, module_location, hostgroup): @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(valid_hostgroups_list())) - def test_positive_create_with_name(self, name, module_org, module_location): + def test_positive_create_with_name(self, name, module_org, module_location, module_target_sat): """Create a hostgroup with different names :id: fd5d353c-fd0c-4752-8a83-8f399b4c3416 @@ -204,13 +206,13 @@ def test_positive_create_with_name(self, name, module_org, module_location): :CaseImportance: Critical """ - hostgroup = entities.HostGroup( + hostgroup = module_target_sat.api.HostGroup( location=[module_location], name=name, organization=[module_org] ).create() assert name == hostgroup.name @pytest.mark.tier1 - def test_positive_clone(self, hostgroup): + def test_positive_clone(self, hostgroup, target_sat): """Create a hostgroup by cloning an existing one :id: 44ac8b3b-9cb0-4a9e-ad9b-2c67b2411922 @@ -220,7 +222,7 @@ def test_positive_clone(self, hostgroup): :CaseImportance: Critical """ hostgroup_cloned_name = gen_string('alpha') - hostgroup_cloned = entities.HostGroup(id=hostgroup.id).clone( + hostgroup_cloned = target_sat.api.HostGroup(id=hostgroup.id).clone( data={'name': hostgroup_cloned_name} ) hostgroup_origin = hostgroup.read_json() @@ -402,20 +404,20 @@ def test_positive_create_with_realm(self, module_org, module_location, target_sa :CaseLevel: Integration """ - realm = entities.Realm( + realm = target_sat.api.Realm( location=[module_location], organization=[module_org], - realm_proxy=entities.SmartProxy().search( + realm_proxy=target_sat.api.SmartProxy().search( query={'search': f'url = {target_sat.url}:9090'} )[0], ).create() - hostgroup = entities.HostGroup( + hostgroup = target_sat.api.HostGroup( location=[module_location], organization=[module_org], realm=realm ).create() assert hostgroup.realm.read().name == realm.name @pytest.mark.tier2 - def test_positive_create_with_locs(self, module_org): + def test_positive_create_with_locs(self, module_org, module_target_sat): """Create a hostgroup with multiple locations specified :id: 0c2ee2ff-9e7a-4931-8cea-f4eecbd8c4c0 @@ -427,12 +429,17 @@ def test_positive_create_with_locs(self, module_org): :CaseLevel: Integration """ - locs = [entities.Location(organization=[module_org]).create() for _ in range(randint(3, 5))] - hostgroup = entities.HostGroup(location=locs, organization=[module_org]).create() + locs = [ + module_target_sat.api.Location(organization=[module_org]).create() + for _ in range(randint(3, 5)) + ] + hostgroup = module_target_sat.api.HostGroup( + location=locs, organization=[module_org] + ).create() assert {loc.name for loc in locs} == {loc.read().name for loc in hostgroup.location} @pytest.mark.tier2 - def test_positive_create_with_orgs(self): + def test_positive_create_with_orgs(self, target_sat): """Create a hostgroup with multiple organizations specified :id: 09642238-cf0d-469a-a0b5-c167b1b8edf5 @@ -444,8 +451,8 @@ def test_positive_create_with_orgs(self): :CaseLevel: Integration """ - orgs = [entities.Organization().create() for _ in range(randint(3, 5))] - hostgroup = entities.HostGroup(organization=orgs).create() + orgs = [target_sat.api.Organization().create() for _ in range(randint(3, 5))] + hostgroup = target_sat.api.HostGroup(organization=orgs).create() assert {org.name for org in orgs}, {org.read().name for org in hostgroup.organization} @pytest.mark.tier1 @@ -497,20 +504,20 @@ def test_positive_update_realm(self, module_org, module_location, target_sat): :CaseLevel: Integration """ - realm = entities.Realm( + realm = target_sat.api.Realm( location=[module_location], organization=[module_org], - realm_proxy=entities.SmartProxy().search( + realm_proxy=target_sat.api.SmartProxy().search( query={'search': f'url = {target_sat.url}:9090'} )[0], ).create() - hostgroup = entities.HostGroup( + hostgroup = target_sat.api.HostGroup( location=[module_location], organization=[module_org], realm=realm ).create() - new_realm = entities.Realm( + new_realm = target_sat.api.Realm( location=[module_location], organization=[module_org], - realm_proxy=entities.SmartProxy().search( + realm_proxy=target_sat.api.SmartProxy().search( query={'search': f'url = {target_sat.url}:9090'} )[0], ).create() @@ -549,7 +556,7 @@ def test_positive_update_content_source(self, hostgroup, target_sat): :CaseLevel: Integration """ - new_content_source = entities.SmartProxy().search( + new_content_source = target_sat.api.SmartProxy().search( query={'search': f'url = {target_sat.url}:9090'} )[0] hostgroup.content_source = new_content_source @@ -557,7 +564,7 @@ def test_positive_update_content_source(self, hostgroup, target_sat): assert hostgroup.content_source.read().name == new_content_source.name @pytest.mark.tier2 - def test_positive_update_locs(self, module_org, hostgroup): + def test_positive_update_locs(self, module_org, hostgroup, module_target_sat): """Update a hostgroup with new multiple locations :id: b045f7e8-d7c0-428b-a29c-8d54e53742e2 @@ -569,14 +576,15 @@ def test_positive_update_locs(self, module_org, hostgroup): :CaseLevel: Integration """ new_locs = [ - entities.Location(organization=[module_org]).create() for _ in range(randint(3, 5)) + module_target_sat.api.Location(organization=[module_org]).create() + for _ in range(randint(3, 5)) ] hostgroup.location = new_locs hostgroup = hostgroup.update(['location']) assert {loc.name for loc in new_locs}, {loc.read().name for loc in hostgroup.location} @pytest.mark.tier2 - def test_positive_update_orgs(self, hostgroup): + def test_positive_update_orgs(self, hostgroup, target_sat): """Update a hostgroup with new multiple organizations :id: 5f6bd4f9-4bd6-4d7e-9a91-de824299020e @@ -587,14 +595,14 @@ def test_positive_update_orgs(self, hostgroup): :CaseLevel: Integration """ - new_orgs = [entities.Organization().create() for _ in range(randint(3, 5))] + new_orgs = [target_sat.api.Organization().create() for _ in range(randint(3, 5))] hostgroup.organization = new_orgs hostgroup = hostgroup.update(['organization']) assert {org.name for org in new_orgs} == {org.read().name for org in hostgroup.organization} @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) - def test_negative_create_with_name(self, name, module_org, module_location): + def test_negative_create_with_name(self, name, module_org, module_location, module_target_sat): """Attempt to create a hostgroup with invalid names :id: 3f5aa17a-8db9-4fe9-b309-b8ec5e739da1 @@ -606,7 +614,7 @@ def test_negative_create_with_name(self, name, module_org, module_location): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.HostGroup( + module_target_sat.api.HostGroup( location=[module_location], name=name, organization=[module_org] ).create() @@ -630,7 +638,7 @@ def test_negative_update_name(self, new_name, hostgroup): assert hostgroup.read().name == original_name @pytest.mark.tier2 - def test_positive_create_with_group_parameters(self, module_org): + def test_positive_create_with_group_parameters(self, module_org, module_target_sat): """Create a hostgroup with 'group parameters' specified :id: 0959e2a2-d635-482b-9b2e-d33990d6f0dc @@ -646,7 +654,7 @@ def test_positive_create_with_group_parameters(self, module_org): :BZ: 1710853 """ group_params = {'name': gen_string('alpha'), 'value': gen_string('alpha')} - hostgroup = entities.HostGroup( + hostgroup = module_target_sat.api.HostGroup( organization=[module_org], group_parameters_attributes=[group_params] ).create() assert group_params['name'] == hostgroup.group_parameters_attributes[0]['name'] diff --git a/tests/foreman/api/test_http_proxy.py b/tests/foreman/api/test_http_proxy.py index f9c3023c77e..ed713078b4f 100644 --- a/tests/foreman/api/test_http_proxy.py +++ b/tests/foreman/api/test_http_proxy.py @@ -17,7 +17,6 @@ :Upstream: No """ from fauxfactory import gen_string -from nailgun import entities import pytest from robottelo import constants @@ -206,7 +205,7 @@ def test_positive_auto_attach_with_http_proxy( @pytest.mark.e2e @pytest.mark.tier2 -def test_positive_assign_http_proxy_to_products(): +def test_positive_assign_http_proxy_to_products(target_sat): """Assign http_proxy to Products and check whether http-proxy is used during sync. @@ -219,15 +218,15 @@ def test_positive_assign_http_proxy_to_products(): :CaseImportance: Critical """ - org = entities.Organization().create() + org = target_sat.api.Organization().create() # create HTTP proxies - http_proxy_a = entities.HTTPProxy( + http_proxy_a = target_sat.api.HTTPProxy( name=gen_string('alpha', 15), url=settings.http_proxy.un_auth_proxy_url, organization=[org], ).create() - http_proxy_b = entities.HTTPProxy( + http_proxy_b = target_sat.api.HTTPProxy( name=gen_string('alpha', 15), url=settings.http_proxy.auth_proxy_url, username=settings.http_proxy.username, @@ -236,20 +235,20 @@ def test_positive_assign_http_proxy_to_products(): ).create() # Create products and repositories - product_a = entities.Product(organization=org).create() - product_b = entities.Product(organization=org).create() - repo_a1 = entities.Repository(product=product_a, http_proxy_policy='none').create() - repo_a2 = entities.Repository( + product_a = target_sat.api.Product(organization=org).create() + product_b = target_sat.api.Product(organization=org).create() + repo_a1 = target_sat.api.Repository(product=product_a, http_proxy_policy='none').create() + repo_a2 = target_sat.api.Repository( product=product_a, http_proxy_policy='use_selected_http_proxy', http_proxy_id=http_proxy_a.id, ).create() - repo_b1 = entities.Repository(product=product_b, http_proxy_policy='none').create() - repo_b2 = entities.Repository( + repo_b1 = target_sat.api.Repository(product=product_b, http_proxy_policy='none').create() + repo_b2 = target_sat.api.Repository( product=product_b, http_proxy_policy='global_default_http_proxy' ).create() # Add http_proxy to products - entities.ProductBulkAction().http_proxy( + target_sat.api.ProductBulkAction().http_proxy( data={ "ids": [product_a.id, product_b.id], "http_proxy_policy": "use_selected_http_proxy", diff --git a/tests/foreman/api/test_ldapauthsource.py b/tests/foreman/api/test_ldapauthsource.py index 10b9851b3d6..2139e7c947f 100644 --- a/tests/foreman/api/test_ldapauthsource.py +++ b/tests/foreman/api/test_ldapauthsource.py @@ -16,7 +16,6 @@ :Upstream: No """ -from nailgun import entities import pytest from requests.exceptions import HTTPError @@ -27,7 +26,9 @@ @pytest.mark.tier3 @pytest.mark.upgrade @pytest.mark.parametrize('auth_source_type', ['AD', 'IPA']) -def test_positive_endtoend(auth_source_type, module_org, module_location, ad_data, ipa_data): +def test_positive_endtoend( + auth_source_type, module_org, module_location, ad_data, ipa_data, module_target_sat +): """Create/update/delete LDAP authentication with AD using names of different types :id: e3607c97-7c48-4cf6-b119-2bfd895d9325 @@ -46,7 +47,7 @@ def test_positive_endtoend(auth_source_type, module_org, module_location, ad_dat auth_source_data = ipa_data auth_source_data['ldap_user_name'] = auth_source_data['ldap_user_cn'] auth_type_attr = LDAP_ATTR['login'] - authsource = entities.AuthSourceLDAP( + authsource = module_target_sat.api.AuthSourceLDAP( onthefly_register=True, account=auth_source_data['ldap_user_cn'], account_password=auth_source_data['ldap_user_passwd'], diff --git a/tests/foreman/api/test_lifecycleenvironment.py b/tests/foreman/api/test_lifecycleenvironment.py index b8e7b3ef347..79cece7c76f 100644 --- a/tests/foreman/api/test_lifecycleenvironment.py +++ b/tests/foreman/api/test_lifecycleenvironment.py @@ -21,7 +21,6 @@ :Upstream: No """ from fauxfactory import gen_string -from nailgun import entities import pytest from requests.exceptions import HTTPError @@ -34,20 +33,20 @@ @pytest.fixture(scope='module') -def module_lce(module_org): - return entities.LifecycleEnvironment( +def module_lce(module_org, module_target_sat): + return module_target_sat.api.LifecycleEnvironment( organization=module_org, description=gen_string('alpha') ).create() @pytest.fixture -def lce(module_org): - return entities.LifecycleEnvironment(organization=module_org).create() +def lce(module_org, module_target_sat): + return module_target_sat.api.LifecycleEnvironment(organization=module_org).create() @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_create_with_name(name): +def test_positive_create_with_name(name, target_sat): """Create lifecycle environment with valid name only :id: ec1d985a-6a39-4de6-b635-c803ecedd832 @@ -58,12 +57,12 @@ def test_positive_create_with_name(name): :parametrized: yes """ - assert entities.LifecycleEnvironment(name=name).create().name == name + assert target_sat.api.LifecycleEnvironment(name=name).create().name == name @pytest.mark.parametrize('desc', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_create_with_description(desc): +def test_positive_create_with_description(desc, target_sat): """Create lifecycle environment with valid description :id: 0bc05510-afc7-4087-ab75-1065ab5ba1d3 @@ -75,11 +74,11 @@ def test_positive_create_with_description(desc): :parametrized: yes """ - assert entities.LifecycleEnvironment(description=desc).create().description == desc + assert target_sat.api.LifecycleEnvironment(description=desc).create().description == desc @pytest.mark.tier1 -def test_positive_create_prior(module_org): +def test_positive_create_prior(module_org, module_target_sat): """Create a lifecycle environment with valid name with Library as prior @@ -90,13 +89,13 @@ def test_positive_create_prior(module_org): :CaseImportance: Critical """ - lc_env = entities.LifecycleEnvironment(organization=module_org).create() + lc_env = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() assert lc_env.prior.read().name == ENVIRONMENT @pytest.mark.parametrize('name', **parametrized(invalid_names_list())) @pytest.mark.tier3 -def test_negative_create_with_invalid_name(name): +def test_negative_create_with_invalid_name(name, target_sat): """Create lifecycle environment providing an invalid name :id: 7e8ea2e6-5927-4e86-8ea8-04c3feb524a6 @@ -108,12 +107,12 @@ def test_negative_create_with_invalid_name(name): :parametrized: yes """ with pytest.raises(HTTPError): - entities.LifecycleEnvironment(name=name).create() + target_sat.api.LifecycleEnvironment(name=name).create() @pytest.mark.parametrize('new_name', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_update_name(module_lce, new_name): +def test_positive_update_name(module_lce, new_name, module_target_sat): """Create lifecycle environment providing the initial name, then update its name to another valid name. @@ -125,13 +124,13 @@ def test_positive_update_name(module_lce, new_name): """ module_lce.name = new_name module_lce.update(['name']) - updated = entities.LifecycleEnvironment(id=module_lce.id).read() + updated = module_target_sat.api.LifecycleEnvironment(id=module_lce.id).read() assert new_name == updated.name @pytest.mark.parametrize('new_desc', **parametrized(valid_data_list())) @pytest.mark.tier2 -def test_positive_update_description(module_lce, new_desc): +def test_positive_update_description(module_lce, new_desc, module_target_sat): """Create lifecycle environment providing the initial description, then update its description to another one. @@ -147,7 +146,7 @@ def test_positive_update_description(module_lce, new_desc): """ module_lce.description = new_desc module_lce.update(['description']) - updated = entities.LifecycleEnvironment(id=module_lce.id).read() + updated = module_target_sat.api.LifecycleEnvironment(id=module_lce.id).read() assert new_desc == updated.description @@ -175,7 +174,7 @@ def test_negative_update_name(module_lce, new_name): @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_delete(lce, name): +def test_positive_delete(lce, name, target_sat): """Create lifecycle environment and then delete it. :id: cd5a97ca-c1e8-41c7-8d6b-f908916b24e1 @@ -188,12 +187,12 @@ def test_positive_delete(lce, name): """ lce.delete() with pytest.raises(HTTPError): - entities.LifecycleEnvironment(id=lce.id).read() + target_sat.api.LifecycleEnvironment(id=lce.id).read() @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier2 -def test_positive_search_in_org(name): +def test_positive_search_in_org(name, target_sat): """Search for a lifecycle environment and specify an org ID. :id: 110e4777-c374-4365-b676-b1db4552fe51 @@ -211,8 +210,8 @@ def test_positive_search_in_org(name): :parametrized: yes """ - new_org = entities.Organization().create() - lc_env = entities.LifecycleEnvironment(organization=new_org).create() + new_org = target_sat.api.Organization().create() + lc_env = target_sat.api.LifecycleEnvironment(organization=new_org).create() lc_envs = lc_env.search({'organization'}) assert len(lc_envs) == 2 assert {lc_env_.name for lc_env_ in lc_envs}, {'Library', lc_env.name} diff --git a/tests/foreman/api/test_media.py b/tests/foreman/api/test_media.py index e5524d914fd..97c6da607f4 100644 --- a/tests/foreman/api/test_media.py +++ b/tests/foreman/api/test_media.py @@ -19,7 +19,6 @@ import random from fauxfactory import gen_string, gen_url -from nailgun import entities import pytest from requests.exceptions import HTTPError @@ -35,8 +34,8 @@ class TestMedia: """Tests for ``api/v2/media``.""" @pytest.fixture(scope='class') - def class_media(self, module_org): - return entities.Media(organization=[module_org]).create() + def class_media(self, module_org, class_target_sat): + return class_target_sat.api.Media(organization=[module_org]).create() @pytest.mark.tier1 @pytest.mark.upgrade @@ -44,7 +43,7 @@ def class_media(self, module_org): 'name, new_name', **parametrized(list(zip(valid_data_list().values(), valid_data_list().values()))) ) - def test_positive_crud_with_name(self, module_org, name, new_name): + def test_positive_crud_with_name(self, module_org, name, new_name, module_target_sat): """Create, update, delete media with valid name only :id: b07a4549-7dd5-4b36-a1b4-9f8d48ddfcb5 @@ -55,9 +54,9 @@ def test_positive_crud_with_name(self, module_org, name, new_name): :CaseImportance: Critical """ - media = entities.Media(organization=[module_org], name=name).create() + media = module_target_sat.api.Media(organization=[module_org], name=name).create() assert media.name == name - media = entities.Media(id=media.id, name=new_name).update(['name']) + media = module_target_sat.api.Media(id=media.id, name=new_name).update(['name']) assert media.name == new_name media.delete() with pytest.raises(HTTPError): @@ -65,7 +64,7 @@ def test_positive_crud_with_name(self, module_org, name, new_name): @pytest.mark.tier1 @pytest.mark.parametrize('os_family', **parametrized(OPERATING_SYSTEMS)) - def test_positive_create_update_with_os_family(self, module_org, os_family): + def test_positive_create_update_with_os_family(self, module_org, os_family, module_target_sat): """Create and update media with every OS family possible :id: d02404f0-b2ad-412c-b1cd-0548254f7c88 @@ -75,14 +74,14 @@ def test_positive_create_update_with_os_family(self, module_org, os_family): :expectedresults: Media entity is created and has proper OS family assigned """ - media = entities.Media(organization=[module_org], os_family=os_family).create() + media = module_target_sat.api.Media(organization=[module_org], os_family=os_family).create() assert media.os_family == os_family new_os_family = new_os_family = random.choice(OPERATING_SYSTEMS) media.os_family = new_os_family assert media.update(['os_family']).os_family == new_os_family @pytest.mark.tier2 - def test_positive_create_with_location(self, module_org, module_location): + def test_positive_create_with_location(self, module_org, module_location, module_target_sat): """Create media entity assigned to non-default location :id: 1c4fa736-c145-46ca-9feb-c4046fc778c6 @@ -91,11 +90,13 @@ def test_positive_create_with_location(self, module_org, module_location): :CaseLevel: Integration """ - media = entities.Media(organization=[module_org], location=[module_location]).create() + media = module_target_sat.api.Media( + organization=[module_org], location=[module_location] + ).create() assert media.location[0].read().name == module_location.name @pytest.mark.tier2 - def test_positive_create_with_os(self, module_org): + def test_positive_create_with_os(self, module_org, module_target_sat): """Create media entity assigned to operation system entity :id: dec22198-ed07-480c-9306-fa5458baec0b @@ -104,12 +105,14 @@ def test_positive_create_with_os(self, module_org): :CaseLevel: Integration """ - os = entities.OperatingSystem().create() - media = entities.Media(organization=[module_org], operatingsystem=[os]).create() + os = module_target_sat.api.OperatingSystem().create() + media = module_target_sat.api.Media( + organization=[module_org], operatingsystem=[os] + ).create() assert os.read().medium[0].read().name == media.name @pytest.mark.tier2 - def test_positive_create_update_url(self, module_org): + def test_positive_create_update_url(self, module_org, module_target_sat): """Create media entity providing the initial url path, then update that url to another valid one. @@ -120,15 +123,15 @@ def test_positive_create_update_url(self, module_org): :CaseImportance: Medium """ url = gen_url(subdomain=gen_string('alpha')) - media = entities.Media(organization=[module_org], path_=url).create() + media = module_target_sat.api.Media(organization=[module_org], path_=url).create() assert media.path_ == url new_url = gen_url(subdomain=gen_string('alpha')) - media = entities.Media(id=media.id, path_=new_url).update(['path_']) + media = module_target_sat.api.Media(id=media.id, path_=new_url).update(['path_']) assert media.path_ == new_url @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) - def test_negative_create_with_invalid_name(self, name): + def test_negative_create_with_invalid_name(self, name, target_sat): """Try to create media entity providing an invalid name :id: 0934f4dc-f674-40fe-a639-035761139c83 @@ -140,10 +143,10 @@ def test_negative_create_with_invalid_name(self, name): :CaseImportance: Medium """ with pytest.raises(HTTPError): - entities.Media(name=name).create() + target_sat.api.Media(name=name).create() @pytest.mark.tier1 - def test_negative_create_with_invalid_url(self): + def test_negative_create_with_invalid_url(self, target_sat): """Try to create media entity providing an invalid URL :id: ae00b6bb-37ed-459e-b9f7-acc92ed0b262 @@ -153,10 +156,10 @@ def test_negative_create_with_invalid_url(self): :CaseImportance: Medium """ with pytest.raises(HTTPError): - entities.Media(path_='NON_EXISTENT_URL').create() + target_sat.api.Media(path_='NON_EXISTENT_URL').create() @pytest.mark.tier1 - def test_negative_create_with_invalid_os_family(self): + def test_negative_create_with_invalid_os_family(self, target_sat): """Try to create media entity providing an invalid OS family :id: 368b7eac-8c52-4071-89c0-1946d7101291 @@ -166,11 +169,11 @@ def test_negative_create_with_invalid_os_family(self): :CaseImportance: Medium """ with pytest.raises(HTTPError): - entities.Media(os_family='NON_EXISTENT_OS').create() + target_sat.api.Media(os_family='NON_EXISTENT_OS').create() @pytest.mark.tier1 @pytest.mark.parametrize('new_name', **parametrized(invalid_values_list())) - def test_negative_update_name(self, module_org, class_media, new_name): + def test_negative_update_name(self, class_media, new_name, target_sat): """Create media entity providing the initial name, then try to update its name to invalid one. @@ -183,10 +186,10 @@ def test_negative_update_name(self, module_org, class_media, new_name): :CaseImportance: Medium """ with pytest.raises(HTTPError): - entities.Media(id=class_media.id, name=new_name).update(['name']) + target_sat.api.Media(id=class_media.id, name=new_name).update(['name']) @pytest.mark.tier1 - def test_negative_update_url(self, module_org, class_media): + def test_negative_update_url(self, class_media, target_sat): """Try to update media with invalid url. :id: 6832f178-4adc-4bb1-957d-0d8d4fd8d9cd @@ -196,10 +199,10 @@ def test_negative_update_url(self, module_org, class_media): :CaseImportance: Medium """ with pytest.raises(HTTPError): - entities.Media(id=class_media.id, path_='NON_EXISTENT_URL').update(['path_']) + target_sat.api.Media(id=class_media.id, path_='NON_EXISTENT_URL').update(['path_']) @pytest.mark.tier1 - def test_negative_update_os_family(self, module_org, class_media): + def test_negative_update_os_family(self, class_media, target_sat): """Try to update media with invalid operation system. :id: f4c5438d-5f98-40b1-9bc7-c0741e81303a @@ -209,4 +212,6 @@ def test_negative_update_os_family(self, module_org, class_media): :CaseImportance: Medium """ with pytest.raises(HTTPError): - entities.Media(id=class_media.id, os_family='NON_EXISTENT_OS').update(['os_family']) + target_sat.api.Media(id=class_media.id, os_family='NON_EXISTENT_OS').update( + ['os_family'] + ) diff --git a/tests/foreman/api/test_multiple_paths.py b/tests/foreman/api/test_multiple_paths.py index 66b4bc7ea9a..4cacae5051e 100644 --- a/tests/foreman/api/test_multiple_paths.py +++ b/tests/foreman/api/test_multiple_paths.py @@ -411,7 +411,7 @@ def test_positive_entity_read(self, entity_cls): assert isinstance(entity_cls(id=entity_id).read(), entity_cls) @pytest.mark.tier1 - def test_positive_architecture_read(self): + def test_positive_architecture_read(self, target_sat): """Create an arch that points to an OS, and read the arch. :id: e4c7babe-11d8-4f85-8382-5267a49046e9 @@ -421,14 +421,14 @@ def test_positive_architecture_read(self): :CaseImportance: Critical """ - os_id = entities.OperatingSystem().create_json()['id'] - arch_id = entities.Architecture(operatingsystem=[os_id]).create_json()['id'] - architecture = entities.Architecture(id=arch_id).read() + os_id = target_sat.api.OperatingSystem().create_json()['id'] + arch_id = target_sat.api.Architecture(operatingsystem=[os_id]).create_json()['id'] + architecture = target_sat.api.Architecture(id=arch_id).read() assert len(architecture.operatingsystem) == 1 assert architecture.operatingsystem[0].id == os_id @pytest.mark.tier1 - def test_positive_syncplan_read(self): + def test_positive_syncplan_read(self, target_sat): """Create a SyncPlan and read it back using ``nailgun.entity_mixins.EntityReadMixin.read``. @@ -439,14 +439,14 @@ def test_positive_syncplan_read(self): :CaseImportance: Critical """ - org_id = entities.Organization().create_json()['id'] - syncplan_id = entities.SyncPlan(organization=org_id).create_json()['id'] + org_id = target_sat.api.Organization().create_json()['id'] + syncplan_id = target_sat.api.SyncPlan(organization=org_id).create_json()['id'] assert isinstance( - entities.SyncPlan(organization=org_id, id=syncplan_id).read(), entities.SyncPlan + target_sat.api.SyncPlan(organization=org_id, id=syncplan_id).read(), entities.SyncPlan ) @pytest.mark.tier1 - def test_positive_osparameter_read(self): + def test_positive_osparameter_read(self, target_sat): """Create an OperatingSystemParameter and get it using ``nailgun.entity_mixins.EntityReadMixin.read``. @@ -457,15 +457,15 @@ def test_positive_osparameter_read(self): :CaseImportance: Critical """ - os_id = entities.OperatingSystem().create_json()['id'] - osp_id = entities.OperatingSystemParameter(operatingsystem=os_id).create_json()['id'] + os_id = target_sat.api.OperatingSystem().create_json()['id'] + osp_id = target_sat.api.OperatingSystemParameter(operatingsystem=os_id).create_json()['id'] assert isinstance( - entities.OperatingSystemParameter(id=osp_id, operatingsystem=os_id).read(), - entities.OperatingSystemParameter, + target_sat.api.OperatingSystemParameter(id=osp_id, operatingsystem=os_id).read(), + target_sat.api.OperatingSystemParameter, ) @pytest.mark.tier1 - def test_positive_permission_read(self): + def test_positive_permission_read(self, target_sat): """Create an Permission entity and get it using ``nailgun.entity_mixins.EntityReadMixin.read``. @@ -476,12 +476,12 @@ class and name and resource_type fields are populated :CaseImportance: Critical """ - perm = entities.Permission().search(query={'per_page': '1'})[0] + perm = target_sat.api.Permission().search(query={'per_page': '1'})[0] assert perm.name assert perm.resource_type @pytest.mark.tier1 - def test_positive_media_read(self): + def test_positive_media_read(self, target_sat): """Create a media pointing at an OS and read the media. :id: 67b656fe-9302-457a-b544-3addb11c85e0 @@ -490,8 +490,8 @@ def test_positive_media_read(self): :CaseImportance: Critical """ - os_id = entities.OperatingSystem().create_json()['id'] - media_id = entities.Media(operatingsystem=[os_id]).create_json()['id'] - media = entities.Media(id=media_id).read() + os_id = target_sat.api.OperatingSystem().create_json()['id'] + media_id = target_sat.api.Media(operatingsystem=[os_id]).create_json()['id'] + media = target_sat.api.Media(id=media_id).read() assert len(media.operatingsystem) == 1 assert media.operatingsystem[0].id == os_id diff --git a/tests/foreman/api/test_organization.py b/tests/foreman/api/test_organization.py index 692504faf79..868ec7368b1 100644 --- a/tests/foreman/api/test_organization.py +++ b/tests/foreman/api/test_organization.py @@ -24,7 +24,7 @@ from random import randint from fauxfactory import gen_string -from nailgun import client, entities +from nailgun import client import pytest from requests.exceptions import HTTPError @@ -44,7 +44,7 @@ def valid_org_data_list(): Note: The maximum allowed length of org name is 242 only. This is an intended behavior (Also note that 255 is the standard across other - entities.) + entities) """ return dict( alpha=gen_string('alpha', randint(1, 242)), @@ -61,7 +61,7 @@ class TestOrganization: """Tests for the ``organizations`` path.""" @pytest.mark.tier1 - def test_positive_create(self): + def test_positive_create(self, target_sat): """Create an organization using a 'text/plain' content-type. :id: 6f67a3f0-0c1d-498c-9a35-28207b0faec2 @@ -70,7 +70,7 @@ def test_positive_create(self): :CaseImportance: Critical """ - organization = entities.Organization() + organization = target_sat.api.Organization() organization.create_missing() response = client.post( organization.path(), @@ -87,7 +87,7 @@ def test_positive_create(self): @pytest.mark.tier1 @pytest.mark.build_sanity @pytest.mark.parametrize('name', **parametrized(valid_org_data_list())) - def test_positive_create_with_name_and_description(self, name): + def test_positive_create_with_name_and_description(self, name, target_sat): """Create an organization and provide a name and description. :id: afeea84b-61ca-40bf-bb16-476432919115 @@ -99,7 +99,7 @@ def test_positive_create_with_name_and_description(self, name): :parametrized: yes """ - org = entities.Organization(name=name, description=name).create() + org = target_sat.api.Organization(name=name, description=name).create() assert org.name == name assert org.description == name @@ -110,7 +110,7 @@ def test_positive_create_with_name_and_description(self, name): @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) - def test_negative_create_with_invalid_name(self, name): + def test_negative_create_with_invalid_name(self, name, target_sat): """Create an org with an incorrect name. :id: 9c6a4b45-a98a-4d76-9865-92d992fa1a22 @@ -120,10 +120,10 @@ def test_negative_create_with_invalid_name(self, name): :parametrized: yes """ with pytest.raises(HTTPError): - entities.Organization(name=name).create() + target_sat.api.Organization(name=name).create() @pytest.mark.tier1 - def test_negative_create_with_same_name(self): + def test_negative_create_with_same_name(self, target_sat): """Create two organizations with identical names. :id: a0f5333c-cc83-403c-9bf7-08fb372909dc @@ -132,9 +132,9 @@ def test_negative_create_with_same_name(self): :CaseImportance: Critical """ - name = entities.Organization().create().name + name = target_sat.api.Organization().create().name with pytest.raises(HTTPError): - entities.Organization(name=name).create() + target_sat.api.Organization(name=name).create() @pytest.mark.tier1 def test_negative_check_org_endpoint(self, module_entitlement_manifest_org): @@ -155,7 +155,7 @@ def test_negative_check_org_endpoint(self, module_entitlement_manifest_org): assert 'BEGIN RSA PRIVATE KEY' not in orgstring @pytest.mark.tier1 - def test_positive_search(self): + def test_positive_search(self, target_sat): """Create an organization, then search for it by name. :id: f6f1d839-21f2-4676-8683-9f899cbdec4c @@ -164,14 +164,14 @@ def test_positive_search(self): :CaseImportance: High """ - org = entities.Organization().create() - orgs = entities.Organization().search(query={'search': f'name="{org.name}"'}) + org = target_sat.api.Organization().create() + orgs = target_sat.api.Organization().search(query={'search': f'name="{org.name}"'}) assert len(orgs) == 1 assert orgs[0].id == org.id assert orgs[0].name == org.name @pytest.mark.tier1 - def test_negative_create_with_wrong_path(self): + def test_negative_create_with_wrong_path(self, target_sat): """Attempt to create an organization using foreman API path (``api/v2/organizations``) @@ -184,7 +184,7 @@ def test_negative_create_with_wrong_path(self): :CaseImportance: Critical """ - org = entities.Organization() + org = target_sat.api.Organization() org._meta['api_path'] = 'api/v2/organizations' with pytest.raises(HTTPError) as err: org.create() @@ -192,7 +192,7 @@ def test_negative_create_with_wrong_path(self): assert 'Route overriden by Katello' in err.value.response.text @pytest.mark.tier2 - def test_default_org_id_check(self): + def test_default_org_id_check(self, target_sat): """test to check the default_organization id :id: df066396-a069-4e9e-b3c1-c6d34a755ec0 @@ -204,7 +204,7 @@ def test_default_org_id_check(self): :CaseImportance: Low """ default_org_id = ( - entities.Organization().search(query={'search': f'name="{DEFAULT_ORG}"'})[0].id + target_sat.api.Organization().search(query={'search': f'name="{DEFAULT_ORG}"'})[0].id ) assert default_org_id == 1 @@ -213,9 +213,9 @@ class TestOrganizationUpdate: """Tests for the ``organizations`` path.""" @pytest.fixture - def module_org(self): + def module_org(self, target_sat): """Create an organization.""" - return entities.Organization().create() + return target_sat.api.Organization().create() @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(valid_org_data_list())) @@ -252,7 +252,7 @@ def test_positive_update_description(self, module_org, desc): assert module_org.description == desc @pytest.mark.tier2 - def test_positive_update_user(self, module_org): + def test_positive_update_user(self, module_org, target_sat): """Update an organization, associate user with it. :id: 2c0c0061-5b4e-4007-9f54-b61d6e65ef58 @@ -261,14 +261,14 @@ def test_positive_update_user(self, module_org): :CaseLevel: Integration """ - user = entities.User().create() + user = target_sat.api.User().create() module_org.user = [user] module_org = module_org.update(['user']) assert len(module_org.user) == 1 assert module_org.user[0].id == user.id @pytest.mark.tier2 - def test_positive_update_subnet(self, module_org): + def test_positive_update_subnet(self, module_org, target_sat): """Update an organization, associate subnet with it. :id: 3aa0b9cb-37f7-4e7e-a6ec-c1b407225e54 @@ -277,14 +277,14 @@ def test_positive_update_subnet(self, module_org): :CaseLevel: Integration """ - subnet = entities.Subnet().create() + subnet = target_sat.api.Subnet().create() module_org.subnet = [subnet] module_org = module_org.update(['subnet']) assert len(module_org.subnet) == 1 assert module_org.subnet[0].id == subnet.id @pytest.mark.tier2 - def test_positive_add_and_remove_hostgroup(self): + def test_positive_add_and_remove_hostgroup(self, target_sat): """Add a hostgroup to an organization and then remove it :id: 7eb1aca7-fd7b-404f-ab18-21be5052a11f @@ -297,8 +297,8 @@ def test_positive_add_and_remove_hostgroup(self): :CaseImportance: Medium """ - org = entities.Organization().create() - hostgroup = entities.HostGroup().create() + org = target_sat.api.Organization().create() + hostgroup = target_sat.api.HostGroup().create() org.hostgroup = [hostgroup] org = org.update(['hostgroup']) assert len(org.hostgroup) == 1 @@ -348,7 +348,7 @@ def test_positive_add_and_remove_smart_proxy(self, target_sat): @pytest.mark.tier1 @pytest.mark.parametrize('update_field', ['name', 'label']) - def test_negative_update(self, module_org, update_field): + def test_negative_update(self, module_org, update_field, target_sat): """Update an organization's attributes with invalid values. :id: b7152d0b-5ab0-4d68-bfdf-f3eabcb5fbc6 @@ -367,4 +367,4 @@ def test_negative_update(self, module_org, update_field): update_field: gen_string(str_type='utf8', length=256 if update_field == 'name' else 10) } with pytest.raises(HTTPError): - entities.Organization(id=module_org.id, **update_dict).update([update_field]) + target_sat.api.Organization(id=module_org.id, **update_dict).update([update_field]) diff --git a/tests/foreman/api/test_oscap_tailoringfiles.py b/tests/foreman/api/test_oscap_tailoringfiles.py index d29eed1ca95..fb6b78750f0 100644 --- a/tests/foreman/api/test_oscap_tailoringfiles.py +++ b/tests/foreman/api/test_oscap_tailoringfiles.py @@ -16,7 +16,6 @@ :Upstream: No """ -from nailgun import entities import pytest from robottelo.utils.datafactory import gen_string @@ -27,7 +26,9 @@ class TestTailoringFile: @pytest.mark.tier1 @pytest.mark.e2e - def test_positive_crud_tailoringfile(self, default_org, default_location, tailoring_file_path): + def test_positive_crud_tailoringfile( + self, default_org, default_location, tailoring_file_path, target_sat + ): """Perform end to end testing for oscap tailoring files component :id: 2441988f-2054-49f7-885e-3675336f712f @@ -39,23 +40,23 @@ def test_positive_crud_tailoringfile(self, default_org, default_location, tailor name = gen_string('alpha') new_name = gen_string('alpha') original_filename = gen_string('alpha') - scap = entities.TailoringFile( + scap = target_sat.api.TailoringFile( name=name, scap_file=tailoring_file_path['local'], organization=[default_org], location=[default_location], ).create() - assert entities.TailoringFile().search(query={'search': f'name={name}'}) - result = entities.TailoringFile(id=scap.id).read() + assert target_sat.api.TailoringFile().search(query={'search': f'name={name}'}) + result = target_sat.api.TailoringFile(id=scap.id).read() assert result.name == name assert result.location[0].id == default_location.id assert result.organization[0].id == default_org.id - scap = entities.TailoringFile( + scap = target_sat.api.TailoringFile( id=scap.id, name=new_name, original_filename=f'{original_filename}' ).update() - result = entities.TailoringFile(id=scap.id).read() + result = target_sat.api.TailoringFile(id=scap.id).read() assert result.name == new_name assert result.original_filename == original_filename - assert entities.TailoringFile().search(query={'search': f'name={new_name}'}) - entities.TailoringFile(id=scap.id).delete() - assert not entities.TailoringFile().search(query={'search': f'name={new_name}'}) + assert target_sat.api.TailoringFile().search(query={'search': f'name={new_name}'}) + target_sat.api.TailoringFile(id=scap.id).delete() + assert not target_sat.api.TailoringFile().search(query={'search': f'name={new_name}'}) diff --git a/tests/foreman/api/test_oscappolicy.py b/tests/foreman/api/test_oscappolicy.py index 1efdd3e3779..c9500c465d1 100644 --- a/tests/foreman/api/test_oscappolicy.py +++ b/tests/foreman/api/test_oscappolicy.py @@ -17,7 +17,6 @@ :Upstream: No """ from fauxfactory import gen_string -from nailgun import entities import pytest @@ -27,7 +26,7 @@ class TestOscapPolicy: @pytest.mark.tier1 @pytest.mark.e2e def test_positive_crud_scap_policy( - self, default_org, default_location, scap_content, tailoring_file + self, default_org, default_location, scap_content, tailoring_file, target_sat ): """Perform end to end testing for oscap policy component @@ -42,11 +41,11 @@ def test_positive_crud_scap_policy( name = gen_string('alpha') new_name = gen_string('alpha') description = gen_string('alpha') - hostgroup = entities.HostGroup( + hostgroup = target_sat.api.HostGroup( location=[default_location], organization=[default_org] ).create() # Create new oscap policy with assigned content and tailoring file - policy = entities.CompliancePolicies( + policy = target_sat.api.CompliancePolicies( name=name, deploy_by='ansible', description=description, @@ -60,7 +59,7 @@ def test_positive_crud_scap_policy( location=[default_location], organization=[default_org], ).create() - assert entities.CompliancePolicies().search(query={'search': f'name="{name}"'}) + assert target_sat.api.CompliancePolicies().search(query={'search': f'name="{name}"'}) # Check that created entity has expected values assert policy.deploy_by == 'ansible' assert policy.name == name @@ -74,9 +73,11 @@ def test_positive_crud_scap_policy( assert str(policy.organization[0].id) == str(default_org.id) assert str(policy.location[0].id) == str(default_location.id) # Update oscap policy with new name - policy = entities.CompliancePolicies(id=policy.id, name=new_name).update() + policy = target_sat.api.CompliancePolicies(id=policy.id, name=new_name).update() assert policy.name == new_name - assert not entities.CompliancePolicies().search(query={'search': f'name="{name}"'}) + assert not target_sat.api.CompliancePolicies().search(query={'search': f'name="{name}"'}) # Delete oscap policy entity - entities.CompliancePolicies(id=policy.id).delete() - assert not entities.CompliancePolicies().search(query={'search': f'name="{new_name}"'}) + target_sat.api.CompliancePolicies(id=policy.id).delete() + assert not target_sat.api.CompliancePolicies().search( + query={'search': f'name="{new_name}"'} + ) diff --git a/tests/foreman/api/test_permission.py b/tests/foreman/api/test_permission.py index f8eb16a1500..094a7875b66 100644 --- a/tests/foreman/api/test_permission.py +++ b/tests/foreman/api/test_permission.py @@ -72,7 +72,7 @@ def create_permissions(self, class_target_sat): cls.permission_names = list(chain.from_iterable(cls.permissions.values())) @pytest.mark.tier1 - def test_positive_search_by_name(self): + def test_positive_search_by_name(self, target_sat): """Search for a permission by name. :id: 1b6117f6-599d-4b2d-80a8-1e0764bdc04d @@ -84,7 +84,9 @@ def test_positive_search_by_name(self): """ failures = {} for permission_name in self.permission_names: - results = entities.Permission().search(query={'search': f'name="{permission_name}"'}) + results = target_sat.api.Permission().search( + query={'search': f'name="{permission_name}"'} + ) if len(results) != 1 or len(results) == 1 and results[0].name != permission_name: failures[permission_name] = { 'length': len(results), @@ -95,7 +97,7 @@ def test_positive_search_by_name(self): pytest.fail(json.dumps(failures, indent=True, sort_keys=True)) @pytest.mark.tier1 - def test_positive_search_by_resource_type(self): + def test_positive_search_by_resource_type(self, target_sat): """Search for permissions by resource type. :id: 29d9362b-1bf3-4722-b40f-a5e8b4d0d9ba @@ -109,7 +111,7 @@ def test_positive_search_by_resource_type(self): for resource_type in self.permission_resource_types: if resource_type is None: continue - perm_group = entities.Permission().search( + perm_group = target_sat.api.Permission().search( query={'search': f'resource_type="{resource_type}"'} ) permissions = {perm.name for perm in perm_group} @@ -128,7 +130,7 @@ def test_positive_search_by_resource_type(self): pytest.fail(json.dumps(failures, indent=True, sort_keys=True)) @pytest.mark.tier1 - def test_positive_search(self): + def test_positive_search(self, target_sat): """search with no parameters return all permissions :id: e58308df-19ec-415d-8fa1-63ebf3cd0ad6 @@ -137,7 +139,7 @@ def test_positive_search(self): :CaseImportance: Critical """ - permissions = entities.Permission().search(query={'per_page': '1000'}) + permissions = target_sat.api.Permission().search(query={'per_page': '1000'}) names = {perm.name for perm in permissions} resource_types = {perm.resource_type for perm in permissions} expected_names = set(self.permission_names) @@ -206,7 +208,7 @@ def create_user(self, target_sat, class_org, class_location): location=[class_location], ).create() - def give_user_permission(self, perm_name): + def give_user_permission(self, perm_name, target_sat): """Give ``self.user`` the ``perm_name`` permission. This method creates a role and filter to accomplish the above goal. @@ -222,10 +224,10 @@ def give_user_permission(self, perm_name): updating ``self.user``'s roles. :returns: Nothing. """ - role = entities.Role().create() - permissions = entities.Permission().search(query={'search': f'name="{perm_name}"'}) + role = target_sat.api.Role().create() + permissions = target_sat.api.Permission().search(query={'search': f'name="{perm_name}"'}) assert len(permissions) == 1 - entities.Filter(permission=permissions, role=role).create() + target_sat.api.Filter(permission=permissions, role=role).create() self.user.role += [role] self.user = self.user.update(['role']) @@ -347,7 +349,7 @@ def test_positive_check_delete(self, entity_cls, class_org, class_location): 'entity_cls', **parametrized([entities.Architecture, entities.Domain, entities.ActivationKey]), ) - def test_positive_check_update(self, entity_cls, class_org, class_location): + def test_positive_check_update(self, entity_cls, class_org, class_location, target_sat): """Check whether the "edit_*" role has an effect. :id: b5de2115-b031-413e-8e5b-eac8cb714174 diff --git a/tests/foreman/api/test_product.py b/tests/foreman/api/test_product.py index 84b6d78f866..dfa9f59f705 100644 --- a/tests/foreman/api/test_product.py +++ b/tests/foreman/api/test_product.py @@ -20,7 +20,6 @@ :Upstream: No """ from fauxfactory import gen_string -from nailgun import entities import pytest from requests.exceptions import HTTPError @@ -40,7 +39,7 @@ @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(valid_data_list())) -def test_positive_create_with_name(request, name, module_org): +def test_positive_create_with_name(request, name, module_org, module_target_sat): """Create a product providing different valid names :id: 3d873b73-6919-4fda-84df-0e26bdf0c1dc @@ -51,12 +50,12 @@ def test_positive_create_with_name(request, name, module_org): :CaseImportance: Critical """ - product = entities.Product(name=name, organization=module_org).create() + product = module_target_sat.api.Product(name=name, organization=module_org).create() assert product.name == name @pytest.mark.tier1 -def test_positive_create_with_label(module_org): +def test_positive_create_with_label(module_org, module_target_sat): """Create a product providing label which is different from its name :id: 95cf8e05-fd09-422e-bf6f-8b1dde762976 @@ -66,14 +65,14 @@ def test_positive_create_with_label(module_org): :CaseImportance: Critical """ label = gen_string('alphanumeric') - product = entities.Product(label=label, organization=module_org).create() + product = module_target_sat.api.Product(label=label, organization=module_org).create() assert product.label == label assert product.name != label @pytest.mark.tier1 @pytest.mark.parametrize('description', **parametrized(valid_data_list())) -def test_positive_create_with_description(description, module_org): +def test_positive_create_with_description(description, module_org, module_target_sat): """Create a product providing different descriptions :id: f3e2df77-6711-440b-800a-9cebbbec36c5 @@ -84,12 +83,14 @@ def test_positive_create_with_description(description, module_org): :CaseImportance: Critical """ - product = entities.Product(description=description, organization=module_org).create() + product = module_target_sat.api.Product( + description=description, organization=module_org + ).create() assert product.description == description @pytest.mark.tier2 -def test_positive_create_with_gpg(module_org): +def test_positive_create_with_gpg(module_org, module_target_sat): """Create a product and provide a GPG key. :id: 57331c1f-15dd-4c9f-b8fc-3010847b2975 @@ -98,17 +99,17 @@ def test_positive_create_with_gpg(module_org): :CaseLevel: Integration """ - gpg_key = entities.GPGKey( + gpg_key = module_target_sat.api.GPGKey( content=DataFile.VALID_GPG_KEY_FILE.read_text(), organization=module_org, ).create() - product = entities.Product(gpg_key=gpg_key, organization=module_org).create() + product = module_target_sat.api.Product(gpg_key=gpg_key, organization=module_org).create() assert product.gpg_key.id == gpg_key.id @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) -def test_negative_create_with_name(name, module_org): +def test_negative_create_with_name(name, module_org, module_target_sat): """Create a product providing invalid names only :id: 76531f53-09ff-4ee9-89b9-09a697526fb1 @@ -120,11 +121,11 @@ def test_negative_create_with_name(name, module_org): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.Product(name=name, organization=module_org).create() + module_target_sat.api.Product(name=name, organization=module_org).create() @pytest.mark.tier1 -def test_negative_create_with_same_name(module_org): +def test_negative_create_with_same_name(module_org, module_target_sat): """Create a product providing a name of already existent entity :id: 039269c5-607a-4b70-91dd-b8fed8e50cc6 @@ -134,13 +135,13 @@ def test_negative_create_with_same_name(module_org): :CaseImportance: Critical """ name = gen_string('alphanumeric') - entities.Product(name=name, organization=module_org).create() + module_target_sat.api.Product(name=name, organization=module_org).create() with pytest.raises(HTTPError): - entities.Product(name=name, organization=module_org).create() + module_target_sat.api.Product(name=name, organization=module_org).create() @pytest.mark.tier1 -def test_negative_create_with_label(module_org): +def test_negative_create_with_label(module_org, module_target_sat): """Create a product providing invalid label :id: 30b1a737-07f1-4786-b68a-734e57c33a62 @@ -150,12 +151,12 @@ def test_negative_create_with_label(module_org): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.Product(label=gen_string('utf8'), organization=module_org).create() + module_target_sat.api.Product(label=gen_string('utf8'), organization=module_org).create() @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(valid_data_list())) -def test_positive_update_name(name, module_org): +def test_positive_update_name(name, module_org, module_target_sat): """Update product name to another valid name. :id: 1a9f6e0d-43fb-42e2-9dbd-e880f03b0297 @@ -166,7 +167,7 @@ def test_positive_update_name(name, module_org): :CaseImportance: Critical """ - product = entities.Product(organization=module_org).create() + product = module_target_sat.api.Product(organization=module_org).create() product.name = name product = product.update(['name']) assert product.name == name @@ -174,7 +175,7 @@ def test_positive_update_name(name, module_org): @pytest.mark.tier1 @pytest.mark.parametrize('description', **parametrized(valid_data_list())) -def test_positive_update_description(description, module_org): +def test_positive_update_description(description, module_org, module_target_sat): """Update product description to another valid one. :id: c960c326-2e9f-4ee7-bdec-35a705305067 @@ -185,14 +186,14 @@ def test_positive_update_description(description, module_org): :CaseImportance: Critical """ - product = entities.Product(organization=module_org).create() + product = module_target_sat.api.Product(organization=module_org).create() product.description = description product = product.update(['description']) assert product.description == description @pytest.mark.tier1 -def test_positive_update_name_to_original(module_org): +def test_positive_update_name_to_original(module_org, module_target_sat): """Rename Product back to original name :id: 3075f17f-4475-4b64-9fbd-1e41ced9142d @@ -201,7 +202,7 @@ def test_positive_update_name_to_original(module_org): :CaseImportance: Critical """ - product = entities.Product(organization=module_org).create() + product = module_target_sat.api.Product(organization=module_org).create() old_name = product.name # Update product name @@ -218,7 +219,7 @@ def test_positive_update_name_to_original(module_org): @pytest.mark.upgrade @pytest.mark.tier2 -def test_positive_update_gpg(module_org): +def test_positive_update_gpg(module_org, module_target_sat): """Create a product and update its GPGKey :id: 3b08f155-a0d6-4987-b281-dc02e8d5a03e @@ -228,14 +229,14 @@ def test_positive_update_gpg(module_org): :CaseLevel: Integration """ # Create a product and make it point to a GPG key. - gpg_key_1 = entities.GPGKey( + gpg_key_1 = module_target_sat.api.GPGKey( content=DataFile.VALID_GPG_KEY_FILE.read_text(), organization=module_org, ).create() - product = entities.Product(gpg_key=gpg_key_1, organization=module_org).create() + product = module_target_sat.api.Product(gpg_key=gpg_key_1, organization=module_org).create() # Update the product and make it point to a new GPG key. - gpg_key_2 = entities.GPGKey( + gpg_key_2 = module_target_sat.api.GPGKey( content=DataFile.VALID_GPG_KEY_BETA_FILE.read_text(), organization=module_org, ).create() @@ -246,7 +247,7 @@ def test_positive_update_gpg(module_org): @pytest.mark.skip_if_open("BZ:1310422") @pytest.mark.tier2 -def test_positive_update_organization(module_org): +def test_positive_update_organization(module_org, module_target_sat): """Create a product and update its organization :id: b298957a-2cdb-4f17-a934-098612f3b659 @@ -257,9 +258,9 @@ def test_positive_update_organization(module_org): :BZ: 1310422 """ - product = entities.Product(organization=module_org).create() + product = module_target_sat.api.Product(organization=module_org).create() # Update the product and make it point to a new organization. - new_org = entities.Organization().create() + new_org = module_target_sat.api.Organization().create() product.organization = new_org product = product.update() assert product.organization.id == new_org.id @@ -267,7 +268,7 @@ def test_positive_update_organization(module_org): @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) -def test_negative_update_name(name, module_org): +def test_negative_update_name(name, module_org, module_target_sat): """Attempt to update product name to invalid one :id: 3eb61fa8-3524-4872-8f1b-4e88004f66f5 @@ -278,13 +279,13 @@ def test_negative_update_name(name, module_org): :CaseImportance: Critical """ - product = entities.Product(organization=module_org).create() + product = module_target_sat.api.Product(organization=module_org).create() with pytest.raises(HTTPError): - entities.Product(id=product.id, name=name).update(['name']) + module_target_sat.api.Product(id=product.id, name=name).update(['name']) @pytest.mark.tier1 -def test_negative_update_label(module_org): +def test_negative_update_label(module_org, module_target_sat): """Attempt to update product label to another one. :id: 065cd673-8d10-46c7-800c-b731b06a5359 @@ -293,7 +294,7 @@ def test_negative_update_label(module_org): :CaseImportance: Critical """ - product = entities.Product(organization=module_org).create() + product = module_target_sat.api.Product(organization=module_org).create() product.label = gen_string('alpha') with pytest.raises(HTTPError): product.update(['label']) @@ -301,7 +302,7 @@ def test_negative_update_label(module_org): @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(valid_data_list())) -def test_positive_delete(name, module_org): +def test_positive_delete(name, module_org, module_target_sat): """Create product and then delete it. :id: 30df95f5-0a4e-41ee-a99f-b418c5c5f2f3 @@ -312,7 +313,7 @@ def test_positive_delete(name, module_org): :CaseImportance: Critical """ - product = entities.Product(name=name, organization=module_org).create() + product = module_target_sat.api.Product(name=name, organization=module_org).create() product.delete() with pytest.raises(HTTPError): product.read() @@ -320,7 +321,7 @@ def test_positive_delete(name, module_org): @pytest.mark.tier1 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_sync(module_org): +def test_positive_sync(module_org, module_target_sat): """Sync product (repository within a product) :id: 860e00a1-c370-4bd0-8987-449338071d56 @@ -329,8 +330,8 @@ def test_positive_sync(module_org): :CaseImportance: Critical """ - product = entities.Product(organization=module_org).create() - repo = entities.Repository( + product = module_target_sat.api.Product(organization=module_org).create() + repo = module_target_sat.api.Repository( product=product, content_type='yum', url=settings.repos.yum_1.url ).create() assert repo.read().content_counts['rpm'] == 0 @@ -341,7 +342,7 @@ def test_positive_sync(module_org): @pytest.mark.tier2 @pytest.mark.upgrade @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_sync_several_repos(module_org): +def test_positive_sync_several_repos(module_org, module_target_sat): """Sync product (all repositories within a product) :id: 07918442-b72f-4db5-96b6-975564f3663a @@ -353,11 +354,11 @@ def test_positive_sync_several_repos(module_org): :BZ: 1389543 """ - product = entities.Product(organization=module_org).create() - rpm_repo = entities.Repository( + product = module_target_sat.api.Product(organization=module_org).create() + rpm_repo = module_target_sat.api.Repository( product=product, content_type='yum', url=settings.repos.yum_1.url ).create() - docker_repo = entities.Repository( + docker_repo = module_target_sat.api.Repository( content_type=REPO_TYPE['docker'], docker_upstream_name=CONTAINER_UPSTREAM_NAME, product=product, @@ -372,7 +373,7 @@ def test_positive_sync_several_repos(module_org): @pytest.mark.tier2 -def test_positive_filter_product_list(module_entitlement_manifest_org): +def test_positive_filter_product_list(module_entitlement_manifest_org, module_target_sat): """Filter products based on param 'custom/redhat_only' :id: e61fb63a-4552-4915-b13d-23ab80138249 @@ -384,9 +385,9 @@ def test_positive_filter_product_list(module_entitlement_manifest_org): :BZ: 1667129 """ org = module_entitlement_manifest_org - product = entities.Product(organization=org).create() - custom_products = entities.Product(organization=org).search(query={'custom': True}) - rh_products = entities.Product(organization=org).search( + product = module_target_sat.api.Product(organization=org).create() + custom_products = module_target_sat.api.Product(organization=org).search(query={'custom': True}) + rh_products = module_target_sat.api.Product(organization=org).search( query={'redhat_only': True, 'per_page': 1000} ) diff --git a/tests/foreman/api/test_reporttemplates.py b/tests/foreman/api/test_reporttemplates.py index 5047a9a616c..7d6bf4d2e1d 100644 --- a/tests/foreman/api/test_reporttemplates.py +++ b/tests/foreman/api/test_reporttemplates.py @@ -18,7 +18,6 @@ """ from broker import Broker from fauxfactory import gen_string -from nailgun import entities import pytest from requests import HTTPError from wait_for import wait_for @@ -49,24 +48,24 @@ def setup_content(module_entitlement_manifest_org, module_target_sat): reposet=REPOSET['rhst7'], releasever=None, ) - rh_repo = entities.Repository(id=rh_repo_id).read() + rh_repo = module_target_sat.api.Repository(id=rh_repo_id).read() rh_repo.sync() - custom_repo = entities.Repository( - product=entities.Product(organization=org).create(), + custom_repo = module_target_sat.api.Repository( + product=module_target_sat.api.Product(organization=org).create(), ).create() custom_repo.sync() - lce = entities.LifecycleEnvironment(organization=org).create() - cv = entities.ContentView( + lce = module_target_sat.api.LifecycleEnvironment(organization=org).create() + cv = module_target_sat.api.ContentView( organization=org, repository=[rh_repo_id, custom_repo.id], ).create() cv.publish() cvv = cv.read().version[0].read() cvv.promote(data={'environment_ids': lce.id, 'force': False}) - ak = entities.ActivationKey( + ak = module_target_sat.api.ActivationKey( content_view=cv, max_hosts=100, organization=org, environment=lce, auto_attach=True ).create() - subscription = entities.Subscription(organization=org).search( + subscription = module_target_sat.api.Subscription(organization=org).search( query={'search': f'name="{DEFAULT_SUBSCRIPTION_NAME}"'} )[0] ak.add_subscriptions(data={'quantity': 1, 'subscription_id': subscription.id}) @@ -78,7 +77,7 @@ def setup_content(module_entitlement_manifest_org, module_target_sat): @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(valid_data_list())) -def test_positive_CRUDL(name): +def test_positive_CRUDL(name, target_sat): """Create, Read, Update, Delete, List :id: a2a577db-144e-4761-a42e-e83885464786 @@ -101,27 +100,27 @@ def test_positive_CRUDL(name): """ # Create template1 = gen_string('alpha') - rt = entities.ReportTemplate(name=name, template=template1).create() + rt = target_sat.api.ReportTemplate(name=name, template=template1).create() # List - res = entities.ReportTemplate().search(query={'search': f'name="{name}"'}) + res = target_sat.api.ReportTemplate().search(query={'search': f'name="{name}"'}) assert name in list(map(lambda x: x.name, res)) # Read - rt = entities.ReportTemplate(id=rt.id).read() + rt = target_sat.api.ReportTemplate(id=rt.id).read() assert name == rt.name assert template1 == rt.template # Update template2 = gen_string('alpha') - entities.ReportTemplate(id=rt.id, template=template2).update(['template']) - rt = entities.ReportTemplate(id=rt.id).read() + target_sat.api.ReportTemplate(id=rt.id, template=template2).update(['template']) + rt = target_sat.api.ReportTemplate(id=rt.id).read() assert template2 == rt.template # Delete - entities.ReportTemplate(id=rt.id).delete() + target_sat.api.ReportTemplate(id=rt.id).delete() with pytest.raises(HTTPError): - rt = entities.ReportTemplate(id=rt.id).read() + rt = target_sat.api.ReportTemplate(id=rt.id).read() @pytest.mark.tier1 -def test_positive_generate_report_nofilter(): +def test_positive_generate_report_nofilter(target_sat): """Generate Host - Statuses report :id: a4b687db-144e-4761-a42e-e93887464986 @@ -137,8 +136,10 @@ def test_positive_generate_report_nofilter(): :CaseImportance: Critical """ host_name = gen_string('alpha').lower() - entities.Host(name=host_name).create() - rt = entities.ReportTemplate().search(query={'search': 'name="Host - Statuses"'})[0].read() + target_sat.api.Host(name=host_name).create() + rt = ( + target_sat.api.ReportTemplate().search(query={'search': 'name="Host - Statuses"'})[0].read() + ) res = rt.generate() for column_name in [ 'Name', @@ -164,7 +165,7 @@ def test_positive_generate_report_nofilter(): @pytest.mark.tier2 -def test_positive_generate_report_filter(): +def test_positive_generate_report_filter(target_sat): """Generate Host - Statuses report :id: a4b677cb-144e-4761-a42e-e93887464986 @@ -181,9 +182,11 @@ def test_positive_generate_report_filter(): """ host1_name = gen_string('alpha').lower() host2_name = gen_string('alpha').lower() - entities.Host(name=host1_name).create() - entities.Host(name=host2_name).create() - rt = entities.ReportTemplate().search(query={'search': 'name="Host - Statuses"'})[0].read() + target_sat.api.Host(name=host1_name).create() + target_sat.api.Host(name=host2_name).create() + rt = ( + target_sat.api.ReportTemplate().search(query={'search': 'name="Host - Statuses"'})[0].read() + ) res = rt.generate(data={"input_values": {"hosts": host2_name}}) for column_name in [ 'Name', @@ -210,7 +213,7 @@ def test_positive_generate_report_filter(): @pytest.mark.tier2 -def test_positive_report_add_userinput(): +def test_positive_report_add_userinput(target_sat): """Add user input to template, use it in template, generate template :id: a4a577db-144e-4761-a42e-e86887464986 @@ -230,21 +233,21 @@ def test_positive_report_add_userinput(): input_value = gen_string('alpha').lower() template_name = gen_string('alpha').lower() template = f'<%= "value=\\"" %><%= input(\'{input_name}\') %><%= "\\"" %>' - entities.Host(name=host_name).create() - rt = entities.ReportTemplate(name=template_name, template=template).create() - entities.TemplateInput( + target_sat.api.Host(name=host_name).create() + rt = target_sat.api.ReportTemplate(name=template_name, template=template).create() + target_sat.api.TemplateInput( name=input_name, input_type="user", template=rt.id, ).create() - ti = entities.TemplateInput(template=rt.id).search()[0].read() + ti = target_sat.api.TemplateInput(template=rt.id).search()[0].read() assert input_name == ti.name res = rt.generate(data={"input_values": {input_name: input_value}}) assert f'value="{input_value}"' in res @pytest.mark.tier2 -def test_positive_lock_clone_nodelete_unlock_report(): +def test_positive_lock_clone_nodelete_unlock_report(target_sat): """Lock report template. Check it can be cloned and can't be deleted or edited. Unlock. Check it can be deleted and edited. @@ -274,15 +277,15 @@ def test_positive_lock_clone_nodelete_unlock_report(): template_clone_name = gen_string('alpha').lower() template1 = gen_string('alpha') template2 = gen_string('alpha') - rt = entities.ReportTemplate(name=template_name, template=template1).create() + rt = target_sat.api.ReportTemplate(name=template_name, template=template1).create() # 2. Lock template - entities.ReportTemplate(id=rt.id, locked=True).update(["locked"]) + target_sat.api.ReportTemplate(id=rt.id, locked=True).update(["locked"]) rt = rt.read() assert rt.locked is True # 3. Clone template, check cloned data rt.clone(data={'name': template_clone_name}) cloned_rt = ( - entities.ReportTemplate() + target_sat.api.ReportTemplate() .search(query={'search': f'name="{template_clone_name}"'})[0] .read() ) @@ -294,24 +297,28 @@ def test_positive_lock_clone_nodelete_unlock_report(): rt.delete() # In BZ1680458, exception is thrown but template is deleted anyway assert ( - len(entities.ReportTemplate().search(query={'search': f'name="{template_name}"'})) != 0 + len(target_sat.api.ReportTemplate().search(query={'search': f'name="{template_name}"'})) + != 0 ) # 5. Try to edit template with pytest.raises(HTTPError): - entities.ReportTemplate(id=rt.id, template=template2).update(["template"]) + target_sat.api.ReportTemplate(id=rt.id, template=template2).update(["template"]) rt = rt.read() assert template1 == rt.template # 6. Unlock template - entities.ReportTemplate(id=rt.id, locked=False).update(["locked"]) + target_sat.api.ReportTemplate(id=rt.id, locked=False).update(["locked"]) rt = rt.read() assert rt.locked is False # 7. Edit template - entities.ReportTemplate(id=rt.id, template=template2).update(["template"]) + target_sat.api.ReportTemplate(id=rt.id, template=template2).update(["template"]) rt = rt.read() assert template2 == rt.template # 8. Delete template rt.delete() - assert len(entities.ReportTemplate().search(query={'search': f'name="{template_name}"'})) == 0 + assert ( + len(target_sat.api.ReportTemplate().search(query={'search': f'name="{template_name}"'})) + == 0 + ) @pytest.mark.tier2 @@ -593,7 +600,7 @@ def test_positive_generate_entitlements_report(setup_content, target_sat): vm.register_contenthost(org.label, ak.name) assert vm.subscribed rt = ( - entities.ReportTemplate() + target_sat.api.ReportTemplate() .search(query={'search': 'name="Subscription - Entitlement Report"'})[0] .read() ) @@ -632,7 +639,7 @@ def test_positive_schedule_entitlements_report(setup_content, target_sat): vm.register_contenthost(org.label, ak.name) assert vm.subscribed rt = ( - entities.ReportTemplate() + target_sat.api.ReportTemplate() .search(query={'search': 'name="Subscription - Entitlement Report"'})[0] .read() ) diff --git a/tests/foreman/api/test_repositories.py b/tests/foreman/api/test_repositories.py index 590ae50ae98..c250d25bbbe 100644 --- a/tests/foreman/api/test_repositories.py +++ b/tests/foreman/api/test_repositories.py @@ -17,7 +17,6 @@ :Upstream: No """ from manifester import Manifester -from nailgun import entities from nailgun.entity_mixins import call_entity_method_with_timeout import pytest from requests.exceptions import HTTPError @@ -179,7 +178,7 @@ def test_positive_sync_kickstart_repo(module_entitlement_manifest_org, target_sa repo=constants.REPOS['kickstart'][distro]['name'], releasever=constants.REPOS['kickstart'][distro]['version'], ) - rh_repo = entities.Repository(id=rh_repo_id).read() + rh_repo = target_sat.api.Repository(id=rh_repo_id).read() rh_repo.sync() rh_repo.download_policy = 'immediate' rh_repo = rh_repo.update(['download_policy']) diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index bfe85d460a4..0df5602bd5c 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -23,7 +23,7 @@ from urllib.parse import urljoin, urlparse, urlunparse from fauxfactory import gen_string -from nailgun import client, entities +from nailgun import client from nailgun.entity_mixins import TaskFailedError, call_entity_method_with_timeout import pytest from requests.exceptions import HTTPError @@ -47,24 +47,24 @@ def repo_options(request, module_org, module_product): @pytest.fixture -def repo_options_custom_product(request, module_org): +def repo_options_custom_product(request, module_org, module_target_sat): """Return the options that were passed as indirect parameters.""" options = getattr(request, 'param', {}).copy() options['organization'] = module_org - options['product'] = entities.Product(organization=module_org).create() + options['product'] = module_target_sat.api.Product(organization=module_org).create() return options @pytest.fixture -def env(module_org): +def env(module_org, module_target_sat): """Create a new puppet environment.""" - return entities.Environment(organization=[module_org]).create() + return module_target_sat.api.Environment(organization=[module_org]).create() @pytest.fixture -def repo(repo_options): +def repo(repo_options, module_target_sat): """Create a new repository.""" - return entities.Repository(**repo_options).create() + return module_target_sat.api.Repository(**repo_options).create() class TestRepository: @@ -197,7 +197,7 @@ def test_positive_create_with_download_policy(self, repo_options, repo): @pytest.mark.parametrize( 'repo_options', **datafactory.parametrized([{'content_type': 'yum'}]), indirect=True ) - def test_positive_create_with_default_download_policy(self, repo): + def test_positive_create_with_default_download_policy(self, repo, target_sat): """Verify if the default download policy is assigned when creating a YUM repo without `download_policy` field @@ -210,7 +210,7 @@ def test_positive_create_with_default_download_policy(self, repo): :CaseImportance: Critical """ - default_dl_policy = entities.Setting().search( + default_dl_policy = target_sat.api.Setting().search( query={'search': 'name=default_download_policy'} ) assert default_dl_policy @@ -310,7 +310,7 @@ def test_positive_create_unprotected(self, repo_options, repo): assert repo.unprotected == repo_options['unprotected'] @pytest.mark.tier2 - def test_positive_create_with_gpg(self, module_org, module_product): + def test_positive_create_with_gpg(self, module_org, module_product, module_target_sat): """Create a repository and provide a GPG key ID. :id: 023cf84b-74f3-4e63-a9d7-10afee6c1990 @@ -319,16 +319,16 @@ def test_positive_create_with_gpg(self, module_org, module_product): :CaseLevel: Integration """ - gpg_key = entities.GPGKey( + gpg_key = module_target_sat.api.GPGKey( organization=module_org, content=DataFile.VALID_GPG_KEY_FILE.read_text(), ).create() - repo = entities.Repository(product=module_product, gpg_key=gpg_key).create() + repo = module_target_sat.api.Repository(product=module_product, gpg_key=gpg_key).create() # Verify that the given GPG key ID is used. assert repo.gpg_key.id == gpg_key.id @pytest.mark.tier2 - def test_positive_create_same_name_different_orgs(self, repo): + def test_positive_create_same_name_different_orgs(self, repo, target_sat): """Create two repos with the same name in two different organizations. :id: bd1bd7e3-e393-44c8-a6d0-42edade40f60 @@ -338,9 +338,9 @@ def test_positive_create_same_name_different_orgs(self, repo): :CaseLevel: Integration """ - org_2 = entities.Organization().create() - product_2 = entities.Product(organization=org_2).create() - repo_2 = entities.Repository(product=product_2, name=repo.name).create() + org_2 = target_sat.api.Organization().create() + product_2 = target_sat.api.Product(organization=org_2).create() + repo_2 = target_sat.api.Repository(product=product_2, name=repo.name).create() assert repo_2.name == repo.name @pytest.mark.tier1 @@ -349,7 +349,7 @@ def test_positive_create_same_name_different_orgs(self, repo): **datafactory.parametrized([{'name': name} for name in datafactory.invalid_values_list()]), indirect=True, ) - def test_negative_create_name(self, repo_options): + def test_negative_create_name(self, repo_options, target_sat): """Attempt to create repository with invalid names only. :id: 24947c92-3415-43df-add6-d6eb38afd8a3 @@ -361,7 +361,7 @@ def test_negative_create_name(self, repo_options): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.Repository(**repo_options).create() + target_sat.api.Repository(**repo_options).create() @pytest.mark.tier1 @pytest.mark.parametrize( @@ -371,7 +371,7 @@ def test_negative_create_name(self, repo_options): ), indirect=True, ) - def test_negative_create_with_same_name(self, repo_options, repo): + def test_negative_create_with_same_name(self, repo_options, repo, target_sat): """Attempt to create a repository providing a name of already existent entity @@ -384,10 +384,10 @@ def test_negative_create_with_same_name(self, repo_options, repo): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.Repository(**repo_options).create() + target_sat.api.Repository(**repo_options).create() @pytest.mark.tier1 - def test_negative_create_label(self, module_product): + def test_negative_create_label(self, module_product, module_target_sat): """Attempt to create repository with invalid label. :id: f646ae84-2660-41bd-9883-331285fa1c9a @@ -397,7 +397,9 @@ def test_negative_create_label(self, module_product): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.Repository(product=module_product, label=gen_string('utf8')).create() + module_target_sat.api.Repository( + product=module_product, label=gen_string('utf8') + ).create() @pytest.mark.tier1 @pytest.mark.parametrize( @@ -405,7 +407,7 @@ def test_negative_create_label(self, module_product): **datafactory.parametrized([{'url': url} for url in datafactory.invalid_names_list()]), indirect=True, ) - def test_negative_create_url(self, repo_options): + def test_negative_create_url(self, repo_options, target_sat): """Attempt to create repository with invalid url. :id: 0bb9fc3f-d442-4437-b5d8-83024bc7ceab @@ -417,7 +419,7 @@ def test_negative_create_url(self, repo_options): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.Repository(**repo_options).create() + target_sat.api.Repository(**repo_options).create() @pytest.mark.tier1 @pytest.mark.skipif( @@ -428,7 +430,7 @@ def test_negative_create_url(self, repo_options): **datafactory.parametrized([{'url': f'http://{gen_string("alpha")}{punctuation}.com'}]), indirect=True, ) - def test_negative_create_with_url_with_special_characters(self, repo_options): + def test_negative_create_with_url_with_special_characters(self, repo_options, target_sat): """Verify that repository URL cannot contain unquoted special characters :id: 2ffaa412-e5e5-4bec-afaa-9ea54315df49 @@ -440,7 +442,7 @@ def test_negative_create_with_url_with_special_characters(self, repo_options): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.Repository(**repo_options).create() + target_sat.api.Repository(**repo_options).create() @pytest.mark.tier1 @pytest.mark.parametrize( @@ -450,7 +452,7 @@ def test_negative_create_with_url_with_special_characters(self, repo_options): ), indirect=True, ) - def test_negative_create_with_invalid_download_policy(self, repo_options): + def test_negative_create_with_invalid_download_policy(self, repo_options, target_sat): """Verify that YUM repository cannot be created with invalid download policy @@ -464,13 +466,13 @@ def test_negative_create_with_invalid_download_policy(self, repo_options): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.Repository(**repo_options).create() + target_sat.api.Repository(**repo_options).create() @pytest.mark.tier1 @pytest.mark.parametrize( 'repo_options', **datafactory.parametrized([{'content_type': 'yum'}]), indirect=True ) - def test_negative_update_to_invalid_download_policy(self, repo): + def test_negative_update_to_invalid_download_policy(self, repo, target_sat): """Verify that YUM repository cannot be updated to invalid download policy @@ -499,7 +501,7 @@ def test_negative_update_to_invalid_download_policy(self, repo): ), indirect=True, ) - def test_negative_create_non_yum_with_download_policy(self, repo_options): + def test_negative_create_non_yum_with_download_policy(self, repo_options, target_sat): """Verify that non-YUM repositories cannot be created with download policy @@ -513,7 +515,7 @@ def test_negative_create_non_yum_with_download_policy(self, repo_options): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.Repository(**repo_options).create() + target_sat.api.Repository(**repo_options).create() @pytest.mark.tier1 @pytest.mark.parametrize( @@ -523,7 +525,7 @@ def test_negative_create_non_yum_with_download_policy(self, repo_options): ), indirect=True, ) - def test_negative_create_checksum(self, repo_options): + def test_negative_create_checksum(self, repo_options, target_sat): """Attempt to create repository with invalid checksum type. :id: c49a3c49-110d-4b74-ae14-5c9494a4541c @@ -535,7 +537,7 @@ def test_negative_create_checksum(self, repo_options): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.Repository(**repo_options).create() + target_sat.api.Repository(**repo_options).create() @pytest.mark.tier1 @pytest.mark.parametrize( @@ -547,7 +549,7 @@ def test_negative_create_checksum(self, repo_options): ids=['sha1', 'sha256'], indirect=True, ) - def test_negative_create_checksum_with_on_demand_policy(self, repo_options): + def test_negative_create_checksum_with_on_demand_policy(self, repo_options, target_sat): """Attempt to create repository with checksum and on_demand policy. :id: de8b157c-ed62-454b-94eb-22659ce1158e @@ -559,7 +561,7 @@ def test_negative_create_checksum_with_on_demand_policy(self, repo_options): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.Repository(**repo_options).create() + target_sat.api.Repository(**repo_options).create() @pytest.mark.tier1 @pytest.mark.parametrize( @@ -669,7 +671,7 @@ def test_positive_update_unprotected(self, repo): assert repo.unprotected is True @pytest.mark.tier2 - def test_positive_update_gpg(self, module_org, module_product): + def test_positive_update_gpg(self, module_org, module_product, module_target_sat): """Create a repository and update its GPGKey :id: 0e9319dc-c922-4ecf-9f83-d221cfdf54c2 @@ -679,14 +681,14 @@ def test_positive_update_gpg(self, module_org, module_product): :CaseLevel: Integration """ # Create a repo and make it point to a GPG key. - gpg_key_1 = entities.GPGKey( + gpg_key_1 = module_target_sat.api.GPGKey( organization=module_org, content=DataFile.VALID_GPG_KEY_FILE.read_text(), ).create() - repo = entities.Repository(product=module_product, gpg_key=gpg_key_1).create() + repo = module_target_sat.api.Repository(product=module_product, gpg_key=gpg_key_1).create() # Update the repo and make it point to a new GPG key. - gpg_key_2 = entities.GPGKey( + gpg_key_2 = module_target_sat.api.GPGKey( organization=module_org, content=DataFile.VALID_GPG_KEY_BETA_FILE.read_text(), ).create() @@ -712,7 +714,7 @@ def test_positive_update_contents(self, repo): @pytest.mark.tier1 @pytest.mark.upgrade - def test_positive_upload_delete_srpm(self, repo): + def test_positive_upload_delete_srpm(self, repo, target_sat): """Create a repository and upload, delete SRPM contents. :id: e091a725-048f-44ca-90cc-c016c450ced9 @@ -726,12 +728,12 @@ def test_positive_upload_delete_srpm(self, repo): :BZ: 1378442 """ # upload srpm - entities.ContentUpload(repository=repo).upload( + target_sat.api.ContentUpload(repository=repo).upload( filepath=DataFile.SRPM_TO_UPLOAD, content_type='srpm', ) assert repo.read().content_counts['srpm'] == 1 - srpm_detail = entities.Srpms().search(query={'repository_id': repo.id}) + srpm_detail = target_sat.api.Srpms().search(query={'repository_id': repo.id}) assert len(srpm_detail) == 1 # Delete srpm @@ -750,7 +752,7 @@ def test_positive_upload_delete_srpm(self, repo): ids=['yum_fake'], indirect=True, ) - def test_positive_create_delete_srpm_repo(self, repo): + def test_positive_create_delete_srpm_repo(self, repo, target_sat): """Create a repository, sync SRPM contents and remove repo :id: e091a725-042f-43ca-99cc-c017c450ced9 @@ -763,7 +765,7 @@ def test_positive_create_delete_srpm_repo(self, repo): """ repo.sync() assert repo.read().content_counts['srpm'] == 3 - assert len(entities.Srpms().search(query={'repository_id': repo.id})) == 3 + assert len(target_sat.api.Srpms().search(query={'repository_id': repo.id})) == 3 repo.delete() with pytest.raises(HTTPError): repo.read() @@ -778,7 +780,7 @@ def test_positive_create_delete_srpm_repo(self, repo): ids=['yum_fake_2'], indirect=True, ) - def test_positive_remove_contents(self, repo): + def test_positive_remove_contents(self, repo, target_sat): """Synchronize a repository and remove rpm content. :id: f686b74b-7ee9-4806-b999-bc05ffe61a9d @@ -795,7 +797,7 @@ def test_positive_remove_contents(self, repo): repo.sync() assert repo.read().content_counts['rpm'] >= 1 # Find repo packages and remove them - packages = entities.Package(repository=repo).search(query={'per_page': '1000'}) + packages = target_sat.api.Package(repository=repo).search(query={'per_page': '1000'}) repo.remove_content(data={'ids': [package.id for package in packages]}) assert repo.read().content_counts['rpm'] == 0 @@ -953,7 +955,7 @@ def test_negative_synchronize_auth_yum_repo(self, repo): ids=['yum_fake_2'], indirect=True, ) - def test_positive_resynchronize_rpm_repo(self, repo): + def test_positive_resynchronize_rpm_repo(self, repo, target_sat): """Check that repository content is resynced after packages were removed from repository @@ -971,7 +973,7 @@ def test_positive_resynchronize_rpm_repo(self, repo): repo.sync() assert repo.read().content_counts['rpm'] >= 1 # Find repo packages and remove them - packages = entities.Package(repository=repo).search(query={'per_page': '1000'}) + packages = target_sat.api.Package(repository=repo).search(query={'per_page': '1000'}) repo.remove_content(data={'ids': [package.id for package in packages]}) assert repo.read().content_counts['rpm'] == 0 # Re-synchronize repository @@ -1173,7 +1175,7 @@ def test_positive_recreate_pulp_repositories(self, module_entitlement_manifest_o reposet=constants.REPOSET['rhst7'], releasever=None, ) - call_entity_method_with_timeout(entities.Repository(id=repo_id).sync, timeout=1500) + call_entity_method_with_timeout(target_sat.api.Repository(id=repo_id).sync, timeout=1500) with target_sat.session.shell() as sh: sh.send('foreman-rake console') time.sleep(30) # sleep to allow time for console to open @@ -1210,9 +1212,9 @@ def test_positive_mirroring_policy(self, target_sat): repo_url = settings.repos.yum_0.url packages_count = constants.FAKE_0_YUM_REPO_PACKAGES_COUNT - org = entities.Organization().create() - prod = entities.Product(organization=org).create() - repo = entities.Repository( + org = target_sat.api.Organization().create() + prod = target_sat.api.Product(organization=org).create() + repo = target_sat.api.Repository( download_policy='immediate', mirroring_policy='mirror_complete', product=prod, @@ -1223,7 +1225,7 @@ def test_positive_mirroring_policy(self, target_sat): assert repo.content_counts['rpm'] == packages_count # remove all packages from the repo and upload another one - packages = entities.Package(repository=repo).search(query={'per_page': '1000'}) + packages = target_sat.api.Package(repository=repo).search(query={'per_page': '1000'}) repo.remove_content(data={'ids': [package.id for package in packages]}) with open(DataFile.RPM_TO_UPLOAD, 'rb') as handle: @@ -1337,7 +1339,7 @@ class TestRepositorySync: """Tests for ``/katello/api/repositories/:id/sync``.""" @pytest.mark.tier2 - def test_positive_sync_repos_with_lots_files(self): + def test_positive_sync_repos_with_lots_files(self, target_sat): """Attempt to synchronize repository containing a lot of files inside rpms. @@ -1351,9 +1353,9 @@ def test_positive_sync_repos_with_lots_files(self): :expectedresults: repository was successfully synchronized """ - org = entities.Organization().create() - product = entities.Product(organization=org).create() - repo = entities.Repository(product=product, url=settings.repos.yum_8.url).create() + org = target_sat.api.Organization().create() + product = target_sat.api.Product(organization=org).create() + repo = target_sat.api.Repository(product=product, url=settings.repos.yum_8.url).create() response = repo.sync() assert response, f"Repository {repo} failed to sync." @@ -1376,7 +1378,7 @@ def test_positive_sync_rh(self, module_entitlement_manifest_org, target_sat): reposet=constants.REPOSET['rhst7'], releasever=None, ) - entities.Repository(id=repo_id).sync() + target_sat.api.Repository(id=repo_id).sync() @pytest.mark.tier2 @pytest.mark.skipif( @@ -1452,19 +1454,19 @@ def test_positive_bulk_cancel_sync(self, target_sat, module_entitlement_manifest releasever=repo['releasever'], ) repo_ids.append(repo_id) - rh_repo = entities.Repository(id=repo_id).read() + rh_repo = target_sat.api.Repository(id=repo_id).read() rh_repo.download_policy = 'immediate' rh_repo = rh_repo.update() sync_ids = [] for repo_id in repo_ids: - sync_task = entities.Repository(id=repo_id).sync(synchronous=False) + sync_task = target_sat.api.Repository(id=repo_id).sync(synchronous=False) sync_ids.append(sync_task['id']) - entities.ForemanTask().bulk_cancel(data={"task_ids": sync_ids[0:5]}) + target_sat.api.ForemanTask().bulk_cancel(data={"task_ids": sync_ids[0:5]}) # Give some time for sync cancels to calm down time.sleep(30) - entities.ForemanTask().bulk_cancel(data={"task_ids": sync_ids[5:]}) + target_sat.api.ForemanTask().bulk_cancel(data={"task_ids": sync_ids[5:]}) for sync_id in sync_ids: - sync_result = entities.ForemanTask(id=sync_id).poll(canceled=True) + sync_result = target_sat.api.ForemanTask(id=sync_id).poll(canceled=True) assert ( 'Task canceled' in sync_result['humanized']['errors'] or 'No content added' in sync_result['humanized']['output'] @@ -1576,11 +1578,11 @@ def test_positive_sync_kickstart_check_os( repo=constants.REPOS['kickstart'][distro]['name'], releasever=constants.REPOS['kickstart'][distro]['version'], ) - rh_repo = entities.Repository(id=repo_id).read() + rh_repo = target_sat.api.Repository(id=repo_id).read() rh_repo.sync() major, minor = constants.REPOS['kickstart'][distro]['version'].split('.') - os = entities.OperatingSystem().search( + os = target_sat.api.OperatingSystem().search( query={'search': f'name="RedHat" AND major="{major}" AND minor="{minor}"'} ) assert len(os) @@ -1701,7 +1703,7 @@ def test_positive_synchronize(self, repo): ), indirect=True, ) - def test_positive_cancel_docker_repo_sync(self, repo): + def test_positive_cancel_docker_repo_sync(self, repo, target_sat): """Cancel a large, syncing Docker-type repository :id: 86534979-be49-40ad-8290-05ac71c801b2 @@ -1724,8 +1726,8 @@ def test_positive_cancel_docker_repo_sync(self, repo): sync_task = repo.sync(synchronous=False) # Need to wait for sync to actually start up time.sleep(2) - entities.ForemanTask().bulk_cancel(data={"task_ids": [sync_task['id']]}) - sync_task = entities.ForemanTask(id=sync_task['id']).poll(canceled=True) + target_sat.api.ForemanTask().bulk_cancel(data={"task_ids": [sync_task['id']]}) + sync_task = target_sat.api.ForemanTask(id=sync_task['id']).poll(canceled=True) assert 'Task canceled' in sync_task['humanized']['errors'] assert 'No content added' in sync_task['humanized']['output'] @@ -1744,7 +1746,9 @@ def test_positive_cancel_docker_repo_sync(self, repo): ), indirect=True, ) - def test_positive_delete_product_with_synced_repo(self, repo_options_custom_product): + def test_positive_delete_product_with_synced_repo( + self, repo_options_custom_product, target_sat + ): """Create and sync a Docker-type repository, delete the product. :id: c3d33836-54df-484d-97e1-f9fc9e22d23c @@ -1759,7 +1763,7 @@ def test_positive_delete_product_with_synced_repo(self, repo_options_custom_prod :BZ: 1867287 """ - repo = entities.Repository(**repo_options_custom_product).create() + repo = target_sat.api.Repository(**repo_options_custom_product).create() repo.sync(timeout=600) assert repo.read().content_counts['docker_manifest'] >= 1 assert repo.product.delete() @@ -1814,7 +1818,7 @@ def test_positive_synchronize_private_registry(self, repo): :parametrized: yes - :expectedresults: A repository is created with a private Docker \ + :expectedresults: A repository is created with a private Docker repository and it is synchronized. :customerscenario: true @@ -1945,7 +1949,7 @@ def test_negative_synchronize_private_registry_no_passwd( match='422 Client Error: Unprocessable Entity for url: ' f'{target_sat.url}:443/katello/api/v2/repositories', ): - entities.Repository(**repo_options).create() + target_sat.api.Repository(**repo_options).create() @pytest.mark.tier2 @pytest.mark.upgrade @@ -2222,7 +2226,7 @@ def test_negative_synchronize_docker_repo_with_invalid_tags(self, repo_options, # releasever=None, # basearch=None, # ) -# call_entity_method_with_timeout(entities.Repository(id=repo_id).sync, timeout=1500) +# call_entity_method_with_timeout(target_sat.api.Repository(id=repo_id).sync, timeout=1500) class TestSRPMRepository: @@ -2231,7 +2235,9 @@ class TestSRPMRepository: @pytest.mark.skip_if_open("BZ:2016047") @pytest.mark.upgrade @pytest.mark.tier2 - def test_positive_srpm_upload_publish_promote_cv(self, module_org, env, repo): + def test_positive_srpm_upload_publish_promote_cv( + self, module_org, env, repo, module_target_sat + ): """Upload SRPM to repository, add repository to content view and publish, promote content view @@ -2239,20 +2245,27 @@ def test_positive_srpm_upload_publish_promote_cv(self, module_org, env, repo): :expectedresults: srpms can be listed in organization, content view, Lifecycle env """ - entities.ContentUpload(repository=repo).upload( + module_target_sat.api.ContentUpload(repository=repo).upload( filepath=DataFile.SRPM_TO_UPLOAD, content_type='srpm', ) - cv = entities.ContentView(organization=module_org, repository=[repo]).create() + cv = module_target_sat.api.ContentView(organization=module_org, repository=[repo]).create() cv.publish() cv = cv.read() assert cv.repository[0].read().content_counts['srpm'] == 1 - assert len(entities.Srpms().search(query={'organization_id': module_org.id})) >= 1 + assert ( + len(module_target_sat.api.Srpms().search(query={'organization_id': module_org.id})) >= 1 + ) assert ( - len(entities.Srpms().search(query={'content_view_version_id': cv.version[0].id})) == 1 + len( + module_target_sat.api.Srpms().search( + query={'content_view_version_id': cv.version[0].id} + ) + ) + == 1 ) @pytest.mark.upgrade @@ -2266,7 +2279,7 @@ def test_positive_srpm_upload_publish_promote_cv(self, module_org, env, repo): **datafactory.parametrized({'fake_srpm': {'url': repo_constants.FAKE_YUM_SRPM_REPO}}), indirect=True, ) - def test_positive_repo_sync_publish_promote_cv(self, module_org, env, repo): + def test_positive_repo_sync_publish_promote_cv(self, module_org, env, repo, target_sat): """Synchronize repository with SRPMs, add repository to content view and publish, promote content view @@ -2278,19 +2291,20 @@ def test_positive_repo_sync_publish_promote_cv(self, module_org, env, repo): """ repo.sync() - cv = entities.ContentView(organization=module_org, repository=[repo]).create() + cv = target_sat.api.ContentView(organization=module_org, repository=[repo]).create() cv.publish() cv = cv.read() assert cv.repository[0].read().content_counts['srpm'] == 3 - assert len(entities.Srpms().search(query={'organization_id': module_org.id})) >= 3 + assert len(target_sat.api.Srpms().search(query={'organization_id': module_org.id})) >= 3 assert ( - len(entities.Srpms().search(query={'content_view_version_id': cv.version[0].id})) >= 3 + len(target_sat.api.Srpms().search(query={'content_view_version_id': cv.version[0].id})) + >= 3 ) cv.version[0].promote(data={'environment_ids': env.id, 'force': False}) - assert len(entities.Srpms().search(query={'environment_id': env.id})) == 3 + assert len(target_sat.api.Srpms().search(query={'environment_id': env.id})) == 3 class TestSRPMRepositoryIgnoreContent: @@ -2405,7 +2419,7 @@ class TestFileRepository: **parametrized([{'content_type': 'file', 'url': repo_constants.CUSTOM_FILE_REPO}]), indirect=True, ) - def test_positive_upload_file_to_file_repo(self, repo): + def test_positive_upload_file_to_file_repo(self, repo, target_sat): """Check arbitrary file can be uploaded to File Repository :id: fdb46481-f0f4-45aa-b075-2a8f6725e51b @@ -2423,7 +2437,9 @@ def test_positive_upload_file_to_file_repo(self, repo): repo.upload_content(files={'content': DataFile.RPM_TO_UPLOAD.read_bytes()}) assert repo.read().content_counts['file'] == 1 - filesearch = entities.File().search(query={"search": f"name={constants.RPM_TO_UPLOAD}"}) + filesearch = target_sat.api.File().search( + query={"search": f"name={constants.RPM_TO_UPLOAD}"} + ) assert constants.RPM_TO_UPLOAD == filesearch[0].name @pytest.mark.stubbed @@ -2454,7 +2470,7 @@ def test_positive_file_permissions(self): **parametrized([{'content_type': 'file', 'url': repo_constants.CUSTOM_FILE_REPO}]), indirect=True, ) - def test_positive_remove_file(self, repo): + def test_positive_remove_file(self, repo, target_sat): """Check arbitrary file can be removed from File Repository :id: 65068b4c-9018-4baa-b87b-b6e9d7384a5d @@ -2475,7 +2491,7 @@ def test_positive_remove_file(self, repo): repo.upload_content(files={'content': DataFile.RPM_TO_UPLOAD.read_bytes()}) assert repo.read().content_counts['file'] == 1 - file_detail = entities.File().search(query={'repository_id': repo.id}) + file_detail = target_sat.api.File().search(query={'repository_id': repo.id}) repo.remove_content(data={'ids': [file_detail[0].id], 'content_type': 'file'}) assert repo.read().content_counts['file'] == 0 @@ -2569,7 +2585,9 @@ class TestTokenAuthContainerRepository: """ @pytest.mark.tier2 - def test_positive_create_with_long_token(self, module_org, module_product, request): + def test_positive_create_with_long_token( + self, module_org, module_product, request, module_target_sat + ): """Create and sync Docker-type repo from the Red Hat Container registry Using token based auth, with very long tokens (>255 characters). @@ -2606,7 +2624,7 @@ def test_positive_create_with_long_token(self, module_org, module_product, reque if not len(repo_options['upstream_password']) > 255: pytest.skip('The "long_pass" registry does not meet length requirement') - repo = entities.Repository(**repo_options).create() + repo = module_target_sat.api.Repository(**repo_options).create() @request.addfinalizer def clean_repo(): @@ -2628,7 +2646,9 @@ def clean_repo(): @pytest.mark.tier2 @pytest.mark.parametrize('repo_key', container_repo_keys) - def test_positive_tag_whitelist(self, request, repo_key, module_org, module_product): + def test_positive_tag_whitelist( + self, request, repo_key, module_org, module_product, module_target_sat + ): """Create and sync Docker-type repos from multiple supported registries with a tag whitelist :id: 4f8ea85b-4c69-4da6-a8ef-bd467ee35147 @@ -2651,7 +2671,7 @@ def test_positive_tag_whitelist(self, request, repo_key, module_org, module_prod repo_options['organization'] = module_org repo_options['product'] = module_product - repo = entities.Repository(**repo_options).create() + repo = module_target_sat.api.Repository(**repo_options).create() @request.addfinalizer def clean_repo(): diff --git a/tests/foreman/api/test_repository_set.py b/tests/foreman/api/test_repository_set.py index 449bb1be33c..e4766585824 100644 --- a/tests/foreman/api/test_repository_set.py +++ b/tests/foreman/api/test_repository_set.py @@ -19,7 +19,6 @@ :Upstream: No """ -from nailgun import entities import pytest from robottelo.constants import PRDS, REPOSET @@ -33,17 +32,17 @@ @pytest.fixture -def product(function_entitlement_manifest_org): +def product(function_entitlement_manifest_org, module_target_sat): """Find and return the product matching PRODUCT_NAME.""" - return entities.Product( + return module_target_sat.api.Product( name=PRODUCT_NAME, organization=function_entitlement_manifest_org ).search()[0] @pytest.fixture -def reposet(product): +def reposet(product, module_target_sat): """Find and return the repository set matching REPOSET_NAME and product.""" - return entities.RepositorySet(name=REPOSET_NAME, product=product).search()[0] + return module_target_sat.api.RepositorySet(name=REPOSET_NAME, product=product).search()[0] @pytest.fixture diff --git a/tests/foreman/api/test_role.py b/tests/foreman/api/test_role.py index 4b42408114d..fabd0d3008c 100644 --- a/tests/foreman/api/test_role.py +++ b/tests/foreman/api/test_role.py @@ -20,7 +20,6 @@ :Upstream: No """ -from nailgun import entities from nailgun.config import ServerConfig import pytest from requests.exceptions import HTTPError @@ -41,7 +40,7 @@ class TestRole: 'name, new_name', **parametrized(list(zip(generate_strings_list(), generate_strings_list()))), ) - def test_positive_crud(self, name, new_name): + def test_positive_crud(self, name, new_name, target_sat): """Create, update and delete role with name ``name_generator()``. :id: 02c7d04d-a52c-4bc2-9e17-9938ab2ee5b2 @@ -55,7 +54,7 @@ def test_positive_crud(self, name, new_name): :CaseImportance: Critical """ - role = entities.Role(name=name).create() + role = target_sat.api.Role(name=name).create() assert role.name == name role.name = new_name assert role.update(['name']).name == new_name @@ -67,7 +66,7 @@ def test_positive_crud(self, name, new_name): class TestCannedRole: """Implements Canned Roles tests from API""" - def create_org_admin_role(self, name=None, orgs=None, locs=None): + def create_org_admin_role(self, target_sat, name=None, orgs=None, locs=None): """Helper function to create org admin role for particular organizations and locations by cloning 'Organization admin' role. @@ -80,17 +79,19 @@ def create_org_admin_role(self, name=None, orgs=None, locs=None): data returned from 'clone' function """ name = gen_string('alpha') if not name else name - default_org_admin = entities.Role().search(query={'search': 'name="Organization admin"'}) - org_admin = entities.Role(id=default_org_admin[0].id).clone( + default_org_admin = target_sat.api.Role().search( + query={'search': 'name="Organization admin"'} + ) + org_admin = target_sat.api.Role(id=default_org_admin[0].id).clone( data={ 'role': {'name': name, 'organization_ids': orgs or [], 'location_ids': locs or []} } ) if 'role' in org_admin: - return entities.Role(id=org_admin['role']['id']).read() - return entities.Role(id=org_admin['id']).read() + return target_sat.api.Role(id=org_admin['role']['id']).read() + return target_sat.api.Role(id=org_admin['id']).read() - def create_org_admin_user(self, role_taxos, user_taxos): + def create_org_admin_user(self, role_taxos, user_taxos, target_sat): """Helper function to create an Org Admin user by assigning org admin role and assign taxonomies to Role and User @@ -105,12 +106,12 @@ def create_org_admin_user(self, role_taxos, user_taxos): """ # Create Org Admin Role org_admin = self.create_org_admin_role( - orgs=[role_taxos['org'].id], locs=[role_taxos['loc'].id] + target_sat, orgs=[role_taxos['org'].id], locs=[role_taxos['loc'].id] ) # Create Org Admin User user_login = gen_string('alpha') user_passwd = gen_string('alphanumeric') - user = entities.User( + user = target_sat.api.User( login=user_login, password=user_passwd, organization=[user_taxos['org'].id], @@ -120,7 +121,7 @@ def create_org_admin_user(self, role_taxos, user_taxos): user.passwd = user_passwd return user - def create_simple_user(self, filter_taxos, role=None): + def create_simple_user(self, target_sat, filter_taxos, role=None): """Creates simple user and assigns taxonomies :param dict filter_taxos: Filter taxonomiest specified as dictionary containing @@ -130,7 +131,7 @@ def create_simple_user(self, filter_taxos, role=None): passwd attr """ user_passwd = gen_string('alphanumeric') - user = entities.User( + user = target_sat.api.User( login=gen_string('alpha'), password=user_passwd, organization=[filter_taxos['org'].id], @@ -140,14 +141,16 @@ def create_simple_user(self, filter_taxos, role=None): user.passwd = user_passwd return user - def create_domain(self, orgs, locs): + def create_domain(self, target_sat, orgs, locs): """Creates domain in given orgs and locs :param list orgs: List of Organization ids :param list locs: List of Location ids :return Domain: Returns the ```nailgun.entities.Domain``` object """ - return entities.Domain(name=gen_string('alpha'), organization=orgs, location=locs).create() + return target_sat.api.Domain( + name=gen_string('alpha'), organization=orgs, location=locs + ).create() def user_config(self, user, satellite): """Returns The ```nailgun.confin.ServerConfig``` for given user @@ -160,19 +163,19 @@ def user_config(self, user, satellite): ) @pytest.fixture - def role_taxonomies(self): + def role_taxonomies(self, target_sat): """Create role taxonomies""" return { - 'org': entities.Organization().create(), - 'loc': entities.Location().create(), + 'org': target_sat.api.Organization().create(), + 'loc': target_sat.api.Location().create(), } @pytest.fixture - def filter_taxonomies(self): + def filter_taxonomies(self, target_sat): """Create filter taxonomies""" return { - 'org': entities.Organization().create(), - 'loc': entities.Location().create(), + 'org': target_sat.api.Organization().create(), + 'loc': target_sat.api.Location().create(), } @pytest.fixture @@ -184,7 +187,7 @@ def create_ldap(self, ad_data, target_sat, module_location, module_org): sat_url=target_sat.url, ldap_user_name=ad_data['ldap_user_name'], ldap_user_passwd=ad_data['ldap_user_passwd'], - authsource=entities.AuthSourceLDAP( + authsource=target_sat.api.AuthSourceLDAP( onthefly_register=True, account=fr"{ad_data['workgroup']}\{ad_data['ldap_user_name']}", account_password=ad_data['ldap_user_passwd'], @@ -210,7 +213,7 @@ def create_ldap(self, ad_data, target_sat, module_location, module_org): LDAPAuthSource.delete({'name': authsource_name}) @pytest.mark.tier1 - def test_positive_create_role_with_taxonomies(self, role_taxonomies): + def test_positive_create_role_with_taxonomies(self, role_taxonomies, target_sat): """create role with taxonomies :id: fa449217-889c-429b-89b5-0b6c018ffd9e @@ -222,7 +225,7 @@ def test_positive_create_role_with_taxonomies(self, role_taxonomies): :CaseImportance: Critical """ role_name = gen_string('alpha') - role = entities.Role( + role = target_sat.api.Role( name=role_name, organization=[role_taxonomies['org']], location=[role_taxonomies['loc']], @@ -232,7 +235,7 @@ def test_positive_create_role_with_taxonomies(self, role_taxonomies): assert role_taxonomies['loc'].id == role.location[0].id @pytest.mark.tier1 - def test_positive_create_role_without_taxonomies(self): + def test_positive_create_role_without_taxonomies(self, target_sat): """Create role without taxonomies :id: fe65a691-1b04-4bfe-a24b-adb48feb31d1 @@ -244,13 +247,13 @@ def test_positive_create_role_without_taxonomies(self): :CaseImportance: Critical """ role_name = gen_string('alpha') - role = entities.Role(name=role_name, organization=[], location=[]).create() + role = target_sat.api.Role(name=role_name, organization=[], location=[]).create() assert role.name == role_name assert not role.organization assert not role.location @pytest.mark.tier1 - def test_positive_create_filter_without_override(self, role_taxonomies): + def test_positive_create_filter_without_override(self, role_taxonomies, target_sat): """Create filter in role w/o overriding it :id: 1aadb7ea-ff76-4171-850f-188ba6f87021 @@ -269,27 +272,27 @@ def test_positive_create_filter_without_override(self, role_taxonomies): :CaseImportance: Critical """ role_name = gen_string('alpha') - role = entities.Role( + role = target_sat.api.Role( name=role_name, organization=[role_taxonomies['org']], location=[role_taxonomies['loc']], ).create() assert role.name == role_name - dom_perm = entities.Permission().search(query={'search': 'resource_type="Domain"'}) - filtr = entities.Filter(permission=dom_perm, role=role.id).create() + dom_perm = target_sat.api.Permission().search(query={'search': 'resource_type="Domain"'}) + filtr = target_sat.api.Filter(permission=dom_perm, role=role.id).create() assert role.id == filtr.role.id assert role_taxonomies['org'].id == filtr.organization[0].id assert role_taxonomies['loc'].id == filtr.location[0].id assert not filtr.override @pytest.mark.tier1 - def test_positive_create_non_overridable_filter(self): + def test_positive_create_non_overridable_filter(self, target_sat): """Create non overridable filter in role :id: f891e2e1-76f8-4edf-8c96-b41d05483298 :steps: Create a filter to which taxonomies cannot be associated. - e.g Architecture filter + e.g. Architecture filter :expectedresults: @@ -299,21 +302,23 @@ def test_positive_create_non_overridable_filter(self): :CaseImportance: Critical """ role_name = gen_string('alpha') - role = entities.Role(name=role_name).create() + role = target_sat.api.Role(name=role_name).create() assert role.name == role_name - arch_perm = entities.Permission().search(query={'search': 'resource_type="Architecture"'}) - filtr = entities.Filter(permission=arch_perm, role=role.id).create() + arch_perm = target_sat.api.Permission().search( + query={'search': 'resource_type="Architecture"'} + ) + filtr = target_sat.api.Filter(permission=arch_perm, role=role.id).create() assert role.id == filtr.role.id assert not filtr.override @pytest.mark.tier1 - def test_negative_override_non_overridable_filter(self, filter_taxonomies): + def test_negative_override_non_overridable_filter(self, filter_taxonomies, target_sat): """Override non overridable filter :id: 7793be96-e8eb-451b-a986-51a46a1ab4f9 :steps: Attempt to override a filter to which taxonomies cannot be - associated. e.g Architecture filter + associated. e.g. Architecture filter :expectedresults: Filter is not overrided as taxonomies cannot be applied to that filter @@ -321,11 +326,13 @@ def test_negative_override_non_overridable_filter(self, filter_taxonomies): :CaseImportance: Critical """ role_name = gen_string('alpha') - role = entities.Role(name=role_name).create() + role = target_sat.api.Role(name=role_name).create() assert role.name == role_name - arch_perm = entities.Permission().search(query={'search': 'resource_type="Architecture"'}) + arch_perm = target_sat.api.Permission().search( + query={'search': 'resource_type="Architecture"'} + ) with pytest.raises(HTTPError): - entities.Filter( + target_sat.api.Filter( permission=arch_perm, role=[role.id], override=True, @@ -335,7 +342,9 @@ def test_negative_override_non_overridable_filter(self, filter_taxonomies): @pytest.mark.tier1 @pytest.mark.upgrade - def test_positive_create_overridable_filter(self, role_taxonomies, filter_taxonomies): + def test_positive_create_overridable_filter( + self, role_taxonomies, filter_taxonomies, target_sat + ): """Create overridable filter in role :id: c7ea9377-9b9e-495e-accd-3576166d504e @@ -355,14 +364,14 @@ def test_positive_create_overridable_filter(self, role_taxonomies, filter_taxono :CaseImportance: Critical """ role_name = gen_string('alpha') - role = entities.Role( + role = target_sat.api.Role( name=role_name, organization=[role_taxonomies['org']], location=[role_taxonomies['loc']], ).create() assert role.name == role_name - dom_perm = entities.Permission().search(query={'search': 'resource_type="Domain"'}) - filtr = entities.Filter( + dom_perm = target_sat.api.Permission().search(query={'search': 'resource_type="Domain"'}) + filtr = target_sat.api.Filter( permission=dom_perm, role=role.id, override=True, @@ -377,7 +386,7 @@ def test_positive_create_overridable_filter(self, role_taxonomies, filter_taxono assert role_taxonomies['loc'].id != filtr.location[0].id @pytest.mark.tier1 - def test_positive_update_role_taxonomies(self, role_taxonomies, filter_taxonomies): + def test_positive_update_role_taxonomies(self, role_taxonomies, filter_taxonomies, target_sat): """Update role taxonomies which applies to its non-overrided filters :id: 902dcb32-2126-4ff4-b733-3e86749ccd1e @@ -390,29 +399,29 @@ def test_positive_update_role_taxonomies(self, role_taxonomies, filter_taxonomie :CaseImportance: Critical """ role_name = gen_string('alpha') - role = entities.Role( + role = target_sat.api.Role( name=role_name, organization=[role_taxonomies['org']], location=[role_taxonomies['loc']], ).create() assert role.name == role_name - dom_perm = entities.Permission().search(query={'search': 'resource_type="Domain"'}) - filtr = entities.Filter(permission=dom_perm, role=role.id).create() + dom_perm = target_sat.api.Permission().search(query={'search': 'resource_type="Domain"'}) + filtr = target_sat.api.Filter(permission=dom_perm, role=role.id).create() assert role.id == filtr.role.id role.organization = [filter_taxonomies['org']] role.location = [filter_taxonomies['loc']] role = role.update(['organization', 'location']) # Updated Role - role = entities.Role(id=role.id).read() + role = target_sat.api.Role(id=role.id).read() assert filter_taxonomies['org'].id == role.organization[0].id assert filter_taxonomies['loc'].id == role.location[0].id # Updated Filter - filtr = entities.Filter(id=filtr.id).read() + filtr = target_sat.api.Filter(id=filtr.id).read() assert filter_taxonomies['org'].id == filtr.organization[0].id assert filter_taxonomies['loc'].id == filtr.location[0].id @pytest.mark.tier1 - def test_negative_update_role_taxonomies(self, role_taxonomies, filter_taxonomies): + def test_negative_update_role_taxonomies(self, role_taxonomies, filter_taxonomies, target_sat): """Update role taxonomies which doesnt applies to its overrided filters :id: 9f3bf95a-f71a-4063-b51c-12610bc655f2 @@ -426,14 +435,14 @@ def test_negative_update_role_taxonomies(self, role_taxonomies, filter_taxonomie :CaseImportance: Critical """ role_name = gen_string('alpha') - role = entities.Role( + role = target_sat.api.Role( name=role_name, organization=[role_taxonomies['org']], location=[role_taxonomies['loc']], ).create() assert role.name == role_name - dom_perm = entities.Permission().search(query={'search': 'resource_type="Domain"'}) - filtr = entities.Filter( + dom_perm = target_sat.api.Permission().search(query={'search': 'resource_type="Domain"'}) + filtr = target_sat.api.Filter( permission=dom_perm, role=role.id, override=True, @@ -442,23 +451,23 @@ def test_negative_update_role_taxonomies(self, role_taxonomies, filter_taxonomie ).create() assert role.id == filtr.role.id # Creating new Taxonomies - org_new = entities.Organization().create() - loc_new = entities.Location().create() + org_new = target_sat.api.Organization().create() + loc_new = target_sat.api.Location().create() # Updating Taxonomies role.organization = [org_new] role.location = [loc_new] role = role.update(['organization', 'location']) # Updated Role - role = entities.Role(id=role.id).read() + role = target_sat.api.Role(id=role.id).read() assert org_new.id == role.organization[0].id assert loc_new.id == role.location[0].id # Updated Filter - filtr = entities.Filter(id=filtr.id).read() + filtr = target_sat.api.Filter(id=filtr.id).read() assert org_new.id != filtr.organization[0].id assert loc_new.id != filtr.location[0].id @pytest.mark.tier1 - def test_positive_disable_filter_override(self, role_taxonomies, filter_taxonomies): + def test_positive_disable_filter_override(self, role_taxonomies, filter_taxonomies, target_sat): """Unsetting override flag resets filter taxonomies :id: eaa7b921-7c12-45c5-989b-d82aa2b6e3a6 @@ -477,14 +486,14 @@ def test_positive_disable_filter_override(self, role_taxonomies, filter_taxonomi :CaseImportance: Critical """ role_name = gen_string('alpha') - role = entities.Role( + role = target_sat.api.Role( name=role_name, organization=[role_taxonomies['org']], location=[role_taxonomies['loc']], ).create() assert role.name == role_name - dom_perm = entities.Permission().search(query={'search': 'resource_type="Domain"'}) - filtr = entities.Filter( + dom_perm = target_sat.api.Permission().search(query={'search': 'resource_type="Domain"'}) + filtr = target_sat.api.Filter( permission=dom_perm, role=role.id, override=True, @@ -500,7 +509,7 @@ def test_positive_disable_filter_override(self, role_taxonomies, filter_taxonomi assert filter_taxonomies['loc'].id != filtr.location[0].id @pytest.mark.tier1 - def test_positive_create_org_admin_from_clone(self): + def test_positive_create_org_admin_from_clone(self, target_sat): """Create Org Admin role which has access to most of the resources within organization @@ -515,14 +524,16 @@ def test_positive_create_org_admin_from_clone(self): :BZ: 1637436 """ - default_org_admin = entities.Role().search(query={'search': 'name="Organization admin"'}) + default_org_admin = target_sat.api.Role().search( + query={'search': 'name="Organization admin"'} + ) org_admin = self.create_org_admin_role() - default_filters = entities.Role(id=default_org_admin[0].id).read().filters - orgadmin_filters = entities.Role(id=org_admin.id).read().filters + default_filters = target_sat.api.Role(id=default_org_admin[0].id).read().filters + orgadmin_filters = target_sat.api.Role(id=org_admin.id).read().filters assert len(default_filters) == len(orgadmin_filters) @pytest.mark.tier1 - def test_positive_create_cloned_role_with_taxonomies(self, role_taxonomies): + def test_positive_create_cloned_role_with_taxonomies(self, role_taxonomies, target_sat): """Taxonomies can be assigned to cloned role :id: 31079015-5ede-439a-a062-e20d1ffd66df @@ -542,7 +553,7 @@ def test_positive_create_cloned_role_with_taxonomies(self, role_taxonomies): org_admin = self.create_org_admin_role( orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) - org_admin = entities.Role(id=org_admin.id).read() + org_admin = target_sat.api.Role(id=org_admin.id).read() assert role_taxonomies['org'].id == org_admin.organization[0].id assert role_taxonomies['loc'].id == org_admin.location[0].id @@ -575,7 +586,7 @@ def test_negative_access_entities_from_org_admin( sc = self.user_config(user, target_sat) # Getting the domain from user with pytest.raises(HTTPError): - entities.Domain(sc, id=domain.id).read() + target_sat.api.Domain(sc, id=domain.id).read() @pytest.mark.tier3 def test_negative_access_entities_from_user( @@ -606,10 +617,10 @@ def test_negative_access_entities_from_user( sc = self.user_config(user, target_sat) # Getting the domain from user with pytest.raises(HTTPError): - entities.Domain(sc, id=domain.id).read() + target_sat.api.Domain(sc, id=domain.id).read() @pytest.mark.tier2 - def test_positive_override_cloned_role_filter(self, role_taxonomies): + def test_positive_override_cloned_role_filter(self, role_taxonomies, target_sat): """Cloned role filter overrides :id: 8a32ed5f-b93f-4f31-aff4-16602fbe7fab @@ -625,27 +636,27 @@ def test_positive_override_cloned_role_filter(self, role_taxonomies): :CaseLevel: Integration """ role_name = gen_string('alpha') - role = entities.Role(name=role_name).create() - dom_perm = entities.Permission().search(query={'search': 'resource_type="Domain"'}) - entities.Filter(permission=dom_perm, role=role.id).create() + role = target_sat.api.Role(name=role_name).create() + dom_perm = target_sat.api.Permission().search(query={'search': 'resource_type="Domain"'}) + target_sat.api.Filter(permission=dom_perm, role=role.id).create() cloned_role_name = gen_string('alpha') - cloned_role = entities.Role(id=role.id).clone(data={'name': cloned_role_name}) + cloned_role = target_sat.api.Role(id=role.id).clone(data={'name': cloned_role_name}) assert cloned_role_name == cloned_role['name'] - filter_cloned_id = entities.Role(id=cloned_role['id']).read().filters[0].id - filter_cloned = entities.Filter(id=filter_cloned_id).read() + filter_cloned_id = target_sat.api.Role(id=cloned_role['id']).read().filters[0].id + filter_cloned = target_sat.api.Filter(id=filter_cloned_id).read() filter_cloned.override = True filter_cloned.organization = [role_taxonomies['org']] filter_cloned.location = [role_taxonomies['loc']] filter_cloned.update(['override', 'organization', 'location']) # Updated Filter - filter_cloned = entities.Filter(id=filter_cloned_id).read() + filter_cloned = target_sat.api.Filter(id=filter_cloned_id).read() assert filter_cloned.override assert role_taxonomies['org'].id == filter_cloned.organization[0].id assert role_taxonomies['loc'].id == filter_cloned.location[0].id @pytest.mark.tier2 def test_positive_emptiness_of_filter_taxonomies_on_role_clone( - self, role_taxonomies, filter_taxonomies + self, role_taxonomies, filter_taxonomies, target_sat ): """Taxonomies of filters in cloned role are set to None for filters that are overridden in parent role @@ -667,29 +678,29 @@ def test_positive_emptiness_of_filter_taxonomies_on_role_clone( :CaseLevel: Integration """ - role = entities.Role( + role = target_sat.api.Role( name=gen_string('alpha'), organization=[role_taxonomies['org']], location=[role_taxonomies['loc']], ).create() - dom_perm = entities.Permission().search(query={'search': 'resource_type="Domain"'}) - entities.Filter( + dom_perm = target_sat.api.Permission().search(query={'search': 'resource_type="Domain"'}) + target_sat.api.Filter( permission=dom_perm, role=role.id, override=True, organization=[filter_taxonomies['org']], location=[filter_taxonomies['loc']], ).create() - cloned_role = entities.Role(id=role.id).clone(data={'name': gen_string('alpha')}) - cloned_role_filter = entities.Role(id=cloned_role['id']).read().filters[0] - filter_cloned = entities.Filter(id=cloned_role_filter.id).read() + cloned_role = target_sat.api.Role(id=role.id).clone(data={'name': gen_string('alpha')}) + cloned_role_filter = target_sat.api.Role(id=cloned_role['id']).read().filters[0] + filter_cloned = target_sat.api.Filter(id=cloned_role_filter.id).read() assert not filter_cloned.organization assert not filter_cloned.location assert filter_cloned.override @pytest.mark.tier2 def test_positive_clone_role_having_overridden_filter_with_taxonomies( - self, role_taxonomies, filter_taxonomies + self, role_taxonomies, filter_taxonomies, target_sat ): """When taxonomies assigned to cloned role, Unlimited and Override flag sets on filter for filter that is overridden in parent role @@ -710,34 +721,34 @@ def test_positive_clone_role_having_overridden_filter_with_taxonomies( :CaseLevel: Integration """ - role = entities.Role( + role = target_sat.api.Role( name=gen_string('alpha'), organization=[role_taxonomies['org']], location=[role_taxonomies['loc']], ).create() - dom_perm = entities.Permission().search(query={'search': 'resource_type="Domain"'}) - entities.Filter( + dom_perm = target_sat.api.Permission().search(query={'search': 'resource_type="Domain"'}) + target_sat.api.Filter( permission=dom_perm, role=role.id, override=True, organization=[filter_taxonomies['org']], location=[filter_taxonomies['loc']], ).create() - cloned_role = entities.Role(id=role.id).clone( + cloned_role = target_sat.api.Role(id=role.id).clone( data={ 'name': gen_string('alpha'), 'organization_ids': [role_taxonomies['org'].id], 'location_ids': [role_taxonomies['loc'].id], } ) - cloned_role_filter = entities.Role(id=cloned_role['id']).read().filters[0] - cloned_filter = entities.Filter(id=cloned_role_filter.id).read() + cloned_role_filter = target_sat.api.Role(id=cloned_role['id']).read().filters[0] + cloned_filter = target_sat.api.Filter(id=cloned_role_filter.id).read() assert cloned_filter.unlimited assert cloned_filter.override @pytest.mark.tier2 def test_positive_clone_role_having_non_overridden_filter_with_taxonomies( - self, role_taxonomies + self, role_taxonomies, target_sat ): """When taxonomies assigned to cloned role, Neither unlimited nor override sets on filter for filter that is not overridden in parent @@ -758,27 +769,29 @@ def test_positive_clone_role_having_non_overridden_filter_with_taxonomies( :CaseLevel: Integration """ - role = entities.Role( + role = target_sat.api.Role( name=gen_string('alpha'), organization=[role_taxonomies['org']], location=[role_taxonomies['loc']], ).create() - dom_perm = entities.Permission().search(query={'search': 'resource_type="Domain"'}) - entities.Filter(permission=dom_perm, role=role.id).create() - cloned_role = entities.Role(id=role.id).clone( + dom_perm = target_sat.api.Permission().search(query={'search': 'resource_type="Domain"'}) + target_sat.api.Filter(permission=dom_perm, role=role.id).create() + cloned_role = target_sat.api.Role(id=role.id).clone( data={ 'name': gen_string('alpha'), 'organization_ids': [role_taxonomies['org'].id], 'location_ids': [role_taxonomies['loc'].id], } ) - cloned_role_filter = entities.Role(id=cloned_role['id']).read().filters[0] - cloned_filter = entities.Filter(id=cloned_role_filter.id).read() + cloned_role_filter = target_sat.api.Role(id=cloned_role['id']).read().filters[0] + cloned_filter = target_sat.api.Filter(id=cloned_role_filter.id).read() assert not cloned_filter.unlimited assert not cloned_filter.override @pytest.mark.tier2 - def test_positive_clone_role_having_unlimited_filter_with_taxonomies(self, role_taxonomies): + def test_positive_clone_role_having_unlimited_filter_with_taxonomies( + self, role_taxonomies, target_sat + ): """When taxonomies assigned to cloned role, Neither unlimited nor override sets on filter for filter that is unlimited in parent role @@ -797,28 +810,28 @@ def test_positive_clone_role_having_unlimited_filter_with_taxonomies(self, role_ :CaseLevel: Integration """ - role = entities.Role( + role = target_sat.api.Role( name=gen_string('alpha'), organization=[role_taxonomies['org']], location=[role_taxonomies['loc']], ).create() - dom_perm = entities.Permission().search(query={'search': 'resource_type="Domain"'}) - entities.Filter(permission=dom_perm, role=role.id, unlimited=True).create() - cloned_role = entities.Role(id=role.id).clone( + dom_perm = target_sat.api.Permission().search(query={'search': 'resource_type="Domain"'}) + target_sat.api.Filter(permission=dom_perm, role=role.id, unlimited=True).create() + cloned_role = target_sat.api.Role(id=role.id).clone( data={ 'name': gen_string('alpha'), 'organization_ids': [role_taxonomies['org'].id], 'location_ids': [role_taxonomies['loc'].id], } ) - cloned_role_filter = entities.Role(id=cloned_role['id']).read().filters[0] - cloned_filter = entities.Filter(id=cloned_role_filter.id).read() + cloned_role_filter = target_sat.api.Role(id=cloned_role['id']).read().filters[0] + cloned_filter = target_sat.api.Filter(id=cloned_role_filter.id).read() assert not cloned_filter.unlimited assert not cloned_filter.override @pytest.mark.tier2 def test_positive_clone_role_having_overridden_filter_without_taxonomies( - self, role_taxonomies, filter_taxonomies + self, role_taxonomies, filter_taxonomies, target_sat ): # noqa """When taxonomies not assigned to cloned role, Unlimited and override flags sets on filter for filter that is overridden in parent role @@ -838,27 +851,29 @@ def test_positive_clone_role_having_overridden_filter_without_taxonomies( :CaseLevel: Integration """ - role = entities.Role( + role = target_sat.api.Role( name=gen_string('alpha'), organization=[role_taxonomies['org']], location=[role_taxonomies['loc']], ).create() - dom_perm = entities.Permission().search(query={'search': 'resource_type="Domain"'}) - entities.Filter( + dom_perm = target_sat.api.Permission().search(query={'search': 'resource_type="Domain"'}) + target_sat.api.Filter( permission=dom_perm, role=role.id, override=True, organization=[filter_taxonomies['org']], location=[filter_taxonomies['loc']], ).create() - cloned_role = entities.Role(id=role.id).clone(data={'name': gen_string('alpha')}) - cloned_role_filter = entities.Role(id=cloned_role['id']).read().filters[0] - cloned_filter = entities.Filter(id=cloned_role_filter.id).read() + cloned_role = target_sat.api.Role(id=role.id).clone(data={'name': gen_string('alpha')}) + cloned_role_filter = target_sat.api.Role(id=cloned_role['id']).read().filters[0] + cloned_filter = target_sat.api.Filter(id=cloned_role_filter.id).read() assert cloned_filter.unlimited assert cloned_filter.override @pytest.mark.tier2 - def test_positive_clone_role_without_taxonomies_non_overided_filter(self, role_taxonomies): + def test_positive_clone_role_without_taxonomies_non_overided_filter( + self, role_taxonomies, target_sat + ): """When taxonomies not assigned to cloned role, only unlimited but not override flag sets on filter for filter that is overridden in parent role @@ -881,23 +896,25 @@ def test_positive_clone_role_without_taxonomies_non_overided_filter(self, role_t :BZ: 1488908 """ - role = entities.Role( + role = target_sat.api.Role( name=gen_string('alpha'), organization=[role_taxonomies['org']], location=[role_taxonomies['loc']], ).create() - dom_perm = entities.Permission().search(query={'search': 'resource_type="Domain"'}) - entities.Filter(permission=dom_perm, role=role.id).create() - cloned_role = entities.Role(id=role.id).clone( + dom_perm = target_sat.api.Permission().search(query={'search': 'resource_type="Domain"'}) + target_sat.api.Filter(permission=dom_perm, role=role.id).create() + cloned_role = target_sat.api.Role(id=role.id).clone( data={'role': {'name': gen_string('alpha'), 'location_ids': [], 'organization_ids': []}} ) - cloned_role_filter = entities.Role(id=cloned_role['id']).read().filters[0] - cloned_filter = entities.Filter(id=cloned_role_filter.id).read() + cloned_role_filter = target_sat.api.Role(id=cloned_role['id']).read().filters[0] + cloned_filter = target_sat.api.Filter(id=cloned_role_filter.id).read() assert cloned_filter.unlimited assert not cloned_filter.override @pytest.mark.tier2 - def test_positive_clone_role_without_taxonomies_unlimited_filter(self, role_taxonomies): + def test_positive_clone_role_without_taxonomies_unlimited_filter( + self, role_taxonomies, target_sat + ): """When taxonomies not assigned to cloned role, Unlimited and override flags sets on filter for filter that is unlimited in parent role @@ -919,18 +936,18 @@ def test_positive_clone_role_without_taxonomies_unlimited_filter(self, role_taxo :BZ: 1488908 """ - role = entities.Role( + role = target_sat.api.Role( name=gen_string('alpha'), organization=[role_taxonomies['org']], location=[role_taxonomies['loc']], ).create() - dom_perm = entities.Permission().search(query={'search': 'resource_type="Domain"'}) - entities.Filter(permission=dom_perm, role=role.id, unlimited=True).create() - cloned_role = entities.Role(id=role.id).clone( + dom_perm = target_sat.api.Permission().search(query={'search': 'resource_type="Domain"'}) + target_sat.api.Filter(permission=dom_perm, role=role.id, unlimited=True).create() + cloned_role = target_sat.api.Role(id=role.id).clone( data={'role': {'name': gen_string('alpha'), 'location_ids': [], 'organization_ids': []}} ) - cloned_role_filter = entities.Role(id=cloned_role['id']).read().filters[0] - cloned_filter = entities.Filter(id=cloned_role_filter.id).read() + cloned_role_filter = target_sat.api.Role(id=cloned_role['id']).read().filters[0] + cloned_filter = target_sat.api.Filter(id=cloned_role_filter.id).read() assert cloned_filter.unlimited assert not cloned_filter.override @@ -948,7 +965,7 @@ def test_positive_user_group_users_access_as_org_admin(self, role_taxonomies, ta 2. Assign an organization A and Location A to the Org Admin role 3. Create two users without assigning roles while creating them 4. Assign Organization A and Location A to both users - 5. Create an user group with above two users + 5. Create a user group with above two users 6. Assign Org Admin role to User Group :expectedresults: Both the user should have access to the resources of @@ -961,7 +978,7 @@ def test_positive_user_group_users_access_as_org_admin(self, role_taxonomies, ta ) userone_login = gen_string('alpha') userone_pass = gen_string('alphanumeric') - user_one = entities.User( + user_one = target_sat.api.User( login=userone_login, password=userone_pass, organization=[role_taxonomies['org'].id], @@ -970,7 +987,7 @@ def test_positive_user_group_users_access_as_org_admin(self, role_taxonomies, ta assert userone_login == user_one.login usertwo_login = gen_string('alpha') usertwo_pass = gen_string('alphanumeric') - user_two = entities.User( + user_two = target_sat.api.User( login=usertwo_login, password=usertwo_pass, organization=[role_taxonomies['org'].id], @@ -978,17 +995,17 @@ def test_positive_user_group_users_access_as_org_admin(self, role_taxonomies, ta ).create() assert usertwo_login == user_two.login ug_name = gen_string('alpha') - user_group = entities.UserGroup( + user_group = target_sat.api.UserGroup( name=ug_name, role=[org_admin.id], user=[user_one.id, user_two.id] ).create() assert user_group.name == ug_name # Creating Subnets and Domains to verify if user really can access them - subnet = entities.Subnet( + subnet = target_sat.api.Subnet( name=gen_string('alpha'), organization=[role_taxonomies['org'].id], location=[role_taxonomies['loc'].id], ).create() - domain = entities.Domain( + domain = target_sat.api.Domain( name=gen_string('alpha'), organization=[role_taxonomies['org'].id], location=[role_taxonomies['loc'].id], @@ -998,13 +1015,13 @@ def test_positive_user_group_users_access_as_org_admin(self, role_taxonomies, ta auth=(login, password), url=target_sat.url, verify=settings.server.verify_ca ) try: - entities.Domain(sc).search( + target_sat.api.Domain(sc).search( query={ 'organization-id': role_taxonomies['org'].id, 'location-id': role_taxonomies['loc'].id, } ) - entities.Subnet(sc).search( + target_sat.api.Subnet(sc).search( query={ 'organization-id': role_taxonomies['org'].id, 'location-id': role_taxonomies['loc'].id, @@ -1012,8 +1029,8 @@ def test_positive_user_group_users_access_as_org_admin(self, role_taxonomies, ta ) except HTTPError as err: pytest.fail(str(err)) - assert domain.id in [dom.id for dom in entities.Domain(sc).search()] - assert subnet.id in [sub.id for sub in entities.Subnet(sc).search()] + assert domain.id in [dom.id for dom in target_sat.api.Domain(sc).search()] + assert subnet.id in [sub.id for sub in target_sat.api.Subnet(sc).search()] @pytest.mark.tier3 def test_positive_user_group_users_access_contradict_as_org_admins(self): @@ -1067,10 +1084,10 @@ def test_negative_assign_org_admin_to_user_group( org_admin = self.create_org_admin_role( orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) - user_one = self.create_simple_user(filter_taxos=filter_taxonomies) - user_two = self.create_simple_user(filter_taxos=filter_taxonomies) + user_one = self.create_simple_user(target_sat, filter_taxos=filter_taxonomies) + user_two = self.create_simple_user(target_sat, filter_taxos=filter_taxonomies) ug_name = gen_string('alpha') - user_group = entities.UserGroup( + user_group = target_sat.api.UserGroup( name=ug_name, role=[org_admin.id], user=[user_one.id, user_two.id] ).create() assert user_group.name == ug_name @@ -1078,7 +1095,7 @@ def test_negative_assign_org_admin_to_user_group( for user in [user_one, user_two]: sc = self.user_config(user, target_sat) with pytest.raises(HTTPError): - entities.Domain(sc, id=dom.id).read() + target_sat.api.Domain(sc, id=dom.id).read() @pytest.mark.tier2 def test_negative_assign_taxonomies_by_org_admin( @@ -1111,13 +1128,13 @@ def test_negative_assign_taxonomies_by_org_admin( ) # Creating resource dom_name = gen_string('alpha') - dom = entities.Domain( + dom = target_sat.api.Domain( name=dom_name, organization=[role_taxonomies['org']], location=[role_taxonomies['loc']] ).create() assert dom_name == dom.name user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') - user = entities.User( + user = target_sat.api.User( login=user_login, password=user_pass, role=[org_admin.id], @@ -1129,13 +1146,13 @@ def test_negative_assign_taxonomies_by_org_admin( auth=(user_login, user_pass), url=target_sat.url, verify=settings.server.verify_ca ) # Getting the domain from user1 - dom = entities.Domain(sc, id=dom.id).read() + dom = target_sat.api.Domain(sc, id=dom.id).read() dom.organization = [filter_taxonomies['org']] with pytest.raises(HTTPError): dom.update(['organization']) @pytest.mark.tier1 - def test_positive_remove_org_admin_role(self, role_taxonomies): + def test_positive_remove_org_admin_role(self, role_taxonomies, target_sat): """Super Admin user can remove Org Admin role :id: 03fac76c-22ac-43cf-9068-b96e255b3c3c @@ -1156,21 +1173,23 @@ def test_positive_remove_org_admin_role(self, role_taxonomies): ) user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') - user = entities.User(login=user_login, password=user_pass, role=[org_admin.id]).create() + user = target_sat.api.User( + login=user_login, password=user_pass, role=[org_admin.id] + ).create() assert user_login == user.login try: - entities.Role(id=org_admin.id).delete() + target_sat.api.Role(id=org_admin.id).delete() except HTTPError as err: pytest.fail(str(err)) # Getting updated user - user = entities.User(id=user.id).read() + user = target_sat.api.User(id=user.id).read() assert org_admin.id not in [role.id for role in user.role] @pytest.mark.tier2 def test_positive_taxonomies_control_to_superadmin_with_org_admin( self, role_taxonomies, target_sat ): - """Super Admin can access entities in taxonomies assigned to Org Admin + """Super Admin can access target_sat.api in taxonomies assigned to Org Admin :id: 37db0b40-ed35-4e70-83e8-83cff27caae2 @@ -1179,9 +1198,9 @@ def test_positive_taxonomies_control_to_superadmin_with_org_admin( 1. Create Org Admin role and assign organization A and Location A 2. Create User and assign above Org Admin role 3. Login with SuperAdmin who created the above Org Admin role and - access entities in Organization A and Location A + access target_sat.api in Organization A and Location A - :expectedresults: Super admin should be able to access the entities in + :expectedresults: Super admin should be able to access the target_sat.api in taxonomies assigned to Org Admin :CaseLevel: Integration @@ -1190,7 +1209,7 @@ def test_positive_taxonomies_control_to_superadmin_with_org_admin( sc = self.user_config(user, target_sat) # Creating resource dom_name = gen_string('alpha') - dom = entities.Domain( + dom = target_sat.api.Domain( sc, name=dom_name, organization=[role_taxonomies['org']], @@ -1198,7 +1217,7 @@ def test_positive_taxonomies_control_to_superadmin_with_org_admin( ).create() assert dom_name == dom.name try: - entities.Subnet().search( + target_sat.api.Subnet().search( query={ 'organization-id': role_taxonomies['org'].id, 'location-id': role_taxonomies['loc'].id, @@ -1211,7 +1230,7 @@ def test_positive_taxonomies_control_to_superadmin_with_org_admin( def test_positive_taxonomies_control_to_superadmin_without_org_admin( self, role_taxonomies, target_sat ): - """Super Admin can access entities in taxonomies assigned to Org Admin + """Super Admin can access target_sat.api in taxonomies assigned to Org Admin after deleting Org Admin role/user :id: 446f66a5-16e0-4298-b326-262913502955 @@ -1224,7 +1243,7 @@ def test_positive_taxonomies_control_to_superadmin_without_org_admin( 4. Login with SuperAdmin who created the above Org Admin role and access entities in Organization A and Location A - :expectedresults: Super admin should be able to access the entities in + :expectedresults: Super admin should be able to access the target_sat.api in taxonomies assigned to Org Admin after deleting Org Admin :CaseLevel: Integration @@ -1233,21 +1252,21 @@ def test_positive_taxonomies_control_to_superadmin_without_org_admin( sc = self.user_config(user, target_sat) # Creating resource dom_name = gen_string('alpha') - dom = entities.Domain( + dom = target_sat.api.Domain( sc, name=dom_name, organization=[role_taxonomies['org']], location=[role_taxonomies['loc']], ).create() assert dom_name == dom.name - user_role = entities.Role(id=user.role[0].id).read() - entities.Role(id=user_role.id).delete() - entities.User(id=user.id).delete() + user_role = target_sat.api.Role(id=user.role[0].id).read() + target_sat.api.Role(id=user_role.id).delete() + target_sat.api.User(id=user.id).delete() with pytest.raises(HTTPError): user_role.read() user.read() try: - entities.Domain().search( + target_sat.api.Domain().search( query={ 'organization-id': role_taxonomies['org'].id, 'location-id': role_taxonomies['loc'].id, @@ -1259,7 +1278,7 @@ def test_positive_taxonomies_control_to_superadmin_without_org_admin( @pytest.mark.tier1 @pytest.mark.upgrade def test_negative_create_roles_by_org_admin(self, role_taxonomies, target_sat): - """Org Admin doesnt have permissions to create new roles + """Org Admin doesn't have permissions to create new roles :id: 806ecc16-0dc7-405b-90d3-0584eced27a3 @@ -1278,7 +1297,7 @@ def test_negative_create_roles_by_org_admin(self, role_taxonomies, target_sat): ) user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') - user = entities.User( + user = target_sat.api.User( login=user_login, password=user_pass, role=[org_admin.id], @@ -1291,7 +1310,7 @@ def test_negative_create_roles_by_org_admin(self, role_taxonomies, target_sat): ) role_name = gen_string('alpha') with pytest.raises(HTTPError): - entities.Role( + target_sat.api.Role( sc, name=role_name, organization=[role_taxonomies['org']], @@ -1315,9 +1334,11 @@ def test_negative_modify_roles_by_org_admin(self, role_taxonomies, target_sat): existing roles """ user = self.create_org_admin_user(role_taxos=role_taxonomies, user_taxos=role_taxonomies) - test_role = entities.Role().create() + test_role = target_sat.api.Role().create() sc = self.user_config(user, target_sat) - test_role = entities.Role(sc, id=test_role.id).read() + test_role = target_sat.api.Role(sc, id=test_role.id).read() + test_role.organization = [role_taxonomies['org']] + test_role.location = [role_taxonomies['loc']] with pytest.raises(HTTPError): test_role.organization = [role_taxonomies['org']] test_role.location = [role_taxonomies['loc']] @@ -1345,7 +1366,7 @@ def test_negative_admin_permissions_to_org_admin(self, role_taxonomies, target_s ) user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') - user = entities.User( + user = target_sat.api.User( login=user_login, password=user_pass, role=[org_admin.id], @@ -1357,7 +1378,7 @@ def test_negative_admin_permissions_to_org_admin(self, role_taxonomies, target_s auth=(user_login, user_pass), url=target_sat.url, verify=settings.server.verify_ca ) with pytest.raises(HTTPError): - entities.User(sc, id=1).read() + target_sat.api.User(sc, id=1).read() @pytest.mark.tier2 @pytest.mark.upgrade @@ -1392,7 +1413,7 @@ def test_positive_create_user_by_org_admin(self, role_taxonomies, target_sat): ) user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') - user = entities.User( + user = target_sat.api.User( login=user_login, password=user_pass, role=[org_admin.id], @@ -1405,7 +1426,7 @@ def test_positive_create_user_by_org_admin(self, role_taxonomies, target_sat): ) user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') - user = entities.User( + user = target_sat.api.User( sc_user, login=user_login, password=user_pass, @@ -1417,7 +1438,7 @@ def test_positive_create_user_by_org_admin(self, role_taxonomies, target_sat): assert org_admin.id == user.role[0].id if not is_open('BZ:1825698'): name = gen_string('alphanumeric') - location = entities.Location(sc_user, name=name).create() + location = target_sat.api.Location(sc_user, name=name).create() assert location.name == name @pytest.mark.tier2 @@ -1445,7 +1466,7 @@ def test_positive_access_users_inside_org_admin_taxonomies(self, role_taxonomies test_user = self.create_simple_user(filter_taxos=role_taxonomies) sc = self.user_config(user, target_sat) try: - entities.User(sc, id=test_user.id).read() + target_sat.api.User(sc, id=test_user.id).read() except HTTPError as err: pytest.fail(str(err)) @@ -1472,7 +1493,7 @@ def test_positive_create_nested_location(self, role_taxonomies, target_sat): """ user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') - user = entities.User( + user = target_sat.api.User( login=user_login, password=user_pass, organization=[role_taxonomies['org']], @@ -1487,7 +1508,7 @@ def test_positive_create_nested_location(self, role_taxonomies, target_sat): auth=(user_login, user_pass), url=target_sat.url, verify=settings.server.verify_ca ) name = gen_string('alphanumeric') - location = entities.Location(sc, name=name, parent=role_taxonomies['loc'].id).create() + location = target_sat.api.Location(sc, name=name, parent=role_taxonomies['loc'].id).create() assert location.name == name @pytest.mark.tier2 @@ -1517,7 +1538,7 @@ def test_negative_access_users_outside_org_admin_taxonomies( test_user = self.create_simple_user(filter_taxos=filter_taxonomies) sc = self.user_config(user, target_sat) with pytest.raises(HTTPError): - entities.User(sc, id=test_user.id).read() + target_sat.api.User(sc, id=test_user.id).read() @pytest.mark.tier1 def test_negative_create_taxonomies_by_org_admin(self, role_taxonomies, target_sat): @@ -1541,7 +1562,7 @@ def test_negative_create_taxonomies_by_org_admin(self, role_taxonomies, target_s org_admin = self.create_org_admin_role(orgs=[role_taxonomies['org'].id]) user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') - user = entities.User( + user = target_sat.api.User( login=user_login, password=user_pass, role=[org_admin.id], @@ -1553,11 +1574,11 @@ def test_negative_create_taxonomies_by_org_admin(self, role_taxonomies, target_s auth=(user_login, user_pass), url=target_sat.url, verify=settings.server.verify_ca ) with pytest.raises(HTTPError): - entities.Organization(sc, name=gen_string('alpha')).create() + target_sat.api.Organization(sc, name=gen_string('alpha')).create() if not is_open("BZ:1825698"): try: loc_name = gen_string('alpha') - loc = entities.Location(sc, name=loc_name).create() + loc = target_sat.api.Location(sc, name=loc_name).create() except HTTPError as err: pytest.fail(str(err)) assert loc_name == loc.name @@ -1567,7 +1588,7 @@ def test_negative_create_taxonomies_by_org_admin(self, role_taxonomies, target_s def test_positive_access_all_global_entities_by_org_admin( self, role_taxonomies, filter_taxonomies, target_sat ): - """Org Admin can access all global entities in any taxonomies + """Org Admin can access all global target_sat.api in any taxonomies regardless of its own assigned taxonomies :id: add5feb3-7a3f-45a1-a633-49f1141b029b @@ -1578,16 +1599,16 @@ def test_positive_access_all_global_entities_by_org_admin( 2. Create new user and assign Org A,B and Location A,B 3. Assign Org Admin role to User 4. Login with Org Admin user - 5. Attempt to create all the global entities in org B and Loc B - e.g Architectures, Operating System + 5. Attempt to create all the global target_sat.api in org B and Loc B + e.g. Architectures, Operating System :expectedresults: Org Admin should have access to all the global - entities in any taxonomies + target_sat.api in any taxonomies """ org_admin = self.create_org_admin_role(orgs=[role_taxonomies['org'].id]) user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') - user = entities.User( + user = target_sat.api.User( login=user_login, password=user_pass, role=[org_admin.id], @@ -1600,22 +1621,24 @@ def test_positive_access_all_global_entities_by_org_admin( ) try: for entity in [ - entities.Architecture, - entities.Audit, - entities.Bookmark, - entities.CommonParameter, - entities.LibvirtComputeResource, - entities.OVirtComputeResource, - entities.VMWareComputeResource, - entities.Errata, - entities.OperatingSystem, + target_sat.api.Architecture, + target_sat.api.Audit, + target_sat.api.Bookmark, + target_sat.api.CommonParameter, + target_sat.api.LibvirtComputeResource, + target_sat.api.OVirtComputeResource, + target_sat.api.VMWareComputeResource, + target_sat.api.Errata, + target_sat.api.OperatingSystem, ]: entity(sc).search() except HTTPError as err: pytest.fail(str(err)) @pytest.mark.tier3 - def test_negative_access_entities_from_ldap_org_admin(self, role_taxonomies, create_ldap): + def test_negative_access_entities_from_ldap_org_admin( + self, role_taxonomies, create_ldap, target_sat + ): """LDAP User can not access resources in taxonomies assigned to role if its own taxonomies are not same as its role @@ -1649,17 +1672,19 @@ def test_negative_access_entities_from_ldap_org_admin(self, role_taxonomies, cre verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): - entities.Architecture(sc).search() - user = entities.User().search(query={'search': f"login={create_ldap['ldap_user_name']}"})[0] - user.role = [entities.Role(id=org_admin.id).read()] + target_sat.api.Architecture(sc).search() + user = target_sat.api.User().search( + query={'search': f"login={create_ldap['ldap_user_name']}"} + )[0] + user.role = [target_sat.api.Role(id=org_admin.id).read()] user.update(['role']) # Trying to access the domain resource created in org admin role with pytest.raises(HTTPError): - entities.Domain(sc, id=domain.id).read() + target_sat.api.Domain(sc, id=domain.id).read() @pytest.mark.tier3 def test_negative_access_entities_from_ldap_user( - self, role_taxonomies, create_ldap, module_location, module_org + self, role_taxonomies, create_ldap, module_location, module_org, target_sat ): """LDAP User can not access resources within its own taxonomies if assigned role does not have permissions for same taxonomies @@ -1692,16 +1717,20 @@ def test_negative_access_entities_from_ldap_user( verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): - entities.Architecture(sc).search() - user = entities.User().search(query={'search': f"login={create_ldap['ldap_user_name']}"})[0] - user.role = [entities.Role(id=org_admin.id).read()] + target_sat.api.Architecture(sc).search() + user = target_sat.api.User().search( + query={'search': f"login={create_ldap['ldap_user_name']}"} + )[0] + user.role = [target_sat.api.Role(id=org_admin.id).read()] user.update(['role']) # Trying to access the Domain resource with pytest.raises(HTTPError): - entities.Domain(sc, id=domain.id).read() + target_sat.api.Domain(sc, id=domain.id).read() @pytest.mark.tier3 - def test_positive_assign_org_admin_to_ldap_user_group(self, role_taxonomies, create_ldap): + def test_positive_assign_org_admin_to_ldap_user_group( + self, role_taxonomies, create_ldap, target_sat + ): """Users in LDAP usergroup can access to the resources in taxonomies if the taxonomies of Org Admin role are same @@ -1735,7 +1764,7 @@ def test_positive_assign_org_admin_to_ldap_user_group(self, role_taxonomies, cre locs=[create_ldap['authsource'].location[0].id], ) users = [ - entities.User( + target_sat.api.User( login=gen_string("alpha"), password=password, organization=create_ldap['authsource'].organization, @@ -1743,9 +1772,11 @@ def test_positive_assign_org_admin_to_ldap_user_group(self, role_taxonomies, cre ).create() for _ in range(2) ] - user_group = entities.UserGroup(name=group_name, user=users, role=[org_admin]).create() + user_group = target_sat.api.UserGroup( + name=group_name, user=users, role=[org_admin] + ).create() # Adding LDAP authsource to the usergroup - entities.ExternalUserGroup( + target_sat.api.ExternalUserGroup( name='foobargroup', usergroup=user_group, auth_source=create_ldap['authsource'] ).create() @@ -1756,10 +1787,12 @@ def test_positive_assign_org_admin_to_ldap_user_group(self, role_taxonomies, cre verify=settings.server.verify_ca, ) # Accessing the Domain resource - entities.Domain(sc, id=domain.id).read() + target_sat.api.Domain(sc, id=domain.id).read() @pytest.mark.tier3 - def test_negative_assign_org_admin_to_ldap_user_group(self, create_ldap, role_taxonomies): + def test_negative_assign_org_admin_to_ldap_user_group( + self, create_ldap, role_taxonomies, target_sat + ): """Users in LDAP usergroup can not have access to the resources in taxonomies if the taxonomies of Org Admin role is not same @@ -1791,7 +1824,7 @@ def test_negative_assign_org_admin_to_ldap_user_group(self, create_ldap, role_ta orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) users = [ - entities.User( + target_sat.api.User( login=gen_string("alpha"), password=password, organization=create_ldap['authsource'].organization, @@ -1799,9 +1832,11 @@ def test_negative_assign_org_admin_to_ldap_user_group(self, create_ldap, role_ta ).create() for _ in range(2) ] - user_group = entities.UserGroup(name=group_name, user=users, role=[org_admin]).create() + user_group = target_sat.api.UserGroup( + name=group_name, user=users, role=[org_admin] + ).create() # Adding LDAP authsource to usergroup - entities.ExternalUserGroup( + target_sat.api.ExternalUserGroup( name='foobargroup', usergroup=user_group, auth_source=create_ldap['authsource'] ).create() @@ -1813,7 +1848,7 @@ def test_negative_assign_org_admin_to_ldap_user_group(self, create_ldap, role_ta ) # Trying to access the Domain resource with pytest.raises(HTTPError): - entities.Domain(sc, id=domain.id).read() + target_sat.api.Domain(sc, id=domain.id).read() class TestRoleSearchFilter: diff --git a/tests/foreman/api/test_settings.py b/tests/foreman/api/test_settings.py index b5d0542d35d..b4fe49c23c1 100644 --- a/tests/foreman/api/test_settings.py +++ b/tests/foreman/api/test_settings.py @@ -18,7 +18,6 @@ """ import random -from nailgun import entities import pytest from requests.exceptions import HTTPError @@ -188,7 +187,7 @@ def test_negative_discover_host_with_invalid_prefix(): @pytest.mark.tier2 @pytest.mark.parametrize('download_policy', ["immediate", "on_demand"]) @pytest.mark.parametrize('setting_update', ['default_download_policy'], indirect=True) -def test_positive_custom_repo_download_policy(setting_update, download_policy): +def test_positive_custom_repo_download_policy(setting_update, download_policy, target_sat): """Check the set custom repository download policy for newly created custom repository. :id: d5150cce-ba85-4ea0-a8d1-6a54d0d29571 @@ -209,11 +208,11 @@ def test_positive_custom_repo_download_policy(setting_update, download_policy): :CaseLevel: Acceptance """ - org = entities.Organization().create() - prod = entities.Product(organization=org).create() + org = target_sat.api.Organization().create() + prod = target_sat.api.Product(organization=org).create() setting_update.value = download_policy setting_update.update({'value'}) - repo = entities.Repository(product=prod, content_type='yum', organization=org).create() + repo = target_sat.api.Repository(product=prod, content_type='yum', organization=org).create() assert repo.download_policy == download_policy repo.delete() prod.delete() diff --git a/tests/foreman/api/test_subnet.py b/tests/foreman/api/test_subnet.py index c5188d12808..75d7bff0b9e 100644 --- a/tests/foreman/api/test_subnet.py +++ b/tests/foreman/api/test_subnet.py @@ -23,7 +23,6 @@ """ import re -from nailgun import entities import pytest from requests.exceptions import HTTPError @@ -36,7 +35,7 @@ @pytest.mark.tier1 -def test_positive_create_with_parameter(): +def test_positive_create_with_parameter(target_sat): """Subnet can be created along with parameters :id: ec581cb5-8c48-4b9c-b536-302c0b7ec30f @@ -47,14 +46,14 @@ def test_positive_create_with_parameter(): :expectedresults: The Subnet is created with parameter """ parameter = [{'name': gen_string('alpha'), 'value': gen_string('alpha')}] - subnet = entities.Subnet(subnet_parameters_attributes=parameter).create() + subnet = target_sat.api.Subnet(subnet_parameters_attributes=parameter).create() assert subnet.subnet_parameters_attributes[0]['name'] == parameter[0]['name'] assert subnet.subnet_parameters_attributes[0]['value'] == parameter[0]['value'] @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(generate_strings_list())) -def test_positive_add_parameter(name): +def test_positive_add_parameter(name, target_sat): """Parameters can be created in subnet :id: c1dae6f4-45b1-45db-8529-d7918e41a99b @@ -70,15 +69,15 @@ def test_positive_add_parameter(name): :CaseImportance: Medium """ - subnet = entities.Subnet().create() + subnet = target_sat.api.Subnet().create() value = gen_string('utf8') - subnet_param = entities.Parameter(subnet=subnet.id, name=name, value=value).create() + subnet_param = target_sat.api.Parameter(subnet=subnet.id, name=name, value=value).create() assert subnet_param.name == name assert subnet_param.value == value @pytest.mark.tier1 -def test_positive_add_parameter_with_values_and_separator(): +def test_positive_add_parameter_with_values_and_separator(target_sat): """Subnet parameters can be created with values separated by comma :id: b3de6f96-7c39-4c44-b91c-a6d141f5dd6a @@ -94,10 +93,10 @@ def test_positive_add_parameter_with_values_and_separator(): :CaseImportance: Low """ - subnet = entities.Subnet().create() + subnet = target_sat.api.Subnet().create() name = gen_string('alpha') values = ', '.join(generate_strings_list()) - subnet_param = entities.Parameter(name=name, subnet=subnet.id, value=values).create() + subnet_param = target_sat.api.Parameter(name=name, subnet=subnet.id, value=values).create() assert subnet_param.name == name assert subnet_param.value == values @@ -106,7 +105,7 @@ def test_positive_add_parameter_with_values_and_separator(): @pytest.mark.parametrize( 'separator', **parametrized({'comma': ',', 'slash': '/', 'dash': '-', 'pipe': '|'}) ) -def test_positive_create_with_parameter_and_valid_separator(separator): +def test_positive_create_with_parameter_and_valid_separator(separator, target_sat): """Subnet parameters can be created with name with valid separators :id: d1e2d75a-a1e8-4767-93f1-0bb1b75e10a0 @@ -124,16 +123,16 @@ def test_positive_create_with_parameter_and_valid_separator(separator): :CaseImportance: Low """ name = f'{separator}'.join(generate_strings_list()) - subnet = entities.Subnet().create() + subnet = target_sat.api.Subnet().create() value = gen_string('utf8') - subnet_param = entities.Parameter(name=name, subnet=subnet.id, value=value).create() + subnet_param = target_sat.api.Parameter(name=name, subnet=subnet.id, value=value).create() assert subnet_param.name == name assert subnet_param.value == value @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(invalid_values_list() + ['name with space'])) -def test_negative_create_with_parameter_and_invalid_separator(name): +def test_negative_create_with_parameter_and_invalid_separator(name, target_sat): """Subnet parameters can not be created with name with invalid separators @@ -155,13 +154,13 @@ def test_negative_create_with_parameter_and_invalid_separator(name): :CaseImportance: Low """ - subnet = entities.Subnet().create() + subnet = target_sat.api.Subnet().create() with pytest.raises(HTTPError): - entities.Parameter(name=name, subnet=subnet.id).create() + target_sat.api.Parameter(name=name, subnet=subnet.id).create() @pytest.mark.tier1 -def test_negative_create_with_duplicated_parameters(): +def test_negative_create_with_duplicated_parameters(target_sat): """Attempt to create multiple parameters with same key name for the same subnet @@ -180,10 +179,10 @@ def test_negative_create_with_duplicated_parameters(): :CaseImportance: Low """ - subnet = entities.Subnet().create() - entities.Parameter(name='duplicateParameter', subnet=subnet.id).create() + subnet = target_sat.api.Subnet().create() + target_sat.api.Parameter(name='duplicateParameter', subnet=subnet.id).create() with pytest.raises(HTTPError) as context: - entities.Parameter(name='duplicateParameter', subnet=subnet.id).create() + target_sat.api.Parameter(name='duplicateParameter', subnet=subnet.id).create() assert re.search("Name has already been taken", context.value.response.text) @@ -244,7 +243,7 @@ def test_positive_subnet_parameters_override_from_host(): @pytest.mark.tier3 -def test_positive_subnet_parameters_override_impact_on_subnet(): +def test_positive_subnet_parameters_override_impact_on_subnet(target_sat): """Override subnet parameter from host impact on subnet parameter :id: 6fe963ed-93a3-496e-bfd9-599bf91a61f3 @@ -266,15 +265,15 @@ def test_positive_subnet_parameters_override_impact_on_subnet(): # Create subnet with valid parameters parameter = [{'name': gen_string('alpha'), 'value': gen_string('alpha')}] - org = entities.Organization().create() - loc = entities.Location(organization=[org]).create() - org_subnet = entities.Subnet( + org = target_sat.api.Organization().create() + loc = target_sat.api.Location(organization=[org]).create() + org_subnet = target_sat.api.Subnet( location=[loc], organization=[org], subnet_parameters_attributes=parameter ).create() assert org_subnet.subnet_parameters_attributes[0]['name'] == parameter[0]['name'] assert org_subnet.subnet_parameters_attributes[0]['value'] == parameter[0]['value'] # Create host with above subnet - host = entities.Host(location=loc, organization=org, subnet=org_subnet).create() + host = target_sat.api.Host(location=loc, organization=org, subnet=org_subnet).create() assert host.subnet.read().name == org_subnet.name parameter_new_value = [ { @@ -293,7 +292,7 @@ def test_positive_subnet_parameters_override_impact_on_subnet(): @pytest.mark.tier1 -def test_positive_update_parameter(): +def test_positive_update_parameter(target_sat): """Subnet parameter can be updated :id: 8c389c3f-60ef-4856-b8fc-c5b066c67a2f @@ -309,7 +308,7 @@ def test_positive_update_parameter(): :CaseImportance: Medium """ parameter = [{'name': gen_string('alpha'), 'value': gen_string('alpha')}] - subnet = entities.Subnet(subnet_parameters_attributes=parameter).create() + subnet = target_sat.api.Subnet(subnet_parameters_attributes=parameter).create() update_parameter = [{'name': gen_string('utf8'), 'value': gen_string('utf8')}] subnet.subnet_parameters_attributes = update_parameter up_subnet = subnet.update(['subnet_parameters_attributes']) @@ -319,7 +318,7 @@ def test_positive_update_parameter(): @pytest.mark.tier1 @pytest.mark.parametrize('new_name', **parametrized(invalid_values_list() + ['name with space'])) -def test_negative_update_parameter(new_name): +def test_negative_update_parameter(new_name, target_sat): """Subnet parameter can not be updated with invalid names :id: fcdbad13-ad96-4152-8e20-e023d61a2853 @@ -339,8 +338,8 @@ def test_negative_update_parameter(new_name): :CaseImportance: Medium """ - subnet = entities.Subnet().create() - sub_param = entities.Parameter( + subnet = target_sat.api.Subnet().create() + sub_param = target_sat.api.Parameter( name=gen_string('utf8'), subnet=subnet.id, value=gen_string('utf8') ).create() with pytest.raises(HTTPError): @@ -377,7 +376,7 @@ def test_positive_update_subnet_parameter_host_impact(): @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_delete_subnet_parameter(): +def test_positive_delete_subnet_parameter(target_sat): """Subnet parameter can be deleted :id: 972b66ec-d506-4fcb-9786-c62f2f79ac1a @@ -389,8 +388,8 @@ def test_positive_delete_subnet_parameter(): :expectedresults: The parameter should be deleted from subnet """ - subnet = entities.Subnet().create() - sub_param = entities.Parameter(subnet=subnet.id).create() + subnet = target_sat.api.Subnet().create() + sub_param = target_sat.api.Parameter(subnet=subnet.id).create() sub_param.delete() with pytest.raises(HTTPError): sub_param.read() @@ -452,7 +451,7 @@ def test_positive_delete_subnet_overridden_parameter_host_impact(): @pytest.mark.tier1 -def test_positive_list_parameters(): +def test_positive_list_parameters(target_sat): """Satellite lists all the subnet parameters :id: ce86d531-bf6b-45a9-81e3-67e1b3398f76 @@ -467,9 +466,9 @@ def test_positive_list_parameters(): parameters """ parameter = {'name': gen_string('alpha'), 'value': gen_string('alpha')} - org = entities.Organization().create() - loc = entities.Location(organization=[org]).create() - org_subnet = entities.Subnet( + org = target_sat.api.Organization().create() + loc = target_sat.api.Location(organization=[org]).create() + org_subnet = target_sat.api.Subnet( location=[loc], organization=[org], ipam='DHCP', @@ -478,10 +477,10 @@ def test_positive_list_parameters(): ).create() assert org_subnet.subnet_parameters_attributes[0]['name'] == parameter['name'] assert org_subnet.subnet_parameters_attributes[0]['value'] == parameter['value'] - sub_param = entities.Parameter( + sub_param = target_sat.api.Parameter( name=gen_string('alpha'), subnet=org_subnet.id, value=gen_string('alpha') ).create() - org_subnet = entities.Subnet(id=org_subnet.id).read() + org_subnet = target_sat.api.Subnet(id=org_subnet.id).read() params_list = { param['name']: param['value'] for param in org_subnet.subnet_parameters_attributes diff --git a/tests/foreman/api/test_subscription.py b/tests/foreman/api/test_subscription.py index 377d43555dd..b27488cd5f7 100644 --- a/tests/foreman/api/test_subscription.py +++ b/tests/foreman/api/test_subscription.py @@ -21,7 +21,6 @@ :Upstream: No """ from fauxfactory import gen_string -from nailgun import entities from nailgun.config import ServerConfig from nailgun.entity_mixins import TaskFailedError import pytest @@ -44,28 +43,30 @@ def rh_repo(module_sca_manifest_org, module_target_sat): reposet=REPOSET['rhst7'], releasever=None, ) - rh_repo = entities.Repository(id=rh_repo_id).read() + rh_repo = module_target_sat.api.Repository(id=rh_repo_id).read() rh_repo.sync() return rh_repo @pytest.fixture(scope='module') -def custom_repo(rh_repo, module_sca_manifest_org): - custom_repo = entities.Repository( - product=entities.Product(organization=module_sca_manifest_org).create(), +def custom_repo(rh_repo, module_sca_manifest_org, module_target_sat): + custom_repo = module_target_sat.api.Repository( + product=module_target_sat.api.Product(organization=module_sca_manifest_org).create(), ).create() custom_repo.sync() return custom_repo @pytest.fixture(scope='module') -def module_ak(module_sca_manifest_org, rh_repo, custom_repo): +def module_ak(module_sca_manifest_org, rh_repo, custom_repo, module_target_sat): """rh_repo and custom_repo are included here to ensure their execution before the AK""" - module_ak = entities.ActivationKey( + module_ak = module_target_sat.api.ActivationKey( content_view=module_sca_manifest_org.default_content_view, max_hosts=100, organization=module_sca_manifest_org, - environment=entities.LifecycleEnvironment(id=module_sca_manifest_org.library.id), + environment=module_target_sat.api.LifecycleEnvironment( + id=module_sca_manifest_org.library.id + ), auto_attach=True, ).create() return module_ak @@ -82,12 +83,12 @@ def test_positive_create(module_entitlement_manifest, module_target_sat): :CaseImportance: Critical """ - org = entities.Organization().create() + org = module_target_sat.api.Organization().create() module_target_sat.upload_manifest(org.id, module_entitlement_manifest.content) @pytest.mark.tier1 -def test_positive_refresh(function_entitlement_manifest_org, request): +def test_positive_refresh(function_entitlement_manifest_org, request, target_sat): """Upload a manifest and refresh it afterwards. :id: cd195db6-e81b-42cb-a28d-ec0eb8a53341 @@ -97,7 +98,7 @@ def test_positive_refresh(function_entitlement_manifest_org, request): :CaseImportance: Critical """ org = function_entitlement_manifest_org - sub = entities.Subscription(organization=org) + sub = target_sat.api.Subscription(organization=org) request.addfinalizer(lambda: sub.delete_manifest(data={'organization_id': org.id})) sub.refresh_manifest(data={'organization_id': org.id}) assert sub.search() @@ -120,9 +121,9 @@ def test_positive_create_after_refresh( :CaseImportance: Critical """ - org_sub = entities.Subscription(organization=function_entitlement_manifest_org) - new_org = entities.Organization().create() - new_org_sub = entities.Subscription(organization=new_org) + org_sub = target_sat.api.Subscription(organization=function_entitlement_manifest_org) + new_org = target_sat.api.Organization().create() + new_org_sub = target_sat.api.Subscription(organization=new_org) try: org_sub.refresh_manifest(data={'organization_id': function_entitlement_manifest_org.id}) assert org_sub.search() @@ -133,7 +134,7 @@ def test_positive_create_after_refresh( @pytest.mark.tier1 -def test_positive_delete(function_entitlement_manifest_org): +def test_positive_delete(function_entitlement_manifest_org, target_sat): """Delete an Uploaded manifest. :id: 4c21c7c9-2b26-4a65-a304-b978d5ba34fc @@ -142,7 +143,7 @@ def test_positive_delete(function_entitlement_manifest_org): :CaseImportance: Critical """ - sub = entities.Subscription(organization=function_entitlement_manifest_org) + sub = target_sat.api.Subscription(organization=function_entitlement_manifest_org) assert sub.search() sub.delete_manifest(data={'organization_id': function_entitlement_manifest_org.id}) assert len(sub.search()) == 0 @@ -157,12 +158,12 @@ def test_negative_upload(function_entitlement_manifest, target_sat): :expectedresults: The manifest is not uploaded to the second organization. """ - orgs = [entities.Organization().create() for _ in range(2)] + orgs = [target_sat.api.Organization().create() for _ in range(2)] with function_entitlement_manifest as manifest: target_sat.upload_manifest(orgs[0].id, manifest.content) with pytest.raises(TaskFailedError): target_sat.upload_manifest(orgs[1].id, manifest.content) - assert len(entities.Subscription(organization=orgs[1]).search()) == 0 + assert len(target_sat.api.Subscription(organization=orgs[1]).search()) == 0 @pytest.mark.tier2 @@ -208,11 +209,11 @@ def test_positive_delete_manifest_as_another_user( ) # use the first admin to upload a manifest with function_entitlement_manifest as manifest: - entities.Subscription(sc1, organization=function_org).upload( + target_sat.api.Subscription(sc1, organization=function_org).upload( data={'organization_id': function_org.id}, files={'content': manifest.content} ) # try to search and delete the manifest with another admin - entities.Subscription(sc2, organization=function_org).delete_manifest( + target_sat.api.Subscription(sc2, organization=function_org).delete_manifest( data={'organization_id': function_org.id} ) assert len(Subscription.list({'organization-id': function_org.id})) == 0 @@ -238,7 +239,7 @@ def test_positive_subscription_status_disabled( rhel_contenthost.install_katello_ca(target_sat) rhel_contenthost.register_contenthost(module_sca_manifest_org.label, module_ak.name) assert rhel_contenthost.subscribed - host_content = entities.Host(id=rhel_contenthost.nailgun_host.id).read_raw().content + host_content = target_sat.api.Host(id=rhel_contenthost.nailgun_host.id).read_raw().content assert 'Simple Content Access' in str(host_content) @@ -266,9 +267,12 @@ def test_sca_end_to_end( rhel7_contenthost.register_contenthost(module_sca_manifest_org.label, module_ak.name) assert rhel7_contenthost.subscribed # Check to see if Organization is in SCA Mode - assert entities.Organization(id=module_sca_manifest_org.id).read().simple_content_access is True + assert ( + target_sat.api.Organization(id=module_sca_manifest_org.id).read().simple_content_access + is True + ) # Verify that you cannot attach a subscription to an activation key in SCA Mode - subscription = entities.Subscription(organization=module_sca_manifest_org).search( + subscription = target_sat.api.Subscription(organization=module_sca_manifest_org).search( query={'search': f'name="{DEFAULT_SUBSCRIPTION_NAME}"'} )[0] with pytest.raises(HTTPError) as ak_context: @@ -276,12 +280,12 @@ def test_sca_end_to_end( assert 'Simple Content Access' in ak_context.value.response.text # Verify that you cannot attach a subscription to an Host in SCA Mode with pytest.raises(HTTPError) as host_context: - entities.HostSubscription(host=rhel7_contenthost.nailgun_host.id).add_subscriptions( + target_sat.api.HostSubscription(host=rhel7_contenthost.nailgun_host.id).add_subscriptions( data={'subscriptions': [{'id': subscription.id, 'quantity': 1}]} ) assert 'Simple Content Access' in host_context.value.response.text # Create a content view with repos and check to see that the client has access - content_view = entities.ContentView(organization=module_sca_manifest_org).create() + content_view = target_sat.api.ContentView(organization=module_sca_manifest_org).create() content_view.repository = [rh_repo, custom_repo] content_view.update(['repository']) content_view.publish() @@ -327,34 +331,34 @@ def test_positive_candlepin_events_processed_by_stomp( :CaseImportance: High """ - repo = entities.Repository( - product=entities.Product(organization=function_org).create() + repo = target_sat.api.Repository( + product=target_sat.api.Product(organization=function_org).create() ).create() repo.sync() - ak = entities.ActivationKey( + ak = target_sat.api.ActivationKey( content_view=function_org.default_content_view, max_hosts=100, organization=function_org, - environment=entities.LifecycleEnvironment(id=function_org.library.id), + environment=target_sat.api.LifecycleEnvironment(id=function_org.library.id), auto_attach=True, ).create() rhel7_contenthost.install_katello_ca(target_sat) rhel7_contenthost.register_contenthost(function_org.name, ak.name) - host = entities.Host().search(query={'search': f'name={rhel7_contenthost.hostname}'}) + host = target_sat.api.Host().search(query={'search': f'name={rhel7_contenthost.hostname}'}) host_id = host[0].id - host_content = entities.Host(id=host_id).read_json() + host_content = target_sat.api.Host(id=host_id).read_json() assert host_content['subscription_status'] == 2 with function_entitlement_manifest as manifest: target_sat.upload_manifest(function_org.id, manifest.content) - subscription = entities.Subscription(organization=function_org).search( + subscription = target_sat.api.Subscription(organization=function_org).search( query={'search': f'name="{DEFAULT_SUBSCRIPTION_NAME}"'} )[0] - entities.HostSubscription(host=host_id).add_subscriptions( + target_sat.api.HostSubscription(host=host_id).add_subscriptions( data={'subscriptions': [{'id': subscription.cp_id, 'quantity': 1}]} ) - host_content = entities.Host(id=host_id).read_json() + host_content = target_sat.api.Host(id=host_id).read_json() assert host_content['subscription_status'] == 0 - response = entities.Ping().search_json()['services']['candlepin_events'] + response = target_sat.api.Ping().search_json()['services']['candlepin_events'] assert response['status'] == 'ok' assert '0 Failed' in response['message'] @@ -386,11 +390,11 @@ def test_positive_expired_SCA_cert_handling(module_sca_manifest_org, rhel7_conte :CaseImportance: High """ - ak = entities.ActivationKey( + ak = target_sat.api.ActivationKey( content_view=module_sca_manifest_org.default_content_view, max_hosts=100, organization=module_sca_manifest_org, - environment=entities.LifecycleEnvironment(id=module_sca_manifest_org.library.id), + environment=target_sat.api.LifecycleEnvironment(id=module_sca_manifest_org.library.id), auto_attach=True, ).create() # registering the content host with no content enabled/synced in the org @@ -411,7 +415,7 @@ def test_positive_expired_SCA_cert_handling(module_sca_manifest_org, rhel7_conte reposet=REPOSET['rhst7'], releasever=None, ) - rh_repo = entities.Repository(id=rh_repo_id).read() + rh_repo = target_sat.api.Repository(id=rh_repo_id).read() rh_repo.sync() # re-registering the host should test whether Candlepin gracefully handles # registration of a host with an expired SCA cert diff --git a/tests/foreman/api/test_syncplan.py b/tests/foreman/api/test_syncplan.py index 9653322bd55..566d06d692a 100644 --- a/tests/foreman/api/test_syncplan.py +++ b/tests/foreman/api/test_syncplan.py @@ -24,7 +24,7 @@ from time import sleep from fauxfactory import gen_choice, gen_string -from nailgun import client, entities +from nailgun import client import pytest from requests.exceptions import HTTPError @@ -101,7 +101,7 @@ def validate_repo_content(repo, content_types, after_sync=True): @pytest.mark.tier1 -def test_positive_get_routes(): +def test_positive_get_routes(target_sat): """Issue an HTTP GET response to both available routes. :id: 9e40ea7f-71ea-4ced-94ba-cde03620c654 @@ -112,8 +112,8 @@ def test_positive_get_routes(): :CaseImportance: Critical """ - org = entities.Organization().create() - entities.SyncPlan(organization=org).create() + org = target_sat.api.Organization().create() + target_sat.api.SyncPlan(organization=org).create() response1 = client.get( f'{get_url()}/katello/api/v2/sync_plans', auth=get_credentials(), @@ -144,7 +144,7 @@ def test_positive_create_enabled_disabled(module_org, enabled, request, target_s :CaseImportance: Critical """ - sync_plan = entities.SyncPlan(enabled=enabled, organization=module_org).create() + sync_plan = target_sat.api.SyncPlan(enabled=enabled, organization=module_org).create() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) sync_plan = sync_plan.read() assert sync_plan.enabled == enabled @@ -152,7 +152,7 @@ def test_positive_create_enabled_disabled(module_org, enabled, request, target_s @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_create_with_name(module_org, name): +def test_positive_create_with_name(module_org, name, module_target_sat): """Create a sync plan with a random name. :id: c1263134-0d7c-425a-82fd-df5274e1f9ba @@ -163,14 +163,16 @@ def test_positive_create_with_name(module_org, name): :CaseImportance: Critical """ - sync_plan = entities.SyncPlan(enabled=False, name=name, organization=module_org).create() + sync_plan = module_target_sat.api.SyncPlan( + enabled=False, name=name, organization=module_org + ).create() sync_plan = sync_plan.read() assert sync_plan.name == name @pytest.mark.parametrize('description', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_create_with_description(module_org, description): +def test_positive_create_with_description(module_org, description, module_target_sat): """Create a sync plan with a random description. :id: 3e5745e8-838d-44a5-ad61-7e56829ad47c @@ -182,7 +184,7 @@ def test_positive_create_with_description(module_org, description): :CaseImportance: Critical """ - sync_plan = entities.SyncPlan( + sync_plan = module_target_sat.api.SyncPlan( enabled=False, description=description, organization=module_org ).create() sync_plan = sync_plan.read() @@ -191,7 +193,7 @@ def test_positive_create_with_description(module_org, description): @pytest.mark.parametrize('interval', **parametrized(valid_sync_interval())) @pytest.mark.tier1 -def test_positive_create_with_interval(module_org, interval): +def test_positive_create_with_interval(module_org, interval, module_target_sat): """Create a sync plan with a random interval. :id: d160ed1c-b698-42dc-be0b-67ac693c7840 @@ -202,7 +204,7 @@ def test_positive_create_with_interval(module_org, interval): :CaseImportance: Critical """ - sync_plan = entities.SyncPlan( + sync_plan = module_target_sat.api.SyncPlan( enabled=False, description=gen_string('alpha'), organization=module_org, interval=interval ) if interval == SYNC_INTERVAL['custom']: @@ -235,7 +237,7 @@ def test_positive_create_with_sync_date(module_org, sync_delta, target_sat): @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) @pytest.mark.tier1 -def test_negative_create_with_invalid_name(module_org, name): +def test_negative_create_with_invalid_name(module_org, name, module_target_sat): """Create a sync plan with an invalid name. :id: a3a0f844-2f81-4f87-9f68-c25506c29ce2 @@ -248,12 +250,12 @@ def test_negative_create_with_invalid_name(module_org, name): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.SyncPlan(name=name, organization=module_org).create() + module_target_sat.api.SyncPlan(name=name, organization=module_org).create() @pytest.mark.parametrize('interval', **parametrized(invalid_values_list())) @pytest.mark.tier1 -def test_negative_create_with_invalid_interval(module_org, interval): +def test_negative_create_with_invalid_interval(module_org, interval, module_target_sat): """Create a sync plan with invalid interval specified. :id: f5844526-9f58-4be3-8a96-3849a465fc02 @@ -266,11 +268,11 @@ def test_negative_create_with_invalid_interval(module_org, interval): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.SyncPlan(interval=interval, organization=module_org).create() + module_target_sat.api.SyncPlan(interval=interval, organization=module_org).create() @pytest.mark.tier1 -def test_negative_create_with_empty_interval(module_org): +def test_negative_create_with_empty_interval(module_org, module_target_sat): """Create a sync plan with no interval specified. :id: b4686463-69c8-4538-b040-6fb5246a7b00 @@ -280,7 +282,7 @@ def test_negative_create_with_empty_interval(module_org): :CaseImportance: Critical """ - sync_plan = entities.SyncPlan(organization=module_org) + sync_plan = module_target_sat.api.SyncPlan(organization=module_org) sync_plan.create_missing() del sync_plan.interval with pytest.raises(HTTPError): @@ -300,7 +302,7 @@ def test_positive_update_enabled(module_org, enabled, request, target_sat): :CaseImportance: Critical """ - sync_plan = entities.SyncPlan(enabled=not enabled, organization=module_org).create() + sync_plan = target_sat.api.SyncPlan(enabled=not enabled, organization=module_org).create() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) sync_plan.enabled = enabled sync_plan.update(['enabled']) @@ -310,7 +312,7 @@ def test_positive_update_enabled(module_org, enabled, request, target_sat): @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_update_name(module_org, name): +def test_positive_update_name(module_org, name, module_target_sat): """Create a sync plan and update its name. :id: dbfadf4f-50af-4aa8-8d7d-43988dc4528f @@ -322,7 +324,7 @@ def test_positive_update_name(module_org, name): :CaseImportance: Critical """ - sync_plan = entities.SyncPlan(enabled=False, organization=module_org).create() + sync_plan = module_target_sat.api.SyncPlan(enabled=False, organization=module_org).create() sync_plan.name = name sync_plan.update(['name']) sync_plan = sync_plan.read() @@ -331,7 +333,7 @@ def test_positive_update_name(module_org, name): @pytest.mark.parametrize('description', **parametrized(valid_data_list())) @pytest.mark.tier2 -def test_positive_update_description(module_org, description): +def test_positive_update_description(module_org, description, module_target_sat): """Create a sync plan and update its description. :id: 4769fe9c-9eec-40c8-b015-1e3d7e570bec @@ -341,7 +343,7 @@ def test_positive_update_description(module_org, description): :expectedresults: A sync plan is created and its description can be updated with the specified description. """ - sync_plan = entities.SyncPlan( + sync_plan = module_target_sat.api.SyncPlan( enabled=False, description=gen_string('alpha'), organization=module_org ).create() sync_plan.description = description @@ -352,7 +354,7 @@ def test_positive_update_description(module_org, description): @pytest.mark.parametrize('interval', **parametrized(valid_sync_interval())) @pytest.mark.tier1 -def test_positive_update_interval(module_org, interval): +def test_positive_update_interval(module_org, interval, module_target_sat): """Create a sync plan and update its interval. :id: cf2eddf8-b4db-430e-a9b0-83c626b45068 @@ -364,7 +366,7 @@ def test_positive_update_interval(module_org, interval): :CaseImportance: Critical """ - sync_plan = entities.SyncPlan( + sync_plan = module_target_sat.api.SyncPlan( enabled=False, description=gen_string('alpha'), organization=module_org, interval=interval ) if interval == SYNC_INTERVAL['custom']: @@ -384,7 +386,7 @@ def test_positive_update_interval(module_org, interval): @pytest.mark.parametrize('interval', **parametrized(valid_sync_interval())) @pytest.mark.tier1 -def test_positive_update_interval_custom_cron(module_org, interval): +def test_positive_update_interval_custom_cron(module_org, interval, module_target_sat): """Create a sync plan and update its interval to custom cron. :id: 26c58319-cae0-4b0c-b388-2a1fe3f22344 @@ -397,7 +399,7 @@ def test_positive_update_interval_custom_cron(module_org, interval): :CaseImportance: Critical """ if interval != SYNC_INTERVAL['custom']: - sync_plan = entities.SyncPlan( + sync_plan = module_target_sat.api.SyncPlan( enabled=False, description=gen_string('alpha'), organization=module_org, @@ -436,7 +438,7 @@ def test_positive_update_sync_date(module_org, sync_delta, target_sat): @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) @pytest.mark.tier1 -def test_negative_update_name(module_org, name): +def test_negative_update_name(module_org, name, module_target_sat): """Try to update a sync plan with an invalid name. :id: ae502053-9d3c-4cad-aee4-821f846ceae5 @@ -448,7 +450,7 @@ def test_negative_update_name(module_org, name): :CaseImportance: Critical """ - sync_plan = entities.SyncPlan(enabled=False, organization=module_org).create() + sync_plan = module_target_sat.api.SyncPlan(enabled=False, organization=module_org).create() sync_plan.name = name with pytest.raises(HTTPError): sync_plan.update(['name']) @@ -456,7 +458,7 @@ def test_negative_update_name(module_org, name): @pytest.mark.parametrize('interval', **parametrized(invalid_values_list())) @pytest.mark.tier1 -def test_negative_update_interval(module_org, interval): +def test_negative_update_interval(module_org, interval, module_target_sat): """Try to update a sync plan with invalid interval. :id: 8c981174-6f55-49c0-8baa-40e5c3fc598c @@ -468,7 +470,7 @@ def test_negative_update_interval(module_org, interval): :CaseImportance: Critical """ - sync_plan = entities.SyncPlan(enabled=False, organization=module_org).create() + sync_plan = module_target_sat.api.SyncPlan(enabled=False, organization=module_org).create() sync_plan.interval = interval with pytest.raises(HTTPError): sync_plan.update(['interval']) @@ -626,14 +628,14 @@ def test_negative_synchronize_custom_product_past_sync_date(module_org, request, :CaseLevel: System """ - product = entities.Product(organization=module_org).create() - repo = entities.Repository(product=product).create() + product = target_sat.api.Product(organization=module_org).create() + repo = target_sat.api.Repository(product=product).create() # Verify product is not synced and doesn't have any content with pytest.raises(AssertionError): validate_task_status(target_sat, repo.id, module_org.id, max_tries=2) validate_repo_content(repo, ['erratum', 'rpm', 'package_group'], after_sync=False) # Create and Associate sync plan with product - sync_plan = entities.SyncPlan( + sync_plan = target_sat.api.SyncPlan( organization=module_org, enabled=True, sync_date=datetime.utcnow().replace(second=0) ).create() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) @@ -660,10 +662,10 @@ def test_positive_synchronize_custom_product_past_sync_date(module_org, request, """ interval = 60 * 60 # 'hourly' sync interval in seconds delay = 2 * 60 - product = entities.Product(organization=module_org).create() - repo = entities.Repository(product=product).create() + product = target_sat.api.Product(organization=module_org).create() + repo = target_sat.api.Repository(product=product).create() # Create and Associate sync plan with product - sync_plan = entities.SyncPlan( + sync_plan = target_sat.api.SyncPlan( organization=module_org, enabled=True, interval='hourly', @@ -706,8 +708,8 @@ def test_positive_synchronize_custom_product_future_sync_date(module_org, reques :BZ: 1655595, 1695733 """ delay = 2 * 60 # delay for sync date in seconds - product = entities.Product(organization=module_org).create() - repo = entities.Repository(product=product).create() + product = target_sat.api.Product(organization=module_org).create() + repo = target_sat.api.Repository(product=product).create() # Verify product is not synced and doesn't have any content with pytest.raises(AssertionError): validate_task_status(target_sat, repo.id, module_org.id, max_tries=1) @@ -716,7 +718,7 @@ def test_positive_synchronize_custom_product_future_sync_date(module_org, reques # BZ:1695733 is closed WONTFIX so apply this workaround logger.info('Need to set seconds to zero because BZ#1695733') sync_date = datetime.utcnow().replace(second=0) + timedelta(seconds=delay) - sync_plan = entities.SyncPlan( + sync_plan = target_sat.api.SyncPlan( organization=module_org, enabled=True, sync_date=sync_date ).create() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) @@ -758,9 +760,11 @@ def test_positive_synchronize_custom_products_future_sync_date(module_org, reque """ # Test with multiple products and multiple repos needs more delay. delay = 8 * 60 # delay for sync date in seconds - products = [entities.Product(organization=module_org).create() for _ in range(2)] + products = [target_sat.api.Product(organization=module_org).create() for _ in range(2)] repos = [ - entities.Repository(product=product).create() for product in products for _ in range(2) + target_sat.api.Repository(product=product).create() + for product in products + for _ in range(2) ] # Verify products have not been synced yet logger.info( @@ -774,7 +778,7 @@ def test_positive_synchronize_custom_products_future_sync_date(module_org, reque # BZ:1695733 is closed WONTFIX so apply this workaround logger.info('Need to set seconds to zero because BZ#1695733') sync_date = datetime.utcnow().replace(second=0) + timedelta(seconds=delay) - sync_plan = entities.SyncPlan( + sync_plan = target_sat.api.SyncPlan( organization=module_org, enabled=True, sync_date=sync_date ).create() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) @@ -831,9 +835,9 @@ def test_positive_synchronize_rh_product_past_sync_date( reposet=REPOSET['rhst7'], releasever=None, ) - product = entities.Product(name=PRDS['rhel'], organization=org).search()[0] - repo = entities.Repository(id=repo_id).read() - sync_plan = entities.SyncPlan( + product = target_sat.api.Product(name=PRDS['rhel'], organization=org).search()[0] + repo = target_sat.api.Repository(id=repo_id).read() + sync_plan = target_sat.SyncPlan( organization=org, enabled=True, interval='hourly', @@ -864,7 +868,7 @@ def test_positive_synchronize_rh_product_past_sync_date( # Add disassociate RH product from sync plan check for BZ#1879537 assert len(sync_plan.read().product) == 1 # Disable the reposet - reposet = entities.RepositorySet(name=REPOSET['rhst7'], product=product).search()[0] + reposet = target_sat.api.RepositorySet(name=REPOSET['rhst7'], product=product).search()[0] reposet.disable(data={'basearch': 'x86_64', 'releasever': None, 'product_id': product.id}) # Assert that the Sync Plan now has no product associated with it assert len(sync_plan.read().product) == 0 @@ -895,12 +899,12 @@ def test_positive_synchronize_rh_product_future_sync_date( reposet=REPOSET['rhst7'], releasever=None, ) - product = entities.Product(name=PRDS['rhel'], organization=org).search()[0] - repo = entities.Repository(id=repo_id).read() + product = target_sat.api.Product(name=PRDS['rhel'], organization=org).search()[0] + repo = target_sat.api.Repository(id=repo_id).read() # BZ:1695733 is closed WONTFIX so apply this workaround logger.info('Need to set seconds to zero because BZ#1695733') sync_date = datetime.utcnow().replace(second=0) + timedelta(seconds=delay) - sync_plan = entities.SyncPlan( + sync_plan = target_sat.api.SyncPlan( organization=org, enabled=True, interval='hourly', sync_date=sync_date ).create() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) @@ -944,11 +948,11 @@ def test_positive_synchronize_custom_product_daily_recurrence(module_org, reques :CaseLevel: System """ delay = 2 * 60 - product = entities.Product(organization=module_org).create() - repo = entities.Repository(product=product).create() + product = target_sat.api.Product(organization=module_org).create() + repo = target_sat.api.Repository(product=product).create() start_date = datetime.utcnow().replace(second=0) - timedelta(days=1) + timedelta(seconds=delay) # Create and Associate sync plan with product - sync_plan = entities.SyncPlan( + sync_plan = target_sat.api.SyncPlan( organization=module_org, enabled=True, interval='daily', sync_date=start_date ).create() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) @@ -989,11 +993,11 @@ def test_positive_synchronize_custom_product_weekly_recurrence(module_org, reque :CaseLevel: System """ delay = 2 * 60 - product = entities.Product(organization=module_org).create() - repo = entities.Repository(product=product).create() + product = target_sat.api.Product(organization=module_org).create() + repo = target_sat.api.Repository(product=product).create() start_date = datetime.utcnow().replace(second=0) - timedelta(weeks=1) + timedelta(seconds=delay) # Create and Associate sync plan with product - sync_plan = entities.SyncPlan( + sync_plan = target_sat.api.SyncPlan( organization=module_org, enabled=True, interval='weekly', sync_date=start_date ).create() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) @@ -1061,7 +1065,7 @@ def test_positive_delete_products(module_org, target_sat): @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_delete_synced_product(module_org): +def test_positive_delete_synced_product(module_org, module_target_sat): """Create a sync plan with one synced product and delete it. :id: 195d8fec-1fa0-42ab-84a5-32dd81a285ca @@ -1071,9 +1075,9 @@ def test_positive_delete_synced_product(module_org): :CaseLevel: Integration """ - sync_plan = entities.SyncPlan(organization=module_org).create() - product = entities.Product(organization=module_org).create() - entities.Repository(product=product).create() + sync_plan = module_target_sat.api.SyncPlan(organization=module_org).create() + product = module_target_sat.api.Product(organization=module_org).create() + module_target_sat.api.Repository(product=product).create() sync_plan.add_products(data={'product_ids': [product.id]}) product.sync() sync_plan.delete() @@ -1083,7 +1087,7 @@ def test_positive_delete_synced_product(module_org): @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_delete_synced_product_custom_cron(module_org): +def test_positive_delete_synced_product_custom_cron(module_org, module_target_sat): """Create a sync plan with custom cron with one synced product and delete it. @@ -1094,13 +1098,13 @@ def test_positive_delete_synced_product_custom_cron(module_org): :CaseLevel: Integration """ - sync_plan = entities.SyncPlan( + sync_plan = module_target_sat.api.SyncPlan( organization=module_org, interval='custom cron', cron_expression=gen_choice(valid_cron_expressions()), ).create() - product = entities.Product(organization=module_org).create() - entities.Repository(product=product).create() + product = module_target_sat.api.Product(organization=module_org).create() + module_target_sat.api.Repository(product=product).create() sync_plan.add_products(data={'product_ids': [product.id]}) product.sync() product = product.read() diff --git a/tests/foreman/api/test_templatesync.py b/tests/foreman/api/test_templatesync.py index 061b8db1dd1..8e38540d395 100644 --- a/tests/foreman/api/test_templatesync.py +++ b/tests/foreman/api/test_templatesync.py @@ -19,7 +19,6 @@ import time from fauxfactory import gen_string -from nailgun import entities import pytest import requests @@ -67,7 +66,9 @@ def setUpClass(self, module_target_sat): ) @pytest.mark.tier2 - def test_positive_import_filtered_templates_from_git(self, module_org, module_location): + def test_positive_import_filtered_templates_from_git( + self, module_org, module_location, module_target_sat + ): """Assure only templates with a given filter regex are pulled from git repo. @@ -91,7 +92,7 @@ def test_positive_import_filtered_templates_from_git(self, module_org, module_lo :CaseImportance: High """ prefix = gen_string('alpha') - filtered_imported_templates = entities.Template().imports( + filtered_imported_templates = module_target_sat.api.Template().imports( data={ 'repo': FOREMAN_TEMPLATE_IMPORT_URL, 'branch': 'automation', @@ -105,7 +106,7 @@ def test_positive_import_filtered_templates_from_git(self, module_org, module_lo template['imported'] for template in filtered_imported_templates['message']['templates'] ].count(True) assert imported_count == 8 - ptemplates = entities.ProvisioningTemplate().search( + ptemplates = module_target_sat.api.ProvisioningTemplate().search( query={ 'per_page': '100', 'search': f'name~{prefix}', @@ -114,7 +115,7 @@ def test_positive_import_filtered_templates_from_git(self, module_org, module_lo } ) assert len(ptemplates) == 5 - ptables = entities.PartitionTable().search( + ptables = module_target_sat.api.PartitionTable().search( query={ 'per_page': '100', 'search': f'name~{prefix}', @@ -123,7 +124,7 @@ def test_positive_import_filtered_templates_from_git(self, module_org, module_lo } ) assert len(ptables) == 1 - jtemplates = entities.JobTemplate().search( + jtemplates = module_target_sat.api.JobTemplate().search( query={ 'per_page': '100', 'search': f'name~{prefix}', @@ -132,7 +133,7 @@ def test_positive_import_filtered_templates_from_git(self, module_org, module_lo } ) assert len(jtemplates) == 1 - rtemplates = entities.ReportTemplate().search( + rtemplates = module_target_sat.api.ReportTemplate().search( query={ 'per_page': '10', 'search': f'name~{prefix}', @@ -143,7 +144,7 @@ def test_positive_import_filtered_templates_from_git(self, module_org, module_lo assert len(rtemplates) == 1 @pytest.mark.tier2 - def test_import_filtered_templates_from_git_with_negate(self, module_org): + def test_import_filtered_templates_from_git_with_negate(self, module_org, module_target_sat): """Assure templates with a given filter regex are NOT pulled from git repo. @@ -162,7 +163,7 @@ def test_import_filtered_templates_from_git_with_negate(self, module_org): :CaseImportance: Medium """ prefix = gen_string('alpha') - filtered_imported_templates = entities.Template().imports( + filtered_imported_templates = module_target_sat.api.Template().imports( data={ 'repo': FOREMAN_TEMPLATE_IMPORT_URL, 'branch': 'automation', @@ -176,15 +177,15 @@ def test_import_filtered_templates_from_git_with_negate(self, module_org): template['imported'] for template in filtered_imported_templates['message']['templates'] ].count(False) assert not_imported_count == 9 - ptemplates = entities.ProvisioningTemplate().search( + ptemplates = module_target_sat.api.ProvisioningTemplate().search( query={'per_page': '100', 'search': 'name~jenkins', 'organization_id': module_org.id} ) assert len(ptemplates) == 6 - ptables = entities.PartitionTable().search( + ptables = module_target_sat.api.PartitionTable().search( query={'per_page': '100', 'search': 'name~jenkins', 'organization_id': module_org.id} ) assert len(ptables) == 1 - rtemplates = entities.ReportTemplate().search( + rtemplates = module_target_sat.api.ReportTemplate().search( query={'per_page': '100', 'search': 'name~jenkins', 'organization_id': module_org.id} ) assert len(rtemplates) == 1 @@ -267,7 +268,7 @@ def test_positive_import_and_associate( prefix = gen_string('alpha') _, dir_path = create_import_export_local_dir # Associate Never - entities.Template().imports( + target_sat.api.Template().imports( data={ 'repo': dir_path, 'prefix': prefix, @@ -277,7 +278,7 @@ def test_positive_import_and_associate( } ) # - Template 1 imported in X and Y taxonomies - ptemplate = entities.ProvisioningTemplate().search( + ptemplate = target_sat.api.ProvisioningTemplate().search( query={ 'per_page': '10', 'search': f'name~{prefix}', @@ -288,7 +289,7 @@ def test_positive_import_and_associate( assert ptemplate assert len(ptemplate[0].read().organization) == 1 # - Template 1 not imported in metadata taxonomies - ptemplate = entities.ProvisioningTemplate().search( + ptemplate = target_sat.api.ProvisioningTemplate().search( query={ 'per_page': '10', 'search': f'name~{prefix}', @@ -302,7 +303,7 @@ def test_positive_import_and_associate( f'cp {dir_path}/example_template.erb {dir_path}/another_template.erb && ' f'sed -ie "s/name: .*/name: another_template/" {dir_path}/another_template.erb' ) - entities.Template().imports( + target_sat.api.Template().imports( data={ 'repo': dir_path, 'prefix': prefix, @@ -312,7 +313,7 @@ def test_positive_import_and_associate( } ) # - Template 1 taxonomies are not changed - ptemplate = entities.ProvisioningTemplate().search( + ptemplate = target_sat.api.ProvisioningTemplate().search( query={ 'per_page': '10', 'search': f'name~{prefix}example_template', @@ -323,7 +324,7 @@ def test_positive_import_and_associate( assert ptemplate assert len(ptemplate[0].read().organization) == 1 # - Template 2 should be imported in importing taxonomies - ptemplate = entities.ProvisioningTemplate().search( + ptemplate = target_sat.api.ProvisioningTemplate().search( query={ 'per_page': '10', 'search': f'name~{prefix}another_template', @@ -334,7 +335,7 @@ def test_positive_import_and_associate( assert ptemplate assert len(ptemplate[0].read().organization) == 1 # Associate Always - entities.Template().imports( + target_sat.api.Template().imports( data={ 'repo': dir_path, 'prefix': prefix, @@ -344,7 +345,7 @@ def test_positive_import_and_associate( } ) # - Template 1 taxonomies are not changed - ptemplate = entities.ProvisioningTemplate().search( + ptemplate = target_sat.api.ProvisioningTemplate().search( query={ 'per_page': '10', 'search': f'name~{prefix}example_template', @@ -355,7 +356,7 @@ def test_positive_import_and_associate( assert ptemplate assert len(ptemplate[0].read().organization) == 1 # - Template 2 taxonomies are not changed - ptemplate = entities.ProvisioningTemplate().search( + ptemplate = target_sat.api.ProvisioningTemplate().search( query={ 'per_page': '10', 'search': f'name~{prefix}another_template', @@ -367,7 +368,7 @@ def test_positive_import_and_associate( assert len(ptemplate[0].read().organization) == 1 @pytest.mark.tier2 - def test_positive_import_from_subdirectory(self, module_org): + def test_positive_import_from_subdirectory(self, module_org, module_target_sat): """Assure templates are imported from specific repositories subdirectory :id: 8ea11a1a-165e-4834-9387-7accb4c94e77 @@ -384,7 +385,7 @@ def test_positive_import_from_subdirectory(self, module_org): :CaseImportance: Medium """ prefix = gen_string('alpha') - filtered_imported_templates = entities.Template().imports( + filtered_imported_templates = module_target_sat.api.Template().imports( data={ 'repo': FOREMAN_TEMPLATE_IMPORT_URL, 'branch': 'automation', @@ -423,7 +424,7 @@ def test_positive_export_filtered_templates_to_localdir( :CaseImportance: Low """ dir_name, dir_path = create_import_export_local_dir - exported_temps = entities.Template().exports( + exported_temps = target_sat.api.Template().exports( data={ 'repo': FOREMAN_TEMPLATE_ROOT_DIR, 'dirname': dir_name, @@ -459,7 +460,7 @@ def test_positive_export_filtered_templates_negate( """ # Export some filtered templates to local dir _, dir_path = create_import_export_local_dir - entities.Template().exports( + target_sat.api.Template().exports( data={ 'repo': dir_path, 'organization_ids': [module_org.id], @@ -498,7 +499,7 @@ def test_positive_export_and_import_with_metadata( ex_template = 'example_template.erb' prefix = gen_string('alpha') _, dir_path = create_import_export_local_dir - entities.Template().imports( + target_sat.api.Template().imports( data={ 'repo': dir_path, 'location_ids': [module_location.id], @@ -508,7 +509,7 @@ def test_positive_export_and_import_with_metadata( ) export_file = f'{prefix.lower()}{ex_template}' # Export same template to local dir with refreshed metadata - entities.Template().exports( + target_sat.api.Template().exports( data={ 'metadata_export_mode': 'refresh', 'repo': dir_path, @@ -522,7 +523,7 @@ def test_positive_export_and_import_with_metadata( ) assert result.status == 0 # Export same template to local dir with keeping metadata - entities.Template().exports( + target_sat.api.Template().exports( data={ 'metadata_export_mode': 'keep', 'repo': dir_path, @@ -536,7 +537,7 @@ def test_positive_export_and_import_with_metadata( ) assert result.status == 1 # Export same template to local dir with removed metadata - entities.Template().exports( + target_sat.api.Template().exports( data={ 'metadata_export_mode': 'remove', 'repo': dir_path, @@ -553,7 +554,7 @@ def test_positive_export_and_import_with_metadata( # Take Templates out of Tech Preview Feature Tests @pytest.mark.tier3 @pytest.mark.parametrize('verbose', [True, False]) - def test_positive_import_json_output_verbose(self, module_org, verbose): + def test_positive_import_json_output_verbose(self, module_org, verbose, module_target_sat): """Assert all the required fields displayed in import output when verbose is True and False @@ -575,7 +576,7 @@ def test_positive_import_json_output_verbose(self, module_org, verbose): :CaseImportance: Low """ prefix = gen_string('alpha') - templates = entities.Template().imports( + templates = module_target_sat.api.Template().imports( data={ 'repo': FOREMAN_TEMPLATE_IMPORT_URL, 'branch': 'master', @@ -628,19 +629,19 @@ def test_positive_import_json_output_changed_key_true( """ prefix = gen_string('alpha') _, dir_path = create_import_export_local_dir - pre_template = entities.Template().imports( + pre_template = target_sat.api.Template().imports( data={'repo': dir_path, 'organization_ids': [module_org.id], 'prefix': prefix} ) assert bool(pre_template['message']['templates'][0]['imported']) target_sat.execute(f'echo " Updating Template data." >> {dir_path}/example_template.erb') - post_template = entities.Template().imports( + post_template = target_sat.api.Template().imports( data={'repo': dir_path, 'organization_ids': [module_org.id], 'prefix': prefix} ) assert bool(post_template['message']['templates'][0]['changed']) @pytest.mark.tier2 def test_positive_import_json_output_changed_key_false( - self, create_import_export_local_dir, module_org + self, create_import_export_local_dir, module_org, module_target_sat ): """Assert template imports output `changed` key returns `False` when template data gets updated @@ -663,11 +664,11 @@ def test_positive_import_json_output_changed_key_false( """ prefix = gen_string('alpha') _, dir_path = create_import_export_local_dir - pre_template = entities.Template().imports( + pre_template = module_target_sat.api.Template().imports( data={'repo': dir_path, 'organization_ids': [module_org.id], 'prefix': prefix} ) assert bool(pre_template['message']['templates'][0]['imported']) - post_template = entities.Template().imports( + post_template = module_target_sat.api.Template().imports( data={'repo': dir_path, 'organization_ids': [module_org.id], 'prefix': prefix} ) assert not bool(post_template['message']['templates'][0]['changed']) @@ -697,7 +698,7 @@ def test_positive_import_json_output_name_key( target_sat.execute( f'sed -ie "s/name: .*/name: {template_name}/" {dir_path}/example_template.erb' ) - template = entities.Template().imports( + template = target_sat.api.Template().imports( data={'repo': dir_path, 'organization_ids': [module_org.id]} ) assert 'name' in template['message']['templates'][0].keys() @@ -705,7 +706,7 @@ def test_positive_import_json_output_name_key( @pytest.mark.tier2 def test_positive_import_json_output_imported_key( - self, create_import_export_local_dir, module_org + self, create_import_export_local_dir, module_org, module_target_sat ): """Assert template imports output `imported` key returns `True` on successful import @@ -725,13 +726,15 @@ def test_positive_import_json_output_imported_key( """ prefix = gen_string('alpha') _, dir_path = create_import_export_local_dir - template = entities.Template().imports( + template = module_target_sat.api.Template().imports( data={'repo': dir_path, 'organization_ids': [module_org.id], 'prefix': prefix} ) assert bool(template['message']['templates'][0]['imported']) @pytest.mark.tier2 - def test_positive_import_json_output_file_key(self, create_import_export_local_dir, module_org): + def test_positive_import_json_output_file_key( + self, create_import_export_local_dir, module_org, module_target_sat + ): """Assert template imports output `file` key returns correct file name from where the template is imported @@ -750,7 +753,7 @@ def test_positive_import_json_output_file_key(self, create_import_export_local_d :CaseImportance: Low """ _, dir_path = create_import_export_local_dir - template = entities.Template().imports( + template = module_target_sat.api.Template().imports( data={'repo': dir_path, 'organization_ids': [module_org.id]} ) assert 'example_template.erb' == template['message']['templates'][0]['file'] @@ -780,7 +783,7 @@ def test_positive_import_json_output_corrupted_metadata( """ _, dir_path = create_import_export_local_dir target_sat.execute(f'sed -ie "s/<%#/$#$#@%^$^@@RT$$/" {dir_path}/example_template.erb') - template = entities.Template().imports( + template = target_sat.api.Template().imports( data={'repo': dir_path, 'organization_ids': [module_org.id]} ) assert not bool(template['message']['templates'][0]['imported']) @@ -791,7 +794,7 @@ def test_positive_import_json_output_corrupted_metadata( @pytest.mark.skip_if_open('BZ:1787355') @pytest.mark.tier2 def test_positive_import_json_output_filtered_skip_message( - self, create_import_export_local_dir, module_org + self, create_import_export_local_dir, module_org, module_target_sat ): """Assert template imports output returns template import skipped info for templates whose name doesnt match the filter @@ -812,7 +815,7 @@ def test_positive_import_json_output_filtered_skip_message( :CaseImportance: Low """ _, dir_path = create_import_export_local_dir - template = entities.Template().imports( + template = module_target_sat.api.Template().imports( data={ 'repo': dir_path, 'organization_ids': [module_org.id], @@ -850,7 +853,7 @@ def test_positive_import_json_output_no_name_error( """ _, dir_path = create_import_export_local_dir target_sat.execute(f'sed -ie "s/name: .*/name: /" {dir_path}/example_template.erb') - template = entities.Template().imports( + template = target_sat.api.Template().imports( data={'repo': dir_path, 'organization_ids': [module_org.id]} ) assert not bool(template['message']['templates'][0]['imported']) @@ -884,7 +887,7 @@ def test_positive_import_json_output_no_model_error( """ _, dir_path = create_import_export_local_dir target_sat.execute(f'sed -ie "/model: .*/d" {dir_path}/example_template.erb') - template = entities.Template().imports( + template = target_sat.api.Template().imports( data={'repo': dir_path, 'organization_ids': [module_org.id]} ) assert not bool(template['message']['templates'][0]['imported']) @@ -918,7 +921,7 @@ def test_positive_import_json_output_blank_model_error( """ _, dir_path = create_import_export_local_dir target_sat.execute(f'sed -ie "s/model: .*/model: /" {dir_path}/example_template.erb') - template = entities.Template().imports( + template = target_sat.api.Template().imports( data={'repo': dir_path, 'organization_ids': [module_org.id]} ) assert not bool(template['message']['templates'][0]['imported']) @@ -948,7 +951,7 @@ def test_positive_export_json_output( :CaseImportance: Low """ prefix = gen_string('alpha') - imported_templates = entities.Template().imports( + imported_templates = target_sat.api.Template().imports( data={ 'repo': FOREMAN_TEMPLATE_IMPORT_URL, 'branch': 'automation', @@ -963,7 +966,7 @@ def test_positive_export_json_output( assert imported_count == 17 # Total Count # Export some filtered templates to local dir _, dir_path = create_import_export_local_dir - exported_templates = entities.Template().exports( + exported_templates = target_sat.api.Template().exports( data={'repo': dir_path, 'organization_ids': [module_org.id], 'filter': prefix} ) exported_count = [ @@ -1000,7 +1003,7 @@ def test_positive_import_log_to_production(self, module_org, target_sat): :CaseImportance: Low """ - entities.Template().imports( + target_sat.api.Template().imports( data={ 'repo': FOREMAN_TEMPLATE_IMPORT_URL, 'branch': 'master', @@ -1038,7 +1041,7 @@ def test_positive_export_log_to_production( :CaseImportance: Low """ - entities.Template().imports( + target_sat.api.Template().imports( data={ 'repo': FOREMAN_TEMPLATE_IMPORT_URL, 'branch': 'master', @@ -1047,7 +1050,7 @@ def test_positive_export_log_to_production( } ) _, dir_path = create_import_export_local_dir - entities.Template().exports( + target_sat.api.Template().exports( data={'repo': dir_path, 'organization_ids': [module_org.id], 'filter': 'empty'} ) time.sleep(5) @@ -1076,7 +1079,7 @@ def test_positive_export_log_to_production( ids=['non_empty_repo', 'empty_repo'], ) def test_positive_export_all_templates_to_repo( - self, module_org, git_repository, git_branch, url + self, module_org, git_repository, git_branch, url, module_target_sat ): """Assure all templates are exported if no filter is specified. @@ -1094,7 +1097,7 @@ def test_positive_export_all_templates_to_repo( :CaseImportance: Low """ - output = entities.Template().exports( + output = module_target_sat.api.Template().exports( data={ 'repo': f'{url}/{git.username}/{git_repository["name"]}', 'branch': git_branch, @@ -1118,7 +1121,7 @@ def test_positive_export_all_templates_to_repo( assert len(output['message']['templates']) == git_count @pytest.mark.tier2 - def test_positive_import_all_templates_from_repo(self, module_org): + def test_positive_import_all_templates_from_repo(self, module_org, module_target_sat): """Assure all templates are imported if no filter is specified. :id: 95ac9543-d989-44f4-b4d9-18f20a0b58b9 @@ -1131,7 +1134,7 @@ def test_positive_import_all_templates_from_repo(self, module_org): :CaseImportance: Low """ - output = entities.Template().imports( + output = module_target_sat.api.Template().imports( data={ 'repo': FOREMAN_TEMPLATE_IMPORT_URL, 'branch': 'master', @@ -1150,7 +1153,7 @@ def test_positive_import_all_templates_from_repo(self, module_org): assert len(output['message']['templates']) == git_count @pytest.mark.tier2 - def test_negative_import_locked_template(self, module_org): + def test_negative_import_locked_template(self, module_org, module_target_sat): """Assure locked templates are not pulled from repository. :id: 88e21cad-448e-45e0-add2-94493a1319c5 @@ -1164,7 +1167,7 @@ def test_negative_import_locked_template(self, module_org): :CaseImportance: Medium """ # import template with lock - output = entities.Template().imports( + output = module_target_sat.api.Template().imports( data={ 'repo': FOREMAN_TEMPLATE_IMPORT_URL, 'branch': 'locked', @@ -1176,7 +1179,7 @@ def test_negative_import_locked_template(self, module_org): ) assert output['message']['templates'][0]['imported'] # try to import same template with changed content - output = entities.Template().imports( + output = module_target_sat.api.Template().imports( data={ 'repo': FOREMAN_TEMPLATE_IMPORT_URL, 'branch': 'locked', @@ -1193,13 +1196,13 @@ def test_negative_import_locked_template(self, module_org): ) res.raise_for_status() git_content = base64.b64decode(json.loads(res.text)['content']) - sat_content = entities.ProvisioningTemplate( + sat_content = module_target_sat.api.ProvisioningTemplate( id=output['message']['templates'][0]['id'] ).read() assert git_content.decode('utf-8') == sat_content.template @pytest.mark.tier2 - def test_positive_import_locked_template(self, module_org): + def test_positive_import_locked_template(self, module_org, module_target_sat): """Assure locked templates are pulled from repository while using force parameter. :id: 936c91cc-1947-45b0-8bf0-79ba4be87b97 @@ -1213,7 +1216,7 @@ def test_positive_import_locked_template(self, module_org): :CaseImportance: Medium """ # import template with lock - output = entities.Template().imports( + output = module_target_sat.api.Template().imports( data={ 'repo': FOREMAN_TEMPLATE_IMPORT_URL, 'branch': 'locked', @@ -1225,7 +1228,7 @@ def test_positive_import_locked_template(self, module_org): ) assert output['message']['templates'][0]['imported'] # force import same template with changed content - output = entities.Template().imports( + output = module_target_sat.api.Template().imports( data={ 'repo': FOREMAN_TEMPLATE_IMPORT_URL, 'branch': 'locked', @@ -1244,7 +1247,7 @@ def test_positive_import_locked_template(self, module_org): ) res.raise_for_status() git_content = base64.b64decode(json.loads(res.text)['content']) - sat_content = entities.ProvisioningTemplate( + sat_content = module_target_sat.api.ProvisioningTemplate( id=output['message']['templates'][0]['id'] ).read() assert git_content.decode('utf-8') == sat_content.template diff --git a/tests/foreman/api/test_user.py b/tests/foreman/api/test_user.py index eff47fbba0f..cd2c704e249 100644 --- a/tests/foreman/api/test_user.py +++ b/tests/foreman/api/test_user.py @@ -23,7 +23,6 @@ import json import re -from nailgun import entities from nailgun.config import ServerConfig import pytest from requests.exceptions import HTTPError @@ -45,9 +44,9 @@ @pytest.fixture(scope='module') -def create_user(): +def create_user(module_target_sat): """Create a user""" - return entities.User().create() + return module_target_sat.api.User().create() class TestUser: @@ -55,7 +54,7 @@ class TestUser: @pytest.mark.tier1 @pytest.mark.parametrize('username', **parametrized(valid_usernames_list())) - def test_positive_create_with_username(self, username): + def test_positive_create_with_username(self, username, target_sat): """Create User for all variations of Username :id: a9827cda-7f6d-4785-86ff-3b6969c9c00a @@ -66,14 +65,14 @@ def test_positive_create_with_username(self, username): :CaseImportance: Critical """ - user = entities.User(login=username).create() + user = target_sat.api.User(login=username).create() assert user.login == username @pytest.mark.tier1 @pytest.mark.parametrize( 'firstname', **parametrized(generate_strings_list(exclude_types=['html'], max_length=50)) ) - def test_positive_create_with_firstname(self, firstname): + def test_positive_create_with_firstname(self, firstname, target_sat): """Create User for all variations of First Name :id: 036bb958-227c-420c-8f2b-c607136f12e0 @@ -86,14 +85,14 @@ def test_positive_create_with_firstname(self, firstname): """ if len(str.encode(firstname)) > 50: firstname = firstname[:20] - user = entities.User(firstname=firstname).create() + user = target_sat.api.User(firstname=firstname).create() assert user.firstname == firstname @pytest.mark.tier1 @pytest.mark.parametrize( 'lastname', **parametrized(generate_strings_list(exclude_types=['html'], max_length=50)) ) - def test_positive_create_with_lastname(self, lastname): + def test_positive_create_with_lastname(self, lastname, target_sat): """Create User for all variations of Last Name :id: 95d3b571-77e7-42a1-9c48-21f242e8cdc2 @@ -106,12 +105,12 @@ def test_positive_create_with_lastname(self, lastname): """ if len(str.encode(lastname)) > 50: lastname = lastname[:20] - user = entities.User(lastname=lastname).create() + user = target_sat.api.User(lastname=lastname).create() assert user.lastname == lastname @pytest.mark.tier1 @pytest.mark.parametrize('mail', **parametrized(valid_emails_list())) - def test_positive_create_with_email(self, mail): + def test_positive_create_with_email(self, mail, target_sat): """Create User for all variations of Email :id: e68caf51-44ba-4d32-b79b-9ab9b67b9590 @@ -122,12 +121,12 @@ def test_positive_create_with_email(self, mail): :CaseImportance: Critical """ - user = entities.User(mail=mail).create() + user = target_sat.api.User(mail=mail).create() assert user.mail == mail @pytest.mark.tier1 @pytest.mark.parametrize('description', **parametrized(valid_data_list())) - def test_positive_create_with_description(self, description): + def test_positive_create_with_description(self, description, target_sat): """Create User for all variations of Description :id: 1463d71c-b77d-4223-84fa-8370f77b3edf @@ -138,14 +137,14 @@ def test_positive_create_with_description(self, description): :CaseImportance: Critical """ - user = entities.User(description=description).create() + user = target_sat.api.User(description=description).create() assert user.description == description @pytest.mark.tier1 @pytest.mark.parametrize( 'password', **parametrized(generate_strings_list(exclude_types=['html'], max_length=50)) ) - def test_positive_create_with_password(self, password): + def test_positive_create_with_password(self, password, target_sat): """Create User for all variations of Password :id: 53d0a419-0730-4f7d-9170-d855adfc5070 @@ -156,13 +155,13 @@ def test_positive_create_with_password(self, password): :CaseImportance: Critical """ - user = entities.User(password=password).create() + user = target_sat.api.User(password=password).create() assert user is not None @pytest.mark.tier1 @pytest.mark.upgrade @pytest.mark.parametrize('mail', **parametrized(valid_emails_list())) - def test_positive_delete(self, mail): + def test_positive_delete(self, mail, target_sat): """Create random users and then delete it. :id: df6059e7-85c5-42fa-99b5-b7f1ef809f52 @@ -173,7 +172,7 @@ def test_positive_delete(self, mail): :CaseImportance: Critical """ - user = entities.User(mail=mail).create() + user = target_sat.api.User(mail=mail).create() user.delete() with pytest.raises(HTTPError): user.read() @@ -307,7 +306,7 @@ def test_positive_update_description(self, create_user, description): @pytest.mark.tier1 @pytest.mark.parametrize('admin_enable', [True, False]) - def test_positive_update_admin(self, admin_enable): + def test_positive_update_admin(self, admin_enable, target_sat): """Update a user and provide the ``admin`` attribute. :id: b5fedf65-37f5-43ca-806a-ac9a7979b19d @@ -318,13 +317,13 @@ def test_positive_update_admin(self, admin_enable): :CaseImportance: Critical """ - user = entities.User(admin=admin_enable).create() + user = target_sat.api.User(admin=admin_enable).create() user.admin = not admin_enable assert user.update().admin == (not admin_enable) @pytest.mark.tier1 @pytest.mark.parametrize('mail', **parametrized(invalid_emails_list())) - def test_negative_create_with_invalid_email(self, mail): + def test_negative_create_with_invalid_email(self, mail, target_sat): """Create User with invalid Email Address :id: ebbd1f5f-e71f-41f4-a956-ce0071b0a21c @@ -336,11 +335,11 @@ def test_negative_create_with_invalid_email(self, mail): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.User(mail=mail).create() + target_sat.api.User(mail=mail).create() @pytest.mark.tier1 @pytest.mark.parametrize('invalid_name', **parametrized(invalid_usernames_list())) - def test_negative_create_with_invalid_username(self, invalid_name): + def test_negative_create_with_invalid_username(self, invalid_name, target_sat): """Create User with invalid Username :id: aaf157a9-0375-4405-ad87-b13970e0609b @@ -352,11 +351,11 @@ def test_negative_create_with_invalid_username(self, invalid_name): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.User(login=invalid_name).create() + target_sat.api.User(login=invalid_name).create() @pytest.mark.tier1 @pytest.mark.parametrize('invalid_name', **parametrized(invalid_names_list())) - def test_negative_create_with_invalid_firstname(self, invalid_name): + def test_negative_create_with_invalid_firstname(self, invalid_name, target_sat): """Create User with invalid Firstname :id: cb1ca8a9-38b1-4d58-ae32-915b47b91657 @@ -368,11 +367,11 @@ def test_negative_create_with_invalid_firstname(self, invalid_name): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.User(firstname=invalid_name).create() + target_sat.api.User(firstname=invalid_name).create() @pytest.mark.tier1 @pytest.mark.parametrize('invalid_name', **parametrized(invalid_names_list())) - def test_negative_create_with_invalid_lastname(self, invalid_name): + def test_negative_create_with_invalid_lastname(self, invalid_name, target_sat): """Create User with invalid Lastname :id: 59546d26-2b6b-400b-990f-0b5d1c35004e @@ -384,10 +383,10 @@ def test_negative_create_with_invalid_lastname(self, invalid_name): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.User(lastname=invalid_name).create() + target_sat.api.User(lastname=invalid_name).create() @pytest.mark.tier1 - def test_negative_create_with_blank_authorized_by(self): + def test_negative_create_with_blank_authorized_by(self, target_sat): """Create User with blank authorized by :id: 1fe2d1e3-728c-4d89-97ae-3890e904f413 @@ -397,7 +396,7 @@ def test_negative_create_with_blank_authorized_by(self): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.User(auth_source='').create() + target_sat.api.User(auth_source='').create() @pytest.mark.tier1 def test_positive_table_preferences(self, module_target_sat): @@ -413,16 +412,16 @@ def test_positive_table_preferences(self, module_target_sat): :BZ: 1757394 """ - existing_roles = entities.Role().search() + existing_roles = module_target_sat.api.Role().search() password = gen_string('alpha') - user = entities.User(role=existing_roles, password=password).create() + user = module_target_sat.api.User(role=existing_roles, password=password).create() name = "hosts" columns = ["power_status", "name", "comment"] sc = ServerConfig( auth=(user.login, password), url=module_target_sat.url, verify=settings.server.verify_ca ) - entities.TablePreferences(sc, user=user, name=name, columns=columns).create() - table_preferences = entities.TablePreferences(sc, user=user).search() + module_target_sat.api.TablePreferences(sc, user=user, name=name, columns=columns).create() + table_preferences = module_target_sat.api.TablePreferences(sc, user=user).search() assert len(table_preferences) == 1 tp = table_preferences[0] assert hasattr(tp, 'name') @@ -437,14 +436,14 @@ class TestUserRole: """Test associations between users and roles.""" @pytest.fixture(scope='class') - def make_roles(self): + def make_roles(self, class_target_sat): """Create two roles.""" - return [entities.Role().create() for _ in range(2)] + return [class_target_sat.api.Role().create() for _ in range(2)] @pytest.mark.tier1 @pytest.mark.build_sanity @pytest.mark.parametrize('number_of_roles', range(1, 3)) - def test_positive_create_with_role(self, make_roles, number_of_roles): + def test_positive_create_with_role(self, make_roles, number_of_roles, class_target_sat): """Create a user with the ``role`` attribute. :id: 32daacf1-eed4-49b1-81e1-ab0a5b0113f2 @@ -458,7 +457,7 @@ def test_positive_create_with_role(self, make_roles, number_of_roles): :CaseImportance: Critical """ chosen_roles = make_roles[:number_of_roles] - user = entities.User(role=chosen_roles).create() + user = class_target_sat.api.User(role=chosen_roles).create() assert len(user.role) == number_of_roles assert {role.id for role in user.role} == {role.id for role in chosen_roles} @@ -488,14 +487,14 @@ class TestSshKeyInUser: """Implements the SSH Key in User Tests""" @pytest.fixture(scope='class') - def create_user(self): + def create_user(self, class_target_sat): """Create an user and import different keys from data json file""" - user = entities.User().create() + user = class_target_sat.api.User().create() data_keys = json.loads(DataFile.SSH_KEYS_JSON.read_bytes()) return dict(user=user, data_keys=data_keys) @pytest.mark.tier1 - def test_positive_CRD_ssh_key(self): + def test_positive_CRD_ssh_key(self, class_target_sat): """SSH Key can be added to User :id: d00905f6-3a70-4e2f-a5ae-fcac18274bb7 @@ -511,18 +510,18 @@ def test_positive_CRD_ssh_key(self): :CaseImportance: Critical """ - user = entities.User().create() + user = class_target_sat.api.User().create() ssh_name = gen_string('alpha') ssh_key = gen_ssh_keypairs()[1] - user_sshkey = entities.SSHKey(user=user, name=ssh_name, key=ssh_key).create() + user_sshkey = class_target_sat.api.SSHKey(user=user, name=ssh_name, key=ssh_key).create() assert ssh_name == user_sshkey.name assert ssh_key == user_sshkey.key user_sshkey.delete() - result = entities.SSHKey(user=user).search() + result = class_target_sat.api.SSHKey(user=user).search() assert len(result) == 0 @pytest.mark.tier1 - def test_negative_create_ssh_key(self, create_user): + def test_negative_create_ssh_key(self, create_user, target_sat): """Invalid ssh key can not be added in User Template :id: e924ff03-8b2c-4ab9-a054-ea491413e143 @@ -542,7 +541,7 @@ def test_negative_create_ssh_key(self, create_user): """ invalid_sshkey = gen_string('alpha', length=256) with pytest.raises(HTTPError) as context: - entities.SSHKey( + target_sat.api.SSHKey( user=create_user['user'], name=gen_string('alpha'), key=invalid_sshkey ).create() assert re.search('Key is not a valid public ssh key', context.value.response.text) @@ -551,7 +550,7 @@ def test_negative_create_ssh_key(self, create_user): assert re.search('Length could not be calculated', context.value.response.text) @pytest.mark.tier1 - def test_negative_create_invalid_length_ssh_key(self, create_user): + def test_negative_create_invalid_length_ssh_key(self, create_user, target_sat): """Attempt to add SSH key that has invalid length :id: 899f0c46-c7fe-4610-80f1-1add4a9cbc26 @@ -568,14 +567,14 @@ def test_negative_create_invalid_length_ssh_key(self, create_user): """ invalid_length_key = create_user['data_keys']['ssh_keys']['invalid_ssh_key'] with pytest.raises(HTTPError) as context: - entities.SSHKey( + target_sat.api.SSHKey( user=create_user['user'], name=gen_string('alpha'), key=invalid_length_key ).create() assert re.search('Length could not be calculated', context.value.response.text) assert not re.search('Fingerprint could not be generated', context.value.response.text) @pytest.mark.tier1 - def test_negative_create_ssh_key_with_invalid_name(self, create_user): + def test_negative_create_ssh_key_with_invalid_name(self, create_user, target_sat): """Attempt to add SSH key that has invalid name length :id: e1e17839-a392-45bb-bb1e-28d3cd9dba1c @@ -591,14 +590,14 @@ def test_negative_create_ssh_key_with_invalid_name(self, create_user): """ invalid_ssh_key_name = gen_string('alpha', length=300) with pytest.raises(HTTPError) as context: - entities.SSHKey( + target_sat.api.SSHKey( user=create_user['user'], name=invalid_ssh_key_name, key=gen_ssh_keypairs()[1] ).create() assert re.search("Name is too long", context.value.response.text) @pytest.mark.tier1 @pytest.mark.upgrade - def test_positive_create_multiple_ssh_key_types(self, create_user): + def test_positive_create_multiple_ssh_key_types(self, create_user, class_target_sat): """Multiple types of ssh keys can be added to user :id: d1ffa908-dc86-40c8-b6f0-20650cc67046 @@ -615,15 +614,15 @@ def test_positive_create_multiple_ssh_key_types(self, create_user): dsa = create_user['data_keys']['ssh_keys']['dsa'] ecdsa = create_user['data_keys']['ssh_keys']['ecdsa'] ed = create_user['data_keys']['ssh_keys']['ed'] - user = entities.User().create() + user = class_target_sat.api.User().create() for key in [rsa, dsa, ecdsa, ed]: - entities.SSHKey(user=user, name=gen_string('alpha'), key=key).create() - user_sshkeys = entities.SSHKey(user=user).search() + class_target_sat.api.SSHKey(user=user, name=gen_string('alpha'), key=key).create() + user_sshkeys = class_target_sat.api.SSHKey(user=user).search() assert len(user_sshkeys) == 4 @pytest.mark.tier2 @pytest.mark.upgrade - def test_positive_ssh_key_in_host_enc(self, target_sat): + def test_positive_ssh_key_in_host_enc(self, class_target_sat): """SSH key appears in host ENC output :id: 4b70a950-e777-4b2d-a83d-29279715fe6d @@ -639,13 +638,15 @@ def test_positive_ssh_key_in_host_enc(self, target_sat): :CaseLevel: Integration """ - org = entities.Organization().create() - loc = entities.Location(organization=[org]).create() - user = entities.User(organization=[org], location=[loc]).create() + org = class_target_sat.api.Organization().create() + loc = class_target_sat.api.Location(organization=[org]).create() + user = class_target_sat.api.User(organization=[org], location=[loc]).create() ssh_key = gen_ssh_keypairs()[1] - entities.SSHKey(user=user, name=gen_string('alpha'), key=ssh_key).create() - host = entities.Host(owner=user, owner_type='User', organization=org, location=loc).create() - sshkey_updated_for_host = f'{ssh_key} {user.login}@{target_sat.hostname}' + class_target_sat.api.SSHKey(user=user, name=gen_string('alpha'), key=ssh_key).create() + host = class_target_sat.api.Host( + owner=user, owner_type='User', organization=org, location=loc + ).create() + sshkey_updated_for_host = f'{ssh_key} {user.login}@{class_target_sat.hostname}' host_enc_key = host.enc()['data']['parameters']['ssh_authorized_keys'] assert sshkey_updated_for_host == host_enc_key[0] @@ -695,7 +696,7 @@ def create_ldap(self, ad_data, module_target_sat): @pytest.mark.tier2 @pytest.mark.upgrade @pytest.mark.parametrize('username', **parametrized(valid_usernames_list())) - def test_positive_create_in_ldap_mode(self, username, create_ldap): + def test_positive_create_in_ldap_mode(self, username, create_ldap, target_sat): """Create User in ldap mode :id: 6f8616b1-5380-40d2-8678-7c4434050cfb @@ -706,14 +707,14 @@ def test_positive_create_in_ldap_mode(self, username, create_ldap): :CaseLevel: Integration """ - user = entities.User( + user = target_sat.api.User( login=username, auth_source=create_ldap['authsource'], password='' ).create() assert user.login == username @pytest.mark.tier3 - def test_positive_ad_basic_no_roles(self, create_ldap): - """Login with LDAP Auth- AD for user with no roles/rights + def test_positive_ad_basic_no_roles(self, create_ldap, target_sat): + """Login with LDAP Auth AD for user with no roles/rights :id: 3910c6eb-6eff-4ab7-a50d-ba40f5c24c08 @@ -721,7 +722,7 @@ def test_positive_ad_basic_no_roles(self, create_ldap): :steps: Login to server with an AD user. - :expectedresults: Log in to foreman successfully but cannot access entities. + :expectedresults: Log in to foreman successfully but cannot access target_sat.api. :CaseLevel: System """ @@ -731,7 +732,7 @@ def test_positive_ad_basic_no_roles(self, create_ldap): verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): - entities.Architecture(sc).search() + target_sat.api.Architecture(sc).search() @pytest.mark.tier3 @pytest.mark.upgrade @@ -764,8 +765,10 @@ def test_positive_access_entities_from_ldap_org_admin(self, create_ldap, module_ user.delete() role_name = gen_string('alpha') - default_org_admin = entities.Role().search(query={'search': 'name="Organization admin"'}) - org_admin = entities.Role(id=default_org_admin[0].id).clone( + default_org_admin = module_target_sat.api.Role().search( + query={'search': 'name="Organization admin"'} + ) + org_admin = module_target_sat.api.Role(id=default_org_admin[0].id).clone( data={ 'role': { 'name': role_name, @@ -780,22 +783,22 @@ def test_positive_access_entities_from_ldap_org_admin(self, create_ldap, module_ verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): - entities.Architecture(sc).search() - user = entities.User().search( + module_target_sat.api.Architecture(sc).search() + user = module_target_sat.api.User().search( query={'search': 'login={}'.format(create_ldap['ldap_user_name'])} )[0] - user.role = [entities.Role(id=org_admin['id']).read()] + user.role = [module_target_sat.api.Role(id=org_admin['id']).read()] user.update(['role']) for entity in [ - entities.Architecture, - entities.Audit, - entities.Bookmark, - entities.CommonParameter, - entities.LibvirtComputeResource, - entities.OVirtComputeResource, - entities.VMWareComputeResource, - entities.Errata, - entities.OperatingSystem, + module_target_sat.api.Architecture, + module_target_sat.api.Audit, + module_target_sat.api.Bookmark, + module_target_sat.api.CommonParameter, + module_target_sat.api.LibvirtComputeResource, + module_target_sat.api.OVirtComputeResource, + module_target_sat.api.VMWareComputeResource, + module_target_sat.api.Errata, + module_target_sat.api.OperatingSystem, ]: entity(sc).search() @@ -843,7 +846,7 @@ def create_ldap(self, class_target_sat): user.delete() @pytest.mark.tier3 - def test_positive_ipa_basic_no_roles(self, create_ldap): + def test_positive_ipa_basic_no_roles(self, create_ldap, target_sat): """Login with LDAP Auth- FreeIPA for user with no roles/rights :id: 901a241d-aa76-4562-ab1a-a752e6fb7ed5 @@ -852,7 +855,7 @@ def test_positive_ipa_basic_no_roles(self, create_ldap): :steps: Login to server with an FreeIPA user. - :expectedresults: Log in to foreman successfully but cannot access entities. + :expectedresults: Log in to foreman successfully but cannot access target_sat.api. :CaseLevel: System """ @@ -862,11 +865,11 @@ def test_positive_ipa_basic_no_roles(self, create_ldap): verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): - entities.Architecture(sc).search() + target_sat.api.Architecture(sc).search() @pytest.mark.tier3 @pytest.mark.upgrade - def test_positive_access_entities_from_ipa_org_admin(self, create_ldap): + def test_positive_access_entities_from_ipa_org_admin(self, create_ldap, target_sat): """LDAP FreeIPA User can access resources within its taxonomies if assigned role has permission for same taxonomies @@ -885,8 +888,10 @@ def test_positive_access_entities_from_ipa_org_admin(self, create_ldap): :CaseLevel: System """ role_name = gen_string('alpha') - default_org_admin = entities.Role().search(query={'search': 'name="Organization admin"'}) - org_admin = entities.Role(id=default_org_admin[0].id).clone( + default_org_admin = target_sat.api.Role().search( + query={'search': 'name="Organization admin"'} + ) + org_admin = target_sat.api.Role(id=default_org_admin[0].id).clone( data={ 'role': { 'name': role_name, @@ -901,22 +906,22 @@ def test_positive_access_entities_from_ipa_org_admin(self, create_ldap): verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): - entities.Architecture(sc).search() - user = entities.User().search(query={'search': 'login={}'.format(create_ldap['username'])})[ - 0 - ] - user.role = [entities.Role(id=org_admin['id']).read()] + target_sat.api.Architecture(sc).search() + user = target_sat.api.User().search( + query={'search': 'login={}'.format(create_ldap['username'])} + )[0] + user.role = [target_sat.api.Role(id=org_admin['id']).read()] user.update(['role']) for entity in [ - entities.Architecture, - entities.Audit, - entities.Bookmark, - entities.CommonParameter, - entities.LibvirtComputeResource, - entities.OVirtComputeResource, - entities.VMWareComputeResource, - entities.Errata, - entities.OperatingSystem, + target_sat.api.Architecture, + target_sat.api.Audit, + target_sat.api.Bookmark, + target_sat.api.CommonParameter, + target_sat.api.LibvirtComputeResource, + target_sat.api.OVirtComputeResource, + target_sat.api.VMWareComputeResource, + target_sat.api.Errata, + target_sat.api.OperatingSystem, ]: entity(sc).search() diff --git a/tests/foreman/api/test_usergroup.py b/tests/foreman/api/test_usergroup.py index 01dcef27010..a01ee213a38 100644 --- a/tests/foreman/api/test_usergroup.py +++ b/tests/foreman/api/test_usergroup.py @@ -22,7 +22,6 @@ from random import randint from fauxfactory import gen_string -from nailgun import entities import pytest from requests.exceptions import HTTPError @@ -38,12 +37,12 @@ class TestUserGroup: """Tests for the ``usergroups`` path.""" @pytest.fixture - def user_group(self): - return entities.UserGroup().create() + def user_group(self, target_sat): + return target_sat.api.UserGroup().create() @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(valid_data_list())) - def test_positive_create_with_name(self, name): + def test_positive_create_with_name(self, target_sat, name): """Create new user group using different valid names :id: 3a2255d9-f48d-4f22-a4b9-132361bd9224 @@ -54,12 +53,12 @@ def test_positive_create_with_name(self, name): :CaseImportance: Critical """ - user_group = entities.UserGroup(name=name).create() + user_group = target_sat.api.UserGroup(name=name).create() assert user_group.name == name @pytest.mark.tier1 @pytest.mark.parametrize('login', **parametrized(valid_usernames_list())) - def test_positive_create_with_user(self, login): + def test_positive_create_with_user(self, target_sat, login): """Create new user group using valid user attached to that group. :id: ab127e09-31d2-4c5b-ae6c-726e4b11a21e @@ -70,13 +69,13 @@ def test_positive_create_with_user(self, login): :CaseImportance: Critical """ - user = entities.User(login=login).create() - user_group = entities.UserGroup(user=[user]).create() + user = target_sat.api.User(login=login).create() + user_group = target_sat.api.UserGroup(user=[user]).create() assert len(user_group.user) == 1 assert user_group.user[0].read().login == login @pytest.mark.tier1 - def test_positive_create_with_users(self): + def test_positive_create_with_users(self, target_sat): """Create new user group using multiple users attached to that group. :id: b8dbbacd-b5cb-49b1-985d-96df21440652 @@ -86,15 +85,15 @@ def test_positive_create_with_users(self): :CaseImportance: Critical """ - users = [entities.User().create() for _ in range(randint(3, 5))] - user_group = entities.UserGroup(user=users).create() + users = [target_sat.api.User().create() for _ in range(randint(3, 5))] + user_group = target_sat.api.UserGroup(user=users).create() assert sorted(user.login for user in users) == sorted( user.read().login for user in user_group.user ) @pytest.mark.tier1 @pytest.mark.parametrize('role_name', **parametrized(valid_data_list())) - def test_positive_create_with_role(self, role_name): + def test_positive_create_with_role(self, target_sat, role_name): """Create new user group using valid role attached to that group. :id: c4fac71a-9dda-4e5f-a5df-be362d3cbd52 @@ -105,13 +104,13 @@ def test_positive_create_with_role(self, role_name): :CaseImportance: Critical """ - role = entities.Role(name=role_name).create() - user_group = entities.UserGroup(role=[role]).create() + role = target_sat.api.Role(name=role_name).create() + user_group = target_sat.api.UserGroup(role=[role]).create() assert len(user_group.role) == 1 assert user_group.role[0].read().name == role_name @pytest.mark.tier1 - def test_positive_create_with_roles(self): + def test_positive_create_with_roles(self, target_sat): """Create new user group using multiple roles attached to that group. :id: 5838fcfd-e256-49cf-aef8-b2bf215b3586 @@ -121,15 +120,15 @@ def test_positive_create_with_roles(self): :CaseImportance: Critical """ - roles = [entities.Role().create() for _ in range(randint(3, 5))] - user_group = entities.UserGroup(role=roles).create() + roles = [target_sat.api.Role().create() for _ in range(randint(3, 5))] + user_group = target_sat.api.UserGroup(role=roles).create() assert sorted(role.name for role in roles) == sorted( role.read().name for role in user_group.role ) @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(valid_data_list())) - def test_positive_create_with_usergroup(self, name): + def test_positive_create_with_usergroup(self, target_sat, name): """Create new user group using another user group attached to the initial group. @@ -141,13 +140,13 @@ def test_positive_create_with_usergroup(self, name): :CaseImportance: Critical """ - sub_user_group = entities.UserGroup(name=name).create() - user_group = entities.UserGroup(usergroup=[sub_user_group]).create() + sub_user_group = target_sat.api.UserGroup(name=name).create() + user_group = target_sat.api.UserGroup(usergroup=[sub_user_group]).create() assert len(user_group.usergroup) == 1 assert user_group.usergroup[0].read().name == name @pytest.mark.tier2 - def test_positive_create_with_usergroups(self): + def test_positive_create_with_usergroups(self, target_sat): """Create new user group using multiple user groups attached to that initial group. @@ -158,15 +157,15 @@ def test_positive_create_with_usergroups(self): :CaseLevel: Integration """ - sub_user_groups = [entities.UserGroup().create() for _ in range(randint(3, 5))] - user_group = entities.UserGroup(usergroup=sub_user_groups).create() + sub_user_groups = [target_sat.api.UserGroup().create() for _ in range(randint(3, 5))] + user_group = target_sat.api.UserGroup(usergroup=sub_user_groups).create() assert sorted(usergroup.name for usergroup in sub_user_groups) == sorted( usergroup.read().name for usergroup in user_group.usergroup ) @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) - def test_negative_create_with_name(self, name): + def test_negative_create_with_name(self, target_sat, name): """Attempt to create user group with invalid name. :id: 1a3384dc-5d52-442c-87c8-e38048a61dfa @@ -178,10 +177,10 @@ def test_negative_create_with_name(self, name): :CaseImportance: Critical """ with pytest.raises(HTTPError): - entities.UserGroup(name=name).create() + target_sat.api.UserGroup(name=name).create() @pytest.mark.tier1 - def test_negative_create_with_same_name(self): + def test_negative_create_with_same_name(self, target_sat): """Attempt to create user group with a name of already existent entity. :id: aba0925a-d5ec-4e90-86c6-404b9b6f0179 @@ -190,9 +189,9 @@ def test_negative_create_with_same_name(self): :CaseImportance: Critical """ - user_group = entities.UserGroup().create() + user_group = target_sat.api.UserGroup().create() with pytest.raises(HTTPError): - entities.UserGroup(name=user_group.name).create() + target_sat.api.UserGroup(name=user_group.name).create() @pytest.mark.tier1 @pytest.mark.parametrize('new_name', **parametrized(valid_data_list())) @@ -212,7 +211,7 @@ def test_positive_update(self, user_group, new_name): assert new_name == user_group.name @pytest.mark.tier1 - def test_positive_update_with_new_user(self): + def test_positive_update_with_new_user(self, target_sat): """Add new user to user group :id: e11b57c3-5f86-4963-9cc6-e10e2f02468b @@ -221,14 +220,14 @@ def test_positive_update_with_new_user(self): :CaseImportance: Critical """ - user = entities.User().create() - user_group = entities.UserGroup().create() + user = target_sat.api.User().create() + user_group = target_sat.api.UserGroup().create() user_group.user = [user] user_group = user_group.update(['user']) assert user.login == user_group.user[0].read().login @pytest.mark.tier2 - def test_positive_update_with_existing_user(self): + def test_positive_update_with_existing_user(self, target_sat): """Update user that assigned to user group with another one :id: 71b78f64-867d-4bf5-9b1e-02698a17fb38 @@ -237,14 +236,14 @@ def test_positive_update_with_existing_user(self): :CaseLevel: Integration """ - users = [entities.User().create() for _ in range(2)] - user_group = entities.UserGroup(user=[users[0]]).create() + users = [target_sat.api.User().create() for _ in range(2)] + user_group = target_sat.api.UserGroup(user=[users[0]]).create() user_group.user[0] = users[1] user_group = user_group.update(['user']) assert users[1].login == user_group.user[0].read().login @pytest.mark.tier1 - def test_positive_update_with_new_role(self): + def test_positive_update_with_new_role(self, target_sat): """Add new role to user group :id: 8e0872c1-ae88-4971-a6fc-cd60127d6663 @@ -253,15 +252,15 @@ def test_positive_update_with_new_role(self): :CaseImportance: Critical """ - new_role = entities.Role().create() - user_group = entities.UserGroup().create() + new_role = target_sat.api.Role().create() + user_group = target_sat.api.UserGroup().create() user_group.role = [new_role] user_group = user_group.update(['role']) assert new_role.name == user_group.role[0].read().name @pytest.mark.tier1 @pytest.mark.upgrade - def test_positive_update_with_new_usergroup(self): + def test_positive_update_with_new_usergroup(self, target_sat): """Add new user group to existing one :id: 3cb29d07-5789-4f94-9fd9-a7e494b3c110 @@ -270,8 +269,8 @@ def test_positive_update_with_new_usergroup(self): :CaseImportance: Critical """ - new_usergroup = entities.UserGroup().create() - user_group = entities.UserGroup().create() + new_usergroup = target_sat.api.UserGroup().create() + user_group = target_sat.api.UserGroup().create() user_group.usergroup = [new_usergroup] user_group = user_group.update(['usergroup']) assert new_usergroup.name == user_group.usergroup[0].read().name @@ -295,7 +294,7 @@ def test_negative_update(self, user_group, new_name): assert user_group.read().name != new_name @pytest.mark.tier1 - def test_negative_update_with_same_name(self): + def test_negative_update_with_same_name(self, target_sat): """Attempt to update user group with a name of already existent entity. :id: 14888998-9282-4d81-9e99-234d19706783 @@ -305,15 +304,15 @@ def test_negative_update_with_same_name(self): :CaseImportance: Critical """ name = gen_string('alphanumeric') - entities.UserGroup(name=name).create() - new_user_group = entities.UserGroup().create() + target_sat.api.UserGroup(name=name).create() + new_user_group = target_sat.api.UserGroup().create() new_user_group.name = name with pytest.raises(HTTPError): new_user_group.update(['name']) assert new_user_group.read().name != name @pytest.mark.tier1 - def test_positive_delete(self): + def test_positive_delete(self, target_sat): """Create user group with valid name and then delete it :id: c5cfcc4a-9177-47bb-8f19-7a8930eb7ca3 @@ -322,7 +321,7 @@ def test_positive_delete(self): :CaseImportance: Critical """ - user_group = entities.UserGroup().create() + user_group = target_sat.api.UserGroup().create() user_group.delete() with pytest.raises(HTTPError): user_group.read() diff --git a/tests/foreman/api/test_webhook.py b/tests/foreman/api/test_webhook.py index b806dbb24c0..a220b19af99 100644 --- a/tests/foreman/api/test_webhook.py +++ b/tests/foreman/api/test_webhook.py @@ -18,7 +18,6 @@ """ import re -from nailgun import entities import pytest from requests.exceptions import HTTPError from wait_for import TimedOutError, wait_for @@ -68,7 +67,7 @@ def assert_event_triggered(channel, event): class TestWebhook: @pytest.mark.tier2 - def test_negative_invalid_event(self): + def test_negative_invalid_event(self, target_sat): """Test negative webhook creation with an invalid event :id: 60cd456a-9943-45cb-a72e-23a83a691499 @@ -78,11 +77,11 @@ def test_negative_invalid_event(self): :CaseImportance: High """ with pytest.raises(HTTPError): - entities.Webhooks(event='invalid_event').create() + target_sat.api.Webhooks(event='invalid_event').create() @pytest.mark.tier2 @pytest.mark.parametrize('event', **parametrized(WEBHOOK_EVENTS)) - def test_positive_valid_event(self, event): + def test_positive_valid_event(self, event, target_sat): """Test positive webhook creation with a valid event :id: 9b505f1b-7ee1-4362-b44c-f3107d043a05 @@ -91,11 +90,11 @@ def test_positive_valid_event(self, event): :CaseImportance: High """ - hook = entities.Webhooks(event=event).create() + hook = target_sat.api.Webhooks(event=event).create() assert event in hook.event @pytest.mark.tier2 - def test_negative_invalid_method(self): + def test_negative_invalid_method(self, target_sat): """Test negative webhook creation with an invalid HTTP method :id: 573be312-7bf3-4d9e-aca1-e5cac810d04b @@ -105,11 +104,11 @@ def test_negative_invalid_method(self): :CaseImportance: High """ with pytest.raises(HTTPError): - entities.Webhooks(http_method='NONE').create() + target_sat.api.Webhooks(http_method='NONE').create() @pytest.mark.tier2 @pytest.mark.parametrize('method', **parametrized(WEBHOOK_METHODS)) - def test_positive_valid_method(self, method): + def test_positive_valid_method(self, method, target_sat): """Test positive webhook creation with a valid HTTP method :id: cf8f276a-d21e-44d0-92f2-657232240c7e @@ -118,12 +117,12 @@ def test_positive_valid_method(self, method): :CaseImportance: High """ - hook = entities.Webhooks(http_method=method).create() + hook = target_sat.api.Webhooks(http_method=method).create() assert hook.http_method == method @pytest.mark.tier1 @pytest.mark.e2e - def test_positive_end_to_end(self): + def test_positive_end_to_end(self, target_sat): """Create a new webhook. :id: 7593a04e-cf7e-414c-9e7e-3fe2936cc32a @@ -132,7 +131,7 @@ def test_positive_end_to_end(self): :CaseImportance: Critical """ - hook = entities.Webhooks().create() + hook = target_sat.api.Webhooks().create() assert hook hook.name = "testing" hook.http_method = "GET" @@ -149,7 +148,7 @@ def test_positive_end_to_end(self): (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) @pytest.mark.tier2 - def test_positive_event_triggered(self, module_org, target_sat): + def test_positive_event_triggered(self, module_org, module_target_sat): """Create a webhook and trigger the event associated with it. @@ -160,13 +159,13 @@ def test_positive_event_triggered(self, module_org, target_sat): :CaseImportance: Critical """ - hook = entities.Webhooks( + hook = module_target_sat.api.Webhooks( event='actions.katello.repository.sync_succeeded', http_method='GET' ).create() - repo = entities.Repository( + repo = module_target_sat.api.Repository( organization=module_org, content_type='yum', url=settings.repos.yum_0.url ).create() - with target_sat.session.shell() as shell: + with module_target_sat.api.session.shell() as shell: shell.send('foreman-tail') repo.sync() assert_event_triggered(shell, hook.event) From b0e08faef46c078d4245577f3e41fc319c1f24c8 Mon Sep 17 00:00:00 2001 From: Peter Ondrejka Date: Mon, 13 Nov 2023 16:57:47 +0100 Subject: [PATCH 317/586] [6.14] improved webhook event trigger test (#13077) improved webhook event trigger test --- tests/foreman/api/test_webhook.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/foreman/api/test_webhook.py b/tests/foreman/api/test_webhook.py index a220b19af99..019ac42484c 100644 --- a/tests/foreman/api/test_webhook.py +++ b/tests/foreman/api/test_webhook.py @@ -148,7 +148,9 @@ def test_positive_end_to_end(self, target_sat): (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) @pytest.mark.tier2 - def test_positive_event_triggered(self, module_org, module_target_sat): + @pytest.mark.e2e + @pytest.mark.parametrize('setting_update', ['safemode_render=False'], indirect=True) + def test_positive_event_triggered(self, module_org, target_sat, setting_update): """Create a webhook and trigger the event associated with it. @@ -159,13 +161,14 @@ def test_positive_event_triggered(self, module_org, module_target_sat): :CaseImportance: Critical """ - hook = module_target_sat.api.Webhooks( + hook = target_sat.api.Webhooks( event='actions.katello.repository.sync_succeeded', http_method='GET' ).create() - repo = module_target_sat.api.Repository( + repo = target_sat.api.Repository( organization=module_org, content_type='yum', url=settings.repos.yum_0.url ).create() - with module_target_sat.api.session.shell() as shell: + with target_sat.session.shell() as shell: shell.send('foreman-tail') repo.sync() assert_event_triggered(shell, hook.event) + target_sat.wait_for_tasks(f'Deliver webhook {hook.name}') From 7887ce11ac3c43d9bdebaa70dba00975c61f1da4 Mon Sep 17 00:00:00 2001 From: Jake Callahan Date: Tue, 14 Nov 2023 11:45:12 -0500 Subject: [PATCH 318/586] [6.14.z] Add Ruff pytest standards (#13061) Add Ruff pytest standards This is a big one! Most of the changes are automatic, but a number of these are manual. I've deselected rules relating to fixture naming, but we can have a conversation later about whether we want to adopt the underscore naming conventions for fixtures. --- pyproject.toml | 6 ++++ pytest_fixtures/component/http_proxy.py | 2 +- pytest_fixtures/component/katello_agent.py | 6 ++-- .../component/katello_certs_check.py | 2 +- pytest_fixtures/component/lce.py | 4 +-- pytest_fixtures/component/maintain.py | 4 +-- pytest_fixtures/component/provision_azure.py | 8 ++--- pytest_fixtures/component/provision_gce.py | 10 +++--- .../component/provision_libvirt.py | 2 +- pytest_fixtures/component/puppet.py | 8 ++--- pytest_fixtures/component/repository.py | 6 ++-- pytest_fixtures/component/rh_cloud.py | 6 ++-- pytest_fixtures/component/satellite_auth.py | 14 ++++---- pytest_fixtures/component/settings.py | 2 +- pytest_fixtures/component/taxonomy.py | 8 ++--- pytest_fixtures/component/templatesync.py | 6 ++-- pytest_fixtures/core/contenthosts.py | 10 +++--- pytest_fixtures/core/sat_cap_factory.py | 12 +++---- pytest_fixtures/core/sys.py | 2 +- pytest_fixtures/core/upgrade.py | 4 +-- tests/foreman/api/test_activationkey.py | 2 +- tests/foreman/api/test_bookmarks.py | 4 +-- tests/foreman/api/test_capsulecontent.py | 2 +- tests/foreman/api/test_classparameters.py | 10 +++--- .../api/test_computeresource_libvirt.py | 6 ++-- tests/foreman/api/test_contentview.py | 6 ++-- tests/foreman/api/test_discoveredhost.py | 2 +- tests/foreman/api/test_docker.py | 2 +- tests/foreman/api/test_foremantask.py | 2 +- tests/foreman/api/test_http_proxy.py | 2 +- .../foreman/api/test_lifecycleenvironment.py | 2 +- tests/foreman/api/test_media.py | 2 +- tests/foreman/api/test_partitiontable.py | 4 +-- tests/foreman/api/test_permission.py | 13 ++++---- tests/foreman/api/test_repositories.py | 8 ++--- tests/foreman/api/test_repository.py | 3 +- tests/foreman/api/test_rhcloud_inventory.py | 3 +- tests/foreman/api/test_rhsm.py | 2 +- tests/foreman/api/test_role.py | 5 ++- tests/foreman/api/test_subnet.py | 2 +- tests/foreman/api/test_user.py | 4 +-- tests/foreman/cli/test_activationkey.py | 6 ++-- tests/foreman/cli/test_computeresource_osp.py | 2 +- tests/foreman/cli/test_contentview.py | 2 +- tests/foreman/cli/test_discoveryrule.py | 2 +- tests/foreman/cli/test_errata.py | 16 ++++----- tests/foreman/cli/test_filter.py | 2 +- tests/foreman/cli/test_host.py | 33 +++++++++---------- tests/foreman/cli/test_ldapauthsource.py | 2 +- tests/foreman/cli/test_leapp_client.py | 2 +- tests/foreman/cli/test_model.py | 4 +-- tests/foreman/cli/test_partitiontable.py | 2 +- tests/foreman/cli/test_remoteexecution.py | 10 +++--- tests/foreman/cli/test_reporttemplates.py | 2 +- tests/foreman/cli/test_role.py | 13 ++++---- tests/foreman/cli/test_satellitesync.py | 32 +++++++++--------- tests/foreman/cli/test_subnet.py | 8 ++--- tests/foreman/cli/test_user.py | 2 +- tests/foreman/cli/test_usergroup.py | 18 +++++----- tests/foreman/cli/test_webhook.py | 2 +- .../destructive/test_capsule_loadbalancer.py | 2 +- tests/foreman/destructive/test_infoblox.py | 2 +- .../destructive/test_ldap_authentication.py | 18 +++++----- .../destructive/test_ldapauthsource.py | 14 +++----- tests/foreman/destructive/test_ping.py | 2 +- tests/foreman/maintain/test_health.py | 10 +++--- tests/foreman/sys/test_katello_certs_check.py | 4 +-- tests/foreman/ui/test_acs.py | 2 +- tests/foreman/ui/test_contenthost.py | 7 ++-- tests/foreman/ui/test_contentview.py | 24 ++++++++------ tests/foreman/ui/test_discoveryrule.py | 2 +- tests/foreman/ui/test_errata.py | 4 +-- tests/foreman/ui/test_host.py | 8 ++--- tests/foreman/ui/test_jobinvocation.py | 2 +- tests/foreman/ui/test_ldap_authentication.py | 16 ++++----- tests/foreman/ui/test_partitiontable.py | 2 +- tests/foreman/ui/test_provisioningtemplate.py | 2 +- tests/foreman/ui/test_repository.py | 11 ++++--- tests/foreman/ui/test_rhc.py | 2 +- tests/foreman/ui/test_settings.py | 12 +++---- tests/foreman/ui/test_subscription.py | 3 +- tests/foreman/virtwho/api/test_esx.py | 5 ++- tests/foreman/virtwho/api/test_esx_sca.py | 5 ++- tests/foreman/virtwho/api/test_nutanix.py | 5 ++- tests/foreman/virtwho/cli/test_esx.py | 9 +++-- tests/foreman/virtwho/cli/test_esx_sca.py | 9 +++-- tests/foreman/virtwho/cli/test_hyperv.py | 4 +-- tests/foreman/virtwho/cli/test_hyperv_sca.py | 4 +-- tests/foreman/virtwho/cli/test_kubevirt.py | 4 +-- .../foreman/virtwho/cli/test_kubevirt_sca.py | 4 +-- tests/foreman/virtwho/cli/test_libvirt.py | 4 +-- tests/foreman/virtwho/cli/test_libvirt_sca.py | 4 +-- tests/foreman/virtwho/cli/test_nutanix.py | 9 +++-- tests/foreman/virtwho/cli/test_nutanix_sca.py | 4 +-- tests/foreman/virtwho/conftest.py | 4 +-- tests/foreman/virtwho/ui/test_esx.py | 5 ++- tests/foreman/virtwho/ui/test_esx_sca.py | 5 ++- tests/foreman/virtwho/ui/test_nutanix.py | 5 ++- tests/foreman/virtwho/ui/test_nutanix_sca.py | 5 ++- tests/robottelo/conftest.py | 4 +-- tests/robottelo/test_cli.py | 2 +- tests/robottelo/test_datafactory.py | 2 +- tests/robottelo/test_decorators.py | 2 +- tests/robottelo/test_func_locker.py | 6 ++-- tests/robottelo/test_func_shared.py | 6 ++-- tests/robottelo/test_issue_handlers.py | 2 +- tests/upgrades/test_activation_key.py | 4 +-- tests/upgrades/test_classparameter.py | 9 ++--- tests/upgrades/test_host.py | 2 +- 109 files changed, 327 insertions(+), 323 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b042ee54c72..7988aa2a58d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,12 +25,15 @@ select = [ "F", # flake8 "I", # isort # "Q", # flake8-quotes + "PT", # flake8-pytest "UP", # pyupgrade "W", # pycodestyle ] ignore = [ "E501", # line too long - handled by black + "PT004", # pytest underscrore prefix for non-return fixtures + "PT005", # pytest no underscrore prefix for return fixtures ] [tool.ruff.isort] @@ -40,6 +43,9 @@ known-first-party = [ ] combine-as-imports = true +[tool.ruff.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false [tool.ruff.flake8-quotes] inline-quotes = "single" diff --git a/pytest_fixtures/component/http_proxy.py b/pytest_fixtures/component/http_proxy.py index ee8efc9c16a..8c6095092dd 100644 --- a/pytest_fixtures/component/http_proxy.py +++ b/pytest_fixtures/component/http_proxy.py @@ -3,7 +3,7 @@ from robottelo.config import settings -@pytest.fixture(scope='function') +@pytest.fixture def setup_http_proxy(request, module_manifest_org, target_sat): """Create a new HTTP proxy and set related settings based on proxy""" http_proxy = target_sat.api_factory.make_http_proxy(module_manifest_org, request.param) diff --git a/pytest_fixtures/component/katello_agent.py b/pytest_fixtures/component/katello_agent.py index 0428f91a740..3fafd4315d0 100644 --- a/pytest_fixtures/component/katello_agent.py +++ b/pytest_fixtures/component/katello_agent.py @@ -21,7 +21,7 @@ def sat_with_katello_agent(module_target_sat): InstallerCommand('foreman-proxy-content-enable-katello-agent true') ) assert result.status == 0 - yield module_target_sat + return module_target_sat @pytest.fixture @@ -33,7 +33,7 @@ def katello_agent_client_for_upgrade(sat_with_katello_agent, sat_upgrade_chost, ) sat_upgrade_chost.install_katello_agent() host_info = sat_with_katello_agent.cli.Host.info({'name': sat_upgrade_chost.hostname}) - yield Box( + return Box( { 'client': sat_upgrade_chost, 'host_info': host_info, @@ -54,4 +54,4 @@ def katello_agent_client(sat_with_katello_agent, rhel_contenthost): ) rhel_contenthost.install_katello_agent() host_info = sat_with_katello_agent.cli.Host.info({'name': rhel_contenthost.hostname}) - yield Box({'client': rhel_contenthost, 'host_info': host_info, 'sat': sat_with_katello_agent}) + return Box({'client': rhel_contenthost, 'host_info': host_info, 'sat': sat_with_katello_agent}) diff --git a/pytest_fixtures/component/katello_certs_check.py b/pytest_fixtures/component/katello_certs_check.py index f14299ebd52..1ee97acc565 100644 --- a/pytest_fixtures/component/katello_certs_check.py +++ b/pytest_fixtures/component/katello_certs_check.py @@ -13,7 +13,7 @@ def certs_data(sat_ready_rhel): cert_data['key_file_name'] = f'{sat_ready_rhel.hostname}/{sat_ready_rhel.hostname}.key' cert_data['cert_file_name'] = f'{sat_ready_rhel.hostname}/{sat_ready_rhel.hostname}.crt' sat_ready_rhel.custom_cert_generate(cert_data['capsule_hostname']) - yield cert_data + return cert_data @pytest.fixture diff --git a/pytest_fixtures/component/lce.py b/pytest_fixtures/component/lce.py index 368c1329131..2a3f113e9c8 100644 --- a/pytest_fixtures/component/lce.py +++ b/pytest_fixtures/component/lce.py @@ -16,7 +16,7 @@ def module_lce(module_org, module_target_sat): return module_target_sat.api.LifecycleEnvironment(organization=module_org).create() -@pytest.fixture(scope='function') +@pytest.fixture def function_lce(function_org, target_sat): return target_sat.api.LifecycleEnvironment(organization=function_org).create() @@ -31,7 +31,7 @@ def module_lce_library(module_org, module_target_sat): ) -@pytest.fixture(scope='function') +@pytest.fixture def function_lce_library(function_org, target_sat): """Returns the Library lifecycle environment from chosen organization""" return ( diff --git a/pytest_fixtures/component/maintain.py b/pytest_fixtures/component/maintain.py index 152656f8dc1..844f522de66 100644 --- a/pytest_fixtures/component/maintain.py +++ b/pytest_fixtures/component/maintain.py @@ -18,7 +18,7 @@ def module_stash(request): # Please refer the documentation for more details on stash # https://docs.pytest.org/en/latest/reference/reference.html#stash request.node.stash[synced_repos] = {} - yield request.node.stash + return request.node.stash @pytest.fixture(scope='module') @@ -98,7 +98,7 @@ def module_synced_repos(sat_maintain, module_capsule_configured, module_sca_mani sync_status = module_capsule_configured.nailgun_capsule.content_sync() assert sync_status['result'] == 'success' - yield { + return { 'custom': module_stash[synced_repos]['cust_repo'], 'rh': module_stash[synced_repos]['rh_repo'], } diff --git a/pytest_fixtures/component/provision_azure.py b/pytest_fixtures/component/provision_azure.py index 0483758b51a..27bcea16e1f 100644 --- a/pytest_fixtures/component/provision_azure.py +++ b/pytest_fixtures/component/provision_azure.py @@ -17,22 +17,22 @@ @pytest.fixture(scope='session') def sat_azure(request, session_puppet_enabled_sat, session_target_sat): hosts = {'sat': session_target_sat, 'puppet_sat': session_puppet_enabled_sat} - yield hosts[request.param] + return hosts[request.param] @pytest.fixture(scope='module') def sat_azure_org(sat_azure): - yield sat_azure.api.Organization().create() + return sat_azure.api.Organization().create() @pytest.fixture(scope='module') def sat_azure_loc(sat_azure): - yield sat_azure.api.Location().create() + return sat_azure.api.Location().create() @pytest.fixture(scope='module') def sat_azure_domain(sat_azure, sat_azure_loc, sat_azure_org): - yield sat_azure.api.Domain(location=[sat_azure_loc], organization=[sat_azure_org]).create() + return sat_azure.api.Domain(location=[sat_azure_loc], organization=[sat_azure_org]).create() @pytest.fixture(scope='module') diff --git a/pytest_fixtures/component/provision_gce.py b/pytest_fixtures/component/provision_gce.py index af260bdb38c..4c11ecb0918 100644 --- a/pytest_fixtures/component/provision_gce.py +++ b/pytest_fixtures/component/provision_gce.py @@ -20,22 +20,22 @@ @pytest.fixture(scope='session') def sat_gce(request, session_puppet_enabled_sat, session_target_sat): hosts = {'sat': session_target_sat, 'puppet_sat': session_puppet_enabled_sat} - yield hosts[getattr(request, 'param', 'sat')] + return hosts[getattr(request, 'param', 'sat')] @pytest.fixture(scope='module') def sat_gce_org(sat_gce): - yield sat_gce.api.Organization().create() + return sat_gce.api.Organization().create() @pytest.fixture(scope='module') def sat_gce_loc(sat_gce): - yield sat_gce.api.Location().create() + return sat_gce.api.Location().create() @pytest.fixture(scope='module') def sat_gce_domain(sat_gce, sat_gce_loc, sat_gce_org): - yield sat_gce.api.Domain(location=[sat_gce_loc], organization=[sat_gce_org]).create() + return sat_gce.api.Domain(location=[sat_gce_loc], organization=[sat_gce_org]).create() @pytest.fixture(scope='module') @@ -282,7 +282,7 @@ def module_gce_finishimg( return finish_image -@pytest.fixture() +@pytest.fixture def gce_setting_update(sat_gce): sat_gce.update_setting('destroy_vm_on_host_delete', True) yield diff --git a/pytest_fixtures/component/provision_libvirt.py b/pytest_fixtures/component/provision_libvirt.py index d8638d9a32a..b294ca27a29 100644 --- a/pytest_fixtures/component/provision_libvirt.py +++ b/pytest_fixtures/component/provision_libvirt.py @@ -18,4 +18,4 @@ def module_libvirt_image(module_target_sat, module_cr_libvirt): def module_libvirt_provisioning_sat(module_provisioning_sat): # Configure Libvirt CR for provisioning module_provisioning_sat.sat.configure_libvirt_cr() - yield module_provisioning_sat + return module_provisioning_sat diff --git a/pytest_fixtures/component/puppet.py b/pytest_fixtures/component/puppet.py index f088e05d0a8..0ff20685674 100644 --- a/pytest_fixtures/component/puppet.py +++ b/pytest_fixtures/component/puppet.py @@ -18,22 +18,22 @@ def session_puppet_enabled_sat(session_satellite_host): def session_puppet_enabled_capsule(session_capsule_host, session_puppet_enabled_sat): """Capsule with enabled puppet plugin""" session_capsule_host.capsule_setup(sat_host=session_puppet_enabled_sat) - yield session_capsule_host.enable_puppet_capsule(satellite=session_puppet_enabled_sat) + return session_capsule_host.enable_puppet_capsule(satellite=session_puppet_enabled_sat) @pytest.fixture(scope='module') def module_puppet_org(session_puppet_enabled_sat): - yield session_puppet_enabled_sat.api.Organization().create() + return session_puppet_enabled_sat.api.Organization().create() @pytest.fixture(scope='module') def module_puppet_loc(session_puppet_enabled_sat): - yield session_puppet_enabled_sat.api.Location().create() + return session_puppet_enabled_sat.api.Location().create() @pytest.fixture(scope='module') def module_puppet_domain(session_puppet_enabled_sat, module_puppet_loc, module_puppet_org): - yield session_puppet_enabled_sat.api.Domain( + return session_puppet_enabled_sat.api.Domain( location=[module_puppet_loc], organization=[module_puppet_org] ).create() diff --git a/pytest_fixtures/component/repository.py b/pytest_fixtures/component/repository.py index 2336bf12e5a..5bbf8a81365 100644 --- a/pytest_fixtures/component/repository.py +++ b/pytest_fixtures/component/repository.py @@ -22,7 +22,7 @@ def module_repo(module_repo_options, module_target_sat): return module_target_sat.api.Repository(**module_repo_options).create() -@pytest.fixture(scope='function') +@pytest.fixture def function_product(function_org): return entities.Product(organization=function_org).create() @@ -73,7 +73,7 @@ def module_rhst_repo(module_target_sat, module_org_with_manifest, module_promote return REPOS['rhst7']['id'] -@pytest.fixture(scope="function") +@pytest.fixture def repo_setup(): """ This fixture is used to create an organization, product, repository, and lifecycle environment @@ -85,7 +85,7 @@ def repo_setup(): repo = entities.Repository(name=repo_name, product=product).create() lce = entities.LifecycleEnvironment(organization=org).create() details = {'org': org, 'product': product, 'repo': repo, 'lce': lce} - yield details + return details @pytest.fixture(scope='module') diff --git a/pytest_fixtures/component/rh_cloud.py b/pytest_fixtures/component/rh_cloud.py index 617261c98c5..ead92f68db2 100644 --- a/pytest_fixtures/component/rh_cloud.py +++ b/pytest_fixtures/component/rh_cloud.py @@ -25,7 +25,7 @@ def rhcloud_activation_key(module_target_sat, rhcloud_manifest_org): purpose_role='test-role', auto_attach=False, ).create() - yield ak + return ak @pytest.fixture(scope='module') @@ -62,7 +62,7 @@ def rhel_insights_vm( module_target_sat.generate_inventory_report(rhcloud_manifest_org) # Sync inventory status module_target_sat.sync_inventory_status(rhcloud_manifest_org) - yield rhel_contenthost + return rhel_contenthost @pytest.fixture @@ -90,4 +90,4 @@ def rhcloud_capsule(module_capsule_host, module_target_sat, rhcloud_manifest_org 'location-ids': default_location.id, } ) - yield module_capsule_host + return module_capsule_host diff --git a/pytest_fixtures/component/satellite_auth.py b/pytest_fixtures/component/satellite_auth.py index 9c39ed3ade3..0a1031c9e25 100644 --- a/pytest_fixtures/component/satellite_auth.py +++ b/pytest_fixtures/component/satellite_auth.py @@ -35,7 +35,7 @@ def default_ipa_host(module_target_sat): return IPAHost(module_target_sat) -@pytest.fixture() +@pytest.fixture def ldap_cleanup(): """this is an extra step taken to clean any existing ldap source""" ldap_auth_sources = entities.AuthSourceLDAP().search() @@ -44,7 +44,7 @@ def ldap_cleanup(): for user in users: user.delete() ldap_auth.delete() - yield + return @pytest.fixture(scope='session') @@ -104,7 +104,7 @@ def open_ldap_data(): } -@pytest.fixture(scope='function') +@pytest.fixture def auth_source(ldap_cleanup, module_org, module_location, ad_data): ad_data = ad_data() return entities.AuthSourceLDAP( @@ -127,7 +127,7 @@ def auth_source(ldap_cleanup, module_org, module_location, ad_data): ).create() -@pytest.fixture(scope='function') +@pytest.fixture def auth_source_ipa(ldap_cleanup, default_ipa_host, module_org, module_location): return entities.AuthSourceLDAP( onthefly_register=True, @@ -149,7 +149,7 @@ def auth_source_ipa(ldap_cleanup, default_ipa_host, module_org, module_location) ).create() -@pytest.fixture(scope='function') +@pytest.fixture def auth_source_open_ldap(ldap_cleanup, module_org, module_location, open_ldap_data): return entities.AuthSourceLDAP( onthefly_register=True, @@ -259,7 +259,7 @@ def ldap_auth_source( else: ldap_data['server_type'] = LDAP_SERVER_TYPE['UI']['posix'] ldap_data['attr_login'] = LDAP_ATTR['login'] - yield ldap_data, auth_source + return ldap_data, auth_source @pytest.fixture @@ -459,7 +459,7 @@ def configure_hammer_no_negotiate(parametrized_enrolled_sat): @pytest.mark.external_auth -@pytest.fixture(scope='function') +@pytest.fixture def hammer_logout(parametrized_enrolled_sat): """Logout in Hammer.""" result = parametrized_enrolled_sat.cli.Auth.logout() diff --git a/pytest_fixtures/component/settings.py b/pytest_fixtures/component/settings.py index 937379bd6d6..b541e703733 100644 --- a/pytest_fixtures/component/settings.py +++ b/pytest_fixtures/component/settings.py @@ -2,7 +2,7 @@ import pytest -@pytest.fixture() +@pytest.fixture def setting_update(request, target_sat): """ This fixture is used to create an object of the provided settings parameter that we use in diff --git a/pytest_fixtures/component/taxonomy.py b/pytest_fixtures/component/taxonomy.py index c6140cebb63..ebfc9126eac 100644 --- a/pytest_fixtures/component/taxonomy.py +++ b/pytest_fixtures/component/taxonomy.py @@ -212,7 +212,7 @@ def class_sca_manifest(): yield manifest -@pytest.fixture(scope='function') +@pytest.fixture def function_entitlement_manifest(): """Yields a manifest in entitlement mode with subscriptions determined by the `manifest_category.entitlement` setting in conf/manifest.yaml.""" @@ -220,7 +220,7 @@ def function_entitlement_manifest(): yield manifest -@pytest.fixture(scope='function') +@pytest.fixture def function_secondary_entitlement_manifest(): """Yields a manifest in entitlement mode with subscriptions determined by the `manifest_category.entitlement` setting in conf/manifest.yaml. @@ -229,7 +229,7 @@ def function_secondary_entitlement_manifest(): yield manifest -@pytest.fixture(scope='function') +@pytest.fixture def function_sca_manifest(): """Yields a manifest in Simple Content Access mode with subscriptions determined by the `manifest_category.golden_ticket` setting in conf/manifest.yaml.""" @@ -245,7 +245,7 @@ def smart_proxy_location(module_org, module_target_sat, default_smart_proxy): return location -@pytest.fixture(scope='function') +@pytest.fixture def upgrade_entitlement_manifest(): """Returns a manifest in entitlement mode with subscriptions determined by the `manifest_category.entitlement` setting in conf/manifest.yaml. used only for diff --git a/pytest_fixtures/component/templatesync.py b/pytest_fixtures/component/templatesync.py index 3dee193cdd3..1e195ec838f 100644 --- a/pytest_fixtures/component/templatesync.py +++ b/pytest_fixtures/component/templatesync.py @@ -7,7 +7,7 @@ from robottelo.logging import logger -@pytest.fixture() +@pytest.fixture def create_import_export_local_dir(target_sat): """Creates a local directory inside root_dir on satellite from where the templates will be imported from or exported to. @@ -76,7 +76,7 @@ def git_pub_key(session_target_sat, git_port): res.raise_for_status() -@pytest.fixture(scope='function') +@pytest.fixture def git_repository(git_port, git_pub_key, request): """Creates a new repository on git provider for exporting templates. @@ -96,7 +96,7 @@ def git_repository(git_port, git_pub_key, request): res.raise_for_status() -@pytest.fixture() +@pytest.fixture def git_branch(git_repository): """Creates a new branch in the git repository for exporting templates. diff --git a/pytest_fixtures/core/contenthosts.py b/pytest_fixtures/core/contenthosts.py index 586fa95eb57..485591ed7d9 100644 --- a/pytest_fixtures/core/contenthosts.py +++ b/pytest_fixtures/core/contenthosts.py @@ -94,7 +94,7 @@ def rhel9_contenthost(request): yield host -@pytest.fixture() +@pytest.fixture def content_hosts(request): """A function-level fixture that provides two rhel content hosts object""" with Broker(**host_conf(request), host_class=ContentHost, _count=2) as hosts: @@ -110,7 +110,7 @@ def mod_content_hosts(request): yield hosts -@pytest.fixture() +@pytest.fixture def registered_hosts(request, target_sat, module_org, module_ak_with_cv): """Fixture that registers content hosts to Satellite, based on rh_cloud setup""" with Broker(**host_conf(request), host_class=ContentHost, _count=2) as hosts: @@ -134,7 +134,7 @@ def katello_host_tools_host(target_sat, module_org, rhel_contenthost): rhel_contenthost.register(module_org, None, ak.name, target_sat, repo=repo) rhel_contenthost.install_katello_host_tools() - yield rhel_contenthost + return rhel_contenthost @pytest.fixture @@ -149,7 +149,7 @@ def cockpit_host(class_target_sat, class_org, rhel_contenthost): rhel_contenthost.execute(f"hostnamectl set-hostname {rhel_contenthost.hostname} --static") rhel_contenthost.install_cockpit() rhel_contenthost.add_rex_key(satellite=class_target_sat) - yield rhel_contenthost + return rhel_contenthost @pytest.fixture @@ -174,7 +174,7 @@ def katello_host_tools_tracer_host(rex_contenthost, target_sat): **{f'rhel{rhelver}_os': settings.repos[f'rhel{rhelver}_os']} ) rex_contenthost.install_tracer() - yield rex_contenthost + return rex_contenthost @pytest.fixture diff --git a/pytest_fixtures/core/sat_cap_factory.py b/pytest_fixtures/core/sat_cap_factory.py index f08e722cbbf..2fbb8cec7b6 100644 --- a/pytest_fixtures/core/sat_cap_factory.py +++ b/pytest_fixtures/core/sat_cap_factory.py @@ -150,28 +150,28 @@ def session_capsule_host(request, capsule_factory): def capsule_configured(capsule_host, target_sat): """Configure the capsule instance with the satellite from settings.server.hostname""" capsule_host.capsule_setup(sat_host=target_sat) - yield capsule_host + return capsule_host @pytest.fixture def large_capsule_configured(large_capsule_host, target_sat): """Configure the capsule instance with the satellite from settings.server.hostname""" large_capsule_host.capsule_setup(sat_host=target_sat) - yield large_capsule_host + return large_capsule_host @pytest.fixture(scope='module') def module_capsule_configured(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) - yield module_capsule_host + return module_capsule_host @pytest.fixture(scope='session') def session_capsule_configured(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) - yield session_capsule_host + return session_capsule_host @pytest.fixture(scope='module') @@ -186,7 +186,7 @@ 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') - yield module_capsule_configured + return module_capsule_configured @pytest.fixture(scope='module') @@ -218,7 +218,7 @@ def module_capsule_configured_async_ssh(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('ssh-async') - yield module_capsule_configured + return module_capsule_configured @pytest.fixture(scope='module', params=['IDM', 'AD']) diff --git a/pytest_fixtures/core/sys.py b/pytest_fixtures/core/sys.py index b9106eac7ad..88768508ee4 100644 --- a/pytest_fixtures/core/sys.py +++ b/pytest_fixtures/core/sys.py @@ -54,7 +54,7 @@ def puppet_proxy_port_range(session_puppet_enabled_sat): @pytest.fixture(scope='class') def class_cockpit_sat(class_subscribe_satellite): class_subscribe_satellite.install_cockpit() - yield class_subscribe_satellite + return class_subscribe_satellite @pytest.fixture(scope='module') diff --git a/pytest_fixtures/core/upgrade.py b/pytest_fixtures/core/upgrade.py index a79f4867979..caf4532713b 100644 --- a/pytest_fixtures/core/upgrade.py +++ b/pytest_fixtures/core/upgrade.py @@ -5,7 +5,7 @@ from robottelo.logging import logger -@pytest.fixture(scope="function") +@pytest.fixture def dependent_scenario_name(request): """ This fixture is used to collect the dependent test case name. @@ -15,7 +15,7 @@ def dependent_scenario_name(request): for mark in request.node.own_markers if 'depend_on' in mark.kwargs ][0] - yield depend_test_name + return depend_test_name @pytest.fixture(scope="session") diff --git a/tests/foreman/api/test_activationkey.py b/tests/foreman/api/test_activationkey.py index 8c6f9ea194d..16e968ed7e8 100644 --- a/tests/foreman/api/test_activationkey.py +++ b/tests/foreman/api/test_activationkey.py @@ -296,7 +296,7 @@ def test_positive_get_releases_content(target_sat): act_key = target_sat.api.ActivationKey().create() response = client.get(act_key.path('releases'), auth=get_credentials(), verify=False).json() assert 'results' in response.keys() - assert type(response['results']) == list + assert isinstance(response['results'], list) @pytest.mark.tier2 diff --git a/tests/foreman/api/test_bookmarks.py b/tests/foreman/api/test_bookmarks.py index 6ae674f7699..694d0a6654a 100644 --- a/tests/foreman/api/test_bookmarks.py +++ b/tests/foreman/api/test_bookmarks.py @@ -82,7 +82,7 @@ def test_positive_create_with_query(controller, target_sat): @pytest.mark.tier1 -@pytest.mark.parametrize('public', (True, False)) +@pytest.mark.parametrize('public', [True, False]) @pytest.mark.parametrize('controller', CONTROLLERS) def test_positive_create_public(controller, public, target_sat): """Create a public bookmark @@ -367,7 +367,7 @@ def test_negative_update_empty_query(controller, target_sat): @pytest.mark.tier1 -@pytest.mark.parametrize('public', (True, False)) +@pytest.mark.parametrize('public', [True, False]) @pytest.mark.parametrize('controller', CONTROLLERS) def test_positive_update_public(controller, public, target_sat): """Update a bookmark public state to private and vice versa diff --git a/tests/foreman/api/test_capsulecontent.py b/tests/foreman/api/test_capsulecontent.py index 2a48b2a434c..2e72bc8998a 100644 --- a/tests/foreman/api/test_capsulecontent.py +++ b/tests/foreman/api/test_capsulecontent.py @@ -1208,7 +1208,7 @@ def test_positive_sync_CV_to_multiple_LCEs( result = module_capsule_configured.nailgun_capsule.content_lifecycle_environments() # there can and will be LCEs from other tests and orgs, but len() >= 2 assert len(result['results']) >= 2 - assert lce1.id and lce2.id in [capsule_lce['id'] for capsule_lce in result['results']] + assert {lce1.id, lce2.id}.issubset([capsule_lce['id'] for capsule_lce in result['results']]) # Create a Content View, add the repository and publish it. cv = target_sat.api.ContentView( diff --git a/tests/foreman/api/test_classparameters.py b/tests/foreman/api/test_classparameters.py index de34f81ae11..446bdf14182 100644 --- a/tests/foreman/api/test_classparameters.py +++ b/tests/foreman/api/test_classparameters.py @@ -137,10 +137,10 @@ def test_negative_update_parameter_type(self, test_data, module_puppet): 2. Error raised for invalid default value. """ sc_param = module_puppet['sc_params'].pop() + sc_param.override = True + sc_param.parameter_type = test_data['sc_type'] + sc_param.default_value = test_data['value'] with pytest.raises(HTTPError) as context: - sc_param.override = True - sc_param.parameter_type = test_data['sc_type'] - sc_param.default_value = test_data['value'] sc_param.update(['override', 'parameter_type', 'default_value']) assert sc_param.read().default_value != test_data['value'] assert 'Validation failed: Default value is invalid' in context.value.response.text @@ -370,9 +370,9 @@ def test_negative_validate_matcher_and_default_value( session_puppet_enabled_sat.api.OverrideValue( smart_class_parameter=sc_param, match='domain=example.com', value=gen_string('alpha') ).create() + sc_param.parameter_type = 'boolean' + sc_param.default_value = gen_string('alpha') with pytest.raises(HTTPError) as context: - sc_param.parameter_type = 'boolean' - sc_param.default_value = gen_string('alpha') sc_param.update(['parameter_type', 'default_value']) assert ( 'Validation failed: Default value is invalid, Lookup values is invalid' diff --git a/tests/foreman/api/test_computeresource_libvirt.py b/tests/foreman/api/test_computeresource_libvirt.py index e7e0112a0d8..a02d741e899 100644 --- a/tests/foreman/api/test_computeresource_libvirt.py +++ b/tests/foreman/api/test_computeresource_libvirt.py @@ -244,8 +244,8 @@ def test_negative_update_invalid_name( location=[module_location], name=name, organization=[module_org], url=LIBVIRT_URL ).create() request.addfinalizer(compresource.delete) + compresource.name = new_name with pytest.raises(HTTPError): - compresource.name = new_name compresource.update(['name']) assert compresource.read().name == name @@ -269,8 +269,8 @@ def test_negative_update_same_name(request, module_target_sat, module_org, modul new_compresource = module_target_sat.api.LibvirtComputeResource( location=[module_location], organization=[module_org], url=LIBVIRT_URL ).create() + new_compresource.name = name with pytest.raises(HTTPError): - new_compresource.name = name new_compresource.update(['name']) assert new_compresource.read().name != name @@ -299,7 +299,7 @@ def test_negative_update_url(url, request, module_target_sat, module_org, module location=[module_location], organization=[module_org], url=LIBVIRT_URL ).create() request.addfinalizer(compresource.delete) + compresource.url = url with pytest.raises(HTTPError): - compresource.url = url compresource.update(['url']) assert compresource.read().url != url diff --git a/tests/foreman/api/test_contentview.py b/tests/foreman/api/test_contentview.py index bab306e29e0..f2015bd11cf 100644 --- a/tests/foreman/api/test_contentview.py +++ b/tests/foreman/api/test_contentview.py @@ -248,8 +248,8 @@ def test_negative_add_dupe_repos( yum_repo = module_target_sat.api.Repository(product=module_product).create() yum_repo.sync() assert len(content_view.repository) == 0 + content_view.repository = [yum_repo, yum_repo] with pytest.raises(HTTPError): - content_view.repository = [yum_repo, yum_repo] content_view.update(['repository']) assert len(content_view.read().repository) == 0 @@ -875,7 +875,7 @@ class TestContentViewUpdate: """Tests for updating content views.""" @pytest.mark.parametrize( - 'key, value', + ('key', 'value'), **(lambda x: {'argvalues': list(x.items()), 'ids': list(x.keys())})( {'description': gen_utf8(), 'name': gen_utf8()} ), @@ -929,8 +929,8 @@ def test_negative_update_name(self, module_cv, new_name, module_target_sat): :CaseImportance: Critical """ + module_cv.name = new_name with pytest.raises(HTTPError): - module_cv.name = new_name module_cv.update(['name']) cv = module_cv.read() assert cv.name != new_name diff --git a/tests/foreman/api/test_discoveredhost.py b/tests/foreman/api/test_discoveredhost.py index e61bf069b0a..70beeba274e 100644 --- a/tests/foreman/api/test_discoveredhost.py +++ b/tests/foreman/api/test_discoveredhost.py @@ -157,7 +157,7 @@ def assert_discovered_host_provisioned(channel, ksrepo): raise AssertionError(f'Timed out waiting for {pattern} from VM') -@pytest.fixture() +@pytest.fixture def discovered_host_cleanup(target_sat): hosts = target_sat.api.DiscoveredHost().search() for host in hosts: diff --git a/tests/foreman/api/test_docker.py b/tests/foreman/api/test_docker.py index 4b6dc88537c..68a2fe6e378 100644 --- a/tests/foreman/api/test_docker.py +++ b/tests/foreman/api/test_docker.py @@ -972,8 +972,8 @@ def test_negative_promote_and_set_non_unique_name_pattern(self, module_org, modu cvv = content_view.read().version[0] lce = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() cvv.promote(data={'environment_ids': lce.id, 'force': False}) + lce.registry_name_pattern = new_pattern with pytest.raises(HTTPError): - lce.registry_name_pattern = new_pattern lce.update(['registry_name_pattern']) diff --git a/tests/foreman/api/test_foremantask.py b/tests/foreman/api/test_foremantask.py index 65918863cac..6d8bee0b42d 100644 --- a/tests/foreman/api/test_foremantask.py +++ b/tests/foreman/api/test_foremantask.py @@ -49,4 +49,4 @@ def test_positive_get_summary(target_sat): summary = target_sat.api.ForemanTask().summary() assert isinstance(summary, list) for item in summary: - assert type(item) is dict + assert isinstance(item, dict) diff --git a/tests/foreman/api/test_http_proxy.py b/tests/foreman/api/test_http_proxy.py index ed713078b4f..0b4446069bb 100644 --- a/tests/foreman/api/test_http_proxy.py +++ b/tests/foreman/api/test_http_proxy.py @@ -292,7 +292,7 @@ def test_positive_sync_proxy_with_certificate(request, target_sat, module_org, m # Create and fetch new cerfiticate target_sat.custom_cert_generate(proxy_host) cacert = target_sat.execute(f'cat {cacert_path}').stdout - assert 'BEGIN CERTIFICATE' and 'END CERTIFICATE' in cacert + assert 'END CERTIFICATE' in cacert # Create http-proxy and repository http_proxy = target_sat.api.HTTPProxy( diff --git a/tests/foreman/api/test_lifecycleenvironment.py b/tests/foreman/api/test_lifecycleenvironment.py index 79cece7c76f..dfc3ed1ba27 100644 --- a/tests/foreman/api/test_lifecycleenvironment.py +++ b/tests/foreman/api/test_lifecycleenvironment.py @@ -164,8 +164,8 @@ def test_negative_update_name(module_lce, new_name): :parametrized: yes """ + module_lce.name = new_name with pytest.raises(HTTPError): - module_lce.name = new_name module_lce.update(['name']) lce = module_lce.read() assert lce.name != new_name diff --git a/tests/foreman/api/test_media.py b/tests/foreman/api/test_media.py index 97c6da607f4..8fb0ec5af66 100644 --- a/tests/foreman/api/test_media.py +++ b/tests/foreman/api/test_media.py @@ -40,7 +40,7 @@ def class_media(self, module_org, class_target_sat): @pytest.mark.tier1 @pytest.mark.upgrade @pytest.mark.parametrize( - 'name, new_name', + ('name', 'new_name'), **parametrized(list(zip(valid_data_list().values(), valid_data_list().values()))) ) def test_positive_crud_with_name(self, module_org, name, new_name, module_target_sat): diff --git a/tests/foreman/api/test_partitiontable.py b/tests/foreman/api/test_partitiontable.py index b1f64b8f5ac..6d78ba1135b 100644 --- a/tests/foreman/api/test_partitiontable.py +++ b/tests/foreman/api/test_partitiontable.py @@ -60,7 +60,7 @@ def test_positive_create_with_one_character_name(self, target_sat, name): @pytest.mark.tier1 @pytest.mark.parametrize( - 'name, new_name', + ('name', 'new_name'), **parametrized( list( zip( @@ -95,7 +95,7 @@ def test_positive_crud_with_name(self, target_sat, name, new_name): @pytest.mark.tier1 @pytest.mark.parametrize( - 'layout, new_layout', **parametrized(list(zip(valid_data_list(), valid_data_list()))) + ('layout', 'new_layout'), **parametrized(list(zip(valid_data_list(), valid_data_list()))) ) def test_positive_create_update_with_layout(self, target_sat, layout, new_layout): """Create new and update partition tables using different inputs as a diff --git a/tests/foreman/api/test_permission.py b/tests/foreman/api/test_permission.py index 094a7875b66..9ea92305b39 100644 --- a/tests/foreman/api/test_permission.py +++ b/tests/foreman/api/test_permission.py @@ -368,13 +368,14 @@ def test_positive_check_update(self, entity_cls, class_org, class_location, targ new_entity = self.set_taxonomies(entity_cls(), class_org, class_location) new_entity = new_entity.create() name = new_entity.get_fields()['name'].gen_value() + if entity_cls is entities.ActivationKey: + update_entity = entity_cls( + self.cfg, id=new_entity.id, name=name, organization=class_org + ) + else: + update_entity = entity_cls(self.cfg, id=new_entity.id, name=name) with pytest.raises(HTTPError): - if entity_cls is entities.ActivationKey: - entity_cls(self.cfg, id=new_entity.id, name=name, organization=class_org).update( - ['name'] - ) - else: - entity_cls(self.cfg, id=new_entity.id, name=name).update(['name']) + update_entity.update(['name']) self.give_user_permission(_permission_name(entity_cls, 'update')) # update() calls read() under the hood, which triggers # permission error diff --git a/tests/foreman/api/test_repositories.py b/tests/foreman/api/test_repositories.py index c250d25bbbe..49b0ff61fea 100644 --- a/tests/foreman/api/test_repositories.py +++ b/tests/foreman/api/test_repositories.py @@ -64,10 +64,10 @@ def test_negative_disable_repository_with_cv(module_entitlement_manifest_org, ta with pytest.raises(HTTPError) as error: reposet.disable(data=data) # assert error.value.response.status_code == 500 - assert ( - 'Repository cannot be deleted since it has already been ' - 'included in a published Content View' in error.value.response.text - ) + assert ( + 'Repository cannot be deleted since it has already been ' + 'included in a published Content View' in error.value.response.text + ) @pytest.mark.tier1 diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index 0df5602bd5c..b93bcf4c736 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -1189,7 +1189,8 @@ def test_positive_recreate_pulp_repositories(self, module_entitlement_manifest_o f' --key /etc/foreman/client_key.pem' ) command_output = target_sat.execute('foreman-rake katello:correct_repositories COMMIT=true') - assert 'Recreating' in command_output.stdout and 'TaskError' not in command_output.stdout + assert 'Recreating' in command_output.stdout + assert 'TaskError' not in command_output.stdout @pytest.mark.tier2 def test_positive_mirroring_policy(self, target_sat): diff --git a/tests/foreman/api/test_rhcloud_inventory.py b/tests/foreman/api/test_rhcloud_inventory.py index de1fe67d8cb..67bd321c073 100644 --- a/tests/foreman/api/test_rhcloud_inventory.py +++ b/tests/foreman/api/test_rhcloud_inventory.py @@ -108,7 +108,8 @@ def test_rhcloud_inventory_api_e2e( infrastructure_type = [ host['system_profile']['infrastructure_type'] for host in json_data['hosts'] ] - assert 'physical' and 'virtual' in infrastructure_type + assert 'physical' in infrastructure_type + assert 'virtual' in infrastructure_type # Verify installed packages are present in report. all_host_profiles = [host['system_profile'] for host in json_data['hosts']] for host_profiles in all_host_profiles: diff --git a/tests/foreman/api/test_rhsm.py b/tests/foreman/api/test_rhsm.py index 2ebd5b517e0..2194ecdb147 100644 --- a/tests/foreman/api/test_rhsm.py +++ b/tests/foreman/api/test_rhsm.py @@ -49,4 +49,4 @@ def test_positive_path(): response = client.get(path, auth=get_credentials(), verify=False) assert response.status_code == http.client.OK assert 'application/json' in response.headers['content-type'] - assert type(response.json()) is list + assert isinstance(response.json(), list) diff --git a/tests/foreman/api/test_role.py b/tests/foreman/api/test_role.py index fabd0d3008c..9ed32e118ec 100644 --- a/tests/foreman/api/test_role.py +++ b/tests/foreman/api/test_role.py @@ -37,7 +37,7 @@ class TestRole: @pytest.mark.tier1 @pytest.mark.upgrade @pytest.mark.parametrize( - 'name, new_name', + ('name', 'new_name'), **parametrized(list(zip(generate_strings_list(), generate_strings_list()))), ) def test_positive_crud(self, name, new_name, target_sat): @@ -1264,6 +1264,7 @@ def test_positive_taxonomies_control_to_superadmin_without_org_admin( target_sat.api.User(id=user.id).delete() with pytest.raises(HTTPError): user_role.read() + with pytest.raises(HTTPError): user.read() try: target_sat.api.Domain().search( @@ -1340,8 +1341,6 @@ def test_negative_modify_roles_by_org_admin(self, role_taxonomies, target_sat): test_role.organization = [role_taxonomies['org']] test_role.location = [role_taxonomies['loc']] with pytest.raises(HTTPError): - test_role.organization = [role_taxonomies['org']] - test_role.location = [role_taxonomies['loc']] test_role.update(['organization', 'location']) @pytest.mark.tier2 diff --git a/tests/foreman/api/test_subnet.py b/tests/foreman/api/test_subnet.py index 75d7bff0b9e..9ac9fca0979 100644 --- a/tests/foreman/api/test_subnet.py +++ b/tests/foreman/api/test_subnet.py @@ -342,8 +342,8 @@ def test_negative_update_parameter(new_name, target_sat): sub_param = target_sat.api.Parameter( name=gen_string('utf8'), subnet=subnet.id, value=gen_string('utf8') ).create() + sub_param.name = new_name with pytest.raises(HTTPError): - sub_param.name = new_name sub_param.update(['name']) diff --git a/tests/foreman/api/test_user.py b/tests/foreman/api/test_user.py index cd2c704e249..29442aa3080 100644 --- a/tests/foreman/api/test_user.py +++ b/tests/foreman/api/test_user.py @@ -207,8 +207,8 @@ def test_negative_update_username(self, create_user, login): :CaseImportance: Critical """ + create_user.login = login with pytest.raises(HTTPError): - create_user.login = login create_user.update(['login']) @pytest.mark.tier1 @@ -283,8 +283,8 @@ def test_negative_update_email(self, create_user, mail): :CaseImportance: Critical """ + create_user.mail = mail with pytest.raises(HTTPError): - create_user.mail = mail create_user.update(['mail']) @pytest.mark.tier1 diff --git a/tests/foreman/cli/test_activationkey.py b/tests/foreman/cli/test_activationkey.py index 126b4144d3c..3a860981e0c 100644 --- a/tests/foreman/cli/test_activationkey.py +++ b/tests/foreman/cli/test_activationkey.py @@ -269,15 +269,15 @@ def test_negative_create_with_usage_limit_with_not_integers(module_org, limit): # invalid_values.append(0.5) with pytest.raises(CLIFactoryError) as raise_ctx: make_activation_key({'organization-id': module_org.id, 'max-hosts': limit}) - if type(limit) is int: + if isinstance(limit, int): if limit < 1: assert 'Max hosts cannot be less than one' in str(raise_ctx) - if type(limit) is str: + if isinstance(limit, str): assert 'Numeric value is required.' in str(raise_ctx) @pytest.mark.tier3 -@pytest.mark.parametrize('invalid_values', ('-1', '-500', 0)) +@pytest.mark.parametrize('invalid_values', ['-1', '-500', 0]) def test_negative_create_with_usage_limit_with_invalid_integers(module_org, invalid_values): """Create Activation key with invalid integers Usage Limit diff --git a/tests/foreman/cli/test_computeresource_osp.py b/tests/foreman/cli/test_computeresource_osp.py index c9018995dc2..acb74283973 100644 --- a/tests/foreman/cli/test_computeresource_osp.py +++ b/tests/foreman/cli/test_computeresource_osp.py @@ -46,7 +46,7 @@ def cr_cleanup(self, cr_id, id_type, target_sat): @pytest.fixture def osp_version(request): versions = {'osp16': settings.osp.api_url.osp16, 'osp17': settings.osp.api_url.osp17} - yield versions[getattr(request, 'param', 'osp16')] + return versions[getattr(request, 'param', 'osp16')] @pytest.mark.upgrade @pytest.mark.tier3 diff --git a/tests/foreman/cli/test_contentview.py b/tests/foreman/cli/test_contentview.py index 4effe29ecf4..2f22d4aefb1 100644 --- a/tests/foreman/cli/test_contentview.py +++ b/tests/foreman/cli/test_contentview.py @@ -2473,7 +2473,7 @@ def test_positive_sub_host_with_restricted_user_perm_at_default_loc( # role info (note: view_roles is not in the required permissions) with pytest.raises(CLIReturnCodeError) as context: Role.with_user(user_name, user_password).info({'id': role['id']}) - assert '403 Forbidden' in str(context) + assert '403 Forbidden' in str(context) # Create a lifecycle environment env = cli_factory.make_lifecycle_environment({'organization-id': org['id']}) # Create a product diff --git a/tests/foreman/cli/test_discoveryrule.py b/tests/foreman/cli/test_discoveryrule.py index efdc081d79f..8657e010b87 100644 --- a/tests/foreman/cli/test_discoveryrule.py +++ b/tests/foreman/cli/test_discoveryrule.py @@ -61,7 +61,7 @@ def gen_int32(min_value=1): class TestDiscoveryRule: """Implements Foreman discovery Rules tests in CLI.""" - @pytest.fixture(scope='function') + @pytest.fixture def discoveryrule_factory(self, class_org, class_location, class_hostgroup): def _create_discoveryrule(org, loc, hostgroup, options=None): """Makes a new discovery rule and asserts its success""" diff --git a/tests/foreman/cli/test_errata.py b/tests/foreman/cli/test_errata.py index 988a578ab12..d5adf0ccc66 100644 --- a/tests/foreman/cli/test_errata.py +++ b/tests/foreman/cli/test_errata.py @@ -186,7 +186,7 @@ def hosts(request): """Deploy hosts via broker.""" num_hosts = getattr(request, 'param', 2) with Broker(nick='rhel7', host_class=ContentHost, _count=num_hosts) as hosts: - if type(hosts) is not list or len(hosts) != num_hosts: + if not isinstance(hosts, list) or len(hosts) != num_hosts: pytest.fail('Failed to provision the expected number of hosts.') yield hosts @@ -374,9 +374,9 @@ def cv_filter_cleanup(filter_id, cv, org, lce): @pytest.mark.tier3 -@pytest.mark.parametrize('filter_by_hc', ('id', 'name'), ids=('hc_id', 'hc_name')) +@pytest.mark.parametrize('filter_by_hc', ['id', 'name'], ids=('hc_id', 'hc_name')) @pytest.mark.parametrize( - 'filter_by_org', ('id', 'name', 'title'), ids=('org_id', 'org_name', 'org_title') + 'filter_by_org', ['id', 'name', 'title'], ids=('org_id', 'org_name', 'org_title') ) @pytest.mark.no_containers def test_positive_install_by_host_collection_and_org( @@ -1002,10 +1002,10 @@ def cleanup(): @pytest.mark.tier3 -@pytest.mark.parametrize('sort_by_date', ('issued', 'updated'), ids=('issued_date', 'updated_date')) +@pytest.mark.parametrize('sort_by_date', ['issued', 'updated'], ids=('issued_date', 'updated_date')) @pytest.mark.parametrize( 'filter_by_org', - ('id', 'name', 'label', None), + ['id', 'name', 'label', None], ids=('org_id', 'org_name', 'org_label', 'no_org_filter'), ) def test_positive_list_filter_by_org_sort_by_date( @@ -1052,9 +1052,9 @@ def test_positive_list_filter_by_product_id(products_with_repos): @pytest.mark.tier3 -@pytest.mark.parametrize('filter_by_product', ('id', 'name'), ids=('product_id', 'product_name')) +@pytest.mark.parametrize('filter_by_product', ['id', 'name'], ids=('product_id', 'product_name')) @pytest.mark.parametrize( - 'filter_by_org', ('id', 'name', 'label'), ids=('org_id', 'org_name', 'org_label') + 'filter_by_org', ['id', 'name', 'label'], ids=('org_id', 'org_name', 'org_label') ) def test_positive_list_filter_by_product_and_org( products_with_repos, filter_by_product, filter_by_org @@ -1118,7 +1118,7 @@ def test_negative_list_filter_by_product_name(products_with_repos): @pytest.mark.tier3 @pytest.mark.parametrize( - 'filter_by_org', ('id', 'name', 'label'), ids=('org_id', 'org_name', 'org_label') + 'filter_by_org', ['id', 'name', 'label'], ids=('org_id', 'org_name', 'org_label') ) def test_positive_list_filter_by_org(products_with_repos, filter_by_org): """Filter errata by org id, name, or label. diff --git a/tests/foreman/cli/test_filter.py b/tests/foreman/cli/test_filter.py index da5eccb86c2..6881d8b8b01 100644 --- a/tests/foreman/cli/test_filter.py +++ b/tests/foreman/cli/test_filter.py @@ -34,7 +34,7 @@ def module_perms(): return perms -@pytest.fixture(scope='function') +@pytest.fixture def function_role(): """Create a role that a filter would be assigned""" return make_role() diff --git a/tests/foreman/cli/test_host.py b/tests/foreman/cli/test_host.py index 538c8ebe27a..20310ab564d 100644 --- a/tests/foreman/cli/test_host.py +++ b/tests/foreman/cli/test_host.py @@ -67,7 +67,7 @@ def module_default_proxy(module_target_sat): return module_target_sat.cli.Proxy.list({'search': f'url = {module_target_sat.url}:9090'})[0] -@pytest.fixture(scope="function") +@pytest.fixture def function_host(target_sat): host_template = target_sat.api.Host() host_template.create_missing() @@ -90,7 +90,7 @@ def function_host(target_sat): Host.delete({'id': host['id']}) -@pytest.fixture(scope="function") +@pytest.fixture def function_user(target_sat, function_host): """ Returns dict with user object and with password to this user @@ -116,7 +116,7 @@ def function_user(target_sat, function_host): user.delete() -@pytest.fixture(scope='function') +@pytest.fixture def tracer_host(katello_host_tools_tracer_host): # create a custom, rhel version-specific mock-service repo rhelver = katello_host_tools_tracer_host.os_version.major @@ -132,7 +132,7 @@ def tracer_host(katello_host_tools_tracer_host): ) katello_host_tools_tracer_host.execute(f'systemctl start {settings.repos["MOCK_SERVICE_RPM"]}') - yield katello_host_tools_tracer_host + return katello_host_tools_tracer_host def update_smart_proxy(sat, location, smart_proxy): @@ -1494,18 +1494,13 @@ def test_positive_provision_baremetal_with_uefi_secureboot(): """ -@pytest.fixture(scope="function") +@pytest.fixture def setup_custom_repo(target_sat, module_org, katello_host_tools_host, request): """Create custom repository content""" - def restore_sca_setting(): - """Restore the original SCA setting for module_org""" - module_org.sca_enable() if sca_enabled else module_org.sca_disable() - - if module_org.sca_eligible().get('simple_content_access_eligible', False): + if sca_eligible := module_org.sca_eligible().get('simple_content_access_eligible', False): sca_enabled = module_org.simple_content_access module_org.sca_disable() - request.addfinalizer(restore_sca_setting) # get package details details = {} @@ -1551,10 +1546,14 @@ def restore_sca_setting(): ) # refresh repository metadata katello_host_tools_host.subscription_manager_list_repos() - return details + if sca_eligible: + yield + module_org.sca_enable() if sca_enabled else module_org.sca_disable() + else: + return details -@pytest.fixture(scope="function") +@pytest.fixture def yum_security_plugin(katello_host_tools_host): """Enable yum-security-plugin if the distro version requires it. Rhel6 yum version does not support updating of a specific advisory out of the box. @@ -1825,10 +1824,10 @@ def test_positive_install_package_via_rex( # -------------------------- HOST SUBSCRIPTION SUBCOMMAND FIXTURES -------------------------- @pytest.mark.skip_if_not_set('clients') -@pytest.fixture(scope="function") +@pytest.fixture def host_subscription_client(rhel7_contenthost, target_sat): rhel7_contenthost.install_katello_ca(target_sat) - yield rhel7_contenthost + return rhel7_contenthost @pytest.fixture @@ -2503,14 +2502,14 @@ def test_positive_host_with_puppet( session_puppet_enabled_sat.cli.Host.delete({'id': host['id']}) -@pytest.fixture(scope="function") +@pytest.fixture def function_proxy(session_puppet_enabled_sat, puppet_proxy_port_range): proxy = session_puppet_enabled_sat.cli_factory.make_proxy() yield proxy session_puppet_enabled_sat.cli.Proxy.delete({'id': proxy['id']}) -@pytest.fixture(scope="function") +@pytest.fixture def function_host_content_source( session_puppet_enabled_sat, session_puppet_enabled_proxy, diff --git a/tests/foreman/cli/test_ldapauthsource.py b/tests/foreman/cli/test_ldapauthsource.py index 3feed9bad43..be218a95796 100644 --- a/tests/foreman/cli/test_ldapauthsource.py +++ b/tests/foreman/cli/test_ldapauthsource.py @@ -34,7 +34,7 @@ from robottelo.utils.datafactory import generate_strings_list, parametrized -@pytest.fixture() +@pytest.fixture def ldap_tear_down(): """Teardown the all ldap settings user, usergroup and ldap delete""" yield diff --git a/tests/foreman/cli/test_leapp_client.py b/tests/foreman/cli/test_leapp_client.py index 37d1784926c..a6d547f5314 100644 --- a/tests/foreman/cli/test_leapp_client.py +++ b/tests/foreman/cli/test_leapp_client.py @@ -78,7 +78,7 @@ def module_stash(request): # Please refer the documentation for more details on stash # https://docs.pytest.org/en/latest/reference/reference.html#stash request.node.stash[synced_repos] = {} - yield request.node.stash + return request.node.stash @pytest.fixture(scope='module') diff --git a/tests/foreman/cli/test_model.py b/tests/foreman/cli/test_model.py index fe8050d13e9..ddfd4dfe19c 100644 --- a/tests/foreman/cli/test_model.py +++ b/tests/foreman/cli/test_model.py @@ -33,7 +33,7 @@ class TestModel: """Test class for Model CLI""" - @pytest.fixture() + @pytest.fixture def class_model(self): """Shared model for tests""" return make_model() @@ -41,7 +41,7 @@ def class_model(self): @pytest.mark.tier1 @pytest.mark.upgrade @pytest.mark.parametrize( - 'name, new_name', + ('name', 'new_name'), **parametrized(list(zip(valid_data_list().values(), valid_data_list().values()))) ) def test_positive_crud_with_name(self, name, new_name): diff --git a/tests/foreman/cli/test_partitiontable.py b/tests/foreman/cli/test_partitiontable.py index 6540a5d755b..fa8af557d36 100644 --- a/tests/foreman/cli/test_partitiontable.py +++ b/tests/foreman/cli/test_partitiontable.py @@ -51,7 +51,7 @@ def test_positive_create_with_one_character_name(self, name): @pytest.mark.tier1 @pytest.mark.upgrade @pytest.mark.parametrize( - 'name, new_name', + ('name', 'new_name'), **parametrized( list( zip( diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index 09e1eda28af..3493245a4df 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -48,7 +48,7 @@ from robottelo.utils import ohsnap -@pytest.fixture() +@pytest.fixture def fixture_sca_vmsetup(request, module_sca_manifest_org, target_sat): """Create VM and register content host to Simple Content Access organization""" if '_count' in request.param.keys(): @@ -66,10 +66,10 @@ def fixture_sca_vmsetup(request, module_sca_manifest_org, target_sat): yield client -@pytest.fixture() +@pytest.fixture def infra_host(request, target_sat, module_capsule_configured): infra_hosts = {'target_sat': target_sat, 'module_capsule_configured': module_capsule_configured} - yield infra_hosts[request.param] + return infra_hosts[request.param] def assert_job_invocation_result(invocation_command_id, client_hostname, expected_result='success'): @@ -788,7 +788,7 @@ def class_rexmanager_user(self, module_org): rexmanager = gen_string('alpha') make_user({'login': rexmanager, 'password': password, 'organization-ids': module_org.id}) User.add_role({'login': rexmanager, 'role': 'Remote Execution Manager'}) - yield (rexmanager, password) + return (rexmanager, password) @pytest.fixture(scope='class') def class_rexinfra_user(self, module_org): @@ -814,7 +814,7 @@ def class_rexinfra_user(self, module_org): make_filter({'role-id': role['id'], 'permissions': permissions}) User.add_role({'login': rexinfra, 'role': role['name']}) User.add_role({'login': rexinfra, 'role': 'Remote Execution Manager'}) - yield (rexinfra, password) + return (rexinfra, password) @pytest.mark.tier3 @pytest.mark.upgrade diff --git a/tests/foreman/cli/test_reporttemplates.py b/tests/foreman/cli/test_reporttemplates.py index 8f2729046b3..f5027c028f1 100644 --- a/tests/foreman/cli/test_reporttemplates.py +++ b/tests/foreman/cli/test_reporttemplates.py @@ -988,7 +988,7 @@ def test_negative_generate_hostpkgcompare_nonexistent_host(): 'inputs': 'Host 1 = nonexistent1, ' 'Host 2 = nonexistent2', } ) - assert "At least one of the hosts couldn't be found" in cm.exception.stderr + assert "At least one of the hosts couldn't be found" in cm.exception.stderr @pytest.mark.rhel_ver_list([7, 8, 9]) diff --git a/tests/foreman/cli/test_role.py b/tests/foreman/cli/test_role.py index feeb1c928be..21900c30523 100644 --- a/tests/foreman/cli/test_role.py +++ b/tests/foreman/cli/test_role.py @@ -44,7 +44,7 @@ class TestRole: @pytest.mark.tier1 @pytest.mark.parametrize( - 'name, new_name', + ('name', 'new_name'), **parametrized( list(zip(generate_strings_list(length=10), generate_strings_list(length=10))) ), @@ -147,14 +147,13 @@ def test_negative_list_filters_without_parameters(self): :BZ: 1296782 """ - with pytest.raises(CLIReturnCodeError) as err: - try: - Role.filters() - except CLIDataBaseError as err: - pytest.fail(err) + with pytest.raises(CLIReturnCodeError, CLIDataBaseError) as err: + Role.filters() + if isinstance(err.type, CLIDataBaseError): + pytest.fail(err) assert re.search('At least one of options .* is required', err.value.msg) - @pytest.fixture() + @pytest.fixture def make_role_with_permissions(self): """Create new role with a filter""" role = make_role() diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index 19e3b8a178a..2154535d618 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -67,7 +67,7 @@ def config_export_import_settings(): Settings.set({'name': 'subscription_connection_enabled', 'value': subs_conn_enabled_value}) -@pytest.fixture(scope='function') +@pytest.fixture def export_import_cleanup_function(target_sat, function_org): """Deletes export/import dirs of function org""" yield @@ -76,7 +76,7 @@ def export_import_cleanup_function(target_sat, function_org): ) -@pytest.fixture(scope='function') # perform the cleanup after each testcase of a module +@pytest.fixture # perform the cleanup after each testcase of a module def export_import_cleanup_module(target_sat, module_org): """Deletes export/import dirs of module_org""" yield @@ -85,19 +85,19 @@ def export_import_cleanup_module(target_sat, module_org): ) -@pytest.fixture(scope='function') +@pytest.fixture def function_import_org(target_sat): """Creates an Organization for content import.""" org = target_sat.api.Organization().create() - yield org + return org -@pytest.fixture(scope='function') +@pytest.fixture def function_import_org_with_manifest(target_sat, function_import_org): """Creates and sets an Organization with a brand-new manifest for content import.""" with Manifester(manifest_category=settings.manifest.golden_ticket) as manifest: target_sat.upload_manifest(function_import_org.id, manifest) - yield function_import_org + return function_import_org @pytest.fixture(scope='module') @@ -111,10 +111,10 @@ def module_synced_custom_repo(module_target_sat, module_org, module_product): } ) module_target_sat.cli.Repository.synchronize({'id': repo['id']}) - yield repo + return repo -@pytest.fixture(scope='function') +@pytest.fixture def function_synced_custom_repo(target_sat, function_org, function_product): repo = target_sat.cli_factory.make_repository( { @@ -125,10 +125,10 @@ def function_synced_custom_repo(target_sat, function_org, function_product): } ) target_sat.cli.Repository.synchronize({'id': repo['id']}) - yield repo + return repo -@pytest.fixture(scope='function') +@pytest.fixture def function_synced_rhel_repo(request, target_sat, function_sca_manifest_org): """Enable and synchronize rhel content with immediate policy""" repo_dict = ( @@ -165,7 +165,7 @@ def function_synced_rhel_repo(request, target_sat, function_sca_manifest_org): return repo -@pytest.fixture(scope='function') +@pytest.fixture def function_synced_file_repo(target_sat, function_org, function_product): repo = target_sat.cli_factory.make_repository( { @@ -176,10 +176,10 @@ def function_synced_file_repo(target_sat, function_org, function_product): } ) target_sat.cli.Repository.synchronize({'id': repo['id']}) - yield repo + return repo -@pytest.fixture(scope='function') +@pytest.fixture def function_synced_docker_repo(target_sat, function_org): product = target_sat.cli_factory.make_product({'organization-id': function_org.id}) repo = target_sat.cli_factory.make_repository( @@ -193,10 +193,10 @@ def function_synced_docker_repo(target_sat, function_org): } ) target_sat.cli.Repository.synchronize({'id': repo['id']}) - yield repo + return repo -@pytest.fixture(scope='function') +@pytest.fixture def function_synced_AC_repo(target_sat, function_org, function_product): repo = target_sat.cli_factory.make_repository( { @@ -210,7 +210,7 @@ def function_synced_AC_repo(target_sat, function_org, function_product): } ) target_sat.cli.Repository.synchronize({'id': repo['id']}) - yield repo + return repo @pytest.mark.run_in_one_thread diff --git a/tests/foreman/cli/test_subnet.py b/tests/foreman/cli/test_subnet.py index 6dc36a640f7..5eefb2c24ad 100644 --- a/tests/foreman/cli/test_subnet.py +++ b/tests/foreman/cli/test_subnet.py @@ -210,10 +210,10 @@ def test_negative_update_attributes(options): options['id'] = subnet['id'] with pytest.raises(CLIReturnCodeError, match='Could not update the subnet:'): Subnet.update(options) - # check - subnet is not updated - result = Subnet.info({'id': subnet['id']}) - for key in options.keys(): - assert subnet[key] == result[key] + # check - subnet is not updated + result = Subnet.info({'id': subnet['id']}) + for key in options.keys(): + assert subnet[key] == result[key] @pytest.mark.tier2 diff --git a/tests/foreman/cli/test_user.py b/tests/foreman/cli/test_user.py index c44d1807070..8946837d2aa 100644 --- a/tests/foreman/cli/test_user.py +++ b/tests/foreman/cli/test_user.py @@ -72,7 +72,7 @@ def roles_helper(): yield make_role({'name': role_name}) stubbed_roles = {role['id']: role for role in roles_helper()} - yield stubbed_roles + return stubbed_roles @pytest.mark.parametrize('email', **parametrized(valid_emails_list())) @pytest.mark.tier2 diff --git a/tests/foreman/cli/test_usergroup.py b/tests/foreman/cli/test_usergroup.py index 57e3c8b9b17..61feba18d2f 100644 --- a/tests/foreman/cli/test_usergroup.py +++ b/tests/foreman/cli/test_usergroup.py @@ -34,11 +34,11 @@ from robottelo.utils.datafactory import valid_usernames_list -@pytest.fixture(scope='function') +@pytest.fixture def function_user_group(): """Create new usergroup per each test""" user_group = make_usergroup() - yield user_group + return user_group @pytest.mark.tier1 @@ -241,10 +241,10 @@ def test_negative_automate_bz1437578(ldap_auth_source, function_user_group): 'name': 'Domain Users', } ) - assert ( - 'Could not create external user group: ' - 'Name is not found in the authentication source' - 'Name Domain Users is a special group in AD.' - ' Unfortunately, we cannot obtain membership information' - ' from a LDAP search and therefore sync it.' == result - ) + assert ( + 'Could not create external user group: ' + 'Name is not found in the authentication source' + 'Name Domain Users is a special group in AD.' + ' Unfortunately, we cannot obtain membership information' + ' from a LDAP search and therefore sync it.' == result + ) diff --git a/tests/foreman/cli/test_webhook.py b/tests/foreman/cli/test_webhook.py index 3ca83dbe70f..29c08189449 100644 --- a/tests/foreman/cli/test_webhook.py +++ b/tests/foreman/cli/test_webhook.py @@ -28,7 +28,7 @@ from robottelo.constants import WEBHOOK_EVENTS, WEBHOOK_METHODS -@pytest.fixture(scope='function') +@pytest.fixture def webhook_factory(request, class_org, class_location): def _create_webhook(org, loc, options=None): """Function for creating a new Webhook diff --git a/tests/foreman/destructive/test_capsule_loadbalancer.py b/tests/foreman/destructive/test_capsule_loadbalancer.py index 04ba4614438..77adccd1fcf 100644 --- a/tests/foreman/destructive/test_capsule_loadbalancer.py +++ b/tests/foreman/destructive/test_capsule_loadbalancer.py @@ -88,7 +88,7 @@ def setup_capsules( {'id': capsule_id, 'organization-id': module_org.id} ) - yield { + return { 'capsule_1': module_lb_capsule[0], 'capsule_2': module_lb_capsule[1], } diff --git a/tests/foreman/destructive/test_infoblox.py b/tests/foreman/destructive/test_infoblox.py index 113a4e4f067..8aa92f22a83 100644 --- a/tests/foreman/destructive/test_infoblox.py +++ b/tests/foreman/destructive/test_infoblox.py @@ -91,7 +91,7 @@ @pytest.mark.tier4 @pytest.mark.parametrize( - 'command_args,command_opts,rpm_command', + ('command_args', 'command_opts', 'rpm_command'), params, ids=['isc_dhcp', 'infoblox_dhcp', 'infoblox_dns'], ) diff --git a/tests/foreman/destructive/test_ldap_authentication.py b/tests/foreman/destructive/test_ldap_authentication.py index 39f5a6b4f65..65ef31aa2cf 100644 --- a/tests/foreman/destructive/test_ldap_authentication.py +++ b/tests/foreman/destructive/test_ldap_authentication.py @@ -80,7 +80,7 @@ def set_certificate_in_satellite(server_type, sat, hostname=None): raise AssertionError(f'Failed to restart the httpd after applying {server_type} cert') -@pytest.fixture() +@pytest.fixture def ldap_tear_down(module_target_sat): """Teardown the all ldap settings user, usergroup and ldap delete""" yield @@ -92,14 +92,14 @@ def ldap_tear_down(module_target_sat): ldap_auth.delete() -@pytest.fixture() +@pytest.fixture def external_user_count(module_target_sat): """return the external auth source user count""" users = module_target_sat.api.User().search() - yield len([user for user in users if user.auth_source_name == 'External']) + return len([user for user in users if user.auth_source_name == 'External']) -@pytest.fixture() +@pytest.fixture def groups_teardown(module_target_sat): """teardown for groups created for external/remote groups""" yield @@ -112,7 +112,7 @@ def groups_teardown(module_target_sat): user_groups[0].delete() -@pytest.fixture() +@pytest.fixture def rhsso_groups_teardown(module_target_sat, default_sso_host): """Teardown the rhsso groups""" yield @@ -384,6 +384,7 @@ def test_external_new_user_login_and_check_count_rhsso( with module_target_sat.ui_session(login=False) as rhsso_session: with pytest.raises(NavigationTriesExceeded) as error: rhsso_session.rhsso_login.login(login_details) + with pytest.raises(NavigationTriesExceeded) as error: rhsso_session.task.read_all() assert error.typename == 'NavigationTriesExceeded' @@ -430,6 +431,7 @@ def test_login_failure_rhsso_user_if_internal_user_exist( with module_target_sat.ui_session(login=False) as rhsso_session: with pytest.raises(NavigationTriesExceeded) as error: rhsso_session.rhsso_login.login(login_details) + with pytest.raises(NavigationTriesExceeded) as error: rhsso_session.task.read_all() assert error.typename == 'NavigationTriesExceeded' @@ -875,7 +877,7 @@ def test_positive_negotiate_logout( @pytest.mark.parametrize( - 'parametrized_enrolled_sat,user_not_exists', + ('parametrized_enrolled_sat', 'user_not_exists'), [('IDM', settings.ipa.user), ('AD', f'{settings.ldap.username}@{settings.ldap.realm.lower()}')], indirect=True, ids=['IDM', 'AD'], @@ -931,7 +933,7 @@ def test_positive_autonegotiate( @pytest.mark.parametrize( - 'parametrized_enrolled_sat,user_not_exists', + ('parametrized_enrolled_sat', 'user_not_exists'), [('IDM', settings.ipa.user), ('AD', f'{settings.ldap.username}@{settings.ldap.realm.lower()}')], indirect=True, ids=['IDM', 'AD'], @@ -1013,7 +1015,7 @@ def test_positive_negotiate_manual_with_autonegotiation_disabled( ids=['sessions_enabled', 'sessions_disabled'], ) @pytest.mark.parametrize( - 'parametrized_enrolled_sat,user_not_exists', + ('parametrized_enrolled_sat', 'user_not_exists'), [('IDM', settings.ipa.user), ('AD', f'{settings.ldap.username}@{settings.ldap.realm.lower()}')], indirect=True, ids=['IDM', 'AD'], diff --git a/tests/foreman/destructive/test_ldapauthsource.py b/tests/foreman/destructive/test_ldapauthsource.py index fdbc2da11ba..58e3ceb3e84 100644 --- a/tests/foreman/destructive/test_ldapauthsource.py +++ b/tests/foreman/destructive/test_ldapauthsource.py @@ -34,20 +34,16 @@ def configure_hammer_session(sat, enable=True): sat.execute(f"echo ' :use_sessions: {'true' if enable else 'false'}' >> {HAMMER_CONFIG}") -@pytest.fixture() +@pytest.fixture def rh_sso_hammer_auth_setup(module_target_sat, default_sso_host, request): """rh_sso hammer setup before running the auth login tests""" configure_hammer_session(module_target_sat) client_config = {'publicClient': 'true'} default_sso_host.update_client_configuration(client_config) - - def rh_sso_hammer_auth_cleanup(): - """restore the hammer config backup file and rhsso client settings""" - module_target_sat.execute(f'mv {HAMMER_CONFIG}.backup {HAMMER_CONFIG}') - client_config = {'publicClient': 'false'} - default_sso_host.update_client_configuration(client_config) - - request.addfinalizer(rh_sso_hammer_auth_cleanup) + yield + module_target_sat.execute(f'mv {HAMMER_CONFIG}.backup {HAMMER_CONFIG}') + client_config = {'publicClient': 'false'} + default_sso_host.update_client_configuration(client_config) def test_rhsso_login_using_hammer( diff --git a/tests/foreman/destructive/test_ping.py b/tests/foreman/destructive/test_ping.py index 67818b6a74e..e683a84a2b6 100644 --- a/tests/foreman/destructive/test_ping.py +++ b/tests/foreman/destructive/test_ping.py @@ -29,7 +29,7 @@ def tomcat_service_teardown(request, module_target_sat): def _finalize(): assert module_target_sat.cli.Service.start(options={'only': 'tomcat.service'}).status == 0 - yield module_target_sat + return module_target_sat def test_negative_cli_ping_fail_status(tomcat_service_teardown): diff --git a/tests/foreman/maintain/test_health.py b/tests/foreman/maintain/test_health.py index 0dd4320d633..3793dad8b01 100644 --- a/tests/foreman/maintain/test_health.py +++ b/tests/foreman/maintain/test_health.py @@ -589,9 +589,8 @@ def test_positive_health_check_foreman_proxy_verify_dhcp_config_syntax(sat_maint result = sat_maintain.cli.Health.check( options={'label': 'foreman-proxy-verify-dhcp-config-syntax'} ) - assert ( - 'No scenario matching label' and 'foreman-proxy-verify-dhcp-config-syntax' in result.stdout - ) + assert 'No scenario matching label' + assert 'foreman-proxy-verify-dhcp-config-syntax' in result.stdout # Enable DHCP installer = sat_maintain.install( InstallerCommand('enable-foreman-proxy-plugin-dhcp-remote-isc', 'foreman-proxy-dhcp true') @@ -617,9 +616,8 @@ def test_positive_health_check_foreman_proxy_verify_dhcp_config_syntax(sat_maint result = sat_maintain.cli.Health.check( options={'label': 'foreman-proxy-verify-dhcp-config-syntax'} ) - assert ( - 'No scenario matching label' and 'foreman-proxy-verify-dhcp-config-syntax' in result.stdout - ) + assert 'No scenario matching label' + assert 'foreman-proxy-verify-dhcp-config-syntax' in result.stdout def test_positive_remove_job_file(sat_maintain): diff --git a/tests/foreman/sys/test_katello_certs_check.py b/tests/foreman/sys/test_katello_certs_check.py index f058328e2e4..3ce28168e46 100644 --- a/tests/foreman/sys/test_katello_certs_check.py +++ b/tests/foreman/sys/test_katello_certs_check.py @@ -188,7 +188,7 @@ def test_katello_certs_check_output_wildcard_inputs(self, cert_setup_teardown): result = target_sat.execute(command) self.validate_output(result, cert_data) - @pytest.mark.parametrize('error, cert_file, key_file, ca_file', invalid_inputs) + @pytest.mark.parametrize(('error', 'cert_file', 'key_file', 'ca_file'), invalid_inputs) @pytest.mark.tier1 def test_katello_certs_check_output_invalid_input( self, @@ -264,7 +264,7 @@ def test_negative_check_expiration_of_certificate(self, cert_setup_teardown): assert message == check break else: - assert False, f'Failed, Unable to find message "{message}" in result' + pytest.fail(f'Failed, Unable to find message "{message}" in result') target_sat.execute("date -s 'last year'") @pytest.mark.stubbed diff --git a/tests/foreman/ui/test_acs.py b/tests/foreman/ui/test_acs.py index e1c40911c6e..09809d16075 100644 --- a/tests/foreman/ui/test_acs.py +++ b/tests/foreman/ui/test_acs.py @@ -459,5 +459,5 @@ def test_acs_positive_end_to_end(self, session, acs_setup): # Delete ACS and check if trying to read it afterwards fails session.acs.delete_acs(acs_name='testAcsToBeDeleted') - with pytest.raises(ValueError): + with pytest.raises(ValueError): # noqa: PT011 - TODO determine better exception session.acs.get_row_drawer_content(acs_name='testAcsToBeDeleted') diff --git a/tests/foreman/ui/test_contenthost.py b/tests/foreman/ui/test_contenthost.py index 1db98df1d70..7fce51f0055 100644 --- a/tests/foreman/ui/test_contenthost.py +++ b/tests/foreman/ui/test_contenthost.py @@ -76,7 +76,7 @@ def vm(module_repos_collection_with_manifest, rhel7_contenthost, target_sat): module_repos_collection_with_manifest.setup_virtual_machine(rhel7_contenthost) rhel7_contenthost.add_rex_key(target_sat) rhel7_contenthost.run(r'subscription-manager repos --enable \*') - yield rhel7_contenthost + return rhel7_contenthost @pytest.fixture @@ -86,7 +86,7 @@ def vm_module_streams(module_repos_collection_with_manifest, rhel8_contenthost, rhel8_contenthost, install_katello_agent=False ) rhel8_contenthost.add_rex_key(satellite=target_sat) - yield rhel8_contenthost + return rhel8_contenthost def set_ignore_facts_for_os(value=False): @@ -1008,7 +1008,8 @@ def test_module_stream_actions_on_content_host(session, default_location, vm_mod ) assert module_stream[0]['Name'] == FAKE_2_CUSTOM_PACKAGE_NAME assert module_stream[0]['Stream'] == stream_version - assert 'Enabled' and 'Installed' in module_stream[0]['Status'] + assert 'Enabled' in module_stream[0]['Status'] + assert 'Installed' in module_stream[0]['Status'] # remove Module Stream result = session.contenthost.execute_module_stream_action( diff --git a/tests/foreman/ui/test_contentview.py b/tests/foreman/ui/test_contentview.py index bf8771092ca..89e02be56fa 100644 --- a/tests/foreman/ui/test_contentview.py +++ b/tests/foreman/ui/test_contentview.py @@ -2063,7 +2063,8 @@ def test_positive_update_inclusive_filter_package_version(session, module_org, t cv.name, VERSION, 'name = "{}" and version = "{}"'.format(package_name, '0.71') ) assert len(packages) == 1 - assert packages[0]['Name'] == package_name and packages[0]['Version'] == '0.71' + assert packages[0]['Name'] == package_name + assert packages[0]['Version'] == '0.71' packages = session.contentview.search_version_package( cv.name, VERSION, 'name = "{}" and version = "{}"'.format(package_name, '5.21') ) @@ -2084,7 +2085,8 @@ def test_positive_update_inclusive_filter_package_version(session, module_org, t cv.name, new_version, 'name = "{}" and version = "{}"'.format(package_name, '5.21') ) assert len(packages) == 1 - assert packages[0]['Name'] == package_name and packages[0]['Version'] == '5.21' + assert packages[0]['Name'] == package_name + assert packages[0]['Version'] == '5.21' @pytest.mark.skip_if_open('BZ:2086957') @@ -2125,7 +2127,8 @@ def test_positive_update_exclusive_filter_package_version(session, module_org, t cv.name, VERSION, 'name = "{}" and version = "{}"'.format(package_name, '5.21') ) assert len(packages) == 1 - assert packages[0]['Name'] == package_name and packages[0]['Version'] == '5.21' + assert packages[0]['Name'] == package_name + assert packages[0]['Version'] == '5.21' packages = session.contentview.search_version_package( cv.name, VERSION, 'name = "{}" and version = "{}"'.format(package_name, '0.71') ) @@ -2146,7 +2149,8 @@ def test_positive_update_exclusive_filter_package_version(session, module_org, t cv.name, new_version, 'name = "{}" and version = "{}"'.format(package_name, '0.71') ) assert len(packages) == 1 - assert packages[0]['Name'] == package_name and packages[0]['Version'] == '0.71' + assert packages[0]['Name'] == package_name + assert packages[0]['Version'] == '0.71' @pytest.mark.skip_if_open('BZ:2086957') @@ -2516,7 +2520,8 @@ def test_positive_update_filter_affected_repos(session, module_org, target_sat): cv.name, VERSION, 'name = "{}" and version = "{}"'.format(repo1_package_name, '4.2.8') ) assert len(packages) == 1 - assert packages[0]['Name'] == repo1_package_name and packages[0]['Version'] == '4.2.8' + assert packages[0]['Name'] == repo1_package_name + assert packages[0]['Version'] == '4.2.8' packages = session.contentview.search_version_package( cv.name, VERSION, 'name = "{}" and version = "{}"'.format(repo1_package_name, '4.2.9') ) @@ -2529,7 +2534,8 @@ def test_positive_update_filter_affected_repos(session, module_org, target_sat): 'name = "{}" and version = "{}"'.format(repo2_package_name, '3.10.232'), ) assert len(packages) == 1 - assert packages[0]['Name'] == repo2_package_name and packages[0]['Version'] == '3.10.232' + assert packages[0]['Name'] == repo2_package_name + assert packages[0]['Version'] == '3.10.232' @pytest.mark.tier3 @@ -3040,10 +3046,8 @@ def test_positive_search_module_streams_in_content_view(session, module_org, tar f'name = "{module_stream}" and stream = "{module_version}"', ) assert len(module_streams) == 1 - assert ( - module_streams[0]['Name'] == module_stream - and module_streams[0]['Stream'] == module_version - ) + assert module_streams[0]['Name'] == module_stream + assert module_streams[0]['Stream'] == module_version @pytest.mark.tier2 diff --git a/tests/foreman/ui/test_discoveryrule.py b/tests/foreman/ui/test_discoveryrule.py index e529578efca..d61d5067f85 100644 --- a/tests/foreman/ui/test_discoveryrule.py +++ b/tests/foreman/ui/test_discoveryrule.py @@ -155,7 +155,7 @@ def test_negative_delete_rule_with_non_admin_user( location=[module_location], ).create() with Session(user=reader_user.login, password=reader_user.password) as session: - with pytest.raises(ValueError): + with pytest.raises(ValueError): # noqa: PT011 - TODO Adarsh determine better exception session.discoveryrule.delete(dr.name) dr_val = session.discoveryrule.read_all() assert dr.name in [rule['Name'] for rule in dr_val] diff --git a/tests/foreman/ui/test_errata.py b/tests/foreman/ui/test_errata.py index fb0c703530b..3bec22806b8 100644 --- a/tests/foreman/ui/test_errata.py +++ b/tests/foreman/ui/test_errata.py @@ -155,12 +155,12 @@ def errata_status_installable(): _set_setting_value(errata_status_installable, original_value) -@pytest.fixture(scope='function') +@pytest.fixture def vm(module_repos_collection_with_setup, rhel7_contenthost, target_sat): """Virtual machine registered in satellite""" module_repos_collection_with_setup.setup_virtual_machine(rhel7_contenthost) rhel7_contenthost.add_rex_key(satellite=target_sat) - yield rhel7_contenthost + return rhel7_contenthost @pytest.mark.e2e diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index 08ce12c02ff..54ba1197ed7 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -64,7 +64,7 @@ def ui_user(ui_user, smart_proxy_location, module_target_sat): id=ui_user.id, default_location=smart_proxy_location, ).update(['default_location']) - yield ui_user + return ui_user @pytest.fixture @@ -133,7 +133,7 @@ def tracer_install_host(rex_contenthost, target_sat): rex_contenthost.create_custom_repos( **{f'rhel{rhelver}_os': settings.repos[f'rhel{rhelver}_os']} ) - yield rex_contenthost + return rex_contenthost @pytest.mark.e2e @@ -750,9 +750,9 @@ def test_positive_check_permissions_affect_create_procedure( ] with Session(test_name, user=user.login, password=user_password) as session: for host_field in host_fields: + values = {host_field['name']: host_field['unexpected_value']} + values.update(host_field.get('other_fields_values', {})) with pytest.raises(NoSuchElementException) as context: - values = {host_field['name']: host_field['unexpected_value']} - values.update(host_field.get('other_fields_values', {})) session.host.helper.read_create_view(values) error_message = str(context.value) assert host_field['unexpected_value'] in error_message diff --git a/tests/foreman/ui/test_jobinvocation.py b/tests/foreman/ui/test_jobinvocation.py index 62ab4ed5ef4..bb773841ef6 100644 --- a/tests/foreman/ui/test_jobinvocation.py +++ b/tests/foreman/ui/test_jobinvocation.py @@ -29,7 +29,7 @@ def module_rhel_client_by_ip(module_org, smart_proxy_location, rhel7_contenthost target_sat.api_factory.update_vm_host_location( rhel7_contenthost, location_id=smart_proxy_location.id ) - yield rhel7_contenthost + return rhel7_contenthost @pytest.mark.tier4 diff --git a/tests/foreman/ui/test_ldap_authentication.py b/tests/foreman/ui/test_ldap_authentication.py index e1983767e77..b72d1bbcd8f 100644 --- a/tests/foreman/ui/test_ldap_authentication.py +++ b/tests/foreman/ui/test_ldap_authentication.py @@ -59,7 +59,7 @@ def set_certificate_in_satellite(server_type, target_sat, hostname=None): raise AssertionError(f'Failed to restart the httpd after applying {server_type} cert') -@pytest.fixture() +@pytest.fixture def ldap_usergroup_name(): """Return some random usergroup name, and attempt to delete such usergroup when test finishes. @@ -71,7 +71,7 @@ def ldap_usergroup_name(): user_groups[0].delete() -@pytest.fixture() +@pytest.fixture def ldap_tear_down(): """Teardown the all ldap settings user, usergroup and ldap delete""" yield @@ -83,14 +83,14 @@ def ldap_tear_down(): ldap_auth.delete() -@pytest.fixture() +@pytest.fixture def external_user_count(): """return the external auth source user count""" users = entities.User().search() - yield len([user for user in users if user.auth_source_name == 'External']) + return len([user for user in users if user.auth_source_name == 'External']) -@pytest.fixture() +@pytest.fixture def groups_teardown(): """teardown for groups created for external/remote groups""" yield @@ -101,7 +101,7 @@ def groups_teardown(): user_groups[0].delete() -@pytest.fixture() +@pytest.fixture def rhsso_groups_teardown(default_sso_host): """Teardown the rhsso groups""" yield @@ -109,7 +109,7 @@ def rhsso_groups_teardown(default_sso_host): default_sso_host.delete_rhsso_group(group_name) -@pytest.fixture() +@pytest.fixture def multigroup_setting_cleanup(default_ipa_host): """Adding and removing the user to/from ipa group""" sat_users = settings.ipa.groups @@ -119,7 +119,7 @@ def multigroup_setting_cleanup(default_ipa_host): default_ipa_host.remove_user_from_usergroup(idm_users[1], sat_users[0]) -@pytest.fixture() +@pytest.fixture def ipa_add_user(default_ipa_host): """Create an IPA user and delete it""" test_user = gen_string('alpha') diff --git a/tests/foreman/ui/test_partitiontable.py b/tests/foreman/ui/test_partitiontable.py index 393a2ca634f..e36d448698d 100644 --- a/tests/foreman/ui/test_partitiontable.py +++ b/tests/foreman/ui/test_partitiontable.py @@ -167,7 +167,7 @@ def test_positive_delete_with_lock_and_unlock(session): ) assert session.partitiontable.search(name)[0]['Name'] == name session.partitiontable.lock(name) - with pytest.raises(ValueError): + with pytest.raises(ValueError): # noqa: PT011 - TODO determine better exception session.partitiontable.delete(name) session.partitiontable.unlock(name) session.partitiontable.delete(name) diff --git a/tests/foreman/ui/test_provisioningtemplate.py b/tests/foreman/ui/test_provisioningtemplate.py index 579ca711f7b..62b9169a729 100644 --- a/tests/foreman/ui/test_provisioningtemplate.py +++ b/tests/foreman/ui/test_provisioningtemplate.py @@ -27,7 +27,7 @@ def template_data(): return DataFile.OS_TEMPLATE_DATA_FILE.read_text() -@pytest.fixture() +@pytest.fixture def clone_setup(target_sat, module_org, module_location): name = gen_string('alpha') content = gen_string('alpha') diff --git a/tests/foreman/ui/test_repository.py b/tests/foreman/ui/test_repository.py index 63b7df93577..8395d096967 100644 --- a/tests/foreman/ui/test_repository.py +++ b/tests/foreman/ui/test_repository.py @@ -227,8 +227,8 @@ def test_positive_create_as_non_admin_user_with_cv_published(module_org, test_na with Session(test_name, user_login, user_password) as session: # ensure that the created user is not a global admin user # check administer->users page + pswd = gen_string('alphanumeric') with pytest.raises(NavigationTriesExceeded): - pswd = gen_string('alphanumeric') session.user.create( { 'user.login': gen_string('alphanumeric'), @@ -774,7 +774,8 @@ def test_positive_reposet_disable(session, target_sat, function_entitlement_mani ) ] ) - assert results and all([result == 'Syncing Complete.' for result in results]) + assert results + assert all([result == 'Syncing Complete.' for result in results]) session.redhatrepository.disable(repository_name) assert not session.redhatrepository.search( f'name = "{repository_name}"', category='Enabled' @@ -825,7 +826,8 @@ def test_positive_reposet_disable_after_manifest_deleted( ) ] ) - assert results and all([result == 'Syncing Complete.' for result in results]) + assert results + assert all([result == 'Syncing Complete.' for result in results]) # Delete manifest sub.delete_manifest(data={'organization_id': org.id}) # Verify that the displayed repository name is correct @@ -904,7 +906,8 @@ def test_positive_delete_rhel_repo(session, module_entitlement_manifest_org, tar ) ] ) - assert results and all([result == 'Syncing Complete.' for result in results]) + assert results + assert all([result == 'Syncing Complete.' for result in results]) session.repository.delete(product_name, repository_name) assert not session.redhatrepository.search( f'name = "{repository_name}"', category='Enabled' diff --git a/tests/foreman/ui/test_rhc.py b/tests/foreman/ui/test_rhc.py index 6fa17a18589..2ffe7b11e12 100644 --- a/tests/foreman/ui/test_rhc.py +++ b/tests/foreman/ui/test_rhc.py @@ -64,7 +64,7 @@ def module_rhc_org(module_target_sat): return org -@pytest.fixture() +@pytest.fixture def fixture_setup_rhc_satellite( request, module_target_sat, diff --git a/tests/foreman/ui/test_settings.py b/tests/foreman/ui/test_settings.py index ade4cd9afbe..923d542443d 100644 --- a/tests/foreman/ui/test_settings.py +++ b/tests/foreman/ui/test_settings.py @@ -83,10 +83,10 @@ def test_positive_update_restrict_composite_view(session, setting_update, repo_s session.contentview.promote( composite_cv.name, 'Version 1.0', repo_setup['lce'].name ) - assert ( - 'Administrator -> Settings -> Content page using the ' - 'restrict_composite_view flag.' in str(context.value) - ) + assert ( + 'Administrator -> Settings -> Content page using the ' + 'restrict_composite_view flag.' in str(context.value) + ) else: result = session.contentview.promote( composite_cv.name, 'Version 1.0', repo_setup['lce'].name @@ -139,7 +139,7 @@ def test_negative_validate_foreman_url_error_message(session, setting_update): invalid_value = [invalid_value for invalid_value in invalid_settings_values()][0] with pytest.raises(AssertionError) as context: session.settings.update(f'name = {property_name}', invalid_value) - assert 'Value is invalid: must be integer' in str(context.value) + assert 'Value is invalid: must be integer' in str(context.value) @pytest.mark.tier2 @@ -509,7 +509,7 @@ def test_negative_update_hostname_with_empty_fact(session, setting_update): with session: with pytest.raises(AssertionError) as context: session.settings.update(property_name, new_hostname) - assert 'can\'t be blank' in str(context.value) + assert 'can\'t be blank' in str(context.value) @pytest.mark.run_in_one_thread diff --git a/tests/foreman/ui/test_subscription.py b/tests/foreman/ui/test_subscription.py index 617c9753403..473753fa28b 100644 --- a/tests/foreman/ui/test_subscription.py +++ b/tests/foreman/ui/test_subscription.py @@ -398,7 +398,8 @@ def test_positive_view_vdc_guest_subscription_products( f'subscription_name = "{VDC_SUBSCRIPTION_NAME}" ' f'and name = "{virt_who_hypervisor_host["name"]}"' ) - assert content_hosts and content_hosts[0]['Name'] == virt_who_hypervisor_host['name'] + assert content_hosts + assert content_hosts[0]['Name'] == virt_who_hypervisor_host['name'] # ensure that hypervisor guests subscription provided products list is not empty and # that the product is in provided products. provided_products = session.subscription.provided_products( diff --git a/tests/foreman/virtwho/api/test_esx.py b/tests/foreman/virtwho/api/test_esx.py index 1e0396d4ce5..e7544047d7e 100644 --- a/tests/foreman/virtwho/api/test_esx.py +++ b/tests/foreman/virtwho/api/test_esx.py @@ -383,10 +383,9 @@ def test_positive_remove_env_option( env_error = ( f"option {{\'{option}\'}} is not exist or not be enabled in {{\'{config_file}\'}}" ) - try: + with pytest.raises(Exception) as exc_info: # noqa: PT011 - TODO determine better exception get_configure_option({option}, {config_file}) - except Exception as VirtWhoError: - assert env_error == str(VirtWhoError) + assert str(exc_info.value) == env_error # Check /var/log/messages should not display warning message env_warning = f"Ignoring unknown configuration option \"{option}\"" result = target_sat.execute(f'grep "{env_warning}" /var/log/messages') diff --git a/tests/foreman/virtwho/api/test_esx_sca.py b/tests/foreman/virtwho/api/test_esx_sca.py index 3a967cbded1..3ac780254f0 100644 --- a/tests/foreman/virtwho/api/test_esx_sca.py +++ b/tests/foreman/virtwho/api/test_esx_sca.py @@ -431,10 +431,9 @@ def test_positive_remove_env_option( env_error = ( f"option {{\'{option}\'}} is not exist or not be enabled in {{\'{config_file}\'}}" ) - try: + with pytest.raises(Exception) as exc_info: # noqa: PT011 - TODO determine better exception get_configure_option({option}, {config_file}) - except Exception as VirtWhoError: - assert env_error == str(VirtWhoError) + assert str(exc_info.value) == env_error # Check /var/log/messages should not display warning message env_warning = f"Ignoring unknown configuration option \"{option}\"" result = target_sat.execute(f'grep "{env_warning}" /var/log/messages') diff --git a/tests/foreman/virtwho/api/test_nutanix.py b/tests/foreman/virtwho/api/test_nutanix.py index d9ddc34938f..ae2726cf610 100644 --- a/tests/foreman/virtwho/api/test_nutanix.py +++ b/tests/foreman/virtwho/api/test_nutanix.py @@ -250,10 +250,9 @@ def test_positive_ahv_internal_debug_option( config_file = get_configure_file(virtwho_config_api.id) option = 'ahv_internal_debug' env_error = f"option {option} is not exist or not be enabled in {config_file}" - try: + with pytest.raises(Exception) as exc_info: # noqa: PT011 - TODO determine better exception get_configure_option("ahv_internal_debug", config_file) - except Exception as VirtWhoError: - assert env_error == str(VirtWhoError) + assert str(exc_info.value) == env_error # check message exist in log file /var/log/rhsm/rhsm.log message = 'Value for "ahv_internal_debug" not set, using default: False' assert check_message_in_rhsm_log(message) == message diff --git a/tests/foreman/virtwho/cli/test_esx.py b/tests/foreman/virtwho/cli/test_esx.py index fe2b1827bf6..c604fc6f3f2 100644 --- a/tests/foreman/virtwho/cli/test_esx.py +++ b/tests/foreman/virtwho/cli/test_esx.py @@ -38,7 +38,7 @@ ) -@pytest.fixture() +@pytest.fixture def form_data(target_sat, default_org): form = { 'name': gen_string('alpha'), @@ -56,7 +56,7 @@ def form_data(target_sat, default_org): return form -@pytest.fixture() +@pytest.fixture def virtwho_config(form_data, target_sat): virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] yield virtwho_config @@ -446,10 +446,9 @@ def test_positive_remove_env_option(self, default_org, form_data, virtwho_config env_error = ( f"option {{\'{option}\'}} is not exist or not be enabled in {{\'{config_file}\'}}" ) - try: + with pytest.raises(Exception) as exc_info: # noqa: PT011 - TODO determine better exception get_configure_option({option}, {config_file}) - except Exception as VirtWhoError: - assert env_error == str(VirtWhoError) + assert str(exc_info.value) == env_error # Check /var/log/messages should not display warning message env_warning = f"Ignoring unknown configuration option \"{option}\"" result = target_sat.execute(f'grep "{env_warning}" /var/log/messages') diff --git a/tests/foreman/virtwho/cli/test_esx_sca.py b/tests/foreman/virtwho/cli/test_esx_sca.py index df15cce8105..d8ec7c71d9d 100644 --- a/tests/foreman/virtwho/cli/test_esx_sca.py +++ b/tests/foreman/virtwho/cli/test_esx_sca.py @@ -36,7 +36,7 @@ ) -@pytest.fixture() +@pytest.fixture def form_data(target_sat, module_sca_manifest_org): form = { 'name': gen_string('alpha'), @@ -54,7 +54,7 @@ def form_data(target_sat, module_sca_manifest_org): return form -@pytest.fixture() +@pytest.fixture def virtwho_config(form_data, target_sat): virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] yield virtwho_config @@ -545,10 +545,9 @@ def test_positive_remove_env_option( env_error = ( f"option {{\'{option}\'}} is not exist or not be enabled in {{\'{config_file}\'}}" ) - try: + with pytest.raises(Exception) as exc_info: # noqa: PT011 - TODO determine better exception get_configure_option({option}, {config_file}) - except Exception as VirtWhoError: - assert env_error == str(VirtWhoError) + assert str(exc_info.value) == env_error # Check /var/log/messages should not display warning message env_warning = f"Ignoring unknown configuration option \"{option}\"" result = target_sat.execute(f'grep "{env_warning}" /var/log/messages') diff --git a/tests/foreman/virtwho/cli/test_hyperv.py b/tests/foreman/virtwho/cli/test_hyperv.py index 1fa31db9f00..35e90d61eb1 100644 --- a/tests/foreman/virtwho/cli/test_hyperv.py +++ b/tests/foreman/virtwho/cli/test_hyperv.py @@ -29,7 +29,7 @@ ) -@pytest.fixture() +@pytest.fixture def form_data(target_sat, default_org): form = { 'name': gen_string('alpha'), @@ -47,7 +47,7 @@ def form_data(target_sat, default_org): return form -@pytest.fixture() +@pytest.fixture def virtwho_config(form_data, target_sat): virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] yield virtwho_config diff --git a/tests/foreman/virtwho/cli/test_hyperv_sca.py b/tests/foreman/virtwho/cli/test_hyperv_sca.py index e3909489e21..6d60e285c9d 100644 --- a/tests/foreman/virtwho/cli/test_hyperv_sca.py +++ b/tests/foreman/virtwho/cli/test_hyperv_sca.py @@ -29,7 +29,7 @@ ) -@pytest.fixture() +@pytest.fixture def form_data(target_sat, module_sca_manifest_org): form = { 'name': gen_string('alpha'), @@ -47,7 +47,7 @@ def form_data(target_sat, module_sca_manifest_org): return form -@pytest.fixture() +@pytest.fixture def virtwho_config(form_data, target_sat): virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] yield virtwho_config diff --git a/tests/foreman/virtwho/cli/test_kubevirt.py b/tests/foreman/virtwho/cli/test_kubevirt.py index c6d38de60dd..e003294de87 100644 --- a/tests/foreman/virtwho/cli/test_kubevirt.py +++ b/tests/foreman/virtwho/cli/test_kubevirt.py @@ -29,7 +29,7 @@ ) -@pytest.fixture() +@pytest.fixture def form_data(target_sat, default_org): form = { 'name': gen_string('alpha'), @@ -45,7 +45,7 @@ def form_data(target_sat, default_org): return form -@pytest.fixture() +@pytest.fixture def virtwho_config(form_data, target_sat): virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] yield virtwho_config diff --git a/tests/foreman/virtwho/cli/test_kubevirt_sca.py b/tests/foreman/virtwho/cli/test_kubevirt_sca.py index 99e4335c9f6..9746d80c34b 100644 --- a/tests/foreman/virtwho/cli/test_kubevirt_sca.py +++ b/tests/foreman/virtwho/cli/test_kubevirt_sca.py @@ -27,7 +27,7 @@ ) -@pytest.fixture() +@pytest.fixture def form_data(target_sat, module_sca_manifest_org): form = { 'name': gen_string('alpha'), @@ -43,7 +43,7 @@ def form_data(target_sat, module_sca_manifest_org): return form -@pytest.fixture() +@pytest.fixture def virtwho_config(form_data, target_sat): virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] yield virtwho_config diff --git a/tests/foreman/virtwho/cli/test_libvirt.py b/tests/foreman/virtwho/cli/test_libvirt.py index 1f5b034b473..5cd4280e4f3 100644 --- a/tests/foreman/virtwho/cli/test_libvirt.py +++ b/tests/foreman/virtwho/cli/test_libvirt.py @@ -29,7 +29,7 @@ ) -@pytest.fixture() +@pytest.fixture def form_data(target_sat, default_org): form = { 'name': gen_string('alpha'), @@ -46,7 +46,7 @@ def form_data(target_sat, default_org): return form -@pytest.fixture() +@pytest.fixture def virtwho_config(form_data, target_sat): virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] yield virtwho_config diff --git a/tests/foreman/virtwho/cli/test_libvirt_sca.py b/tests/foreman/virtwho/cli/test_libvirt_sca.py index b29ffaf667f..b1a359c0095 100644 --- a/tests/foreman/virtwho/cli/test_libvirt_sca.py +++ b/tests/foreman/virtwho/cli/test_libvirt_sca.py @@ -27,7 +27,7 @@ ) -@pytest.fixture() +@pytest.fixture def form_data(target_sat, module_sca_manifest_org): form = { 'name': gen_string('alpha'), @@ -44,7 +44,7 @@ def form_data(target_sat, module_sca_manifest_org): return form -@pytest.fixture() +@pytest.fixture def virtwho_config(form_data, target_sat): virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] yield virtwho_config diff --git a/tests/foreman/virtwho/cli/test_nutanix.py b/tests/foreman/virtwho/cli/test_nutanix.py index 9b9437d0386..ed9ad20ce49 100644 --- a/tests/foreman/virtwho/cli/test_nutanix.py +++ b/tests/foreman/virtwho/cli/test_nutanix.py @@ -31,7 +31,7 @@ ) -@pytest.fixture() +@pytest.fixture def form_data(target_sat, default_org): form = { 'name': gen_string('alpha'), @@ -51,7 +51,7 @@ def form_data(target_sat, default_org): return form -@pytest.fixture() +@pytest.fixture def virtwho_config(form_data, target_sat): virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] yield virtwho_config @@ -260,10 +260,9 @@ def test_positive_ahv_internal_debug_option( config_file = get_configure_file(virtwho_config['id']) option = 'ahv_internal_debug' env_error = f"option {option} is not exist or not be enabled in {config_file}" - try: + with pytest.raises(Exception) as exc_info: # noqa: PT011 - TODO determine better exception get_configure_option("ahv_internal_debug", config_file) - except Exception as VirtWhoError: - assert env_error == str(VirtWhoError) + assert str(exc_info.value) == env_error # check message exist in log file /var/log/rhsm/rhsm.log message = 'Value for "ahv_internal_debug" not set, using default: False' assert check_message_in_rhsm_log(message) == message diff --git a/tests/foreman/virtwho/cli/test_nutanix_sca.py b/tests/foreman/virtwho/cli/test_nutanix_sca.py index 59ba58f6f60..44876700c23 100644 --- a/tests/foreman/virtwho/cli/test_nutanix_sca.py +++ b/tests/foreman/virtwho/cli/test_nutanix_sca.py @@ -29,7 +29,7 @@ ) -@pytest.fixture() +@pytest.fixture def form_data(target_sat, module_sca_manifest_org): sca_form = { 'name': gen_string('alpha'), @@ -48,7 +48,7 @@ def form_data(target_sat, module_sca_manifest_org): return sca_form -@pytest.fixture() +@pytest.fixture def virtwho_config(form_data, target_sat): virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] yield virtwho_config diff --git a/tests/foreman/virtwho/conftest.py b/tests/foreman/virtwho/conftest.py index 0bd09647bf6..8e422b22940 100644 --- a/tests/foreman/virtwho/conftest.py +++ b/tests/foreman/virtwho/conftest.py @@ -36,7 +36,7 @@ def module_user(request, module_target_sat, default_org, default_location): logger.warning('Unable to delete session user: %s', str(err)) -@pytest.fixture() +@pytest.fixture def session(test_name, module_user): """Session fixture which automatically initializes (but does not start!) airgun UI session and correctly passes current test name to it. Uses shared @@ -84,7 +84,7 @@ def module_user_sca(request, module_target_sat, module_org, module_location): logger.warning('Unable to delete session user: %s', str(err)) -@pytest.fixture() +@pytest.fixture def session_sca(test_name, module_user_sca): """Session fixture which automatically initializes (but does not start!) airgun UI session and correctly passes current test name to it. Uses shared diff --git a/tests/foreman/virtwho/ui/test_esx.py b/tests/foreman/virtwho/ui/test_esx.py index 2715febb34f..75652da9bd6 100644 --- a/tests/foreman/virtwho/ui/test_esx.py +++ b/tests/foreman/virtwho/ui/test_esx.py @@ -752,10 +752,9 @@ def test_positive_remove_env_option( env_error = ( f"option {{\'{option}\'}} is not exist or not be enabled in {{\'{config_file}\'}}" ) - try: + with pytest.raises(Exception) as exc_info: # noqa: PT011 - TODO determine better exception get_configure_option({option}, {config_file}) - except Exception as VirtWhoError: - assert env_error == str(VirtWhoError) + assert str(exc_info.value) == env_error # Check /var/log/messages should not display warning message env_warning = f"Ignoring unknown configuration option \"{option}\"" result = target_sat.execute(f'grep "{env_warning}" /var/log/messages') diff --git a/tests/foreman/virtwho/ui/test_esx_sca.py b/tests/foreman/virtwho/ui/test_esx_sca.py index f8eb03fea98..63c4c55a16b 100644 --- a/tests/foreman/virtwho/ui/test_esx_sca.py +++ b/tests/foreman/virtwho/ui/test_esx_sca.py @@ -327,10 +327,9 @@ def test_positive_remove_env_option( env_error = ( f"option {{\'{option}\'}} is not exist or not be enabled in {{\'{config_file}\'}}" ) - try: + with pytest.raises(Exception) as exc_info: # noqa: PT011 - TODO determine better exception get_configure_option({option}, {config_file}) - except Exception as VirtWhoError: - assert env_error == str(VirtWhoError) + assert str(exc_info.value) == env_error # Check /var/log/messages should not display warning message env_warning = f"Ignoring unknown configuration option \"{option}\"" result = target_sat.execute(f'grep "{env_warning}" /var/log/messages') diff --git a/tests/foreman/virtwho/ui/test_nutanix.py b/tests/foreman/virtwho/ui/test_nutanix.py index 8bd1a3b23c7..652f3354559 100644 --- a/tests/foreman/virtwho/ui/test_nutanix.py +++ b/tests/foreman/virtwho/ui/test_nutanix.py @@ -236,10 +236,9 @@ def test_positive_ahv_internal_debug_option( # ahv_internal_debug does not set in virt-who-config-X.conf option = 'ahv_internal_debug' env_error = f"option {option} is not exist or not be enabled in {config_file}" - try: + with pytest.raises(Exception) as exc_info: # noqa: PT011 - TODO determine better exception get_configure_option("ahv_internal_debug", config_file) - except Exception as VirtWhoError: - assert env_error == str(VirtWhoError) + assert str(exc_info.value) == env_error # check message exist in log file /var/log/rhsm/rhsm.log message = 'Value for "ahv_internal_debug" not set, using default: False' assert check_message_in_rhsm_log(message) == message diff --git a/tests/foreman/virtwho/ui/test_nutanix_sca.py b/tests/foreman/virtwho/ui/test_nutanix_sca.py index 42de668055e..eb2d7d889ad 100644 --- a/tests/foreman/virtwho/ui/test_nutanix_sca.py +++ b/tests/foreman/virtwho/ui/test_nutanix_sca.py @@ -205,10 +205,9 @@ def test_positive_ahv_internal_debug_option( # ahv_internal_debug does not set in virt-who-config-X.conf option = 'ahv_internal_debug' env_error = f"option {option} is not exist or not be enabled in {config_file}" - try: + with pytest.raises(Exception) as exc_info: # noqa: PT011 - TODO determine better exception get_configure_option("ahv_internal_debug", config_file) - except Exception as VirtWhoError: - assert env_error == str(VirtWhoError) + assert str(exc_info.value) == env_error # check message exist in log file /var/log/rhsm/rhsm.log message = 'Value for "ahv_internal_debug" not set, using default: False' assert check_message_in_rhsm_log(message) == message diff --git a/tests/robottelo/conftest.py b/tests/robottelo/conftest.py index df6419a4f24..d8ffb1ad75b 100644 --- a/tests/robottelo/conftest.py +++ b/tests/robottelo/conftest.py @@ -13,13 +13,13 @@ def align_to_satellite(): pass -@pytest.fixture(scope='function') +@pytest.fixture def dummy_test(request): """This should be indirectly parametrized to provide dynamic dummy_tests to exec_test""" return request.param -@pytest.fixture(scope='function') +@pytest.fixture def exec_test(request, dummy_test): """Create a temporary file with the string provided by dummy_test, and run it with pytest.main diff --git a/tests/robottelo/test_cli.py b/tests/robottelo/test_cli.py index 78b0f6f0cf8..706a1d47785 100644 --- a/tests/robottelo/test_cli.py +++ b/tests/robottelo/test_cli.py @@ -301,7 +301,7 @@ def test_exists_with_option_and_no_empty_return(self, lst_method): assert 1 == response @mock.patch('robottelo.cli.base.Base.command_requires_org') - def test_info_requires_organization_id(self, _): + def test_info_requires_organization_id(self, _): # noqa: PT019 - not a fixture """Check info raises CLIError with organization-id is not present in options """ diff --git a/tests/robottelo/test_datafactory.py b/tests/robottelo/test_datafactory.py index 04e081a9b39..b93d401ed89 100644 --- a/tests/robottelo/test_datafactory.py +++ b/tests/robottelo/test_datafactory.py @@ -13,7 +13,7 @@ class TestFilteredDataPoint: """Tests for :meth:`robottelo.utils.datafactory.filtered_datapoint` decorator""" - @pytest.fixture(scope="function") + @pytest.fixture def run_one_datapoint(self, request): # Modify run_one_datapoint on settings singleton based on the indirect param # default to false when not parametrized diff --git a/tests/robottelo/test_decorators.py b/tests/robottelo/test_decorators.py index ff333fbb7e1..0f8ac441ce9 100644 --- a/tests/robottelo/test_decorators.py +++ b/tests/robottelo/test_decorators.py @@ -9,7 +9,7 @@ class TestCacheable: """Tests for :func:`robottelo.utils.decorators.cacheable`.""" - @pytest.fixture(scope="function") + @pytest.fixture def make_foo(self): mocked_object_cache_patcher = mock.patch.dict('robottelo.utils.decorators.OBJECT_CACHE') mocked_object_cache_patcher.start() diff --git a/tests/robottelo/test_func_locker.py b/tests/robottelo/test_func_locker.py index ed010e56076..ad4ab5c74aa 100644 --- a/tests/robottelo/test_func_locker.py +++ b/tests/robottelo/test_func_locker.py @@ -193,7 +193,7 @@ def simple_function_not_locked(): class TestFuncLocker: - @pytest.fixture(scope="function", autouse=True) + @pytest.fixture(autouse=True) def count_and_pool(self): global counter_file counter_file.write('0') @@ -371,7 +371,9 @@ def test_recursive_lock_function(self, count_and_pool, recursive_function): """Ensure that recursive calls to locked function is detected using lock_function decorator""" res = count_and_pool.apply_async(recursive_function, ()) - with pytest.raises(func_locker.FunctionLockerError, match=r'.*recursion detected.*'): + with pytest.raises( # noqa: PT012 + func_locker.FunctionLockerError, match=r'.*recursion detected.*' + ): try: res.get(timeout=5) except multiprocessing.TimeoutError: diff --git a/tests/robottelo/test_func_shared.py b/tests/robottelo/test_func_shared.py index 8ab9fb2f06d..7cc635e1aa0 100644 --- a/tests/robottelo/test_func_shared.py +++ b/tests/robottelo/test_func_shared.py @@ -163,13 +163,13 @@ def scope(self): # generate a new namespace scope = gen_string('alpha', 10) set_default_scope(scope) - yield scope + return scope - @pytest.fixture(scope='function', autouse=True) + @pytest.fixture(autouse=True) def enable(self): enable_shared_function(True) - @pytest.fixture(scope='function') + @pytest.fixture def pool(self): pool = multiprocessing.Pool(DEFAULT_POOL_SIZE) yield pool diff --git a/tests/robottelo/test_issue_handlers.py b/tests/robottelo/test_issue_handlers.py index 090e031b6f7..4da0b6df011 100644 --- a/tests/robottelo/test_issue_handlers.py +++ b/tests/robottelo/test_issue_handlers.py @@ -342,8 +342,8 @@ def test_bz_should_not_deselect(self): @pytest.mark.parametrize('issue', ["BZ123456", "XX:123456", "KK:89456", "123456", 999999]) def test_invalid_handler(self, issue): """Assert is_open w/ invalid handlers raise AttributeError""" + issue_deselect = should_deselect(issue) with pytest.raises(AttributeError): - issue_deselect = should_deselect(issue) is_open(issue) assert issue_deselect is None diff --git a/tests/upgrades/test_activation_key.py b/tests/upgrades/test_activation_key.py index 7e7cdf9de21..e58c06fee13 100644 --- a/tests/upgrades/test_activation_key.py +++ b/tests/upgrades/test_activation_key.py @@ -26,7 +26,7 @@ class TestActivationKey: operated/modified. """ - @pytest.fixture(scope='function') + @pytest.fixture def activation_key_setup(self, request, target_sat): """ The purpose of this fixture is to setup the activation key based on the provided @@ -45,7 +45,7 @@ def activation_key_setup(self, request, target_sat): content_view=cv, organization=org, name=f"{request.param}_ak" ).create() ak_details = {'org': org, "cv": cv, 'ak': ak, 'custom_repo': custom_repo} - yield ak_details + return ak_details @pytest.mark.pre_upgrade @pytest.mark.parametrize( diff --git a/tests/upgrades/test_classparameter.py b/tests/upgrades/test_classparameter.py index c22d39c48b1..f113f7ab235 100644 --- a/tests/upgrades/test_classparameter.py +++ b/tests/upgrades/test_classparameter.py @@ -51,7 +51,7 @@ class TestScenarioPositivePuppetParameterAndDatatypeIntact: """ @pytest.fixture(scope="class") - def _setup_scenario(self, class_target_sat): + def setup_scenario(self, class_target_sat): """Import some parametrized puppet classes. This is required to make sure that we have smart class variable available. Read all available smart class parameters for imported puppet class to @@ -100,7 +100,7 @@ def _validate_value(self, data, sc_param): @pytest.mark.pre_upgrade @pytest.mark.parametrize('count', list(range(1, 10))) def test_pre_puppet_class_parameter_data_and_type( - self, class_target_sat, count, _setup_scenario, save_test_data + self, class_target_sat, count, setup_scenario, save_test_data ): """Puppet Class parameters with different data type are created @@ -116,7 +116,7 @@ def test_pre_puppet_class_parameter_data_and_type( :expectedresults: The parameters are updated with different data types """ - save_test_data(_setup_scenario) + save_test_data(setup_scenario) data = _valid_sc_parameters_data()[count - 1] sc_param = class_target_sat.api.SmartClassParameters().search( query={'search': f'parameter="api_classparameters_scp_00{count}"'} @@ -130,9 +130,10 @@ def test_pre_puppet_class_parameter_data_and_type( self._validate_value(data, sc_param) @pytest.mark.post_upgrade(depend_on=test_pre_puppet_class_parameter_data_and_type) + @pytest.mark.usefixtures('_clean_scenario') @pytest.mark.parametrize('count', list(range(1, 10))) def test_post_puppet_class_parameter_data_and_type( - self, count, _clean_scenario, class_pre_upgrade_data, class_target_sat + self, count, class_pre_upgrade_data, class_target_sat ): """Puppet Class Parameters value and type is intact post upgrade diff --git a/tests/upgrades/test_host.py b/tests/upgrades/test_host.py index 72a4465eea9..a53a36814ff 100644 --- a/tests/upgrades/test_host.py +++ b/tests/upgrades/test_host.py @@ -90,7 +90,7 @@ def class_host( image=module_gce_finishimg, root_pass=gen_string('alphanumeric'), ).create() - yield host + return host def google_host(self, googleclient): """Returns the Google Client Host object to perform the assertions""" From 60980c509236256ebf96d00f7b1b7cd92a565ebd Mon Sep 17 00:00:00 2001 From: Jake Callahan Date: Tue, 14 Nov 2023 10:11:58 -0500 Subject: [PATCH 319/586] Add SharedResource class to robottelo.utils (#12341) This class will be used to manage shared resources between multitple processes or threads. The most common case will likely be a shared upgrade satellite between multiple xdist workers. I also handled an issues I encountered with the config_helpers script. (cherry picked from commit 2d3cd63d6b34b9d2449c25c5404e03f6d5d0447a) --- robottelo/utils/shared_resource.py | 199 ++++++++++++++++++++++++ scripts/config_helpers.py | 5 +- tests/robottelo/test_shared_resource.py | 61 ++++++++ 3 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 robottelo/utils/shared_resource.py create mode 100644 tests/robottelo/test_shared_resource.py diff --git a/robottelo/utils/shared_resource.py b/robottelo/utils/shared_resource.py new file mode 100644 index 00000000000..0ad0bd92e46 --- /dev/null +++ b/robottelo/utils/shared_resource.py @@ -0,0 +1,199 @@ +"""Allow multiple processes to communicate status on a single shared resource. + +This is useful for cases where multiple processes need to wait for all other processes to be ready +before continuing with some common action. The most common use case in this framework will likely +be to wait for all pre-upgrade setups to be ready before performing the upgrade. + +The system works by creating a file in /tmp with the name of the resource. This is a common file +where each process can communicate its status. The first process to register will be the main +watcher. The main watcher will wait for all other processes to be ready, then perform the action. +If the main actor fails to complete the action, and the action is recoverable, another process +will take over as the main watcher and attempt to perform the action. If the action is not +recoverable, the main watcher will fail and release all other processes. + +It is recommended to use this class as a context manager, as it will automatically register and +report when the process is done. + +Example: + >>> with SharedResource("target_sat.hostname", upgrade_action, **upgrade_kwargs) as resource: + ... # Do pre-upgrade setup steps + ... resource.ready() # tell the other processes that we are ready + ... yield target_sat # give the upgraded satellite to the test + ... # Do post-upgrade cleanup steps if any +""" +import json +from pathlib import Path +import time +from uuid import uuid4 + +from broker.helpers import FileLock + + +class SharedResource: + """A class representing a shared resource. + + Attributes: + action (function): The function to be executed when the resource is ready. + action_args (tuple): The arguments to be passed to the action function. + action_kwargs (dict): The keyword arguments to be passed to the action function. + action_is_recoverable (bool): Whether the action is recoverable or not. + id (str): The unique identifier of the shared resource. + resource_file (Path): The path to the file representing the shared resource. + is_main (bool): Whether the current instance is the main watcher or not. + is_recovering (bool): Whether the current instance is recovering from an error or not. + """ + + def __init__(self, resource_name, action, *action_args, **action_kwargs): + """Initializes a new instance of the SharedResource class. + + Args: + resource_name (str): The name of the shared resource. + action (function): The function to be executed when the resource is ready. + action_args (tuple): The arguments to be passed to the action function. + action_kwargs (dict): The keyword arguments to be passed to the action function. + """ + self.resource_file = Path(f"/tmp/{resource_name}.shared") + self.lock_file = FileLock(self.resource_file) + self.id = str(uuid4().fields[-1]) + self.action = action + self.action_is_recoverable = action_kwargs.pop("action_is_recoverable", False) + self.action_args = action_args + self.action_kwargs = action_kwargs + self.is_recovering = False + + def _update_status(self, status): + """Updates the status of the shared resource. + + Args: + status (str): The new status of the shared resource. + """ + with self.lock_file: + curr_data = json.loads(self.resource_file.read_text()) + curr_data["statuses"][self.id] = status + self.resource_file.write_text(json.dumps(curr_data, indent=4)) + + def _update_main_status(self, status): + """Updates the main status of the shared resource. + + Args: + status (str): The new main status of the shared resource. + """ + with self.lock_file: + curr_data = json.loads(self.resource_file.read_text()) + curr_data["main_status"] = status + self.resource_file.write_text(json.dumps(curr_data, indent=4)) + + def _check_all_status(self, status): + """Checks if all watchers have the specified status. + + Args: + status (str): The status to check for. + + Returns: + bool: True if all watchers have the specified status, False otherwise. + """ + with self.lock_file: + curr_data = json.loads(self.resource_file.read_text()) + for watcher_id in curr_data["watchers"]: + if curr_data["statuses"].get(watcher_id) != status: + return False + return True + + def _wait_for_status(self, status): + """Waits until all watchers have the specified status. + + Args: + status (str): The status to wait for. + """ + while not self._check_all_status(status): + time.sleep(1) + + def _wait_for_main_watcher(self): + """Waits for the main watcher to finish.""" + while True: + curr_data = json.loads(self.resource_file.read_text()) + if curr_data["main_status"] != "done": + time.sleep(60) + elif curr_data["main_status"] == "action_error": + self._try_take_over() + elif curr_data["main_status"] == "error": + raise Exception(f"Error in main watcher: {curr_data['main_watcher']}") + else: + break + + def _try_take_over(self): + """Tries to take over as the main watcher.""" + with self.lock_file: + curr_data = json.loads(self.resource_file.read_text()) + if curr_data["main_status"] in ("action_error", "error"): + curr_data["main_status"] = "recovering" + curr_data["main_watcher"] = self.id + self.resource_file.write_text(json.dumps(curr_data, indent=4)) + self.is_main = True + self.is_recovering = True + self.wait() + + def register(self): + """Registers the current process as a watcher.""" + with self.lock_file: + if self.resource_file.exists(): + curr_data = json.loads(self.resource_file.read_text()) + self.is_main = False + else: # First watcher to register, becomes the main watcher, and creates the file + curr_data = { + "watchers": [], + "statuses": {}, + "main_watcher": self.id, + "main_status": "waiting", + } + self.is_main = True + curr_data["watchers"].append(self.id) + curr_data["statuses"][self.id] = "pending" + self.resource_file.write_text(json.dumps(curr_data, indent=4)) + + def ready(self): + """Marks the current process as ready to perform the action.""" + self._update_status("ready") + self.wait() + + def done(self): + """Marks the current process as done performing post actions.""" + self._update_status("done") + + def act(self): + """Attempt to perform the action.""" + try: + self.action(*self.action_args, **self.action_kwargs) + except Exception as err: + self._update_main_status("error") + raise err + + def wait(self): + """Top-level wait function, separating behavior between main and non-main watchers.""" + if self.is_main and not (self.is_recovering and not self.action_is_recoverable): + self._wait_for_status("ready") + self._update_main_status("acting") + self.act() + self._update_main_status("done") + else: + self._wait_for_main_watcher() + + def __enter__(self): + """Registers the current process as a watcher and returns the instance.""" + self.register() + return self + + def __exit__(self, exc_type, exc_value, traceback): + """Marks the current process as done and updates the main watcher if needed.""" + if exc_type is FileNotFoundError: + raise exc_value + if exc_type is None: + self.done() + if self.is_main: + self._wait_for_status("done") + self.resource_file.unlink() + else: + self._update_status("error") + if self.is_main: + self._update_main_status("error") + raise exc_value diff --git a/scripts/config_helpers.py b/scripts/config_helpers.py index 85283bea05e..feb37c9bd62 100644 --- a/scripts/config_helpers.py +++ b/scripts/config_helpers.py @@ -14,8 +14,8 @@ def merge_nested_dictionaries(original, new, overwrite=False): # if the key is not in the original, add it if key not in original: original[key] = value - # if the key is in the original, and the value is a dictionary, recurse - elif isinstance(value, dict): + # if the key is in the original, and original[key] and value are dictionaries, recurse + elif isinstance(original[key], dict) and isinstance(value, dict): # use deepdiff to check if the dictionaries are the same if deepdiff.DeepDiff(original[key], value): original[key] = merge_nested_dictionaries(original[key], value, overwrite) @@ -24,6 +24,7 @@ def merge_nested_dictionaries(original, new, overwrite=False): # if the key is in the original, and the value is a list, ask the user elif overwrite == "ask": choice_prompt = ( + "-------------------------\n" f"The current value for {key} is {original[key]}.\n" "Please choose an option:\n" "1. Keep the current value\n" diff --git a/tests/robottelo/test_shared_resource.py b/tests/robottelo/test_shared_resource.py new file mode 100644 index 00000000000..ff2146cd80a --- /dev/null +++ b/tests/robottelo/test_shared_resource.py @@ -0,0 +1,61 @@ +import multiprocessing +from pathlib import Path +import random +from threading import Thread +import time + +from robottelo.utils.shared_resource import SharedResource + + +def upgrade_action(*args, **kwargs): + print(f"Upgrading satellite with {args=} and {kwargs=}") + time.sleep(1) + print("Satellite upgraded!") + + +def run_resource(resource_name): + time.sleep(random.random() * 5) # simulate random pre-setup + with SharedResource(resource_name, upgrade_action) as resource: + assert Path(f"/tmp/{resource_name}.shared").exists() + time.sleep(5) # simulate setup actions + resource.ready() + time.sleep(1) # simulate cleanup actions + + +def test_shared_resource(): + """Test the SharedResource class.""" + with SharedResource("test_resource", upgrade_action, 1, 2, 3, foo="bar") as resource: + assert Path("/tmp/test_resource.shared").exists() + assert resource.is_main + assert not resource.is_recovering + assert resource.action == upgrade_action + assert resource.action_args == (1, 2, 3) + assert resource.action_kwargs == {"foo": "bar"} + assert not resource.action_is_recoverable + + resource.ready() + assert resource._check_all_status("ready") + + assert not Path("/tmp/test_resource.shared").exists() + + +def test_shared_resource_multiprocessing(): + """Test the SharedResource class with multiprocessing.""" + with multiprocessing.Pool(2) as pool: + pool.map(run_resource, ["test_resource_mp", "test_resource_mp"]) + + assert not Path("/tmp/test_resource_mp.shared").exists() + + +def test_shared_resource_multithreading(): + """Test the SharedResource class with multithreading.""" + t1 = Thread(target=run_resource, args=("test_resource_th",)) + t2 = Thread(target=run_resource, args=("test_resource_th",)) + + t1.start() + t2.start() + + t1.join() + t2.join() + + assert not Path("/tmp/test_resource_th.shared").exists() From d8e4e429746ea688d5852b60d598c9d796b30268 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 15 Nov 2023 02:56:03 -0500 Subject: [PATCH 320/586] [6.14.z] Bump deepdiff from 6.7.0 to 6.7.1 (#13092) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 750aba55730..bccef65b173 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ betelgeuse==1.10.0 broker[docker]==0.4.1 cryptography==41.0.5 -deepdiff==6.7.0 +deepdiff==6.7.1 dynaconf[vault]==3.2.4 fauxfactory==3.1.0 jinja2==3.1.2 From 6f6c77d938715342ef9831e513987d7dd31a1eb6 Mon Sep 17 00:00:00 2001 From: Jacob Callahan Date: Wed, 1 Nov 2023 10:57:33 -0400 Subject: [PATCH 321/586] Remove BROKER_DIRECTORY from robottelo config This is something that broker handles on its own and, due to import timing, isn't handled correctly in robottelo anyway. Removing the field and config mechanism should eliminate confusion around this behavior. (cherry picked from commit bc53d944e9f7869efb43d722f60884e10ab61a55) --- conf/broker.yaml.template | 4 +--- robottelo/config/__init__.py | 9 --------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/conf/broker.yaml.template b/conf/broker.yaml.template index 5f5067c321a..0c039df13bb 100644 --- a/conf/broker.yaml.template +++ b/conf/broker.yaml.template @@ -1,7 +1,5 @@ BROKER: - # The path where your broker settings and inventory are located - # If you leave it blank, the default is the output of `broker --version` - BROKER_DIRECTORY: + # Broker has its own config which you can find by running `broker --version` HOST_WORKFLOWS: POWER_CONTROL: vm-power-operation EXTEND: extend-vm diff --git a/robottelo/config/__init__.py b/robottelo/config/__init__.py index e078d2fcc63..2d66d00054b 100644 --- a/robottelo/config/__init__.py +++ b/robottelo/config/__init__.py @@ -48,15 +48,6 @@ def get_settings(): settings = get_settings() - - -if not os.getenv('BROKER_DIRECTORY'): - # set the BROKER_DIRECTORY envar so broker knows where to operate from - if _broker_dir := settings.robottelo.get('BROKER_DIRECTORY'): - logger.debug(f'Setting BROKER_DIRECTORY to {_broker_dir}') - os.environ['BROKER_DIRECTORY'] = _broker_dir - - robottelo_tmp_dir = Path(settings.robottelo.tmp_dir) robottelo_tmp_dir.mkdir(parents=True, exist_ok=True) From 2fc4aa9b9afc5c2b33b7e4d47f9571fd016411ec Mon Sep 17 00:00:00 2001 From: Jacob Callahan Date: Wed, 27 Sep 2023 16:01:47 -0400 Subject: [PATCH 322/586] Add a mechanism to swap nailgun versions on demand Satellite._swap_nailgun("x.y.z") can be used to change out the operating version of nailgun. (cherry picked from commit f10676ef421b3dbc69e18b86e0a2850e95f28c62) --- robottelo/hosts.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 2971cdcd79a..fe13c06cab5 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1740,6 +1740,18 @@ def __init__(self, hostname=None, **kwargs): self._api = type('api', (), {'_configured': False}) self._cli = type('cli', (), {'_configured': False}) + def _swap_nailgun(self, new_version): + """Install a different version of nailgun from GitHub and invalidate the module cache.""" + import sys + + from pip._internal import main as pip_main + + pip_main(['uninstall', '-y', 'nailgun']) + pip_main(['install', f'https://github.com/SatelliteQE/nailgun/archive/{new_version}.zip']) + self._api = type('api', (), {'_configured': False}) + to_clear = [k for k in sys.modules.keys() if 'nailgun' in k] + [sys.modules.pop(k) for k in to_clear] + @property def api(self): """Import all nailgun entities and wrap them under self.api""" @@ -1747,7 +1759,7 @@ def api(self): self._api = type('api', (), {'_configured': False}) if self._api._configured: return self._api - + from nailgun import entities as _entities # use a private import from nailgun.config import ServerConfig from nailgun.entity_mixins import Entity @@ -1767,7 +1779,7 @@ class DecClass(cls): verify=settings.server.verify_ca, ) # add each nailgun entity to self.api, injecting our server config - for name, obj in entities.__dict__.items(): + for name, obj in _entities.__dict__.items(): try: if Entity in obj.mro(): # create a copy of the class and inject our server config into the __init__ From 1596a01da6c4decda048d2205db87d2a7f99ecc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Gajdu=C5=A1ek?= Date: Thu, 16 Nov 2023 14:28:16 +0100 Subject: [PATCH 323/586] [6.14.z] Bump actions/github-script from 6 to 7 (#13078) (#13105) Bump actions/github-script from 6 to 7 (#13078) (cherry picked from commit 1a7b56644dcd0e28bc99fb13f6ca1bd62c2f9305) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto_cherry_pick.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto_cherry_pick.yml b/.github/workflows/auto_cherry_pick.yml index f36ff0119e0..0ce0b8e9956 100644 --- a/.github/workflows/auto_cherry_pick.yml +++ b/.github/workflows/auto_cherry_pick.yml @@ -78,7 +78,7 @@ jobs: - name: is autoMerging enabled for Auto CherryPicked PRs ? if: ${{ always() && steps.cherrypick.outcome == 'success' && contains(github.event.pull_request.labels.*.name, 'AutoMerge_Cherry_Picked') }} - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: github-token: ${{ secrets.CHERRYPICK_PAT }} script: | From b60c379e393fc473b66e442c4c1aca86659703e9 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 16 Nov 2023 08:43:57 -0500 Subject: [PATCH 324/586] [6.14.z] Test fix for discovery reboot_all scenario (#13112) Test fix for discovery reboot_all scenario (#13102) Fix discovery test for reboot all scenario (cherry picked from commit f6024657f357d0101efa7181d018732abd30f503) Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> --- tests/foreman/api/test_discoveredhost.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/foreman/api/test_discoveredhost.py b/tests/foreman/api/test_discoveredhost.py index 70beeba274e..7d260b3d5e8 100644 --- a/tests/foreman/api/test_discoveredhost.py +++ b/tests/foreman/api/test_discoveredhost.py @@ -414,7 +414,6 @@ def test_positive_reboot_all_pxe_hosts( provision_multiple_hosts, provisioning_hostgroup, pxe_loader, - count, ): """Rebooting all pxe-based discovered hosts From cbc9fb43f7efe3e1facbb58e04b25d02a0a3d115 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 16 Nov 2023 09:46:21 -0500 Subject: [PATCH 325/586] [6.14.z] Bump pytest-xdist from 3.3.1 to 3.4.0 (#13116) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bccef65b173..9beba263473 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ pytest==7.4.3 pytest-services==2.2.1 pytest-mock==3.12.0 pytest-reportportal==5.3.0 -pytest-xdist==3.3.1 +pytest-xdist==3.4.0 pytest-ibutsu==2.2.4 PyYAML==6.0.1 requests==2.31.0 From 5d0fdedd8207a172bb1c13a9375be986861927bd Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:09:54 -0500 Subject: [PATCH 326/586] [6.14.z] Stream fix for e2e api http_proxy failure (#13118) --- tests/foreman/api/test_http_proxy.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/foreman/api/test_http_proxy.py b/tests/foreman/api/test_http_proxy.py index 0b4446069bb..1e6da4dfcf6 100644 --- a/tests/foreman/api/test_http_proxy.py +++ b/tests/foreman/api/test_http_proxy.py @@ -69,6 +69,7 @@ def test_positive_end_to_end(setup_http_proxy, module_target_sat, module_manifes reposet=constants.REPOSET['rhae2'], releasever=None, ) + module_target_sat.api.Repository(id=rh_repo_id).sync() rh_repo = module_target_sat.api.Repository( id=rh_repo_id, http_proxy_policy=http_proxy_policy, @@ -96,12 +97,13 @@ def test_positive_end_to_end(setup_http_proxy, module_target_sat, module_manifes # Use global_default_http_proxy repo_options['http_proxy_policy'] = 'global_default_http_proxy' repo_2 = module_target_sat.api.Repository(**repo_options).create() + repo_2.sync() assert repo_2.http_proxy_policy == 'global_default_http_proxy' # Update to selected_http_proxy repo_2.http_proxy_policy = 'none' repo_2.update(['http_proxy_policy']) - assert repo_2.http_proxy_policy == 'none' + assert repo_2.read().http_proxy_policy == 'none' # test scenario for yum type repo discovery. repo_name = 'fakerepo01' @@ -116,16 +118,16 @@ def test_positive_end_to_end(setup_http_proxy, module_target_sat, module_manifes assert yum_repo['output'][0] == f'{settings.repos.repo_discovery.url}/{repo_name}/' # test scenario for docker type repo discovery. - yum_repo = module_target_sat.api.Organization(id=module_manifest_org.id).repo_discover( + docker_repo = module_target_sat.api.Organization(id=module_manifest_org.id).repo_discover( data={ "id": module_manifest_org.id, "url": 'quay.io', "content_type": "docker", - "search": 'quay/busybox', + "search": 'foreman/foreman', } ) - assert len(yum_repo['output']) >= 1 - assert 'quay/busybox' in yum_repo['output'] + assert len(docker_repo['output']) > 0 + assert docker_repo['result'] == 'success' @pytest.mark.upgrade From d39c3d501aaf8b71675db5a8566a86b4e5382c17 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 17 Nov 2023 08:27:38 -0500 Subject: [PATCH 327/586] [6.14.z] Bump el9, el8 kickstart version to 9.3, 8.9 respectively (#13109) * Bump el9 kickstart version to 9.3 (#13069) Signed-off-by: Gaurav Talreja (cherry picked from commit 0cd07ec9a216d88fe77e7461ea7797e3a59ed4a5) * Bump el8 kickstart version to 8.9 --------- Co-authored-by: Gaurav Talreja Co-authored-by: Shubham Ganar --- robottelo/constants/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index 05b675454fa..de13702ecc8 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -530,32 +530,32 @@ class Colored(Box): }, 'rhel8_bos': { 'id': 'rhel-8-for-x86_64-baseos-kickstart', - 'name': 'Red Hat Enterprise Linux 8 for x86_64 - BaseOS Kickstart 8.8', - 'version': '8.8', + 'name': 'Red Hat Enterprise Linux 8 for x86_64 - BaseOS Kickstart 8.9', + 'version': '8.9', 'reposet': REPOSET['kickstart']['rhel8'], 'product': PRDS['rhel8'], 'distro': 'rhel8', }, 'rhel8_aps': { 'id': 'rhel-8-for-x86_64-appstream-kickstart', - 'name': 'Red Hat Enterprise Linux 8 for x86_64 - AppStream Kickstart 8.8', - 'version': '8.8', + 'name': 'Red Hat Enterprise Linux 8 for x86_64 - AppStream Kickstart 8.9', + 'version': '8.9', 'reposet': REPOSET['kickstart']['rhel8_aps'], 'product': PRDS['rhel8'], 'distro': 'rhel8', }, 'rhel9_bos': { 'id': 'rhel-9-for-x86_64-baseos-kickstart', - 'name': 'Red Hat Enterprise Linux 9 for x86_64 - BaseOS Kickstart 9.2', - 'version': '9.2', + 'name': 'Red Hat Enterprise Linux 9 for x86_64 - BaseOS Kickstart 9.3', + 'version': '9.3', 'reposet': REPOSET['kickstart']['rhel9'], 'product': PRDS['rhel9'], 'distro': 'rhel9', }, 'rhel9_aps': { 'id': 'rhel-9-for-x86_64-appstream-kickstart', - 'name': 'Red Hat Enterprise Linux 9 for x86_64 - AppStream Kickstart 9.2', - 'version': '9.2', + 'name': 'Red Hat Enterprise Linux 9 for x86_64 - AppStream Kickstart 9.3', + 'version': '9.3', 'reposet': REPOSET['kickstart']['rhel9_aps'], 'product': PRDS['rhel9'], 'distro': 'rhel9', From 32ac580b8c64fca88966112e17bdbba79c5bf407 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 17 Nov 2023 14:20:03 -0500 Subject: [PATCH 328/586] [6.14.z] use one fixture instead of multiple and remove problematic one (#13122) --- pytest_fixtures/component/repository.py | 18 -------- tests/foreman/cli/test_contentaccess.py | 58 +++++++++++++++---------- 2 files changed, 34 insertions(+), 42 deletions(-) diff --git a/pytest_fixtures/component/repository.py b/pytest_fixtures/component/repository.py index 5bbf8a81365..fc98c32f302 100644 --- a/pytest_fixtures/component/repository.py +++ b/pytest_fixtures/component/repository.py @@ -32,24 +32,6 @@ def module_product(module_org, module_target_sat): return module_target_sat.api.Product(organization=module_org).create() -@pytest.fixture(scope='module') -def rh_repo_gt_manifest(module_gt_manifest_org, module_target_sat): - """Use GT manifest org, creates RH tools repo, syncs and returns RH repo.""" - # enable rhel repo and return its ID - rh_repo_id = module_target_sat.api_factory.enable_rhrepo_and_fetchid( - basearch=DEFAULT_ARCHITECTURE, - org_id=module_gt_manifest_org.id, - product=PRDS['rhel'], - repo=REPOS['rhst7']['name'], - reposet=REPOSET['rhst7'], - releasever=None, - ) - # Sync step because repo is not synced by default - rh_repo = entities.Repository(id=rh_repo_id).read() - rh_repo.sync() - return rh_repo - - @pytest.fixture(scope='module') def module_rhst_repo(module_target_sat, module_org_with_manifest, module_promoted_cv, module_lce): """Use module org with manifest, creates RH tools repo, syncs and returns RH repo id.""" diff --git a/tests/foreman/cli/test_contentaccess.py b/tests/foreman/cli/test_contentaccess.py index fb8aaca5228..307d62ddf6b 100644 --- a/tests/foreman/cli/test_contentaccess.py +++ b/tests/foreman/cli/test_contentaccess.py @@ -23,10 +23,13 @@ from robottelo.cli.package import Package from robottelo.config import settings from robottelo.constants import ( + DEFAULT_ARCHITECTURE, + PRDS, REAL_0_ERRATA_ID, REAL_RHEL7_0_2_PACKAGE_FILENAME, REAL_RHEL7_0_2_PACKAGE_NAME, REPOS, + REPOSET, ) pytestmark = [ @@ -38,42 +41,48 @@ @pytest.fixture(scope='module') -def module_lce(module_sca_manifest_org): - return entities.LifecycleEnvironment(organization=module_sca_manifest_org).create() - +def rh_repo_setup_ak(module_sca_manifest_org, module_target_sat): + """Use module sca manifest org, creates rhst repo & syncs it, + also create CV, LCE & AK and return AK""" + rh_repo_id = module_target_sat.api_factory.enable_rhrepo_and_fetchid( + basearch=DEFAULT_ARCHITECTURE, + org_id=module_sca_manifest_org.id, + product=PRDS['rhel'], + repo=REPOS['rhst7']['name'], + reposet=REPOSET['rhst7'], + releasever=None, + ) + # Sync step because repo is not synced by default + rh_repo = module_target_sat.api.Repository(id=rh_repo_id).read() + rh_repo.sync() -@pytest.fixture(scope="module") -def rh_repo_cv(module_sca_manifest_org, rh_repo_gt_manifest, module_lce): - rh_repo_cv = entities.ContentView(organization=module_sca_manifest_org).create() + # Create CV, LCE and AK + cv = module_target_sat.api.ContentView(organization=module_sca_manifest_org).create() + lce = module_target_sat.api.LifecycleEnvironment(organization=module_sca_manifest_org).create() # Add CV to AK - rh_repo_cv.repository = [rh_repo_gt_manifest] - rh_repo_cv.update(['repository']) - rh_repo_cv.publish() - rh_repo_cv = rh_repo_cv.read() + cv.repository = [rh_repo] + cv.update(['repository']) + cv.publish() + cv = cv.read() # promote the last version published into the module lce - rh_repo_cv.version[-1].promote(data={'environment_ids': module_lce.id, 'force': False}) - return rh_repo_cv + cv.version[-1].promote(data={'environment_ids': lce.id, 'force': False}) - -@pytest.fixture(scope="module") -def module_ak(rh_repo_cv, module_sca_manifest_org, module_lce): - module_ak = entities.ActivationKey( - content_view=rh_repo_cv, - environment=module_lce, + ak = module_target_sat.api.ActivationKey( + content_view=cv, + environment=lce, organization=module_sca_manifest_org, ).create() # Ensure tools repo is enabled in the activation key - module_ak.content_override( + ak.content_override( data={'content_overrides': [{'content_label': REPOS['rhst7']['id'], 'value': '1'}]} ) - return module_ak + return ak @pytest.fixture(scope="module") def vm( - rh_repo_gt_manifest, + rh_repo_setup_ak, module_sca_manifest_org, - module_ak, rhel7_contenthost_module, module_target_sat, ): @@ -82,8 +91,9 @@ def vm( 'rpm -Uvh https://download.fedoraproject.org/pub/epel/7/x86_64/Packages/p/' 'python2-psutil-5.6.7-1.el7.x86_64.rpm' ) - rhel7_contenthost_module.install_katello_ca(module_target_sat) - rhel7_contenthost_module.register_contenthost(module_sca_manifest_org.label, module_ak.name) + rhel7_contenthost_module.register( + module_sca_manifest_org, None, rh_repo_setup_ak.name, module_target_sat + ) host = entities.Host().search(query={'search': f'name={rhel7_contenthost_module.hostname}'}) host_id = host[0].id host_content = entities.Host(id=host_id).read_json() From 27db253a3c3e43d74a6285e0c7045d42bbe0a226 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 20 Nov 2023 07:32:02 -0500 Subject: [PATCH 329/586] [6.14.z] Add UEFI HTTP Boot provisioning test (#13134) Add UEFI HTTP Boot provisioning test (#12908) Signed-off-by: Shubham Ganar (cherry picked from commit cd2d2b0e90bcc3bb1918a1ef4db672f0bdf0984a) Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> --- pytest_fixtures/component/provision_pxe.py | 1 + tests/foreman/api/test_provisioning.py | 167 ++++++++++++++++++++- 2 files changed, 167 insertions(+), 1 deletion(-) diff --git a/pytest_fixtures/component/provision_pxe.py b/pytest_fixtures/component/provision_pxe.py index 90e7c1798ff..f39701f408c 100644 --- a/pytest_fixtures/component/provision_pxe.py +++ b/pytest_fixtures/component/provision_pxe.py @@ -313,6 +313,7 @@ def pxe_loader(request): 'bios': {'vm_firmware': 'bios', 'pxe_loader': 'PXELinux BIOS'}, 'uefi': {'vm_firmware': 'uefi', 'pxe_loader': 'Grub2 UEFI'}, 'ipxe': {'vm_firmware': 'bios', 'pxe_loader': 'iPXE Embedded'}, + 'http_uefi': {'vm_firmware': 'uefi', 'pxe_loader': 'Grub2 UEFI HTTP'}, } return Box(PXE_LOADER_MAP[getattr(request, 'param', 'bios')]) diff --git a/tests/foreman/api/test_provisioning.py b/tests/foreman/api/test_provisioning.py index 3b4ded29dc8..f28e1d12730 100644 --- a/tests/foreman/api/test_provisioning.py +++ b/tests/foreman/api/test_provisioning.py @@ -16,14 +16,53 @@ :Upstream: No """ +import re + from fauxfactory import gen_string import pytest -from wait_for import wait_for +from wait_for import TimedOutError, wait_for from robottelo.config import settings +from robottelo.logging import logger from robottelo.utils.installer import InstallerCommand +def _read_log(ch, pattern): + """Read the first line from the given channel buffer and return the matching line""" + # read lines until the buffer is empty + for log_line in ch.stdout().splitlines(): + logger.debug(f'foreman-tail: {log_line}') + if re.search(pattern, log_line): + return log_line + else: + return None + + +def _wait_for_log(channel, pattern, timeout=5, delay=0.2): + """_read_log method enclosed in wait_for method""" + matching_log = wait_for( + _read_log, + func_args=( + channel, + pattern, + ), + fail_condition=None, + timeout=timeout, + delay=delay, + logger=logger, + ) + return matching_log.out + + +def assert_host_logs(channel, pattern): + """Reads foreman logs until given pattern found""" + try: + log = _wait_for_log(channel, pattern, timeout=300, delay=10) + assert pattern in log + except TimedOutError: + raise AssertionError(f'Timed out waiting for {pattern} from VM') + + @pytest.mark.e2e @pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) @pytest.mark.on_premises_provisioning @@ -276,3 +315,129 @@ def test_rhel_ipxe_provisioning( # assert that the host is subscribed and consumes # subsctiption provided by the activation key assert provisioning_host.subscribed, 'Host is not subscribed' + + +@pytest.mark.skip_if_open("BZ:2242925") +@pytest.mark.e2e +@pytest.mark.parametrize('pxe_loader', ['http_uefi'], indirect=True) +@pytest.mark.on_premises_provisioning +@pytest.mark.rhel_ver_match('[^6]') +def test_rhel_httpboot_provisioning( + request, + module_provisioning_sat, + module_sca_manifest_org, + module_location, + provisioning_host, + pxe_loader, + module_provisioning_rhel_content, + provisioning_hostgroup, + module_lce_library, + module_default_org_view, +): + """Provision a host using httpboot workflow + + :id: 98c2865e-5d21-402e-ad01-c474b7fc4eee + + :steps: + 1. Configure satellite for provisioning + 2. provision a host using pxe loader as Grub2 UEFI HTTP + 3. Check that resulting host is registered to Satellite + 4. Check host is subscribed to Satellite + + :expectedresults: + 1. Provisioning via HTTP is successful + 2. Host installs right version of RHEL + 3. Satellite is able to run REX job on the host + 4. Host is registered to Satellite and subscription status is 'Success' + + :parametrized: yes + + :BZ: 2242925 + """ + sat = module_provisioning_sat.sat + # update grub2-efi package + sat.cli.Packages.update(packages='grub2-efi', options={'assumeyes': True}) + + host_mac_addr = provisioning_host._broker_args['provisioning_nic_mac_addr'] + host = sat.api.Host( + hostgroup=provisioning_hostgroup, + organization=module_sca_manifest_org, + location=module_location, + name=gen_string('alpha').lower(), + mac=host_mac_addr, + operatingsystem=module_provisioning_rhel_content.os, + subnet=module_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) + # check for proper HTTP requests + shell = module_provisioning_sat.session.shell() + shell.send('foreman-tail') + assert_host_logs(shell, f'GET /httpboot/grub2/grub.cfg-{host_mac_addr} with 200') + # 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 = {sat.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' From a69a97221c6d04e592a0b438aa4868942ee139db Mon Sep 17 00:00:00 2001 From: rmynar <64528205+rmynar@users.noreply.github.com> Date: Tue, 21 Nov 2023 13:13:44 +0100 Subject: [PATCH 330/586] satellite and capsule installation improvements (#12961) * satellite and capsule installation improvements * check logs with journal instead of syslog * check logs for errors only * skip assertion if BZ is open * fix pit marker * improved logs testing (cherry picked from commit c4cef13c33aa8b28bb443e02c50191a89aa9ac4c) --- tests/foreman/installer/test_installer.py | 119 +++++++++------------- 1 file changed, 47 insertions(+), 72 deletions(-) diff --git a/tests/foreman/installer/test_installer.py b/tests/foreman/installer/test_installer.py index f76b899385b..7a6f3f04a55 100644 --- a/tests/foreman/installer/test_installer.py +++ b/tests/foreman/installer/test_installer.py @@ -24,6 +24,7 @@ from robottelo.constants import DEFAULT_ORG, FOREMAN_SETTINGS_YML, PRDS, REPOS, REPOSET from robottelo.hosts import setup_capsule from robottelo.utils.installer import InstallerCommand +from robottelo.utils.issue_handlers import is_open PREVIOUS_INSTALLER_OPTIONS = { '-', @@ -1324,10 +1325,28 @@ def extract_help(filter='params'): def common_sat_install_assertions(satellite): sat_version = 'stream' if satellite.is_stream else satellite.version assert settings.server.version.release == sat_version + + # no errors/failures in journald result = satellite.execute( - r'grep "\[ERROR" --after-context=100 /var/log/foreman-installer/satellite.log' + r'journalctl --quiet --no-pager --boot --priority err -u "dynflow-sidekiq*" -u "foreman-proxy" -u "foreman" -u "httpd" -u "postgresql" -u "pulpcore-api" -u "pulpcore-content" -u "pulpcore-worker*" -u "redis" -u "tomcat"' ) assert len(result.stdout) == 0 + # no errors in /var/log/foreman/production.log + result = satellite.execute(r'grep --context=100 -E "\[E\|" /var/log/foreman/production.log') + if not is_open('BZ:2247484'): + assert len(result.stdout) == 0 + # no errors/failures in /var/log/foreman-installer/satellite.log + result = satellite.execute( + r'grep "\[ERROR" --context=100 /var/log/foreman-installer/satellite.log' + ) + assert len(result.stdout) == 0 + # no errors/failures in /var/log/httpd/* + result = satellite.execute(r'grep -iR "error" /var/log/httpd/*') + assert len(result.stdout) == 0 + # no errors/failures in /var/log/candlepin/* + result = satellite.execute(r'grep -iR "error" /var/log/candlepin/*') + assert len(result.stdout) == 0 + result = satellite.cli.Health.check() assert 'FAIL' not in result.stdout @@ -1350,7 +1369,6 @@ def install_satellite(satellite, installer_args): InstallerCommand(installer_args=installer_args).get_command(), timeout='30m', ) - common_sat_install_assertions(satellite) @pytest.fixture(scope='module') @@ -1361,7 +1379,8 @@ def sat_default_install(module_sat_ready_rhels): f'foreman-initial-admin-password {settings.server.admin_password}', ] install_satellite(module_sat_ready_rhels[0], installer_args) - return module_sat_ready_rhels[0] + yield module_sat_ready_rhels[0] + common_sat_install_assertions(module_sat_ready_rhels[0]) @pytest.fixture(scope='module') @@ -1374,11 +1393,13 @@ def sat_non_default_install(module_sat_ready_rhels): 'foreman-proxy-content-pulpcore-hide-guarded-distributions false', ] install_satellite(module_sat_ready_rhels[1], installer_args) - return module_sat_ready_rhels[1] + yield module_sat_ready_rhels[1] + common_sat_install_assertions(module_sat_ready_rhels[1]) @pytest.mark.e2e @pytest.mark.tier1 +@pytest.mark.pit_client def test_capsule_installation(sat_default_install, cap_ready_rhel, default_org): """Run a basic Capsule installation @@ -1392,6 +1413,8 @@ def test_capsule_installation(sat_default_install, cap_ready_rhel, default_org): :expectedresults: 1. Capsule is installed and setup correctly + 2. no unexpected errors in logs + 3. health check runs successfully :CaseImportance: Critical """ @@ -1413,14 +1436,29 @@ def test_capsule_installation(sat_default_install, cap_ready_rhel, default_org): assert sat_default_install.api.Capsule().search( query={'search': f'name={cap_ready_rhel.hostname}'} )[0] + + # no errors/failures in journald + result = cap_ready_rhel.execute( + r'journalctl --quiet --no-pager --boot --priority err -u foreman-proxy -u httpd -u postgresql -u pulpcore-api -u pulpcore-content -u pulpcore-worker* -u redis' + ) + assert len(result.stdout) == 0 + # no errors/failures /var/log/foreman-installer/satellite.log result = cap_ready_rhel.execute( - r'grep "\[ERROR" --after-context=100 /var/log/foreman-installer/satellite.log' + r'grep "\[ERROR" --context=100 /var/log/foreman-installer/satellite.log' ) assert len(result.stdout) == 0 + # no errors/failures /var/log/foreman-installer/capsule.log result = cap_ready_rhel.execute( - r'grep "\[ERROR" --after-context=100 /var/log/foreman-installer/capsule.log' + r'grep "\[ERROR" --context=100 /var/log/foreman-installer/capsule.log' ) assert len(result.stdout) == 0 + # no errors/failures in /var/log/httpd/* + result = cap_ready_rhel.execute(r'grep -iR "error" /var/log/httpd/*') + assert len(result.stdout) == 0 + # no errors/failures in /var/log/foreman-proxy/* + result = cap_ready_rhel.execute(r'grep -iR "error" /var/log/foreman-proxy/*') + assert len(result.stdout) == 0 + result = cap_ready_rhel.cli.Health.check() assert 'FAIL' not in result.stdout @@ -1687,71 +1725,6 @@ def test_installer_check_on_ipv6(): """ -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_positive_satellite_installer_logfile_check(): - """Verify the no ERROR or FATAL messages appears in the log file during the satellite - installation - - :id: c2f10f43-c52e-4f32-b3e9-7bc4b07e3b00 - - :steps: - 1. Configure all the repositories(custom and cdn) for satellite installation. - 2. Run yum update -y - 3. Run satellite-installer -y - 4. Check all the relevant log-files for ERROR/FATAL - - :expectedresults: No Unexpected ERROR/FATAL message should appear in the following log - files during the satellite-installation. - - 1. /var/log/messages, - 2. /var/log/foreman/production.log - 3. /var/log/foreman-installer/satellite.log - 4. /var/log/httpd, - 5. /var/log/candlepin - - :CaseLevel: System - - :CaseAutomation: NotAutomated - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_positive_capsule_installer_logfile_check(): - """Verify the no ERROR or FATAL messages appears in the log file during the capsule - installation - - :id: cd505a5e-141e-47eb-98d8-a05acd74c3b3 - - :steps: - 1. Install the satellite. - 2. Add all the required cdn and custom repositories in satellite to install - the capsule. - 3. Create life-cycle environment,content view and activation key. - 4. Subscribe the capsule with created activation key. - 5. Run 'yum update -y' on capsule. - 6. Run 'yum install -y satellite-capsule' on capsule. - 7. Create a certificate on satellite for new installed capsule. - 8. Copy capsule certificate from satellite to capsule. - 9. Run the satellite-installer(copy the satellite-installer command from step-7'th - generated output) command on capsule to integrate the capsule with satellite. - 10. Check all the relevant log-files for ERROR/FATAL - - :expectedresults: No Unexpected ERROR/FATAL message should appear in the following log - files during the capsule-installation. - - 1. /var/log/messages - 2. /var/log/foreman-installer/capsule.log - 3. /var/log/httpd - 4. /var/log/foreman-proxy - - :CaseLevel: System - - :CaseAutomation: NotAutomated - """ - - @pytest.mark.tier3 def test_installer_cap_pub_directory_accessibility(capsule_configured): """Verify the public directory accessibility from capsule url after disabling it from the @@ -1809,6 +1782,7 @@ def test_installer_cap_pub_directory_accessibility(capsule_configured): @pytest.mark.tier1 @pytest.mark.build_sanity @pytest.mark.first_sanity +@pytest.mark.pit_client def test_satellite_installation(installer_satellite): """Run a basic Satellite installation @@ -1824,7 +1798,8 @@ def test_satellite_installation(installer_satellite): :expectedresults: 1. Correct satellite packaged is installed 2. satellite-installer runs successfully - 3. satellite-maintain health check runs successfully + 3. no unexpected errors in logs + 4. satellite-maintain health check runs successfully :CaseImportance: Critical From d890a4db9e077be7c041cb965cf037342df91602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Gajdu=C5=A1ek?= Date: Tue, 21 Nov 2023 17:36:03 +0100 Subject: [PATCH 331/586] [6.14.z] Add abstraction for debian repo (#13147) Add abstraction for debian repo (cherry picked from commit 4f1652681d2ad26ee9fa0d554c07e04c1f12637e) Co-authored-by: dosas --- robottelo/constants/__init__.py | 1 + robottelo/host_helpers/repository_mixins.py | 47 +++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index de13702ecc8..f543d4dc050 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -219,6 +219,7 @@ class Colored(Box): SYNC_INTERVAL = {'hour': "hourly", 'day': "daily", 'week': "weekly", 'custom': "custom cron"} REPO_TYPE = { + "deb": "deb", 'yum': "yum", 'ostree': "ostree", 'docker': "docker", diff --git a/robottelo/host_helpers/repository_mixins.py b/robottelo/host_helpers/repository_mixins.py index 99b5576edb9..53b83c80b84 100644 --- a/robottelo/host_helpers/repository_mixins.py +++ b/robottelo/host_helpers/repository_mixins.py @@ -123,6 +123,53 @@ class YumRepository(BaseRepository): _type = constants.REPO_TYPE['yum'] +class DebianRepository(BaseRepository): + """Custom Debian repository.""" + + _type = constants.REPO_TYPE["deb"] + + def __init__( + self, url=None, distro=None, content_type=None, deb_errata_url=None, deb_releases=None + ): + super().__init__(url=url, distro=distro, content_type=content_type) + self._deb_errata_url = deb_errata_url + self._deb_releases = deb_releases + + @property + def deb_errata_url(self): + return self._deb_errata_url + + @property + def deb_releases(self): + return self._deb_releases + + def create( + self, + organization_id, + product_id, + download_policy=None, + synchronize=True, + ): + """Create the repository for the supplied product id""" + create_options = { + 'product-id': product_id, + 'content-type': self.content_type, + 'url': self.url, + 'deb-releases': self._deb_releases, + } + + if self._deb_errata_url is not None: + create_options['deb-errata-url'] = self._deb_errata_url + + repo_info = self.satellite.cli_factory.make_repository(create_options) + self._repo_info = repo_info + + if synchronize: + self.synchronize() + + return repo_info + + class DockerRepository(BaseRepository): """Custom Docker repository""" From 0f66f1571748ec9e9cf24c142ca6ccb5ae58f615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Gajdu=C5=A1ek?= Date: Tue, 21 Nov 2023 11:45:54 +0100 Subject: [PATCH 332/586] Yield details always, no need to check for SCA eligibility (cherry picked from commit e39ce219fd476397c9883452ddce4d64c9116488) --- tests/foreman/cli/test_host.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/foreman/cli/test_host.py b/tests/foreman/cli/test_host.py index 20310ab564d..f687b785231 100644 --- a/tests/foreman/cli/test_host.py +++ b/tests/foreman/cli/test_host.py @@ -1498,9 +1498,8 @@ def test_positive_provision_baremetal_with_uefi_secureboot(): def setup_custom_repo(target_sat, module_org, katello_host_tools_host, request): """Create custom repository content""" - if sca_eligible := module_org.sca_eligible().get('simple_content_access_eligible', False): - sca_enabled = module_org.simple_content_access - module_org.sca_disable() + sca_enabled = module_org.simple_content_access + module_org.sca_disable() # get package details details = {} @@ -1546,9 +1545,9 @@ def setup_custom_repo(target_sat, module_org, katello_host_tools_host, request): ) # refresh repository metadata katello_host_tools_host.subscription_manager_list_repos() - if sca_eligible: - yield - module_org.sca_enable() if sca_enabled else module_org.sca_disable() + if sca_enabled: + yield details + module_org.sca_enable() else: return details From dfe74889ca5b4dba757747876cdd01c0130e6ff5 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 21 Nov 2023 18:55:39 -0500 Subject: [PATCH 333/586] [6.14.z] Parametrize Satellite and Capsule deployments (#13154) --- pytest_fixtures/core/sat_cap_factory.py | 15 +++++++++------ robottelo/hosts.py | 5 +++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pytest_fixtures/core/sat_cap_factory.py b/pytest_fixtures/core/sat_cap_factory.py index 2fbb8cec7b6..8f83704702e 100644 --- a/pytest_fixtures/core/sat_cap_factory.py +++ b/pytest_fixtures/core/sat_cap_factory.py @@ -1,6 +1,7 @@ from contextlib import contextmanager from broker import Broker +from packaging.version import Version import pytest from wait_for import wait_for @@ -244,11 +245,12 @@ def parametrized_enrolled_sat( def get_deploy_args(request): + rhel_version = get_sat_rhel_version() deploy_args = { - 'deploy_rhel_version': get_sat_rhel_version().base_version, + 'deploy_rhel_version': rhel_version.base_version, 'deploy_flavor': settings.flavors.default, 'promtail_config_template_file': 'config_sat.j2', - 'workflow': 'deploy-rhel', + 'workflow': settings.content_host.get(f'rhel{rhel_version.major}').vm.workflow, } if hasattr(request, 'param'): if isinstance(request.param, dict): @@ -274,11 +276,12 @@ def module_sat_ready_rhels(request): @pytest.fixture def cap_ready_rhel(): - rhel8 = settings.content_host.rhel8.vm + rhel_version = Version(settings.capsule.version.release) + settings.content_host.get(f'rhel{rhel_version.major}').vm.workflow deploy_args = { - 'deploy_rhel_version': rhel8.deploy_rhel_version, - 'deploy_flavor': 'satqe-ssd.standard.std', - 'workflow': rhel8.workflow, + 'deploy_rhel_version': rhel_version.base_version, + 'deploy_flavor': settings.flavors.default, + 'workflow': settings.content_host.get(f'rhel{rhel_version.major}').vm.workflow, } with Broker(**deploy_args, host_class=Capsule) as host: yield host diff --git a/robottelo/hosts.py b/robottelo/hosts.py index fe13c06cab5..929d6ddf80e 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -69,11 +69,12 @@ @lru_cache def lru_sat_ready_rhel(rhel_ver): + rhel_version = rhel_ver or settings.server.version.rhel_version deploy_args = { - 'deploy_rhel_version': rhel_ver or settings.server.version.rhel_version, + 'deploy_rhel_version': rhel_version, 'deploy_flavor': settings.flavors.default, 'promtail_config_template_file': 'config_sat.j2', - 'workflow': 'deploy-rhel', + 'workflow': settings.content_host.get(f'rhel{Version(rhel_version).major}').vm.workflow, } sat_ready_rhel = Broker(**deploy_args, host_class=Satellite).checkout() return sat_ready_rhel From dd3780bb909a72dec8487996cc7c586811dab63d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 22 Nov 2023 02:29:24 -0500 Subject: [PATCH 334/586] [6.14.z] remove test_installer_inventory_plugin_update (#13156) remove test_installer_inventory_plugin_update (#13148) (cherry picked from commit c38d4f0161b764a04a1804c6623d3ed002ee231f) Co-authored-by: rmynar <64528205+rmynar@users.noreply.github.com> --- tests/foreman/destructive/test_installer.py | 41 --------------------- 1 file changed, 41 deletions(-) diff --git a/tests/foreman/destructive/test_installer.py b/tests/foreman/destructive/test_installer.py index aa683870591..b06df33d968 100644 --- a/tests/foreman/destructive/test_installer.py +++ b/tests/foreman/destructive/test_installer.py @@ -19,7 +19,6 @@ from fauxfactory import gen_domain, gen_string import pytest -from robottelo.config import settings from robottelo.constants import SATELLITE_ANSWER_FILE from robottelo.utils.installer import InstallerCommand @@ -89,46 +88,6 @@ def test_installer_sat_pub_directory_accessibility(target_sat): assert 'Success!' in command_output.stdout -def test_installer_inventory_plugin_update(target_sat): - """DB consistency should not break after enabling the inventory plugin flags - - :id: a2b66d38-e819-428f-9529-23bed398c916 - - :steps: - 1. Enable the cloud inventory plugin flag - - :expectedresults: inventory flag should be updated successfully without any db consistency - error. - - :CaseImportance: High - - :CaseLevel: System - - :BZ: 1863597 - - :customerscenario: true - - """ - target_sat.create_custom_repos(rhel7=settings.repos.rhel7_os) - installer_cmd = target_sat.install( - InstallerCommand( - 'enable-foreman-plugin-rh-cloud', - foreman_proxy_plugin_remote_execution_script_install_key=['true'], - ) - ) - assert 'Success!' in installer_cmd.stdout - verify_rhcloud_flag = target_sat.install( - InstallerCommand(help='|grep "\'foreman_plugin_rh_cloud\' puppet module (default: true)"') - ) - assert 'true' in verify_rhcloud_flag.stdout - verify_proxy_plugin_flag = target_sat.install( - InstallerCommand( - **{'full-help': '| grep -A1 foreman-proxy-plugin-remote-execution-script-install-key'} - ) - ) - assert '(current: true)' in verify_proxy_plugin_flag.stdout - - def test_positive_mismatched_satellite_fqdn(target_sat, set_random_fqdn): """The satellite-installer should display the mismatched FQDN From 62fae3c5f3c371a5be0cbc34816370c9ae62c227 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 22 Nov 2023 04:03:09 -0500 Subject: [PATCH 335/586] [6.14.z] Discovery Provisioning UI coverage (#13139) Discovery Provisioning UI coverage (#12991) (cherry picked from commit e803cc4bb77fa8a7ff877dfaaae133b56841918a) Co-authored-by: Adarsh dubey --- pytest_fixtures/component/discovery.py | 11 ++ tests/foreman/ui/test_discoveredhost.py | 221 +++++++++--------------- 2 files changed, 92 insertions(+), 140 deletions(-) diff --git a/pytest_fixtures/component/discovery.py b/pytest_fixtures/component/discovery.py index e05b3859f2f..c9d4375484a 100644 --- a/pytest_fixtures/component/discovery.py +++ b/pytest_fixtures/component/discovery.py @@ -29,3 +29,14 @@ def discovery_location(module_location, module_target_sat): discovery_loc = module_target_sat.update_setting('discovery_location', module_location.name) yield module_location module_target_sat.update_setting('discovery_location', discovery_loc) + + +@pytest.fixture(scope='module') +def provisioning_env(module_target_sat, discovery_org, discovery_location): + # Build PXE default template to get default PXE file + module_target_sat.cli.ProvisioningTemplate().build_pxe_default() + return module_target_sat.api_factory.configure_provisioning( + org=discovery_org, + loc=discovery_location, + os=f'Redhat {module_target_sat.cli_factory.RHELRepository().repo_data["version"]}', + ) diff --git a/tests/foreman/ui/test_discoveredhost.py b/tests/foreman/ui/test_discoveredhost.py index ce3ea4d506e..322571f5954 100644 --- a/tests/foreman/ui/test_discoveredhost.py +++ b/tests/foreman/ui/test_discoveredhost.py @@ -14,71 +14,20 @@ :Upstream: No """ -from fauxfactory import gen_ipaddr, gen_string -from nailgun import entities +from fauxfactory import gen_string import pytest +from wait_for import wait_for from robottelo.utils import ssh pytestmark = [pytest.mark.run_in_one_thread] -@pytest.fixture(scope='module') -def discovery_org(module_org): - # Update default discovered host organization - discovery_org = entities.Setting().search(query={'search': 'name="discovery_organization"'})[0] - default_discovery_org = discovery_org.value - discovery_org.value = module_org.name - discovery_org.update(['value']) - yield module_org - discovery_org.value = default_discovery_org - discovery_org.update(['value']) - - -@pytest.fixture(scope='module') -def discovery_location(module_location): - # Update default discovered host location - discovery_loc = entities.Setting().search(query={'search': 'name="discovery_location"'})[0] - default_discovery_loc = discovery_loc.value - discovery_loc.value = module_location.name - discovery_loc.update(['value']) - yield module_location - discovery_loc.value = default_discovery_loc - discovery_loc.update(['value']) - - -@pytest.fixture(scope='module') -def provisioning_env(module_target_sat, discovery_org, discovery_location): - # Build PXE default template to get default PXE file - module_target_sat.cli.ProvisioningTemplate().build_pxe_default() - return module_target_sat.api_factory.configure_provisioning( - org=discovery_org, - loc=discovery_location, - os='Redhat {}'.format(module_target_sat.cli_factory.RHELRepository().repo_data['version']), - ) - - @pytest.fixture def discovered_host(target_sat): return target_sat.api_factory.create_discovered_host() -@pytest.fixture(scope='module') -def module_host_group(discovery_org, discovery_location): - host = entities.Host(organization=discovery_org, location=discovery_location) - host.create_missing() - return entities.HostGroup( - organization=[discovery_org], - location=[discovery_location], - medium=host.medium, - root_pass=gen_string('alpha'), - operatingsystem=host.operatingsystem, - ptable=host.ptable, - domain=host.domain, - architecture=host.architecture, - ).create() - - def _is_host_reachable(host, retries=12, iteration_sleep=5, expect_reachable=True): """Helper to ensure given IP/hostname is reachable or not. The return value returned depend from expect reachable value. @@ -100,53 +49,23 @@ def _is_host_reachable(host, retries=12, iteration_sleep=5, expect_reachable=Tru return bool(result.status) -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_positive_pxe_based_discovery(session, provisioning_env): - """Discover a host via PXE boot by setting "proxy.type=proxy" in - PXE default - - :id: 43a8857d-2f08-436e-97fb-ffec6a0c84dd - - :Setup: Provisioning should be configured - - :Steps: PXE boot a host/VM - - :expectedresults: Host should be successfully discovered - - :BZ: 1731112 - - :CaseImportance: Critical - """ - - -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_positive_pxe_less_with_dhcp_unattended(session, provisioning_env): - """Discover a host with dhcp via bootable discovery ISO by setting - "proxy.type=proxy" in PXE default in unattended mode. - - :id: fc13167f-6fa0-4fe5-8584-7716292866ce - - :Setup: Provisioning should be configured - - :Steps: Boot a host/VM using modified discovery ISO. - - :expectedresults: Host should be successfully discovered - - :BZ: 1731112 - - :CaseImportance: Critical - """ - - @pytest.mark.tier3 @pytest.mark.upgrade -def test_positive_provision_using_quick_host_button( - session, discovery_org, discovery_location, discovered_host, module_host_group +@pytest.mark.on_premises_provisioning +@pytest.mark.parametrize('module_provisioning_sat', ['discovery'], indirect=True) +@pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) +@pytest.mark.rhel_ver_match('9') +def test_positive_provision_pxe_host( + session, + module_location, + module_org, + module_provisioning_rhel_content, + module_discovery_sat, + provisioning_host, + provisioning_hostgroup, + pxe_loader, ): - """Associate hostgroup while provisioning a discovered host from - host properties model window and select quick host. + """Provision a PXE-based discoveredhost :id: 34c1e9ea-f210-4a1e-aead-421eb962643b @@ -155,22 +74,36 @@ def test_positive_provision_using_quick_host_button( 1. Host should already be discovered 2. Hostgroup should already be created with all required entities. - :expectedresults: Host should be quickly provisioned and entry from + :expectedresults: Host should be provisioned and entry from discovered host should be auto removed. :BZ: 1728306, 1731112 :CaseImportance: High """ - discovered_host_name = discovered_host['name'] - domain_name = module_host_group.domain.read().name + sat = module_discovery_sat.sat + provisioning_host.power_control(ensure=False) + mac = provisioning_host._broker_args['provisioning_nic_mac_addr'] + wait_for( + lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], + timeout=240, + delay=20, + ) + discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] + discovered_host.hostgroup = provisioning_hostgroup + discovered_host.location = provisioning_hostgroup.location[0] + discovered_host.organization = provisioning_hostgroup.organization[0] + discovered_host.build = True + + discovered_host_name = discovered_host.name + domain_name = provisioning_hostgroup.domain.read().name host_name = f'{discovered_host_name}.{domain_name}' with session: session.discoveredhosts.provision( discovered_host_name, - module_host_group.name, - discovery_org.name, - discovery_location.name, + provisioning_hostgroup.name, + module_org.name, + module_location.name, ) values = session.host.get_details(host_name) assert values['properties']['properties_table']['Status'] == 'OK' @@ -179,7 +112,7 @@ def test_positive_provision_using_quick_host_button( @pytest.mark.tier3 def test_positive_update_name( - session, discovery_org, discovery_location, module_host_group, discovered_host + session, discovery_org, discovery_location, module_discovery_hostgroup, discovered_host ): """Update the discovered host name and provision it @@ -195,7 +128,7 @@ def test_positive_update_name( :CaseImportance: High """ discovered_host_name = discovered_host['name'] - domain_name = module_host_group.domain.read().name + domain_name = module_discovery_hostgroup.domain.read().name new_name = gen_string('alpha').lower() new_host_name = f'{new_name}.{domain_name}' with session: @@ -203,7 +136,7 @@ def test_positive_update_name( assert discovered_host_values['Name'] == discovered_host_name session.discoveredhosts.provision( discovered_host_name, - module_host_group.name, + module_discovery_hostgroup.name, discovery_org.name, discovery_location.name, quick=False, @@ -217,10 +150,21 @@ def test_positive_update_name( @pytest.mark.tier3 @pytest.mark.upgrade +@pytest.mark.on_premises_provisioning +@pytest.mark.parametrize('module_provisioning_sat', ['discovery'], indirect=True) +@pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) +@pytest.mark.rhel_ver_match('9') def test_positive_auto_provision_host_with_rule( - session, discovery_org, discovery_location, module_host_group, target_sat + session, + module_org, + module_location, + module_provisioning_rhel_content, + module_discovery_sat, + pxeless_discovery_host, + provisioning_hostgroup, + pxe_loader, ): - """Create a new discovery rule and automatically create host from discovered host using that + """Create a new discovery rule and automatically provision host from discovered host using that discovery rule. Set query as (e.g IP=IP_of_discovered_host) @@ -229,29 +173,44 @@ def test_positive_auto_provision_host_with_rule( :Setup: Host should already be discovered - :expectedresults: Host should reboot and provision + :expectedresults: Host should be successfully provisioned :BZ: 1665471, 1731112 :CaseImportance: High """ - host_ip = gen_ipaddr() - discovered_host_name = target_sat.api_factory.create_discovered_host(ip_address=host_ip)['name'] - domain = module_host_group.domain.read() - discovery_rule = entities.DiscoveryRule( - max_count=1, - hostgroup=module_host_group, - search_=f'ip = {host_ip}', - location=[discovery_location], - organization=[discovery_org], + sat = module_discovery_sat.sat + pxeless_discovery_host.power_control(ensure=False) + mac = pxeless_discovery_host._broker_args['provisioning_nic_mac_addr'] + wait_for( + lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], + timeout=240, + delay=20, + ) + discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] + discovered_host.hostgroup = provisioning_hostgroup + discovered_host.location = provisioning_hostgroup.location[0] + discovered_host.organization = provisioning_hostgroup.organization[0] + discovered_host.build = True + + discovered_host_name = discovered_host.name + domain_name = provisioning_hostgroup.domain.name + host_name = f'{discovered_host_name}.{domain_name}' + + discovery_rule = sat.api.DiscoveryRule( + max_count=10, + hostgroup=provisioning_hostgroup, + search_=f'name = {discovered_host_name}', + location=[module_location], + organization=[module_org], ).create() with session: + session.organization.select(org_name=module_org.name) + session.location.select(loc_name=module_location.name) session.discoveredhosts.apply_action('Auto Provision', [discovered_host_name]) - host_name = f'{discovered_host_name}.{domain.name}' assert session.host.search(host_name)[0]['Name'] == host_name host_values = session.host.get_details(host_name) assert host_values['properties']['properties_table']['Status'] == 'OK' - assert host_values['properties']['properties_table']['IP Address'] == host_ip assert ( host_values['properties']['properties_table']['Comment'] == f"Auto-discovered and provisioned via rule '{discovery_rule.name}'" @@ -296,10 +255,10 @@ def test_positive_update_default_taxonomies(session, discovery_org, discovery_lo :CaseImportance: High """ host_names = [target_sat.api_factory.create_discovered_host()['name'] for _ in range(2)] - new_org = entities.Organization().create() + new_org = target_sat.api.Organization().create() discovery_location.organization.append(new_org) discovery_location.update(['organization']) - new_loc = entities.Location(organization=[discovery_org]).create() + new_loc = target_sat.api.Location(organization=[discovery_org]).create() with session: values = session.discoveredhosts.search('name = "{}" or name = "{}"'.format(*host_names)) assert set(host_names) == {value['Name'] for value in values} @@ -319,21 +278,3 @@ def test_positive_update_default_taxonomies(session, discovery_org, discovery_lo assert set(host_names) == {value['Name'] for value in values} values = session.dashboard.read('DiscoveredHosts') assert len(values['hosts']) == 2 - - -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_positive_reboot(session, provisioning_env): - """Reboot a discovered host. - - :id: 5edc6831-bfc8-4e69-9029-b4c0caa3ee32 - - :Setup: Host should already be discovered - - :expectedresults: Discovered host without provision is going to shutdown after reboot command - is passed. - - :BZ: 1731112 - - :CaseImportance: Medium - """ From 37e2147d4af4d61bc8adb198da5a851129a37d02 Mon Sep 17 00:00:00 2001 From: Gaurav Talreja Date: Wed, 22 Nov 2023 17:09:58 +0530 Subject: [PATCH 336/586] [6.14.z] Component Audit Part-2 (#13158) Component Audit Part-2 (#12519) (cherry picked from commit 2e46f7e24ce7e97499a8e8064a556ee940ca9e3d) Co-authored-by: Shweta Singh --- pytest_fixtures/component/activationkey.py | 11 ++++- tests/foreman/api/test_registration.py | 39 +++++++++++++-- tests/foreman/ui/test_host.py | 57 +++++++++++++++++++++- 3 files changed, 101 insertions(+), 6 deletions(-) diff --git a/pytest_fixtures/component/activationkey.py b/pytest_fixtures/component/activationkey.py index 05c479c65b7..9981d60f13d 100644 --- a/pytest_fixtures/component/activationkey.py +++ b/pytest_fixtures/component/activationkey.py @@ -6,8 +6,15 @@ @pytest.fixture(scope='module') -def module_activation_key(module_org, module_target_sat): - return module_target_sat.api.ActivationKey(organization=module_org).create() +def module_activation_key(module_entitlement_manifest_org, module_target_sat): + """Create activation key using default CV and library environment.""" + activation_key = module_target_sat.api.ActivationKey( + auto_attach=True, + content_view=module_entitlement_manifest_org.default_content_view.id, + environment=module_entitlement_manifest_org.library.id, + organization=module_entitlement_manifest_org, + ).create() + return activation_key @pytest.fixture(scope='module') diff --git a/tests/foreman/api/test_registration.py b/tests/foreman/api/test_registration.py index d9267bb5d2f..eb74c4afe24 100644 --- a/tests/foreman/api/test_registration.py +++ b/tests/foreman/api/test_registration.py @@ -20,7 +20,8 @@ import pytest -from robottelo.constants import CLIENT_PORT +from robottelo import constants +from robottelo.config import settings pytestmark = pytest.mark.tier1 @@ -60,7 +61,7 @@ def test_host_registration_end_to_end( # Verify server.hostname and server.port from subscription-manager config assert module_target_sat.hostname == rhel_contenthost.subscription_config['server']['hostname'] - assert CLIENT_PORT == rhel_contenthost.subscription_config['server']['port'] + assert constants.CLIENT_PORT == rhel_contenthost.subscription_config['server']['port'] # Update module_capsule_configured to include module_org/module_location nc = module_capsule_configured.nailgun_smart_proxy @@ -84,7 +85,7 @@ def test_host_registration_end_to_end( module_capsule_configured.hostname == rhel_contenthost.subscription_config['server']['hostname'] ) - assert CLIENT_PORT == rhel_contenthost.subscription_config['server']['port'] + assert constants.CLIENT_PORT == rhel_contenthost.subscription_config['server']['port'] @pytest.mark.tier3 @@ -125,3 +126,35 @@ def test_positive_allow_reregistration_when_dmi_uuid_changed( ).create() result = rhel_contenthost.execute(command) assert result.status == 0 + + +def test_positive_update_packages_registration( + module_target_sat, + module_entitlement_manifest_org, + module_location, + rhel8_contenthost, + module_activation_key, +): + """Test package update on host post registration + + :id: 3d0a3252-ab81-4acf-bca6-253b746f26bb + + :expectedresults: Package update is successful on host post registration. + + :CaseLevel: Component + """ + org = module_entitlement_manifest_org + command = module_target_sat.api.RegistrationCommand( + organization=org, + location=module_location, + activation_keys=[module_activation_key.name], + update_packages=True, + ).create() + result = rhel8_contenthost.execute(command) + assert result.status == 0, f'Failed to register host: {result.stderr}' + + package = constants.FAKE_7_CUSTOM_PACKAGE + repo_url = settings.repos.yum_3['url'] + rhel8_contenthost.create_custom_repos(fake_yum=repo_url) + result = rhel8_contenthost.execute(f"yum install -y {package}") + assert result.status == 0 diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index 54ba1197ed7..193eebff940 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -510,7 +510,7 @@ def test_positive_view_hosts_with_non_admin_user( :expectedresults: user with only view_hosts, edit_hosts and view_organization permissions is able to read content hosts and hosts - :CaseLevel: System + :CaseLevel: Component """ user_password = gen_string('alpha') role = target_sat.api.Role(organization=[module_org]).create() @@ -2396,3 +2396,58 @@ def test_positive_tracer_enable_reload(tracer_install_host, target_sat): ) tracer = session.host_new.get_tracer(tracer_install_host.hostname) assert tracer['title'] == "No applications to restart" + + +def test_positive_host_registration_with_non_admin_user( + test_name, + module_entitlement_manifest_org, + module_location, + target_sat, + rhel8_contenthost, + module_activation_key, +): + """Register hosts from a non-admin user with only register_hosts, edit_hosts + and view_organization permissions + + :id: 35458bbc-4556-41b9-ba26-ae0b15179731 + + :expectedresults: User with register hosts permission able to register hosts. + + :CaseLevel: Component + """ + user_password = gen_string('alpha') + org = module_entitlement_manifest_org + role = target_sat.api.Role(organization=[org]).create() + + user_permissions = { + 'Organization': ['view_organizations'], + 'Host': ['view_hosts'], + } + target_sat.api_factory.create_role_permissions(role, user_permissions) + user = target_sat.api.User( + role=[role], + admin=False, + password=user_password, + organization=[org], + location=[module_location], + default_organization=org, + default_location=module_location, + ).create() + role = target_sat.cli.Role.info({'name': 'Register hosts'}) + target_sat.cli.User.add_role({'id': user.id, 'role-id': role['id']}) + + with Session(test_name, user=user.login, password=user_password) as session: + + cmd = session.host_new.get_register_command( + { + 'general.insecure': True, + 'general.activation_keys': module_activation_key.name, + } + ) + + result = rhel8_contenthost.execute(cmd) + assert result.status == 0, f'Failed to register host: {result.stderr}' + + # Verify server.hostname and server.port from subscription-manager config + assert target_sat.hostname == rhel8_contenthost.subscription_config['server']['hostname'] + assert constants.CLIENT_PORT == rhel8_contenthost.subscription_config['server']['port'] From 8fd3c1a0bd1c0b9cbd099a2e8219158686f24d4c Mon Sep 17 00:00:00 2001 From: Jacob Callahan Date: Tue, 14 Nov 2023 09:49:21 -0500 Subject: [PATCH 337/586] Add basic fixture cli This change adds a new fixture_cli.py script under the scripts directory. The basic usage of this script is to take in a space-separated list of global fixtures and run them together in a single temporary test. e.g. python scripts/fixture_cli.py module_ak_with_synced_repo module_lce Additionally, I had to make some minor adjustments to a couple of plugins since this temporary test doesn't follow the same rules as the rest of our framework. (cherry picked from commit 150e7a2262f6fee144226d91904f633acb78772b) --- pytest_plugins/fspath_plugins.py | 4 +-- pytest_plugins/issue_handlers.py | 4 ++- scripts/fixture_cli.py | 46 ++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 scripts/fixture_cli.py diff --git a/pytest_plugins/fspath_plugins.py b/pytest_plugins/fspath_plugins.py index 316d066a6b0..c6241cac9cc 100644 --- a/pytest_plugins/fspath_plugins.py +++ b/pytest_plugins/fspath_plugins.py @@ -12,5 +12,5 @@ def pytest_collection_modifyitems(session, items, config): if item.nodeid.startswith('tests/robottelo/') or item.nodeid.startswith('tests/upgrades/'): continue - endpoint = endpoint_regex.findall(item.location[0])[0] - item.user_properties.append(('endpoint', endpoint)) + if endpoints := endpoint_regex.findall(item.location[0]): + item.user_properties.append(('endpoint', endpoints[0])) diff --git a/pytest_plugins/issue_handlers.py b/pytest_plugins/issue_handlers.py index 3da2044faff..614c75a45be 100644 --- a/pytest_plugins/issue_handlers.py +++ b/pytest_plugins/issue_handlers.py @@ -214,7 +214,9 @@ def generate_issue_collection(items, config): # pragma: no cover filepath, lineno, testcase = item.location # Component and importance marks are determined by testimony tokens # Testimony.yaml as of writing has both as required, so any - component_mark = item.get_closest_marker('component').args[0] + if not (components := item.get_closest_marker('component')): + continue + component_mark = components.args[0] component_slug = slugify_component(component_mark, False) importance_mark = item.get_closest_marker('importance').args[0] for marker in item.iter_markers(): diff --git a/scripts/fixture_cli.py b/scripts/fixture_cli.py new file mode 100644 index 00000000000..f8eb7cf3424 --- /dev/null +++ b/scripts/fixture_cli.py @@ -0,0 +1,46 @@ +from pathlib import Path + +import click +import pytest + + +@click.command() +@click.argument("fixtures", nargs=-1, required=True) +@click.option( + "--from-file", + "-f", + type=click.File("w"), + help="Run the fixtures from within a file, inheriting the file's context.", +) +def run_fixtures(fixtures, from_file): + """Create a temporary test that depends on each fixture, then run it. + + You can also run the fixtures from the context of a file, which is useful when testing fixtures + that don't live at a global scope. + + Examples: + python scripts/fixture_cli.py module_published_cv module_subscribe_satellite + python scripts/fixture_cli.py module_lce --from-file tests/foreman/api/test_activationkey.py + """ + fixture_string = ", ".join(filter(None, fixtures)) + test_template = f"def test_fake({fixture_string}):\n assert True" + if from_file: + from_file = Path(from_file.name) + # inject the test at the end of the file + with from_file.open("a") as f: + eof_pos = f.tell() + f.write(f"\n\n{test_template}") + pytest.main(["-qq", str(from_file.resolve()), "-k", "test_fake"]) + # remove the test from the file + with from_file.open("r+") as f: + f.seek(eof_pos) + f.truncate() + else: + temp_file = Path("test_DELETEME.py") + temp_file.write_text(test_template) + pytest.main(["-qq", str(temp_file)]) + temp_file.unlink() + + +if __name__ == "__main__": + run_fixtures() From 9990219def636d41eaa61183751358dcd71b6bce Mon Sep 17 00:00:00 2001 From: vsedmik <46570670+vsedmik@users.noreply.github.com> Date: Thu, 23 Nov 2023 11:28:58 +0100 Subject: [PATCH 338/586] [6.14.z] Check upgrade warning when KA enabled (#13137) * Add test to check upgrade warning when KA enabled * Address PR comments --- robottelo/constants/__init__.py | 2 + .../foreman/destructive/test_katello_agent.py | 47 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index f543d4dc050..e1aebb9a66e 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -1746,6 +1746,8 @@ class Colored(Box): SATELLITE_MAINTAIN_YML = "/etc/foreman-maintain/foreman_maintain.yml" FOREMAN_SETTINGS_YML = '/etc/foreman/settings.yaml' +FOREMAN_NIGHTLY_URL = 'https://yum.theforeman.org/releases/nightly/el8/x86_64/' + FOREMAN_TEMPLATE_IMPORT_URL = 'https://github.com/SatelliteQE/foreman_templates.git' FOREMAN_TEMPLATE_IMPORT_API_URL = 'http://api.github.com/repos/SatelliteQE/foreman_templates' diff --git a/tests/foreman/destructive/test_katello_agent.py b/tests/foreman/destructive/test_katello_agent.py index 97b92c495c8..e64a0e3ec34 100644 --- a/tests/foreman/destructive/test_katello_agent.py +++ b/tests/foreman/destructive/test_katello_agent.py @@ -147,3 +147,50 @@ def test_positive_install_and_remove_package_group(katello_agent_client): sat.cli.Host.package_group_remove(hammer_args) for package in constants.FAKE_0_CUSTOM_PACKAGE_GROUP: assert client.run(f'rpm -q {package}').status != 0 + + +def test_positive_upgrade_warning(sat_with_katello_agent): + """Ensure a warning is dispayed when upgrading with katello-agent enabled. + + :id: 2bfc5e3e-e147-4e7a-a25c-85be22ef6921 + + :expectedresults: Upgrade check fails and warning with proper message is displayed. + + :CaseLevel: System + """ + sat = sat_with_katello_agent + ver = sat.version.split('.') + target_ver = f'{ver[0]}.{int(ver[1]) + 1}' + warning = ( + 'The katello-agent feature is enabled on this system. As of Satellite 6.15, katello-agent ' + 'is removed and will no longer function. Before proceeding with the upgrade, you should ' + 'ensure that you have deployed and configured an alternative tool for remote package ' + 'management and patching for content hosts, such as Remote Execution (REX) with pull-based ' + 'transport. See the Managing Hosts guide in the Satellite documentation for more info. ' + 'Disable katello-agent with the command `satellite-installer --foreman-proxy-content-enable' + '-katello-agent false` before proceeding with the upgrade. Alternatively, you may skip ' + 'this check and proceed by running satellite-maintain again with the `--whitelist` option, ' + 'which will automatically uninstall katello-agent.' + ) + + upstream_rpms = sat.get_repo_files_by_url(constants.FOREMAN_NIGHTLY_URL) + fm_rpm = [rpm for rpm in upstream_rpms if 'foreman_maintain' in rpm] + assert fm_rpm, 'No upstream foreman-maintain package found' + + for rpm in fm_rpm: + res = sat.execute(f'yum -y install {constants.FOREMAN_NIGHTLY_URL}{rpm}') + assert res.status == 0, f'{rpm} installation failed' + + res = sat.cli.Upgrade.list_versions() + assert res.status == 0, 'Upgrade list-versions command failed' + assert target_ver in res.stdout, 'Target version or Scenario not found' + + res = sat.cli.Upgrade.check( + options={ + 'target-version': target_ver, + 'whitelist': 'repositories-setup,repositories-validate', + 'assumeyes': True, + } + ) + assert res.status, 'Upgrade check passed unexpectedly' + assert warning in res.stdout, 'Katello-agent warning message missing or changed' From 7e73dc8b2c8eac29de8de41a207d40f697353464 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 24 Nov 2023 03:43:26 -0500 Subject: [PATCH 339/586] [6.14.z] Bump pytest-xdist from 3.4.0 to 3.5.0 (#13181) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9beba263473..cc48fb5a161 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ pytest==7.4.3 pytest-services==2.2.1 pytest-mock==3.12.0 pytest-reportportal==5.3.0 -pytest-xdist==3.4.0 +pytest-xdist==3.5.0 pytest-ibutsu==2.2.4 PyYAML==6.0.1 requests==2.31.0 From ed72662106b7ea2fae5665162614579913d44a4c Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 24 Nov 2023 04:35:16 -0500 Subject: [PATCH 340/586] [6.14.z] recording ui-session-id for report portal logging (#13182) recording ui-session-id for report portal logging (#13055) * recording ui-session-id for report portal logging * moving the fixture to common location and adding fspath check (cherry picked from commit 3e4abae7a5f6b8db045f2f36bc967f9b793cfcfb) Co-authored-by: Omkar Khatavkar --- pytest_fixtures/core/ui.py | 3 ++- robottelo/hosts.py | 28 +++++++++++++++++++-------- tests/foreman/conftest.py | 25 ++++++++++++++++++++++++ tests/foreman/destructive/conftest.py | 3 ++- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/pytest_fixtures/core/ui.py b/pytest_fixtures/core/ui.py index 7edbd39b731..f87f4cec0ad 100644 --- a/pytest_fixtures/core/ui.py +++ b/pytest_fixtures/core/ui.py @@ -49,7 +49,8 @@ def test_foo(session): session.architecture.create({'name': 'bar'}) """ - return target_sat.ui_session(test_name, ui_user.login, ui_user.password) + with target_sat.ui_session(test_name, ui_user.login, ui_user.password) as session: + yield session @pytest.fixture diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 929d6ddf80e..679a4a8ba39 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1740,6 +1740,7 @@ def __init__(self, hostname=None, **kwargs): # create dummy classes for later population self._api = type('api', (), {'_configured': False}) self._cli = type('cli', (), {'_configured': False}) + self.record_property = None def _swap_nailgun(self, new_version): """Install a different version of nailgun from GitHub and invalidate the module cache.""" @@ -1828,6 +1829,7 @@ def omit_credentials(self): yield self.omitting_credentials = False + @contextmanager def ui_session(self, testname=None, user=None, password=None, url=None, login=True): """Initialize an airgun Session object and store it as self.ui_session""" @@ -1840,14 +1842,24 @@ def get_caller(): if frame.function.startswith('test_'): return frame.function - return Session( - session_name=testname or get_caller(), - user=user or settings.server.admin_username, - password=password or settings.server.admin_password, - url=url, - hostname=self.hostname, - login=login, - ) + try: + ui_session = Session( + session_name=testname or get_caller(), + user=user or settings.server.admin_username, + password=password or settings.server.admin_password, + url=url, + hostname=self.hostname, + login=login, + ) + yield ui_session + except Exception: + raise + finally: + video_url = settings.ui.grid_url.replace( + ':4444', f'/videos/{ui_session.ui_session_id}.mp4' + ) + self.record_property('video_url', video_url) + self.record_property('session_id', ui_session.ui_session_id) @property def satellite(self): diff --git a/tests/foreman/conftest.py b/tests/foreman/conftest.py index 4e3aca8a77c..1e11a276863 100644 --- a/tests/foreman/conftest.py +++ b/tests/foreman/conftest.py @@ -1,6 +1,8 @@ """Configurations for py.test runner""" + import pytest +from robottelo.hosts import Satellite from robottelo.logging import collection_logger @@ -40,3 +42,26 @@ def pytest_collection_modifyitems(session, items, config): config.hook.pytest_deselected(items=deselected_items) items[:] = [item for item in items if item not in deselected_items] + + +@pytest.fixture(autouse=True) +def ui_session_record_property(request, record_property): + """ + Autouse fixture to set the record_property attribute for Satellite instances in the test. + + This fixture iterates over all fixtures in the current test node + (excluding the current fixture) and sets the record_property attribute + for instances of the Satellite class. + + Args: + request: The pytest request object. + record_property: The value to set for the record_property attribute. + """ + test_directories = ['tests/foreman/destructive', 'tests/foreman/ui'] + test_file_path = request.node.fspath.strpath + if any(directory in test_file_path for directory in test_directories): + for fixture in request.node.fixturenames: + if request.fixturename != fixture: + if isinstance(request.getfixturevalue(fixture), Satellite): + sat = request.getfixturevalue(fixture) + sat.record_property = record_property diff --git a/tests/foreman/destructive/conftest.py b/tests/foreman/destructive/conftest.py index f83f6a6313e..190bc2cabe0 100644 --- a/tests/foreman/destructive/conftest.py +++ b/tests/foreman/destructive/conftest.py @@ -22,4 +22,5 @@ def test_foo(session): session.architecture.create({'name': 'bar'}) """ - return module_target_sat.ui_session(test_name, ui_user.login, ui_user.password) + with module_target_sat.ui_session(test_name, ui_user.login, ui_user.password) as session: + yield session From c51cedb6e6ac645c9d234b571d98a6638bcb8879 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 27 Nov 2023 23:05:20 -0500 Subject: [PATCH 341/586] [6.14.z] Bump cryptography from 41.0.5 to 41.0.7 (#13191) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cc48fb5a161..7a9f94b9106 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ betelgeuse==1.10.0 broker[docker]==0.4.1 -cryptography==41.0.5 +cryptography==41.0.7 deepdiff==6.7.1 dynaconf[vault]==3.2.4 fauxfactory==3.1.0 From 6890a0565496c33c96cf13cd859e10b4dac05d2b Mon Sep 17 00:00:00 2001 From: Peter Ondrejka Date: Tue, 28 Nov 2023 08:08:22 +0100 Subject: [PATCH 342/586] fix for webhook event trigger test (#13170) (cherry picked from commit bb2350402914d1b04b693e462e76bf1de661ca3d) --- tests/foreman/api/test_webhook.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/foreman/api/test_webhook.py b/tests/foreman/api/test_webhook.py index 019ac42484c..77d8784ca5e 100644 --- a/tests/foreman/api/test_webhook.py +++ b/tests/foreman/api/test_webhook.py @@ -162,7 +162,9 @@ def test_positive_event_triggered(self, module_org, target_sat, setting_update): :CaseImportance: Critical """ hook = target_sat.api.Webhooks( - event='actions.katello.repository.sync_succeeded', http_method='GET' + event='actions.katello.repository.sync_succeeded', + http_method='GET', + target_url=settings.repos.yum_0.url, ).create() repo = target_sat.api.Repository( organization=module_org, content_type='yum', url=settings.repos.yum_0.url From 0f657fb7bef0e83a855a87213d1fc04fd8b7324e Mon Sep 17 00:00:00 2001 From: Cole Higgins Date: Wed, 23 Aug 2023 14:30:16 -0400 Subject: [PATCH 343/586] Adding test for invalid repository publish --- tests/foreman/cli/test_repositories.py | 43 ++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/foreman/cli/test_repositories.py b/tests/foreman/cli/test_repositories.py index f35a93603ab..2e41c832cb6 100644 --- a/tests/foreman/cli/test_repositories.py +++ b/tests/foreman/cli/test_repositories.py @@ -17,6 +17,9 @@ :Upstream: No """ import pytest +import time +from requests.exceptions import HTTPError + @pytest.mark.rhel_ver_match('[^6]') @@ -47,3 +50,43 @@ def test_positive_custom_products_disabled_by_default( assert rhel_contenthost.subscribed product_details = rhel_contenthost.run('subscription-manager repos --list') assert 'Enabled: 0' in product_details.stdout + + +def test_negative_invalid_repo_fails_publish( + module_repository, + module_org, + target_sat, +): + """Verify that an invalid repository fails when trying to publish in a content view + + :id: 64e03f28-8213-467a-a229-44c8cbfaaef1 + + :steps: + 1. Create custom product and upload repository + 2. Run Katello commands to make repository invalid + 3. Create content view and add repository + 4. Verify Publish fails + + :expectedresults: Publishing a content view with an invalid repository fails + + :customerscenario: true + + :BZ: 2032040 + """ + repo = module_repository + with target_sat.session.shell() as sh: + sh.send('foreman-rake console') + time.sleep(30) # sleep to allow time for console to open + sh.send(f'root = ::Katello::RootRepository.last') + time.sleep(3) # give enough time for the command to complete + sh.send(f'::Katello::Resources::Candlepin::Product.remove_content(root.product.organization.label, root.product.cp_id, root.content_id)') + time.sleep(3) + sh.send(f'::Katello::Resources::Candlepin::Content.destroy(root.product.organization.label, root.content_id)') + time.sleep(3) + cv = target_sat.api.ContentView( + organization=module_org.name, + repository=[repo.id], + ).create() + with pytest.raises(HTTPError) as context: + cv.publish() + assert 'Remove the invalid repository before publishing again' in context.value.response.text \ No newline at end of file From d7235873d4fd88a74f0a7e2a1eb370b15ed1514b Mon Sep 17 00:00:00 2001 From: Cole Higgins Date: Tue, 3 Oct 2023 11:33:11 -0400 Subject: [PATCH 344/586] fixed flake 8 issues with too many chars --- tests/foreman/cli/test_repositories.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/tests/foreman/cli/test_repositories.py b/tests/foreman/cli/test_repositories.py index 2e41c832cb6..2a98e9dbd58 100644 --- a/tests/foreman/cli/test_repositories.py +++ b/tests/foreman/cli/test_repositories.py @@ -17,11 +17,9 @@ :Upstream: No """ import pytest -import time from requests.exceptions import HTTPError - @pytest.mark.rhel_ver_match('[^6]') def test_positive_custom_products_disabled_by_default( setup_content, @@ -64,8 +62,8 @@ def test_negative_invalid_repo_fails_publish( :steps: 1. Create custom product and upload repository 2. Run Katello commands to make repository invalid - 3. Create content view and add repository - 4. Verify Publish fails + 3. Create content view and add repository + 4. Verify Publish fails :expectedresults: Publishing a content view with an invalid repository fails @@ -74,19 +72,16 @@ def test_negative_invalid_repo_fails_publish( :BZ: 2032040 """ repo = module_repository - with target_sat.session.shell() as sh: - sh.send('foreman-rake console') - time.sleep(30) # sleep to allow time for console to open - sh.send(f'root = ::Katello::RootRepository.last') - time.sleep(3) # give enough time for the command to complete - sh.send(f'::Katello::Resources::Candlepin::Product.remove_content(root.product.organization.label, root.product.cp_id, root.content_id)') - time.sleep(3) - sh.send(f'::Katello::Resources::Candlepin::Content.destroy(root.product.organization.label, root.content_id)') - time.sleep(3) + target_sat.execute( + 'echo "root = ::Katello::RootRepository.last; ::Katello::Resources::Candlepin::Product.' + 'remove_content(root.product.organization.label, root.product.cp_id, root.content_id); ' + '::Katello::Resources::Candlepin::Content.destroy(root.product.organization.label, ' + 'root.content_id)" | foreman-rake console' + ) cv = target_sat.api.ContentView( organization=module_org.name, repository=[repo.id], ).create() with pytest.raises(HTTPError) as context: cv.publish() - assert 'Remove the invalid repository before publishing again' in context.value.response.text \ No newline at end of file + assert 'Remove the invalid repository before publishing again' in context.value.response.text From 96073186529cd3c3817f068ccf333a14e830a1e0 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:35:14 -0500 Subject: [PATCH 345/586] [6.14.z] added the missing directory path for ui_session (#13216) added the missing direcotry path for ui_session (#13212) (cherry picked from commit 2fa446e66da71884a8e7cfc2dfa21e7e567a3666) Co-authored-by: Omkar Khatavkar --- tests/foreman/conftest.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/foreman/conftest.py b/tests/foreman/conftest.py index 1e11a276863..59616f233b7 100644 --- a/tests/foreman/conftest.py +++ b/tests/foreman/conftest.py @@ -57,7 +57,12 @@ def ui_session_record_property(request, record_property): request: The pytest request object. record_property: The value to set for the record_property attribute. """ - test_directories = ['tests/foreman/destructive', 'tests/foreman/ui'] + test_directories = [ + 'tests/foreman/destructive', + 'tests/foreman/ui', + 'tests/foreman/sanity', + 'tests/foreman/virtwho', + ] test_file_path = request.node.fspath.strpath if any(directory in test_file_path for directory in test_directories): for fixture in request.node.fixturenames: From f15958620668ee90aa25b77fe6e9378595ddd8fa Mon Sep 17 00:00:00 2001 From: Cole Higgins Date: Wed, 29 Nov 2023 12:45:50 -0500 Subject: [PATCH 346/586] [6.14.z] Cherrypick test sync multiple large repos 6.14.z (#12946) * test for syncing multiple large repos * Adressing comments * addressing comments * precommit fix --- tests/foreman/api/test_repositories.py | 50 +++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/tests/foreman/api/test_repositories.py b/tests/foreman/api/test_repositories.py index 49b0ff61fea..18051c0c344 100644 --- a/tests/foreman/api/test_repositories.py +++ b/tests/foreman/api/test_repositories.py @@ -24,7 +24,7 @@ from robottelo import constants from robottelo.cli.base import CLIReturnCodeError from robottelo.config import settings -from robottelo.constants import MIRRORING_POLICIES +from robottelo.constants import DEFAULT_ARCHITECTURE, MIRRORING_POLICIES, REPOS from robottelo.utils.datafactory import parametrized @@ -242,3 +242,51 @@ def test_positive_multiple_orgs_with_same_repo(target_sat): repo_counts = target_sat.api.Repository(id=repo.id).read().content_counts repos.append(repo_counts) assert repos[0] == repos[1] == repos[2] + + +def test_positive_sync_mulitple_large_repos(module_target_sat, module_entitlement_manifest_org): + """Enable and bulk sync multiple large repositories + + :id: b51c4a3d-d532-4342-be61-e868f7c3a723 + + :Steps: + 1. Enabled multiple large Repositories + Red Hat Enterprise Linux 8 for x86_64 - AppStream RPMs 8 + Red Hat Enterprise Linux 8 for x86_64 - BaseOS RPMs 8 + Red Hat Enterprise Linux 8 for x86_64 - AppStream Kickstart 8 + Red Hat Enterprise Linux 8 for x86_64 - BaseOS Kickstart 8 + 2. Sync all four repositories at the same time + 3. Assert that the bulk sync succeeds + + :expectedresults: All repositories should sync with no errors + + :BZ: 2224031 + """ + repo_names = ['rhel8_bos', 'rhel8_aps'] + kickstart_names = ['rhel8_bos', 'rhel8_aps'] + for name in repo_names: + module_target_sat.api_factory.enable_rhrepo_and_fetchid( + basearch=DEFAULT_ARCHITECTURE, + org_id=module_entitlement_manifest_org.id, + product=REPOS[name]['product'], + repo=REPOS[name]['name'], + reposet=REPOS[name]['reposet'], + releasever=REPOS[name]['releasever'], + ) + + for name in kickstart_names: + rh_repo_id = module_target_sat.api_factory.enable_rhrepo_and_fetchid( + basearch=constants.DEFAULT_ARCHITECTURE, + org_id=module_entitlement_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'], + ) + rh_repos = module_target_sat.api.Repository(id=rh_repo_id).read() + rh_product = module_target_sat.api.Product(id=rh_repos.product.id).read() + assert len(rh_product.repository) == 4 + res = module_target_sat.api.ProductBulkAction().sync( + data={'ids': [rh_product.id]}, timeout=2000 + ) + assert res['result'] == 'success' From 49ef83e309d6e951621a4e0ddf3cb5f69dbe7a6c Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:50:55 -0500 Subject: [PATCH 347/586] [6.14.z] Pytest FixtureCollection external plugin (#13207) Pytest FixtureCollection external plugin (#13176) (cherry picked from commit a9ffccdae690732f10ea24bd6f9b9ba711b78b47) Co-authored-by: Jitendra Yejare --- conftest.py | 1 - pytest_plugins/fixture_collection.py | 39 ---------------------------- requirements.txt | 1 + 3 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 pytest_plugins/fixture_collection.py diff --git a/conftest.py b/conftest.py index efc896f486a..cf350ce8b12 100644 --- a/conftest.py +++ b/conftest.py @@ -17,7 +17,6 @@ 'pytest_plugins.settings_skip', 'pytest_plugins.rerun_rp.rerun_rp', 'pytest_plugins.fspath_plugins', - 'pytest_plugins.fixture_collection', 'pytest_plugins.factory_collection', 'pytest_plugins.requirements.update_requirements', 'pytest_plugins.sanity_plugin', diff --git a/pytest_plugins/fixture_collection.py b/pytest_plugins/fixture_collection.py deleted file mode 100644 index 6f61f2b3360..00000000000 --- a/pytest_plugins/fixture_collection.py +++ /dev/null @@ -1,39 +0,0 @@ -# Pytest Plugin to modify collection of test cases based on fixtures used by tests. -from robottelo.logging import collection_logger as logger - - -def pytest_addoption(parser): - """Add options for pytest to collect tests based on fixtures its using""" - help_text = ''' - Collects tests based on fixtures used by tests - - Usage: --uses-fixtures [options] - - Options: [ specific_fixture_name | list_of fixture names ] - - example: pytest tests/foreman --uses-fixtures target_sat module_target_sat - ''' - parser.addoption("--uses-fixtures", nargs='?', help=help_text) - - -def pytest_collection_modifyitems(items, config): - - if not config.getoption('uses_fixtures', False): - return - - filter_fixtures = config.getvalue('uses_fixtures') - fixtures_list = filter_fixtures.split(',') if ',' in filter_fixtures else [filter_fixtures] - selected = [] - deselected = [] - - for item in items: - if set(item.fixturenames).intersection(set(fixtures_list)): - selected.append(item) - else: - deselected.append(item) - logger.debug( - f'Selected {len(selected)} and deselected {len(deselected)} ' - f'tests based on given fixtures {fixtures_list} used by tests' - ) - config.hook.pytest_deselected(items=deselected) - items[:] = selected diff --git a/requirements.txt b/requirements.txt index 7a9f94b9106..9d02fce25a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ pytest-services==2.2.1 pytest-mock==3.12.0 pytest-reportportal==5.3.0 pytest-xdist==3.5.0 +pytest-fixturecollection==0.1.1 pytest-ibutsu==2.2.4 PyYAML==6.0.1 requests==2.31.0 From f6ad8593e85be517995b0eb3698edb9c3583f464 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 30 Nov 2023 10:03:07 -0500 Subject: [PATCH 348/586] [6.14.z] fixture support for virt-who config cli : data_form deploy_type virtwho_config delete_host& manifest content move (#13228) fixture support for virt-who config cli : data_form deploy_type virtwho_config delete_host& manifest content move (#12619) * fixture support for virt-who config cli : data_form deploy_type virtwho_config & manifest content move * add fixture for virt-who config * Update virt-who config ui fixture data_form_ui & virtwho_config_ui deploy_type_ui * remove used import * Update virtwho_config.py update the format for import * Update taxonomy.py Revert the changes * Update test_libvirt_sca.py * Update test_libvirt.py * Update test_nutanix.py * Update test_nutanix_sca.py * Update test_esx_sca.py * fix pre-commit issue * use pytest.mark.usefixtures for delete_host * put delete_host to virtwho_config fixture (cherry picked from commit d4079a663adfdf58deecf94f189ff23aa2ea5b69) Co-authored-by: yanpliu --- conftest.py | 1 + pytest_fixtures/component/virtwho_config.py | 347 ++++++++++++++++++ tests/foreman/virtwho/api/test_esx.py | 2 +- tests/foreman/virtwho/api/test_kubevirt.py | 2 +- tests/foreman/virtwho/api/test_nutanix.py | 2 +- tests/foreman/virtwho/cli/test_esx.py | 177 +++++---- tests/foreman/virtwho/cli/test_esx_sca.py | 239 ++++++------ tests/foreman/virtwho/cli/test_hyperv.py | 61 +-- tests/foreman/virtwho/cli/test_hyperv_sca.py | 62 +--- tests/foreman/virtwho/cli/test_kubevirt.py | 59 +-- .../foreman/virtwho/cli/test_kubevirt_sca.py | 59 +-- tests/foreman/virtwho/cli/test_libvirt.py | 60 +-- tests/foreman/virtwho/cli/test_libvirt_sca.py | 60 +-- tests/foreman/virtwho/cli/test_nutanix.py | 118 +++--- tests/foreman/virtwho/cli/test_nutanix_sca.py | 93 ++--- tests/foreman/virtwho/ui/test_esx.py | 2 +- tests/foreman/virtwho/ui/test_esx_sca.py | 2 +- 17 files changed, 680 insertions(+), 666 deletions(-) create mode 100644 pytest_fixtures/component/virtwho_config.py diff --git a/conftest.py b/conftest.py index cf350ce8b12..14b8601c4d2 100644 --- a/conftest.py +++ b/conftest.py @@ -66,6 +66,7 @@ 'pytest_fixtures.component.templatesync', 'pytest_fixtures.component.user', 'pytest_fixtures.component.user_role', + 'pytest_fixtures.component.virtwho_config', # upgrade 'pytest_plugins.upgrade.scenario_workers', ] diff --git a/pytest_fixtures/component/virtwho_config.py b/pytest_fixtures/component/virtwho_config.py new file mode 100644 index 00000000000..fc7a262e802 --- /dev/null +++ b/pytest_fixtures/component/virtwho_config.py @@ -0,0 +1,347 @@ +import pytest + +from robottelo.config import settings +from robottelo.utils.datafactory import gen_string +from robottelo.utils.virtwho import ( + deploy_configure_by_command, + deploy_configure_by_script, + get_configure_command, + get_guest_info, +) + +LOGGEDOUT = 'Logged out.' + + +@pytest.fixture +def org_module(request, default_org, module_sca_manifest_org): + if 'sca' in request.module.__name__.split('.')[-1]: + org_module = module_sca_manifest_org + else: + org_module = default_org + return org_module + + +@pytest.fixture +def org_session(request, session, session_sca): + if 'sca' in request.module.__name__.split('.')[-1]: + org_session = session_sca + else: + org_session = session + return org_session + + +@pytest.fixture +def form_data_cli(request, target_sat, org_module): + hypervisor_type = request.module.__name__.split('.')[-1].split('_', 1)[-1] + if 'esx' in hypervisor_type: + form = { + 'name': gen_string('alpha'), + 'debug': 1, + 'interval': '60', + 'hypervisor-id': 'hostname', + 'hypervisor-type': settings.virtwho.esx.hypervisor_type, + 'hypervisor-server': settings.virtwho.esx.hypervisor_server, + 'organization-id': org_module.id, + 'filtering-mode': 'none', + 'satellite-url': target_sat.hostname, + 'hypervisor-username': settings.virtwho.esx.hypervisor_username, + 'hypervisor-password': settings.virtwho.esx.hypervisor_password, + } + elif 'hyperv' in hypervisor_type: + form = { + 'name': gen_string('alpha'), + 'debug': 1, + 'interval': '60', + 'hypervisor-id': 'hostname', + 'hypervisor-type': settings.virtwho.hyperv.hypervisor_type, + 'hypervisor-server': settings.virtwho.hyperv.hypervisor_server, + 'organization-id': org_module.id, + 'filtering-mode': 'none', + 'satellite-url': target_sat.hostname, + 'hypervisor-username': settings.virtwho.hyperv.hypervisor_username, + 'hypervisor-password': settings.virtwho.hyperv.hypervisor_password, + } + elif 'kubevirt' in hypervisor_type: + form = { + 'name': gen_string('alpha'), + 'debug': 1, + 'interval': '60', + 'hypervisor-id': 'hostname', + 'hypervisor-type': settings.virtwho.kubevirt.hypervisor_type, + 'organization-id': org_module.id, + 'filtering-mode': 'none', + 'satellite-url': target_sat.hostname, + 'kubeconfig-path': settings.virtwho.kubevirt.hypervisor_config_file, + } + elif 'libvirt' in hypervisor_type: + form = { + 'name': gen_string('alpha'), + 'debug': 1, + 'interval': '60', + 'hypervisor-id': 'hostname', + 'hypervisor-type': settings.virtwho.libvirt.hypervisor_type, + 'hypervisor-server': settings.virtwho.libvirt.hypervisor_server, + 'organization-id': org_module.id, + 'filtering-mode': 'none', + 'satellite-url': target_sat.hostname, + 'hypervisor-username': settings.virtwho.libvirt.hypervisor_username, + } + elif 'nutanix' in hypervisor_type: + form = { + 'name': gen_string('alpha'), + 'debug': 1, + 'interval': '60', + 'hypervisor-id': 'hostname', + 'hypervisor-type': settings.virtwho.ahv.hypervisor_type, + 'hypervisor-server': settings.virtwho.ahv.hypervisor_server, + 'organization-id': org_module.id, + 'filtering-mode': 'none', + 'satellite-url': target_sat.hostname, + 'hypervisor-username': settings.virtwho.ahv.hypervisor_username, + 'hypervisor-password': settings.virtwho.ahv.hypervisor_password, + 'prism-flavor': settings.virtwho.ahv.prism_flavor, + 'ahv-internal-debug': 'false', + } + return form + + +@pytest.fixture +def form_data_api(request, target_sat, org_module): + hypervisor_type = request.module.__name__.split('.')[-1].split('_', 1)[-1] + if 'esx' in hypervisor_type: + form = { + 'name': gen_string('alpha'), + 'debug': 1, + 'interval': '60', + 'hypervisor_id': 'hostname', + 'hypervisor_type': settings.virtwho.esx.hypervisor_type, + 'hypervisor_server': settings.virtwho.esx.hypervisor_server, + 'organization_id': org_module.id, + 'filtering_mode': 'none', + 'satellite_url': target_sat.hostname, + 'hypervisor_username': settings.virtwho.esx.hypervisor_username, + 'hypervisor_password': settings.virtwho.esx.hypervisor_password, + } + elif 'hyperv' in hypervisor_type: + form = { + 'name': gen_string('alpha'), + 'debug': 1, + 'interval': '60', + 'hypervisor_id': 'hostname', + 'hypervisor_type': settings.virtwho.hyperv.hypervisor_type, + 'hypervisor_server': settings.virtwho.hyperv.hypervisor_server, + 'organization_id': org_module.id, + 'filtering_mode': 'none', + 'satellite_url': target_sat.hostname, + 'hypervisor_username': settings.virtwho.hyperv.hypervisor_username, + 'hypervisor_password': settings.virtwho.hyperv.hypervisor_password, + } + elif 'kubevirt' in hypervisor_type: + form = { + 'name': gen_string('alpha'), + 'debug': 1, + 'interval': '60', + 'hypervisor_id': 'hostname', + 'hypervisor_type': settings.virtwho.kubevirt.hypervisor_type, + 'organization_id': org_module.id, + 'filtering_mode': 'none', + 'satellite_url': target_sat.hostname, + 'kubeconfig_path': settings.virtwho.kubevirt.hypervisor_config_file, + } + elif 'libvirt' in hypervisor_type: + form = { + 'name': gen_string('alpha'), + 'debug': 1, + 'interval': '60', + 'hypervisor_id': 'hostname', + 'hypervisor_type': settings.virtwho.libvirt.hypervisor_type, + 'hypervisor_server': settings.virtwho.libvirt.hypervisor_server, + 'organization_id': org_module.id, + 'filtering_mode': 'none', + 'satellite_url': target_sat.hostname, + 'hypervisor_username': settings.virtwho.libvirt.hypervisor_username, + } + elif 'nutanix' in hypervisor_type: + form = { + 'name': gen_string('alpha'), + 'debug': 1, + 'interval': '60', + 'hypervisor_id': 'hostname', + 'hypervisor_type': settings.virtwho.ahv.hypervisor_type, + 'hypervisor_server': settings.virtwho.ahv.hypervisor_server, + 'organization_id': org_module.id, + 'filtering_mode': 'none', + 'satellite_url': target_sat.hostname, + 'hypervisor_username': settings.virtwho.ahv.hypervisor_username, + 'hypervisor_password': settings.virtwho.ahv.hypervisor_password, + 'prism_flavor': settings.virtwho.ahv.prism_flavor, + 'ahv_internal_debug': 'false', + } + return form + + +@pytest.fixture +def form_data_ui(request, target_sat, org_module): + hypervisor_type = request.module.__name__.split('.')[-1].split('_', 1)[-1] + if 'esx' in hypervisor_type: + form = { + 'debug': True, + 'interval': 'Every hour', + 'hypervisor_id': 'hostname', + 'hypervisor_type': settings.virtwho.esx.hypervisor_type, + 'hypervisor_content.server': settings.virtwho.esx.hypervisor_server, + 'hypervisor_content.username': settings.virtwho.esx.hypervisor_username, + 'hypervisor_content.password': settings.virtwho.esx.hypervisor_password, + } + elif 'hyperv' in hypervisor_type: + form = { + 'debug': True, + 'interval': 'Every hour', + 'hypervisor_id': 'hostname', + 'hypervisor_type': settings.virtwho.hyperv.hypervisor_type, + 'hypervisor_content.server': settings.virtwho.hyperv.hypervisor_server, + 'hypervisor_content.username': settings.virtwho.hyperv.hypervisor_username, + 'hypervisor_content.password': settings.virtwho.hyperv.hypervisor_password, + } + elif 'kubevirt' in hypervisor_type: + form = { + 'debug': True, + 'interval': 'Every hour', + 'hypervisor_id': 'hostname', + 'hypervisor_type': settings.virtwho.kubevirt.hypervisor_type, + 'hypervisor_content.kubeconfig': settings.virtwho.kubevirt.hypervisor_config_file, + } + elif 'libvirt' in hypervisor_type: + form = { + 'debug': True, + 'interval': 'Every hour', + 'hypervisor_id': 'hostname', + 'hypervisor_type': settings.virtwho.libvirt.hypervisor_type, + 'hypervisor_content.server': settings.virtwho.libvirt.hypervisor_server, + 'hypervisor_content.username': settings.virtwho.libvirt.hypervisor_username, + } + elif 'nutanix' in hypervisor_type: + form = { + 'debug': True, + 'interval': 'Every hour', + 'hypervisor_id': 'hostname', + 'hypervisor_type': settings.virtwho.ahv.hypervisor_type, + 'hypervisor_content.server': settings.virtwho.ahv.hypervisor_server, + 'hypervisor_content.username': settings.virtwho.ahv.hypervisor_username, + 'hypervisor_content.password': settings.virtwho.ahv.hypervisor_password, + 'hypervisor_content.prism_flavor': "Prism Element", + 'ahv_internal_debug': False, + } + return form + + +@pytest.fixture +def virtwho_config_cli(form_data_cli, target_sat): + virtwho_config_cli = target_sat.cli.VirtWhoConfig.create(form_data_cli)['general-information'] + yield virtwho_config_cli + target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config_cli['name']}) + assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data_cli['name'])) + + +@pytest.fixture +def virtwho_config_api(form_data_api, target_sat): + virtwho_config_api = target_sat.api.VirtWhoConfig(**form_data_api).create() + yield virtwho_config_api + virtwho_config_api.delete() + assert not target_sat.api.VirtWhoConfig().search( + query={'search': f"name={form_data_api['name']}"} + ) + + +@pytest.fixture +def virtwho_config_ui(form_data_ui, target_sat, org_session): + name = gen_string('alpha') + form_data_ui['name'] = name + with org_session: + org_session.virtwho_configure.create(form_data_ui) + yield virtwho_config_ui + org_session.virtwho_configure.delete(name) + assert not org_session.virtwho_configure.search(name) + + +@pytest.fixture +def deploy_type_cli( + request, + org_module, + form_data_cli, + virtwho_config_cli, + target_sat, +): + deploy_type = request.param.lower() + assert virtwho_config_cli['status'] == 'No Report Yet' + if "id" in deploy_type: + command = get_configure_command(virtwho_config_cli['id'], org_module.name) + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data_cli['hypervisor-type'], debug=True, org=org_module.label + ) + elif "script" in deploy_type: + script = target_sat.cli.VirtWhoConfig.fetch( + {'id': virtwho_config_cli['id']}, output_format='base' + ) + hypervisor_name, guest_name = deploy_configure_by_script( + script, form_data_cli['hypervisor-type'], debug=True, org=org_module.label + ) + return hypervisor_name, guest_name + + +@pytest.fixture +def deploy_type_api( + request, + org_module, + form_data_api, + virtwho_config_api, + target_sat, +): + deploy_type = request.param.lower() + assert virtwho_config_api.status == 'unknown' + if "id" in deploy_type: + command = get_configure_command(virtwho_config_api.id, org_module.name) + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data_api['hypervisor_type'], debug=True, org=org_module.label + ) + elif "script" in deploy_type: + script = virtwho_config_api.deploy_script() + hypervisor_name, guest_name = deploy_configure_by_script( + script['virt_who_config_script'], + form_data_api['hypervisor_type'], + debug=True, + org=org_module.label, + ) + return hypervisor_name, guest_name + + +@pytest.fixture +def deploy_type_ui( + request, + org_module, + form_data_ui, + org_session, + virtwho_config_ui, + target_sat, +): + deploy_type = request.param.lower() + values = org_session.virtwho_configure.read(form_data_ui['name']) + if "id" in deploy_type: + command = values['deploy']['command'] + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data_ui['hypervisor_type'], debug=True, org=org_module.label + ) + elif "script" in deploy_type: + script = values['deploy']['script'] + hypervisor_name, guest_name = deploy_configure_by_script( + script, form_data_ui['hypervisor_type'], debug=True, org=org_module.label + ) + return hypervisor_name, guest_name + + +@pytest.fixture +def delete_host(form_data_api, target_sat): + guest_name, _ = get_guest_info(form_data_api['hypervisor_type']) + results = target_sat.api.Host().search(query={'search': guest_name}) + if results: + target_sat.api.Host(id=results[0].read_json()['id']).delete() diff --git a/tests/foreman/virtwho/api/test_esx.py b/tests/foreman/virtwho/api/test_esx.py index e7544047d7e..1eda85fde0b 100644 --- a/tests/foreman/virtwho/api/test_esx.py +++ b/tests/foreman/virtwho/api/test_esx.py @@ -30,7 +30,7 @@ ) -@pytest.mark.delete_host +@pytest.mark.usefixtures('delete_host') class TestVirtWhoConfigforEsx: @pytest.mark.tier2 @pytest.mark.parametrize('deploy_type_api', ['id', 'script'], indirect=True) diff --git a/tests/foreman/virtwho/api/test_kubevirt.py b/tests/foreman/virtwho/api/test_kubevirt.py index 88292e772db..b1bfe96a34b 100644 --- a/tests/foreman/virtwho/api/test_kubevirt.py +++ b/tests/foreman/virtwho/api/test_kubevirt.py @@ -27,7 +27,7 @@ ) -@pytest.mark.delete_host +@pytest.mark.usefixtures('delete_host') class TestVirtWhoConfigforKubevirt: @pytest.mark.tier2 @pytest.mark.parametrize('deploy_type_api', ['id', 'script'], indirect=True) diff --git a/tests/foreman/virtwho/api/test_nutanix.py b/tests/foreman/virtwho/api/test_nutanix.py index ae2726cf610..c065f164a5a 100644 --- a/tests/foreman/virtwho/api/test_nutanix.py +++ b/tests/foreman/virtwho/api/test_nutanix.py @@ -30,7 +30,7 @@ ) -@pytest.mark.delete_host +@pytest.mark.usefixtures('delete_host') class TestVirtWhoConfigforNutanix: @pytest.mark.tier2 @pytest.mark.parametrize('deploy_type_api', ['id', 'script'], indirect=True) diff --git a/tests/foreman/virtwho/cli/test_esx.py b/tests/foreman/virtwho/cli/test_esx.py index c604fc6f3f2..c5d5a5b1364 100644 --- a/tests/foreman/virtwho/cli/test_esx.py +++ b/tests/foreman/virtwho/cli/test_esx.py @@ -29,7 +29,6 @@ create_http_proxy, deploy_configure_by_command, deploy_configure_by_command_check, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_option, @@ -38,37 +37,11 @@ ) -@pytest.fixture -def form_data(target_sat, default_org): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor-id': 'hostname', - 'hypervisor-type': settings.virtwho.esx.hypervisor_type, - 'hypervisor-server': settings.virtwho.esx.hypervisor_server, - 'organization-id': default_org.id, - 'filtering-mode': 'none', - 'satellite-url': target_sat.hostname, - 'hypervisor-username': settings.virtwho.esx.hypervisor_username, - 'hypervisor-password': settings.virtwho.esx.hypervisor_password, - } - return form - - -@pytest.fixture -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] - yield virtwho_config - target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config['name']}) - assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) - - class TestVirtWhoConfigforEsx: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_cli', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, default_org, form_data, virtwho_config, target_sat, deploy_type + self, default_org, target_sat, virtwho_config_cli, deploy_type_cli ): """Verify " hammer virt-who-config deploy" @@ -80,20 +53,9 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config['status'] == 'No Report Yet' - if deploy_type == "id": - command = get_configure_command(virtwho_config['id'], default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=default_org.label - ) - elif deploy_type == "script": - script = target_sat.cli.VirtWhoConfig.fetch( - {'id': virtwho_config['id']}, output_format='base' - ) - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor-type'], debug=True, org=default_org.label - ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ + assert virtwho_config_cli['status'] == 'No Report Yet' + hypervisor_name, guest_name = deploy_type_cli + virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']})[ 'general-information' ]['status'] assert virt_who_instance == 'OK' @@ -124,7 +86,7 @@ def test_positive_deploy_configure_by_id_script( assert result.strip() == 'Subscription attached to the host successfully.' @pytest.mark.tier2 - def test_positive_debug_option(self, default_org, form_data, target_sat): + def test_positive_debug_option(self, default_org, form_data_cli, target_sat): """Verify debug option by hammer virt-who-config update" :id: c98bc518-828c-49ba-a644-542db3190263 @@ -135,8 +97,8 @@ def test_positive_debug_option(self, default_org, form_data, target_sat): :CaseImportance: Medium """ - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] - assert virtwho_config['name'] == form_data['name'] + virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data_cli)['general-information'] + assert virtwho_config['name'] == form_data_cli['name'] new_name = gen_string('alphanumeric') target_sat.cli.VirtWhoConfig.update({'id': virtwho_config['id'], 'new-name': new_name}) virt_who_instance_name = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ @@ -148,12 +110,14 @@ def test_positive_debug_option(self, default_org, form_data, target_sat): target_sat.cli.VirtWhoConfig.update({'id': virtwho_config['id'], 'debug': key}) command = get_configure_command(virtwho_config['id'], default_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=default_org.label + command, form_data_cli['hypervisor-type'], org=default_org.label ) assert get_configure_option('debug', ETC_VIRTWHO_CONFIG) == value @pytest.mark.tier2 - def test_positive_interval_option(self, default_org, form_data, virtwho_config, target_sat): + def test_positive_interval_option( + self, default_org, form_data_cli, virtwho_config_cli, target_sat + ): """Verify interval option by hammer virt-who-config update" :id: 5d558bca-534c-4bd4-b401-a0c362033c57 @@ -175,16 +139,16 @@ def test_positive_interval_option(self, default_org, form_data, virtwho_config, '4320': '259200', } for key, value in sorted(options.items(), key=lambda item: int(item[0])): - target_sat.cli.VirtWhoConfig.update({'id': virtwho_config['id'], 'interval': key}) - command = get_configure_command(virtwho_config['id'], default_org.name) + target_sat.cli.VirtWhoConfig.update({'id': virtwho_config_cli['id'], 'interval': key}) + command = get_configure_command(virtwho_config_cli['id'], default_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=default_org.label + command, form_data_cli['hypervisor-type'], org=default_org.label ) assert get_configure_option('interval', ETC_VIRTWHO_CONFIG) == value @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, default_org, form_data, virtwho_config, target_sat + self, default_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify hypervisor_id option by hammer virt-who-config update" @@ -200,19 +164,21 @@ def test_positive_hypervisor_id_option( values = ['uuid', 'hostname', 'hwuuid'] for value in values: target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'hypervisor-id': value} + {'id': virtwho_config_cli['id'], 'hypervisor-id': value} ) - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['connection']['hypervisor-id'] == value - config_file = get_configure_file(virtwho_config['id']) - command = get_configure_command(virtwho_config['id'], default_org.name) + config_file = get_configure_file(virtwho_config_cli['id']) + command = get_configure_command(virtwho_config_cli['id'], default_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=default_org.label + command, form_data_cli['hypervisor-type'], org=default_org.label ) assert get_configure_option('hypervisor_id', config_file) == value @pytest.mark.tier2 - def test_positive_filter_option(self, default_org, form_data, virtwho_config, target_sat): + def test_positive_filter_option( + self, default_org, form_data_cli, virtwho_config_cli, target_sat + ): """Verify filter option by hammer virt-who-config update" :id: aaf45c5e-9504-47ce-8f25-b8073c2de036 @@ -224,34 +190,48 @@ def test_positive_filter_option(self, default_org, form_data, virtwho_config, ta :CaseImportance: Medium """ regex = '.*redhat.com' - whitelist = {'id': virtwho_config['id'], 'filtering-mode': 'whitelist', 'whitelist': regex} - blacklist = {'id': virtwho_config['id'], 'filtering-mode': 'blacklist', 'blacklist': regex} + whitelist = { + 'id': virtwho_config_cli['id'], + 'filtering-mode': 'whitelist', + 'whitelist': regex, + } + blacklist = { + 'id': virtwho_config_cli['id'], + 'filtering-mode': 'blacklist', + 'blacklist': regex, + } # esx support filter-host-parents and exclude-host-parents options whitelist['filter-host-parents'] = regex blacklist['exclude-host-parents'] = regex - config_file = get_configure_file(virtwho_config['id']) - command = get_configure_command(virtwho_config['id'], default_org.name) + config_file = get_configure_file(virtwho_config_cli['id']) + command = get_configure_command(virtwho_config_cli['id'], default_org.name) # Update Whitelist and check the result target_sat.cli.VirtWhoConfig.update(whitelist) - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['connection']['filtering'] == 'Whitelist' assert result['connection']['filtered-hosts'] == regex assert result['connection']['filter-host-parents'] == regex - deploy_configure_by_command(command, form_data['hypervisor-type'], org=default_org.label) + deploy_configure_by_command( + command, form_data_cli['hypervisor-type'], org=default_org.label + ) assert get_configure_option('filter_hosts', config_file) == regex assert get_configure_option('filter_host_parents', config_file) == regex # Update Blacklist and check the result target_sat.cli.VirtWhoConfig.update(blacklist) - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['connection']['filtering'] == 'Blacklist' assert result['connection']['excluded-hosts'] == regex assert result['connection']['exclude-host-parents'] == regex - deploy_configure_by_command(command, form_data['hypervisor-type'], org=default_org.label) + deploy_configure_by_command( + command, form_data_cli['hypervisor-type'], org=default_org.label + ) assert get_configure_option('exclude_hosts', config_file) == regex assert get_configure_option('exclude_host_parents', config_file) == regex @pytest.mark.tier2 - def test_positive_proxy_option(self, default_org, form_data, virtwho_config, target_sat): + def test_positive_proxy_option( + self, default_org, form_data_cli, virtwho_config_cli, target_sat + ): """Verify http_proxy option by hammer virt-who-config update" :id: 409d108e-e814-482b-93ed-09db89d21dda @@ -268,13 +248,15 @@ def test_positive_proxy_option(self, default_org, form_data, virtwho_config, tar https_proxy_url, https_proxy_name, https_proxy_id = create_http_proxy(org=default_org) no_proxy = 'test.satellite.com' target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'http-proxy': https_proxy_name, 'no-proxy': no_proxy} + {'id': virtwho_config_cli['id'], 'http-proxy': https_proxy_name, 'no-proxy': no_proxy} ) - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['http-proxy']['http-proxy-name'] == https_proxy_name assert result['connection']['ignore-proxy'] == no_proxy - command = get_configure_command(virtwho_config['id'], default_org.name) - deploy_configure_by_command(command, form_data['hypervisor-type'], org=default_org.label) + command = get_configure_command(virtwho_config_cli['id'], default_org.name) + deploy_configure_by_command( + command, form_data_cli['hypervisor-type'], org=default_org.label + ) assert get_configure_option('https_proxy', ETC_VIRTWHO_CONFIG) == https_proxy_url assert get_configure_option('no_proxy', ETC_VIRTWHO_CONFIG) == no_proxy @@ -283,13 +265,15 @@ def test_positive_proxy_option(self, default_org, form_data, virtwho_config, tar http_type='http', org=default_org ) target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'http-proxy-id': http_proxy_id} + {'id': virtwho_config_cli['id'], 'http-proxy-id': http_proxy_id} + ) + deploy_configure_by_command( + command, form_data_cli['hypervisor-type'], org=default_org.label ) - deploy_configure_by_command(command, form_data['hypervisor-type'], org=default_org.label) assert get_configure_option('http_proxy', ETC_VIRTWHO_CONFIG) == http_proxy_url @pytest.mark.tier2 - def test_positive_rhsm_option(self, default_org, form_data, virtwho_config, target_sat): + def test_positive_rhsm_option(self, default_org, form_data_cli, virtwho_config_cli, target_sat): """Verify rhsm options in the configure file" :id: b5b93d4d-e780-41c0-9eaa-2407cc1dcc9b @@ -302,9 +286,11 @@ def test_positive_rhsm_option(self, default_org, form_data, virtwho_config, targ :CaseImportance: Medium """ - config_file = get_configure_file(virtwho_config['id']) - command = get_configure_command(virtwho_config['id'], default_org.name) - deploy_configure_by_command(command, form_data['hypervisor-type'], org=default_org.label) + config_file = get_configure_file(virtwho_config_cli['id']) + command = get_configure_command(virtwho_config_cli['id'], default_org.name) + deploy_configure_by_command( + command, form_data_cli['hypervisor-type'], org=default_org.label + ) rhsm_username = get_configure_option('rhsm_username', config_file) assert not User.exists(search=('login', rhsm_username)) assert get_configure_option('rhsm_hostname', config_file) == target_sat.hostname @@ -340,7 +326,7 @@ def test_positive_post_hypervisors(self, function_org, target_sat): @pytest.mark.tier2 def test_positive_foreman_packages_protection( - self, default_org, form_data, virtwho_config, target_sat + self, default_org, form_data_cli, virtwho_config_cli, target_sat ): """foreman-protector should allow virt-who to be installed @@ -359,16 +345,21 @@ def test_positive_foreman_packages_protection( :BZ: 1783987 """ virtwho_package_locked() - command = get_configure_command(virtwho_config['id'], default_org.name) - deploy_configure_by_command(command, form_data['hypervisor-type'], org=default_org.label) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ + command = get_configure_command(virtwho_config_cli['id'], default_org.name) + deploy_configure_by_command( + command, form_data_cli['hypervisor-type'], org=default_org.label + ) + virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']})[ + 'general-information' + ]['status'] + virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']})[ 'general-information' ]['status'] assert virt_who_instance == 'OK' @pytest.mark.tier2 def test_positive_deploy_configure_hypervisor_password_with_special_characters( - self, default_org, form_data, target_sat + self, default_org, form_data_cli, target_sat ): """Verify " hammer virt-who-config deploy hypervisor with special characters" @@ -385,8 +376,8 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( :customerscenario: true """ # check the hypervisor password contains single quotes - form_data['hypervisor-password'] = "Tes't" - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] + form_data_cli['hypervisor-password'] = "Tes't" + virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data_cli)['general-information'] assert virtwho_config['status'] == 'No Report Yet' command = get_configure_command(virtwho_config['id'], default_org.name) deploy_status = deploy_configure_by_command_check(command) @@ -398,11 +389,11 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( == settings.virtwho.esx.hypervisor_username ) target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config['name']}) - assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) + assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data_cli['name'])) # check the hypervisor password contains backtick - form_data['hypervisor-password'] = r"my\`password" - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] + form_data_cli['hypervisor-password'] = r"my\`password" + virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data_cli)['general-information'] assert virtwho_config['status'] == 'No Report Yet' command = get_configure_command(virtwho_config['id'], default_org.name) deploy_status = deploy_configure_by_command_check(command) @@ -415,7 +406,9 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( ) @pytest.mark.tier2 - def test_positive_remove_env_option(self, default_org, form_data, virtwho_config, target_sat): + def test_positive_remove_env_option( + self, default_org, form_data_cli, virtwho_config_cli, target_sat + ): """remove option 'env=' from the virt-who configuration file and without any error :id: 509add77-dce7-4ba4-b9e5-2a5818c39731 @@ -432,17 +425,17 @@ def test_positive_remove_env_option(self, default_org, form_data, virtwho_config :customerscenario: true """ - command = get_configure_command(virtwho_config['id'], default_org.name) + command = get_configure_command(virtwho_config_cli['id'], default_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=default_org.label + command, form_data_cli['hypervisor-type'], debug=True, org=default_org.label ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ + virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']})[ 'general-information' ]['status'] assert virt_who_instance == 'OK' # Check the option "env=" should be removed from etc/virt-who.d/virt-who.conf option = "env" - config_file = get_configure_file(virtwho_config['id']) + config_file = get_configure_file(virtwho_config_cli['id']) env_error = ( f"option {{\'{option}\'}} is not exist or not be enabled in {{\'{config_file}\'}}" ) diff --git a/tests/foreman/virtwho/cli/test_esx_sca.py b/tests/foreman/virtwho/cli/test_esx_sca.py index d8ec7c71d9d..ab942933bbe 100644 --- a/tests/foreman/virtwho/cli/test_esx_sca.py +++ b/tests/foreman/virtwho/cli/test_esx_sca.py @@ -27,7 +27,6 @@ create_http_proxy, deploy_configure_by_command, deploy_configure_by_command_check, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_option, @@ -36,38 +35,12 @@ ) -@pytest.fixture -def form_data(target_sat, module_sca_manifest_org): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor-id': 'hostname', - 'hypervisor-type': settings.virtwho.esx.hypervisor_type, - 'hypervisor-server': settings.virtwho.esx.hypervisor_server, - 'organization-id': module_sca_manifest_org.id, - 'filtering-mode': 'none', - 'satellite-url': target_sat.hostname, - 'hypervisor-username': settings.virtwho.esx.hypervisor_username, - 'hypervisor-password': settings.virtwho.esx.hypervisor_password, - } - return form - - -@pytest.fixture -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] - yield virtwho_config - target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config['name']}) - assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) - - class TestVirtWhoConfigforEsx: @pytest.mark.tier2 @pytest.mark.upgrade - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_cli', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat, deploy_type + self, module_sca_manifest_org, target_sat, virtwho_config_cli, deploy_type_cli ): """Verify "hammer virt-who-config deploy & fetch" @@ -81,27 +54,15 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config['status'] == 'No Report Yet' - if deploy_type == "id": - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) - deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=module_sca_manifest_org.label - ) - elif deploy_type == "script": - script = target_sat.cli.VirtWhoConfig.fetch( - {'id': virtwho_config['id']}, output_format='base' - ) - deploy_configure_by_script( - script, form_data['hypervisor-type'], debug=True, org=module_sca_manifest_org.label - ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ + assert virtwho_config_cli['status'] == 'No Report Yet' + virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']})[ 'general-information' ]['status'] assert virt_who_instance == 'OK' @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify hypervisor_id option by hammer virt-who-config update" @@ -115,20 +76,20 @@ def test_positive_hypervisor_id_option( """ for value in ['uuid', 'hostname']: target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'hypervisor-id': value} + {'id': virtwho_config_cli['id'], 'hypervisor-id': value} ) - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['connection']['hypervisor-id'] == value - config_file = get_configure_file(virtwho_config['id']) - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + config_file = get_configure_file(virtwho_config_cli['id']) + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=module_sca_manifest_org.label + command, form_data_cli['hypervisor-type'], org=module_sca_manifest_org.label ) assert get_configure_option('hypervisor_id', config_file) == value @pytest.mark.tier2 def test_positive_debug_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify debug option by hammer virt-who-config update" @@ -140,19 +101,19 @@ def test_positive_debug_option( :CaseImportance: Medium """ - assert virtwho_config['name'] == form_data['name'] + assert virtwho_config_cli['name'] == form_data_cli['name'] options = {'false': '0', 'no': '0', 'true': '1', 'yes': '1'} for key, value in options.items(): - target_sat.cli.VirtWhoConfig.update({'id': virtwho_config['id'], 'debug': key}) - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + target_sat.cli.VirtWhoConfig.update({'id': virtwho_config_cli['id'], 'debug': key}) + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=module_sca_manifest_org.label + command, form_data_cli['hypervisor-type'], org=module_sca_manifest_org.label ) assert get_configure_option('debug', ETC_VIRTWHO_CONFIG) == value @pytest.mark.tier2 def test_positive_name_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify debug option by hammer virt-who-config update" @@ -164,20 +125,20 @@ def test_positive_name_option( :CaseImportance: Medium """ - assert virtwho_config['name'] == form_data['name'] + assert virtwho_config_cli['name'] == form_data_cli['name'] new_name = gen_string('alphanumeric') - target_sat.cli.VirtWhoConfig.update({'id': virtwho_config['id'], 'new-name': new_name}) - virt_who_instance_name = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ - 'general-information' - ]['name'] + target_sat.cli.VirtWhoConfig.update({'id': virtwho_config_cli['id'], 'new-name': new_name}) + virt_who_instance_name = target_sat.cli.VirtWhoConfig.info( + {'id': virtwho_config_cli['id']} + )['general-information']['name'] assert virt_who_instance_name == new_name target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'new-name': form_data['name']} + {'id': virtwho_config_cli['id'], 'new-name': form_data_cli['name']} ) @pytest.mark.tier2 def test_positive_interval_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify interval option by hammer virt-who-config update" @@ -200,10 +161,10 @@ def test_positive_interval_option( '4320': '259200', } for key, value in options.items(): - target_sat.cli.VirtWhoConfig.update({'id': virtwho_config['id'], 'interval': key}) - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + target_sat.cli.VirtWhoConfig.update({'id': virtwho_config_cli['id'], 'interval': key}) + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=module_sca_manifest_org.label + command, form_data_cli['hypervisor-type'], org=module_sca_manifest_org.label ) assert get_configure_option('interval', ETC_VIRTWHO_CONFIG) == value @@ -213,8 +174,8 @@ def test_positive_interval_option( def test_positive_filter_option( self, module_sca_manifest_org, - form_data, - virtwho_config, + form_data_cli, + virtwho_config_cli, target_sat, filter_type, option_type, @@ -236,7 +197,7 @@ def test_positive_filter_option( # Update whitelist or blacklist and check the result if filter_type == "whitelist": whitelist = { - 'id': virtwho_config['id'], + 'id': virtwho_config_cli['id'], 'filtering-mode': 'whitelist', 'whitelist': regex, } @@ -245,17 +206,17 @@ def test_positive_filter_option( target_sat.cli.VirtWhoConfig.update(whitelist) elif filter_type == "blacklist": blacklist = { - 'id': virtwho_config['id'], + 'id': virtwho_config_cli['id'], 'filtering-mode': 'blacklist', 'blacklist': regex, } blacklist['exclude-host-parents'] = regex target_sat.cli.VirtWhoConfig.update(blacklist) - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) - config_file = get_configure_file(virtwho_config['id']) - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) + config_file = get_configure_file(virtwho_config_cli['id']) + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=module_sca_manifest_org.label + command, form_data_cli['hypervisor-type'], org=module_sca_manifest_org.label ) if filter_type == "whitelist": assert result['connection']['filtering'] == 'Whitelist' @@ -271,14 +232,16 @@ def test_positive_filter_option( assert get_configure_option('exclude_host_parents', config_file) == regex elif option_type == "create": # Create a new virt-who config with filtering-mode whitelist or blacklist - target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config['name']}) - assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) - form_data['filtering-mode'] = filter_type - form_data[filter_type] = regex - form_data['filter-host-parents'] = regex - form_data['exclude-host-parents'] = regex - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config_cli['name']}) + assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data_cli['name'])) + form_data_cli['filtering-mode'] = filter_type + form_data_cli[filter_type] = regex + form_data_cli['filter-host-parents'] = regex + form_data_cli['exclude-host-parents'] = regex + virtwho_config_cli = target_sat.cli.VirtWhoConfig.create(form_data_cli)[ + 'general-information' + ] + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) if filter_type == "whitelist": assert result['connection']['filtering'] == 'Whitelist' assert result['connection']['filtered-hosts'] == regex @@ -287,11 +250,11 @@ def test_positive_filter_option( assert result['connection']['filtering'] == 'Blacklist' assert result['connection']['excluded-hosts'] == regex assert result['connection']['exclude-host-parents'] == regex - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=module_sca_manifest_org.label + command, form_data_cli['hypervisor-type'], org=module_sca_manifest_org.label ) - config_file = get_configure_file(virtwho_config['id']) + config_file = get_configure_file(virtwho_config_cli['id']) if filter_type == "whitelist": assert get_configure_option('filter_hosts', config_file) == regex assert get_configure_option('filter_host_parents', config_file) == regex @@ -301,7 +264,7 @@ def test_positive_filter_option( @pytest.mark.tier2 def test_positive_proxy_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify http_proxy option by hammer virt-who-config update" @@ -323,14 +286,14 @@ def test_positive_proxy_option( ) no_proxy = 'test.satellite.com' target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'http-proxy': https_proxy_name, 'no-proxy': no_proxy} + {'id': virtwho_config_cli['id'], 'http-proxy': https_proxy_name, 'no-proxy': no_proxy} ) - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['http-proxy']['http-proxy-name'] == https_proxy_name assert result['connection']['ignore-proxy'] == no_proxy - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=module_sca_manifest_org.label + command, form_data_cli['hypervisor-type'], org=module_sca_manifest_org.label ) assert get_configure_option('https_proxy', ETC_VIRTWHO_CONFIG) == https_proxy_url assert get_configure_option('no_proxy', ETC_VIRTWHO_CONFIG) == no_proxy @@ -340,46 +303,50 @@ def test_positive_proxy_option( http_type='http', org=module_sca_manifest_org ) target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'http-proxy-id': http_proxy_id} + {'id': virtwho_config_cli['id'], 'http-proxy-id': http_proxy_id} ) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=module_sca_manifest_org.label + command, form_data_cli['hypervisor-type'], org=module_sca_manifest_org.label ) assert get_configure_option('http_proxy', ETC_VIRTWHO_CONFIG) == http_proxy_url - target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config['name']}) - assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) + target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config_cli['name']}) + assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data_cli['name'])) # Check the http proxy option, create virt-who config via http proxy id - form_data['http-proxy-id'] = http_proxy_id - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + form_data_cli['http-proxy-id'] = http_proxy_id + virtwho_config_cli = target_sat.cli.VirtWhoConfig.create(form_data_cli)[ + 'general-information' + ] + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=module_sca_manifest_org.label + command, form_data_cli['hypervisor-type'], org=module_sca_manifest_org.label ) assert get_configure_option('http_proxy', ETC_VIRTWHO_CONFIG) == http_proxy_url - target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config['name']}) - assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) + target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config_cli['name']}) + assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data_cli['name'])) # Check the https proxy option, create virt-who config via http proxy name no_proxy = 'test.satellite.com' - form_data['http-proxy'] = https_proxy_name - form_data['no-proxy'] = no_proxy - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + form_data_cli['http-proxy'] = https_proxy_name + form_data_cli['no-proxy'] = no_proxy + virtwho_config_cli = target_sat.cli.VirtWhoConfig.create(form_data_cli)[ + 'general-information' + ] + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['http-proxy']['http-proxy-name'] == https_proxy_name assert result['connection']['ignore-proxy'] == no_proxy - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=module_sca_manifest_org.label + command, form_data_cli['hypervisor-type'], org=module_sca_manifest_org.label ) - get_configure_file(virtwho_config['id']) + get_configure_file(virtwho_config_cli['id']) assert get_configure_option('https_proxy', ETC_VIRTWHO_CONFIG) == https_proxy_url assert get_configure_option('no_proxy', ETC_VIRTWHO_CONFIG) == no_proxy @pytest.mark.tier2 def test_positive_rhsm_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify rhsm options in the configure file" @@ -393,10 +360,10 @@ def test_positive_rhsm_option( :CaseImportance: Medium """ - config_file = get_configure_file(virtwho_config['id']) - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + config_file = get_configure_file(virtwho_config_cli['id']) + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=module_sca_manifest_org.label + command, form_data_cli['hypervisor-type'], org=module_sca_manifest_org.label ) rhsm_username = get_configure_option('rhsm_username', config_file) assert not User.exists(search=('login', rhsm_username)) @@ -433,7 +400,7 @@ def test_positive_post_hypervisors(self, function_org, target_sat): @pytest.mark.tier2 def test_positive_foreman_packages_protection( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_cli, virtwho_config_cli, target_sat ): """foreman-protector should allow virt-who to be installed @@ -452,18 +419,18 @@ def test_positive_foreman_packages_protection( :BZ: 1783987 """ virtwho_package_locked() - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=module_sca_manifest_org.label + command, form_data_cli['hypervisor-type'], org=module_sca_manifest_org.label ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ + virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']})[ 'general-information' ]['status'] assert virt_who_instance == 'OK' @pytest.mark.tier2 def test_positive_deploy_configure_hypervisor_password_with_special_characters( - self, module_sca_manifest_org, form_data, target_sat + self, module_sca_manifest_org, form_data_cli, target_sat ): """Verify "hammer virt-who-config deploy hypervisor with special characters" @@ -480,40 +447,44 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( :customerscenario: true """ # check the hypervisor password contains single quotes - form_data['hypervisor-password'] = "Tes't" - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] - assert virtwho_config['status'] == 'No Report Yet' - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + form_data_cli['hypervisor-password'] = "Tes't" + virtwho_config_cli = target_sat.cli.VirtWhoConfig.create(form_data_cli)[ + 'general-information' + ] + assert virtwho_config_cli['status'] == 'No Report Yet' + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_status = deploy_configure_by_command_check(command) assert deploy_status == 'Finished successfully' - config_file = get_configure_file(virtwho_config['id']) + config_file = get_configure_file(virtwho_config_cli['id']) assert get_configure_option('rhsm_hostname', config_file) == target_sat.hostname assert ( get_configure_option('username', config_file) == settings.virtwho.esx.hypervisor_username ) - target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config['name']}) - assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) + target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config_cli['name']}) + assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data_cli['name'])) # check the hypervisor password contains backtick - form_data['hypervisor-password'] = r"my\`password" - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] - assert virtwho_config['status'] == 'No Report Yet' - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + form_data_cli['hypervisor-password'] = r"my\`password" + virtwho_config_cli = target_sat.cli.VirtWhoConfig.create(form_data_cli)[ + 'general-information' + ] + assert virtwho_config_cli['status'] == 'No Report Yet' + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_status = deploy_configure_by_command_check(command) assert deploy_status == 'Finished successfully' - config_file = get_configure_file(virtwho_config['id']) + config_file = get_configure_file(virtwho_config_cli['id']) assert get_configure_option('rhsm_hostname', config_file) == target_sat.hostname assert ( get_configure_option('username', config_file) == settings.virtwho.esx.hypervisor_username ) - target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config['name']}) - assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) + target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config_cli['name']}) + assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data_cli['name'])) @pytest.mark.tier2 def test_positive_remove_env_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_cli, virtwho_config_cli, target_sat ): """remove option 'env=' from the virt-who configuration file and without any error @@ -531,17 +502,17 @@ def test_positive_remove_env_option( :customerscenario: true """ - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=module_sca_manifest_org.label + command, form_data_cli['hypervisor-type'], debug=True, org=module_sca_manifest_org.label ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ + virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']})[ 'general-information' ]['status'] assert virt_who_instance == 'OK' # Check the option "env=" should be removed from etc/virt-who.d/virt-who.conf option = "env" - config_file = get_configure_file(virtwho_config['id']) + config_file = get_configure_file(virtwho_config_cli['id']) env_error = ( f"option {{\'{option}\'}} is not exist or not be enabled in {{\'{config_file}\'}}" ) diff --git a/tests/foreman/virtwho/cli/test_hyperv.py b/tests/foreman/virtwho/cli/test_hyperv.py index 35e90d61eb1..ffc421506f0 100644 --- a/tests/foreman/virtwho/cli/test_hyperv.py +++ b/tests/foreman/virtwho/cli/test_hyperv.py @@ -16,50 +16,22 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_option, ) -@pytest.fixture -def form_data(target_sat, default_org): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor-id': 'hostname', - 'hypervisor-type': settings.virtwho.hyperv.hypervisor_type, - 'hypervisor-server': settings.virtwho.hyperv.hypervisor_server, - 'organization-id': default_org.id, - 'filtering-mode': 'none', - 'satellite-url': target_sat.hostname, - 'hypervisor-username': settings.virtwho.hyperv.hypervisor_username, - 'hypervisor-password': settings.virtwho.hyperv.hypervisor_password, - } - return form - - -@pytest.fixture -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] - yield virtwho_config - target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config['name']}) - assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) - - class TestVirtWhoConfigforHyperv: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_cli', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, default_org, form_data, virtwho_config, target_sat, deploy_type + self, default_org, virtwho_config_cli, target_sat, deploy_type_cli ): """Verify " hammer virt-who-config deploy & fetch" @@ -73,20 +45,9 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config['status'] == 'No Report Yet' - if deploy_type == "id": - command = get_configure_command(virtwho_config['id'], default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=default_org.label - ) - elif deploy_type == "script": - script = target_sat.cli.VirtWhoConfig.fetch( - {'id': virtwho_config['id']}, output_format='base' - ) - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor-type'], debug=True, org=default_org.label - ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ + assert virtwho_config_cli['status'] == 'No Report Yet' + hypervisor_name, guest_name = deploy_type_cli + virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']})[ 'general-information' ]['status'] assert virt_who_instance == 'OK' @@ -112,7 +73,7 @@ def test_positive_deploy_configure_by_id_script( @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, default_org, form_data, virtwho_config, target_sat + self, default_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify hypervisor_id option by hammer virt-who-config update" @@ -127,13 +88,13 @@ def test_positive_hypervisor_id_option( values = ['uuid', 'hostname'] for value in values: target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'hypervisor-id': value} + {'id': virtwho_config_cli['id'], 'hypervisor-id': value} ) - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['connection']['hypervisor-id'] == value - config_file = get_configure_file(virtwho_config['id']) - command = get_configure_command(virtwho_config['id'], default_org.name) + config_file = get_configure_file(virtwho_config_cli['id']) + command = get_configure_command(virtwho_config_cli['id'], default_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=default_org.label + command, form_data_cli['hypervisor-type'], org=default_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/cli/test_hyperv_sca.py b/tests/foreman/virtwho/cli/test_hyperv_sca.py index 6d60e285c9d..4b2725c5432 100644 --- a/tests/foreman/virtwho/cli/test_hyperv_sca.py +++ b/tests/foreman/virtwho/cli/test_hyperv_sca.py @@ -16,50 +16,21 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest -from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_option, ) -@pytest.fixture -def form_data(target_sat, module_sca_manifest_org): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor-id': 'hostname', - 'hypervisor-type': settings.virtwho.hyperv.hypervisor_type, - 'hypervisor-server': settings.virtwho.hyperv.hypervisor_server, - 'organization-id': module_sca_manifest_org.id, - 'filtering-mode': 'none', - 'satellite-url': target_sat.hostname, - 'hypervisor-username': settings.virtwho.hyperv.hypervisor_username, - 'hypervisor-password': settings.virtwho.hyperv.hypervisor_password, - } - return form - - -@pytest.fixture -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] - yield virtwho_config - target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config['name']}) - assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) - - class TestVirtWhoConfigforHyperv: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_cli', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat, deploy_type + self, module_sca_manifest_org, virtwho_config_cli, target_sat, deploy_type_cli ): """Verify " hammer virt-who-config deploy & fetch" @@ -73,27 +44,16 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config['status'] == 'No Report Yet' - if deploy_type == "id": - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) - deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=module_sca_manifest_org.label - ) - elif deploy_type == "script": - script = target_sat.cli.VirtWhoConfig.fetch( - {'id': virtwho_config['id']}, output_format='base' - ) - deploy_configure_by_script( - script, form_data['hypervisor-type'], debug=True, org=module_sca_manifest_org.label - ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ + assert virtwho_config_cli['status'] == 'No Report Yet' + virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']})[ 'general-information' ]['status'] assert virt_who_instance == 'OK' + @pytest.mark.tier2 @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify hypervisor_id option by hammer virt-who-config update" @@ -107,13 +67,13 @@ def test_positive_hypervisor_id_option( """ for value in ['uuid', 'hostname']: target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'hypervisor-id': value} + {'id': virtwho_config_cli['id'], 'hypervisor-id': value} ) - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['connection']['hypervisor-id'] == value - config_file = get_configure_file(virtwho_config['id']) - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + config_file = get_configure_file(virtwho_config_cli['id']) + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=module_sca_manifest_org.label + command, form_data_cli['hypervisor-type'], org=module_sca_manifest_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/cli/test_kubevirt.py b/tests/foreman/virtwho/cli/test_kubevirt.py index e003294de87..4b8a5c57f06 100644 --- a/tests/foreman/virtwho/cli/test_kubevirt.py +++ b/tests/foreman/virtwho/cli/test_kubevirt.py @@ -16,48 +16,22 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_option, ) -@pytest.fixture -def form_data(target_sat, default_org): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor-id': 'hostname', - 'hypervisor-type': settings.virtwho.kubevirt.hypervisor_type, - 'organization-id': default_org.id, - 'filtering-mode': 'none', - 'satellite-url': target_sat.hostname, - 'kubeconfig-path': settings.virtwho.kubevirt.hypervisor_config_file, - } - return form - - -@pytest.fixture -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] - yield virtwho_config - target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config['name']}) - assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) - - class TestVirtWhoConfigforKubevirt: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_cli', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, default_org, form_data, virtwho_config, target_sat, deploy_type + self, default_org, virtwho_config_cli, target_sat, deploy_type_cli ): """Verify " hammer virt-who-config deploy & fetch" @@ -71,20 +45,9 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config['status'] == 'No Report Yet' - if deploy_type == "id": - command = get_configure_command(virtwho_config['id'], default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=default_org.label - ) - elif deploy_type == "script": - script = target_sat.cli.VirtWhoConfig.fetch( - {'id': virtwho_config['id']}, output_format='base' - ) - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor-type'], debug=True, org=default_org.label - ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ + assert virtwho_config_cli['status'] == 'No Report Yet' + hypervisor_name, guest_name = deploy_type_cli + virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']})[ 'general-information' ]['status'] assert virt_who_instance == 'OK' @@ -110,7 +73,7 @@ def test_positive_deploy_configure_by_id_script( @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, default_org, form_data, virtwho_config, target_sat + self, default_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify hypervisor_id option by hammer virt-who-config update" @@ -125,13 +88,13 @@ def test_positive_hypervisor_id_option( values = ['uuid', 'hostname'] for value in values: target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'hypervisor-id': value} + {'id': virtwho_config_cli['id'], 'hypervisor-id': value} ) - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['connection']['hypervisor-id'] == value - config_file = get_configure_file(virtwho_config['id']) - command = get_configure_command(virtwho_config['id'], default_org.name) + config_file = get_configure_file(virtwho_config_cli['id']) + command = get_configure_command(virtwho_config_cli['id'], default_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=default_org.label + command, form_data_cli['hypervisor-type'], org=default_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/cli/test_kubevirt_sca.py b/tests/foreman/virtwho/cli/test_kubevirt_sca.py index 9746d80c34b..2d7fd4fca97 100644 --- a/tests/foreman/virtwho/cli/test_kubevirt_sca.py +++ b/tests/foreman/virtwho/cli/test_kubevirt_sca.py @@ -14,48 +14,21 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest -from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_option, ) -@pytest.fixture -def form_data(target_sat, module_sca_manifest_org): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor-id': 'hostname', - 'hypervisor-type': settings.virtwho.kubevirt.hypervisor_type, - 'organization-id': module_sca_manifest_org.id, - 'filtering-mode': 'none', - 'satellite-url': target_sat.hostname, - 'kubeconfig-path': settings.virtwho.kubevirt.hypervisor_config_file, - } - return form - - -@pytest.fixture -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] - yield virtwho_config - target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config['name']}) - assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) - - class TestVirtWhoConfigforKubevirt: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_cli', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat, deploy_type + self, module_sca_manifest_org, virtwho_config_cli, target_sat, deploy_type_cli ): """Verify " hammer virt-who-config deploy & fetch" @@ -69,27 +42,15 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config['status'] == 'No Report Yet' - if deploy_type == "id": - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) - deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=module_sca_manifest_org.label - ) - elif deploy_type == "script": - script = target_sat.cli.VirtWhoConfig.fetch( - {'id': virtwho_config['id']}, output_format='base' - ) - deploy_configure_by_script( - script, form_data['hypervisor-type'], debug=True, org=module_sca_manifest_org.label - ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ + assert virtwho_config_cli['status'] == 'No Report Yet' + virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']})[ 'general-information' ]['status'] assert virt_who_instance == 'OK' @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify hypervisor_id option by hammer virt-who-config update" @@ -102,13 +63,13 @@ def test_positive_hypervisor_id_option( """ for value in ['uuid', 'hostname']: target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'hypervisor-id': value} + {'id': virtwho_config_cli['id'], 'hypervisor-id': value} ) - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['connection']['hypervisor-id'] == value - config_file = get_configure_file(virtwho_config['id']) - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + config_file = get_configure_file(virtwho_config_cli['id']) + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=module_sca_manifest_org.label + command, form_data_cli['hypervisor-type'], org=module_sca_manifest_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/cli/test_libvirt.py b/tests/foreman/virtwho/cli/test_libvirt.py index 5cd4280e4f3..5c9cfda2dbd 100644 --- a/tests/foreman/virtwho/cli/test_libvirt.py +++ b/tests/foreman/virtwho/cli/test_libvirt.py @@ -16,49 +16,22 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_option, ) -@pytest.fixture -def form_data(target_sat, default_org): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor-id': 'hostname', - 'hypervisor-type': settings.virtwho.libvirt.hypervisor_type, - 'hypervisor-server': settings.virtwho.libvirt.hypervisor_server, - 'organization-id': default_org.id, - 'filtering-mode': 'none', - 'satellite-url': target_sat.hostname, - 'hypervisor-username': settings.virtwho.libvirt.hypervisor_username, - } - return form - - -@pytest.fixture -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] - yield virtwho_config - target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config['name']}) - assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) - - class TestVirtWhoConfigforLibvirt: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_cli', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, default_org, form_data, virtwho_config, target_sat, deploy_type + self, default_org, virtwho_config_cli, target_sat, deploy_type_cli ): """Verify " hammer virt-who-config deploy & fetch" @@ -72,20 +45,9 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config['status'] == 'No Report Yet' - if deploy_type == "id": - command = get_configure_command(virtwho_config['id'], default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=default_org.label - ) - elif deploy_type == "script": - script = target_sat.cli.VirtWhoConfig.fetch( - {'id': virtwho_config['id']}, output_format='base' - ) - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor-type'], debug=True, org=default_org.label - ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ + assert virtwho_config_cli['status'] == 'No Report Yet' + hypervisor_name, guest_name = deploy_type_cli + virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']})[ 'general-information' ]['status'] assert virt_who_instance == 'OK' @@ -111,7 +73,7 @@ def test_positive_deploy_configure_by_id_script( @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, default_org, form_data, virtwho_config, target_sat + self, default_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify hypervisor_id option by hammer virt-who-config update" @@ -126,13 +88,13 @@ def test_positive_hypervisor_id_option( values = ['uuid', 'hostname'] for value in values: target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'hypervisor-id': value} + {'id': virtwho_config_cli['id'], 'hypervisor-id': value} ) - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['connection']['hypervisor-id'] == value - config_file = get_configure_file(virtwho_config['id']) - command = get_configure_command(virtwho_config['id'], default_org.name) + config_file = get_configure_file(virtwho_config_cli['id']) + command = get_configure_command(virtwho_config_cli['id'], default_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=default_org.label + command, form_data_cli['hypervisor-type'], org=default_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/cli/test_libvirt_sca.py b/tests/foreman/virtwho/cli/test_libvirt_sca.py index b1a359c0095..3532bb7983c 100644 --- a/tests/foreman/virtwho/cli/test_libvirt_sca.py +++ b/tests/foreman/virtwho/cli/test_libvirt_sca.py @@ -14,49 +14,21 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest -from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, - deploy_configure_by_script, get_configure_command, get_configure_file, get_configure_option, ) -@pytest.fixture -def form_data(target_sat, module_sca_manifest_org): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor-id': 'hostname', - 'hypervisor-type': settings.virtwho.libvirt.hypervisor_type, - 'hypervisor-server': settings.virtwho.libvirt.hypervisor_server, - 'organization-id': module_sca_manifest_org.id, - 'filtering-mode': 'none', - 'satellite-url': target_sat.hostname, - 'hypervisor-username': settings.virtwho.libvirt.hypervisor_username, - } - return form - - -@pytest.fixture -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] - yield virtwho_config - target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config['name']}) - assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) - - class TestVirtWhoConfigforLibvirt: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_cli', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat, deploy_type + self, module_sca_manifest_org, virtwho_config_cli, target_sat, deploy_type_cli ): """Verify " hammer virt-who-config deploy & fetch" @@ -70,27 +42,15 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config['status'] == 'No Report Yet' - if deploy_type == "id": - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) - deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=module_sca_manifest_org.label - ) - elif deploy_type == "script": - script = target_sat.cli.VirtWhoConfig.fetch( - {'id': virtwho_config['id']}, output_format='base' - ) - deploy_configure_by_script( - script, form_data['hypervisor-type'], debug=True, org=module_sca_manifest_org.label - ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ + assert virtwho_config_cli['status'] == 'No Report Yet' + virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']})[ 'general-information' ]['status'] assert virt_who_instance == 'OK' @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify hypervisor_id option by hammer virt-who-config update" @@ -104,13 +64,13 @@ def test_positive_hypervisor_id_option( """ for value in ['uuid', 'hostname']: target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'hypervisor-id': value} + {'id': virtwho_config_cli['id'], 'hypervisor-id': value} ) - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['connection']['hypervisor-id'] == value - config_file = get_configure_file(virtwho_config['id']) - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + config_file = get_configure_file(virtwho_config_cli['id']) + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=module_sca_manifest_org.label + command, form_data_cli['hypervisor-type'], org=module_sca_manifest_org.label ) assert get_configure_option('hypervisor_id', config_file) == value diff --git a/tests/foreman/virtwho/cli/test_nutanix.py b/tests/foreman/virtwho/cli/test_nutanix.py index ed9ad20ce49..052101f792f 100644 --- a/tests/foreman/virtwho/cli/test_nutanix.py +++ b/tests/foreman/virtwho/cli/test_nutanix.py @@ -16,7 +16,6 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest from robottelo.config import settings @@ -31,39 +30,11 @@ ) -@pytest.fixture -def form_data(target_sat, default_org): - form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor-id': 'hostname', - 'hypervisor-type': settings.virtwho.ahv.hypervisor_type, - 'hypervisor-server': settings.virtwho.ahv.hypervisor_server, - 'organization-id': default_org.id, - 'filtering-mode': 'none', - 'satellite-url': target_sat.hostname, - 'hypervisor-username': settings.virtwho.ahv.hypervisor_username, - 'hypervisor-password': settings.virtwho.ahv.hypervisor_password, - 'prism-flavor': settings.virtwho.ahv.prism_flavor, - 'ahv-internal-debug': 'false', - } - return form - - -@pytest.fixture -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] - yield virtwho_config - target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config['name']}) - assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) - - class TestVirtWhoConfigforNutanix: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_cli', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, default_org, form_data, virtwho_config, target_sat, deploy_type + self, default_org, virtwho_config_cli, target_sat, deploy_type_cli ): """Verify "hammer virt-who-config deploy & fetch" @@ -77,20 +48,9 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config['status'] == 'No Report Yet' - if deploy_type == "id": - command = get_configure_command(virtwho_config['id'], default_org.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=default_org.label - ) - elif deploy_type == "script": - script = target_sat.cli.VirtWhoConfig.fetch( - {'id': virtwho_config['id']}, output_format='base' - ) - hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor-type'], debug=True, org=default_org.label - ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ + assert virtwho_config_cli['status'] == 'No Report Yet' + hypervisor_name, guest_name = deploy_type_cli + virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']})[ 'general-information' ]['status'] assert virt_who_instance == 'OK' @@ -116,7 +76,7 @@ def test_positive_deploy_configure_by_id_script( @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, default_org, form_data, virtwho_config, target_sat + self, default_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify hypervisor_id option by hammer virt-who-config update" @@ -131,21 +91,21 @@ def test_positive_hypervisor_id_option( values = ['uuid', 'hostname'] for value in values: target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'hypervisor-id': value} + {'id': virtwho_config_cli['id'], 'hypervisor-id': value} ) - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['connection']['hypervisor-id'] == value - config_file = get_configure_file(virtwho_config['id']) - command = get_configure_command(virtwho_config['id'], default_org.name) + config_file = get_configure_file(virtwho_config_cli['id']) + command = get_configure_command(virtwho_config_cli['id'], default_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=default_org.label + command, form_data_cli['hypervisor-type'], org=default_org.label ) assert get_configure_option('hypervisor_id', config_file) == value @pytest.mark.tier2 @pytest.mark.parametrize('deploy_type', ['id', 'script']) def test_positive_prism_central_deploy_configure_by_id_script( - self, default_org, form_data, target_sat, deploy_type + self, default_org, form_data_cli, target_sat, deploy_type ): """Verify "hammer virt-who-config deploy" on nutanix prism central mode @@ -159,20 +119,20 @@ def test_positive_prism_central_deploy_configure_by_id_script( :CaseImportance: High """ - form_data['prism-flavor'] = "central" - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] + form_data_cli['prism-flavor'] = "central" + virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data_cli)['general-information'] assert virtwho_config['status'] == 'No Report Yet' if deploy_type == "id": command = get_configure_command(virtwho_config['id'], default_org.name) hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=default_org.label + command, form_data_cli['hypervisor-type'], debug=True, org=default_org.label ) elif deploy_type == "script": script = target_sat.cli.VirtWhoConfig.fetch( {'id': virtwho_config['id']}, output_format='base' ) hypervisor_name, guest_name = deploy_configure_by_script( - script, form_data['hypervisor-type'], debug=True, org=default_org.label + script, form_data_cli['hypervisor-type'], debug=True, org=default_org.label ) # Check the option "prism_central=true" should be set in etc/virt-who.d/virt-who.conf config_file = get_configure_file(virtwho_config['id']) @@ -203,7 +163,7 @@ def test_positive_prism_central_deploy_configure_by_id_script( @pytest.mark.tier2 def test_positive_prism_central_prism_central_option( - self, default_org, form_data, virtwho_config, target_sat + self, default_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify prism_central option by hammer virt-who-config update" @@ -217,19 +177,23 @@ def test_positive_prism_central_prism_central_option( """ value = 'central' result = target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'prism-flavor': value} + {'id': virtwho_config_cli['id'], 'prism-flavor': value} + ) + assert ( + result[0]['message'] == f"Virt Who configuration [{virtwho_config_cli['name']}] updated" ) - assert result[0]['message'] == f"Virt Who configuration [{virtwho_config['name']}] updated" - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['general-information']['ahv-prism-flavor'] == value - config_file = get_configure_file(virtwho_config['id']) - command = get_configure_command(virtwho_config['id'], default_org.name) - deploy_configure_by_command(command, form_data['hypervisor-type'], org=default_org.label) + config_file = get_configure_file(virtwho_config_cli['id']) + command = get_configure_command(virtwho_config_cli['id'], default_org.name) + deploy_configure_by_command( + command, form_data_cli['hypervisor-type'], org=default_org.label + ) assert get_configure_option("prism_central", config_file) == 'true' @pytest.mark.tier2 def test_positive_ahv_internal_debug_option( - self, default_org, form_data, virtwho_config, target_sat + self, default_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify ahv_internal_debug option by hammer virt-who-config" @@ -250,14 +214,14 @@ def test_positive_ahv_internal_debug_option( :BZ: 2141719 :customerscenario: true """ - command = get_configure_command(virtwho_config['id'], default_org.name) + command = get_configure_command(virtwho_config_cli['id'], default_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=default_org.label + command, form_data_cli['hypervisor-type'], debug=True, org=default_org.label ) - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['general-information']['enable-ahv-debug'] == 'no' # ahv_internal_debug does not set in virt-who-config-X.conf - config_file = get_configure_file(virtwho_config['id']) + config_file = get_configure_file(virtwho_config_cli['id']) option = 'ahv_internal_debug' env_error = f"option {option} is not exist or not be enabled in {config_file}" with pytest.raises(Exception) as exc_info: # noqa: PT011 - TODO determine better exception @@ -270,18 +234,22 @@ def test_positive_ahv_internal_debug_option( # Update ahv_internal_debug option to true value = 'true' result = target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'ahv-internal-debug': value} + {'id': virtwho_config_cli['id'], 'ahv-internal-debug': value} ) - assert result[0]['message'] == f"Virt Who configuration [{virtwho_config['name']}] updated" - command = get_configure_command(virtwho_config['id'], default_org.name) + assert ( + result[0]['message'] == f"Virt Who configuration [{virtwho_config_cli['name']}] updated" + ) + command = get_configure_command(virtwho_config_cli['id'], default_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=default_org.label + command, form_data_cli['hypervisor-type'], debug=True, org=default_org.label + ) + assert ( + get_hypervisor_ahv_mapping(form_data_cli['hypervisor-type']) == 'Host UUID found for VM' ) - assert get_hypervisor_ahv_mapping(form_data['hypervisor-type']) == 'Host UUID found for VM' - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['general-information']['enable-ahv-debug'] == 'yes' # ahv_internal_debug bas been set to true in virt-who-config-X.conf - config_file = get_configure_file(virtwho_config['id']) + config_file = get_configure_file(virtwho_config_cli['id']) assert get_configure_option("ahv_internal_debug", config_file) == 'true' # check message does not exist in log file /var/log/rhsm/rhsm.log message = 'Value for "ahv_internal_debug" not set, using default: False' diff --git a/tests/foreman/virtwho/cli/test_nutanix_sca.py b/tests/foreman/virtwho/cli/test_nutanix_sca.py index 44876700c23..f41daa8d75c 100644 --- a/tests/foreman/virtwho/cli/test_nutanix_sca.py +++ b/tests/foreman/virtwho/cli/test_nutanix_sca.py @@ -16,10 +16,8 @@ :Upstream: No """ -from fauxfactory import gen_string import pytest -from robottelo.config import settings from robottelo.utils.virtwho import ( deploy_configure_by_command, deploy_configure_by_script, @@ -29,38 +27,11 @@ ) -@pytest.fixture -def form_data(target_sat, module_sca_manifest_org): - sca_form = { - 'name': gen_string('alpha'), - 'debug': 1, - 'interval': '60', - 'hypervisor-id': 'hostname', - 'hypervisor-type': settings.virtwho.ahv.hypervisor_type, - 'hypervisor-server': settings.virtwho.ahv.hypervisor_server, - 'organization-id': module_sca_manifest_org.id, - 'filtering-mode': 'none', - 'satellite-url': target_sat.hostname, - 'hypervisor-username': settings.virtwho.ahv.hypervisor_username, - 'hypervisor-password': settings.virtwho.ahv.hypervisor_password, - 'prism-flavor': settings.virtwho.ahv.prism_flavor, - } - return sca_form - - -@pytest.fixture -def virtwho_config(form_data, target_sat): - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] - yield virtwho_config - target_sat.cli.VirtWhoConfig.delete({'name': virtwho_config['name']}) - assert not target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) - - class TestVirtWhoConfigforNutanix: @pytest.mark.tier2 - @pytest.mark.parametrize('deploy_type', ['id', 'script']) + @pytest.mark.parametrize('deploy_type_cli', ['id', 'script'], indirect=True) def test_positive_deploy_configure_by_id_script( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat, deploy_type + self, module_sca_manifest_org, virtwho_config_cli, target_sat, deploy_type_cli ): """Verify "hammer virt-who-config deploy & fetch" @@ -74,27 +45,15 @@ def test_positive_deploy_configure_by_id_script( :CaseImportance: High """ - assert virtwho_config['status'] == 'No Report Yet' - if deploy_type == "id": - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) - deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=module_sca_manifest_org.label - ) - elif deploy_type == "script": - script = target_sat.cli.VirtWhoConfig.fetch( - {'id': virtwho_config['id']}, output_format='base' - ) - deploy_configure_by_script( - script, form_data['hypervisor-type'], debug=True, org=module_sca_manifest_org.label - ) - virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']})[ + assert virtwho_config_cli['status'] == 'No Report Yet' + virt_who_instance = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']})[ 'general-information' ]['status'] assert virt_who_instance == 'OK' @pytest.mark.tier2 def test_positive_hypervisor_id_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify hypervisor_id option by hammer virt-who-config update" @@ -108,21 +67,21 @@ def test_positive_hypervisor_id_option( """ for value in ['uuid', 'hostname']: target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'hypervisor-id': value} + {'id': virtwho_config_cli['id'], 'hypervisor-id': value} ) - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['connection']['hypervisor-id'] == value - config_file = get_configure_file(virtwho_config['id']) - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + config_file = get_configure_file(virtwho_config_cli['id']) + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=module_sca_manifest_org.label + command, form_data_cli['hypervisor-type'], org=module_sca_manifest_org.label ) assert get_configure_option('hypervisor_id', config_file) == value @pytest.mark.tier2 @pytest.mark.parametrize('deploy_type', ['id', 'script']) def test_positive_prism_central_deploy_configure_by_id_script( - self, module_sca_manifest_org, form_data, target_sat, deploy_type + self, module_sca_manifest_org, target_sat, form_data_cli, deploy_type ): """Verify "hammer virt-who-config deploy & fetch" on nutanix prism central mode @@ -137,20 +96,26 @@ def test_positive_prism_central_deploy_configure_by_id_script( :CaseImportance: High """ - form_data['prism-flavor'] = "central" - virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data)['general-information'] + form_data_cli['prism-flavor'] = "central" + virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data_cli)['general-information'] assert virtwho_config['status'] == 'No Report Yet' if deploy_type == "id": command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], debug=True, org=module_sca_manifest_org.label + command, + form_data_cli['hypervisor-type'], + debug=True, + org=module_sca_manifest_org.label, ) elif deploy_type == "script": script = target_sat.cli.VirtWhoConfig.fetch( {'id': virtwho_config['id']}, output_format='base' ) deploy_configure_by_script( - script, form_data['hypervisor-type'], debug=True, org=module_sca_manifest_org.label + script, + form_data_cli['hypervisor-type'], + debug=True, + org=module_sca_manifest_org.label, ) # Check the option "prism_central=true" should be set in etc/virt-who.d/virt-who.conf config_file = get_configure_file(virtwho_config['id']) @@ -162,7 +127,7 @@ def test_positive_prism_central_deploy_configure_by_id_script( @pytest.mark.tier2 def test_positive_prism_element_prism_central_option( - self, module_sca_manifest_org, form_data, virtwho_config, target_sat + self, module_sca_manifest_org, form_data_cli, virtwho_config_cli, target_sat ): """Verify prism_central option by hammer virt-who-config update" @@ -176,14 +141,16 @@ def test_positive_prism_element_prism_central_option( """ value = 'central' result = target_sat.cli.VirtWhoConfig.update( - {'id': virtwho_config['id'], 'prism-flavor': value} + {'id': virtwho_config_cli['id'], 'prism-flavor': value} ) - assert result[0]['message'] == f"Virt Who configuration [{virtwho_config['name']}] updated" - result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config['id']}) + assert ( + result[0]['message'] == f"Virt Who configuration [{virtwho_config_cli['name']}] updated" + ) + result = target_sat.cli.VirtWhoConfig.info({'id': virtwho_config_cli['id']}) assert result['general-information']['ahv-prism-flavor'] == value - config_file = get_configure_file(virtwho_config['id']) - command = get_configure_command(virtwho_config['id'], module_sca_manifest_org.name) + config_file = get_configure_file(virtwho_config_cli['id']) + command = get_configure_command(virtwho_config_cli['id'], module_sca_manifest_org.name) deploy_configure_by_command( - command, form_data['hypervisor-type'], org=module_sca_manifest_org.label + command, form_data_cli['hypervisor-type'], org=module_sca_manifest_org.label ) assert get_configure_option("prism_central", config_file) == 'true' diff --git a/tests/foreman/virtwho/ui/test_esx.py b/tests/foreman/virtwho/ui/test_esx.py index 75652da9bd6..7c669a62022 100644 --- a/tests/foreman/virtwho/ui/test_esx.py +++ b/tests/foreman/virtwho/ui/test_esx.py @@ -41,7 +41,7 @@ ) -@pytest.mark.delete_host +@pytest.mark.usefixtures('delete_host') class TestVirtwhoConfigforEsx: @pytest.mark.tier2 @pytest.mark.parametrize('deploy_type_ui', ['id', 'script'], indirect=True) diff --git a/tests/foreman/virtwho/ui/test_esx_sca.py b/tests/foreman/virtwho/ui/test_esx_sca.py index 63c4c55a16b..068b8c9c84c 100644 --- a/tests/foreman/virtwho/ui/test_esx_sca.py +++ b/tests/foreman/virtwho/ui/test_esx_sca.py @@ -38,7 +38,7 @@ ) -@pytest.mark.delete_host +@pytest.mark.usefixtures('delete_host') class TestVirtwhoConfigforEsx: @pytest.mark.tier2 @pytest.mark.upgrade From 842389ab77d9faff29a2b1c85df53c43a082cfc8 Mon Sep 17 00:00:00 2001 From: Jitendra Yejare Date: Thu, 30 Nov 2023 20:41:43 +0530 Subject: [PATCH 349/586] [6.14.z] Remove cli.factory dependencies from the repo (#11544) (#13224) * Remove cli.factory dependencies from the repo (#11544) Remove cli_factory * Add default content type as yum for make_repository helper (#13196) Signed-off-by: Gaurav Talreja --------- Signed-off-by: Gaurav Talreja Co-authored-by: Shweta Singh Co-authored-by: Gaurav Talreja --- pytest_fixtures/component/host.py | 5 +- pytest_fixtures/component/maintain.py | 2 +- pytest_fixtures/component/oscap.py | 7 +- robottelo/cli/base.py | 52 +- robottelo/cli/factory.py | 2261 ----------------- robottelo/cli/report_template.py | 3 +- robottelo/content_info.py | 2 +- robottelo/exceptions.py | 53 + robottelo/host_helpers/cli_factory.py | 7 +- robottelo/host_helpers/satellite_mixins.py | 2 +- robottelo/hosts.py | 28 +- tests/foreman/api/test_errata.py | 24 +- tests/foreman/api/test_repositories.py | 2 +- tests/foreman/api/test_role.py | 3 +- tests/foreman/api/test_subscription.py | 3 +- tests/foreman/cli/test_acs.py | 2 +- tests/foreman/cli/test_activationkey.py | 486 ++-- tests/foreman/cli/test_architecture.py | 36 +- tests/foreman/cli/test_auth.py | 117 +- tests/foreman/cli/test_capsule.py | 8 +- tests/foreman/cli/test_classparameters.py | 8 +- .../cli/test_computeresource_azurerm.py | 12 +- tests/foreman/cli/test_computeresource_ec2.py | 14 +- .../cli/test_computeresource_libvirt.py | 92 +- tests/foreman/cli/test_computeresource_osp.py | 2 +- .../foreman/cli/test_computeresource_rhev.py | 76 +- .../foreman/cli/test_container_management.py | 77 +- tests/foreman/cli/test_contentaccess.py | 12 +- tests/foreman/cli/test_contentcredentials.py | 3 +- tests/foreman/cli/test_contentview.py | 1953 ++++++++------ tests/foreman/cli/test_contentviewfilter.py | 313 ++- tests/foreman/cli/test_discoveryrule.py | 13 +- tests/foreman/cli/test_docker.py | 678 +++-- tests/foreman/cli/test_domain.py | 54 +- tests/foreman/cli/test_environment.py | 2 +- tests/foreman/cli/test_errata.py | 248 +- tests/foreman/cli/test_fact.py | 16 +- tests/foreman/cli/test_filter.py | 97 +- tests/foreman/cli/test_globalparam.py | 12 +- tests/foreman/cli/test_host.py | 409 +-- tests/foreman/cli/test_hostcollection.py | 155 +- tests/foreman/cli/test_hostgroup.py | 119 +- tests/foreman/cli/test_http_proxy.py | 40 +- tests/foreman/cli/test_jobtemplate.py | 38 +- tests/foreman/cli/test_ldapauthsource.py | 141 +- .../foreman/cli/test_lifecycleenvironment.py | 83 +- tests/foreman/cli/test_location.py | 182 +- tests/foreman/cli/test_logging.py | 11 +- tests/foreman/cli/test_medium.py | 46 +- tests/foreman/cli/test_model.py | 38 +- tests/foreman/cli/test_operatingsystem.py | 16 +- tests/foreman/cli/test_organization.py | 310 +-- tests/foreman/cli/test_oscap.py | 241 +- .../foreman/cli/test_oscap_tailoringfiles.py | 58 +- tests/foreman/cli/test_ostreebranch.py | 63 +- tests/foreman/cli/test_partitiontable.py | 72 +- tests/foreman/cli/test_product.py | 94 +- .../foreman/cli/test_provisioningtemplate.py | 2 +- tests/foreman/cli/test_realm.py | 40 +- tests/foreman/cli/test_remoteexecution.py | 282 +- tests/foreman/cli/test_report.py | 2 +- tests/foreman/cli/test_reporttemplates.py | 323 +-- tests/foreman/cli/test_repository.py | 703 ++--- tests/foreman/cli/test_repository_set.py | 72 +- tests/foreman/cli/test_role.py | 151 +- tests/foreman/cli/test_satellitesync.py | 193 +- tests/foreman/cli/test_settings.py | 107 +- tests/foreman/cli/test_subnet.py | 44 +- tests/foreman/cli/test_subscription.py | 87 +- tests/foreman/cli/test_syncplan.py | 196 +- tests/foreman/cli/test_templatesync.py | 26 +- tests/foreman/cli/test_user.py | 188 +- tests/foreman/cli/test_usergroup.py | 119 +- .../cli/test_vm_install_products_package.py | 7 +- tests/foreman/cli/test_webhook.py | 41 +- .../destructive/test_ldap_authentication.py | 10 +- .../destructive/test_ldapauthsource.py | 2 +- tests/foreman/destructive/test_realm.py | 14 +- .../destructive/test_remoteexecution.py | 2 +- tests/foreman/endtoend/test_cli_endtoend.py | 96 +- tests/foreman/longrun/test_oscap.py | 65 +- tests/foreman/ui/test_activationkey.py | 5 +- tests/foreman/ui/test_contenthost.py | 8 +- tests/foreman/ui/test_host.py | 4 +- tests/foreman/ui/test_settings.py | 5 +- tests/foreman/ui/test_subscription.py | 3 +- tests/foreman/virtwho/cli/test_esx.py | 3 +- tests/foreman/virtwho/cli/test_esx_sca.py | 3 +- tests/robottelo/test_cli.py | 4 +- tests/upgrades/test_usergroup.py | 6 +- tests/upgrades/test_virtwho.py | 18 +- 91 files changed, 5234 insertions(+), 6498 deletions(-) delete mode 100644 robottelo/cli/factory.py diff --git a/pytest_fixtures/component/host.py b/pytest_fixtures/component/host.py index 4f070a86a64..11645f89f84 100644 --- a/pytest_fixtures/component/host.py +++ b/pytest_fixtures/component/host.py @@ -3,7 +3,6 @@ from nailgun import entities import pytest -from robottelo.cli.factory import setup_org_for_a_rh_repo from robottelo.constants import DEFAULT_CV, ENVIRONMENT, PRDS, REPOS, REPOSET @@ -24,7 +23,7 @@ def module_model(): @pytest.mark.skip_if_not_set('clients', 'fake_manifest') @pytest.fixture(scope="module") -def setup_rhst_repo(): +def setup_rhst_repo(module_target_sat): """Prepare Satellite tools repository for usage in specified organization""" org = entities.Organization().create() cv = entities.ContentView(organization=org).create() @@ -34,7 +33,7 @@ def setup_rhst_repo(): organization=org, ).create() repo_name = 'rhst7' - setup_org_for_a_rh_repo( + module_target_sat.cli_factory.setup_org_for_a_rh_repo( { 'product': PRDS['rhel'], 'repository-set': REPOSET[repo_name], diff --git a/pytest_fixtures/component/maintain.py b/pytest_fixtures/component/maintain.py index 844f522de66..68aa82f5c86 100644 --- a/pytest_fixtures/component/maintain.py +++ b/pytest_fixtures/component/maintain.py @@ -111,7 +111,7 @@ def setup_sync_plan(request, sat_maintain): """ org = sat_maintain.api.Organization().create() # Setup sync-plan - new_sync_plan = sat_maintain.cli_factory.make_sync_plan( + new_sync_plan = sat_maintain.cli_factory.sync_plan( { 'enabled': 'true', 'interval': 'weekly', diff --git a/pytest_fixtures/component/oscap.py b/pytest_fixtures/component/oscap.py index 356050f94a7..e8a7d230603 100644 --- a/pytest_fixtures/component/oscap.py +++ b/pytest_fixtures/component/oscap.py @@ -4,7 +4,6 @@ from nailgun import entities import pytest -from robottelo.cli.factory import make_scapcontent from robottelo.config import robottelo_tmp_dir, settings from robottelo.constants import OSCAP_PROFILE, OSCAP_TAILORING_FILE, DataFile @@ -29,9 +28,11 @@ def oscap_content_path(session_target_sat): @pytest.fixture(scope="module") -def scap_content(import_ansible_roles): +def scap_content(import_ansible_roles, module_target_sat): title = f"rhel-content-{gen_string('alpha')}" - scap_info = make_scapcontent({'title': title, 'scap-file': f'{settings.oscap.content_path}'}) + scap_info = module_target_sat.cli_factory.scapcontent( + {'title': title, 'scap-file': f'{settings.oscap.content_path}'} + ) scap_id = scap_info['id'] scap_info = entities.ScapContents(id=scap_id).read() diff --git a/robottelo/cli/base.py b/robottelo/cli/base.py index d5ce2ae224f..2d3d7974696 100644 --- a/robottelo/cli/base.py +++ b/robottelo/cli/base.py @@ -6,59 +6,11 @@ from robottelo import ssh from robottelo.cli import hammer from robottelo.config import settings +from robottelo.exceptions import CLIDataBaseError, CLIError, CLIReturnCodeError from robottelo.logging import logger from robottelo.utils.ssh import get_client -class CLIError(Exception): - """Indicates that a CLI command could not be run.""" - - -class CLIBaseError(Exception): - """Indicates that a CLI command has finished with return code different - from zero. - - :param status: CLI command return code - :param stderr: contents of the ``stderr`` - :param msg: explanation of the error - - """ - - def __init__(self, status, stderr, msg): - self.status = status - self.stderr = stderr - self.msg = msg - super().__init__(msg) - self.message = msg - - def __str__(self): - """Include class name, status, stderr and msg to string repr so - assertRaisesRegexp can be used to assert error present on any - attribute - """ - return repr(self) - - def __repr__(self): - """Include class name status, stderr and msg to improve logging""" - return '{}(status={!r}, stderr={!r}, msg={!r}'.format( - type(self).__name__, self.status, self.stderr, self.msg - ) - - -class CLIReturnCodeError(CLIBaseError): - """Error to be raised when an error occurs due to some validation error - when execution hammer cli. - See: https://github.com/SatelliteQE/robottelo/issues/3790 for more details - """ - - -class CLIDataBaseError(CLIBaseError): - """Error to be raised when an error occurs due to some missing parameter - which cause a data base error on hammer - See: https://github.com/SatelliteQE/robottelo/issues/3790 for more details - """ - - class Base: """Base class for hammer CLI interaction @@ -84,7 +36,7 @@ def _handle_response(cls, response, ignore_stderr=None): :param ignore_stderr: indicates whether to throw a warning in logs if ``stderr`` is not empty. :returns: contents of ``stdout``. - :raises robottelo.cli.base.CLIReturnCodeError: If return code is + :raises robottelo.exceptions.CLIReturnCodeError: If return code is different from zero. """ if isinstance(response.stderr, tuple): diff --git a/robottelo/cli/factory.py b/robottelo/cli/factory.py deleted file mode 100644 index c1ac9cb4268..00000000000 --- a/robottelo/cli/factory.py +++ /dev/null @@ -1,2261 +0,0 @@ -""" -Factory object creation for all CLI methods -""" -import datetime -import os -from os import chmod -import pprint -import random -from tempfile import mkstemp -from time import sleep - -from fauxfactory import ( - gen_alphanumeric, - gen_choice, - gen_integer, - gen_ipaddr, - gen_mac, - gen_netmask, - gen_string, - gen_url, -) - -from robottelo import constants -from robottelo.cli.activationkey import ActivationKey -from robottelo.cli.architecture import Architecture -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.computeresource import ComputeResource -from robottelo.cli.content_credentials import ContentCredential -from robottelo.cli.contentview import ( - ContentView, - ContentViewFilter, - ContentViewFilterRule, -) -from robottelo.cli.discoveryrule import DiscoveryRule -from robottelo.cli.domain import Domain -from robottelo.cli.environment import Environment -from robottelo.cli.filter import Filter -from robottelo.cli.host import Host -from robottelo.cli.hostcollection import HostCollection -from robottelo.cli.hostgroup import HostGroup -from robottelo.cli.http_proxy import HttpProxy -from robottelo.cli.job_invocation import JobInvocation -from robottelo.cli.job_template import JobTemplate -from robottelo.cli.ldapauthsource import LDAPAuthSource -from robottelo.cli.lifecycleenvironment import LifecycleEnvironment -from robottelo.cli.location import Location -from robottelo.cli.medium import Medium -from robottelo.cli.model import Model -from robottelo.cli.operatingsys import OperatingSys -from robottelo.cli.org import Org -from robottelo.cli.partitiontable import PartitionTable -from robottelo.cli.product import Product -from robottelo.cli.realm import Realm -from robottelo.cli.report_template import ReportTemplate -from robottelo.cli.repository import Repository -from robottelo.cli.repository_set import RepositorySet -from robottelo.cli.role import Role -from robottelo.cli.scap_policy import Scappolicy -from robottelo.cli.scap_tailoring_files import TailoringFiles -from robottelo.cli.scapcontent import Scapcontent -from robottelo.cli.subnet import Subnet -from robottelo.cli.subscription import Subscription -from robottelo.cli.syncplan import SyncPlan -from robottelo.cli.template import Template -from robottelo.cli.template_input import TemplateInput -from robottelo.cli.user import User -from robottelo.cli.usergroup import UserGroup, UserGroupExternal -from robottelo.cli.virt_who_config import VirtWhoConfig -from robottelo.config import settings -from robottelo.logging import logger -from robottelo.utils import ssh -from robottelo.utils.datafactory import valid_cron_expressions -from robottelo.utils.decorators import cacheable -from robottelo.utils.manifest import clone - -ORG_KEYS = ['organization', 'organization-id', 'organization-label'] -CONTENT_VIEW_KEYS = ['content-view', 'content-view-id'] -LIFECYCLE_KEYS = ['lifecycle-environment', 'lifecycle-environment-id'] - - -class CLIFactoryError(Exception): - """Indicates an error occurred while creating an entity using hammer""" - - -def create_object(cli_object, options, values): - """ - Creates with dictionary of arguments. - - :param cli_object: A valid CLI object. - :param dict options: The default options accepted by the cli_object - create - :param dict values: Custom values to override default ones. - :raise robottelo.cli.factory.CLIFactoryError: Raise an exception if object - cannot be created. - :rtype: dict - :return: A dictionary representing the newly created resource. - - """ - if values: - diff = set(values.keys()).difference(set(options.keys())) - if diff: - logger.debug( - "Option(s) {} not supported by CLI factory. Please check for " - "a typo or update default options".format(diff) - ) - for key in set(options.keys()).intersection(set(values.keys())): - options[key] = values[key] - - try: - result = cli_object.create(options) - except CLIReturnCodeError as err: - # If the object is not created, raise exception, stop the show. - raise CLIFactoryError( - 'Failed to create {} with data:\n{}\n{}'.format( - cli_object.__name__, pprint.pformat(options, indent=2), err.msg - ) - ) - - # Sometimes we get a list with a dictionary and not - # a dictionary. - if type(result) is list and len(result) > 0: - result = result[0] - - return result - - -def _entity_with_credentials(credentials, cli_entity_cls): - """Create entity class using credentials. If credentials is None will - return cli_entity_cls itself - - :param credentials: tuple (login, password) - :param cli_entity_cls: Cli Entity Class - :return: Cli Entity Class - """ - if credentials is not None: - cli_entity_cls = cli_entity_cls.with_user(*credentials) - return cli_entity_cls - - -@cacheable -def make_activation_key(options=None): - """Creates an Activation Key - - :param options: Check options using `hammer activation-key create --help` on satellite. - - :returns ActivationKey object - """ - # Organization Name, Label or ID is a required field. - if ( - not options - or not options.get('organization') - and not options.get('organization-label') - and not options.get('organization-id') - ): - raise CLIFactoryError('Please provide a valid Organization.') - - args = { - 'content-view': None, - 'content-view-id': None, - 'description': None, - 'lifecycle-environment': None, - 'lifecycle-environment-id': None, - 'max-hosts': None, - 'name': gen_alphanumeric(), - 'organization': None, - 'organization-id': None, - 'organization-label': None, - 'unlimited-hosts': None, - 'service-level': None, - 'purpose-role': None, - 'purpose-usage': None, - 'purpose-addons': None, - } - - return create_object(ActivationKey, args, options) - - -@cacheable -def make_architecture(options=None): - """Creates an Architecture - - :param options: Check options using `hammer architecture create --help` on satellite. - - :returns Architecture object - """ - args = {'name': gen_alphanumeric(), 'operatingsystem-ids': None} - - return create_object(Architecture, args, options) - - -@cacheable -def make_content_view(options=None): - """Creates a Content View - - :param options: Check options using `hammer content-view create --help` on satellite. - - :returns ContentView object - """ - return make_content_view_with_credentials(options) - - -def make_content_view_with_credentials(options=None, credentials=None): - """Helper function to create CV with credentials - - If credentials is None, the default credentials in robottelo.properties will be used. - """ - # Organization ID is a required field. - if not options or not options.get('organization-id'): - raise CLIFactoryError('Please provide a valid ORG ID.') - args = { - 'component-ids': None, - 'composite': False, - 'description': None, - 'import-only': False, - 'label': None, - 'name': gen_string('alpha', 10), - 'organization': None, - 'organization-id': None, - 'organization-label': None, - 'product': None, - 'product-id': None, - 'repositories': None, - 'repository-ids': None, - } - - cv_cls = _entity_with_credentials(credentials, ContentView) - return create_object(cv_cls, args, options) - - -@cacheable -def make_content_view_filter(options=None): - """Creates a Content View Filter - - :param options: Check options using `hammer content-view filter create --help` on satellite. - - :returns ContentViewFilter object - """ - - args = { - 'content-view': None, - 'content-view-id': None, - 'description': None, - 'inclusion': None, - 'name': gen_string('alpha', 10), - 'organization': None, - 'organization-id': None, - 'organization-label': None, - 'original-packages': None, - 'repositories': None, - 'repository-ids': None, - 'type': None, - } - - return create_object(ContentViewFilter, args, options) - - -@cacheable -def make_content_view_filter_rule(options=None): - """Creates a Content View Filter Rule - - :param options: Check options using `hammer content-view filter rule create --help` on - satellite. - - :returns ContentViewFilterRule object - """ - - args = { - 'content-view': None, - 'content-view-filter': None, - 'content-view-filter-id': None, - 'content-view-id': None, - 'date-type': None, - 'end-date': None, - 'errata-id': None, - 'errata-ids': None, - 'max-version': None, - 'min-version': None, - 'name': None, - 'names': None, - 'start-date': None, - 'types': None, - 'version': None, - } - - return create_object(ContentViewFilterRule, args, options) - - -@cacheable -def make_discoveryrule(options=None): - """Creates a Discovery Rule - - :param options: Check options using `hammer discovery-rule create --help` on satellite. - - :returns DiscoveryRule object - """ - - # Organizations, Locations, search query, hostgroup are required fields. - if not options: - raise CLIFactoryError('Please provide required parameters') - # Organizations fields is required - if not any(options.get(key) for key in ['organizations', 'organization-ids']): - raise CLIFactoryError('Please provide a valid organization field.') - # Locations field is required - if not any(options.get(key) for key in ['locations', 'location-ids']): - raise CLIFactoryError('Please provide a valid location field.') - # search query is required - if not options.get('search'): - raise CLIFactoryError('Please provider a valid search query') - # hostgroup is required - if not any(options.get(key) for key in ['hostgroup', 'hostgroup-id']): - raise CLIFactoryError('Please provider a valid hostgroup') - - args = { - 'enabled': None, - 'hostgroup': None, - 'hostgroup-id': None, - 'hostgroup-title': None, - 'hostname': None, - 'hosts-limit': None, - 'location-ids': None, - 'locations': None, - 'max-count': None, - 'name': gen_alphanumeric(), - 'organizations': None, - 'organization-ids': None, - 'priority': None, - 'search': None, - } - - return create_object(DiscoveryRule, args, options) - - -@cacheable -def make_content_credential(options=None): - """Creates a content credential. - - In Satellite 6.8, only gpg_key option is supported. - - :param options: Check options using `hammer content-credential create --help` on satellite. - - :returns ContentCredential object - """ - # Organization ID is a required field. - if not options or not options.get('organization-id'): - raise CLIFactoryError('Please provide a valid ORG ID.') - - # Create a fake gpg key file if none was provided - if not options.get('path'): - (_, key_filename) = mkstemp(text=True) - os.chmod(key_filename, 0o700) - with open(key_filename, 'w') as gpg_key_file: - gpg_key_file.write(gen_alphanumeric(gen_integer(20, 50))) - else: - # If the key is provided get its local path and remove it from options - # to not override the remote path - key_filename = options.pop('path') - - args = { - 'path': f'/tmp/{gen_alphanumeric()}', - 'content-type': 'gpg_key', - 'name': gen_alphanumeric(), - 'organization': None, - 'organization-id': None, - 'organization-label': None, - } - - # Upload file to server - ssh.get_client().put(key_filename, args['path']) - - return create_object(ContentCredential, args, options) - - -@cacheable -def make_location(options=None): - """Creates a Location - - :param options: Check options using `hammer location create --help` on satellite. - - :returns Location object - """ - args = { - 'compute-resource-ids': None, - 'compute-resources': None, - 'description': None, - 'domain-ids': None, - 'domains': None, - 'environment-ids': None, - 'environments': None, - 'puppet-environment-ids': None, - 'puppet-environments': None, - 'hostgroup-ids': None, - 'hostgroups': None, - 'medium-ids': None, - 'name': gen_alphanumeric(), - 'parent-id': None, - 'provisioning-template-ids': None, - 'provisioning-templates': None, - 'realm-ids': None, - 'realms': None, - 'smart-proxy-ids': None, - 'smart-proxies': None, - 'subnet-ids': None, - 'subnets': None, - 'user-ids': None, - 'users': None, - } - - return create_object(Location, args, options) - - -@cacheable -def make_model(options=None): - """Creates a Hardware Model - - :param options: Check options using `hammer model create --help` on satellite. - - :returns Model object - """ - args = {'hardware-model': None, 'info': None, 'name': gen_alphanumeric(), 'vendor-class': None} - - return create_object(Model, args, options) - - -@cacheable -def make_partition_table(options=None): - """Creates a Partition Table - - :param options: Check options using `hammer partition-table create --help` on satellite. - - :returns PartitionTable object - """ - if options is None: - options = {} - (_, layout) = mkstemp(text=True) - os.chmod(layout, 0o700) - with open(layout, 'w') as ptable: - ptable.write(options.get('content', 'default ptable content')) - - args = { - 'file': f'/tmp/{gen_alphanumeric()}', - 'location-ids': None, - 'locations': None, - 'name': gen_alphanumeric(), - 'operatingsystem-ids': None, - 'operatingsystems': None, - 'organization-ids': None, - 'organizations': None, - 'os-family': random.choice(constants.OPERATING_SYSTEMS), - } - - # Upload file to server - ssh.get_client().put(layout, args['file']) - - return create_object(PartitionTable, args, options) - - -@cacheable -def make_product(options=None): - """Creates a Product - - :param options: Check options using `hammer product create --help` on satellite. - - :returns Product object - """ - return make_product_with_credentials(options) - - -def make_product_with_credentials(options=None, credentials=None): - """Helper function to create product with credentials""" - # Organization ID is a required field. - if not options or not options.get('organization-id'): - raise CLIFactoryError('Please provide a valid ORG ID.') - - args = { - 'description': gen_string('alpha', 20), - 'gpg-key': None, - 'gpg-key-id': None, - 'label': gen_string('alpha', 20), - 'name': gen_string('alpha', 20), - 'organization': None, - 'organization-id': None, - 'organization-label': None, - 'sync-plan': None, - 'sync-plan-id': None, - } - product_cls = _entity_with_credentials(credentials, Product) - return create_object(product_cls, args, options) - - -def make_product_wait(options=None, wait_for=5): - """Wrapper function for make_product to make it wait before erroring out. - - This is a temporary workaround for BZ#1332650: Sometimes cli product - create errors for no reason when there are multiple product creation - requests at the sametime although the product entities are created. This - workaround will attempt to wait for 5 seconds and query the - product again to make sure it is actually created. If it is not found, - it will fail and stop. - - Note: This wrapper method is created instead of patching make_product - because this issue does not happen for all entities and this workaround - should be removed once the root cause is identified/fixed. - """ - # Organization ID is a required field. - if not options or not options.get('organization-id'): - raise CLIFactoryError('Please provide a valid ORG ID.') - options['name'] = options.get('name', gen_string('alpha')) - try: - product = make_product(options) - except CLIFactoryError as err: - sleep(wait_for) - try: - product = Product.info( - {'name': options.get('name'), 'organization-id': options.get('organization-id')} - ) - except CLIReturnCodeError: - raise err - if not product: - raise err - return product - - -@cacheable -def make_repository(options=None): - """Creates a Repository - - :param options: Check options using `hammer repository create --help` on satellite. - - :returns Repository object - """ - return make_repository_with_credentials(options) - - -def make_repository_with_credentials(options=None, credentials=None): - """Helper function to create Repository with credentials""" - # Product ID is a required field. - if not options or not options.get('product-id'): - raise CLIFactoryError('Please provide a valid Product ID.') - - args = { - 'checksum-type': None, - 'content-type': 'yum', - 'include-tags': None, - 'docker-upstream-name': None, - 'download-policy': None, - 'gpg-key': None, - 'gpg-key-id': None, - 'ignorable-content': None, - 'label': None, - 'mirroring-policy': None, - 'name': gen_string('alpha', 15), - 'organization': None, - 'organization-id': None, - 'organization-label': None, - 'product': None, - 'product-id': None, - 'publish-via-http': 'true', - 'http-proxy': None, - 'http-proxy-id': None, - 'http-proxy-policy': None, - 'ansible-collection-requirements': None, - 'ansible-collection-requirements-file': None, - 'ansible-collection-auth-token': None, - 'ansible-collection-auth-url': None, - 'url': settings.repos.yum_1.url, - 'upstream-username': None, - 'upstream-password': None, - } - repo_cls = _entity_with_credentials(credentials, Repository) - return create_object(repo_cls, args, options) - - -@cacheable -def make_role(options=None): - """Creates a Role - - :param options: Check options using `hammer role create --help` on satellite. - - :returns Role object - """ - # Assigning default values for attributes - args = {'name': gen_alphanumeric(6)} - - return create_object(Role, args, options) - - -@cacheable -def make_filter(options=None): - """Creates a Role Filter - - :param options: Check options using `hammer filter create --help` on satellite. - - :returns Role object - """ - args = { - 'location-ids': None, - 'locations': None, - 'organization-ids': None, - 'organizations': None, - 'override': None, - 'permission-ids': None, - 'permissions': None, - 'role': None, - 'role-id': None, - 'search': None, - } - - # Role and permissions are required fields. - if not options: - raise CLIFactoryError('Please provide required parameters') - - # Do we have at least one role field? - if not any(options.get(key) for key in ['role', 'role-id']): - raise CLIFactoryError('Please provide a valid role field.') - - # Do we have at least one permissions field? - if not any(options.get(key) for key in ['permissions', 'permission-ids']): - raise CLIFactoryError('Please provide a valid permissions field.') - - return create_object(Filter, args, options) - - -@cacheable -def make_scap_policy(options=None): - """Creates a Scap Policy - - :param options: Check options using `hammer policy create --help` on satellite. - - :returns Scappolicy object - """ - # Assigning default values for attributes - # SCAP ID and SCAP profile ID is a required field. - if ( - not options - and not options.get('scap-content-id') - and not options.get('scap-content-profile-id') - and not options.get('period') - and not options.get('deploy-by') - ): - raise CLIFactoryError( - 'Please provide a valid SCAP ID or SCAP Profile ID or Period or Deploy by option' - ) - args = { - 'description': None, - 'scap-content-id': None, - 'scap-content-profile-id': None, - 'deploy-by': None, - 'period': None, - 'weekday': None, - 'day-of-month': None, - 'cron-line': None, - 'hostgroup-ids': None, - 'hostgroups': None, - 'locations': None, - 'organizations': None, - 'tailoring-file': None, - 'tailoring-file-id': None, - 'tailoring-file-profile-id': None, - 'location-ids': None, - 'name': gen_alphanumeric().lower(), - 'organization-ids': None, - } - - return create_object(Scappolicy, args, options) - - -@cacheable -def make_subnet(options=None): - """Creates a Subnet - - :param options: Check options using `hammer subnet create --help` on satellite. - - :returns Subnet object - """ - args = { - 'boot-mode': None, - 'dhcp-id': None, - 'dns-id': None, - 'dns-primary': None, - 'dns-secondary': None, - 'domain-ids': None, - 'domains': None, - 'from': None, - 'gateway': None, - 'ipam': None, - 'location-ids': None, - 'locations': None, - 'mask': gen_netmask(), - 'name': gen_alphanumeric(8), - 'network': gen_ipaddr(ip3=True), - 'organization-ids': None, - 'organizations': None, - 'tftp-id': None, - 'to': None, - 'vlanid': None, - } - - return create_object(Subnet, args, options) - - -@cacheable -def make_sync_plan(options=None): - """Creates a Sync Plan - - :param options: Check options using `hammer sync-plan create --help` on satellite. - - :returns SyncPlan object - """ - # Organization ID is a required field. - if not options or not options.get('organization-id'): - raise CLIFactoryError('Please provide a valid ORG ID.') - - args = { - 'description': gen_string('alpha', 20), - 'enabled': 'true', - 'interval': random.choice(list(constants.SYNC_INTERVAL.values())), - 'name': gen_string('alpha', 20), - 'organization': None, - 'organization-id': None, - 'organization-label': None, - 'sync-date': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'cron-expression': None, - } - if options.get('interval', args['interval']) == constants.SYNC_INTERVAL[ - 'custom' - ] and not options.get('cron-expression'): - args['cron-expression'] = gen_choice(valid_cron_expressions()) - return create_object(SyncPlan, args, options) - - -@cacheable -def make_host(options=None): - """Creates a Host - - :param options: Check options using `hammer host create --help` on satellite. - - :returns Host object - """ - args = { - 'architecture': None, - 'architecture-id': None, - 'ask-root-password': None, - 'autoheal': None, - 'build': None, - 'comment': None, - 'compute-attributes': None, - 'compute-profile': None, - 'compute-profile-id': None, - 'compute-resource': None, - 'compute-resource-id': None, - 'content-source-id': None, - 'content-view': None, - 'content-view-id': None, - 'domain': None, - 'domain-id': None, - 'enabled': None, - 'environment': None, - 'environment-id': None, - 'hostgroup': None, - 'hostgroup-id': None, - 'hostgroup-title': None, - 'hypervisor-guest-uuids': None, - 'image': None, - 'image-id': None, - 'interface': None, - 'ip': gen_ipaddr(), - 'kickstart-repository-id': None, - 'lifecycle-environment': None, - 'lifecycle-environment-id': None, - 'location': None, - 'location-id': None, - 'mac': gen_mac(multicast=False), - 'managed': None, - 'medium': None, - 'medium-id': None, - 'model': None, - 'model-id': None, - 'name': gen_string('alpha', 10), - 'operatingsystem': None, - 'operatingsystem-id': None, - 'openscap-proxy-id': None, - 'organization': None, - 'organization-id': None, - 'overwrite': None, - 'owner': None, - 'owner-id': None, - 'owner-type': None, - 'parameters': None, - 'partition-table': None, - 'partition-table-id': None, - 'progress-report-id': None, - 'provision-method': None, - 'puppet-ca-proxy': None, - 'puppet-ca-proxy-id': None, - 'puppet-class-ids': None, - 'puppet-classes': None, - 'puppet-proxy': None, - 'puppet-proxy-id': None, - 'pxe-loader': None, - 'realm': None, - 'realm-id': None, - 'root-password': gen_string('alpha', 8), - 'service-level': None, - 'subnet': None, - 'subnet-id': None, - 'volume': None, - } - - return create_object(Host, args, options) - - -@cacheable -def make_fake_host(options=None): - """Wrapper function for make_host to pass all required options for creation - of a fake host - """ - if options is None: - options = {} - - # Try to use default Satellite entities, otherwise create them if they were - # not passed or defined previously - if not options.get('organization') and not options.get('organization-id'): - try: - options['organization-id'] = Org.info({'name': constants.DEFAULT_ORG})['id'] - except CLIReturnCodeError: - options['organization-id'] = make_org()['id'] - if not options.get('location') and not options.get('location-id'): - try: - options['location-id'] = Location.info({'name': constants.DEFAULT_LOC})['id'] - except CLIReturnCodeError: - options['location-id'] = make_location()['id'] - if not options.get('domain') and not options.get('domain-id'): - options['domain-id'] = make_domain( - { - 'location-ids': options.get('location-id'), - 'locations': options.get('location'), - 'organization-ids': options.get('organization-id'), - 'organizations': options.get('organization'), - } - )['id'] - if not options.get('architecture') and not options.get('architecture-id'): - try: - options['architecture-id'] = Architecture.info( - {'name': constants.DEFAULT_ARCHITECTURE} - )['id'] - except CLIReturnCodeError: - options['architecture-id'] = make_architecture()['id'] - if not options.get('operatingsystem') and not options.get('operatingsystem-id'): - try: - options['operatingsystem-id'] = OperatingSys.list( - {'search': 'name="RedHat" AND (major="7" OR major="8")'} - )[0]['id'] - except IndexError: - options['operatingsystem-id'] = make_os( - { - 'architecture-ids': options.get('architecture-id'), - 'architectures': options.get('architecture'), - 'partition-table-ids': options.get('partition-table-id'), - 'partition-tables': options.get('partition-table'), - } - )['id'] - if not options.get('partition-table') and not options.get('partition-table-id'): - try: - options['partition-table-id'] = PartitionTable.list( - { - 'operatingsystem': options.get('operatingsystem'), - 'operatingsystem-id': options.get('operatingsystem-id'), - } - )[0]['id'] - except IndexError: - options['partition-table-id'] = make_partition_table( - { - 'location-ids': options.get('location-id'), - 'locations': options.get('location'), - 'operatingsystem-ids': options.get('operatingsystem-id'), - 'organization-ids': options.get('organization-id'), - 'organizations': options.get('organization'), - } - )['id'] - - # Finally, create a new medium (if none was passed) - if not options.get('medium') and not options.get('medium-id'): - options['medium-id'] = make_medium( - { - 'location-ids': options.get('location-id'), - 'locations': options.get('location'), - 'operatingsystems': options.get('operatingsystem'), - 'operatingsystem-ids': options.get('operatingsystem-id'), - 'organization-ids': options.get('organization-id'), - 'organizations': options.get('organization'), - } - )['id'] - - return make_host(options) - - -@cacheable -def make_host_collection(options=None): - """Creates a Host Collection - - :param options: Check options using `hammer host-collection create --help` on satellite. - - :returns HostCollection object - """ - # Assigning default values for attributes - args = { - 'description': None, - 'host-collection-ids': None, - 'hosts': None, - 'max-hosts': None, - 'name': gen_string('alpha', 15), - 'organization': None, - 'organization-id': None, - 'organization-label': None, - 'unlimited-hosts': None, - } - - return create_object(HostCollection, args, options) - - -@cacheable -def make_job_invocation(options=None): - """Creates a Job Invocation - - :param options: Check options using `hammer job-invocation create --help` on satellite. - - :returns JobInvocation object - """ - return make_job_invocation_with_credentials(options) - - -def make_job_invocation_with_credentials(options=None, credentials=None): - """Helper function to create Job Invocation with credentials""" - - args = { - 'async': None, - 'bookmark': None, - 'bookmark-id': None, - 'concurrency-level': None, - 'cron-line': None, - 'description-format': None, - 'dynamic': None, - 'effective-user': None, - 'end-time': None, - 'input-files': None, - 'inputs': None, - 'job-template': None, - 'job-template-id': None, - 'max-iteration': None, - 'search-query': None, - 'start-at': None, - 'start-before': None, - 'time-span': None, - } - - jinv_cls = _entity_with_credentials(credentials, JobInvocation) - return create_object(jinv_cls, args, options) - - -@cacheable -def make_job_template(options=None): - """Creates a Job Template - - :param options: Check options using `hammer job-template create --help` on satellite. - - :returns JobTemplate object - """ - args = { - 'audit-comment': None, - 'current-user': None, - 'description-format': None, - 'file': None, - 'job-category': 'Miscellaneous', - 'location-ids': None, - 'locations': None, - 'name': None, - 'organization-ids': None, - 'organizations': None, - 'overridable': None, - 'provider-type': 'SSH', - 'snippet': None, - 'value': None, - } - - return create_object(JobTemplate, args, options) - - -@cacheable -def make_user(options=None): - """Creates a User - - :param options: Check options using `hammer user create --help` on satellite. - - :returns User object - """ - login = gen_alphanumeric(6) - - # Assigning default values for attributes - args = { - 'admin': None, - 'auth-source-id': 1, - 'default-location-id': None, - 'default-organization-id': None, - 'description': None, - 'firstname': gen_alphanumeric(), - 'lastname': gen_alphanumeric(), - 'location-ids': None, - 'login': login, - 'mail': f'{login}@example.com', - 'organization-ids': None, - 'password': gen_alphanumeric(), - 'timezone': None, - } - logger.debug( - 'User "{}" password not provided {} was generated'.format(args['login'], args['password']) - ) - - return create_object(User, args, options) - - -@cacheable -def make_usergroup(options=None): - """Creates a User Group - - :param options: Check options using `hammer user-group create --help` on satellite. - - :returns UserGroup object - """ - # Assigning default values for attributes - args = { - 'admin': None, - 'name': gen_alphanumeric(), - 'role-ids': None, - 'roles': None, - 'user-group-ids': None, - 'user-groups': None, - 'user-ids': None, - 'users': None, - } - - return create_object(UserGroup, args, options) - - -@cacheable -def make_usergroup_external(options=None): - """Creates an External User Group - - :param options: Check options using `hammer user-group external create --help` on satellite. - - :returns UserGroupExternal object - """ - # UserGroup Name or ID is a required field. - if not options or not options.get('user-group') and not options.get('user-group-id'): - raise CLIFactoryError('Please provide a valid UserGroup.') - - # Assigning default values for attributes - args = { - 'auth-source-id': 1, - 'name': gen_alphanumeric(8), - 'user-group': None, - 'user-group-id': None, - } - - return create_object(UserGroupExternal, args, options) - - -@cacheable -def make_ldap_auth_source(options=None): - """Creates an LDAP Auth Source - - :param options: Check options using `hammer auth-source ldap create --help` on satellite. - - :returns LDAPAuthSource object - """ - # Assigning default values for attributes - args = { - 'account': None, - 'account-password': None, - 'attr-firstname': None, - 'attr-lastname': None, - 'attr-login': None, - 'attr-mail': None, - 'attr-photo': None, - 'base-dn': None, - 'groups-base': None, - 'host': None, - 'ldap-filter': None, - 'location-ids': None, - 'locations': None, - 'name': gen_alphanumeric(), - 'onthefly-register': None, - 'organization-ids': None, - 'organizations': None, - 'port': None, - 'server-type': None, - 'tls': None, - 'usergroup-sync': None, - } - - return create_object(LDAPAuthSource, args, options) - - -@cacheable -def make_compute_resource(options=None): - """Creates a Compute Resource - - :param options: Check options using `hammer compute-resource create --help` on satellite. - - :returns ComputeResource object - """ - args = { - 'caching-enabled': None, - 'datacenter': None, - 'description': None, - 'display-type': None, - 'domain': None, - 'location': None, - 'location-id': None, - 'location-ids': None, - 'location-title': None, - 'location-titles': None, - 'locations': None, - 'name': gen_alphanumeric(8), - 'organization': None, - 'organization-id': None, - 'organization-ids': None, - 'organization-title': None, - 'organization-titles': None, - 'organizations': None, - 'ovirt-quota': None, - 'password': None, - 'project-domain-id': None, - 'project-domain-name': None, - 'provider': None, - 'public-key': None, - 'public-key-path': None, - 'region': None, - 'server': None, - 'set-console-password': None, - 'tenant': None, - 'url': None, - 'use-v4': None, - 'user': None, - 'uuid': None, - } - - if options is None: - options = {} - - if options.get('provider') is None: - options['provider'] = constants.FOREMAN_PROVIDERS['libvirt'] - if options.get('url') is None: - options['url'] = 'qemu+tcp://localhost:16509/system' - - return create_object(ComputeResource, args, options) - - -@cacheable -def make_org(options=None): - """Creates an Organization - - :param options: Check options using `hammer organization create --help` on satellite. - - :returns Organization object - """ - return make_org_with_credentials(options) - - -def make_org_with_credentials(options=None, credentials=None): - """Helper function to create organization with credentials""" - # Assigning default values for attributes - args = { - 'compute-resource-ids': None, - 'compute-resources': None, - 'provisioning-template-ids': None, - 'provisioning-templates': None, - 'description': None, - 'domain-ids': None, - 'environment-ids': None, - 'environments': None, - 'hostgroup-ids': None, - 'hostgroups': None, - 'label': None, - 'media-ids': None, - 'media': None, - 'name': gen_alphanumeric(6), - 'realm-ids': None, - 'realms': None, - 'smart-proxy-ids': None, - 'smart-proxies': None, - 'subnet-ids': None, - 'subnets': None, - 'user-ids': None, - 'users': None, - } - org_cls = _entity_with_credentials(credentials, Org) - return create_object(org_cls, args, options) - - -@cacheable -def make_realm(options=None): - """Creates a REALM - - :param options: Check options using `hammer realm create --help` on satellite. - - :returns Realm object - """ - # Assigning default values for attributes - args = { - 'location-ids': None, - 'locations': None, - 'name': gen_alphanumeric(6), - 'organization-ids': None, - 'organizations': None, - 'realm-proxy-id': None, - 'realm-type': None, - } - - return create_object(Realm, args, options) - - -@cacheable -def make_report_template(options=None): - """Creates a Report Template - - :param options: Check options using `hammer report-template create --help` on satellite. - - :returns ReportTemplate object - """ - if options is not None and 'content' in options.keys(): - content = options.pop('content') - else: - content = gen_alphanumeric() - - args = { - 'audit-comment': None, - 'default': None, - 'file': content, - 'interactive': None, - 'location': None, - 'location-id': None, - 'location-ids': None, - 'location-title': None, - 'location-titles': None, - 'locations': None, - 'locked': None, - 'name': gen_alphanumeric(10), - 'organization': None, - 'organization-id': None, - 'organization-ids': None, - 'organization-title': None, - 'organization-titles': None, - 'organizations': None, - 'snippet': None, - } - return create_object(ReportTemplate, args, options) - - -@cacheable -def make_os(options=None): - """Creates an Operating System - - :param options: Check options using `hammer os create --help` on satellite. - - :returns OperatingSys object - """ - # Assigning default values for attributes - args = { - 'architecture-ids': None, - 'architectures': None, - 'provisioning-template-ids': None, - 'provisioning-templates': None, - 'description': None, - 'family': None, - 'major': random.randint(0, 10), - 'media': None, - 'medium-ids': None, - 'minor': random.randint(0, 10), - 'name': gen_alphanumeric(6), - 'partition-table-ids': None, - 'partition-tables': None, - 'password-hash': None, - 'release-name': None, - } - return create_object(OperatingSys, args, options) - - -@cacheable -def make_scapcontent(options=None): - """Creates Scap Content - - :param options: Check options using `hammer scap-content create --help` on satellite. - - :returns ScapContent object - """ - # Assigning default values for attributes - args = { - 'scap-file': None, - 'original-filename': None, - 'location-ids': None, - 'locations': None, - 'title': gen_alphanumeric().lower(), - 'organization-ids': None, - 'organizations': None, - } - - return create_object(Scapcontent, args, options) - - -@cacheable -def make_domain(options=None): - """Creates a Domain - - :param options: Check options using `hammer domain create --help` on satellite. - - :returns Domain object - """ - # Assigning default values for attributes - args = { - 'description': None, - 'dns-id': None, - 'location-ids': None, - 'locations': None, - 'name': gen_alphanumeric().lower(), - 'organization-ids': None, - 'organizations': None, - } - - return create_object(Domain, args, options) - - -@cacheable -def make_hostgroup(options=None): - """Creates a Hostgroup - - :param options: Check options using `hammer hostgroup create --help` on satellite. - - :returns Hostgroup object - """ - # Assigning default values for attributes - args = { - 'architecture': None, - 'architecture-id': None, - 'compute-profile': None, - 'compute-profile-id': None, - 'config-group-ids': None, - 'config-groups': None, - 'content-source-id': None, - 'content-source': None, - 'content-view': None, - 'content-view-id': None, - 'domain': None, - 'domain-id': None, - 'environment': None, - 'puppet-environment': None, - 'environment-id': None, - 'puppet-environment-id': None, - 'locations': None, - 'location-ids': None, - 'kickstart-repository-id': None, - 'lifecycle-environment': None, - 'lifecycle-environment-id': None, - 'lifecycle-environment-organization-id': None, - 'medium': None, - 'medium-id': None, - 'name': gen_alphanumeric(6), - 'operatingsystem': None, - 'operatingsystem-id': None, - 'organizations': None, - 'organization-titles': None, - 'organization-ids': None, - 'parent': None, - 'parent-id': None, - 'parent-title': None, - 'partition-table': None, - 'partition-table-id': None, - 'puppet-ca-proxy': None, - 'puppet-ca-proxy-id': None, - 'puppet-class-ids': None, - 'puppet-classes': None, - 'puppet-proxy': None, - 'puppet-proxy-id': None, - 'pxe-loader': None, - 'query-organization': None, - 'query-organization-id': None, - 'query-organization-label': None, - 'realm': None, - 'realm-id': None, - 'root-password': None, - 'subnet': None, - 'subnet-id': None, - } - - return create_object(HostGroup, args, options) - - -@cacheable -def make_medium(options=None): - """Creates a Medium - - :param options: Check options using `hammer medium create --help` on satellite. - - :returns Medium object - """ - # Assigning default values for attributes - args = { - 'location-ids': None, - 'locations': None, - 'name': gen_alphanumeric(6), - 'operatingsystem-ids': None, - 'operatingsystems': None, - 'organization-ids': None, - 'organizations': None, - 'os-family': None, - 'path': 'http://{}'.format(gen_string('alpha', 6)), - } - - return create_object(Medium, args, options) - - -@cacheable -def make_environment(options=None): - """Creates a Puppet Environment - - :param options: Check options using `hammer environment create --help` on satellite. - - :returns Environment object - """ - # Assigning default values for attributes - args = { - 'location-ids': None, - 'locations': None, - 'name': gen_alphanumeric(6), - 'organization-ids': None, - 'organizations': None, - } - - return create_object(Environment, args, options) - - -@cacheable -def make_lifecycle_environment(options=None): - """Creates a Lifecycle Environment - - :param options: Check options using `hammer lifecycle-environment create --help` on satellite. - - :returns LifecycleEnvironment object - """ - # Organization Name, Label or ID is a required field. - if ( - not options - or 'organization' not in options - and 'organization-label' not in options - and 'organization-id' not in options - ): - raise CLIFactoryError('Please provide a valid Organization.') - - if not options.get('prior'): - options['prior'] = 'Library' - - # Assigning default values for attributes - args = { - 'description': None, - 'label': None, - 'name': gen_alphanumeric(6), - 'organization': None, - 'organization-id': None, - 'organization-label': None, - 'prior': None, - 'registry-name-pattern': None, - 'registry-unauthenticated-pull': None, - } - - return create_object(LifecycleEnvironment, args, options) - - -@cacheable -def make_tailoringfile(options=None): - """Creates a tailoring File - - :param options: Check options using `hammer tailoring-file create --help` on satellite. - - :returns TailoringFile object - """ - # Assigning default values for attributes - args = { - 'scap-file': None, - 'original-filename': None, - 'location-ids': None, - 'locations': None, - 'name': gen_alphanumeric().lower(), - 'organization-ids': None, - 'organizations': None, - } - - return create_object(TailoringFiles, args, options) - - -@cacheable -def make_template(options=None): - """Creates a Template - - :param options: Check options using `hammer template create --help` on satellite. - - :returns Template object - """ - # Assigning default values for attribute - args = { - 'audit-comment': None, - 'file': f'/root/{gen_alphanumeric()}', - 'location-ids': None, - 'locked': None, - 'name': gen_alphanumeric(6), - 'operatingsystem-ids': None, - 'organization-ids': None, - 'type': random.choice(constants.TEMPLATE_TYPES), - } - - # Write content to file or random text - if options is not None and 'content' in options.keys(): - content = options.pop('content') - else: - content = gen_alphanumeric() - - # Special handling for template factory - (_, layout) = mkstemp(text=True) - chmod(layout, 0o700) - with open(layout, 'w') as ptable: - ptable.write(content) - # Upload file to server - ssh.get_client().put(layout, args['file']) - # End - Special handling for template factory - - return create_object(Template, args, options) - - -@cacheable -def make_template_input(options=None): - """ - Creates Template Input - - :param options: Check options using `hammer template-input create --help` on satellite. - - :returns TemplateInput object - """ - if not options or not options.get('input-type') or not options.get('template-id'): - raise CLIFactoryError('Please provide valid template-id and input-type') - - args = { - 'advanced': None, - 'description': None, - 'fact-name': None, - 'input-type': None, - 'location': None, - 'location-id': None, - 'location-title': None, - 'name': gen_alphanumeric(6), - 'options': None, - 'organization': None, - 'organization-id': None, - 'organization-title': None, - 'puppet-class-name': None, - 'puppet-parameter-name': None, - 'required': None, - 'resource-type': None, - 'template-id': None, - 'value-type': None, - 'variable-name': None, - } - return create_object(TemplateInput, args, options) - - -@cacheable -def make_virt_who_config(options=None): - """Creates a Virt Who Configuration - - :param options: Check options using `hammer virt-who-config create --help` on satellite. - - :returns VirtWhoConfig object - """ - args = { - 'blacklist': None, - 'debug': None, - 'filtering-mode': 'none', - 'hypervisor-id': 'hostname', - 'hypervisor-password': None, - 'hypervisor-server': None, - 'hypervisor-type': None, - 'hypervisor-username': None, - 'interval': '60', - 'name': gen_alphanumeric(6), - 'no-proxy': None, - 'organization': None, - 'organization-id': None, - 'organization-title': None, - 'proxy': None, - 'satellite-url': settings.server.hostname, - 'whitelist': None, - } - return create_object(VirtWhoConfig, args, options) - - -def activationkey_add_subscription_to_repo(options=None): - """Helper function that adds subscription to an activation key""" - if ( - not options - or not options.get('organization-id') - or not options.get('activationkey-id') - or not options.get('subscription') - ): - raise CLIFactoryError('Please provide valid organization, activation key and subscription.') - # List the subscriptions in given org - subscriptions = Subscription.list( - {'organization-id': options['organization-id']}, per_page=False - ) - # Add subscription to activation-key - if options['subscription'] not in (sub['name'] for sub in subscriptions): - raise CLIFactoryError( - 'Subscription {} not found in the given org'.format(options['subscription']) - ) - for subscription in subscriptions: - if subscription['name'] == options['subscription']: - if subscription['quantity'] != 'Unlimited' and int(subscription['quantity']) == 0: - raise CLIFactoryError('All the subscriptions are already consumed') - try: - ActivationKey.add_subscription( - { - 'id': options['activationkey-id'], - 'subscription-id': subscription['id'], - 'quantity': 1, - } - ) - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to add subscription to activation key\n{err.msg}') - - -def setup_org_for_a_custom_repo(options=None): - """Sets up Org for the given custom repo by: - - 1. Checks if organization and lifecycle environment were given, otherwise - creates new ones. - 2. Creates a new product with the custom repo. Synchronizes the repo. - 3. Checks if content view was given, otherwise creates a new one and - - adds the RH repo - - publishes - - promotes to the lifecycle environment - 4. Checks if activation key was given, otherwise creates a new one and - associates it with the content view. - 5. Adds the custom repo subscription to the activation key - - :return: A dictionary with the entity ids of Activation key, Content view, - Lifecycle Environment, Organization, Product and Repository - - """ - if not options or not options.get('url'): - raise CLIFactoryError('Please provide valid custom repo URL.') - # Create new organization and lifecycle environment if needed - if options.get('organization-id') is None: - org_id = make_org()['id'] - else: - org_id = options['organization-id'] - if options.get('lifecycle-environment-id') is None: - env_id = make_lifecycle_environment({'organization-id': org_id})['id'] - else: - env_id = options['lifecycle-environment-id'] - # Create custom product and repository - custom_product = make_product({'organization-id': org_id}) - custom_repo = make_repository( - {'content-type': 'yum', 'product-id': custom_product['id'], 'url': options.get('url')} - ) - # Synchronize custom repository - try: - Repository.synchronize({'id': custom_repo['id']}) - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to synchronize repository\n{err.msg}') - # Create CV if needed and associate repo with it - if options.get('content-view-id') is None: - cv_id = make_content_view({'organization-id': org_id})['id'] - else: - cv_id = options['content-view-id'] - try: - ContentView.add_repository( - {'id': cv_id, 'organization-id': org_id, 'repository-id': custom_repo['id']} - ) - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to add repository to content view\n{err.msg}') - # Publish a new version of CV - try: - ContentView.publish({'id': cv_id}) - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to publish new version of content view\n{err.msg}') - # Get the content view info - cv_info = ContentView.info({'id': cv_id}) - lce_promoted = cv_info['lifecycle-environments'] - cvv = cv_info['versions'][-1] - # Promote version to next env - try: - if env_id not in [int(lce['id']) for lce in lce_promoted]: - ContentView.version_promote( - {'id': cvv['id'], 'organization-id': org_id, 'to-lifecycle-environment-id': env_id} - ) - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to promote version to next environment\n{err.msg}') - # Create activation key if needed and associate content view with it - if options.get('activationkey-id') is None: - activationkey_id = make_activation_key( - { - 'content-view-id': cv_id, - 'lifecycle-environment-id': env_id, - 'organization-id': org_id, - } - )['id'] - else: - activationkey_id = options['activationkey-id'] - # Given activation key may have no (or different) CV associated. - # Associate activation key with CV just to be sure - try: - ActivationKey.update( - {'content-view-id': cv_id, 'id': activationkey_id, 'organization-id': org_id} - ) - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to associate activation-key with CV\n{err.msg}') - - # Add custom_product subscription to activation-key, if SCA mode is disabled - if ( - Org.info({'fields': 'Simple content access', 'id': org_id})['simple-content-access'] - != 'Enabled' - ): - activationkey_add_subscription_to_repo( - { - 'activationkey-id': activationkey_id, - 'organization-id': org_id, - 'subscription': custom_product['name'], - } - ) - # Override custom product to true ( turned off by default in 6.14 ) - custom_repo = Repository.info({'id': custom_repo['id']}) - ActivationKey.content_override( - {'id': activationkey_id, 'content-label': custom_repo['content-label'], 'value': 'true'} - ) - return { - 'activationkey-id': activationkey_id, - 'content-view-id': cv_id, - 'lifecycle-environment-id': env_id, - 'organization-id': org_id, - 'product-id': custom_product['id'], - 'repository-id': custom_repo['id'], - } - - -def _setup_org_for_a_rh_repo(options=None): - """Sets up Org for the given Red Hat repository by: - - 1. Checks if organization and lifecycle environment were given, otherwise - creates new ones. - 2. Clones and uploads manifest. - 3. Enables RH repo and synchronizes it. - 4. Checks if content view was given, otherwise creates a new one and - - adds the RH repo - - publishes - - promotes to the lifecycle environment - 5. Checks if activation key was given, otherwise creates a new one and - associates it with the content view. - 6. Adds the RH repo subscription to the activation key - - Note that in most cases you should use ``setup_org_for_a_rh_repo`` instead - as it's more flexible. - - :return: A dictionary with the entity ids of Activation key, Content view, - Lifecycle Environment, Organization and Repository - - """ - if ( - not options - or not options.get('product') - or not options.get('repository-set') - or not options.get('repository') - ): - raise CLIFactoryError('Please provide valid product, repository-set and repo.') - # Create new organization and lifecycle environment if needed - if options.get('organization-id') is None: - org_id = make_org()['id'] - else: - org_id = options['organization-id'] - if options.get('lifecycle-environment-id') is None: - env_id = make_lifecycle_environment({'organization-id': org_id})['id'] - else: - env_id = options['lifecycle-environment-id'] - # Enable repo from Repository Set - try: - RepositorySet.enable( - { - 'basearch': 'x86_64', - 'name': options['repository-set'], - 'organization-id': org_id, - 'product': options['product'], - 'releasever': options.get('releasever'), - } - ) - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to enable repository set\n{err.msg}') - # Fetch repository info - try: - rhel_repo = Repository.info( - { - 'name': options['repository'], - 'organization-id': org_id, - 'product': options['product'], - } - ) - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to fetch repository info\n{err.msg}') - # Synchronize the RH repository - try: - Repository.synchronize( - { - 'name': options['repository'], - 'organization-id': org_id, - 'product': options['product'], - } - ) - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to synchronize repository\n{err.msg}') - # Create CV if needed and associate repo with it - if options.get('content-view-id') is None: - cv_id = make_content_view({'organization-id': org_id})['id'] - else: - cv_id = options['content-view-id'] - try: - ContentView.add_repository( - {'id': cv_id, 'organization-id': org_id, 'repository-id': rhel_repo['id']} - ) - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to add repository to content view\n{err.msg}') - # Publish a new version of CV - try: - ContentView.publish({'id': cv_id}) - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to publish new version of content view\n{err.msg}') - # Get the version id - try: - cvv = ContentView.info({'id': cv_id})['versions'][-1] - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to fetch content view info\n{err.msg}') - # Promote version1 to next env - try: - ContentView.version_promote( - {'id': cvv['id'], 'organization-id': org_id, 'to-lifecycle-environment-id': env_id} - ) - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to promote version to next environment\n{err.msg}') - # Create activation key if needed and associate content view with it - if options.get('activationkey-id') is None: - activationkey_id = make_activation_key( - { - 'content-view-id': cv_id, - 'lifecycle-environment-id': env_id, - 'organization-id': org_id, - } - )['id'] - else: - activationkey_id = options['activationkey-id'] - # Given activation key may have no (or different) CV associated. - # Associate activation key with CV just to be sure - try: - ActivationKey.update( - {'id': activationkey_id, 'organization-id': org_id, 'content-view-id': cv_id} - ) - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to associate activation-key with CV\n{err.msg}') - - # Add default subscription to activation-key, if SCA mode is disabled - if ( - Org.info({'fields': 'Simple content access', 'id': org_id})['simple-content-access'] - != 'Enabled' - ): - if constants.DEFAULT_SUBSCRIPTION_NAME not in ActivationKey.subscriptions( - {'id': activationkey_id, 'organization-id': org_id} - ): - activationkey_add_subscription_to_repo( - { - 'organization-id': org_id, - 'activationkey-id': activationkey_id, - 'subscription': options.get( - 'subscription', constants.DEFAULT_SUBSCRIPTION_NAME - ), - } - ) - - return { - 'activationkey-id': activationkey_id, - 'content-view-id': cv_id, - 'lifecycle-environment-id': env_id, - 'organization-id': org_id, - 'repository-id': rhel_repo['id'], - } - - -def setup_org_for_a_rh_repo(options=None, force_manifest_upload=False, force_use_cdn=False): - """Wrapper above ``_setup_org_for_a_rh_repo`` to use custom downstream repo - instead of CDN's 'Satellite Capsule', 'Satellite Tools' and base OS repos if - ``settings.robottelo.cdn == 0`` and URL for custom repositories is set in properties. - - :param options: a dict with options to pass to function - ``_setup_org_for_a_rh_repo``. See its docstring for more details - :param force_use_cdn: bool flag whether to use CDN even if there's - downstream repo available and ``settings.robottelo.cdn == 0``. - :param force_manifest_upload: bool flag whether to upload a manifest to - organization even if downstream custom repo is used instead of CDN. - Useful when test relies on organization with manifest (e.g. uses some - other RH repo afterwards). Defaults to False. - - :return: a dict with entity ids (see ``_setup_org_for_a_rh_repo`` and - ``setup_org_for_a_custom_repo``). - """ - custom_repo_url = None - if options.get('repository') == constants.REPOS['rhst6']['name']: - custom_repo_url = settings.repos.sattools_repo.rhel6 - elif options.get('repository') == constants.REPOS['rhst7']['name']: - custom_repo_url = settings.repos.sattools_repo.rhel7 - elif options.get('repository') == constants.REPOS['rhel6']['name']: - custom_repo_url = settings.repos.rhel6_os - elif options.get('repository') == constants.REPOS['rhel7']['name']: - custom_repo_url = settings.repos.rhel7_os - elif 'Satellite Capsule' in options.get('repository'): - custom_repo_url = settings.repos.capsule_repo - if force_use_cdn or settings.robottelo.cdn or not custom_repo_url: - return _setup_org_for_a_rh_repo(options) - else: - options['url'] = custom_repo_url - result = setup_org_for_a_custom_repo(options) - if force_manifest_upload: - with clone() as manifest: - ssh.get_client().put(manifest, manifest.filename) - try: - Subscription.upload( - {'file': manifest.filename, 'organization-id': result.get('organization-id')} - ) - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to upload manifest\n{err.msg}') - - # Add default subscription to activation-key, if SCA mode is disabled - if ( - Org.info({'fields': 'Simple content access', 'id': result['organization-id']})[ - 'simple-content-access' - ] - != 'Enabled' - ): - activationkey_add_subscription_to_repo( - { - 'activationkey-id': result['activationkey-id'], - 'organization-id': result['organization-id'], - 'subscription': constants.DEFAULT_SUBSCRIPTION_NAME, - } - ) - return result - - -def _get_capsule_vm_distro_repos(distro): - """Return the right RH repos info for the capsule setup""" - rh_repos = [] - if distro == 'rhel7': - # Red Hat Enterprise Linux 7 Server - rh_product_arch = constants.REPOS['rhel7']['arch'] - rh_product_releasever = constants.REPOS['rhel7']['releasever'] - rh_repos.append( - { - 'product': constants.PRDS['rhel'], - 'repository-set': constants.REPOSET['rhel7'], - 'repository': constants.REPOS['rhel7']['name'], - 'repository-id': constants.REPOS['rhel7']['id'], - 'releasever': rh_product_releasever, - 'arch': rh_product_arch, - 'cdn': True, - } - ) - # Red Hat Software Collections (for 7 Server) - rh_repos.append( - { - 'product': constants.PRDS['rhscl'], - 'repository-set': constants.REPOSET['rhscl7'], - 'repository': constants.REPOS['rhscl7']['name'], - 'repository-id': constants.REPOS['rhscl7']['id'], - 'releasever': rh_product_releasever, - 'arch': rh_product_arch, - 'cdn': True, - } - ) - # Red Hat Satellite Capsule 6.2 (for RHEL 7 Server) - rh_repos.append( - { - 'product': constants.PRDS['rhsc'], - 'repository-set': constants.REPOSET['rhsc7'], - 'repository': constants.REPOS['rhsc7']['name'], - 'repository-id': constants.REPOS['rhsc7']['id'], - 'url': settings.repos.capsule_repo, - 'cdn': settings.robottelo.cdn or not settings.repos.capsule_repo, - } - ) - else: - raise CLIFactoryError(f'distro "{distro}" not supported') - - return rh_product_arch, rh_product_releasever, rh_repos - - -def add_role_permissions(role_id, resource_permissions): - """Create role permissions found in resource permissions dict - - :param role_id: The role id - :param resource_permissions: a dict containing resources with permission - names and other Filter options - - Usage:: - - role = make_role({'organization-id': org['id']}) - resource_permissions = { - 'Katello::ActivationKey': { - 'permissions': [ - 'view_activation_keys', - 'create_activation_keys', - 'edit_activation_keys', - 'destroy_activation_keys' - ], - 'search': "name ~ {}".format(ak_name_like) - }, - } - add_role_permissions(role['id'], resource_permissions) - """ - available_permissions = Filter.available_permissions() - # group the available permissions by resource type - available_rc_permissions = {} - for permission in available_permissions: - permission_resource = permission['resource'] - if permission_resource not in available_rc_permissions: - available_rc_permissions[permission_resource] = [] - available_rc_permissions[permission_resource].append(permission) - # create only the required role permissions per resource type - for resource_type, permission_data in resource_permissions.items(): - permission_names = permission_data.get('permissions') - if permission_names is None: - raise CLIFactoryError(f'Permissions not provided for resource: {resource_type}') - # ensure that the required resource type is available - if resource_type not in available_rc_permissions: - raise CLIFactoryError( - f'Resource "{resource_type}" not in the list of available resources' - ) - available_permission_names = [ - permission['name'] - for permission in available_rc_permissions[resource_type] - if permission['name'] in permission_names - ] - # ensure that all the required permissions are available - missing_permissions = set(permission_names).difference(set(available_permission_names)) - if missing_permissions: - raise CLIFactoryError( - 'Permissions "{}" are not available in Resource "{}"'.format( - list(missing_permissions), resource_type - ) - ) - # Create the current resource type role permissions - options = {'role-id': role_id} - options.update(permission_data) - make_filter(options=options) - - -def setup_cdn_and_custom_repositories(org_id, repos, download_policy='on_demand', synchronize=True): - """Setup cdn and custom repositories - - :param int org_id: The organization id - :param list repos: a list of dict repositories options - :param str download_policy: update the repositories with this download - policy - :param bool synchronize: Whether to synchronize the repositories. - :return: a dict containing the content view and repos info - """ - custom_product = None - repos_info = [] - for repo in repos: - custom_repo_url = repo.get('url') - cdn = repo.get('cdn', False) - if not cdn and not custom_repo_url: - raise CLIFactoryError('Custom repository with url not supplied') - if cdn: - RepositorySet.enable( - { - 'organization-id': org_id, - 'product': repo['product'], - 'name': repo['repository-set'], - 'basearch': repo.get('arch', constants.DEFAULT_ARCHITECTURE), - 'releasever': repo.get('releasever'), - } - ) - repo_info = Repository.info( - {'organization-id': org_id, 'name': repo['repository'], 'product': repo['product']} - ) - else: - if custom_product is None: - custom_product = make_product_wait({'organization-id': org_id}) - repo_info = make_repository( - { - 'product-id': custom_product['id'], - 'organization-id': org_id, - 'url': custom_repo_url, - } - ) - if download_policy: - # Set download policy - Repository.update({'download-policy': download_policy, 'id': repo_info['id']}) - repos_info.append(repo_info) - if synchronize: - # Synchronize the repositories - for repo_info in repos_info: - Repository.synchronize({'id': repo_info['id']}, timeout=4800000) - return custom_product, repos_info - - -def setup_cdn_and_custom_repos_content( - target_sat, - org_id, - lce_id=None, - repos=None, - upload_manifest=True, - download_policy='on_demand', - rh_subscriptions=None, - default_cv=False, -): - """Setup cdn and custom repositories, content view and activations key - - :param target_sat: sat object - :param int org_id: The organization id - :param int lce_id: the lifecycle environment id - :param list repos: a list of dict repositories options - :param bool default_cv: whether to use the Default Organization CV - :param bool upload_manifest: whether to upload the organization manifest - :param str download_policy: update the repositories with this download - policy - :param list rh_subscriptions: a list of RH subscription to attach to - activation key - :return: a dict containing the activation key, content view and repos info - """ - if lce_id is None and not default_cv: - raise TypeError('lce_id must be specified') - if repos is None: - repos = [] - if rh_subscriptions is None: - rh_subscriptions = [] - - if upload_manifest: - # Upload the organization manifest - try: - target_sat.upload_manifest(org_id, clone(), interface='CLI') - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to upload manifest\n{err.msg}') - - custom_product, repos_info = setup_cdn_and_custom_repositories( - org_id=org_id, repos=repos, download_policy=download_policy - ) - if default_cv: - activation_key = make_activation_key( - {'organization-id': org_id, 'lifecycle-environment': 'Library'} - ) - content_view = ContentView.info( - {'organization-id': org_id, 'name': 'Default Organization View'} - ) - else: - # Create a content view - content_view = make_content_view({'organization-id': org_id}) - # Add repositories to content view - for repo_info in repos_info: - ContentView.add_repository( - { - 'id': content_view['id'], - 'organization-id': org_id, - 'repository-id': repo_info['id'], - } - ) - # Publish the content view - ContentView.publish({'id': content_view['id']}) - # Get the latest content view version id - content_view_version = ContentView.info({'id': content_view['id']})['versions'][-1] - # Promote content view version to lifecycle environment - ContentView.version_promote( - { - 'id': content_view_version['id'], - 'organization-id': org_id, - 'to-lifecycle-environment-id': lce_id, - } - ) - content_view = ContentView.info({'id': content_view['id']}) - activation_key = make_activation_key( - { - 'organization-id': org_id, - 'lifecycle-environment-id': lce_id, - 'content-view-id': content_view['id'], - } - ) - # Get organization subscriptions - subscriptions = Subscription.list({'organization-id': org_id}, per_page=False) - # Add subscriptions to activation-key - needed_subscription_names = list(rh_subscriptions) - if custom_product: - needed_subscription_names.append(custom_product['name']) - added_subscription_names = [] - for subscription in subscriptions: - if ( - subscription['name'] in needed_subscription_names - and subscription['name'] not in added_subscription_names - ): - ActivationKey.add_subscription( - {'id': activation_key['id'], 'subscription-id': subscription['id'], 'quantity': 1} - ) - added_subscription_names.append(subscription['name']) - if len(added_subscription_names) == len(needed_subscription_names): - break - missing_subscription_names = set(needed_subscription_names).difference( - set(added_subscription_names) - ) - if missing_subscription_names: - raise CLIFactoryError(f'Missing subscriptions: {missing_subscription_names}') - data = dict( - activation_key=activation_key, - content_view=content_view, - product=custom_product, - repos=repos_info, - ) - if lce_id: - lce = LifecycleEnvironment.info({'id': lce_id, 'organization-id': org_id}) - data['lce'] = lce - - return data - - -@cacheable -def make_http_proxy(options=None): - """Creates a HTTP Proxy - - :param options: Check options using `hammer http-proxy create --help` on satellite. - - :returns HttpProxy object - """ - # Assigning default values for attributes - args = { - 'location': None, - 'location-id': None, - 'location-title': None, - 'locations': None, - 'location-ids': None, - 'location-titles': None, - 'name': gen_string('alpha', 15), - 'organization': None, - 'organization-id': None, - 'organization-title': None, - 'organizations': None, - 'organization-ids': None, - 'organization-titles': None, - 'password': None, - 'url': '{}:{}'.format(gen_url(scheme='https'), gen_integer(min_value=10, max_value=9999)), - 'username': None, - } - - return create_object(HttpProxy, args, options) diff --git a/robottelo/cli/report_template.py b/robottelo/cli/report_template.py index e8a60b9beb6..962c5929bc7 100644 --- a/robottelo/cli/report_template.py +++ b/robottelo/cli/report_template.py @@ -25,8 +25,9 @@ from tempfile import mkstemp from robottelo import ssh -from robottelo.cli.base import Base, CLIError +from robottelo.cli.base import Base from robottelo.constants import REPORT_TEMPLATE_FILE, DataFile +from robottelo.exceptions import CLIError class ReportTemplate(Base): diff --git a/robottelo/content_info.py b/robottelo/content_info.py index 1e8fd01ed64..fd404f5a7d9 100644 --- a/robottelo/content_info.py +++ b/robottelo/content_info.py @@ -5,7 +5,7 @@ import requests from robottelo import ssh -from robottelo.cli.base import CLIReturnCodeError +from robottelo.exceptions import CLIReturnCodeError def get_repo_files(repo_path, extension='rpm', hostname=None): diff --git a/robottelo/exceptions.py b/robottelo/exceptions.py index d1f9886dce0..83022dfcd6e 100644 --- a/robottelo/exceptions.py +++ b/robottelo/exceptions.py @@ -65,3 +65,56 @@ class ProxyError(Exception): class DownloadFileError(Exception): """Indicates an error when failure in downloading file from server.""" + + +class CLIFactoryError(Exception): + """Indicates an error occurred while creating an entity using hammer""" + + +class CLIError(Exception): + """Indicates that a CLI command could not be run.""" + + +class CLIBaseError(Exception): + """Indicates that a CLI command has finished with return code different + from zero. + + :param status: CLI command return code + :param stderr: contents of the ``stderr`` + :param msg: explanation of the error + + """ + + def __init__(self, status, stderr, msg): + self.status = status + self.stderr = stderr + self.msg = msg + super().__init__(msg) + self.message = msg + + def __str__(self): + """Include class name, status, stderr and msg to string repr so + assertRaisesRegexp can be used to assert error present on any + attribute + """ + return repr(self) + + def __repr__(self): + """Include class name status, stderr and msg to improve logging""" + return '{}(status={!r}, stderr={!r}, msg={!r}'.format( + type(self).__name__, self.status, self.stderr, self.msg + ) + + +class CLIReturnCodeError(CLIBaseError): + """Error to be raised when an error occurs due to some validation error + when execution hammer cli. + See: https://github.com/SatelliteQE/robottelo/issues/3790 for more details + """ + + +class CLIDataBaseError(CLIBaseError): + """Error to be raised when an error occurs due to some missing parameter + which cause a data base error on hammer + See: https://github.com/SatelliteQE/robottelo/issues/3790 for more details + """ diff --git a/robottelo/host_helpers/cli_factory.py b/robottelo/host_helpers/cli_factory.py index cf7d8a28058..bcf7120eedf 100644 --- a/robottelo/host_helpers/cli_factory.py +++ b/robottelo/host_helpers/cli_factory.py @@ -26,17 +26,13 @@ ) from robottelo import constants -from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.proxy import CapsuleTunnelError from robottelo.config import settings +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.host_helpers.repository_mixins import initiate_repo_helpers from robottelo.utils.manifest import clone -class CLIFactoryError(Exception): - """Indicates an error occurred while creating an entity using hammer""" - - def create_object(cli_object, options, values=None, credentials=None): """ Creates with dictionary of arguments. @@ -129,6 +125,7 @@ def create_object(cli_object, options, values=None, credentials=None): '_entity_cls': 'Repository', 'name': gen_alpha, 'url': settings.repos.yum_1.url, + 'content-type': 'yum', }, 'role': {'name': gen_alphanumeric}, 'filter': {}, diff --git a/robottelo/host_helpers/satellite_mixins.py b/robottelo/host_helpers/satellite_mixins.py index 263510f6783..b17325a8492 100644 --- a/robottelo/host_helpers/satellite_mixins.py +++ b/robottelo/host_helpers/satellite_mixins.py @@ -7,7 +7,6 @@ import requests -from robottelo.cli.base import CLIReturnCodeError from robottelo.cli.proxy import CapsuleTunnelError from robottelo.config import settings from robottelo.constants import ( @@ -16,6 +15,7 @@ PUPPET_COMMON_INSTALLER_OPTS, PUPPET_SATELLITE_INSTALLER, ) +from robottelo.exceptions import CLIReturnCodeError from robottelo.host_helpers.api_factory import APIFactory from robottelo.host_helpers.cli_factory import CLIFactory from robottelo.host_helpers.ui_factory import UIFactory diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 679a4a8ba39..24cc7e41529 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -29,7 +29,6 @@ from robottelo import constants from robottelo.cli.base import Base -from robottelo.cli.factory import CLIFactoryError from robottelo.config import ( configure_airgun, configure_nailgun, @@ -52,7 +51,7 @@ RHSSO_USER_UPDATE, SATELLITE_VERSION, ) -from robottelo.exceptions import DownloadFileError, HostPingFailed +from robottelo.exceptions import CLIFactoryError, DownloadFileError, HostPingFailed from robottelo.host_helpers import CapsuleMixins, ContentHostMixins, SatelliteMixins from robottelo.logging import logger from robottelo.utils import validate_ssh_pub_key @@ -1261,18 +1260,19 @@ def virt_who_hypervisor_config( :param bool upload_manifest: whether to upload the organization manifest :param list extra_repos: (Optional) repositories dict options to setup additionally. """ - from robottelo.cli import factory as cli_factory - from robottelo.cli.lifecycleenvironment import LifecycleEnvironment - from robottelo.cli.org import Org - from robottelo.cli.subscription import Subscription - from robottelo.cli.virt_who_config import VirtWhoConfig - org = cli_factory.make_org() if org_id is None else Org.info({'id': org_id}) + org = ( + satellite.cli_factory.make_org() + if org_id is None + else satellite.cli.Org.info({'id': org_id}) + ) if lce_id is None: - lce = cli_factory.make_lifecycle_environment({'organization-id': org['id']}) + lce = satellite.cli_factory.make_lifecycle_environment({'organization-id': org['id']}) else: - lce = LifecycleEnvironment.info({'id': lce_id, 'organization-id': org['id']}) + lce = satellite.cli.LifecycleEnvironment.info( + {'id': lce_id, 'organization-id': org['id']} + ) extra_repos = extra_repos or [] repos = [ # Red Hat Satellite Tools @@ -1286,7 +1286,7 @@ def virt_who_hypervisor_config( } ] repos.extend(extra_repos) - content_setup_data = cli_factory.setup_cdn_and_custom_repos_content( + content_setup_data = satellite.cli_factory.setup_cdn_and_custom_repos_content( org[id], lce[id], repos, @@ -1331,7 +1331,7 @@ def virt_who_hypervisor_config( # create the virt-who directory on satellite satellite = Satellite() satellite.execute(f'mkdir -p {virt_who_deploy_directory}') - VirtWhoConfig.fetch({'id': config_id, 'output': virt_who_deploy_file}) + satellite.cli.VirtWhoConfig.fetch({'id': config_id, 'output': virt_who_deploy_file}) # remote_copy from satellite to self satellite.session.remote_copy(virt_who_deploy_file, self) @@ -1397,7 +1397,9 @@ def virt_who_hypervisor_config( virt_who_hypervisor_host = org_hosts[0] subscription_id = None if hypervisor_hostname and subscription_name: - subscriptions = Subscription.list({'organization-id': org_id}, per_page=False) + subscriptions = satellite.cli.Subscription.list( + {'organization-id': org_id}, per_page=False + ) for subscription in subscriptions: if subscription['name'] == subscription_name: subscription_id = subscription['id'] diff --git a/tests/foreman/api/test_errata.py b/tests/foreman/api/test_errata.py index d88b27411bc..601f5f3832d 100644 --- a/tests/foreman/api/test_errata.py +++ b/tests/foreman/api/test_errata.py @@ -23,8 +23,6 @@ import pytest from robottelo import constants -from robottelo.cli.factory import setup_org_for_a_custom_repo, setup_org_for_a_rh_repo -from robottelo.cli.host import Host from robottelo.config import settings from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME @@ -48,8 +46,10 @@ def activation_key(module_org, module_lce, module_target_sat): @pytest.fixture(scope='module') -def rh_repo(module_entitlement_manifest_org, module_lce, module_cv, activation_key): - return setup_org_for_a_rh_repo( +def rh_repo( + module_entitlement_manifest_org, module_lce, module_cv, activation_key, module_target_sat +): + return module_target_sat.cli_factory.setup_org_for_a_rh_repo( { 'product': constants.PRDS['rhel'], 'repository-set': constants.REPOSET['rhst7'], @@ -63,8 +63,8 @@ def rh_repo(module_entitlement_manifest_org, module_lce, module_cv, activation_k @pytest.fixture(scope='module') -def custom_repo(module_org, module_lce, module_cv, activation_key): - return setup_org_for_a_custom_repo( +def custom_repo(module_org, module_lce, module_cv, activation_key, module_target_sat): + return module_target_sat.cli_factory.setup_org_for_a_custom_repo( { 'url': settings.repos.yum_9.url, 'organization-id': module_org.id, @@ -529,7 +529,7 @@ def test_positive_get_diff_for_cv_envs(target_sat): content_view = target_sat.api.ContentView(organization=org).create() activation_key = target_sat.api.ActivationKey(environment=env, organization=org).create() for repo_url in [settings.repos.yum_9.url, CUSTOM_REPO_URL]: - setup_org_for_a_custom_repo( + target_sat.cli_factory.setup_org_for_a_custom_repo( { 'url': repo_url, 'organization-id': org.id, @@ -726,7 +726,7 @@ def test_errata_installation_with_swidtags( _run_remote_command_on_content_host( module_org, f'dnf -y module install {module_name}:0:{version}', rhel8_contenthost ) - Host.errata_recalculate({'host-id': rhel8_contenthost.nailgun_host.id}) + target_sat.cli.Host.errata_recalculate({'host-id': rhel8_contenthost.nailgun_host.id}) # validate swid tags Installed before_errata_apply_result = _run_remote_command_on_content_host( module_org, @@ -743,7 +743,7 @@ def test_errata_installation_with_swidtags( module_org, f'dnf -y module update {module_name}', rhel8_contenthost ) _run_remote_command_on_content_host(module_org, 'dnf -y upload-profile', rhel8_contenthost) - Host.errata_recalculate({'host-id': rhel8_contenthost.nailgun_host.id}) + target_sat.cli.Host.errata_recalculate({'host-id': rhel8_contenthost.nailgun_host.id}) applicable_errata_count -= 1 assert rhel8_contenthost.applicable_errata_count == applicable_errata_count after_errata_apply_result = _run_remote_command_on_content_host( @@ -781,9 +781,9 @@ def rh_repo_module_manifest(module_entitlement_manifest_org, module_target_sat): @pytest.fixture(scope='module') -def rhel8_custom_repo_cv(module_entitlement_manifest_org): +def rhel8_custom_repo_cv(module_entitlement_manifest_org, module_target_sat): """Create repo and publish CV so that packages are in Library""" - return setup_org_for_a_custom_repo( + return module_target_sat.cli_factory.setup_org_for_a_custom_repo( { 'url': settings.repos.module_stream_1.url, 'organization-id': module_entitlement_manifest_org.id, @@ -887,7 +887,7 @@ def test_apply_modular_errata_using_default_content_view( assert result.status == 0 # Check that there is now two errata applicable errata = _fetch_available_errata(module_entitlement_manifest_org, host, 2) - Host.errata_recalculate({'host-id': rhel8_contenthost.nailgun_host.id}) + target_sat.cli.Host.errata_recalculate({'host-id': rhel8_contenthost.nailgun_host.id}) assert len(errata) == 2 # Assert that errata package is required assert constants.FAKE_3_CUSTOM_PACKAGE in errata[0]['module_streams'][0]['packages'] diff --git a/tests/foreman/api/test_repositories.py b/tests/foreman/api/test_repositories.py index 18051c0c344..f8639a13367 100644 --- a/tests/foreman/api/test_repositories.py +++ b/tests/foreman/api/test_repositories.py @@ -22,9 +22,9 @@ from requests.exceptions import HTTPError from robottelo import constants -from robottelo.cli.base import CLIReturnCodeError from robottelo.config import settings from robottelo.constants import DEFAULT_ARCHITECTURE, MIRRORING_POLICIES, REPOS +from robottelo.exceptions import CLIReturnCodeError from robottelo.utils.datafactory import parametrized diff --git a/tests/foreman/api/test_role.py b/tests/foreman/api/test_role.py index 9ed32e118ec..1fc5ae89067 100644 --- a/tests/foreman/api/test_role.py +++ b/tests/foreman/api/test_role.py @@ -24,7 +24,6 @@ import pytest from requests.exceptions import HTTPError -from robottelo.cli.ldapauthsource import LDAPAuthSource from robottelo.config import settings from robottelo.constants import LDAP_ATTR, LDAP_SERVER_TYPE from robottelo.utils.datafactory import gen_string, generate_strings_list, parametrized @@ -210,7 +209,7 @@ def create_ldap(self, ad_data, target_sat, module_location, module_org): query={'search': f'login={ad_data["ldap_user_name"]}'} ): user.delete() - LDAPAuthSource.delete({'name': authsource_name}) + target_sat.cli.LDAPAuthSource.delete({'name': authsource_name}) @pytest.mark.tier1 def test_positive_create_role_with_taxonomies(self, role_taxonomies, target_sat): diff --git a/tests/foreman/api/test_subscription.py b/tests/foreman/api/test_subscription.py index b27488cd5f7..5d5a2aedd38 100644 --- a/tests/foreman/api/test_subscription.py +++ b/tests/foreman/api/test_subscription.py @@ -26,7 +26,6 @@ import pytest from requests.exceptions import HTTPError -from robottelo.cli.subscription import Subscription from robottelo.config import settings from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME, PRDS, REPOS, REPOSET @@ -216,7 +215,7 @@ def test_positive_delete_manifest_as_another_user( target_sat.api.Subscription(sc2, organization=function_org).delete_manifest( data={'organization_id': function_org.id} ) - assert len(Subscription.list({'organization-id': function_org.id})) == 0 + assert len(target_sat.cli.Subscription.list({'organization-id': function_org.id})) == 0 @pytest.mark.tier2 diff --git a/tests/foreman/cli/test_acs.py b/tests/foreman/cli/test_acs.py index 39ba1701dee..37750aed9bf 100644 --- a/tests/foreman/cli/test_acs.py +++ b/tests/foreman/cli/test_acs.py @@ -19,8 +19,8 @@ from fauxfactory import gen_alphanumeric import pytest -from robottelo.cli.base import CLIReturnCodeError from robottelo.constants.repos import PULP_FIXTURE_ROOT, PULP_SUBPATHS_COMBINED +from robottelo.exceptions import CLIReturnCodeError ACS_UPDATED = 'Alternate Content Source updated.' ACS_DELETED = 'Alternate Content Source deleted.' diff --git a/tests/foreman/cli/test_activationkey.py b/tests/foreman/cli/test_activationkey.py index 3a860981e0c..da61301e19f 100644 --- a/tests/foreman/cli/test_activationkey.py +++ b/tests/foreman/cli/test_activationkey.py @@ -23,28 +23,10 @@ from fauxfactory import gen_alphanumeric, gen_string import pytest -from robottelo.cli.activationkey import ActivationKey -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.contentview import ContentView from robottelo.cli.defaults import Defaults -from robottelo.cli.factory import ( - CLIFactoryError, - add_role_permissions, - make_activation_key, - make_content_view, - make_host_collection, - make_lifecycle_environment, - make_role, - make_user, - setup_org_for_a_custom_repo, - setup_org_for_a_rh_repo, -) -from robottelo.cli.lifecycleenvironment import LifecycleEnvironment -from robottelo.cli.repository import Repository -from robottelo.cli.subscription import Subscription -from robottelo.cli.user import User from robottelo.config import settings from robottelo.constants import PRDS, REPOS, REPOSET +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.hosts import ContentHost from robottelo.utils.datafactory import ( invalid_values_list, @@ -55,9 +37,11 @@ @pytest.fixture(scope='module') -def get_default_env(module_org): +def get_default_env(module_org, module_target_sat): """Get default lifecycle environment""" - return LifecycleEnvironment.info({'organization-id': module_org.id, 'name': 'Library'}) + return module_target_sat.cli.LifecycleEnvironment.info( + {'organization-id': module_org.id, 'name': 'Library'} + ) @pytest.mark.tier1 @@ -82,7 +66,7 @@ def test_positive_create_with_name(module_target_sat, module_entitlement_manifes @pytest.mark.tier1 @pytest.mark.parametrize('desc', **parametrized(valid_data_list())) -def test_positive_create_with_description(desc, module_org): +def test_positive_create_with_description(desc, module_org, module_target_sat): """Create Activation key for all variations of Description :id: 5a5ca7f9-1449-4365-ac8a-978605620bf2 @@ -93,12 +77,14 @@ def test_positive_create_with_description(desc, module_org): :parametrized: yes """ - new_ak = make_activation_key({'organization-id': module_org.id, 'description': desc}) + new_ak = module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id, 'description': desc} + ) assert new_ak['description'] == desc @pytest.mark.tier1 -def test_positive_create_with_default_lce_by_id(module_org, get_default_env): +def test_positive_create_with_default_lce_by_id(module_org, get_default_env, target_sat): """Create Activation key with associated default environment :id: 9171adb2-c9ac-4cda-978f-776826668aa3 @@ -108,14 +94,14 @@ def test_positive_create_with_default_lce_by_id(module_org, get_default_env): :CaseImportance: Critical """ lce = get_default_env - new_ak_env = make_activation_key( + new_ak_env = target_sat.cli_factory.make_activation_key( {'organization-id': module_org.id, 'lifecycle-environment-id': lce['id']} ) assert new_ak_env['lifecycle-environment'] == lce['name'] @pytest.mark.tier1 -def test_positive_create_with_non_default_lce(module_org): +def test_positive_create_with_non_default_lce(module_org, module_target_sat): """Create Activation key with associated custom environment :id: ad4d4611-3fb5-4449-ae47-305f9931350e @@ -125,15 +111,17 @@ def test_positive_create_with_non_default_lce(module_org): :CaseImportance: Critical """ - env = make_lifecycle_environment({'organization-id': module_org.id}) - new_ak_env = make_activation_key( + env = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + new_ak_env = module_target_sat.cli_factory.make_activation_key( {'organization-id': module_org.id, 'lifecycle-environment-id': env['id']} ) assert new_ak_env['lifecycle-environment'] == env['name'] @pytest.mark.tier1 -def test_positive_create_with_default_lce_by_name(module_org, get_default_env): +def test_positive_create_with_default_lce_by_name(module_org, get_default_env, module_target_sat): """Create Activation key with associated environment by name :id: 7410f7c4-e8b5-4080-b6d2-65dbcedffe8a @@ -143,7 +131,7 @@ def test_positive_create_with_default_lce_by_name(module_org, get_default_env): :CaseImportance: Critical """ lce = get_default_env - new_ak_env = make_activation_key( + new_ak_env = module_target_sat.cli_factory.make_activation_key( {'organization-id': module_org.id, 'lifecycle-environment': lce['name']} ) assert new_ak_env['lifecycle-environment'] == lce['name'] @@ -151,7 +139,7 @@ def test_positive_create_with_default_lce_by_name(module_org, get_default_env): @pytest.mark.tier2 @pytest.mark.parametrize('name', **parametrized(valid_data_list())) -def test_positive_create_with_cv(name, module_org, get_default_env): +def test_positive_create_with_cv(name, module_org, get_default_env, module_target_sat): """Create Activation key for all variations of Content Views :id: ec7b1af5-c3f4-40c3-b1df-c69c02a3b9a7 @@ -163,8 +151,10 @@ def test_positive_create_with_cv(name, module_org, get_default_env): :parametrized: yes """ - new_cv = make_content_view({'name': name, 'organization-id': module_org.id}) - new_ak_cv = make_activation_key( + new_cv = module_target_sat.cli_factory.make_content_view( + {'name': name, 'organization-id': module_org.id} + ) + new_ak_cv = module_target_sat.cli_factory.make_activation_key( { 'content-view': new_cv['name'], 'environment': get_default_env['name'], @@ -175,7 +165,7 @@ def test_positive_create_with_cv(name, module_org, get_default_env): @pytest.mark.tier1 -def test_positive_create_with_usage_limit_default(module_org): +def test_positive_create_with_usage_limit_default(module_org, module_target_sat): """Create Activation key with default Usage limit (Unlimited) :id: cba13c72-9845-486d-beff-e0fb55bb762c @@ -184,12 +174,12 @@ def test_positive_create_with_usage_limit_default(module_org): :CaseImportance: Critical """ - new_ak = make_activation_key({'organization-id': module_org.id}) + new_ak = module_target_sat.cli_factory.make_activation_key({'organization-id': module_org.id}) assert new_ak['host-limit'] == 'Unlimited' @pytest.mark.tier1 -def test_positive_create_with_usage_limit_finite(module_org): +def test_positive_create_with_usage_limit_finite(module_org, module_target_sat): """Create Activation key with finite Usage limit :id: 529a0f9e-977f-4e9d-a1af-88bb98c28a6a @@ -198,13 +188,15 @@ def test_positive_create_with_usage_limit_finite(module_org): :CaseImportance: Critical """ - new_ak = make_activation_key({'organization-id': module_org.id, 'max-hosts': '10'}) + new_ak = module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id, 'max-hosts': '10'} + ) assert new_ak['host-limit'] == '10' @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_create_content_and_check_enabled(module_org): +def test_positive_create_content_and_check_enabled(module_org, module_target_sat): """Create activation key and add content to it. Check enabled state. :id: abfc6c6e-acd1-4761-b309-7e68e1d17172 @@ -216,10 +208,10 @@ def test_positive_create_content_and_check_enabled(module_org): :CaseLevel: Integration """ - result = setup_org_for_a_custom_repo( + result = module_target_sat.cli_factory.setup_org_for_a_custom_repo( {'url': settings.repos.yum_0.url, 'organization-id': module_org.id} ) - content = ActivationKey.product_content( + content = module_target_sat.cli.ActivationKey.product_content( {'id': result['activationkey-id'], 'organization-id': module_org.id} ) assert content[0]['default-enabled?'] == 'true' @@ -227,7 +219,7 @@ def test_positive_create_content_and_check_enabled(module_org): @pytest.mark.tier2 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) -def test_negative_create_with_invalid_name(name, module_org): +def test_negative_create_with_invalid_name(name, module_org, module_target_sat): """Create Activation key with invalid Name :id: d9b7e3a9-1d24-4e47-bd4a-dce75772d829 @@ -240,7 +232,9 @@ def test_negative_create_with_invalid_name(name, module_org): :parametrized: yes """ with pytest.raises(CLIFactoryError) as raise_ctx: - make_activation_key({'organization-id': module_org.id, 'name': name}) + module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id, 'name': name} + ) if name in ['', ' ', '\t']: assert 'Name must contain at least 1 character' in str(raise_ctx) if len(name) > 255: @@ -252,7 +246,7 @@ def test_negative_create_with_invalid_name(name, module_org): 'limit', **parametrized([value for value in invalid_values_list() if not value.isdigit()] + [0.5]), ) -def test_negative_create_with_usage_limit_with_not_integers(module_org, limit): +def test_negative_create_with_usage_limit_with_not_integers(module_org, limit, module_target_sat): """Create Activation key with non integers Usage Limit :id: 247ebc2e-c80f-488b-aeaf-6bf5eba55375 @@ -268,7 +262,9 @@ def test_negative_create_with_usage_limit_with_not_integers(module_org, limit): # invalid_values = [value for value in invalid_values_list() if not value.isdigit()] # invalid_values.append(0.5) with pytest.raises(CLIFactoryError) as raise_ctx: - make_activation_key({'organization-id': module_org.id, 'max-hosts': limit}) + module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id, 'max-hosts': limit} + ) if isinstance(limit, int): if limit < 1: assert 'Max hosts cannot be less than one' in str(raise_ctx) @@ -278,7 +274,9 @@ def test_negative_create_with_usage_limit_with_not_integers(module_org, limit): @pytest.mark.tier3 @pytest.mark.parametrize('invalid_values', ['-1', '-500', 0]) -def test_negative_create_with_usage_limit_with_invalid_integers(module_org, invalid_values): +def test_negative_create_with_usage_limit_with_invalid_integers( + module_org, invalid_values, module_target_sat +): """Create Activation key with invalid integers Usage Limit :id: 9089f756-fda8-4e28-855c-cf8273f7c6cd @@ -291,13 +289,15 @@ def test_negative_create_with_usage_limit_with_invalid_integers(module_org, inva :parametrized: yes """ with pytest.raises(CLIFactoryError) as raise_ctx: - make_activation_key({'organization-id': module_org.id, 'max-hosts': invalid_values}) + module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id, 'max-hosts': invalid_values} + ) assert 'Failed to create ActivationKey with data:' in str(raise_ctx) @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(valid_data_list())) -def test_positive_delete_by_name(name, module_org): +def test_positive_delete_by_name(name, module_org, module_target_sat): """Create Activation key and delete it for all variations of Activation key name @@ -309,14 +309,18 @@ def test_positive_delete_by_name(name, module_org): :parametrized: yes """ - new_ak = make_activation_key({'name': name, 'organization-id': module_org.id}) - ActivationKey.delete({'name': new_ak['name'], 'organization-id': module_org.id}) + new_ak = module_target_sat.cli_factory.make_activation_key( + {'name': name, 'organization-id': module_org.id} + ) + module_target_sat.cli.ActivationKey.delete( + {'name': new_ak['name'], 'organization-id': module_org.id} + ) with pytest.raises(CLIReturnCodeError): - ActivationKey.info({'id': new_ak['id']}) + module_target_sat.cli.ActivationKey.info({'id': new_ak['id']}) @pytest.mark.tier1 -def test_positive_delete_by_org_name(module_org): +def test_positive_delete_by_org_name(module_org, module_target_sat): """Create Activation key and delete it using organization name for which that key was created @@ -326,14 +330,16 @@ def test_positive_delete_by_org_name(module_org): :CaseImportance: High """ - new_ak = make_activation_key({'organization-id': module_org.id}) - ActivationKey.delete({'name': new_ak['name'], 'organization-id': module_org.id}) + new_ak = module_target_sat.cli_factory.make_activation_key({'organization-id': module_org.id}) + module_target_sat.cli.ActivationKey.delete( + {'name': new_ak['name'], 'organization-id': module_org.id} + ) with pytest.raises(CLIReturnCodeError): - ActivationKey.info({'id': new_ak['id']}) + module_target_sat.cli.ActivationKey.info({'id': new_ak['id']}) @pytest.mark.tier1 -def test_positive_delete_by_org_label(module_org): +def test_positive_delete_by_org_label(module_org, module_target_sat): """Create Activation key and delete it using organization label for which that key was created @@ -343,15 +349,17 @@ def test_positive_delete_by_org_label(module_org): :CaseImportance: High """ - new_ak = make_activation_key({'organization-id': module_org.id}) - ActivationKey.delete({'name': new_ak['name'], 'organization-label': module_org.label}) + new_ak = module_target_sat.cli_factory.make_activation_key({'organization-id': module_org.id}) + module_target_sat.cli.ActivationKey.delete( + {'name': new_ak['name'], 'organization-label': module_org.label} + ) with pytest.raises(CLIReturnCodeError): - ActivationKey.info({'id': new_ak['id']}) + module_target_sat.cli.ActivationKey.info({'id': new_ak['id']}) @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_delete_with_cv(module_org): +def test_positive_delete_with_cv(module_org, module_target_sat): """Create activation key with content view assigned to it and delete it using activation key id @@ -361,15 +369,17 @@ def test_positive_delete_with_cv(module_org): :CaseLevel: Integration """ - new_cv = make_content_view({'organization-id': module_org.id}) - new_ak = make_activation_key({'organization-id': module_org.id, 'content-view': new_cv['name']}) - ActivationKey.delete({'id': new_ak['id']}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + new_ak = module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id, 'content-view': new_cv['name']} + ) + module_target_sat.cli.ActivationKey.delete({'id': new_ak['id']}) with pytest.raises(CLIReturnCodeError): - ActivationKey.info({'id': new_ak['id']}) + module_target_sat.cli.ActivationKey.info({'id': new_ak['id']}) @pytest.mark.tier2 -def test_positive_delete_with_lce(module_org, get_default_env): +def test_positive_delete_with_lce(module_org, get_default_env, module_target_sat): """Create activation key with lifecycle environment assigned to it and delete it using activation key id @@ -379,17 +389,17 @@ def test_positive_delete_with_lce(module_org, get_default_env): :CaseLevel: Integration """ - new_ak = make_activation_key( + new_ak = module_target_sat.cli_factory.make_activation_key( {'organization-id': module_org.id, 'lifecycle-environment': get_default_env['name']} ) - ActivationKey.delete({'id': new_ak['id']}) + module_target_sat.cli.ActivationKey.delete({'id': new_ak['id']}) with pytest.raises(CLIReturnCodeError): - ActivationKey.info({'id': new_ak['id']}) + module_target_sat.cli.ActivationKey.info({'id': new_ak['id']}) @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(valid_data_list())) -def test_positive_update_name_by_id(module_org, name): +def test_positive_update_name_by_id(module_org, name, module_target_sat): """Update Activation Key Name in Activation key searching by ID :id: bc304894-fd9b-4622-96e3-57c2257e26ca @@ -400,16 +410,18 @@ def test_positive_update_name_by_id(module_org, name): :parametrized: yes """ - activation_key = make_activation_key({'organization-id': module_org.id}) - ActivationKey.update( + activation_key = module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ActivationKey.update( {'id': activation_key['id'], 'new-name': name, 'organization-id': module_org.id} ) - updated_ak = ActivationKey.info({'id': activation_key['id']}) + updated_ak = module_target_sat.cli.ActivationKey.info({'id': activation_key['id']}) assert updated_ak['name'] == name @pytest.mark.tier1 -def test_positive_update_name_by_name(module_org): +def test_positive_update_name_by_name(module_org, module_target_sat): """Update Activation Key Name in an Activation key searching by name @@ -420,17 +432,19 @@ def test_positive_update_name_by_name(module_org): :CaseImportance: Critical """ new_name = gen_string('alpha') - activation_key = make_activation_key({'organization-id': module_org.id}) - ActivationKey.update( + activation_key = module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ActivationKey.update( {'name': activation_key['name'], 'new-name': new_name, 'organization-id': module_org.id} ) - updated_ak = ActivationKey.info({'id': activation_key['id']}) + updated_ak = module_target_sat.cli.ActivationKey.info({'id': activation_key['id']}) assert updated_ak['name'] == new_name @pytest.mark.tier1 @pytest.mark.parametrize('description', **parametrized(valid_data_list())) -def test_positive_update_description(description, module_org): +def test_positive_update_description(description, module_org, module_target_sat): """Update Description in an Activation key :id: 60a4e860-d99c-431e-b70b-9b0fa90d839b @@ -441,20 +455,22 @@ def test_positive_update_description(description, module_org): :parametrized: yes """ - activation_key = make_activation_key({'organization-id': module_org.id}) - ActivationKey.update( + activation_key = module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ActivationKey.update( { 'description': description, 'name': activation_key['name'], 'organization-id': module_org.id, } ) - updated_ak = ActivationKey.info({'id': activation_key['id']}) + updated_ak = module_target_sat.cli.ActivationKey.info({'id': activation_key['id']}) assert updated_ak['description'] == description @pytest.mark.tier2 -def test_positive_update_lce(module_org, get_default_env): +def test_positive_update_lce(module_org, get_default_env, module_target_sat): """Update Environment in an Activation key :id: 55aaee60-b8c8-49f0-995a-6c526b9b653b @@ -463,15 +479,19 @@ def test_positive_update_lce(module_org, get_default_env): :CaseLevel: Integration """ - ak_env = make_activation_key( + ak_env = module_target_sat.cli_factory.make_activation_key( {'organization-id': module_org.id, 'lifecycle-environment-id': get_default_env['id']} ) - env = make_lifecycle_environment({'organization-id': module_org.id}) - new_cv = make_content_view({'organization-id': module_org.id}) - ContentView.publish({'id': new_cv['id']}) - cvv = ContentView.info({'id': new_cv['id']})['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': env['id']}) - ActivationKey.update( + env = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + cvv = module_target_sat.cli.ContentView.info({'id': new_cv['id']})['versions'][0] + module_target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': env['id']} + ) + module_target_sat.cli.ActivationKey.update( { 'id': ak_env['id'], 'lifecycle-environment-id': env['id'], @@ -479,12 +499,12 @@ def test_positive_update_lce(module_org, get_default_env): 'organization-id': module_org.id, } ) - updated_ak = ActivationKey.info({'id': ak_env['id']}) + updated_ak = module_target_sat.cli.ActivationKey.info({'id': ak_env['id']}) assert updated_ak['lifecycle-environment'] == env['name'] @pytest.mark.tier2 -def test_positive_update_cv(module_org): +def test_positive_update_cv(module_org, module_target_sat): """Update Content View in an Activation key :id: aa94997d-fc9b-4532-aeeb-9f27b9834914 @@ -493,18 +513,20 @@ def test_positive_update_cv(module_org): :CaseLevel: Integration """ - cv = make_content_view({'organization-id': module_org.id}) - ak_cv = make_activation_key({'organization-id': module_org.id, 'content-view-id': cv['id']}) - new_cv = make_content_view({'organization-id': module_org.id}) - ActivationKey.update( + cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + ak_cv = module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id, 'content-view-id': cv['id']} + ) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + module_target_sat.cli.ActivationKey.update( {'content-view': new_cv['name'], 'name': ak_cv['name'], 'organization-id': module_org.id} ) - updated_ak = ActivationKey.info({'id': ak_cv['id']}) + updated_ak = module_target_sat.cli.ActivationKey.info({'id': ak_cv['id']}) assert updated_ak['content-view'] == new_cv['name'] @pytest.mark.tier1 -def test_positive_update_usage_limit_to_finite_number(module_org): +def test_positive_update_usage_limit_to_finite_number(module_org, module_target_sat): """Update Usage limit from Unlimited to a finite number :id: a55bb8dc-c7d8-4a6a-ac0f-1d5a377da543 @@ -513,17 +535,17 @@ def test_positive_update_usage_limit_to_finite_number(module_org): :CaseImportance: Critical """ - new_ak = make_activation_key({'organization-id': module_org.id}) + new_ak = module_target_sat.cli_factory.make_activation_key({'organization-id': module_org.id}) assert new_ak['host-limit'] == 'Unlimited' - ActivationKey.update( + module_target_sat.cli.ActivationKey.update( {'max-hosts': '2147483647', 'name': new_ak['name'], 'organization-id': module_org.id} ) - updated_ak = ActivationKey.info({'id': new_ak['id']}) + updated_ak = module_target_sat.cli.ActivationKey.info({'id': new_ak['id']}) assert updated_ak['host-limit'] == '2147483647' @pytest.mark.tier1 -def test_positive_update_usage_limit_to_unlimited(module_org): +def test_positive_update_usage_limit_to_unlimited(module_org, module_target_sat): """Update Usage limit from definite number to Unlimited :id: 0b83657b-41d1-4fb2-9c23-c36011322b83 @@ -532,18 +554,20 @@ def test_positive_update_usage_limit_to_unlimited(module_org): :CaseImportance: Critical """ - new_ak = make_activation_key({'organization-id': module_org.id, 'max-hosts': '10'}) + new_ak = module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id, 'max-hosts': '10'} + ) assert new_ak['host-limit'] == '10' - ActivationKey.update( + module_target_sat.cli.ActivationKey.update( {'unlimited-hosts': True, 'name': new_ak['name'], 'organization-id': module_org.id} ) - updated_ak = ActivationKey.info({'id': new_ak['id']}) + updated_ak = module_target_sat.cli.ActivationKey.info({'id': new_ak['id']}) assert updated_ak['host-limit'] == 'Unlimited' @pytest.mark.tier2 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) -def test_negative_update_name(module_org, name): +def test_negative_update_name(module_org, name, module_target_sat): """Try to update Activation Key using invalid value for its name :id: b75e7c38-fde2-4110-ba65-4157319fc159 @@ -555,16 +579,16 @@ def test_negative_update_name(module_org, name): :parametrized: yes """ - new_ak = make_activation_key({'organization-id': module_org.id}) + new_ak = module_target_sat.cli_factory.make_activation_key({'organization-id': module_org.id}) with pytest.raises(CLIReturnCodeError) as raise_ctx: - ActivationKey.update( + module_target_sat.cli.ActivationKey.update( {'id': new_ak['id'], 'new-name': name, 'organization-id': module_org.id} ) assert 'Could not update the activation key:' in raise_ctx.value.message @pytest.mark.tier2 -def test_negative_update_usage_limit(module_org): +def test_negative_update_usage_limit(module_org, module_target_sat): """Try to update Activation Key using invalid value for its usage limit attribute @@ -575,9 +599,9 @@ def test_negative_update_usage_limit(module_org): :CaseImportance: Low """ - new_ak = make_activation_key({'organization-id': module_org.id}) + new_ak = module_target_sat.cli_factory.make_activation_key({'organization-id': module_org.id}) with pytest.raises(CLIReturnCodeError) as raise_ctx: - ActivationKey.update( + module_target_sat.cli.ActivationKey.update( {'max-hosts': int('9' * 20), 'id': new_ak['id'], 'organization-id': module_org.id} ) assert 'Validation failed: Max hosts must be less than 2147483648' in raise_ctx.value.message @@ -606,12 +630,14 @@ def test_positive_usage_limit(module_org, target_sat): :CaseLevel: System """ - env = make_lifecycle_environment({'organization-id': module_org.id}) - new_cv = make_content_view({'organization-id': module_org.id}) - ContentView.publish({'id': new_cv['id']}) - cvv = ContentView.info({'id': new_cv['id']})['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': env['id']}) - new_ak = make_activation_key( + env = target_sat.cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) + new_cv = target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + target_sat.cli.ContentView.publish({'id': new_cv['id']}) + cvv = target_sat.cli.ContentView.info({'id': new_cv['id']})['versions'][0] + target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': env['id']} + ) + new_ak = target_sat.cli_factory.make_activation_key( { 'lifecycle-environment-id': env['id'], 'content-view': new_cv['name'], @@ -633,7 +659,7 @@ def test_positive_usage_limit(module_org, target_sat): @pytest.mark.tier2 @pytest.mark.parametrize('host_col_name', **parametrized(valid_data_list())) -def test_positive_update_host_collection(module_org, host_col_name): +def test_positive_update_host_collection(module_org, host_col_name, module_target_sat): """Test that host collections can be associated to Activation Keys @@ -648,26 +674,28 @@ def test_positive_update_host_collection(module_org, host_col_name): :parametrized: yes """ - activation_key = make_activation_key({'organization-id': module_org.id}) - new_host_col_name = make_host_collection( + activation_key = module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id} + ) + new_host_col_name = module_target_sat.cli_factory.make_host_collection( {'name': host_col_name, 'organization-id': module_org.id} )['name'] # Assert that name matches data passed assert new_host_col_name == host_col_name - ActivationKey.add_host_collection( + module_target_sat.cli.ActivationKey.add_host_collection( { 'host-collection': new_host_col_name, 'name': activation_key['name'], 'organization-id': module_org.id, } ) - activation_key = ActivationKey.info({'id': activation_key['id']}) + activation_key = module_target_sat.cli.ActivationKey.info({'id': activation_key['id']}) assert activation_key['host-collections'][0]['name'] == host_col_name @pytest.mark.run_in_one_thread @pytest.mark.tier2 -def test_positive_update_host_collection_with_default_org(module_org): +def test_positive_update_host_collection_with_default_org(module_org, module_target_sat): """Test that host collection can be associated to Activation Keys with specified default organization setting in config @@ -680,12 +708,14 @@ def test_positive_update_host_collection_with_default_org(module_org): """ Defaults.add({'param-name': 'organization_id', 'param-value': module_org.id}) try: - activation_key = make_activation_key({'organization-id': module_org.id}) - host_col = make_host_collection() - ActivationKey.add_host_collection( + activation_key = module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id} + ) + host_col = module_target_sat.cli_factory.make_host_collection() + module_target_sat.cli.ActivationKey.add_host_collection( {'host-collection': host_col['name'], 'name': activation_key['name']} ) - activation_key = ActivationKey.info({'id': activation_key['id']}) + activation_key = module_target_sat.cli.ActivationKey.info({'id': activation_key['id']}) assert activation_key['host-collections'][0]['name'] == host_col['name'] finally: Defaults.delete({'param-name': 'organization_id'}) @@ -693,7 +723,7 @@ def test_positive_update_host_collection_with_default_org(module_org): @pytest.mark.run_in_one_thread @pytest.mark.tier3 -def test_positive_add_redhat_product(function_entitlement_manifest_org): +def test_positive_add_redhat_product(function_entitlement_manifest_org, target_sat): """Test that RH product can be associated to Activation Keys :id: 7b15de8e-edde-41aa-937b-ad6aa529891a @@ -707,7 +737,7 @@ def test_positive_add_redhat_product(function_entitlement_manifest_org): # Using CDN as we need this repo to be RH one no matter are we in # downstream or cdn - result = setup_org_for_a_rh_repo( + result = target_sat.cli_factory.setup_org_for_a_rh_repo( { 'product': PRDS['rhel'], 'repository-set': REPOSET['rhst7'], @@ -716,7 +746,7 @@ def test_positive_add_redhat_product(function_entitlement_manifest_org): }, force_use_cdn=True, ) - content = ActivationKey.product_content( + content = target_sat.cli.ActivationKey.product_content( {'id': result['activationkey-id'], 'organization-id': org.id} ) assert content[0]['name'] == REPOSET['rhst7'] @@ -724,7 +754,7 @@ def test_positive_add_redhat_product(function_entitlement_manifest_org): @pytest.mark.tier3 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_add_custom_product(module_org): +def test_positive_add_custom_product(module_org, module_target_sat): """Test that custom product can be associated to Activation Keys :id: 96ace967-e165-4069-8ff7-f54c4c822de0 @@ -736,11 +766,11 @@ def test_positive_add_custom_product(module_org): :BZ: 1426386 """ - result = setup_org_for_a_custom_repo( + result = module_target_sat.cli_factory.setup_org_for_a_custom_repo( {'url': settings.repos.yum_0.url, 'organization-id': module_org.id} ) - repo = Repository.info({'id': result['repository-id']}) - content = ActivationKey.product_content( + repo = module_target_sat.cli.Repository.info({'id': result['repository-id']}) + content = module_target_sat.cli.ActivationKey.product_content( {'id': result['activationkey-id'], 'organization-id': module_org.id} ) assert content[0]['name'] == repo['name'] @@ -773,7 +803,7 @@ def test_positive_add_redhat_and_custom_products( org = function_entitlement_manifest_org # Using CDN as we need this repo to be RH one no matter are we in # downstream or cdn - result = setup_org_for_a_rh_repo( + result = module_target_sat.cli_factory.setup_org_for_a_rh_repo( { 'product': PRDS['rhel'], 'repository-set': REPOSET['rhst7'], @@ -782,7 +812,7 @@ def test_positive_add_redhat_and_custom_products( }, force_use_cdn=True, ) - result = setup_org_for_a_custom_repo( + result = module_target_sat.cli_factory.setup_org_for_a_custom_repo( { 'url': settings.repos.yum_0.url, 'organization-id': org.id, @@ -818,18 +848,18 @@ def test_positive_delete_manifest(function_entitlement_manifest_org, target_sat) :CaseAutomation: Automated """ org = function_entitlement_manifest_org - new_ak = make_activation_key({'organization-id': org.id}) + new_ak = target_sat.cli_factory.make_activation_key({'organization-id': org.id}) ak_subs = target_sat.cli.ActivationKey.subscriptions( {'id': new_ak['id'], 'organization-id': org.id} ) - subscription_result = Subscription.list( + subscription_result = target_sat.cli.Subscription.list( {'organization-id': org.id, 'order': 'id desc'}, per_page=False ) result = target_sat.cli.ActivationKey.add_subscription( {'id': new_ak['id'], 'subscription-id': subscription_result[-1]['id']} ) assert 'Subscription added to activation key.' in result - Subscription.delete_manifest({'organization-id': org.id}) + target_sat.cli.Subscription.delete_manifest({'organization-id': org.id}) ak_subs_info = target_sat.cli.ActivationKey.subscriptions( {'id': new_ak['id'], 'organization-id': org.id} ) @@ -850,15 +880,17 @@ def test_positive_delete_subscription(function_entitlement_manifest_org, module_ :CaseLevel: Integration """ org = function_entitlement_manifest_org - new_ak = make_activation_key({'organization-id': org.id}) - subscription_result = Subscription.list( + new_ak = module_target_sat.cli_factory.make_activation_key({'organization-id': org.id}) + subscription_result = module_target_sat.cli.Subscription.list( {'organization-id': org.id, 'order': 'id desc'}, per_page=False ) result = module_target_sat.cli.ActivationKey.add_subscription( {'id': new_ak['id'], 'subscription-id': subscription_result[-1]['id']} ) assert 'Subscription added to activation key.' in result - ak_subs_info = ActivationKey.subscriptions({'id': new_ak['id'], 'organization-id': org.id}) + ak_subs_info = module_target_sat.cli.ActivationKey.subscriptions( + {'id': new_ak['id'], 'organization-id': org.id} + ) assert subscription_result[-1]['name'] in ak_subs_info result = module_target_sat.cli.ActivationKey.remove_subscription( {'id': new_ak['id'], 'subscription-id': subscription_result[-1]['id']} @@ -886,13 +918,15 @@ def test_positive_update_aks_to_chost(module_org, rhel7_contenthost, target_sat) :CaseLevel: System """ - env = make_lifecycle_environment({'organization-id': module_org.id}) - new_cv = make_content_view({'organization-id': module_org.id}) - ContentView.publish({'id': new_cv['id']}) - cvv = ContentView.info({'id': new_cv['id']})['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': env['id']}) + env = target_sat.cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) + new_cv = target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + target_sat.cli.ContentView.publish({'id': new_cv['id']}) + cvv = target_sat.cli.ContentView.info({'id': new_cv['id']})['versions'][0] + target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': env['id']} + ) new_aks = [ - make_activation_key( + target_sat.cli_factory.make_activation_key( { 'lifecycle-environment-id': env['id'], 'content-view': new_cv['name'], @@ -948,7 +982,9 @@ def test_positive_list_by_name(module_org, name, module_target_sat): :parametrized: yes """ - make_activation_key({'organization-id': module_org.id, 'name': name}) + module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id, 'name': name} + ) result = module_target_sat.cli.ActivationKey.list( {'name': name, 'organization-id': module_org.id} ) @@ -966,8 +1002,10 @@ def test_positive_list_by_cv_id(module_org, module_target_sat): :CaseImportance: High """ - cv = make_content_view({'organization-id': module_org.id}) - make_activation_key({'organization-id': module_org.id, 'content-view-id': cv['id']}) + cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id, 'content-view-id': cv['id']} + ) result = module_target_sat.cli.ActivationKey.list( {'content-view-id': cv['id'], 'organization-id': module_org.id} ) @@ -987,14 +1025,18 @@ def test_positive_create_using_old_name(module_org, module_target_sat): :CaseImportance: High """ name = gen_string('utf8') - activation_key = make_activation_key({'organization-id': module_org.id, 'name': name}) + activation_key = module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id, 'name': name} + ) new_name = gen_string('utf8') module_target_sat.cli.ActivationKey.update( {'id': activation_key['id'], 'new-name': new_name, 'organization-id': module_org.id} ) activation_key = module_target_sat.cli.ActivationKey.info({'id': activation_key['id']}) assert activation_key['name'] == new_name - new_activation_key = make_activation_key({'name': name, 'organization-id': module_org.id}) + new_activation_key = module_target_sat.cli_factory.make_activation_key( + {'name': name, 'organization-id': module_org.id} + ) assert new_activation_key['name'] == name @@ -1022,8 +1064,10 @@ def test_positive_remove_host_collection_by_id(module_org, module_target_sat): :BZ: 1336716 """ - activation_key = make_activation_key({'organization-id': module_org.id}) - new_host_col = make_host_collection( + activation_key = module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id} + ) + new_host_col = module_target_sat.cli_factory.make_host_collection( {'name': gen_string('alpha'), 'organization-id': module_org.id} ) module_target_sat.cli.ActivationKey.add_host_collection( @@ -1035,7 +1079,7 @@ def test_positive_remove_host_collection_by_id(module_org, module_target_sat): ) activation_key = module_target_sat.cli.ActivationKey.info({'id': activation_key['id']}) assert len(activation_key['host-collections']) == 1 - ActivationKey.remove_host_collection( + module_target_sat.cli.ActivationKey.remove_host_collection( { 'host-collection-id': new_host_col['id'], 'name': activation_key['name'], @@ -1071,8 +1115,12 @@ def test_positive_remove_host_collection_by_name(module_org, host_col, module_ta :parametrized: yes """ - activation_key = make_activation_key({'organization-id': module_org.id}) - new_host_col = make_host_collection({'name': host_col, 'organization-id': module_org.id}) + activation_key = module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id} + ) + new_host_col = module_target_sat.cli_factory.make_host_collection( + {'name': host_col, 'organization-id': module_org.id} + ) # Assert that name matches data passed assert new_host_col['name'] == host_col module_target_sat.cli.ActivationKey.add_host_collection( @@ -1113,7 +1161,7 @@ def test_create_ak_with_syspurpose_set(module_entitlement_manifest_org, module_t :BZ: 1789028 """ # Requires Cls org and manifest. Manifest is for self-support values. - new_ak = make_activation_key( + new_ak = module_target_sat.cli_factory.make_activation_key( { 'purpose-addons': "test-addon1, test-addon2", 'purpose-role': "test-role", @@ -1169,7 +1217,7 @@ def test_update_ak_with_syspurpose_values(module_entitlement_manifest_org, modul # Requires Cls org and manifest. Manifest is for self-support values. # Create an AK with no system purpose values set org = module_entitlement_manifest_org - new_ak = make_activation_key({'organization-id': org.id}) + new_ak = module_target_sat.cli_factory.make_activation_key({'organization-id': org.id}) # Assert system purpose values are null after creating the AK and adding the manifest. assert new_ak['system-purpose']['purpose-addons'] == '' assert new_ak['system-purpose']['purpose-role'] == '' @@ -1236,7 +1284,7 @@ def test_positive_add_subscription_by_id(module_entitlement_manifest_org, module :BZ: 1463685 """ org_id = module_entitlement_manifest_org.id - ackey_id = make_activation_key({'organization-id': org_id})['id'] + ackey_id = module_target_sat.cli_factory.make_activation_key({'organization-id': org_id})['id'] subs_id = module_target_sat.cli.Subscription.list({'organization-id': org_id}, per_page=False) result = module_target_sat.cli.ActivationKey.add_subscription( {'id': ackey_id, 'subscription-id': subs_id[0]['id']} @@ -1246,7 +1294,7 @@ def test_positive_add_subscription_by_id(module_entitlement_manifest_org, module @pytest.mark.tier1 @pytest.mark.parametrize('new_name', **parametrized(valid_data_list())) -def test_positive_copy_by_parent_id(module_org, new_name): +def test_positive_copy_by_parent_id(module_org, new_name, module_target_sat): """Copy Activation key for all valid Activation Key name variations @@ -1258,15 +1306,17 @@ def test_positive_copy_by_parent_id(module_org, new_name): :parametrized: yes """ - parent_ak = make_activation_key({'organization-id': module_org.id}) - result = ActivationKey.copy( + parent_ak = module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id} + ) + result = module_target_sat.cli.ActivationKey.copy( {'id': parent_ak['id'], 'new-name': new_name, 'organization-id': module_org.id} ) assert 'Activation key copied.' in result @pytest.mark.tier1 -def test_positive_copy_by_parent_name(module_org): +def test_positive_copy_by_parent_name(module_org, module_target_sat): """Copy Activation key by passing name of parent :id: 5d5405e6-3b26-47a3-96ff-f6c0f6c32607 @@ -1275,8 +1325,10 @@ def test_positive_copy_by_parent_name(module_org): :CaseImportance: Critical """ - parent_ak = make_activation_key({'organization-id': module_org.id}) - result = ActivationKey.copy( + parent_ak = module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id} + ) + result = module_target_sat.cli.ActivationKey.copy( { 'name': parent_ak['name'], 'new-name': gen_string('alpha'), @@ -1287,7 +1339,7 @@ def test_positive_copy_by_parent_name(module_org): @pytest.mark.tier1 -def test_negative_copy_with_same_name(module_org): +def test_negative_copy_with_same_name(module_org, module_target_sat): """Copy activation key with duplicate name :id: f867c468-4155-495c-a1e5-c04d9868a2e0 @@ -1295,9 +1347,11 @@ def test_negative_copy_with_same_name(module_org): :expectedresults: Activation key is not successfully copied """ - parent_ak = make_activation_key({'organization-id': module_org.id}) + parent_ak = module_target_sat.cli_factory.make_activation_key( + {'organization-id': module_org.id} + ) with pytest.raises(CLIReturnCodeError) as raise_ctx: - ActivationKey.copy( + module_target_sat.cli.ActivationKey.copy( { 'name': parent_ak['name'], 'new-name': parent_ak['name'], @@ -1312,7 +1366,7 @@ def test_negative_copy_with_same_name(module_org): @pytest.mark.skip_if_not_set('fake_manifest') @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_copy_subscription(module_entitlement_manifest_org): +def test_positive_copy_subscription(module_entitlement_manifest_org, module_target_sat): """Copy Activation key and verify contents :id: f4ee8096-4120-4d06-8c9a-57ac1eaa8f68 @@ -1329,24 +1383,28 @@ def test_positive_copy_subscription(module_entitlement_manifest_org): """ # Begin test setup org = module_entitlement_manifest_org - parent_ak = make_activation_key({'organization-id': org.id}) - subscription_result = Subscription.list({'organization-id': org.id}, per_page=False) - ActivationKey.add_subscription( + parent_ak = module_target_sat.cli_factory.make_activation_key({'organization-id': org.id}) + subscription_result = module_target_sat.cli.Subscription.list( + {'organization-id': org.id}, per_page=False + ) + module_target_sat.cli.ActivationKey.add_subscription( {'id': parent_ak['id'], 'subscription-id': subscription_result[0]['id']} ) # End test setup new_name = gen_string('utf8') - result = ActivationKey.copy( + result = module_target_sat.cli.ActivationKey.copy( {'id': parent_ak['id'], 'new-name': new_name, 'organization-id': org.id} ) assert 'Activation key copied.' in result - result = ActivationKey.subscriptions({'name': new_name, 'organization-id': org.id}) + result = module_target_sat.cli.ActivationKey.subscriptions( + {'name': new_name, 'organization-id': org.id} + ) # Verify that the subscription copied over assert subscription_result[0]['name'] in result # subscription name # subscription list @pytest.mark.tier1 -def test_positive_update_autoattach_toggle(module_org): +def test_positive_update_autoattach_toggle(module_org, module_target_sat): """Update Activation key with inverse auto-attach value :id: de3b5fb7-7963-420a-b4c9-c66e78a111dc @@ -1361,19 +1419,19 @@ def test_positive_update_autoattach_toggle(module_org): :CaseImportance: Critical """ - new_ak = make_activation_key({'organization-id': module_org.id}) + new_ak = module_target_sat.cli_factory.make_activation_key({'organization-id': module_org.id}) attach_value = new_ak['auto-attach'] # invert value new_value = 'false' if attach_value == 'true' else 'true' - ActivationKey.update( + module_target_sat.cli.ActivationKey.update( {'auto-attach': new_value, 'id': new_ak['id'], 'organization-id': module_org.id} ) - updated_ak = ActivationKey.info({'id': new_ak['id']}) + updated_ak = module_target_sat.cli.ActivationKey.info({'id': new_ak['id']}) assert updated_ak['auto-attach'] == new_value @pytest.mark.tier1 -def test_positive_update_autoattach(module_org): +def test_positive_update_autoattach(module_org, module_target_sat): """Update Activation key with valid auto-attach values :id: 9e18b950-6f0f-4f82-a3ac-ef6aba950a78 @@ -1382,16 +1440,16 @@ def test_positive_update_autoattach(module_org): :CaseImportance: Critical """ - new_ak = make_activation_key({'organization-id': module_org.id}) + new_ak = module_target_sat.cli_factory.make_activation_key({'organization-id': module_org.id}) for new_value in ('1', '0', 'true', 'false', 'yes', 'no'): - result = ActivationKey.update( + result = module_target_sat.cli.ActivationKey.update( {'auto-attach': new_value, 'id': new_ak['id'], 'organization-id': module_org.id} ) assert 'Activation key updated.' == result[0]['message'] @pytest.mark.tier2 -def test_negative_update_autoattach(module_org): +def test_negative_update_autoattach(module_org, module_target_sat): """Attempt to update Activation key with bad auto-attach value :id: 54b6f808-ff54-4e69-a54d-e1f99a4652f9 @@ -1406,9 +1464,9 @@ def test_negative_update_autoattach(module_org): :CaseImportance: Low """ - new_ak = make_activation_key({'organization-id': module_org.id}) + new_ak = module_target_sat.cli_factory.make_activation_key({'organization-id': module_org.id}) with pytest.raises(CLIReturnCodeError) as exe: - ActivationKey.update( + module_target_sat.cli.ActivationKey.update( { 'auto-attach': gen_string('utf8'), 'id': new_ak['id'], @@ -1420,7 +1478,7 @@ def test_negative_update_autoattach(module_org): @pytest.mark.tier3 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_content_override(module_org): +def test_positive_content_override(module_org, module_target_sat): """Positive content override :id: a4912cc0-3bf7-4e90-bb51-ec88b2fad227 @@ -1436,14 +1494,14 @@ def test_positive_content_override(module_org): :CaseLevel: System """ - result = setup_org_for_a_custom_repo( + result = module_target_sat.cli_factory.setup_org_for_a_custom_repo( {'url': settings.repos.yum_0.url, 'organization-id': module_org.id} ) - content = ActivationKey.product_content( + content = module_target_sat.cli.ActivationKey.product_content( {'id': result['activationkey-id'], 'organization-id': module_org.id} ) for override_value in (True, False): - ActivationKey.content_override( + module_target_sat.cli.ActivationKey.content_override( { 'content-label': content[0]['label'], 'id': result['activationkey-id'], @@ -1452,14 +1510,14 @@ def test_positive_content_override(module_org): } ) # Retrieve the product content enabled flag - content = ActivationKey.product_content( + content = module_target_sat.cli.ActivationKey.product_content( {'id': result['activationkey-id'], 'organization-id': module_org.id} ) assert content[0]['override'] == f'enabled:{int(override_value)}' @pytest.mark.tier2 -def test_positive_remove_user(module_org): +def test_positive_remove_user(module_org, module_target_sat): """Delete any user who has previously created an activation key and check that activation key still exists @@ -1470,20 +1528,22 @@ def test_positive_remove_user(module_org): :BZ: 1291271 """ password = gen_string('alpha') - user = make_user({'password': password, 'admin': 'true'}) - ak = ActivationKey.with_user(username=user['login'], password=password).create( - {'name': gen_string('alpha'), 'organization-id': module_org.id} - ) - User.delete({'id': user['id']}) + user = module_target_sat.cli_factory.user({'password': password, 'admin': 'true'}) + ak = module_target_sat.cli.ActivationKey.with_user( + username=user['login'], password=password + ).create({'name': gen_string('alpha'), 'organization-id': module_org.id}) + module_target_sat.cli.User.delete({'id': user['id']}) try: - ActivationKey.info({'id': ak['id']}) + module_target_sat.cli.ActivationKey.info({'id': ak['id']}) except CLIReturnCodeError: pytest.fail("Activation key can't be read") @pytest.mark.run_in_one_thread @pytest.mark.tier3 -def test_positive_view_subscriptions_by_non_admin_user(module_entitlement_manifest_org): +def test_positive_view_subscriptions_by_non_admin_user( + module_entitlement_manifest_org, module_target_sat +): """Attempt to read activation key subscriptions by non admin user :id: af75b640-97be-431b-8ac0-a6367f8f1996 @@ -1525,18 +1585,24 @@ def test_positive_view_subscriptions_by_non_admin_user(module_entitlement_manife f'Test_*_{gen_string("alpha")}', ) ak_name = f'{ak_name_like}_{gen_string("alpha")}' - available_subscriptions = Subscription.list({'organization-id': org.id}, per_page=False) + available_subscriptions = module_target_sat.cli.Subscription.list( + {'organization-id': org.id}, per_page=False + ) assert len(available_subscriptions) > 0 available_subscription_ids = [subscription['id'] for subscription in available_subscriptions] subscription_id = choice(available_subscription_ids) - activation_key = make_activation_key({'name': ak_name, 'organization-id': org.id}) - ActivationKey.add_subscription({'id': activation_key['id'], 'subscription-id': subscription_id}) - subscriptions = ActivationKey.subscriptions( + activation_key = module_target_sat.cli_factory.make_activation_key( + {'name': ak_name, 'organization-id': org.id} + ) + module_target_sat.cli.ActivationKey.add_subscription( + {'id': activation_key['id'], 'subscription-id': subscription_id} + ) + subscriptions = module_target_sat.cli.ActivationKey.subscriptions( {'organization-id': org.id, 'id': activation_key['id']}, output_format='csv', ) assert len(subscriptions) == 1 - role = make_role({'organization-id': org.id}) + role = module_target_sat.cli_factory.make_role({'organization-id': org.id}) resource_permissions = { 'Katello::ActivationKey': { 'permissions': [ @@ -1560,8 +1626,8 @@ def test_positive_view_subscriptions_by_non_admin_user(module_entitlement_manife ] }, } - add_role_permissions(role['id'], resource_permissions) - user = make_user( + module_target_sat.cli_factory.add_role_permissions(role['id'], resource_permissions) + user = module_target_sat.cli_factory.user( { 'admin': False, 'default-organization-id': org.id, @@ -1570,8 +1636,8 @@ def test_positive_view_subscriptions_by_non_admin_user(module_entitlement_manife 'password': user_password, } ) - User.add_role({'id': user['id'], 'role-id': role['id']}) - ak_user_cli_session = ActivationKey.with_user(user_name, user_password) + module_target_sat.cli.User.add_role({'id': user['id'], 'role-id': role['id']}) + ak_user_cli_session = module_target_sat.cli.ActivationKey.with_user(user_name, user_password) subscriptions = ak_user_cli_session.subscriptions( {'organization-id': org.id, 'id': activation_key['id']}, output_format='csv', @@ -1602,7 +1668,7 @@ def test_positive_subscription_quantity_attached(function_org, rhel7_contenthost :BZ: 1633094 """ org = function_org - result = setup_org_for_a_rh_repo( + result = target_sat.cli_factory.setup_org_for_a_rh_repo( { 'product': PRDS['rhel'], 'repository-set': REPOSET['rhst7'], @@ -1611,8 +1677,8 @@ def test_positive_subscription_quantity_attached(function_org, rhel7_contenthost }, force_use_cdn=True, ) - ak = ActivationKey.info({'id': result['activationkey-id']}) - setup_org_for_a_custom_repo( + ak = target_sat.cli.ActivationKey.info({'id': result['activationkey-id']}) + target_sat.cli_factory.setup_org_for_a_custom_repo( { 'url': settings.repos.yum_0.url, 'organization-id': org['id'], @@ -1621,13 +1687,13 @@ def test_positive_subscription_quantity_attached(function_org, rhel7_contenthost 'lifecycle-environment-id': result['lifecycle-environment-id'], } ) - subs = Subscription.list({'organization-id': org['id']}, per_page=False) + subs = target_sat.cli.Subscription.list({'organization-id': org['id']}, per_page=False) subs_lookup = {s['id']: s for s in subs} rhel7_contenthost.install_katello_ca(target_sat) rhel7_contenthost.register_contenthost(org['label'], activation_key=ak['name']) assert rhel7_contenthost.subscribed - ak_subs = ActivationKey.subscriptions( + ak_subs = target_sat.cli.ActivationKey.subscriptions( {'activation-key': ak['name'], 'organization-id': org['id']}, output_format='json' ) assert len(ak_subs) == 2 # one for #rh product, one for custom product diff --git a/tests/foreman/cli/test_architecture.py b/tests/foreman/cli/test_architecture.py index ec212894310..4f0fe73061b 100644 --- a/tests/foreman/cli/test_architecture.py +++ b/tests/foreman/cli/test_architecture.py @@ -19,9 +19,7 @@ from fauxfactory import gen_choice import pytest -from robottelo.cli.architecture import Architecture -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_architecture +from robottelo.exceptions import CLIReturnCodeError from robottelo.utils.datafactory import ( invalid_id_list, invalid_values_list, @@ -34,12 +32,12 @@ class TestArchitecture: """Architecture CLI related tests.""" @pytest.fixture(scope='class') - def class_architecture(self): + def class_architecture(self, class_target_sat): """Shared architecture for tests""" - return make_architecture() + return class_target_sat.cli_factory.make_architecture() @pytest.mark.tier1 - def test_positive_CRUD(self): + def test_positive_CRUD(self, module_target_sat): """Create a new Architecture, update the name and delete the Architecture itself. :id: cd8654b8-e603-11ea-adc1-0242ac120002 @@ -52,18 +50,18 @@ def test_positive_CRUD(self): name = gen_choice(list(valid_data_list().values())) new_name = gen_choice(list(valid_data_list().values())) - architecture = make_architecture({'name': name}) + architecture = module_target_sat.cli_factory.make_architecture({'name': name}) assert architecture['name'] == name - Architecture.update({'id': architecture['id'], 'new-name': new_name}) - architecture = Architecture.info({'id': architecture['id']}) + module_target_sat.cli.Architecture.update({'id': architecture['id'], 'new-name': new_name}) + architecture = module_target_sat.cli.Architecture.info({'id': architecture['id']}) assert architecture['name'] == new_name - Architecture.delete({'id': architecture['id']}) + module_target_sat.cli.Architecture.delete({'id': architecture['id']}) with pytest.raises(CLIReturnCodeError): - Architecture.info({'id': architecture['id']}) + module_target_sat.cli.Architecture.info({'id': architecture['id']}) @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) - def test_negative_create_with_name(self, name): + def test_negative_create_with_name(self, name, module_target_sat): """Don't create an Architecture with invalid data. :id: cfed972e-9b09-4852-bdd2-b5a8a8aed170 @@ -76,13 +74,13 @@ def test_negative_create_with_name(self, name): """ with pytest.raises(CLIReturnCodeError) as error: - Architecture.create({'name': name}) + module_target_sat.cli.Architecture.create({'name': name}) assert 'Could not create the architecture:' in error.value.message @pytest.mark.tier1 @pytest.mark.parametrize('new_name', **parametrized(invalid_values_list())) - def test_negative_update_name(self, class_architecture, new_name): + def test_negative_update_name(self, class_architecture, new_name, module_target_sat): """Create Architecture then fail to update its name :id: 037c4892-5e62-46dd-a2ed-92243e870e40 @@ -95,16 +93,18 @@ def test_negative_update_name(self, class_architecture, new_name): """ with pytest.raises(CLIReturnCodeError) as error: - Architecture.update({'id': class_architecture['id'], 'new-name': new_name}) + module_target_sat.cli.Architecture.update( + {'id': class_architecture['id'], 'new-name': new_name} + ) assert 'Could not update the architecture:' in error.value.message - result = Architecture.info({'id': class_architecture['id']}) + result = module_target_sat.cli.Architecture.info({'id': class_architecture['id']}) assert class_architecture['name'] == result['name'] @pytest.mark.tier1 @pytest.mark.parametrize('entity_id', **parametrized(invalid_id_list())) - def test_negative_delete_by_id(self, entity_id): + def test_negative_delete_by_id(self, entity_id, module_target_sat): """Delete architecture by invalid ID :id: 78bae664-6493-4c74-a587-94170f20746e @@ -116,6 +116,6 @@ def test_negative_delete_by_id(self, entity_id): :CaseImportance: Medium """ with pytest.raises(CLIReturnCodeError) as error: - Architecture.delete({'id': entity_id}) + module_target_sat.cli.Architecture.delete({'id': entity_id}) assert 'Could not delete the architecture' in error.value.message diff --git a/tests/foreman/cli/test_auth.py b/tests/foreman/cli/test_auth.py index 51945a6445a..c57ca0ab277 100644 --- a/tests/foreman/cli/test_auth.py +++ b/tests/foreman/cli/test_auth.py @@ -21,14 +21,9 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.auth import Auth, AuthLogin -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_user -from robottelo.cli.org import Org -from robottelo.cli.settings import Settings -from robottelo.cli.user import User from robottelo.config import settings from robottelo.constants import HAMMER_CONFIG +from robottelo.exceptions import CLIReturnCodeError LOGEDIN_MSG = "Session exists, currently logged in as '{0}'" NOTCONF_MSG = "Credentials are not configured." @@ -52,18 +47,20 @@ def configure_sessions(satellite, enable=True, add_default_creds=False): @pytest.fixture(scope='module') -def admin_user(): +def admin_user(module_target_sat): """create the admin role user for tests""" uname_admin = gen_string('alpha') - return make_user({'login': uname_admin, 'password': password, 'admin': '1'}) + return module_target_sat.cli_factory.user( + {'login': uname_admin, 'password': password, 'admin': '1'} + ) @pytest.fixture(scope='module') -def non_admin_user(): +def non_admin_user(module_target_sat): """create the non-admin role user for tests""" uname_viewer = gen_string('alpha') - user = make_user({'login': uname_viewer, 'password': password}) - User.add_role({'login': uname_viewer, 'role': 'Viewer'}) + user = module_target_sat.cli_factory.user({'login': uname_viewer, 'password': password}) + module_target_sat.cli.User.add_role({'login': uname_viewer, 'role': 'Viewer'}) return user @@ -85,24 +82,24 @@ def test_positive_create_session(admin_user, target_sat): expires after specified time """ try: - idle_timeout = Settings.list({'search': 'name=idle_timeout'})[0]['value'] - Settings.set({'name': 'idle_timeout', 'value': 1}) + idle_timeout = target_sat.cli.Settings.list({'search': 'name=idle_timeout'})[0]['value'] + target_sat.cli.Settings.set({'name': 'idle_timeout', 'value': 1}) result = configure_sessions(target_sat) assert result == 0, 'Failed to configure hammer sessions' - AuthLogin.basic({'username': admin_user['login'], 'password': password}) - result = Auth.with_user().status() + target_sat.cli.AuthLogin.basic({'username': admin_user['login'], 'password': password}) + result = target_sat.cli.Auth.with_user().status() assert LOGEDIN_MSG.format(admin_user['login']) in result[0]['message'] # list organizations without supplying credentials - assert Org.with_user().list() + assert target_sat.cli.Org.with_user().list() # wait until session expires sleep(70) with pytest.raises(CLIReturnCodeError): - Org.with_user().list() - result = Auth.with_user().status() + target_sat.cli.Org.with_user().list() + result = target_sat.cli.Auth.with_user().status() assert NOTCONF_MSG in result[0]['message'] finally: # reset timeout to default - Settings.set({'name': 'idle_timeout', 'value': f'{idle_timeout}'}) + target_sat.cli.Settings.set({'name': 'idle_timeout', 'value': f'{idle_timeout}'}) @pytest.mark.tier1 @@ -124,18 +121,18 @@ def test_positive_disable_session(admin_user, target_sat): """ result = configure_sessions(target_sat) assert result == 0, 'Failed to configure hammer sessions' - AuthLogin.basic({'username': admin_user['login'], 'password': password}) - result = Auth.with_user().status() + target_sat.cli.AuthLogin.basic({'username': admin_user['login'], 'password': password}) + result = target_sat.cli.Auth.with_user().status() assert LOGEDIN_MSG.format(admin_user['login']) in result[0]['message'] # list organizations without supplying credentials - assert Org.with_user().list() + assert target_sat.cli.Org.with_user().list() # disabling sessions result = configure_sessions(satellite=target_sat, enable=False) assert result == 0, 'Failed to configure hammer sessions' - result = Auth.with_user().status() + result = target_sat.cli.Auth.with_user().status() assert NOTCONF_MSG in result[0]['message'] with pytest.raises(CLIReturnCodeError): - Org.with_user().list() + target_sat.cli.Org.with_user().list() @pytest.mark.tier1 @@ -156,16 +153,16 @@ def test_positive_log_out_from_session(admin_user, target_sat): """ result = configure_sessions(target_sat) assert result == 0, 'Failed to configure hammer sessions' - AuthLogin.basic({'username': admin_user['login'], 'password': password}) - result = Auth.with_user().status() + target_sat.cli.AuthLogin.basic({'username': admin_user['login'], 'password': password}) + result = target_sat.cli.Auth.with_user().status() assert LOGEDIN_MSG.format(admin_user['login']) in result[0]['message'] # list organizations without supplying credentials - assert Org.with_user().list() - Auth.logout() - result = Auth.with_user().status() + assert target_sat.cli.Org.with_user().list() + target_sat.cli.Auth.logout() + result = target_sat.cli.Auth.with_user().status() assert NOTCONF_MSG in result[0]['message'] with pytest.raises(CLIReturnCodeError): - Org.with_user().list() + target_sat.cli.Org.with_user().list() @pytest.mark.tier1 @@ -188,15 +185,15 @@ def test_positive_change_session(admin_user, non_admin_user, target_sat): """ result = configure_sessions(target_sat) assert result == 0, 'Failed to configure hammer sessions' - AuthLogin.basic({'username': admin_user['login'], 'password': password}) - result = Auth.with_user().status() + target_sat.cli.AuthLogin.basic({'username': admin_user['login'], 'password': password}) + result = target_sat.cli.Auth.with_user().status() assert LOGEDIN_MSG.format(admin_user['login']) in result[0]['message'] # list organizations without supplying credentials - assert User.with_user().list() - AuthLogin.basic({'username': non_admin_user['login'], 'password': password}) - result = Auth.with_user().status() + assert target_sat.cli.User.with_user().list() + target_sat.cli.AuthLogin.basic({'username': non_admin_user['login'], 'password': password}) + result = target_sat.cli.Auth.with_user().status() assert LOGEDIN_MSG.format(non_admin_user['login']) in result[0]['message'] - assert User.with_user().list() + assert target_sat.cli.User.with_user().list() @pytest.mark.tier1 @@ -219,16 +216,16 @@ def test_positive_session_survives_unauthenticated_call(admin_user, target_sat): """ result = configure_sessions(target_sat) assert result == 0, 'Failed to configure hammer sessions' - AuthLogin.basic({'username': admin_user['login'], 'password': password}) - result = Auth.with_user().status() + target_sat.cli.AuthLogin.basic({'username': admin_user['login'], 'password': password}) + result = target_sat.cli.Auth.with_user().status() assert LOGEDIN_MSG.format(admin_user['login']) in result[0]['message'] # list organizations without supplying credentials - Org.with_user().list() + target_sat.cli.Org.with_user().list() result = target_sat.execute('hammer ping') assert result.status == 0, 'Failed to run hammer ping' - result = Auth.with_user().status() + result = target_sat.cli.Auth.with_user().status() assert LOGEDIN_MSG.format(admin_user['login']) in result[0]['message'] - Org.with_user().list() + target_sat.cli.Org.with_user().list() @pytest.mark.tier1 @@ -251,17 +248,19 @@ def test_positive_session_survives_failed_login(admin_user, non_admin_user, targ """ result = configure_sessions(target_sat) assert result == 0, 'Failed to configure hammer sessions' - AuthLogin.basic({'username': admin_user['login'], 'password': password}) - result = Auth.with_user().status() + target_sat.cli.AuthLogin.basic({'username': admin_user['login'], 'password': password}) + result = target_sat.cli.Auth.with_user().status() assert LOGEDIN_MSG.format(admin_user['login']) in result[0]['message'] - Org.with_user().list() + target_sat.cli.Org.with_user().list() # using invalid password with pytest.raises(CLIReturnCodeError): - AuthLogin.basic({'username': non_admin_user['login'], 'password': gen_string('alpha')}) + target_sat.cli.AuthLogin.basic( + {'username': non_admin_user['login'], 'password': gen_string('alpha')} + ) # checking the session status again - result = Auth.with_user().status() + result = target_sat.cli.Auth.with_user().status() assert LOGEDIN_MSG.format(admin_user['login']) in result[0]['message'] - Org.with_user().list() + target_sat.cli.Org.with_user().list() @pytest.mark.e2e @@ -289,20 +288,20 @@ def test_positive_session_preceeds_saved_credentials(admin_user, target_sat): """ try: - idle_timeout = Settings.list({'search': 'name=idle_timeout'})[0]['value'] - Settings.set({'name': 'idle_timeout', 'value': 1}) + idle_timeout = target_sat.cli.Settings.list({'search': 'name=idle_timeout'})[0]['value'] + target_sat.cli.Settings.set({'name': 'idle_timeout', 'value': 1}) result = configure_sessions(satellite=target_sat, add_default_creds=True) assert result == 0, 'Failed to configure hammer sessions' - AuthLogin.basic({'username': admin_user['login'], 'password': password}) - result = Auth.with_user().status() + target_sat.cli.AuthLogin.basic({'username': admin_user['login'], 'password': password}) + result = target_sat.cli.Auth.with_user().status() assert LOGEDIN_MSG.format(admin_user['login']) in result[0]['message'] # list organizations without supplying credentials sleep(70) with pytest.raises(CLIReturnCodeError): - Org.with_user().list() + target_sat.cli.Org.with_user().list() finally: # reset timeout to default - Settings.set({'name': 'idle_timeout', 'value': f'{idle_timeout}'}) + target_sat.cli.Settings.set({'name': 'idle_timeout', 'value': f'{idle_timeout}'}) @pytest.mark.tier1 @@ -317,10 +316,10 @@ def test_negative_no_credentials(target_sat): """ result = configure_sessions(satellite=target_sat, enable=False) assert result == 0, 'Failed to configure hammer sessions' - result = Auth.with_user().status() + result = target_sat.cli.Auth.with_user().status() assert NOTCONF_MSG in result[0]['message'] with pytest.raises(CLIReturnCodeError): - Org.with_user().list() + target_sat.cli.Org.with_user().list() @pytest.mark.tier1 @@ -336,9 +335,11 @@ def test_negative_no_permissions(admin_user, non_admin_user, target_sat): """ result = configure_sessions(target_sat) assert result == 0, 'Failed to configure hammer sessions' - AuthLogin.basic({'username': non_admin_user['login'], 'password': password}) - result = Auth.with_user().status() + target_sat.cli.AuthLogin.basic({'username': non_admin_user['login'], 'password': password}) + result = target_sat.cli.Auth.with_user().status() assert LOGEDIN_MSG.format(non_admin_user['login']) in result[0]['message'] # try to update user from viewer's session with pytest.raises(CLIReturnCodeError): - User.with_user().update({'login': admin_user['login'], 'new-login': gen_string('alpha')}) + target_sat.cli.User.with_user().update( + {'login': admin_user['login'], 'new-login': gen_string('alpha')} + ) diff --git a/tests/foreman/cli/test_capsule.py b/tests/foreman/cli/test_capsule.py index 8dee2e6a3dc..94d8fdb8f0c 100644 --- a/tests/foreman/cli/test_capsule.py +++ b/tests/foreman/cli/test_capsule.py @@ -18,14 +18,12 @@ """ import pytest -from robottelo.cli.proxy import Proxy - pytestmark = [pytest.mark.run_in_one_thread] @pytest.mark.skip_if_not_set('fake_capsules') @pytest.mark.tier1 -def test_positive_import_puppet_classes(session_puppet_enabled_sat, puppet_proxy_port_range): +def test_positive_import_puppet_classes(session_puppet_enabled_sat): """Import puppet classes from proxy :id: 42e3a9c0-62e1-4049-9667-f3c0cdfe0b04 @@ -38,8 +36,8 @@ def test_positive_import_puppet_classes(session_puppet_enabled_sat, puppet_proxy port = puppet_sat.available_capsule_port with puppet_sat.default_url_on_new_port(9090, port) as url: proxy = puppet_sat.cli_factory.make_proxy({'url': url}) - Proxy.import_classes({'id': proxy['id']}) - Proxy.delete({'id': proxy['id']}) + puppet_sat.cli.Proxy.import_classes({'id': proxy['id']}) + puppet_sat.cli.Proxy.delete({'id': proxy['id']}) @pytest.mark.stubbed diff --git a/tests/foreman/cli/test_classparameters.py b/tests/foreman/cli/test_classparameters.py index f48019c4963..aa0d6e3fb23 100644 --- a/tests/foreman/cli/test_classparameters.py +++ b/tests/foreman/cli/test_classparameters.py @@ -18,8 +18,8 @@ """ import pytest -from robottelo.cli.base import CLIReturnCodeError from robottelo.config import settings +from robottelo.exceptions import CLIReturnCodeError from robottelo.utils.datafactory import gen_string @@ -91,7 +91,7 @@ def test_positive_list( environment=module_puppet['env'].name, ).create() host.add_puppetclass(data={'puppetclass_id': module_puppet['class']['id']}) - hostgroup = session_puppet_enabled_sat.cli_factory.make_hostgroup( + hostgroup = session_puppet_enabled_sat.cli_factory.hostgroup( { 'puppet-environment-id': module_puppet['env'].id, 'puppet-class-ids': module_puppet['class']['id'], @@ -149,9 +149,7 @@ def test_positive_list_with_non_admin_user(self, session_puppet_enabled_sat, mod ] }, } - user = session_puppet_enabled_sat.cli_factory.make_user( - {'admin': '0', 'password': password} - ) + user = session_puppet_enabled_sat.cli_factory.user({'admin': '0', 'password': password}) role = session_puppet_enabled_sat.cli_factory.make_role() session_puppet_enabled_sat.cli_factory.add_role_permissions( role['id'], required_user_permissions diff --git a/tests/foreman/cli/test_computeresource_azurerm.py b/tests/foreman/cli/test_computeresource_azurerm.py index 998175e48ed..e2814110372 100644 --- a/tests/foreman/cli/test_computeresource_azurerm.py +++ b/tests/foreman/cli/test_computeresource_azurerm.py @@ -19,8 +19,6 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.computeresource import ComputeResource -from robottelo.cli.host import Host from robottelo.config import settings from robottelo.constants import ( AZURERM_FILE_URI, @@ -117,7 +115,7 @@ def test_positive_crud_azurerm_cr( # Delete CR sat_azure.cli.ComputeResource.delete({'name': result['name']}) - assert not ComputeResource.exists(search=('name', result['name'])) + assert not sat_azure.cli.ComputeResource.exists(search=('name', result['name'])) @pytest.mark.upgrade @pytest.mark.tier2 @@ -377,8 +375,8 @@ def class_host_ft( ) yield host with sat_azure.api_factory.satellite_setting('destroy_vm_on_host_delete=True'): - if Host.exists(search=('name', host['name'])): - Host.delete({'name': self.fullhostname}, timeout=1800000) + if sat_azure.cli.Host.exists(search=('name', host['name'])): + sat_azure.cli.Host.delete({'name': self.fullhostname}, timeout=1800000) @pytest.fixture(scope='class') def azureclient_host(self, azurermclient, class_host_ft): @@ -505,8 +503,8 @@ def class_host_ud( ) yield host with sat_azure.api_factory.satellite_setting('destroy_vm_on_host_delete=True'): - if Host.exists(search=('name', host['name'])): - Host.delete({'name': self.fullhostname}, timeout=1800000) + if sat_azure.cli.Host.exists(search=('name', host['name'])): + sat_azure.cli.Host.delete({'name': self.fullhostname}, timeout=1800000) @pytest.fixture(scope='class') def azureclient_host(self, azurermclient, class_host_ud): diff --git a/tests/foreman/cli/test_computeresource_ec2.py b/tests/foreman/cli/test_computeresource_ec2.py index e0f3d8cc4c3..61efdd42839 100644 --- a/tests/foreman/cli/test_computeresource_ec2.py +++ b/tests/foreman/cli/test_computeresource_ec2.py @@ -16,18 +16,16 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.factory import make_compute_resource, make_location, make_org -from robottelo.cli.org import Org from robottelo.config import settings from robottelo.constants import EC2_REGION_CA_CENTRAL_1, FOREMAN_PROVIDERS @pytest.fixture(scope='module') -def aws(): +def aws(module_target_sat): aws = type('rhev', (object,), {})() - aws.org = make_org() - aws.loc = make_location() - Org.add_location({'id': aws.org['id'], 'location-id': aws.loc['id']}) + aws.org = module_target_sat.cli_factory.make_org() + aws.loc = module_target_sat.cli_factory.make_location() + module_target_sat.cli.Org.add_location({'id': aws.org['id'], 'location-id': aws.loc['id']}) aws.aws_access_key = settings.ec2.access_key aws.aws_secret_key = settings.ec2.secret_key aws.aws_region = settings.ec2.region @@ -41,7 +39,7 @@ def aws(): @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_create_ec2_with_custom_region(aws): +def test_positive_create_ec2_with_custom_region(aws, module_target_sat): """Create a new ec2 compute resource with custom region :id: 28eb592d-ebf0-4659-900a-87112b3b2ad7 @@ -60,7 +58,7 @@ def test_positive_create_ec2_with_custom_region(aws): """ cr_name = gen_string(str_type='alpha') cr_description = gen_string(str_type='alpha') - cr = make_compute_resource( + cr = module_target_sat.cli_factory.compute_resource( { 'name': cr_name, 'description': cr_description, diff --git a/tests/foreman/cli/test_computeresource_libvirt.py b/tests/foreman/cli/test_computeresource_libvirt.py index c1144c88733..25643439ce3 100644 --- a/tests/foreman/cli/test_computeresource_libvirt.py +++ b/tests/foreman/cli/test_computeresource_libvirt.py @@ -39,11 +39,9 @@ import pytest from wait_for import wait_for -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.computeresource import ComputeResource -from robottelo.cli.factory import make_compute_resource, make_location from robottelo.config import settings from robottelo.constants import FOREMAN_PROVIDERS, LIBVIRT_RESOURCE_URL +from robottelo.exceptions import CLIReturnCodeError from robottelo.utils.datafactory import parametrized from robottelo.utils.issue_handlers import is_open @@ -114,7 +112,7 @@ def libvirt_url(): @pytest.mark.tier1 -def test_positive_create_with_name(libvirt_url): +def test_positive_create_with_name(libvirt_url, module_target_sat): """Create Compute Resource :id: 6460bcc7-d7f7-406a-aecb-b3d54d51e697 @@ -125,7 +123,7 @@ def test_positive_create_with_name(libvirt_url): :CaseLevel: Component """ - ComputeResource.create( + module_target_sat.cli.ComputeResource.create( { 'name': f'cr {gen_string("alpha")}', 'provider': 'Libvirt', @@ -135,7 +133,7 @@ def test_positive_create_with_name(libvirt_url): @pytest.mark.tier1 -def test_positive_info(libvirt_url): +def test_positive_info(libvirt_url, module_target_sat): """Test Compute Resource Info :id: f54af041-4471-4d8e-9429-45d821df0440 @@ -147,7 +145,7 @@ def test_positive_info(libvirt_url): :CaseLevel: Component """ name = gen_string('utf8') - compute_resource = make_compute_resource( + compute_resource = module_target_sat.cli_factory.compute_resource( { 'name': name, 'provider': FOREMAN_PROVIDERS['libvirt'], @@ -159,7 +157,7 @@ def test_positive_info(libvirt_url): @pytest.mark.tier1 -def test_positive_list(libvirt_url): +def test_positive_list(libvirt_url, module_target_sat): """Test Compute Resource List :id: 11123361-ffbc-4c59-a0df-a4af3408af7a @@ -170,17 +168,21 @@ def test_positive_list(libvirt_url): :CaseLevel: Component """ - comp_res = make_compute_resource({'provider': FOREMAN_PROVIDERS['libvirt'], 'url': libvirt_url}) + comp_res = module_target_sat.cli_factory.compute_resource( + {'provider': FOREMAN_PROVIDERS['libvirt'], 'url': libvirt_url} + ) assert comp_res['name'] - result_list = ComputeResource.list({'search': 'name=%s' % comp_res['name']}) + result_list = module_target_sat.cli.ComputeResource.list( + {'search': 'name=%s' % comp_res['name']} + ) assert len(result_list) > 0 - result = ComputeResource.exists(search=('name', comp_res['name'])) + result = module_target_sat.cli.ComputeResource.exists(search=('name', comp_res['name'])) assert result @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_delete_by_name(libvirt_url): +def test_positive_delete_by_name(libvirt_url, module_target_sat): """Test Compute Resource delete :id: 7fcc0b66-f1c1-4194-8a4b-7f04b1dd439a @@ -191,10 +193,12 @@ def test_positive_delete_by_name(libvirt_url): :CaseLevel: Component """ - comp_res = make_compute_resource({'provider': FOREMAN_PROVIDERS['libvirt'], 'url': libvirt_url}) + comp_res = module_target_sat.cli_factory.compute_resource( + {'provider': FOREMAN_PROVIDERS['libvirt'], 'url': libvirt_url} + ) assert comp_res['name'] - ComputeResource.delete({'name': comp_res['name']}) - result = ComputeResource.exists(search=('name', comp_res['name'])) + module_target_sat.cli.ComputeResource.delete({'name': comp_res['name']}) + result = module_target_sat.cli.ComputeResource.exists(search=('name', comp_res['name'])) assert len(result) == 0 @@ -202,7 +206,7 @@ def test_positive_delete_by_name(libvirt_url): @pytest.mark.tier1 @pytest.mark.upgrade @pytest.mark.parametrize('options', **parametrized(valid_name_desc_data())) -def test_positive_create_with_libvirt(libvirt_url, options): +def test_positive_create_with_libvirt(libvirt_url, options, target_sat): """Test Compute Resource create :id: adc6f4f8-6420-4044-89d1-c69e0bfeeab9 @@ -215,7 +219,7 @@ def test_positive_create_with_libvirt(libvirt_url, options): :parametrized: yes """ - ComputeResource.create( + target_sat.cli.ComputeResource.create( { 'description': options['description'], 'name': options['name'], @@ -226,7 +230,7 @@ def test_positive_create_with_libvirt(libvirt_url, options): @pytest.mark.tier2 -def test_positive_create_with_loc(libvirt_url): +def test_positive_create_with_loc(libvirt_url, module_target_sat): """Create Compute Resource with location :id: 224c7cbc-6bac-4a94-8141-d6249896f5a2 @@ -237,14 +241,14 @@ def test_positive_create_with_loc(libvirt_url): :CaseLevel: Integration """ - location = make_location() - comp_resource = make_compute_resource({'location-ids': location['id']}) + location = module_target_sat.cli_factory.make_location() + comp_resource = module_target_sat.cli_factory.compute_resource({'location-ids': location['id']}) assert len(comp_resource['locations']) == 1 assert comp_resource['locations'][0] == location['name'] @pytest.mark.tier2 -def test_positive_create_with_locs(libvirt_url): +def test_positive_create_with_locs(libvirt_url, module_target_sat): """Create Compute Resource with multiple locations :id: f665c586-39bf-480a-a0fc-81d9e1eb7c54 @@ -257,8 +261,8 @@ def test_positive_create_with_locs(libvirt_url): :CaseLevel: Integration """ locations_amount = random.randint(3, 5) - locations = [make_location() for _ in range(locations_amount)] - comp_resource = make_compute_resource( + locations = [module_target_sat.cli_factory.make_location() for _ in range(locations_amount)] + comp_resource = module_target_sat.cli_factory.compute_resource( {'location-ids': [location['id'] for location in locations]} ) assert len(comp_resource['locations']) == locations_amount @@ -271,7 +275,7 @@ def test_positive_create_with_locs(libvirt_url): @pytest.mark.tier2 @pytest.mark.parametrize('options', **parametrized(invalid_create_data())) -def test_negative_create_with_name_url(libvirt_url, options): +def test_negative_create_with_name_url(libvirt_url, options, target_sat): """Compute Resource negative create with invalid values :id: cd432ff3-b3b9-49cd-9a16-ed00d81679dd @@ -285,7 +289,7 @@ def test_negative_create_with_name_url(libvirt_url, options): :parametrized: yes """ with pytest.raises(CLIReturnCodeError): - ComputeResource.create( + target_sat.cli.ComputeResource.create( { 'name': options.get('name', gen_string(str_type='alphanumeric')), 'provider': FOREMAN_PROVIDERS['libvirt'], @@ -295,7 +299,7 @@ def test_negative_create_with_name_url(libvirt_url, options): @pytest.mark.tier2 -def test_negative_create_with_same_name(libvirt_url): +def test_negative_create_with_same_name(libvirt_url, module_target_sat): """Compute Resource negative create with the same name :id: ddb5c45b-1ea3-46d0-b248-56c0388d2e4b @@ -306,9 +310,9 @@ def test_negative_create_with_same_name(libvirt_url): :CaseLevel: Component """ - comp_res = make_compute_resource() + comp_res = module_target_sat.cli_factory.compute_resource() with pytest.raises(CLIReturnCodeError): - ComputeResource.create( + module_target_sat.cli.ComputeResource.create( { 'name': comp_res['name'], 'provider': FOREMAN_PROVIDERS['libvirt'], @@ -322,7 +326,7 @@ def test_negative_create_with_same_name(libvirt_url): @pytest.mark.tier1 @pytest.mark.parametrize('options', **parametrized(valid_update_data())) -def test_positive_update_name(libvirt_url, options): +def test_positive_update_name(libvirt_url, options, module_target_sat): """Compute Resource positive update :id: 213d7f04-4c54-4985-8ca0-d2a1a9e3b305 @@ -335,12 +339,12 @@ def test_positive_update_name(libvirt_url, options): :parametrized: yes """ - comp_res = make_compute_resource() + comp_res = module_target_sat.cli_factory.compute_resource() options.update({'name': comp_res['name']}) # update Compute Resource - ComputeResource.update(options) + module_target_sat.cli.ComputeResource.update(options) # check updated values - result = ComputeResource.info({'id': comp_res['id']}) + result = module_target_sat.cli.ComputeResource.info({'id': comp_res['id']}) assert result['description'] == options.get('description', comp_res['description']) assert result['name'] == options.get('new-name', comp_res['name']) assert result['url'] == options.get('url', comp_res['url']) @@ -352,7 +356,7 @@ def test_positive_update_name(libvirt_url, options): @pytest.mark.tier2 @pytest.mark.parametrize('options', **parametrized(invalid_update_data())) -def test_negative_update(libvirt_url, options): +def test_negative_update(libvirt_url, options, module_target_sat): """Compute Resource negative update :id: e7aa9b39-dd01-4f65-8e89-ff5a6f4ee0e3 @@ -365,10 +369,10 @@ def test_negative_update(libvirt_url, options): :parametrized: yes """ - comp_res = make_compute_resource() + comp_res = module_target_sat.cli_factory.compute_resource() with pytest.raises(CLIReturnCodeError): - ComputeResource.update(dict({'name': comp_res['name']}, **options)) - result = ComputeResource.info({'id': comp_res['id']}) + module_target_sat.cli.ComputeResource.update(dict({'name': comp_res['name']}, **options)) + result = module_target_sat.cli.ComputeResource.info({'id': comp_res['id']}) # check attributes have not changed assert result['name'] == comp_res['name'] options.pop('new-name', None) @@ -378,7 +382,9 @@ def test_negative_update(libvirt_url, options): @pytest.mark.tier2 @pytest.mark.parametrize('set_console_password', ['true', 'false']) -def test_positive_create_with_console_password_and_name(libvirt_url, set_console_password): +def test_positive_create_with_console_password_and_name( + libvirt_url, set_console_password, module_target_sat +): """Create a compute resource with ``--set-console-password``. :id: 5b4c838a-0265-4c71-a73d-305fecbe508a @@ -393,7 +399,7 @@ def test_positive_create_with_console_password_and_name(libvirt_url, set_console :parametrized: yes """ - ComputeResource.create( + module_target_sat.cli.ComputeResource.create( { 'name': gen_string('utf8'), 'provider': 'Libvirt', @@ -405,7 +411,7 @@ def test_positive_create_with_console_password_and_name(libvirt_url, set_console @pytest.mark.tier2 @pytest.mark.parametrize('set_console_password', ['true', 'false']) -def test_positive_update_console_password(libvirt_url, set_console_password): +def test_positive_update_console_password(libvirt_url, set_console_password, module_target_sat): """Update a compute resource with ``--set-console-password``. :id: ef09351e-dcd3-4b4f-8d3b-995e9e5873b3 @@ -421,8 +427,12 @@ def test_positive_update_console_password(libvirt_url, set_console_password): :parametrized: yes """ cr_name = gen_string('utf8') - ComputeResource.create({'name': cr_name, 'provider': 'Libvirt', 'url': gen_url()}) - ComputeResource.update({'name': cr_name, 'set-console-password': set_console_password}) + module_target_sat.cli.ComputeResource.create( + {'name': cr_name, 'provider': 'Libvirt', 'url': gen_url()} + ) + module_target_sat.cli.ComputeResource.update( + {'name': cr_name, 'set-console-password': set_console_password} + ) @pytest.mark.e2e diff --git a/tests/foreman/cli/test_computeresource_osp.py b/tests/foreman/cli/test_computeresource_osp.py index acb74283973..1d76fe561d4 100644 --- a/tests/foreman/cli/test_computeresource_osp.py +++ b/tests/foreman/cli/test_computeresource_osp.py @@ -19,8 +19,8 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.factory import CLIReturnCodeError from robottelo.config import settings +from robottelo.exceptions import CLIReturnCodeError OSP_SETTINGS = Box( username=settings.osp.username, diff --git a/tests/foreman/cli/test_computeresource_rhev.py b/tests/foreman/cli/test_computeresource_rhev.py index 12e2fe9b345..5036bd2fd7c 100644 --- a/tests/foreman/cli/test_computeresource_rhev.py +++ b/tests/foreman/cli/test_computeresource_rhev.py @@ -20,14 +20,8 @@ from wait_for import wait_for from wrapanapi import RHEVMSystem -from robottelo.cli.computeresource import ComputeResource -from robottelo.cli.factory import ( - CLIFactoryError, - CLIReturnCodeError, - make_compute_resource, -) -from robottelo.cli.host import Host from robottelo.config import settings +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError @pytest.fixture(scope='module') @@ -47,7 +41,7 @@ def rhev(): @pytest.mark.tier1 -def test_positive_create_rhev_with_valid_name(rhev): +def test_positive_create_rhev_with_valid_name(rhev, module_target_sat): """Create Compute Resource of type Rhev with valid name :id: 92a577db-144e-4761-a52e-e83887464986 @@ -58,7 +52,7 @@ def test_positive_create_rhev_with_valid_name(rhev): :BZ: 1602835 """ - ComputeResource.create( + module_target_sat.cli.ComputeResource.create( { 'name': f'cr {gen_string(str_type="alpha")}', 'provider': 'Ovirt', @@ -71,7 +65,7 @@ def test_positive_create_rhev_with_valid_name(rhev): @pytest.mark.tier1 -def test_positive_rhev_info(rhev): +def test_positive_rhev_info(rhev, module_target_sat): """List the info of RHEV compute resource :id: 1b18f6e8-c431-41ab-ae49-a2bbb74712f2 @@ -83,7 +77,7 @@ def test_positive_rhev_info(rhev): :BZ: 1602835 """ name = gen_string('utf8') - compute_resource = make_compute_resource( + compute_resource = module_target_sat.cli_factory.compute_resource( { 'name': name, 'provider': 'Ovirt', @@ -97,7 +91,7 @@ def test_positive_rhev_info(rhev): @pytest.mark.tier1 -def test_positive_delete_by_name(rhev): +def test_positive_delete_by_name(rhev, module_target_sat): """Delete the RHEV compute resource by name :id: ac84acbe-3e02-4f49-9695-b668df28b353 @@ -108,7 +102,7 @@ def test_positive_delete_by_name(rhev): :BZ: 1602835 """ - comp_res = make_compute_resource( + comp_res = module_target_sat.cli_factory.compute_resource( { 'provider': 'Ovirt', 'user': rhev.username, @@ -118,13 +112,13 @@ def test_positive_delete_by_name(rhev): } ) assert comp_res['name'] - ComputeResource.delete({'name': comp_res['name']}) - result = ComputeResource.exists(search=('name', comp_res['name'])) + module_target_sat.cli.ComputeResource.delete({'name': comp_res['name']}) + result = module_target_sat.cli.ComputeResource.exists(search=('name', comp_res['name'])) assert not result @pytest.mark.tier1 -def test_positive_delete_by_id(rhev): +def test_positive_delete_by_id(rhev, module_target_sat): """Delete the RHEV compute resource by id :id: 4bcd4fa3-df8b-4773-b142-e47458116552 @@ -135,7 +129,7 @@ def test_positive_delete_by_id(rhev): :BZ: 1602835 """ - comp_res = make_compute_resource( + comp_res = module_target_sat.cli_factory.compute_resource( { 'provider': 'Ovirt', 'user': rhev.username, @@ -145,13 +139,13 @@ def test_positive_delete_by_id(rhev): } ) assert comp_res['name'] - ComputeResource.delete({'id': comp_res['id']}) - result = ComputeResource.exists(search=('name', comp_res['name'])) + module_target_sat.cli.ComputeResource.delete({'id': comp_res['id']}) + result = module_target_sat.cli.ComputeResource.exists(search=('name', comp_res['name'])) assert not result @pytest.mark.tier2 -def test_negative_create_rhev_with_url(rhev): +def test_negative_create_rhev_with_url(rhev, module_target_sat): """RHEV compute resource negative create with invalid values :id: 1f318a4b-8dca-491b-b56d-cff773ed624e @@ -161,7 +155,7 @@ def test_negative_create_rhev_with_url(rhev): :CaseImportance: High """ with pytest.raises(CLIReturnCodeError): - ComputeResource.create( + module_target_sat.cli.ComputeResource.create( { 'provider': 'Ovirt', 'user': rhev.username, @@ -173,7 +167,7 @@ def test_negative_create_rhev_with_url(rhev): @pytest.mark.tier2 -def test_negative_create_with_same_name(rhev): +def test_negative_create_with_same_name(rhev, module_target_sat): """RHEV compute resource negative create with the same name :id: f00813ef-df31-462c-aa87-479b8272aea3 @@ -188,7 +182,7 @@ def test_negative_create_with_same_name(rhev): :CaseImportance: High """ name = gen_string('alpha') - compute_resource = make_compute_resource( + compute_resource = module_target_sat.cli_factory.compute_resource( { 'name': name, 'provider': 'Ovirt', @@ -200,7 +194,7 @@ def test_negative_create_with_same_name(rhev): ) assert compute_resource['name'] == name with pytest.raises(CLIFactoryError): - make_compute_resource( + module_target_sat.cli_factory.compute_resource( { 'name': name, 'provider': 'Ovirt', @@ -214,7 +208,7 @@ def test_negative_create_with_same_name(rhev): @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_update_name(rhev): +def test_positive_update_name(rhev, module_target_sat): """RHEV compute resource positive update :id: 5ca29b81-d1f0-409f-843d-aa5daf957d7f @@ -231,7 +225,7 @@ def test_positive_update_name(rhev): :BZ: 1602835 """ new_name = gen_string('alpha') - comp_res = make_compute_resource( + comp_res = module_target_sat.cli_factory.compute_resource( { 'provider': 'Ovirt', 'user': rhev.username, @@ -241,12 +235,12 @@ def test_positive_update_name(rhev): } ) assert comp_res['name'] - ComputeResource.update({'name': comp_res['name'], 'new-name': new_name}) - assert new_name == ComputeResource.info({'id': comp_res['id']})['name'] + module_target_sat.cli.ComputeResource.update({'name': comp_res['name'], 'new-name': new_name}) + assert new_name == module_target_sat.cli.ComputeResource.info({'id': comp_res['id']})['name'] @pytest.mark.tier2 -def test_positive_add_image_rhev_with_name(rhev, module_os): +def test_positive_add_image_rhev_with_name(rhev, module_os, module_target_sat): """Add images to the RHEV compute resource :id: 2da84165-a56f-4282-9343-94828fa69c13 @@ -265,7 +259,7 @@ def test_positive_add_image_rhev_with_name(rhev, module_os): if rhev.image_uuid is None: pytest.skip('Missing configuration for rhev.image_uuid') - comp_res = make_compute_resource( + comp_res = module_target_sat.cli_factory.compute_resource( { 'provider': 'Ovirt', 'user': rhev.username, @@ -275,7 +269,7 @@ def test_positive_add_image_rhev_with_name(rhev, module_os): } ) assert comp_res['name'] - ComputeResource.image_create( + module_target_sat.cli.ComputeResource.image_create( { 'compute-resource': comp_res['name'], 'name': f'img {gen_string(str_type="alpha")}', @@ -285,13 +279,15 @@ def test_positive_add_image_rhev_with_name(rhev, module_os): 'username': "root", } ) - result = ComputeResource.image_list({'compute-resource': comp_res['name']}) + result = module_target_sat.cli.ComputeResource.image_list( + {'compute-resource': comp_res['name']} + ) assert result[0]['uuid'] == rhev.image_uuid @pytest.mark.skip_if_open("BZ:1829239") @pytest.mark.tier2 -def test_negative_add_image_rhev_with_invalid_uuid(rhev, module_os): +def test_negative_add_image_rhev_with_invalid_uuid(rhev, module_os, module_target_sat): """Attempt to add invalid image to the RHEV compute resource :id: e8a653f9-9749-4c76-95ed-2411a7c0a117 @@ -309,7 +305,7 @@ def test_negative_add_image_rhev_with_invalid_uuid(rhev, module_os): :BZ: 1829239 """ - comp_res = make_compute_resource( + comp_res = module_target_sat.cli_factory.compute_resource( { 'provider': 'Ovirt', 'user': rhev.username, @@ -320,7 +316,7 @@ def test_negative_add_image_rhev_with_invalid_uuid(rhev, module_os): ) assert comp_res['name'] with pytest.raises(CLIReturnCodeError): - ComputeResource.image_create( + module_target_sat.cli.ComputeResource.image_create( { 'compute-resource': comp_res['name'], 'name': f'img {gen_string(str_type="alpha")}', @@ -333,7 +329,7 @@ def test_negative_add_image_rhev_with_invalid_uuid(rhev, module_os): @pytest.mark.tier2 -def test_negative_add_image_rhev_with_invalid_name(rhev, module_os): +def test_negative_add_image_rhev_with_invalid_name(rhev, module_os, module_target_sat): """Attempt to add invalid image name to the RHEV compute resource :id: 873a7d79-1e89-4e4f-81ca-b6db1e0246da @@ -353,7 +349,7 @@ def test_negative_add_image_rhev_with_invalid_name(rhev, module_os): if rhev.image_uuid is None: pytest.skip('Missing configuration for rhev.image_uuid') - comp_res = make_compute_resource( + comp_res = module_target_sat.cli_factory.compute_resource( { 'provider': 'Ovirt', 'user': rhev.username, @@ -365,7 +361,7 @@ def test_negative_add_image_rhev_with_invalid_name(rhev, module_os): assert comp_res['name'] with pytest.raises(CLIReturnCodeError): - ComputeResource.image_create( + module_target_sat.cli.ComputeResource.image_create( { 'compute-resource': comp_res['name'], # too long string (>255 chars) @@ -493,7 +489,7 @@ def test_positive_provision_rhev_with_host_group( # checks hostname = f'{host_name}.{domain_name}' assert hostname == host['name'] - host_info = Host.info({'name': hostname}) + host_info = cli.Host.info({'name': hostname}) # Check on RHV, if VM exists assert rhev.rhv_api.does_vm_exist(hostname) # Get the information of created VM @@ -658,7 +654,7 @@ def test_positive_provision_rhev_image_based_and_disassociate( ) hostname = f'{host_name}.{domain_name}' assert hostname == host['name'] - host_info = Host.info({'name': hostname}) + host_info = cli.Host.info({'name': hostname}) # Check on RHV, if VM exists assert rhev.rhv_api.does_vm_exist(hostname) # Get the information of created VM diff --git a/tests/foreman/cli/test_container_management.py b/tests/foreman/cli/test_container_management.py index 70de242f8ac..9229260475a 100644 --- a/tests/foreman/cli/test_container_management.py +++ b/tests/foreman/cli/test_container_management.py @@ -16,15 +16,6 @@ import pytest from wait_for import wait_for -from robottelo.cli.factory import ( - ContentView, - LifecycleEnvironment, - Repository, - make_content_view, - make_lifecycle_environment, - make_product_wait, - make_repository, -) from robottelo.config import settings from robottelo.constants import ( CONTAINER_REGISTRY_HUB, @@ -34,7 +25,7 @@ from robottelo.logging import logger -def _repo(product_id, name=None, upstream_name=None, url=None): +def _repo(sat, product_id, name=None, upstream_name=None, url=None): """Creates a Docker-based repository. :param product_id: ID of the ``Product``. @@ -46,7 +37,7 @@ def _repo(product_id, name=None, upstream_name=None, url=None): CONTAINER_REGISTRY_HUB constant. :return: A ``Repository`` object. """ - return make_repository( + return sat.cli_factory.make_repository( { 'content-type': REPO_TYPE['docker'], 'docker-upstream-name': upstream_name or CONTAINER_UPSTREAM_NAME, @@ -82,10 +73,10 @@ def test_positive_pull_image(self, module_org, container_contenthost, target_sat :parametrized: yes """ - product = make_product_wait({'organization-id': module_org.id}) - repo = _repo(product['id']) - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + product = target_sat.cli_factory.make_product_wait({'organization-id': module_org.id}) + repo = _repo(target_sat, product['id']) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) try: result = container_contenthost.execute( f'docker login -u {settings.server.admin_username}' @@ -155,18 +146,22 @@ def test_positive_container_admin_end_to_end_search( # Satellite setup: create product and add Docker repository; # create content view and add Docker repository; # create lifecycle environment and promote content view to it - lce = make_lifecycle_environment({'organization-id': module_org.id}) - product = make_product_wait({'organization-id': module_org.id}) - repo = _repo(product['id'], upstream_name=CONTAINER_UPSTREAM_NAME) - Repository.synchronize({'id': repo['id']}) - content_view = make_content_view({'composite': False, 'organization-id': module_org.id}) - ContentView.add_repository({'id': content_view['id'], 'repository-id': repo['id']}) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) - ContentView.version_promote( + lce = target_sat.cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) + product = target_sat.cli_factory.make_product_wait({'organization-id': module_org.id}) + repo = _repo(target_sat, product['id'], upstream_name=CONTAINER_UPSTREAM_NAME) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + content_view = target_sat.cli_factory.make_content_view( + {'composite': False, 'organization-id': module_org.id} + ) + target_sat.cli.ContentView.add_repository( + {'id': content_view['id'], 'repository-id': repo['id']} + ) + target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = target_sat.cli.ContentView.info({'id': content_view['id']}) + target_sat.cli.ContentView.version_promote( {'id': content_view['versions'][0]['id'], 'to-lifecycle-environment-id': lce['id']} ) - LifecycleEnvironment.update( + target_sat.cli.LifecycleEnvironment.update( { 'registry-name-pattern': registry_name_pattern, 'registry-unauthenticated-pull': 'false', @@ -207,7 +202,7 @@ def test_positive_container_admin_end_to_end_search( assert docker_repo_uri not in result.stdout # 8. Set 'Unauthenticated Pull' option to true - LifecycleEnvironment.update( + target_sat.cli.LifecycleEnvironment.update( { 'registry-unauthenticated-pull': 'true', 'id': lce['id'], @@ -259,18 +254,22 @@ def test_positive_container_admin_end_to_end_pull( # Satellite setup: create product and add Docker repository; # create content view and add Docker repository; # create lifecycle environment and promote content view to it - lce = make_lifecycle_environment({'organization-id': module_org.id}) - product = make_product_wait({'organization-id': module_org.id}) - repo = _repo(product['id'], upstream_name=docker_upstream_name) - Repository.synchronize({'id': repo['id']}) - content_view = make_content_view({'composite': False, 'organization-id': module_org.id}) - ContentView.add_repository({'id': content_view['id'], 'repository-id': repo['id']}) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) - ContentView.version_promote( + lce = target_sat.cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) + product = target_sat.cli_factory.make_product_wait({'organization-id': module_org.id}) + repo = _repo(target_sat, product['id'], upstream_name=docker_upstream_name) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + content_view = target_sat.cli_factory.make_content_view( + {'composite': False, 'organization-id': module_org.id} + ) + target_sat.cli.ContentView.add_repository( + {'id': content_view['id'], 'repository-id': repo['id']} + ) + target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = target_sat.cli.ContentView.info({'id': content_view['id']}) + target_sat.cli.ContentView.version_promote( {'id': content_view['versions'][0]['id'], 'to-lifecycle-environment-id': lce['id']} ) - LifecycleEnvironment.update( + target_sat.cli.LifecycleEnvironment.update( { 'registry-name-pattern': registry_name_pattern, 'registry-unauthenticated-pull': 'false', @@ -315,7 +314,7 @@ def test_positive_container_admin_end_to_end_pull( assert result.status == 1 # 8. Set 'Unauthenticated Pull' option to true - LifecycleEnvironment.update( + target_sat.cli.LifecycleEnvironment.update( { 'registry-unauthenticated-pull': 'true', 'id': lce['id'], @@ -364,7 +363,9 @@ def test_negative_pull_content_with_longer_name( {'name': product_name, 'organization-id': module_org.id} ) - repo = _repo(product['id'], name=repo_name, upstream_name=CONTAINER_UPSTREAM_NAME) + repo = _repo( + target_sat, product['id'], name=repo_name, upstream_name=CONTAINER_UPSTREAM_NAME + ) # 2. Sync the repos target_sat.cli.Repository.synchronize({'id': repo['id']}) diff --git a/tests/foreman/cli/test_contentaccess.py b/tests/foreman/cli/test_contentaccess.py index 307d62ddf6b..526ff086d33 100644 --- a/tests/foreman/cli/test_contentaccess.py +++ b/tests/foreman/cli/test_contentaccess.py @@ -19,8 +19,6 @@ from nailgun import entities import pytest -from robottelo.cli.host import Host -from robottelo.cli.package import Package from robottelo.config import settings from robottelo.constants import ( DEFAULT_ARCHITECTURE, @@ -105,7 +103,7 @@ def vm( @pytest.mark.tier2 @pytest.mark.pit_client @pytest.mark.pit_server -def test_positive_list_installable_updates(vm): +def test_positive_list_installable_updates(vm, module_target_sat): """Ensure packages applicability is functioning properly. :id: 4feb692c-165b-4f96-bb97-c8447bd2cf6e @@ -129,7 +127,7 @@ def test_positive_list_installable_updates(vm): :CaseImportance: Critical """ for _ in range(30): - applicable_packages = Package.list( + applicable_packages = module_target_sat.cli.Package.list( { 'host': vm.hostname, 'packages-restrict-applicable': 'true', @@ -149,7 +147,7 @@ def test_positive_list_installable_updates(vm): @pytest.mark.upgrade @pytest.mark.pit_client @pytest.mark.pit_server -def test_positive_erratum_installable(vm): +def test_positive_erratum_installable(vm, module_target_sat): """Ensure erratum applicability is showing properly, without attaching any subscription. @@ -171,7 +169,9 @@ def test_positive_erratum_installable(vm): """ # check that package errata is applicable for _ in range(30): - erratum = Host.errata_list({'host': vm.hostname, 'search': f'id = {REAL_0_ERRATA_ID}'}) + erratum = module_target_sat.cli.Host.errata_list( + {'host': vm.hostname, 'search': f'id = {REAL_0_ERRATA_ID}'} + ) if erratum: break time.sleep(10) diff --git a/tests/foreman/cli/test_contentcredentials.py b/tests/foreman/cli/test_contentcredentials.py index 269170ffdc1..8541ce0f718 100644 --- a/tests/foreman/cli/test_contentcredentials.py +++ b/tests/foreman/cli/test_contentcredentials.py @@ -23,9 +23,8 @@ from fauxfactory import gen_alphanumeric, gen_choice, gen_integer, gen_string import pytest -from robottelo.cli.base import CLIReturnCodeError from robottelo.constants import DataFile -from robottelo.host_helpers.cli_factory import CLIFactoryError +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.utils.datafactory import ( invalid_values_list, parametrized, diff --git a/tests/foreman/cli/test_contentview.py b/tests/foreman/cli/test_contentview.py index 2f22d4aefb1..703787189e9 100644 --- a/tests/foreman/cli/test_contentview.py +++ b/tests/foreman/cli/test_contentview.py @@ -24,28 +24,13 @@ from wrapanapi.entities.vm import VmState from robottelo import constants -from robottelo.cli import factory as cli_factory -from robottelo.cli.activationkey import ActivationKey -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.capsule import Capsule -from robottelo.cli.contentview import ContentView -from robottelo.cli.filter import Filter -from robottelo.cli.host import Host -from robottelo.cli.hostcollection import HostCollection -from robottelo.cli.location import Location -from robottelo.cli.module_stream import ModuleStream -from robottelo.cli.org import Org -from robottelo.cli.package import Package -from robottelo.cli.product import Product -from robottelo.cli.repository import Repository -from robottelo.cli.role import Role -from robottelo.cli.user import User from robottelo.config import settings from robottelo.constants import ( FAKE_2_CUSTOM_PACKAGE, FAKE_2_CUSTOM_PACKAGE_NAME, DataFile, ) +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.utils.datafactory import ( generate_strings_list, invalid_names_list, @@ -55,7 +40,7 @@ @pytest.fixture(scope='module') -def module_rhel_content(module_entitlement_manifest_org): +def module_rhel_content(module_entitlement_manifest_org, module_target_sat): """Returns RH repo after syncing it""" product = entities.Product( name=constants.PRDS['rhel'], organization=module_entitlement_manifest_org @@ -64,14 +49,14 @@ def module_rhel_content(module_entitlement_manifest_org): data = {'basearch': 'x86_64', 'releasever': '6Server', 'product_id': product.id} reposet.enable(data=data) - repo = Repository.info( + repo = module_target_sat.cli.Repository.info( { 'name': constants.REPOS['rhva6']['name'], 'organization-id': module_entitlement_manifest_org.id, 'product': product.name, } ) - Repository.synchronize( + module_target_sat.cli.Repository.synchronize( { 'name': constants.REPOS['rhva6']['name'], 'organization-id': module_entitlement_manifest_org.id, @@ -81,12 +66,12 @@ def module_rhel_content(module_entitlement_manifest_org): return repo -def _get_content_view_version_lce_names_set(content_view_id, version_id): +def _get_content_view_version_lce_names_set(content_view_id, version_id, sat): """returns a set of content view version lifecycle environment names :rtype: set that it belongs under """ - lifecycle_environments = ContentView.version_info( + lifecycle_environments = sat.cli.ContentView.version_info( {'content-view-id': content_view_id, 'id': version_id} )['lifecycle-environments'] return {lce['name'] for lce in lifecycle_environments} @@ -97,7 +82,7 @@ class TestContentView: @pytest.mark.parametrize('name', **parametrized(valid_names_list())) @pytest.mark.tier1 - def test_positive_create_with_name(self, module_org, name): + def test_positive_create_with_name(self, module_org, module_target_sat, name): """create content views with different names :id: a154308c-3982-4cf1-a236-3051e740970e @@ -108,14 +93,14 @@ def test_positive_create_with_name(self, module_org, name): :parametrized: yes """ - content_view = cli_factory.make_content_view( + content_view = module_target_sat.cli_factory.make_content_view( {'name': name, 'organization-id': module_org.id} ) assert content_view['name'] == name.strip() @pytest.mark.parametrize('name', **parametrized(invalid_names_list())) @pytest.mark.tier1 - def test_negative_create_with_invalid_name(self, module_org, name): + def test_negative_create_with_invalid_name(self, module_org, module_target_sat, name): """create content views with invalid names :id: 83046271-76f9-4cda-b579-a2fe63493295 @@ -127,11 +112,13 @@ def test_negative_create_with_invalid_name(self, module_org, name): :parametrized: yes """ - with pytest.raises(cli_factory.CLIFactoryError): - cli_factory.make_content_view({'name': name, 'organization-id': module_org.id}) + with pytest.raises(CLIFactoryError): + module_target_sat.cli_factory.make_content_view( + {'name': name, 'organization-id': module_org.id} + ) @pytest.mark.tier1 - def test_negative_create_with_org_name(self): + def test_negative_create_with_org_name(self, module_target_sat): """Create content view with invalid org name :id: f8b76e98-ccc8-41ac-af04-541650e8f5ba @@ -142,10 +129,10 @@ def test_negative_create_with_org_name(self): :CaseImportance: Critical """ with pytest.raises(CLIReturnCodeError): - ContentView.create({'organization-id': gen_string('alpha')}) + module_target_sat.cli.ContentView.create({'organization-id': gen_string('alpha')}) @pytest.mark.tier2 - def test_positive_create_with_repo_id(self, module_org, module_product): + def test_positive_create_with_repo_id(self, module_org, module_product, module_target_sat): """Create content view providing repository id :id: bb91affe-f8d4-4724-8b61-41f3cb898fd3 @@ -156,15 +143,17 @@ def test_positive_create_with_repo_id(self, module_org, module_product): :CaseImportance: High :BZ: 1213097 """ - repo = cli_factory.make_repository({'product-id': module_product.id}) - cv = cli_factory.make_content_view( + repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': module_product.id} + ) + cv = module_target_sat.cli_factory.make_content_view( {'organization-id': module_org.id, 'repository-ids': [repo['id']]} ) assert cv['yum-repositories'][0]['id'] == repo['id'] @pytest.mark.parametrize('new_name', **parametrized(valid_names_list())) @pytest.mark.tier1 - def test_positive_update_name_by_id(self, module_org, new_name): + def test_positive_update_name_by_id(self, module_org, module_target_sat, new_name): """Find content view by its id and update its name afterwards :id: 35fccf2c-abc4-4ca8-a565-a7a6adaaf429 @@ -177,16 +166,16 @@ def test_positive_update_name_by_id(self, module_org, new_name): :parametrized: yes """ - cv = cli_factory.make_content_view( + cv = module_target_sat.cli_factory.make_content_view( {'name': gen_string('utf8'), 'organization-id': module_org.id} ) - ContentView.update({'id': cv['id'], 'new-name': new_name}) - cv = ContentView.info({'id': cv['id']}) + module_target_sat.cli.ContentView.update({'id': cv['id'], 'new-name': new_name}) + cv = module_target_sat.cli.ContentView.info({'id': cv['id']}) assert cv['name'] == new_name.strip() @pytest.mark.parametrize('new_name', **parametrized(valid_names_list())) @pytest.mark.tier1 - def test_positive_update_name_by_name(self, module_org, new_name): + def test_positive_update_name_by_name(self, module_org, module_target_sat, new_name): """Find content view by its name and update it :id: aa9bced6-ee6c-4a18-90ac-874ab4979711 @@ -199,16 +188,16 @@ def test_positive_update_name_by_name(self, module_org, new_name): :parametrized: yes """ - cv = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.update( + cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + module_target_sat.cli.ContentView.update( {'name': cv['name'], 'organization-label': module_org.label, 'new-name': new_name} ) - cv = ContentView.info({'id': cv['id']}) + cv = module_target_sat.cli.ContentView.info({'id': cv['id']}) assert cv['name'] == new_name.strip() @pytest.mark.run_in_one_thread @pytest.mark.tier2 - def test_positive_update_filter(self, repo_setup): + def test_positive_update_filter(self, repo_setup, module_target_sat): """Edit content views for a rh content, add a filter and update filter :id: 4beab1e4-fc58-460e-af24-cdd2c3d283e6 @@ -221,38 +210,40 @@ def test_positive_update_filter(self, repo_setup): :CaseImportance: High """ # Create CV - new_cv = cli_factory.make_content_view({'organization-id': repo_setup['org'].id}) + new_cv = module_target_sat.cli_factory.make_content_view( + {'organization-id': repo_setup['org'].id} + ) # Associate repo to CV - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( { 'id': new_cv['id'], 'organization-id': repo_setup['org'].id, 'repository-id': repo_setup['repo'].id, } ) - Repository.synchronize({'id': repo_setup['repo'].id}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo_setup['repo'].id}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert new_cv['yum-repositories'][0]['name'] == repo_setup['repo'].name - cvf = cli_factory.make_content_view_filter( + cvf = module_target_sat.cli_factory.make_content_view_filter( {'content-view-id': new_cv['id'], 'inclusion': 'true', 'type': 'erratum'} ) - cvf_rule = cli_factory.make_content_view_filter_rule( + cvf_rule = module_target_sat.cli_factory.content_view_filter_rule( {'content-view-filter-id': cvf['filter-id'], 'types': ['bugfix', 'enhancement']} ) - cvf = ContentView.filter.info({'id': cvf['filter-id']}) + cvf = module_target_sat.cli.ContentView.filter.info({'id': cvf['filter-id']}) assert 'security' not in cvf['rules'][0]['types'] - ContentView.filter.rule.update( + module_target_sat.cli.ContentView.filter.rule.update( { 'id': cvf_rule['rule-id'], 'types': 'security', 'content-view-filter-id': cvf['filter-id'], } ) - cvf = ContentView.filter.info({'id': cvf['filter-id']}) + cvf = module_target_sat.cli.ContentView.filter.info({'id': cvf['filter-id']}) assert 'security' == cvf['rules'][0]['types'] @pytest.mark.tier1 - def test_positive_delete_by_id(self, module_org): + def test_positive_delete_by_id(self, module_org, module_target_sat): """delete content view by its id :id: e96d6d47-8be4-4705-979f-e5c320eca293 @@ -261,13 +252,13 @@ def test_positive_delete_by_id(self, module_org): :CaseImportance: Critical """ - cv = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.delete({'id': cv['id']}) + cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + module_target_sat.cli.ContentView.delete({'id': cv['id']}) with pytest.raises(CLIReturnCodeError): - ContentView.info({'id': cv['id']}) + module_target_sat.cli.ContentView.info({'id': cv['id']}) @pytest.mark.tier1 - def test_positive_delete_by_name(self, module_org): + def test_positive_delete_by_name(self, module_org, module_target_sat): """delete content view by its name :id: 014b85f3-003b-42d9-bbfe-21620e8eb84b @@ -278,13 +269,15 @@ def test_positive_delete_by_name(self, module_org): :CaseImportance: Critical """ - cv = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.delete({'name': cv['name'], 'organization': module_org.name}) + cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + module_target_sat.cli.ContentView.delete( + {'name': cv['name'], 'organization': module_org.name} + ) with pytest.raises(CLIReturnCodeError): - ContentView.info({'id': cv['id']}) + module_target_sat.cli.ContentView.info({'id': cv['id']}) @pytest.mark.tier2 - def test_positive_delete_version_by_name(self, module_org): + def test_positive_delete_version_by_name(self, module_org, module_target_sat): """Create content view and publish it. After that try to disassociate content view from 'Library' environment through 'remove-from-environment' command and delete content view version from @@ -298,28 +291,30 @@ def test_positive_delete_version_by_name(self, module_org): :CaseLevel: Integration """ - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert len(content_view['versions']) == 1 cvv = content_view['versions'][0] env_id = content_view['lifecycle-environments'][0]['id'] - ContentView.remove_from_environment( + module_target_sat.cli.ContentView.remove_from_environment( {'id': content_view['id'], 'lifecycle-environment-id': env_id} ) - ContentView.version_delete( + module_target_sat.cli.ContentView.version_delete( { 'content-view': content_view['name'], 'organization': module_org.name, 'version': cvv['version'], } ) - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert len(content_view['versions']) == 0 @pytest.mark.tier2 @pytest.mark.upgrade - def test_positive_delete_version_by_id(self, module_org, module_product): + def test_positive_delete_version_by_id(self, module_org, module_product, module_target_sat): """Create content view and publish it. After that try to disassociate content view from 'Library' environment through 'remove-from-environment' command and delete content view version from @@ -333,12 +328,14 @@ def test_positive_delete_version_by_id(self, module_org, module_product): :CaseImportance: High """ # Create new organization, product and repository - new_repo = cli_factory.make_repository({'product-id': module_product.id}) + new_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': module_product.id} + ) # Sync REPO - Repository.synchronize({'id': new_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) # Create new content-view and add repository to view - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.add_repository( + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + module_target_sat.cli.ContentView.add_repository( { 'id': new_cv['id'], 'organization-id': module_org.id, @@ -346,25 +343,25 @@ def test_positive_delete_version_by_id(self, module_org, module_product): } ) # Publish a version1 of CV - ContentView.publish({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) # Get the CV info - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['versions']) == 1 # Store the associated environment_id env_id = new_cv['lifecycle-environments'][0]['id'] # Store the version1 id version1_id = new_cv['versions'][0]['id'] # Remove the CV from selected environment - ContentView.remove_from_environment( + module_target_sat.cli.ContentView.remove_from_environment( {'id': new_cv['id'], 'lifecycle-environment-id': env_id} ) # Delete the version - ContentView.version_delete({'id': version1_id}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.version_delete({'id': version1_id}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['versions']) == 0 @pytest.mark.tier2 - def test_negative_delete_version_by_id(self, module_org): + def test_negative_delete_version_by_id(self, module_org, module_target_sat): """Create content view and publish it. Try to delete content view version while content view is still associated with lifecycle environment @@ -377,20 +374,22 @@ def test_negative_delete_version_by_id(self, module_org): :CaseLevel: Integration """ - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert len(content_view['versions']) == 1 cv = content_view['versions'][0] # Try to delete content view version while it is in environment Library with pytest.raises(CLIReturnCodeError): - ContentView.version_delete({'id': cv['id']}) + module_target_sat.cli.ContentView.version_delete({'id': cv['id']}) # Check that version was not actually removed from the cv - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert len(content_view['versions']) == 1 @pytest.mark.tier1 - def test_positive_remove_lce_by_id(self, module_org): + def test_positive_remove_lce_by_id(self, module_org, module_target_sat): """Remove content view from lifecycle environment :id: 1bf8a647-d82e-4145-b13b-f92bf6642532 @@ -399,16 +398,16 @@ def test_positive_remove_lce_by_id(self, module_org): :CaseImportance: Critical """ - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) env = new_cv['lifecycle-environments'][0] - ContentView.remove({'id': new_cv['id'], 'environment-ids': env['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.remove({'id': new_cv['id'], 'environment-ids': env['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['lifecycle-environments']) == 0 @pytest.mark.tier3 - def test_positive_remove_lce_by_id_and_reassign_ak(self, module_org): + def test_positive_remove_lce_by_id_and_reassign_ak(self, module_org, module_target_sat): """Remove content view environment and re-assign activation key to another environment and content view @@ -421,23 +420,33 @@ def test_positive_remove_lce_by_id_and_reassign_ak(self, module_org): :CaseLevel: Integration """ env = [ - cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) + module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) for _ in range(2) ] - source_cv = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.publish({'id': source_cv['id']}) - source_cv = ContentView.info({'id': source_cv['id']}) + source_cv = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.publish({'id': source_cv['id']}) + source_cv = module_target_sat.cli.ContentView.info({'id': source_cv['id']}) cvv = source_cv['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': env[0]['id']}) + module_target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': env[0]['id']} + ) - destination_cv = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.publish({'id': destination_cv['id']}) - destination_cv = ContentView.info({'id': destination_cv['id']}) + destination_cv = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.publish({'id': destination_cv['id']}) + destination_cv = module_target_sat.cli.ContentView.info({'id': destination_cv['id']}) cvv = destination_cv['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': env[1]['id']}) + module_target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': env[1]['id']} + ) - ac_key = cli_factory.make_activation_key( + ac_key = module_target_sat.cli_factory.make_activation_key( { 'content-view-id': source_cv['id'], 'lifecycle-environment-id': env[0]['id'], @@ -445,12 +454,12 @@ def test_positive_remove_lce_by_id_and_reassign_ak(self, module_org): 'organization-id': module_org.id, } ) - source_cv = ContentView.info({'id': source_cv['id']}) + source_cv = module_target_sat.cli.ContentView.info({'id': source_cv['id']}) assert source_cv['activation-keys'][0] == ac_key['name'] - destination_cv = ContentView.info({'id': destination_cv['id']}) + destination_cv = module_target_sat.cli.ContentView.info({'id': destination_cv['id']}) assert len(destination_cv['activation-keys']) == 0 - ContentView.remove( + module_target_sat.cli.ContentView.remove( { 'id': source_cv['id'], 'environment-ids': env[0]['id'], @@ -458,14 +467,14 @@ def test_positive_remove_lce_by_id_and_reassign_ak(self, module_org): 'key-environment-id': env[1]['id'], } ) - source_cv = ContentView.info({'id': source_cv['id']}) + source_cv = module_target_sat.cli.ContentView.info({'id': source_cv['id']}) assert len(source_cv['activation-keys']) == 0 - destination_cv = ContentView.info({'id': destination_cv['id']}) + destination_cv = module_target_sat.cli.ContentView.info({'id': destination_cv['id']}) assert destination_cv['activation-keys'][0] == ac_key['name'] @pytest.mark.tier3 @pytest.mark.upgrade - def test_positive_remove_lce_by_id_and_reassign_chost(self, module_org): + def test_positive_remove_lce_by_id_and_reassign_chost(self, module_org, module_target_sat): """Remove content view environment and re-assign content host to another environment and content view @@ -478,26 +487,36 @@ def test_positive_remove_lce_by_id_and_reassign_chost(self, module_org): :CaseLevel: Integration """ env = [ - cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) + module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) for _ in range(2) ] - source_cv = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.publish({'id': source_cv['id']}) - source_cv = ContentView.info({'id': source_cv['id']}) + source_cv = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.publish({'id': source_cv['id']}) + source_cv = module_target_sat.cli.ContentView.info({'id': source_cv['id']}) cvv = source_cv['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': env[0]['id']}) + module_target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': env[0]['id']} + ) - destination_cv = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.publish({'id': destination_cv['id']}) - destination_cv = ContentView.info({'id': destination_cv['id']}) + destination_cv = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.publish({'id': destination_cv['id']}) + destination_cv = module_target_sat.cli.ContentView.info({'id': destination_cv['id']}) cvv = destination_cv['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': env[1]['id']}) + module_target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': env[1]['id']} + ) - source_cv = ContentView.info({'id': source_cv['id']}) + source_cv = module_target_sat.cli.ContentView.info({'id': source_cv['id']}) assert source_cv['content-host-count'] == '0' - cli_factory.make_fake_host( + module_target_sat.cli_factory.make_fake_host( { 'content-view-id': source_cv['id'], 'lifecycle-environment-id': env[0]['id'], @@ -506,12 +525,12 @@ def test_positive_remove_lce_by_id_and_reassign_chost(self, module_org): } ) - source_cv = ContentView.info({'id': source_cv['id']}) + source_cv = module_target_sat.cli.ContentView.info({'id': source_cv['id']}) assert source_cv['content-host-count'] == '1' - destination_cv = ContentView.info({'id': destination_cv['id']}) + destination_cv = module_target_sat.cli.ContentView.info({'id': destination_cv['id']}) assert destination_cv['content-host-count'] == '0' - ContentView.remove( + module_target_sat.cli.ContentView.remove( { 'environment-ids': env[0]['id'], 'id': source_cv['id'], @@ -519,13 +538,13 @@ def test_positive_remove_lce_by_id_and_reassign_chost(self, module_org): 'system-environment-id': env[1]['id'], } ) - source_cv = ContentView.info({'id': source_cv['id']}) + source_cv = module_target_sat.cli.ContentView.info({'id': source_cv['id']}) assert source_cv['content-host-count'] == '0' - destination_cv = ContentView.info({'id': destination_cv['id']}) + destination_cv = module_target_sat.cli.ContentView.info({'id': destination_cv['id']}) assert destination_cv['content-host-count'] == '1' @pytest.mark.tier1 - def test_positive_remove_version_by_id(self, module_org): + def test_positive_remove_version_by_id(self, module_org, module_target_sat): """Delete content view version using 'remove' command by id :id: e8664353-6601-4566-8478-440be20a089d @@ -534,22 +553,24 @@ def test_positive_remove_version_by_id(self, module_org): :CaseImportance: Critical """ - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['versions']) == 1 env = new_cv['lifecycle-environments'][0] cvv = new_cv['versions'][0] - ContentView.remove_from_environment( + module_target_sat.cli.ContentView.remove_from_environment( {'id': new_cv['id'], 'lifecycle-environment-id': env['id']} ) - ContentView.remove({'content-view-version-ids': cvv['id'], 'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.remove( + {'content-view-version-ids': cvv['id'], 'id': new_cv['id']} + ) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['versions']) == 0 @pytest.mark.tier1 - def test_positive_remove_version_by_name(self, module_org): + def test_positive_remove_version_by_name(self, module_org, module_target_sat): """Delete content view version using 'remove' command by name :id: 2c838716-dcd3-4017-bffc-da53727c22a3 @@ -560,21 +581,23 @@ def test_positive_remove_version_by_name(self, module_org): :CaseImportance: Critical """ - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['versions']) == 1 env = new_cv['lifecycle-environments'][0] cvv = new_cv['versions'][0] - ContentView.remove_from_environment( + module_target_sat.cli.ContentView.remove_from_environment( {'id': new_cv['id'], 'lifecycle-environment-id': env['id']} ) - ContentView.remove({'content-view-versions': cvv['version'], 'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.remove( + {'content-view-versions': cvv['version'], 'id': new_cv['id']} + ) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['versions']) == 0 @pytest.mark.tier1 - def test_positive_remove_repository_by_id(self, module_org, module_product): + def test_positive_remove_repository_by_id(self, module_org, module_product, module_target_sat): """Remove associated repository from content view by id :id: 90703181-b3f8-44f6-959a-b65c79b6b6ee @@ -585,20 +608,28 @@ def test_positive_remove_repository_by_id(self, module_org, module_product): :CaseImportance: Critical """ - new_repo = cli_factory.make_repository({'product-id': module_product.id}) + new_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': module_product.id} + ) # Create CV - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Associate repo to CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': new_repo['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': new_repo['id']} + ) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['yum-repositories']) == 1 # Remove repository from CV - ContentView.remove_repository({'id': new_cv['id'], 'repository-id': new_repo['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.remove_repository( + {'id': new_cv['id'], 'repository-id': new_repo['id']} + ) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['yum-repositories']) == 0 @pytest.mark.tier1 - def test_positive_remove_repository_by_name(self, module_org, module_product): + def test_positive_remove_repository_by_name( + self, module_org, module_product, module_target_sat + ): """Remove associated repository from content view by name :id: dc952fe7-eb89-4760-889b-6a3fa17c3e75 @@ -609,20 +640,26 @@ def test_positive_remove_repository_by_name(self, module_org, module_product): :CaseImportance: Critical """ - new_repo = cli_factory.make_repository({'product-id': module_product.id}) + new_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': module_product.id} + ) # Create CV - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Associate repo to CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': new_repo['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': new_repo['id']} + ) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['yum-repositories']) == 1 # Remove repository from CV - ContentView.remove_repository({'id': new_cv['id'], 'repository': new_repo['name']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.remove_repository( + {'id': new_cv['id'], 'repository': new_repo['name']} + ) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['yum-repositories']) == 0 @pytest.mark.tier2 - def test_positive_create_composite(self, module_org): + def test_positive_create_composite(self, module_org, module_target_sat): """create a composite content view :id: bded6acd-8da3-45ea-9e39-19bdc6c06341 @@ -636,31 +673,37 @@ def test_positive_create_composite(self, module_org): :CaseImportance: High """ # Create REPO - new_product = cli_factory.make_product({'organization-id': module_org.id}) - new_repo = cli_factory.make_repository({'product-id': new_product['id']}) + new_product = module_target_sat.cli_factory.make_product({'organization-id': module_org.id}) + new_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': new_product['id']} + ) # Sync REPO - Repository.synchronize({'id': new_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) # Create CV - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Associate repo to CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': new_repo['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': new_repo['id']} + ) # Publish a new version of CV - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) # Let us now store the version1 id version1_id = new_cv['versions'][0]['id'] # Create CV - con_view = cli_factory.make_content_view( + con_view = module_target_sat.cli_factory.make_content_view( {'composite': True, 'organization-id': module_org.id} ) # Associate version to composite CV - ContentView.add_version({'content-view-version-id': version1_id, 'id': con_view['id']}) + module_target_sat.cli.ContentView.add_version( + {'content-view-version-id': version1_id, 'id': con_view['id']} + ) # Assert whether version was associated to composite CV - con_view = ContentView.info({'id': con_view['id']}) + con_view = module_target_sat.cli.ContentView.info({'id': con_view['id']}) assert con_view['components'][0]['id'] == version1_id @pytest.mark.tier2 - def test_positive_create_composite_by_name(self, module_org): + def test_positive_create_composite_by_name(self, module_org, module_target_sat): """Create a composite content view and add non-composite content view by its name @@ -675,24 +718,30 @@ def test_positive_create_composite_by_name(self, module_org): :CaseImportance: High """ - new_product = cli_factory.make_product({'organization-id': module_org.id}) + new_product = module_target_sat.cli_factory.make_product({'organization-id': module_org.id}) # Create REPO - new_repo = cli_factory.make_repository({'product-id': new_product['id']}) + new_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': new_product['id']} + ) # Sync REPO - Repository.synchronize({'id': new_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) # Create CV - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Associate repo to CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': new_repo['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': new_repo['id']} + ) # Publish a new version of CV - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) cvv = new_cv['versions'][0] # Create CV - cv = cli_factory.make_content_view({'composite': True, 'organization-id': module_org.id}) + cv = module_target_sat.cli_factory.make_content_view( + {'composite': True, 'organization-id': module_org.id} + ) assert len(cv['components']) == 0 # Associate version to composite CV - ContentView.add_version( + module_target_sat.cli.ContentView.add_version( { 'content-view-version': cvv['version'], 'content-view': new_cv['name'], @@ -701,12 +750,14 @@ def test_positive_create_composite_by_name(self, module_org): } ) # Assert whether version was associated to composite CV - cv = ContentView.info({'id': cv['id']}) + cv = module_target_sat.cli.ContentView.info({'id': cv['id']}) assert len(cv['components']) == 1 assert cv['components'][0]['id'] == cvv['id'] @pytest.mark.tier2 - def test_positive_remove_version_by_id_from_composite(self, module_org, module_product): + def test_positive_remove_version_by_id_from_composite( + self, module_org, module_product, module_target_sat + ): """Create a composite content view and remove its content version by id :id: 0ff675d0-45d6-4f15-9e84-3b5ce98ce7de @@ -719,12 +770,14 @@ def test_positive_remove_version_by_id_from_composite(self, module_org, module_p :CaseImportance: High """ # Create new repository - new_repo = cli_factory.make_repository({'product-id': module_product.id}) + new_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': module_product.id} + ) # Sync REPO - Repository.synchronize({'id': new_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) # Create new content-view and add repository to view - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.add_repository( + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + module_target_sat.cli.ContentView.add_repository( { 'id': new_cv['id'], 'organization-id': module_org.id, @@ -732,30 +785,32 @@ def test_positive_remove_version_by_id_from_composite(self, module_org, module_p } ) # Publish a new version of CV - ContentView.publish({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) # Get the CV info - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) # Create a composite CV - comp_cv = cli_factory.make_content_view( + comp_cv = module_target_sat.cli_factory.make_content_view( { 'composite': True, 'organization-id': module_org.id, 'component-ids': new_cv['versions'][0]['id'], } ) - ContentView.publish({'id': comp_cv['id']}) - new_cv = ContentView.info({'id': comp_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': comp_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': comp_cv['id']}) env = new_cv['lifecycle-environments'][0] cvv = new_cv['versions'][0] - ContentView.remove_from_environment( + module_target_sat.cli.ContentView.remove_from_environment( {'id': new_cv['id'], 'lifecycle-environment-id': env['id']} ) - ContentView.remove({'content-view-version-ids': cvv['id'], 'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.remove( + {'content-view-version-ids': cvv['id'], 'id': new_cv['id']} + ) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['versions']) == 0 @pytest.mark.tier2 - def test_positive_remove_component_by_name(self, module_org, module_product): + def test_positive_remove_component_by_name(self, module_org, module_product, module_target_sat): """Create a composite content view and remove component from it by name :id: 908f9cad-b985-4bae-96c0-037ea1d395a6 @@ -770,12 +825,14 @@ def test_positive_remove_component_by_name(self, module_org, module_product): :CaseImportance: High """ # Create new repository - new_repo = cli_factory.make_repository({'product-id': module_product.id}) + new_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': module_product.id} + ) # Sync REPO - Repository.synchronize({'id': new_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) # Create new content-view and add repository to view - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.add_repository( + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + module_target_sat.cli.ContentView.add_repository( { 'id': new_cv['id'], 'organization-id': module_org.id, @@ -783,11 +840,11 @@ def test_positive_remove_component_by_name(self, module_org, module_product): } ) # Publish a new version of CV - ContentView.publish({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) # Get the CV info - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) # Create a composite CV - comp_cv = cli_factory.make_content_view( + comp_cv = module_target_sat.cli_factory.make_content_view( { 'composite': True, 'organization-id': module_org.id, @@ -795,7 +852,7 @@ def test_positive_remove_component_by_name(self, module_org, module_product): } ) assert len(comp_cv['components']) == 1 - ContentView.remove_version( + module_target_sat.cli.ContentView.remove_version( { 'content-view-version': new_cv['versions'][0]['version'], 'content-view': new_cv['name'], @@ -803,11 +860,11 @@ def test_positive_remove_component_by_name(self, module_org, module_product): 'name': comp_cv['name'], } ) - comp_cv = ContentView.info({'id': comp_cv['id']}) + comp_cv = module_target_sat.cli.ContentView.info({'id': comp_cv['id']}) assert len(comp_cv['components']) == 0 @pytest.mark.tier3 - def test_positive_create_composite_with_component_ids(self, module_org): + def test_positive_create_composite_with_component_ids(self, module_org, module_target_sat): """Create a composite content view with a component_ids option which ids are from different content views @@ -822,29 +879,29 @@ def test_positive_create_composite_with_component_ids(self, module_org): :CaseImportance: High """ # Create first CV - cv1 = cli_factory.make_content_view({'organization-id': module_org.id}) + cv1 = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Publish a new version of CV - ContentView.publish({'id': cv1['id']}) - cv1 = ContentView.info({'id': cv1['id']}) + module_target_sat.cli.ContentView.publish({'id': cv1['id']}) + cv1 = module_target_sat.cli.ContentView.info({'id': cv1['id']}) # Create second CV - cv2 = cli_factory.make_content_view({'organization-id': module_org.id}) + cv2 = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Publish a new version of CV - ContentView.publish({'id': cv2['id']}) - cv2 = ContentView.info({'id': cv2['id']}) + module_target_sat.cli.ContentView.publish({'id': cv2['id']}) + cv2 = module_target_sat.cli.ContentView.info({'id': cv2['id']}) # Let us now store the version ids component_ids = [cv1['versions'][0]['id'], cv2['versions'][0]['id']] # Create CV - comp_cv = cli_factory.make_content_view( + comp_cv = module_target_sat.cli_factory.make_content_view( {'composite': True, 'organization-id': module_org.id, 'component-ids': component_ids} ) # Assert whether the composite content view components IDs are equal # to the component_ids input values - comp_cv = ContentView.info({'id': comp_cv['id']}) + comp_cv = module_target_sat.cli.ContentView.info({'id': comp_cv['id']}) assert {comp['id'] for comp in comp_cv['components']} == set(component_ids) @pytest.mark.tier3 - def test_negative_create_composite_with_component_ids(self, module_org): + def test_negative_create_composite_with_component_ids(self, module_org, module_target_sat): """Attempt to create a composite content view with a component_ids option which ids are from the same content view @@ -859,16 +916,16 @@ def test_negative_create_composite_with_component_ids(self, module_org): :CaseImportance: Low """ # Create CV - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Publish a new version of CV twice for _ in range(2): - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) # Let us now store the version ids component_ids = [version['id'] for version in new_cv['versions']] # Try create CV - with pytest.raises(cli_factory.CLIFactoryError) as context: - cli_factory.make_content_view( + with pytest.raises(CLIFactoryError) as context: + module_target_sat.cli_factory.make_content_view( { 'composite': True, 'organization-id': module_org.id, @@ -878,7 +935,7 @@ def test_negative_create_composite_with_component_ids(self, module_org): assert 'Failed to create ContentView with data:' in str(context) @pytest.mark.tier3 - def test_positive_update_composite_with_component_ids(module_org): + def test_positive_update_composite_with_component_ids(module_org, module_target_sat): """Update a composite content view with a component_ids option :id: e6106ff6-c526-40f2-bdc0-ae291f7b267e @@ -891,26 +948,30 @@ def test_positive_update_composite_with_component_ids(module_org): :CaseImportance: Low """ # Create a CV to add to the composite one - cv = cli_factory.make_content_view({'organization-id': module_org.id}) + cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Publish a new version of the CV - ContentView.publish({'id': cv['id']}) - new_cv = ContentView.info({'id': cv['id']}) + module_target_sat.cli.ContentView.publish({'id': cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': cv['id']}) # Let us now store the version ids component_ids = new_cv['versions'][0]['id'] # Create a composite CV - comp_cv = cli_factory.make_content_view( + comp_cv = module_target_sat.cli_factory.make_content_view( {'composite': True, 'organization-id': module_org.id} ) # Update a composite content view with a component id version - ContentView.update({'id': comp_cv['id'], 'component-ids': component_ids}) + module_target_sat.cli.ContentView.update( + {'id': comp_cv['id'], 'component-ids': component_ids} + ) # Assert whether the composite content view components IDs are equal # to the component_ids input values - comp_cv = ContentView.info({'id': comp_cv['id']}) + comp_cv = module_target_sat.cli.ContentView.info({'id': comp_cv['id']}) assert comp_cv['components'][0]['id'] == component_ids @pytest.mark.run_in_one_thread @pytest.mark.tier1 - def test_positive_add_rh_repo_by_id(self, module_entitlement_manifest_org, module_rhel_content): + def test_positive_add_rh_repo_by_id( + self, module_entitlement_manifest_org, module_rhel_content, module_target_sat + ): """Associate Red Hat content to a content view :id: b31a85c3-aa56-461b-9e3a-f7754c742573 @@ -924,18 +985,18 @@ def test_positive_add_rh_repo_by_id(self, module_entitlement_manifest_org, modul :CaseImportance: Critical """ # Create CV - new_cv = cli_factory.make_content_view( + new_cv = module_target_sat.cli_factory.make_content_view( {'organization-id': module_entitlement_manifest_org.id} ) # Associate repo to CV - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( { 'id': new_cv['id'], 'organization-id': module_entitlement_manifest_org.id, 'repository-id': module_rhel_content['id'], } ) - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert new_cv['yum-repositories'][0]['name'] == module_rhel_content['name'] @@ -943,7 +1004,7 @@ def test_positive_add_rh_repo_by_id(self, module_entitlement_manifest_org, modul @pytest.mark.tier3 @pytest.mark.upgrade def test_positive_add_rh_repo_by_id_and_create_filter( - self, module_entitlement_manifest_org, module_rhel_content + self, module_entitlement_manifest_org, module_rhel_content, module_target_sat ): """Associate Red Hat content to a content view and create filter @@ -963,24 +1024,24 @@ def test_positive_add_rh_repo_by_id_and_create_filter( :BZ: 1359665 """ # Create CV - new_cv = cli_factory.make_content_view( + new_cv = module_target_sat.cli_factory.make_content_view( {'organization-id': module_entitlement_manifest_org.id} ) # Associate repo to CV - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( { 'id': new_cv['id'], 'organization-id': module_entitlement_manifest_org.id, 'repository-id': module_rhel_content['id'], } ) - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert new_cv['yum-repositories'][0]['name'] == module_rhel_content['name'] name = gen_string('alphanumeric') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( {'content-view-id': new_cv['id'], 'inclusion': 'true', 'name': name, 'type': 'rpm'} ) - ContentView.filter.rule.create( + module_target_sat.cli.ContentView.filter.rule.create( {'content-view-filter': name, 'content-view-id': new_cv['id'], 'name': 'walgrind'} ) @@ -1016,10 +1077,10 @@ def test_positive_add_module_stream_filter_rule(self, module_org, target_sat): 0 ] content_view = entities.ContentView(organization=module_org.id, repository=[repo]).create() - walrus_stream = ModuleStream.list({'search': "name=walrus, stream=5.21"})[0] - content_view = ContentView.info({'id': content_view.id}) + walrus_stream = target_sat.cli.ModuleStream.list({'search': "name=walrus, stream=5.21"})[0] + content_view = target_sat.cli.ContentView.info({'id': content_view.id}) assert content_view['yum-repositories'][0]['name'] == repo.name - content_view_filter = ContentView.filter.create( + content_view_filter = target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'inclusion': 'true', @@ -1027,18 +1088,20 @@ def test_positive_add_module_stream_filter_rule(self, module_org, target_sat): 'type': 'modulemd', } ) - content_view_filter_rule = ContentView.filter.rule.create( + content_view_filter_rule = target_sat.cli.ContentView.filter.rule.create( { 'content-view-filter': filter_name, 'content-view-id': content_view['id'], 'module-stream-ids': walrus_stream['id'], } ) - filter_info = ContentView.filter.info({'id': content_view_filter['filter-id']}) + filter_info = target_sat.cli.ContentView.filter.info( + {'id': content_view_filter['filter-id']} + ) assert filter_info['rules'][0]['id'] == content_view_filter_rule['rule-id'] @pytest.mark.tier2 - def test_positive_add_custom_repo_by_id(self, module_org, module_product): + def test_positive_add_custom_repo_by_id(self, module_org, module_product, module_target_sat): """Associate custom content to a Content view :id: b813b222-b984-47e0-8d9b-2daa43f9a221 @@ -1051,18 +1114,22 @@ def test_positive_add_custom_repo_by_id(self, module_org, module_product): :CaseLevel: Integration """ - new_repo = cli_factory.make_repository({'product-id': module_product.id}) + new_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': module_product.id} + ) # Sync REPO - Repository.synchronize({'id': new_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) # Create CV - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Associate repo to CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': new_repo['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': new_repo['id']} + ) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert new_cv['yum-repositories'][0]['name'] == new_repo['name'] @pytest.mark.tier1 - def test_positive_add_custom_repo_by_name(self, module_org, module_product): + def test_positive_add_custom_repo_by_name(self, module_org, module_product, module_target_sat): """Associate custom content to a content view with name :id: 62431e11-bec6-4444-abb0-e3758ba25fd8 @@ -1073,13 +1140,15 @@ def test_positive_add_custom_repo_by_name(self, module_org, module_product): :BZ: 1343006 """ - new_repo = cli_factory.make_repository({'product-id': module_product.id}) + new_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': module_product.id} + ) # Sync REPO - Repository.synchronize({'id': new_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) # Create CV - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Associate repo to CV with names. - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( { 'name': new_cv['name'], 'organization': module_org.name, @@ -1087,11 +1156,13 @@ def test_positive_add_custom_repo_by_name(self, module_org, module_product): 'repository': new_repo['name'], } ) - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert new_cv['yum-repositories'][0]['name'] == new_repo['name'] @pytest.mark.tier2 - def test_negative_add_component_in_non_composite_cv(self, module_org, module_product): + def test_negative_add_component_in_non_composite_cv( + self, module_org, module_product, module_target_sat + ): """attempt to associate components in a non-composite content view @@ -1104,25 +1175,31 @@ def test_negative_add_component_in_non_composite_cv(self, module_org, module_pro :CaseLevel: Integration """ # Create REPO - new_repo = cli_factory.make_repository({'product-id': module_product.id}) + new_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': module_product.id} + ) # Sync REPO - Repository.synchronize({'id': new_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) # Create component CV - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Associate repo to CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': new_repo['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': new_repo['id']} + ) # Publish a new version of CV - ContentView.publish({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) # Fetch version id - cv_version = ContentView.version_list({'content-view-id': new_cv['id']}) + cv_version = module_target_sat.cli.ContentView.version_list( + {'content-view-id': new_cv['id']} + ) # Create non-composite CV - with pytest.raises(cli_factory.CLIFactoryError): - cli_factory.make_content_view( + with pytest.raises(CLIFactoryError): + module_target_sat.cli_factory.make_content_view( {'component-ids': cv_version[0]['id'], 'organization-id': module_org.id} ) @pytest.mark.tier2 - def test_negative_add_same_yum_repo_twice(self, module_org, module_product): + def test_negative_add_same_yum_repo_twice(self, module_org, module_product, module_target_sat): """attempt to associate the same repo multiple times within a content view @@ -1134,25 +1211,31 @@ def test_negative_add_same_yum_repo_twice(self, module_org, module_product): :CaseLevel: Integration """ - new_repo = cli_factory.make_repository({'product-id': module_product.id}) + new_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': module_product.id} + ) # Sync REPO - Repository.synchronize({'id': new_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) # Create CV - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Associate repo to CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': new_repo['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': new_repo['id']} + ) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert new_cv['yum-repositories'][0]['name'] == new_repo['name'] repos_length = len(new_cv['yum-repositories']) # Re-associate repo to CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': new_repo['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': new_repo['id']} + ) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['yum-repositories']) == repos_length @pytest.mark.run_in_one_thread @pytest.mark.tier2 def test_positive_promote_rh_content( - self, module_entitlement_manifest_org, module_rhel_content + self, module_entitlement_manifest_org, module_rhel_content, module_target_sat ): """attempt to promote a content view containing RH content @@ -1167,29 +1250,31 @@ def test_positive_promote_rh_content( :CaseLevel: Integration """ # Create CV - new_cv = cli_factory.make_content_view( + new_cv = module_target_sat.cli_factory.make_content_view( {'organization-id': module_entitlement_manifest_org.id} ) # Associate repo to CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': module_rhel_content['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': module_rhel_content['id']} + ) # Publish a new version of CV - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) - env1 = cli_factory.make_lifecycle_environment( + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) + env1 = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_entitlement_manifest_org.id} ) # Promote the Published version of CV to the next env - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': new_cv['versions'][0]['id'], 'to-lifecycle-environment-id': env1['id']} ) - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) environment = {'id': env1['id'], 'name': env1['name']} assert environment in new_cv['lifecycle-environments'] @pytest.mark.run_in_one_thread @pytest.mark.tier3 def test_positive_promote_rh_and_custom_content( - self, module_entitlement_manifest_org, module_rhel_content + self, module_entitlement_manifest_org, module_rhel_content, module_target_sat ): """attempt to promote a content view containing RH content and custom content using filters @@ -1205,26 +1290,31 @@ def test_positive_promote_rh_and_custom_content( :CaseLevel: Integration """ # Create custom repo - new_repo = cli_factory.make_repository( + new_repo = module_target_sat.cli_factory.make_repository( { - 'product-id': cli_factory.make_product( + 'content-type': 'yum', + 'product-id': module_target_sat.cli_factory.make_product( {'organization-id': module_entitlement_manifest_org.id} - )['id'] + )['id'], } ) # Sync custom repo - Repository.synchronize({'id': new_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) # Create CV - new_cv = cli_factory.make_content_view( + new_cv = module_target_sat.cli_factory.make_content_view( {'organization-id': module_entitlement_manifest_org.id} ) # Associate repos with CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': module_rhel_content['id']}) - ContentView.add_repository({'id': new_cv['id'], 'repository-id': new_repo['id']}) - cvf = cli_factory.make_content_view_filter( + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': module_rhel_content['id']} + ) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': new_repo['id']} + ) + cvf = module_target_sat.cli_factory.make_content_view_filter( {'content-view-id': new_cv['id'], 'inclusion': 'false', 'type': 'rpm'} ) - cli_factory.make_content_view_filter_rule( + module_target_sat.cli_factory.content_view_filter_rule( { 'content-view-filter-id': cvf['filter-id'], 'min-version': 5, @@ -1232,22 +1322,22 @@ def test_positive_promote_rh_and_custom_content( } ) # Publish a new version of CV - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) - env1 = cli_factory.make_lifecycle_environment( + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) + env1 = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_entitlement_manifest_org.id} ) # Promote the Published version of CV to the next env - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': new_cv['versions'][0]['id'], 'to-lifecycle-environment-id': env1['id']} ) - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) environment = {'id': env1['id'], 'name': env1['name']} assert environment in new_cv['lifecycle-environments'] @pytest.mark.build_sanity @pytest.mark.tier2 - def test_positive_promote_custom_content(self, module_org, module_product): + def test_positive_promote_custom_content(self, module_org, module_product, module_target_sat): """attempt to promote a content view containing custom content :id: 64c2f1c2-7443-4836-a108-060b913ad2b1 @@ -1261,32 +1351,38 @@ def test_positive_promote_custom_content(self, module_org, module_product): :CaseLevel: Integration """ # Create REPO - new_repo = cli_factory.make_repository({'product-id': module_product.id}) + new_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': module_product.id} + ) # Sync REPO - Repository.synchronize({'id': new_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) # create lce - environment = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) + environment = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) # Create CV - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Associate repo to CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': new_repo['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': new_repo['id']} + ) # Publish a new version of CV - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) # Promote the Published version of CV to the next env - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( { 'id': new_cv['versions'][0]['id'], 'to-lifecycle-environment-id': environment['id'], } ) - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert {'id': environment['id'], 'name': environment['name']} in new_cv[ 'lifecycle-environments' ] @pytest.mark.tier2 - def test_positive_promote_ccv(self, module_org, module_product): + def test_positive_promote_ccv(self, module_org, module_product, module_target_sat): """attempt to promote a content view containing custom content :id: 9d31113d-39ec-4524-854c-7f03b0f028fe @@ -1302,44 +1398,52 @@ def test_positive_promote_ccv(self, module_org, module_product): :CaseLevel: Integration """ # Create REPO - new_repo = cli_factory.make_repository({'product-id': module_product.id}) + new_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': module_product.id} + ) # Sync REPO - Repository.synchronize({'id': new_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) # Create lce - environment = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) + environment = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) # Create CV - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Associate repo to CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': new_repo['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': new_repo['id']} + ) # Publish a new version of CV - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) # Let us now store the version1 id version1_id = new_cv['versions'][0]['id'] # Create CV - con_view = cli_factory.make_content_view( + con_view = module_target_sat.cli_factory.make_content_view( {'composite': True, 'organization-id': module_org.id} ) # Associate version to composite CV - ContentView.add_version({'content-view-version-id': version1_id, 'id': con_view['id']}) + module_target_sat.cli.ContentView.add_version( + {'content-view-version-id': version1_id, 'id': con_view['id']} + ) # Publish a new version of CV - ContentView.publish({'id': con_view['id']}) + module_target_sat.cli.ContentView.publish({'id': con_view['id']}) # As version info is populated after publishing only - con_view = ContentView.info({'id': con_view['id']}) + con_view = module_target_sat.cli.ContentView.info({'id': con_view['id']}) # Promote the Published version of CV to the next env - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( { 'id': con_view['versions'][0]['id'], 'to-lifecycle-environment-id': environment['id'], } ) - con_view = ContentView.info({'id': con_view['id']}) + con_view = module_target_sat.cli.ContentView.info({'id': con_view['id']}) assert {'id': environment['id'], 'name': environment['name']} in con_view[ 'lifecycle-environments' ] @pytest.mark.tier2 - def test_negative_promote_default_cv(self, module_org): + def test_negative_promote_default_cv(self, module_org, module_target_sat): """attempt to promote a default content view :id: ef25a4d9-8852-4d2c-8355-e9b07eb0560b @@ -1350,19 +1454,25 @@ def test_negative_promote_default_cv(self, module_org): :CaseLevel: Integration """ - environment = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) + environment = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) print("Hello, the org ID is currently", module_org.id) - result = ContentView.list({'organization-id': module_org.id}, per_page=False) + result = module_target_sat.cli.ContentView.list( + {'organization-id': module_org.id}, per_page=False + ) content_view = random.choice([cv for cv in result if cv['name'] == constants.DEFAULT_CV]) - cvv = ContentView.version_list({'content-view-id': content_view['content-view-id']})[0] + cvv = module_target_sat.cli.ContentView.version_list( + {'content-view-id': content_view['content-view-id']} + )[0] # Promote the Default CV to the next env with pytest.raises(CLIReturnCodeError): - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': cvv['id'], 'to-lifecycle-environment-id': environment['id']} ) @pytest.mark.tier2 - def test_negative_promote_with_invalid_lce(self, module_org, module_product): + def test_negative_promote_with_invalid_lce(self, module_org, module_product, module_target_sat): """attempt to promote a content view using an invalid environment @@ -1375,20 +1485,24 @@ def test_negative_promote_with_invalid_lce(self, module_org, module_product): :CaseLevel: Integration """ # Create REPO - new_repo = cli_factory.make_repository({'product-id': module_product.id}) + new_repo = module_target_sat.cli_factory.make_repository( + {'product-id': module_product.id, 'content-type': 'yum'} + ) # Sync REPO - Repository.synchronize({'id': new_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) # Create CV - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Associate repo to CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': new_repo['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': new_repo['id']} + ) # Publish a new version of CV - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) # Promote the Published version of CV, # to the previous env which is Library with pytest.raises(CLIReturnCodeError): - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( { 'id': new_cv['versions'][0]['id'], 'to-lifecycle-environment-id': new_cv['lifecycle-environments'][0]['id'], @@ -1401,7 +1515,7 @@ def test_negative_promote_with_invalid_lce(self, module_org, module_product): @pytest.mark.run_in_one_thread @pytest.mark.tier2 def test_positive_publish_rh_content( - self, module_entitlement_manifest_org, module_rhel_content + self, module_entitlement_manifest_org, module_rhel_content, module_target_sat ): """attempt to publish a content view containing RH content @@ -1416,14 +1530,16 @@ def test_positive_publish_rh_content( :CaseLevel: Integration """ # Create CV - new_cv = cli_factory.make_content_view( + new_cv = module_target_sat.cli_factory.make_content_view( {'organization-id': module_entitlement_manifest_org.id} ) # Associate repo to CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': module_rhel_content['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': module_rhel_content['id']} + ) # Publish a new version of CV - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert new_cv['yum-repositories'][0]['name'] == module_rhel_content['name'] assert new_cv['versions'][0]['version'] == '1.0' @@ -1432,7 +1548,7 @@ def test_positive_publish_rh_content( @pytest.mark.pit_server @pytest.mark.tier3 def test_positive_publish_rh_and_custom_content( - self, module_entitlement_manifest_org, module_rhel_content + self, module_entitlement_manifest_org, module_rhel_content, module_target_sat ): """attempt to publish a content view containing a RH and custom repos and has filters @@ -1448,26 +1564,31 @@ def test_positive_publish_rh_and_custom_content( :CaseLevel: Integration """ # Create custom repo - new_repo = cli_factory.make_repository( + new_repo = module_target_sat.cli_factory.make_repository( { - 'product-id': cli_factory.make_product( + 'content-type': 'yum', + 'product-id': module_target_sat.cli_factory.make_product( {'organization-id': module_entitlement_manifest_org.id} - )['id'] + )['id'], } ) # Sync custom repo - Repository.synchronize({'id': new_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) # Create CV - new_cv = cli_factory.make_content_view( + new_cv = module_target_sat.cli_factory.make_content_view( {'organization-id': module_entitlement_manifest_org.id} ) # Associate repos with CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': module_rhel_content['id']}) - ContentView.add_repository({'id': new_cv['id'], 'repository-id': new_repo['id']}) - cvf = cli_factory.make_content_view_filter( + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': module_rhel_content['id']} + ) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': new_repo['id']} + ) + cvf = module_target_sat.cli_factory.make_content_view_filter( {'content-view-id': new_cv['id'], 'inclusion': 'false', 'type': 'rpm'} ) - cli_factory.make_content_view_filter_rule( + module_target_sat.cli_factory.content_view_filter_rule( { 'content-view-filter-id': cvf['filter-id'], 'min-version': 5, @@ -1475,15 +1596,15 @@ def test_positive_publish_rh_and_custom_content( } ) # Publish a new version of CV - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert {module_rhel_content['name'], new_repo['name']}.issubset( {repo['name'] for repo in new_cv['yum-repositories']} ) assert new_cv['versions'][0]['version'] == '1.0' @pytest.mark.tier2 - def test_positive_publish_custom_content(self, module_org, module_product): + def test_positive_publish_custom_content(self, module_org, module_product, module_target_sat): """attempt to publish a content view containing custom content :id: 84158023-3980-45c6-87d8-faacea3c942f @@ -1496,21 +1617,25 @@ def test_positive_publish_custom_content(self, module_org, module_product): :CaseLevel: Integration """ - new_repo = cli_factory.make_repository({'product-id': module_product.id}) + new_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': module_product.id} + ) # Sync REPO - Repository.synchronize({'id': new_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) # Create CV - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Associate repo to CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': new_repo['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': new_repo['id']} + ) # Publish a new version of CV - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert new_cv['yum-repositories'][0]['name'] == new_repo['name'] assert new_cv['versions'][0]['version'] == '1.0' @pytest.mark.tier2 - def test_positive_publish_custom_major_minor_cv_version(self): + def test_positive_publish_custom_major_minor_cv_version(self, module_target_sat): """CV can published with custom major and minor versions :id: 6697cd22-253a-4bdc-a108-7e0af22caaf4 @@ -1528,14 +1653,16 @@ def test_positive_publish_custom_major_minor_cv_version(self): :CaseLevel: System """ - org = cli_factory.make_org() + org = module_target_sat.cli_factory.make_org() major = random.randint(1, 1000) minor = random.randint(1, 1000) - content_view = cli_factory.make_content_view( + content_view = module_target_sat.cli_factory.make_content_view( {'name': gen_string('alpha'), 'organization-id': org['id']} ) - ContentView.publish({'id': content_view['id'], 'major': major, 'minor': minor}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish( + {'id': content_view['id'], 'major': major, 'minor': minor} + ) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) cvv = content_view['versions'][0]['version'] assert cvv.split('.')[0] == str(major) assert cvv.split('.')[1] == str(minor) @@ -1545,7 +1672,9 @@ def test_positive_publish_custom_major_minor_cv_version(self): @pytest.mark.skipif( (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) - def test_positive_publish_custom_content_module_stream(self, module_org, module_product): + def test_positive_publish_custom_content_module_stream( + self, module_org, module_product, module_target_sat + ): """attempt to publish a content view containing custom content module streams @@ -1560,7 +1689,7 @@ def test_positive_publish_custom_content_module_stream(self, module_org, module_ :CaseLevel: Integration """ - software_repo = cli_factory.make_repository( + software_repo = module_target_sat.cli_factory.make_repository( { 'product-id': module_product.id, 'content-type': 'yum', @@ -1568,7 +1697,7 @@ def test_positive_publish_custom_content_module_stream(self, module_org, module_ } ) - animal_repo = cli_factory.make_repository( + animal_repo = module_target_sat.cli_factory.make_repository( { 'product-id': module_product.id, 'content-type': 'yum', @@ -1577,26 +1706,40 @@ def test_positive_publish_custom_content_module_stream(self, module_org, module_ ) # Sync REPO's - Repository.synchronize({'id': animal_repo['id']}) - Repository.synchronize({'id': software_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': animal_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': software_repo['id']}) # Create CV - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Associate repo to CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': animal_repo['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': animal_repo['id']} + ) # Publish a new version of CV - ContentView.publish({'id': new_cv['id']}) - new_cv_version_1 = ContentView.info({'id': new_cv['id']})['versions'][0] - module_streams = ModuleStream.list({'content-view-version-id': (new_cv_version_1['id'])}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv_version_1 = module_target_sat.cli.ContentView.info({'id': new_cv['id']})['versions'][ + 0 + ] + module_streams = module_target_sat.cli.ModuleStream.list( + {'content-view-version-id': (new_cv_version_1['id'])} + ) assert len(module_streams) > 6 # Publish another new version of CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': software_repo['id']}) - ContentView.publish({'id': new_cv['id']}) - new_cv_version_2 = ContentView.info({'id': new_cv['id']})['versions'][1] - module_streams = ModuleStream.list({'content-view-version-id': new_cv_version_2['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': software_repo['id']} + ) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv_version_2 = module_target_sat.cli.ContentView.info({'id': new_cv['id']})['versions'][ + 1 + ] + module_streams = module_target_sat.cli.ModuleStream.list( + {'content-view-version-id': new_cv_version_2['id']} + ) assert len(module_streams) > 13 @pytest.mark.tier2 - def test_positive_republish_after_content_removed(self, module_org, module_product): + def test_positive_republish_after_content_removed( + self, module_org, module_product, module_target_sat + ): """Attempt to re-publish content view after all associated content were removed from that CV @@ -1616,9 +1759,11 @@ def test_positive_republish_after_content_removed(self, module_org, module_produ :CaseLevel: Integration """ # Create new Yum repository - yum_repo = cli_factory.make_repository({'product-id': module_product.id}) + yum_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': module_product.id} + ) # Create new Docker repository - docker_repo = cli_factory.make_repository( + docker_repo = module_target_sat.cli_factory.make_repository( { 'content-type': 'docker', 'docker-upstream-name': 'quay/busybox', @@ -1628,9 +1773,9 @@ def test_positive_republish_after_content_removed(self, module_org, module_produ ) # Sync all three repos for repo_id in [yum_repo['id'], docker_repo['id']]: - Repository.synchronize({'id': repo_id}) + module_target_sat.cli.Repository.synchronize({'id': repo_id}) # Create CV with different content types - new_cv = cli_factory.make_content_view( + new_cv = module_target_sat.cli_factory.make_content_view( { 'organization-id': module_org.id, 'repository-ids': [yum_repo['id'], docker_repo['id']], @@ -1643,27 +1788,29 @@ def test_positive_republish_after_content_removed(self, module_org, module_produ ]: assert len(new_cv[repo_type]) == 1 # Publish a new version of CV - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['versions']) == 1 # Remove content from CV for repo_id in [yum_repo['id'], docker_repo['id']]: - ContentView.remove_repository({'id': new_cv['id'], 'repository-id': repo_id}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.remove_repository( + {'id': new_cv['id'], 'repository-id': repo_id} + ) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) for repo_type in [ 'yum-repositories', 'container-image-repositories', ]: assert len(new_cv[repo_type]) == 0 # Publish a new version of CV - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['versions']) == 2 @pytest.mark.run_in_one_thread @pytest.mark.tier2 def test_positive_republish_after_rh_content_removed( - self, module_entitlement_manifest_org, module_rhel_content + self, module_entitlement_manifest_org, module_rhel_content, module_target_sat ): """Attempt to re-publish content view after all RH associated content was removed from that CV @@ -1683,36 +1830,36 @@ def test_positive_republish_after_rh_content_removed( :CaseImportance: Medium """ - new_cv = cli_factory.make_content_view( + new_cv = module_target_sat.cli_factory.make_content_view( {'organization-id': module_entitlement_manifest_org.id} ) # Associate repo to CV - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( { 'id': new_cv['id'], 'organization-id': module_entitlement_manifest_org.id, 'repository-id': module_rhel_content['id'], } ) - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['yum-repositories']) == 1 # Publish a new version of CV - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['versions']) == 1 # Remove content from CV - ContentView.remove_repository( + module_target_sat.cli.ContentView.remove_repository( {'id': new_cv['id'], 'repository-id': module_rhel_content['id']} ) - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['yum-repositories']) == 0 # Publish a new version of CV once more - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert len(new_cv['versions']) == 2 @pytest.mark.tier2 - def test_positive_publish_ccv(self, module_org, module_product): + def test_positive_publish_ccv(self, module_org, module_product, module_target_sat): """attempt to publish a composite content view containing custom content @@ -1726,37 +1873,45 @@ def test_positive_publish_ccv(self, module_org, module_product): :CaseLevel: Integration """ - repository = cli_factory.make_repository({'product-id': module_product.id}) + repository = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': module_product.id} + ) # Sync REPO - Repository.synchronize({'id': repository['id']}) + module_target_sat.cli.Repository.synchronize({'id': repository['id']}) # Create CV - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) # Associate repo to CV - ContentView.add_repository({'id': content_view['id'], 'repository-id': repository['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': content_view['id'], 'repository-id': repository['id']} + ) # Publish a new version of CV - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) # Let us now store the version1 id version1_id = content_view['versions'][0]['id'] # Create composite CV - composite_cv = cli_factory.make_content_view( + composite_cv = module_target_sat.cli_factory.make_content_view( {'composite': True, 'organization-id': module_org.id} ) # Associate version to composite CV - ContentView.add_version({'content-view-version-id': version1_id, 'id': composite_cv['id']}) + module_target_sat.cli.ContentView.add_version( + {'content-view-version-id': version1_id, 'id': composite_cv['id']} + ) # Assert whether version was associated to composite CV - composite_cv = ContentView.info({'id': composite_cv['id']}) + composite_cv = module_target_sat.cli.ContentView.info({'id': composite_cv['id']}) assert composite_cv['components'][0]['id'] == version1_id # Publish a new version of CV - ContentView.publish({'id': composite_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': composite_cv['id']}) # Assert whether Version1 was created and exists in Library Env. - composite_cv = ContentView.info({'id': composite_cv['id']}) + composite_cv = module_target_sat.cli.ContentView.info({'id': composite_cv['id']}) assert composite_cv['lifecycle-environments'][0]['name'] == constants.ENVIRONMENT assert composite_cv['versions'][0]['version'] == '1.0' @pytest.mark.tier2 @pytest.mark.upgrade - def test_positive_update_version_once(self, module_org, module_product): + def test_positive_update_version_once(self, module_org, module_product, module_target_sat): # Dev notes: # If Dev has version x, then when I promote version y into # Dev, version x goes away (ie when I promote version 1 to Dev, @@ -1782,54 +1937,60 @@ def test_positive_update_version_once(self, module_org, module_product): :CaseImportance: Critical """ # Create REPO - new_repo = cli_factory.make_repository({'product-id': module_product.id}) + new_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': module_product.id} + ) # Sync REPO - Repository.synchronize({'id': new_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) # Create lce - environment = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) + environment = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) # Create CV - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Associate repo to CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': new_repo['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': new_repo['id']} + ) # Publish a version1 of CV - ContentView.publish({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) # Only after we publish version1 the info is populated. - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) # Let us now store the version1 id version1_id = new_cv['versions'][0]['id'] # Actual assert for this test happens HERE # Test whether the version1 now belongs to Library - version1 = ContentView.version_info({'id': version1_id}) + version1 = module_target_sat.cli.ContentView.version_info({'id': version1_id}) assert constants.ENVIRONMENT in [env['label'] for env in version1['lifecycle-environments']] # Promotion of version1 to Dev env - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': version1_id, 'to-lifecycle-environment-id': environment['id']} ) # The only way to validate whether env has the version is to # validate that version has the env. - version1 = ContentView.version_info({'id': version1_id}) + version1 = module_target_sat.cli.ContentView.version_info({'id': version1_id}) assert environment['id'] in [env['id'] for env in version1['lifecycle-environments']] # Now Publish version2 of CV - ContentView.publish({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) # Only after we publish version2 the info is populated. - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) new_cv['versions'].sort(key=lambda version: version['id']) # Let us now store the version2 id version2_id = new_cv['versions'][1]['id'] # Test whether the version2 now belongs to Library - version2 = ContentView.version_info({'id': version2_id}) + version2 = module_target_sat.cli.ContentView.version_info({'id': version2_id}) assert constants.ENVIRONMENT in [env['label'] for env in version2['lifecycle-environments']] # Promotion of version2 to Dev env - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': version2_id, 'to-lifecycle-environment-id': environment['id']} ) # Actual assert for this test happens here. # Test whether the version2 now belongs to next env - version2 = ContentView.version_info({'id': version2_id}) + version2 = module_target_sat.cli.ContentView.version_info({'id': version2_id}) assert environment['id'] in [env['id'] for env in version2['lifecycle-environments']] @pytest.mark.tier2 - def test_positive_update_version_multiple(self, module_org, module_product): + def test_positive_update_version_multiple(self, module_org, module_product, module_target_sat): # Dev notes: # Similarly when I publish version y, version x goes away from # Library (ie when I publish version 2, version 1 disappears) @@ -1853,61 +2014,69 @@ def test_positive_update_version_multiple(self, module_org, module_product): :CaseLevel: Integration """ # Create REPO - new_repo = cli_factory.make_repository({'product-id': module_product.id}) + new_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': module_product.id} + ) # Sync REPO - Repository.synchronize({'id': new_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) # Create lce - environment = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) + environment = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) # Create CV - new_cv = cli_factory.make_content_view({'organization-id': module_org.id}) + new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Associate repo to CV - ContentView.add_repository({'id': new_cv['id'], 'repository-id': new_repo['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': new_cv['id'], 'repository-id': new_repo['id']} + ) # Publish a version1 of CV - ContentView.publish({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) # Only after we publish version1 the info is populated. - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) # Let us now store the version1 id version1_id = new_cv['versions'][0]['id'] # Test whether the version1 now belongs to Library - version = ContentView.version_info({'id': version1_id}) + version = module_target_sat.cli.ContentView.version_info({'id': version1_id}) assert constants.ENVIRONMENT in [env['label'] for env in version['lifecycle-environments']] # Promotion of version1 to Dev env - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': version1_id, 'to-lifecycle-environment-id': environment['id']} ) # The only way to validate whether env has the version is to # validate that version has the env. # Test whether the version1 now belongs to next env - version1 = ContentView.version_info({'id': version1_id}) + version1 = module_target_sat.cli.ContentView.version_info({'id': version1_id}) assert environment['id'] in [env['id'] for env in version1['lifecycle-environments']] # Now Publish version2 of CV - ContentView.publish({'id': new_cv['id']}) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) # As per Dev Notes: # Similarly when I publish version y, version x goes away from Library. # Actual assert for this test happens here. # Test that version1 does not exist in Library after publishing v2 - version1 = ContentView.version_info({'id': version1_id}) + version1 = module_target_sat.cli.ContentView.version_info({'id': version1_id}) assert len(version1['lifecycle-environments']) == 1 assert constants.ENVIRONMENT not in [ env['label'] for env in version1['lifecycle-environments'] ] # Only after we publish version2 the info is populated. - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) new_cv['versions'].sort(key=lambda version: version['id']) # Let us now store the version2 id version2_id = new_cv['versions'][1]['id'] # Promotion of version2 to next env - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': version2_id, 'to-lifecycle-environment-id': environment['id']} ) # Actual assert for this test happens here. # Test that version1 does not exist in any/next env after, # promoting version2 to next env - version1 = ContentView.version_info({'id': version1_id}) + version1 = module_target_sat.cli.ContentView.version_info({'id': version1_id}) assert len(version1['lifecycle-environments']) == 0 @pytest.mark.tier2 - def test_positive_auto_update_composite_to_latest_cv_version(self, module_org): + def test_positive_auto_update_composite_to_latest_cv_version( + self, module_org, module_target_sat + ): """Ensure that composite content view component is auto updated to the latest content view version. @@ -1933,15 +2102,17 @@ def test_positive_auto_update_composite_to_latest_cv_version(self, module_org): :CaseImportance: High """ - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert len(content_view['versions']) == 1 version_1_id = content_view['versions'][0]['id'] - composite_cv = cli_factory.make_content_view( + composite_cv = module_target_sat.cli_factory.make_content_view( {'composite': True, 'organization-id': module_org.id} ) - ContentView.component_add( + module_target_sat.cli.ContentView.component_add( { 'composite-content-view-id': composite_cv['id'], 'component-content-view-id': content_view['id'], @@ -1949,21 +2120,25 @@ def test_positive_auto_update_composite_to_latest_cv_version(self, module_org): } ) # Ensure that version 1 is in composite content view components - components = ContentView.component_list({'composite-content-view-id': composite_cv['id']}) + components = module_target_sat.cli.ContentView.component_list( + {'composite-content-view-id': composite_cv['id']} + ) assert len(components) == 1 component_id = components[0]['content-view-id'] assert components[0]['version-id'] == f'{version_1_id} (Latest)' assert components[0]['current-version'] == '1.0' # Publish the content view a second time - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert len(content_view['versions']) == 2 content_view['versions'].sort(key=lambda version: version['id']) # Ensure that composite content view component has been updated to # version 2 version_2_id = content_view['versions'][1]['id'] assert version_1_id != version_2_id - components = ContentView.component_list({'composite-content-view-id': composite_cv['id']}) + components = module_target_sat.cli.ContentView.component_list( + {'composite-content-view-id': composite_cv['id']} + ) assert len(components) == 1 # Ensure that this is the same component that is updated assert component_id == components[0]['content-view-id'] @@ -1971,7 +2146,7 @@ def test_positive_auto_update_composite_to_latest_cv_version(self, module_org): assert components[0]['current-version'] == '2.0' @pytest.mark.tier3 - def test_positive_subscribe_chost_by_id(self, module_org): + def test_positive_subscribe_chost_by_id(self, module_org, module_target_sat): """Attempt to subscribe content host to content view :id: db0bfd9d-3150-427e-9683-a68af33813e7 @@ -1982,15 +2157,21 @@ def test_positive_subscribe_chost_by_id(self, module_org): :CaseLevel: System """ - env = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + env = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) cvv = content_view['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': env['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': env['id']} + ) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert content_view['content-host-count'] == '0' - cli_factory.make_fake_host( + module_target_sat.cli_factory.make_fake_host( { 'content-view-id': content_view['id'], 'lifecycle-environment-id': env['id'], @@ -1998,13 +2179,13 @@ def test_positive_subscribe_chost_by_id(self, module_org): 'organization-id': module_org.id, } ) - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert content_view['content-host-count'] == '1' @pytest.mark.run_in_one_thread @pytest.mark.tier3 def test_positive_subscribe_chost_by_id_using_rh_content( - self, module_entitlement_manifest_org, module_rhel_content + self, module_entitlement_manifest_org, module_rhel_content, module_target_sat ): """Attempt to subscribe content host to content view that has Red Hat repository assigned to it @@ -2018,28 +2199,30 @@ def test_positive_subscribe_chost_by_id_using_rh_content( :CaseImportance: Medium """ - env = cli_factory.make_lifecycle_environment( + env = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_entitlement_manifest_org.id} ) - content_view = cli_factory.make_content_view( + content_view = module_target_sat.cli_factory.make_content_view( {'organization-id': module_entitlement_manifest_org.id} ) - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( { 'id': content_view['id'], 'organization-id': module_entitlement_manifest_org.id, 'repository-id': module_rhel_content['id'], } ) - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert content_view['yum-repositories'][0]['name'] == module_rhel_content['name'] - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) cvv = content_view['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': env['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': env['id']} + ) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert content_view['content-host-count'] == '0' - cli_factory.make_fake_host( + module_target_sat.cli_factory.make_fake_host( { 'content-view-id': content_view['id'], 'lifecycle-environment-id': env['id'], @@ -2047,14 +2230,14 @@ def test_positive_subscribe_chost_by_id_using_rh_content( 'organization-id': module_entitlement_manifest_org.id, } ) - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert content_view['content-host-count'] == '1' @pytest.mark.run_in_one_thread @pytest.mark.tier3 @pytest.mark.upgrade def test_positive_subscribe_chost_by_id_using_rh_content_and_filters( - self, module_entitlement_manifest_org, module_rhel_content + self, module_entitlement_manifest_org, module_rhel_content, module_target_sat ): """Attempt to subscribe content host to filtered content view that has Red Hat repository assigned to it @@ -2070,24 +2253,24 @@ def test_positive_subscribe_chost_by_id_using_rh_content_and_filters( :CaseImportance: Low """ - env = cli_factory.make_lifecycle_environment( + env = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_entitlement_manifest_org.id} ) - content_view = cli_factory.make_content_view( + content_view = module_target_sat.cli_factory.make_content_view( {'organization-id': module_entitlement_manifest_org.id} ) - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( { 'id': content_view['id'], 'organization-id': module_entitlement_manifest_org.id, 'repository-id': module_rhel_content['id'], } ) - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert content_view['yum-repositories'][0]['name'] == module_rhel_content['name'] name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'inclusion': 'true', @@ -2096,7 +2279,7 @@ def test_positive_subscribe_chost_by_id_using_rh_content_and_filters( } ) - cli_factory.make_content_view_filter_rule( + module_target_sat.cli_factory.content_view_filter_rule( { 'content-view-filter': name, 'content-view-id': content_view['id'], @@ -2104,15 +2287,17 @@ def test_positive_subscribe_chost_by_id_using_rh_content_and_filters( } ) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) cvv = content_view['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': env['id']}) + module_target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': env['id']} + ) - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert content_view['content-host-count'] == '0' - cli_factory.make_fake_host( + module_target_sat.cli_factory.make_fake_host( { 'content-view-id': content_view['id'], 'lifecycle-environment-id': env['id'], @@ -2120,11 +2305,13 @@ def test_positive_subscribe_chost_by_id_using_rh_content_and_filters( 'organization-id': module_entitlement_manifest_org.id, } ) - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert content_view['content-host-count'] == '1' @pytest.mark.tier3 - def test_positive_subscribe_chost_by_id_using_custom_content(self, module_org): + def test_positive_subscribe_chost_by_id_using_custom_content( + self, module_org, module_target_sat + ): """Attempt to subscribe content host to content view that has custom repository assigned to it @@ -2137,12 +2324,18 @@ def test_positive_subscribe_chost_by_id_using_custom_content(self, module_org): :CaseImportance: High """ - new_product = cli_factory.make_product({'organization-id': module_org.id}) - new_repo = cli_factory.make_repository({'product-id': new_product['id']}) - env = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) - Repository.synchronize({'id': new_repo['id']}) - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.add_repository( + new_product = module_target_sat.cli_factory.make_product({'organization-id': module_org.id}) + new_repo = module_target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': new_product['id']} + ) + env = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.add_repository( { 'id': content_view['id'], 'organization-id': module_org.id, @@ -2150,15 +2343,17 @@ def test_positive_subscribe_chost_by_id_using_custom_content(self, module_org): } ) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) cvv = content_view['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': env['id']}) + module_target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': env['id']} + ) - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert content_view['content-host-count'] == '0' - cli_factory.make_fake_host( + module_target_sat.cli_factory.make_fake_host( { 'content-view-id': content_view['id'], 'lifecycle-environment-id': env['id'], @@ -2166,11 +2361,11 @@ def test_positive_subscribe_chost_by_id_using_custom_content(self, module_org): 'organization-id': module_org.id, } ) - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert content_view['content-host-count'] == '1' @pytest.mark.tier3 - def test_positive_subscribe_chost_by_id_using_ccv(self, module_org): + def test_positive_subscribe_chost_by_id_using_ccv(self, module_org, module_target_sat): """Attempt to subscribe content host to composite content view :id: 4be340c0-9e58-4b96-ab37-d7e3b12c724f @@ -2182,19 +2377,23 @@ def test_positive_subscribe_chost_by_id_using_ccv(self, module_org): :CaseLevel: System """ - env = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) - content_view = cli_factory.make_content_view( + env = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + content_view = module_target_sat.cli_factory.make_content_view( {'composite': True, 'organization-id': module_org.id} ) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) cvv = content_view['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': env['id']}) + module_target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': env['id']} + ) - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert content_view['content-host-count'] == '0' - cli_factory.make_fake_host( + module_target_sat.cli_factory.make_fake_host( { 'content-view-id': content_view['id'], 'lifecycle-environment-id': env['id'], @@ -2202,7 +2401,7 @@ def test_positive_subscribe_chost_by_id_using_ccv(self, module_org): 'organization-id': module_org.id, } ) - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert content_view['content-host-count'] == '1' @pytest.mark.tier3 @@ -2274,12 +2473,12 @@ def test_positive_sub_host_with_restricted_user_perm_at_custom_loc( 'Architecture': ['view_architectures'], } # Create a location and organization - loc = cli_factory.make_location() - default_loc = Location.info({'name': constants.DEFAULT_LOC}) - org = cli_factory.make_org() - Org.add_location({'id': org['id'], 'location-id': loc['id']}) + loc = target_sat.cli_factory.make_location() + default_loc = target_sat.cli.Location.info({'name': constants.DEFAULT_LOC}) + org = target_sat.cli_factory.make_org() + target_sat.cli.Org.add_location({'id': org['id'], 'location-id': loc['id']}) # Create a non admin user, for the moment without any permissions - user = cli_factory.make_user( + user = target_sat.cli_factory.user( { 'admin': False, 'default-organization-id': org['id'], @@ -2291,9 +2490,9 @@ def test_positive_sub_host_with_restricted_user_perm_at_custom_loc( } ) # Create a new role - role = cli_factory.make_role() + role = target_sat.cli_factory.make_role() # Get the available permissions - available_permissions = Filter.available_permissions() + available_permissions = target_sat.cli.Filter.available_permissions() # group the available permissions by resource type available_rc_permissions = {} for permission in available_permissions: @@ -2313,40 +2512,42 @@ def test_positive_sub_host_with_restricted_user_perm_at_custom_loc( # assert that all the required permissions are available assert set(permission_names) == set(available_permission_names) # Create the current resource type role permissions - cli_factory.make_filter({'role-id': role['id'], 'permissions': permission_names}) + target_sat.cli_factory.make_filter( + {'role-id': role['id'], 'permissions': permission_names} + ) # Add the created and initiated role with permissions to user - User.add_role({'id': user['id'], 'role-id': role['id']}) + target_sat.cli.User.add_role({'id': user['id'], 'role-id': role['id']}) # assert that the user is not an admin one and cannot read the current # role info (note: view_roles is not in the required permissions) with pytest.raises(CLIReturnCodeError) as context: - Role.with_user(user_name, user_password).info({'id': role['id']}) + target_sat.cli.Role.with_user(user_name, user_password).info({'id': role['id']}) assert 'Access denied' in str(context) # Create a lifecycle environment - env = cli_factory.make_lifecycle_environment({'organization-id': org['id']}) + env = target_sat.cli_factory.make_lifecycle_environment({'organization-id': org['id']}) # Create a product - product = cli_factory.make_product({'organization-id': org['id']}) + product = target_sat.cli_factory.make_product({'organization-id': org['id']}) # Create a yum repository and synchronize - repo = cli_factory.make_repository( - {'product-id': product['id'], 'url': settings.repos.yum_1.url} + repo = target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': product['id'], 'url': settings.repos.yum_1.url} ) - Repository.synchronize({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) # Create a content view, add the yum repository and publish - content_view = cli_factory.make_content_view({'organization-id': org['id']}) - ContentView.add_repository( + content_view = target_sat.cli_factory.make_content_view({'organization-id': org['id']}) + target_sat.cli.ContentView.add_repository( {'id': content_view['id'], 'organization-id': org['id'], 'repository-id': repo['id']} ) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = target_sat.cli.ContentView.info({'id': content_view['id']}) # assert that the content view has been published and has versions assert len(content_view['versions']) > 0 content_view_version = content_view['versions'][0] # Promote the content view version to the created environment - ContentView.version_promote( + target_sat.cli.ContentView.version_promote( {'id': content_view_version['id'], 'to-lifecycle-environment-id': env['id']} ) # assert that the user can read the content view info as per required # permissions - user_content_view = ContentView.with_user(user_name, user_password).info( + user_content_view = target_sat.cli.ContentView.with_user(user_name, user_password).info( {'id': content_view['id']} ) # assert that this is the same content view @@ -2361,7 +2562,7 @@ def test_positive_sub_host_with_restricted_user_perm_at_custom_loc( ) assert rhel7_contenthost.subscribed # check that the client host exist in the system - org_hosts = Host.list({'organization-id': org['id']}) + org_hosts = target_sat.cli.Host.list({'organization-id': org['id']}) assert len(org_hosts) == 1 assert org_hosts[0]['name'] == rhel7_contenthost.hostname @@ -2428,11 +2629,11 @@ def test_positive_sub_host_with_restricted_user_perm_at_default_loc( 'Architecture': ['view_architectures'], } # Create organization - loc = Location.info({'name': constants.DEFAULT_LOC}) - org = cli_factory.make_org() - Org.add_location({'id': org['id'], 'location-id': loc['id']}) + loc = target_sat.cli.Location.info({'name': constants.DEFAULT_LOC}) + org = target_sat.cli_factory.make_org() + target_sat.cli.Org.add_location({'id': org['id'], 'location-id': loc['id']}) # Create a non admin user, for the moment without any permissions - user = cli_factory.make_user( + user = target_sat.cli_factory.user( { 'admin': False, 'default-organization-id': org['id'], @@ -2444,9 +2645,9 @@ def test_positive_sub_host_with_restricted_user_perm_at_default_loc( } ) # Create a new role - role = cli_factory.make_role() + role = target_sat.cli_factory.make_role() # Get the available permissions - available_permissions = Filter.available_permissions() + available_permissions = target_sat.cli.Filter.available_permissions() # group the available permissions by resource type available_rc_permissions = {} for permission in available_permissions: @@ -2466,40 +2667,42 @@ def test_positive_sub_host_with_restricted_user_perm_at_default_loc( # assert that all the required permissions are available assert set(permission_names) == set(available_permission_names) # Create the current resource type role permissions - cli_factory.make_filter({'role-id': role['id'], 'permissions': permission_names}) + target_sat.cli_factory.make_filter( + {'role-id': role['id'], 'permissions': permission_names} + ) # Add the created and initiated role with permissions to user - User.add_role({'id': user['id'], 'role-id': role['id']}) + target_sat.cli.User.add_role({'id': user['id'], 'role-id': role['id']}) # assert that the user is not an admin one and cannot read the current # role info (note: view_roles is not in the required permissions) with pytest.raises(CLIReturnCodeError) as context: - Role.with_user(user_name, user_password).info({'id': role['id']}) + target_sat.cli.Role.with_user(user_name, user_password).info({'id': role['id']}) assert '403 Forbidden' in str(context) # Create a lifecycle environment - env = cli_factory.make_lifecycle_environment({'organization-id': org['id']}) + env = target_sat.cli_factory.make_lifecycle_environment({'organization-id': org['id']}) # Create a product - product = cli_factory.make_product({'organization-id': org['id']}) + product = target_sat.cli_factory.make_product({'organization-id': org['id']}) # Create a yum repository and synchronize - repo = cli_factory.make_repository( - {'product-id': product['id'], 'url': settings.repos.yum_1.url} + repo = target_sat.cli_factory.make_repository( + {'content-type': 'yum', 'product-id': product['id'], 'url': settings.repos.yum_1.url} ) - Repository.synchronize({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) # Create a content view, add the yum repository and publish - content_view = cli_factory.make_content_view({'organization-id': org['id']}) - ContentView.add_repository( + content_view = target_sat.cli_factory.make_content_view({'organization-id': org['id']}) + target_sat.cli.ContentView.add_repository( {'id': content_view['id'], 'organization-id': org['id'], 'repository-id': repo['id']} ) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = target_sat.cli.ContentView.info({'id': content_view['id']}) # assert that the content view has been published and has versions assert len(content_view['versions']) > 0 content_view_version = content_view['versions'][0] # Promote the content view version to the created environment - ContentView.version_promote( + target_sat.cli.ContentView.version_promote( {'id': content_view_version['id'], 'to-lifecycle-environment-id': env['id']} ) # assert that the user can read the content view info as per required # permissions - user_content_view = ContentView.with_user(user_name, user_password).info( + user_content_view = target_sat.cli.ContentView.with_user(user_name, user_password).info( {'id': content_view['id']} ) # assert that this is the same content view @@ -2514,12 +2717,12 @@ def test_positive_sub_host_with_restricted_user_perm_at_default_loc( ) assert rhel7_contenthost.subscribed # check that the client host exist in the system - org_hosts = Host.list({'organization-id': org['id']}) + org_hosts = target_sat.cli.Host.list({'organization-id': org['id']}) assert len(org_hosts) == 1 assert org_hosts[0]['name'] == rhel7_contenthost.hostname @pytest.mark.tier1 - def test_positive_clone_by_id(self, module_org): + def test_positive_clone_by_id(self, module_org, module_target_sat): """Clone existing content view by id :id: e3b63e6e-0964-45fb-a765-e1885c0ecbdd @@ -2529,13 +2732,17 @@ def test_positive_clone_by_id(self, module_org): :CaseImportance: Critical """ cloned_cv_name = gen_string('alpha') - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) - new_cv = ContentView.copy({'id': content_view['id'], 'new-name': cloned_cv_name})[0] - new_cv = ContentView.info({'id': new_cv['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) + new_cv = module_target_sat.cli.ContentView.copy( + {'id': content_view['id'], 'new-name': cloned_cv_name} + )[0] + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert new_cv['name'] == cloned_cv_name @pytest.mark.tier1 - def test_positive_clone_by_name(self, module_org): + def test_positive_clone_by_name(self, module_org, module_target_sat): """Clone existing content view by name :id: b4c94286-ebbe-4e4c-a1df-22cb7055984d @@ -2547,19 +2754,21 @@ def test_positive_clone_by_name(self, module_org): :CaseImportance: Critical """ cloned_cv_name = gen_string('alpha') - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) - new_cv = ContentView.copy( + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) + new_cv = module_target_sat.cli.ContentView.copy( { 'name': content_view['name'], 'organization-id': module_org.id, 'new-name': cloned_cv_name, } )[0] - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert new_cv['name'] == cloned_cv_name @pytest.mark.tier2 - def test_positive_clone_within_same_env(self, module_org): + def test_positive_clone_within_same_env(self, module_org, module_target_sat): """Attempt to create, publish and promote new content view based on existing view within the same environment as the original content view @@ -2573,22 +2782,32 @@ def test_positive_clone_within_same_env(self, module_org): :CaseImportance: High """ cloned_cv_name = gen_string('alpha') - lc_env = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + lc_env = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) cvv = content_view['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': lc_env['id']}) - new_cv = ContentView.copy({'id': content_view['id'], 'new-name': cloned_cv_name})[0] - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': lc_env['id']} + ) + new_cv = module_target_sat.cli.ContentView.copy( + {'id': content_view['id'], 'new-name': cloned_cv_name} + )[0] + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) cvv = new_cv['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': lc_env['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': lc_env['id']} + ) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert {'id': lc_env['id'], 'name': lc_env['name']} in new_cv['lifecycle-environments'] @pytest.mark.tier2 - def test_positive_clone_with_diff_env(self, module_org): + def test_positive_clone_with_diff_env(self, module_org, module_target_sat): """Attempt to create, publish and promote new content view based on existing view but promoted to a different environment @@ -2602,21 +2821,31 @@ def test_positive_clone_with_diff_env(self, module_org): :CaseLevel: Integration """ cloned_cv_name = gen_string('alpha') - lc_env = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) - lc_env_cloned = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + lc_env = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + lc_env_cloned = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) cvv = content_view['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': lc_env['id']}) - new_cv = ContentView.copy({'id': content_view['id'], 'new-name': cloned_cv_name})[0] - ContentView.publish({'id': new_cv['id']}) - new_cv = ContentView.info({'id': new_cv['id']}) + module_target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': lc_env['id']} + ) + new_cv = module_target_sat.cli.ContentView.copy( + {'id': content_view['id'], 'new-name': cloned_cv_name} + )[0] + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) cvv = new_cv['versions'][0] - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': cvv['id'], 'to-lifecycle-environment-id': lc_env_cloned['id']} ) - new_cv = ContentView.info({'id': new_cv['id']}) + new_cv = module_target_sat.cli.ContentView.info({'id': new_cv['id']}) assert {'id': lc_env_cloned['id'], 'name': lc_env_cloned['name']} in new_cv[ 'lifecycle-environments' ] @@ -2657,7 +2886,9 @@ def test_positive_restart_dynflow_publish(self): @pytest.mark.skipif( (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) - def test_positive_remove_renamed_cv_version_from_default_env(self, module_org): + def test_positive_remove_renamed_cv_version_from_default_env( + self, module_org, module_target_sat + ): """Remove version of renamed content view from Library environment :id: aa9bbfda-72e8-45ec-b26d-fdf2691980cf @@ -2678,37 +2909,43 @@ def test_positive_remove_renamed_cv_version_from_default_env(self, module_org): :CaseImportance: Low """ new_name = gen_string('alpha') - custom_yum_product = cli_factory.make_product({'organization-id': module_org.id}) - custom_yum_repo = cli_factory.make_repository( + custom_yum_product = module_target_sat.cli_factory.make_product( + {'organization-id': module_org.id} + ) + custom_yum_repo = module_target_sat.cli_factory.make_repository( { 'content-type': 'yum', 'product-id': custom_yum_product['id'], 'url': settings.repos.yum_1.url, } ) - Repository.synchronize({'id': custom_yum_repo['id']}) - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.add_repository( + module_target_sat.cli.Repository.synchronize({'id': custom_yum_repo['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.add_repository( { 'id': content_view['id'], 'organization-id': module_org.id, 'repository-id': custom_yum_repo['id'], } ) - ContentView.publish({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) # ensure that the published content version is in Library environment - content_view_versions = ContentView.info({'id': content_view['id']})['versions'] + content_view_versions = module_target_sat.cli.ContentView.info({'id': content_view['id']})[ + 'versions' + ] assert len(content_view_versions) > 0 content_view_version = content_view_versions[-1] assert constants.ENVIRONMENT in _get_content_view_version_lce_names_set( - content_view['id'], content_view_version['id'] + content_view['id'], content_view_version['id'], sat=module_target_sat ) # rename the content view - ContentView.update({'id': content_view['id'], 'new-name': new_name}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.update({'id': content_view['id'], 'new-name': new_name}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert new_name == content_view['name'] # remove content view version from Library lifecycle environment - ContentView.remove_from_environment( + module_target_sat.cli.ContentView.remove_from_environment( { 'id': content_view['id'], 'organization-id': module_org.id, @@ -2718,14 +2955,16 @@ def test_positive_remove_renamed_cv_version_from_default_env(self, module_org): # ensure that the published content version is not in Library # environment assert constants.ENVIRONMENT not in _get_content_view_version_lce_names_set( - content_view['id'], content_view_version['id'] + content_view['id'], content_view_version['id'], sat=module_target_sat ) @pytest.mark.tier2 @pytest.mark.skipif( (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) - def test_positive_remove_promoted_cv_version_from_default_env(self, module_org): + def test_positive_remove_promoted_cv_version_from_default_env( + self, module_org, module_target_sat + ): """Remove promoted content view version from Library environment :id: 6643837a-560a-47de-aa4d-90778914dcfa @@ -2747,34 +2986,42 @@ def test_positive_remove_promoted_cv_version_from_default_env(self, module_org): :CaseImportance: High """ - lce_dev = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) - custom_product = cli_factory.make_product({'organization-id': module_org.id}) - custom_yum_repo = cli_factory.make_repository( + lce_dev = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + custom_product = module_target_sat.cli_factory.make_product( + {'organization-id': module_org.id} + ) + custom_yum_repo = module_target_sat.cli_factory.make_repository( { 'content-type': 'yum', 'product-id': custom_product['id'], 'url': settings.repos.yum_1.url, } ) - Repository.synchronize({'id': custom_yum_repo['id']}) - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.add_repository( + module_target_sat.cli.Repository.synchronize({'id': custom_yum_repo['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.add_repository( { 'id': content_view['id'], 'organization-id': module_org.id, 'repository-id': custom_yum_repo['id'], } ) - ContentView.publish({'id': content_view['id']}) - content_view_versions = ContentView.info({'id': content_view['id']})['versions'] + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view_versions = module_target_sat.cli.ContentView.info({'id': content_view['id']})[ + 'versions' + ] assert len(content_view_versions) > 0 content_view_version = content_view_versions[-1] - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': content_view_version['id'], 'to-lifecycle-environment-id': lce_dev['id']} ) # ensure that the published content version is in Library and DEV # environments - content_view_version_info = ContentView.version_info( + content_view_version_info = module_target_sat.cli.ContentView.version_info( { 'organization-id': module_org.id, 'content-view-id': content_view['id'], @@ -2786,7 +3033,7 @@ def test_positive_remove_promoted_cv_version_from_default_env(self, module_org): } assert {constants.ENVIRONMENT, lce_dev['name']} == content_view_version_lce_names # remove content view version from Library lifecycle environment - ContentView.remove_from_environment( + module_target_sat.cli.ContentView.remove_from_environment( { 'id': content_view['id'], 'organization-id': module_org.id, @@ -2794,7 +3041,7 @@ def test_positive_remove_promoted_cv_version_from_default_env(self, module_org): } ) # ensure content view version not in Library and only in DEV - content_view_version_info = ContentView.version_info( + content_view_version_info = module_target_sat.cli.ContentView.version_info( { 'organization-id': module_org.id, 'content-view-id': content_view['id'], @@ -2807,7 +3054,9 @@ def test_positive_remove_promoted_cv_version_from_default_env(self, module_org): assert {lce_dev['name']} == content_view_version_lce_names @pytest.mark.tier2 - def test_positive_remove_qe_promoted_cv_version_from_default_env(self, module_org): + def test_positive_remove_qe_promoted_cv_version_from_default_env( + self, module_org, module_target_sat + ): """Remove QE promoted content view version from Library environment :id: e286697f-4113-40a3-b8e8-9ca50647e6d5 @@ -2828,12 +3077,16 @@ def test_positive_remove_qe_promoted_cv_version_from_default_env(self, module_or :CaseImportance: High """ - lce_dev = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) - lce_qe = cli_factory.make_lifecycle_environment( + lce_dev = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + lce_qe = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id, 'prior': lce_dev['name']} ) - docker_product = cli_factory.make_product({'organization-id': module_org.id}) - docker_repository = cli_factory.make_repository( + docker_product = module_target_sat.cli_factory.make_product( + {'organization-id': module_org.id} + ) + docker_repository = module_target_sat.cli_factory.make_repository( { 'content-type': 'docker', 'docker-upstream-name': constants.CONTAINER_UPSTREAM_NAME, @@ -2842,21 +3095,25 @@ def test_positive_remove_qe_promoted_cv_version_from_default_env(self, module_or 'url': constants.CONTAINER_REGISTRY_HUB, } ) - Repository.synchronize({'id': docker_repository['id']}) - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) - ContentView.add_repository( + module_target_sat.cli.Repository.synchronize({'id': docker_repository['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.add_repository( { 'id': content_view['id'], 'organization-id': module_org.id, 'repository-id': docker_repository['id'], } ) - ContentView.publish({'id': content_view['id']}) - content_view_versions = ContentView.info({'id': content_view['id']})['versions'] + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view_versions = module_target_sat.cli.ContentView.info({'id': content_view['id']})[ + 'versions' + ] assert len(content_view_versions) > 0 content_view_version = content_view_versions[-1] for lce in [lce_dev, lce_qe]: - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': content_view_version['id'], 'to-lifecycle-environment-id': lce['id']} ) # ensure that the published content version is in Library, DEV and QE @@ -2865,9 +3122,11 @@ def test_positive_remove_qe_promoted_cv_version_from_default_env(self, module_or constants.ENVIRONMENT, lce_dev['name'], lce_qe['name'], - } == _get_content_view_version_lce_names_set(content_view['id'], content_view_version['id']) + } == _get_content_view_version_lce_names_set( + content_view['id'], content_view_version['id'], sat=module_target_sat + ) # remove content view version from Library lifecycle environment - ContentView.remove_from_environment( + module_target_sat.cli.ContentView.remove_from_environment( { 'id': content_view['id'], 'organization-id': module_org.id, @@ -2877,14 +3136,16 @@ def test_positive_remove_qe_promoted_cv_version_from_default_env(self, module_or # ensure content view version is not in Library and only in DEV and QE # environments assert {lce_dev['name'], lce_qe['name']} == _get_content_view_version_lce_names_set( - content_view['id'], content_view_version['id'] + content_view['id'], content_view_version['id'], sat=module_target_sat ) @pytest.mark.tier2 @pytest.mark.skipif( (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) - def test_positive_remove_prod_promoted_cv_version_from_default_env(self, module_org): + def test_positive_remove_prod_promoted_cv_version_from_default_env( + self, module_org, module_target_sat + ): """Remove PROD promoted content view version from Library environment :id: ffe3d64e-c3d2-4889-9454-ccc6b10f4db7 @@ -2905,24 +3166,30 @@ def test_positive_remove_prod_promoted_cv_version_from_default_env(self, module_ :CaseLevel: Integration """ - lce_dev = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) - lce_qe = cli_factory.make_lifecycle_environment( + lce_dev = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + lce_qe = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id, 'prior': lce_dev['name']} ) - lce_prod = cli_factory.make_lifecycle_environment( + lce_prod = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id, 'prior': lce_qe['name']} ) - custom_yum_product = cli_factory.make_product({'organization-id': module_org.id}) - custom_yum_repo = cli_factory.make_repository( + custom_yum_product = module_target_sat.cli_factory.make_product( + {'organization-id': module_org.id} + ) + custom_yum_repo = module_target_sat.cli_factory.make_repository( { 'content-type': 'yum', 'product-id': custom_yum_product['id'], 'url': settings.repos.yum_1.url, } ) - Repository.synchronize({'id': custom_yum_repo['id']}) - docker_product = cli_factory.make_product({'organization-id': module_org.id}) - docker_repository = cli_factory.make_repository( + module_target_sat.cli.Repository.synchronize({'id': custom_yum_repo['id']}) + docker_product = module_target_sat.cli_factory.make_product( + {'organization-id': module_org.id} + ) + docker_repository = module_target_sat.cli_factory.make_repository( { 'content-type': 'docker', 'docker-upstream-name': constants.CONTAINER_UPSTREAM_NAME, @@ -2931,22 +3198,26 @@ def test_positive_remove_prod_promoted_cv_version_from_default_env(self, module_ 'url': constants.CONTAINER_REGISTRY_HUB, } ) - Repository.synchronize({'id': docker_repository['id']}) - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) + module_target_sat.cli.Repository.synchronize({'id': docker_repository['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) for repo in [custom_yum_repo, docker_repository]: - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( { 'id': content_view['id'], 'organization-id': module_org.id, 'repository-id': repo['id'], } ) - ContentView.publish({'id': content_view['id']}) - content_view_versions = ContentView.info({'id': content_view['id']})['versions'] + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view_versions = module_target_sat.cli.ContentView.info({'id': content_view['id']})[ + 'versions' + ] assert len(content_view_versions) > 0 content_view_version = content_view_versions[-1] for lce in [lce_dev, lce_qe, lce_prod]: - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': content_view_version['id'], 'to-lifecycle-environment-id': lce['id']} ) # ensure that the published content version is in Library, DEV, QE and @@ -2956,9 +3227,11 @@ def test_positive_remove_prod_promoted_cv_version_from_default_env(self, module_ lce_dev['name'], lce_qe['name'], lce_prod['name'], - } == _get_content_view_version_lce_names_set(content_view['id'], content_view_version['id']) + } == _get_content_view_version_lce_names_set( + content_view['id'], content_view_version['id'], sat=module_target_sat + ) # remove content view version from Library lifecycle environment - ContentView.remove_from_environment( + module_target_sat.cli.ContentView.remove_from_environment( { 'id': content_view['id'], 'organization-id': module_org.id, @@ -2971,13 +3244,15 @@ def test_positive_remove_prod_promoted_cv_version_from_default_env(self, module_ lce_dev['name'], lce_qe['name'], lce_prod['name'], - } == _get_content_view_version_lce_names_set(content_view['id'], content_view_version['id']) + } == _get_content_view_version_lce_names_set( + content_view['id'], content_view_version['id'], sat=module_target_sat + ) @pytest.mark.tier2 @pytest.mark.skipif( (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) - def test_positive_remove_cv_version_from_env(self, module_org): + def test_positive_remove_cv_version_from_env(self, module_org, module_target_sat): """Remove promoted content view version from environment :id: 577757ac-b184-4ece-9310-182dd5ceb718 @@ -3001,27 +3276,33 @@ def test_positive_remove_cv_version_from_env(self, module_org): :CaseImportance: High """ - lce_dev = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) - lce_qe = cli_factory.make_lifecycle_environment( + lce_dev = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + lce_qe = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id, 'prior': lce_dev['name']} ) - lce_stage = cli_factory.make_lifecycle_environment( + lce_stage = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id, 'prior': lce_qe['name']} ) - lce_prod = cli_factory.make_lifecycle_environment( + lce_prod = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id, 'prior': lce_stage['name']} ) - custom_yum_product = cli_factory.make_product({'organization-id': module_org.id}) - custom_yum_repo = cli_factory.make_repository( + custom_yum_product = module_target_sat.cli_factory.make_product( + {'organization-id': module_org.id} + ) + custom_yum_repo = module_target_sat.cli_factory.make_repository( { 'content-type': 'yum', 'product-id': custom_yum_product['id'], 'url': settings.repos.yum_1.url, } ) - Repository.synchronize({'id': custom_yum_repo['id']}) - docker_product = cli_factory.make_product({'organization-id': module_org.id}) - docker_repository = cli_factory.make_repository( + module_target_sat.cli.Repository.synchronize({'id': custom_yum_repo['id']}) + docker_product = module_target_sat.cli_factory.make_product( + {'organization-id': module_org.id} + ) + docker_repository = module_target_sat.cli_factory.make_repository( { 'content-type': 'docker', 'docker-upstream-name': constants.CONTAINER_UPSTREAM_NAME, @@ -3030,22 +3311,26 @@ def test_positive_remove_cv_version_from_env(self, module_org): 'url': constants.CONTAINER_REGISTRY_HUB, } ) - Repository.synchronize({'id': docker_repository['id']}) - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) + module_target_sat.cli.Repository.synchronize({'id': docker_repository['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) for repo in [custom_yum_repo, docker_repository]: - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( { 'id': content_view['id'], 'organization-id': module_org.id, 'repository-id': repo['id'], } ) - ContentView.publish({'id': content_view['id']}) - content_view_versions = ContentView.info({'id': content_view['id']})['versions'] + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view_versions = module_target_sat.cli.ContentView.info({'id': content_view['id']})[ + 'versions' + ] assert len(content_view_versions) > 0 content_view_version = content_view_versions[-1] for lce in [lce_dev, lce_qe, lce_stage, lce_prod]: - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': content_view_version['id'], 'to-lifecycle-environment-id': lce['id']} ) # ensure that the published content version is in Library, DEV, QE, @@ -3056,9 +3341,11 @@ def test_positive_remove_cv_version_from_env(self, module_org): lce_qe['name'], lce_stage['name'], lce_prod['name'], - } == _get_content_view_version_lce_names_set(content_view['id'], content_view_version['id']) + } == _get_content_view_version_lce_names_set( + content_view['id'], content_view_version['id'], sat=module_target_sat + ) # remove content view version from PROD lifecycle environment - ContentView.remove_from_environment( + module_target_sat.cli.ContentView.remove_from_environment( { 'id': content_view['id'], 'organization-id': module_org.id, @@ -3072,9 +3359,11 @@ def test_positive_remove_cv_version_from_env(self, module_org): lce_dev['name'], lce_qe['name'], lce_stage['name'], - } == _get_content_view_version_lce_names_set(content_view['id'], content_view_version['id']) + } == _get_content_view_version_lce_names_set( + content_view['id'], content_view_version['id'], sat=module_target_sat + ) # promote content view version to PROD environment again - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': content_view_version['id'], 'to-lifecycle-environment-id': lce_prod['id']} ) assert { @@ -3083,13 +3372,15 @@ def test_positive_remove_cv_version_from_env(self, module_org): lce_qe['name'], lce_stage['name'], lce_prod['name'], - } == _get_content_view_version_lce_names_set(content_view['id'], content_view_version['id']) + } == _get_content_view_version_lce_names_set( + content_view['id'], content_view_version['id'], sat=module_target_sat + ) @pytest.mark.tier3 @pytest.mark.skipif( (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) - def test_positive_remove_cv_version_from_multi_env(self, module_org): + def test_positive_remove_cv_version_from_multi_env(self, module_org, module_target_sat): """Remove promoted content view version from multiple environment :id: 997cfd7d-9029-47e2-a41e-84f4370b5ce5 @@ -3109,27 +3400,33 @@ def test_positive_remove_cv_version_from_multi_env(self, module_org): :CaseImportance: High """ - lce_dev = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) - lce_qe = cli_factory.make_lifecycle_environment( + lce_dev = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + lce_qe = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id, 'prior': lce_dev['name']} ) - lce_stage = cli_factory.make_lifecycle_environment( + lce_stage = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id, 'prior': lce_qe['name']} ) - lce_prod = cli_factory.make_lifecycle_environment( + lce_prod = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id, 'prior': lce_stage['name']} ) - custom_yum_product = cli_factory.make_product({'organization-id': module_org.id}) - custom_yum_repo = cli_factory.make_repository( + custom_yum_product = module_target_sat.cli_factory.make_product( + {'organization-id': module_org.id} + ) + custom_yum_repo = module_target_sat.cli_factory.make_repository( { 'content-type': 'yum', 'product-id': custom_yum_product['id'], 'url': settings.repos.yum_1.url, } ) - Repository.synchronize({'id': custom_yum_repo['id']}) - docker_product = cli_factory.make_product({'organization-id': module_org.id}) - docker_repository = cli_factory.make_repository( + module_target_sat.cli.Repository.synchronize({'id': custom_yum_repo['id']}) + docker_product = module_target_sat.cli_factory.make_product( + {'organization-id': module_org.id} + ) + docker_repository = module_target_sat.cli_factory.make_repository( { 'content-type': 'docker', 'docker-upstream-name': constants.CONTAINER_UPSTREAM_NAME, @@ -3138,22 +3435,26 @@ def test_positive_remove_cv_version_from_multi_env(self, module_org): 'url': constants.CONTAINER_REGISTRY_HUB, } ) - Repository.synchronize({'id': docker_repository['id']}) - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) + module_target_sat.cli.Repository.synchronize({'id': docker_repository['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) for repo in [custom_yum_repo, docker_repository]: - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( { 'id': content_view['id'], 'organization-id': module_org.id, 'repository-id': repo['id'], } ) - ContentView.publish({'id': content_view['id']}) - content_view_versions = ContentView.info({'id': content_view['id']})['versions'] + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view_versions = module_target_sat.cli.ContentView.info({'id': content_view['id']})[ + 'versions' + ] assert len(content_view_versions) > 0 content_view_version = content_view_versions[-1] for lce in [lce_dev, lce_qe, lce_stage, lce_prod]: - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': content_view_version['id'], 'to-lifecycle-environment-id': lce['id']} ) # ensure that the published content version is in Library, DEV, QE, @@ -3164,11 +3465,13 @@ def test_positive_remove_cv_version_from_multi_env(self, module_org): lce_qe['name'], lce_stage['name'], lce_prod['name'], - } == _get_content_view_version_lce_names_set(content_view['id'], content_view_version['id']) + } == _get_content_view_version_lce_names_set( + content_view['id'], content_view_version['id'], sat=module_target_sat + ) # remove content view version from QE, STAGE, PROD lifecycle # environments for lce in [lce_qe, lce_stage, lce_prod]: - ContentView.remove_from_environment( + module_target_sat.cli.ContentView.remove_from_environment( { 'id': content_view['id'], 'organization-id': module_org.id, @@ -3178,11 +3481,11 @@ def test_positive_remove_cv_version_from_multi_env(self, module_org): # ensure content view version is not in PROD and only in Library, DEV, # QE and STAGE environments assert {constants.ENVIRONMENT, lce_dev['name']} == _get_content_view_version_lce_names_set( - content_view['id'], content_view_version['id'] + content_view['id'], content_view_version['id'], sat=module_target_sat ) @pytest.mark.tier3 - def test_positive_delete_cv_promoted_to_multi_env(self, module_org): + def test_positive_delete_cv_promoted_to_multi_env(self, module_org, module_target_sat): """Delete published content view with version promoted to multiple environments @@ -3204,27 +3507,33 @@ def test_positive_delete_cv_promoted_to_multi_env(self, module_org): :CaseImportance: High """ - lce_dev = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) - lce_qe = cli_factory.make_lifecycle_environment( + lce_dev = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + lce_qe = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id, 'prior': lce_dev['name']} ) - lce_stage = cli_factory.make_lifecycle_environment( + lce_stage = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id, 'prior': lce_qe['name']} ) - lce_prod = cli_factory.make_lifecycle_environment( + lce_prod = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id, 'prior': lce_stage['name']} ) - custom_yum_product = cli_factory.make_product({'organization-id': module_org.id}) - custom_yum_repo = cli_factory.make_repository( + custom_yum_product = module_target_sat.cli_factory.make_product( + {'organization-id': module_org.id} + ) + custom_yum_repo = module_target_sat.cli_factory.make_repository( { 'content-type': 'yum', 'product-id': custom_yum_product['id'], 'url': settings.repos.yum_1.url, } ) - Repository.synchronize({'id': custom_yum_repo['id']}) - docker_product = cli_factory.make_product({'organization-id': module_org.id}) - docker_repository = cli_factory.make_repository( + module_target_sat.cli.Repository.synchronize({'id': custom_yum_repo['id']}) + docker_product = module_target_sat.cli_factory.make_product( + {'organization-id': module_org.id} + ) + docker_repository = module_target_sat.cli_factory.make_repository( { 'content-type': 'docker', 'docker-upstream-name': constants.CONTAINER_UPSTREAM_NAME, @@ -3233,28 +3542,32 @@ def test_positive_delete_cv_promoted_to_multi_env(self, module_org): 'url': constants.CONTAINER_REGISTRY_HUB, } ) - Repository.synchronize({'id': docker_repository['id']}) - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) + module_target_sat.cli.Repository.synchronize({'id': docker_repository['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) for repo in [custom_yum_repo, docker_repository]: - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( { 'id': content_view['id'], 'organization-id': module_org.id, 'repository-id': repo['id'], } ) - ContentView.publish({'id': content_view['id']}) - content_view_versions = ContentView.info({'id': content_view['id']})['versions'] + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view_versions = module_target_sat.cli.ContentView.info({'id': content_view['id']})[ + 'versions' + ] assert len(content_view_versions) > 0 content_view_version = content_view_versions[-1] for lce in [lce_dev, lce_qe, lce_stage, lce_prod]: - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': content_view_version['id'], 'to-lifecycle-environment-id': lce['id']} ) # ensure that the published content version is in Library, DEV, QE, # STAGE and PROD environments promoted_lce_names_set = _get_content_view_version_lce_names_set( - content_view['id'], content_view_version['id'] + content_view['id'], content_view_version['id'], sat=module_target_sat ) assert { constants.ENVIRONMENT, @@ -3265,7 +3578,7 @@ def test_positive_delete_cv_promoted_to_multi_env(self, module_org): } == promoted_lce_names_set # remove from all promoted lifecycle environments for lce_name in promoted_lce_names_set: - ContentView.remove_from_environment( + module_target_sat.cli.ContentView.remove_from_environment( { 'id': content_view['id'], 'organization-id': module_org.id, @@ -3273,12 +3586,12 @@ def test_positive_delete_cv_promoted_to_multi_env(self, module_org): } ) # ensure content view in content views list - content_views = ContentView.list({'organization-id': module_org.id}) + content_views = module_target_sat.cli.ContentView.list({'organization-id': module_org.id}) assert content_view['name'] in [cv['name'] for cv in content_views] # delete the content view - ContentView.delete({'id': content_view['id']}) + module_target_sat.cli.ContentView.delete({'id': content_view['id']}) # ensure the content view is not in content views list - content_views = ContentView.list({'organization-id': module_org.id}) + content_views = module_target_sat.cli.ContentView.list({'organization-id': module_org.id}) assert content_view['name'] not in [cv['name'] for cv in content_views] @pytest.mark.stubbed @@ -3367,7 +3680,7 @@ def test_positive_delete_cv_multi_env_promoted_with_host_registered(self): (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) def test_positive_remove_cv_version_from_multi_env_capsule_scenario( - self, module_org, capsule_configured + self, module_org, capsule_configured, module_target_sat ): """Remove promoted content view version from multiple environment, with satellite setup to use capsule @@ -3405,41 +3718,47 @@ def test_positive_remove_cv_version_from_multi_env_capsule_scenario( """ # Note: This test case requires complete external capsule # configuration. - dev_env = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) - qe_env = cli_factory.make_lifecycle_environment( + dev_env = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + qe_env = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id, 'prior': dev_env['name']} ) - prod_env = cli_factory.make_lifecycle_environment( + prod_env = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id, 'prior': qe_env['name']} ) - capsule = Capsule().info({'name': capsule_configured.hostname}) + capsule = module_target_sat.cli.Capsule().info({'name': capsule_configured.hostname}) # Add all environments to capsule environments = {constants.ENVIRONMENT, dev_env['name'], qe_env['name'], prod_env['name']} for env_name in environments: - Capsule.content_add_lifecycle_environment( + module_target_sat.cli.Capsule.content_add_lifecycle_environment( { 'id': capsule['id'], 'organization-id': module_org.id, 'environment': env_name, } ) - capsule_environments = Capsule.content_lifecycle_environments( + capsule_environments = module_target_sat.cli.Capsule.content_lifecycle_environments( {'id': capsule['id'], 'organization-id': module_org.id} ) capsule_environments_names = {env['name'] for env in capsule_environments} assert environments == capsule_environments_names # Setup a yum repo - custom_yum_product = cli_factory.make_product({'organization-id': module_org.id}) - custom_yum_repo = cli_factory.make_repository( + custom_yum_product = module_target_sat.cli_factory.make_product( + {'organization-id': module_org.id} + ) + custom_yum_repo = module_target_sat.cli_factory.make_repository( { 'content-type': 'yum', 'product-id': custom_yum_product['id'], 'url': settings.repos.yum_1.url, } ) - Repository.synchronize({'id': custom_yum_repo['id']}) - docker_product = cli_factory.make_product({'organization-id': module_org.id}) - docker_repository = cli_factory.make_repository( + module_target_sat.cli.Repository.synchronize({'id': custom_yum_repo['id']}) + docker_product = module_target_sat.cli_factory.make_product( + {'organization-id': module_org.id} + ) + docker_repository = module_target_sat.cli_factory.make_repository( { 'content-type': 'docker', 'docker-upstream-name': constants.CONTAINER_UPSTREAM_NAME, @@ -3448,10 +3767,12 @@ def test_positive_remove_cv_version_from_multi_env_capsule_scenario( 'url': constants.CONTAINER_REGISTRY_HUB, } ) - Repository.synchronize({'id': docker_repository['id']}) - content_view = cli_factory.make_content_view({'organization-id': module_org.id}) + module_target_sat.cli.Repository.synchronize({'id': docker_repository['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) for repo in [custom_yum_repo, docker_repository]: - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( { 'id': content_view['id'], 'organization-id': module_org.id, @@ -3459,11 +3780,13 @@ def test_positive_remove_cv_version_from_multi_env_capsule_scenario( } ) # Publish the content view - ContentView.publish({'id': content_view['id']}) - content_view_version = ContentView.info({'id': content_view['id']})['versions'][-1] + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view_version = module_target_sat.cli.ContentView.info({'id': content_view['id']})[ + 'versions' + ][-1] # Promote the content view to DEV, QE, PROD for env in [dev_env, qe_env, prod_env]: - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( { 'id': content_view_version['id'], 'organization-id': module_org.id, @@ -3471,8 +3794,10 @@ def test_positive_remove_cv_version_from_multi_env_capsule_scenario( } ) # Synchronize the capsule content - Capsule.content_synchronize({'id': capsule['id'], 'organization-id': module_org.id}) - capsule_content_info = Capsule.content_info( + module_target_sat.cli.Capsule.content_synchronize( + {'id': capsule['id'], 'organization-id': module_org.id} + ) + capsule_content_info = module_target_sat.cli.Capsule.content_info( {'id': capsule['id'], 'organization-id': module_org.id} ) # Ensure that all environments exists in capsule content @@ -3502,7 +3827,7 @@ def test_positive_remove_cv_version_from_multi_env_capsule_scenario( pytest.skip('Power control host workflow is not defined') # Remove the content view version from Library and DEV environments for lce_name in [constants.ENVIRONMENT, dev_env['name']]: - ContentView.remove_from_environment( + module_target_sat.cli.ContentView.remove_from_environment( { 'id': content_view['id'], 'organization-id': module_org.id, @@ -3513,7 +3838,7 @@ def test_positive_remove_cv_version_from_multi_env_capsule_scenario( # DEV and exist only in QE and PROD environments_with_cv = {qe_env['name'], prod_env['name']} assert environments_with_cv == _get_content_view_version_lce_names_set( - content_view['id'], content_view_version['id'] + content_view['id'], content_view_version['id'], sat=module_target_sat ) # Resume the capsule with ensure True to ping the virtual machine try: @@ -3522,7 +3847,7 @@ def test_positive_remove_cv_version_from_multi_env_capsule_scenario( pytest.skip('Power control host workflow is not defined') # Assert that in capsule content the content view version # does not exit in Library and DEV and exist only in QE and PROD - capsule_content_info = Capsule.content_info( + capsule_content_info = module_target_sat.cli.Capsule.content_info( {'id': capsule['id'], 'organization-id': module_org.id} ) capsule_content_info_lces = capsule_content_info['lifecycle-environments'] @@ -3541,7 +3866,7 @@ def test_positive_remove_cv_version_from_multi_env_capsule_scenario( assert content_view['name'] not in capsule_content_info_lce_cvs_names @pytest.mark.tier2 - def test_negative_user_with_no_create_view_cv_permissions(self, module_org): + def test_negative_user_with_no_create_view_cv_permissions(self, module_org, module_target_sat): """Unauthorized users are not able to create/view content views :id: 17617893-27c2-4cb2-a2ed-47378ef90e7a @@ -3554,24 +3879,26 @@ def test_negative_user_with_no_create_view_cv_permissions(self, module_org): :CaseImportance: Critical """ password = gen_alphanumeric() - no_rights_user = cli_factory.make_user({'password': password}) + no_rights_user = module_target_sat.cli_factory.user({'password': password}) no_rights_user['password'] = password - org_id = cli_factory.make_org(cached=True)['id'] + org_id = module_target_sat.cli_factory.make_org(cached=True)['id'] for name in generate_strings_list(exclude_types=['cjk']): # test that user can't create with pytest.raises(CLIReturnCodeError): - ContentView.with_user(no_rights_user['login'], no_rights_user['password']).create( - {'name': name, 'organization-id': org_id} - ) + module_target_sat.cli.ContentView.with_user( + no_rights_user['login'], no_rights_user['password'] + ).create({'name': name, 'organization-id': org_id}) # test that user can't read - con_view = cli_factory.make_content_view({'name': name, 'organization-id': org_id}) + con_view = module_target_sat.cli_factory.make_content_view( + {'name': name, 'organization-id': org_id} + ) with pytest.raises(CLIReturnCodeError): - ContentView.with_user(no_rights_user['login'], no_rights_user['password']).info( - {'id': con_view['id']} - ) + module_target_sat.cli.ContentView.with_user( + no_rights_user['login'], no_rights_user['password'] + ).info({'id': con_view['id']}) @pytest.mark.tier2 - def test_negative_user_with_read_only_cv_permission(self, module_org): + def test_negative_user_with_read_only_cv_permission(self, module_org, module_target_sat): """Read-only user is able to view content view :id: 588f57b5-9855-4c14-80d0-64b617c6b6dc @@ -3588,12 +3915,14 @@ def test_negative_user_with_read_only_cv_permission(self, module_org): :CaseImportance: Critical """ - cv = cli_factory.make_content_view({'organization-id': module_org.id}) - environment = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) + cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + environment = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) password = gen_string('alphanumeric') - user = cli_factory.make_user({'password': password}) - role = cli_factory.make_role() - cli_factory.make_filter( + user = module_target_sat.cli_factory.user({'password': password}) + role = module_target_sat.cli_factory.make_role() + module_target_sat.cli_factory.make_filter( { 'organization-ids': module_org.id, 'permissions': 'view_content_views', @@ -3601,26 +3930,28 @@ def test_negative_user_with_read_only_cv_permission(self, module_org): 'override': 1, } ) - User.add_role({'id': user['id'], 'role-id': role['id']}) - ContentView.with_user(user['login'], password).info({'id': cv['id']}) + module_target_sat.cli.User.add_role({'id': user['id'], 'role-id': role['id']}) + module_target_sat.cli.ContentView.with_user(user['login'], password).info({'id': cv['id']}) # Verify read-only user can't either edit CV with pytest.raises(CLIReturnCodeError): - ContentView.with_user(user['login'], password).update( + module_target_sat.cli.ContentView.with_user(user['login'], password).update( {'id': cv['id'], 'new-name': gen_string('alphanumeric')} ) # or create a new one with pytest.raises(CLIReturnCodeError): - ContentView.with_user(user['login'], password).create( + module_target_sat.cli.ContentView.with_user(user['login'], password).create( {'name': gen_string('alphanumeric'), 'organization-id': module_org.id} ) # or publish with pytest.raises(CLIReturnCodeError): - ContentView.with_user(user['login'], password).publish({'id': cv['id']}) - ContentView.publish({'id': cv['id']}) - cvv = ContentView.info({'id': cv['id']})['versions'][-1] + module_target_sat.cli.ContentView.with_user(user['login'], password).publish( + {'id': cv['id']} + ) + module_target_sat.cli.ContentView.publish({'id': cv['id']}) + cvv = module_target_sat.cli.ContentView.info({'id': cv['id']})['versions'][-1] # or promote with pytest.raises(CLIReturnCodeError): - ContentView.with_user(user['login'], password).version_promote( + module_target_sat.cli.ContentView.with_user(user['login'], password).version_promote( { 'id': cvv['id'], 'organization-id': module_org.id, @@ -3629,22 +3960,22 @@ def test_negative_user_with_read_only_cv_permission(self, module_org): ) # or create a product with pytest.raises(CLIReturnCodeError): - Product.with_user(user['login'], password).create( + module_target_sat.cli.Product.with_user(user['login'], password).create( {'name': gen_string('alpha'), 'organization-id': module_org.id} ) # cannot create activation key with pytest.raises(CLIReturnCodeError): - ActivationKey.with_user(user['login'], password).create( + module_target_sat.cli.ActivationKey.with_user(user['login'], password).create( {'name': gen_string('alpha'), 'organization-id': module_org.id} ) # cannot create host collection with pytest.raises(CLIReturnCodeError): - HostCollection.with_user(user['login'], password).create( + module_target_sat.cli.HostCollection.with_user(user['login'], password).create( {'organization-id': module_org.id} ) @pytest.mark.tier2 - def test_positive_user_with_all_cv_permissions(self, module_org): + def test_positive_user_with_all_cv_permissions(self, module_org, module_target_sat): """A user with all content view permissions is able to create, read, modify, promote, publish content views @@ -3661,57 +3992,63 @@ def test_positive_user_with_all_cv_permissions(self, module_org): :CaseImportance: Critical """ - cv = cli_factory.make_content_view({'organization-id': module_org.id}) - environment = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) + cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + environment = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) password = gen_string('alphanumeric') - user = cli_factory.make_user({'password': password, 'organization-ids': module_org.id}) - role = cli_factory.make_role({'organization-ids': module_org.id}) + user = module_target_sat.cli_factory.user( + {'password': password, 'organization-ids': module_org.id} + ) + role = module_target_sat.cli_factory.make_role({'organization-ids': module_org.id}) # note: the filters inherit role organizations - cli_factory.make_filter( + module_target_sat.cli_factory.make_filter( {'permissions': constants.PERMISSIONS['Katello::ContentView'], 'role-id': role['id']} ) - cli_factory.make_filter( + module_target_sat.cli_factory.make_filter( {'permissions': constants.PERMISSIONS['Katello::KTEnvironment'], 'role-id': role['id']} ) - User.add_role({'id': user['id'], 'role-id': role['id']}) + module_target_sat.cli.User.add_role({'id': user['id'], 'role-id': role['id']}) # Make sure user is not admin and has only expected roles assigned - user = User.info({'id': user['id']}) + user = module_target_sat.cli.User.info({'id': user['id']}) assert user['admin'] == 'no' assert set(user['roles']) == {role['name']} # Verify user can either edit CV - ContentView.with_user(user['login'], password).info({'id': cv['id']}) + module_target_sat.cli.ContentView.with_user(user['login'], password).info({'id': cv['id']}) new_name = gen_string('alphanumeric') - ContentView.with_user(user['login'], password).update( + module_target_sat.cli.ContentView.with_user(user['login'], password).update( {'id': cv['id'], 'new-name': new_name} ) - cv = ContentView.info({'id': cv['id']}) + cv = module_target_sat.cli.ContentView.info({'id': cv['id']}) assert cv['name'] == new_name # or create a new one new_cv_name = gen_string('alphanumeric') - new_cv = ContentView.with_user(user['login'], password).create( + new_cv = module_target_sat.cli.ContentView.with_user(user['login'], password).create( {'name': new_cv_name, 'organization-id': module_org.id} ) assert new_cv['name'] == new_cv_name # or publish - ContentView.with_user(user['login'], password).publish({'id': cv['id']}) - cv = ContentView.info({'id': cv['id']}) + module_target_sat.cli.ContentView.with_user(user['login'], password).publish( + {'id': cv['id']} + ) + cv = module_target_sat.cli.ContentView.info({'id': cv['id']}) assert len(cv['versions']) == 1 # or promote - ContentView.with_user(user['login'], password).version_promote( + module_target_sat.cli.ContentView.with_user(user['login'], password).version_promote( { 'id': cv['versions'][-1]['id'], 'organization-id': module_org.id, 'to-lifecycle-environment-id': environment['id'], } ) - cv = ContentView.info({'id': cv['id']}) + cv = module_target_sat.cli.ContentView.info({'id': cv['id']}) assert environment['id'] in [env['id'] for env in cv['lifecycle-environments']] @pytest.mark.tier3 @pytest.mark.skipif( (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) - def test_positive_inc_update_no_lce(self, module_org, module_product): + def test_positive_inc_update_no_lce(self, module_org, module_product, module_target_sat): """Publish incremental update without providing lifecycle environment for a content view version not promoted to any lifecycle environment @@ -3727,25 +4064,25 @@ def test_positive_inc_update_no_lce(self, module_org, module_product): :CaseLevel: Integration """ - repo = cli_factory.make_repository( + repo = module_target_sat.cli_factory.make_repository( { 'product-id': module_product.id, 'content-type': 'yum', 'url': settings.repos.yum_1.url, } ) - Repository.synchronize({'id': repo['id']}) - content_view = cli_factory.make_content_view( + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + content_view = module_target_sat.cli_factory.make_content_view( {'organization-id': module_org.id, 'repository-ids': repo['id']} ) - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( { 'id': content_view['id'], 'organization-id': module_org.id, 'repository-id': repo['id'], } ) - cvf = cli_factory.make_content_view_filter( + cvf = module_target_sat.cli_factory.make_content_view_filter( { 'content-view-id': content_view['id'], 'inclusion': 'true', @@ -3753,25 +4090,25 @@ def test_positive_inc_update_no_lce(self, module_org, module_product): 'type': 'rpm', }, ) - cli_factory.make_content_view_filter_rule( + module_target_sat.cli_factory.content_view_filter_rule( { 'content-view-filter-id': cvf['filter-id'], 'name': FAKE_2_CUSTOM_PACKAGE_NAME, 'version': 5.21, } ) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert len(content_view['versions']) == 1 cvv = content_view['versions'][0] - result = ContentView.version_incremental_update( + result = module_target_sat.cli.ContentView.version_incremental_update( {'content-view-version-id': cvv['id'], 'errata-ids': settings.repos.yum_1.errata[1]} ) # Inc update output format is pretty weird - list of dicts where each # key's value is actual line from stdout result = [line.strip() for line_dict in result for line in line_dict.values()] assert FAKE_2_CUSTOM_PACKAGE not in [line.strip() for line in result] - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert '1.1' in [cvv_['version'] for cvv_ in content_view['versions']] @@ -3788,11 +4125,11 @@ def make_file_repository_upload_contents( if options is None: options = {'product-id': module_product.id, 'content-type': 'file'} if not options.get('content-type'): - raise cli_factory.CLIFactoryError('Please provide a valid Content Type.') - new_repo = cli_factory.make_repository(options) + raise CLIFactoryError('Please provide a valid Content Type.') + new_repo = satellite.cli_factory.make_repository(options) remote_path = f'/tmp/{constants.RPM_TO_UPLOAD}' satellite.put(DataFile.RPM_TO_UPLOAD, remote_path) - Repository.upload_content( + satellite.cli.Repository.upload_content( { 'name': new_repo['name'], 'organization-id': module_org.id, @@ -3800,13 +4137,15 @@ def make_file_repository_upload_contents( 'product-id': new_repo['product']['id'], } ) - new_repo = Repository.info({'id': new_repo['id']}) + new_repo = satellite.cli.Repository.info({'id': new_repo['id']}) assert int(new_repo['content-counts']['files']) > 0 return new_repo @pytest.mark.skip_if_open('BZ:1610309') @pytest.mark.tier3 - def test_positive_arbitrary_file_repo_addition(self, module_org, module_product, target_sat): + def test_positive_arbitrary_file_repo_addition( + self, module_org, module_product, module_target_sat + ): """Check a File Repository with Arbitrary File can be added to a Content View @@ -3831,11 +4170,11 @@ def test_positive_arbitrary_file_repo_addition(self, module_org, module_product, :BZ: 1610309, 1908465 """ repo = self.make_file_repository_upload_contents( - module_org, module_product, satellite=target_sat + module_org, module_product, satellite=module_target_sat ) - cv = cli_factory.make_content_view({'organization-id': module_org.id}) + cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) # Associate repo to CV with names. - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( { 'name': cv['name'], 'organization-id': module_org.id, @@ -3843,12 +4182,14 @@ def test_positive_arbitrary_file_repo_addition(self, module_org, module_product, 'repository': repo['name'], } ) - cv = ContentView.info({'id': cv['id']}) + cv = module_target_sat.cli.ContentView.info({'id': cv['id']}) assert cv['file-repositories'][0]['name'] == repo['name'] @pytest.mark.skip_if_open('BZ:1908465') @pytest.mark.tier3 - def test_positive_arbitrary_file_repo_removal(self, module_org, module_product, target_sat): + def test_positive_arbitrary_file_repo_removal( + self, module_org, module_product, module_target_sat + ): """Check a File Repository with Arbitrary File can be removed from a Content View @@ -3871,14 +4212,16 @@ def test_positive_arbitrary_file_repo_removal(self, module_org, module_product, :BZ: 1908465 """ - cv = cli_factory.make_content_view({'organization-id': module_org.id}) + cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) repo = self.make_file_repository_upload_contents( - module_org, module_product, satellite=target_sat + module_org, module_product, satellite=module_target_sat ) - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( {'id': cv['id'], 'repository-id': repo['id'], 'organization-id': module_org.id} ) - ContentView.remove_repository({'id': cv['id'], 'repository-id': repo['id']}) + module_target_sat.cli.ContentView.remove_repository( + {'id': cv['id'], 'repository-id': repo['id']} + ) assert cv['file-repositories'][0]['id'] != repo['id'] @pytest.mark.stubbed @@ -3909,7 +4252,9 @@ def test_positive_arbitrary_file_sync_over_capsule(self): """ @pytest.mark.tier3 - def test_positive_arbitrary_file_repo_promotion(self, module_org, module_product, target_sat): + def test_positive_arbitrary_file_repo_promotion( + self, module_org, module_product, module_target_sat + ): """Check arbitrary files availability for Content view version after content-view promotion. @@ -3935,20 +4280,24 @@ def test_positive_arbitrary_file_repo_promotion(self, module_org, module_product :CaseImportance: High """ - cv = cli_factory.make_content_view({'organization-id': module_org.id}) + cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) repo = self.make_file_repository_upload_contents( - module_product, module_product, satellite=target_sat + module_product, module_product, satellite=module_target_sat ) - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( {'id': cv['id'], 'repository-id': repo['id'], 'organization-id': module_org.id} ) - env = cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) - ContentView.publish({'id': cv['id']}) - content_view_info = ContentView.version_info({'content-view-id': cv['id'], 'version': 1}) - ContentView.version_promote( + env = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.publish({'id': cv['id']}) + content_view_info = module_target_sat.cli.ContentView.version_info( + {'content-view-id': cv['id'], 'version': 1} + ) + module_target_sat.cli.ContentView.version_promote( {'id': content_view_info['id'], 'to-lifecycle-environment-id': env['id']} ) - expected_repo = ContentView.version_info( + expected_repo = module_target_sat.cli.ContentView.version_info( { 'content-view-id': cv['id'], 'lifecycle-environment': env['name'], @@ -3980,7 +4329,7 @@ def test_positive_katello_repo_rpms_max_int(self, target_sat): assert 'id|bigint' in result.stdout.splitlines()[3].replace(' ', '') @pytest.mark.tier3 - def test_positive_inc_update_should_not_fail(self, module_org): + def test_positive_inc_update_should_not_fail(self, module_org, module_target_sat): """Incremental update after removing a package should not give a 400 error code :BZ: 2041497 @@ -3991,41 +4340,43 @@ def test_positive_inc_update_should_not_fail(self, module_org): :expectedresults: Incremental update is successful """ - custom_yum_product = cli_factory.make_product({'organization-id': module_org.id}) - custom_yum_repo = cli_factory.make_repository( + custom_yum_product = module_target_sat.cli_factory.make_product( + {'organization-id': module_org.id} + ) + custom_yum_repo = module_target_sat.cli_factory.make_repository( { 'content-type': 'yum', 'product-id': custom_yum_product['id'], 'url': settings.repos.yum_1.url, } ) - Repository.synchronize({'id': custom_yum_repo['id']}) - repo = Repository.info({'id': custom_yum_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': custom_yum_repo['id']}) + repo = module_target_sat.cli.Repository.info({'id': custom_yum_repo['id']}) assert repo['content-counts']['packages'] == '32' # grab and remove the 'bear' package - package = Package.list({'repository-id': repo['id']})[0] - Repository.remove_content({'id': repo['id'], 'ids': [package['id']]}) - repo = Repository.info({'id': repo['id']}) + package = module_target_sat.cli.Package.list({'repository-id': repo['id']})[0] + module_target_sat.cli.Repository.remove_content({'id': repo['id'], 'ids': [package['id']]}) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert repo['content-counts']['packages'] == '31' - content_view = cli_factory.make_content_view( + content_view = module_target_sat.cli_factory.make_content_view( {'organization-id': module_org.id, 'repository-ids': repo['id']} ) - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( { 'id': content_view['id'], 'organization-id': module_org.id, 'repository-id': repo['id'], } ) - ContentView.publish({'id': content_view['id']}) - Repository.synchronize({'id': custom_yum_repo['id']}) - repo = Repository.info({'id': custom_yum_repo['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + module_target_sat.cli.Repository.synchronize({'id': custom_yum_repo['id']}) + repo = module_target_sat.cli.Repository.info({'id': custom_yum_repo['id']}) assert repo['content-counts']['packages'] == '32' - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) cvv = content_view['versions'][0] - result = ContentView.version_incremental_update( + result = module_target_sat.cli.ContentView.version_incremental_update( {'content-view-version-id': cvv['id'], 'errata-ids': settings.repos.yum_1.errata[0]} ) assert result[2] - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert '1.1' in [cvv_['version'] for cvv_ in content_view['versions']] diff --git a/tests/foreman/cli/test_contentviewfilter.py b/tests/foreman/cli/test_contentviewfilter.py index c19091df054..dac2b0d0a6a 100644 --- a/tests/foreman/cli/test_contentviewfilter.py +++ b/tests/foreman/cli/test_contentviewfilter.py @@ -21,12 +21,9 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.contentview import ContentView from robottelo.cli.defaults import Defaults -from robottelo.cli.factory import make_content_view, make_repository -from robottelo.cli.repository import Repository from robottelo.constants import CONTAINER_REGISTRY_HUB +from robottelo.exceptions import CLIReturnCodeError from robottelo.utils.datafactory import ( invalid_values_list, parametrized, @@ -35,15 +32,17 @@ @pytest.fixture(scope='module') -def sync_repo(module_org, module_product): - repo = make_repository({'organization-id': module_org.id, 'product-id': module_product.id}) - Repository.synchronize({'id': repo['id']}) +def sync_repo(module_org, module_product, module_target_sat): + repo = module_target_sat.cli_factory.make_repository( + {'organization-id': module_org.id, 'product-id': module_product.id} + ) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) return repo @pytest.fixture -def content_view(module_org, sync_repo): - return make_content_view( +def content_view(module_org, sync_repo, module_target_sat): + return module_target_sat.cli_factory.make_content_view( {'organization-id': module_org.id, 'repository-ids': [sync_repo['id']]} ) @@ -55,7 +54,7 @@ class TestContentViewFilter: @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.parametrize('filter_content_type', ['rpm', 'package_group', 'erratum', 'modulemd']) def test_positive_create_with_name_by_cv_id( - self, name, filter_content_type, module_org, content_view + self, name, filter_content_type, module_org, content_view, module_target_sat ): """Create new content view filter and assign it to existing content view by id. Use different value types as a name and random filter @@ -70,7 +69,7 @@ def test_positive_create_with_name_by_cv_id( :CaseImportance: Critical """ - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': name, @@ -78,14 +77,16 @@ def test_positive_create_with_name_by_cv_id( 'type': filter_content_type, }, ) - cvf = ContentView.filter.info({'content-view-id': content_view['id'], 'name': name}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': name} + ) assert cvf['name'] == name assert cvf['type'] == filter_content_type @pytest.mark.tier1 @pytest.mark.parametrize('filter_content_type', ['rpm', 'package_group', 'erratum', 'modulemd']) def test_positive_create_with_content_type_by_cv_id( - self, filter_content_type, module_org, content_view + self, filter_content_type, module_org, content_view, module_target_sat ): """Create new content view filter and assign it to existing content view by id. Use different content types as a parameter @@ -100,7 +101,7 @@ def test_positive_create_with_content_type_by_cv_id( :CaseImportance: Critical """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': cvf_name, @@ -108,12 +109,16 @@ def test_positive_create_with_content_type_by_cv_id( 'type': filter_content_type, }, ) - cvf = ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) assert cvf['type'] == filter_content_type @pytest.mark.tier1 @pytest.mark.parametrize('inclusion', ['true', 'false']) - def test_positive_create_with_inclusion_by_cv_id(self, inclusion, module_org, content_view): + def test_positive_create_with_inclusion_by_cv_id( + self, inclusion, module_org, content_view, module_target_sat + ): """Create new content view filter and assign it to existing content view by id. Use different inclusions as a parameter @@ -127,7 +132,7 @@ def test_positive_create_with_inclusion_by_cv_id(self, inclusion, module_org, co :CaseImportance: Critical """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'inclusion': inclusion, @@ -136,11 +141,15 @@ def test_positive_create_with_inclusion_by_cv_id(self, inclusion, module_org, co 'type': 'rpm', }, ) - cvf = ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) assert cvf['inclusion'] == inclusion @pytest.mark.tier1 - def test_positive_create_with_description_by_cv_id(self, module_org, content_view): + def test_positive_create_with_description_by_cv_id( + self, module_org, content_view, module_target_sat + ): """Create new content view filter with description and assign it to existing content view. @@ -153,7 +162,7 @@ def test_positive_create_with_description_by_cv_id(self, module_org, content_vie """ description = gen_string('utf8') cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'description': description, @@ -162,13 +171,15 @@ def test_positive_create_with_description_by_cv_id(self, module_org, content_vie 'type': 'package_group', }, ) - cvf = ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) assert cvf['description'] == description @pytest.mark.run_in_one_thread @pytest.mark.tier1 def test_positive_create_with_default_taxonomies( - self, module_org, module_location, content_view + self, module_org, module_location, content_view, module_target_sat ): """Create new content view filter and assign it to existing content view by name. Use default organization and location to find necessary @@ -187,7 +198,7 @@ def test_positive_create_with_default_taxonomies( Defaults.add({'param-name': 'organization_id', 'param-value': module_org.id}) Defaults.add({'param-name': 'location_id', 'param-value': module_location.id}) try: - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view': content_view['name'], 'name': name, @@ -195,14 +206,16 @@ def test_positive_create_with_default_taxonomies( 'inclusion': 'true', }, ) - cvf = ContentView.filter.info({'content-view': content_view['name'], 'name': name}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view': content_view['name'], 'name': name} + ) assert cvf['name'] == name finally: Defaults.delete({'param-name': 'organization_id'}) Defaults.delete({'param-name': 'location_id'}) @pytest.mark.tier1 - def test_positive_list_by_name_and_org(self, module_org, content_view): + def test_positive_list_by_name_and_org(self, module_org, content_view, module_target_sat): """Create new content view filter and try to list it by its name and organization it belongs @@ -217,7 +230,7 @@ def test_positive_list_by_name_and_org(self, module_org, content_view): :CaseImportance: Critical """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': cvf_name, @@ -225,14 +238,14 @@ def test_positive_list_by_name_and_org(self, module_org, content_view): 'type': 'package_group', }, ) - cv_filters = ContentView.filter.list( + cv_filters = module_target_sat.cli.ContentView.filter.list( {'content-view': content_view['name'], 'organization': module_org.name} ) assert len(cv_filters) >= 1 assert cvf_name in [cvf['name'] for cvf in cv_filters] @pytest.mark.tier1 - def test_positive_create_by_cv_name(self, module_org, content_view): + def test_positive_create_by_cv_name(self, module_org, content_view, module_target_sat): """Create new content view filter and assign it to existing content view by name. Use organization id for reference @@ -245,7 +258,7 @@ def test_positive_create_by_cv_name(self, module_org, content_view): :CaseImportance: Critical """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view': content_view['name'], 'inclusion': 'true', @@ -254,10 +267,12 @@ def test_positive_create_by_cv_name(self, module_org, content_view): 'type': 'package_group', }, ) - ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) @pytest.mark.tier1 - def test_positive_create_by_org_name(self, module_org, content_view): + def test_positive_create_by_org_name(self, module_org, content_view, module_target_sat): """Create new content view filter and assign it to existing content view by name. Use organization name for reference @@ -268,7 +283,7 @@ def test_positive_create_by_org_name(self, module_org, content_view): :CaseImportance: Critical """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view': content_view['name'], 'inclusion': 'false', @@ -277,10 +292,12 @@ def test_positive_create_by_org_name(self, module_org, content_view): 'type': 'erratum', }, ) - ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) @pytest.mark.tier1 - def test_positive_create_by_org_label(self, module_org, content_view): + def test_positive_create_by_org_label(self, module_org, content_view, module_target_sat): """Create new content view filter and assign it to existing content view by name. Use organization label for reference @@ -291,7 +308,7 @@ def test_positive_create_by_org_label(self, module_org, content_view): :CaseImportance: Critical """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view': content_view['name'], 'inclusion': 'true', @@ -300,10 +317,14 @@ def test_positive_create_by_org_label(self, module_org, content_view): 'type': 'erratum', }, ) - ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) @pytest.mark.tier1 - def test_positive_create_with_repo_by_id(self, module_org, sync_repo, content_view): + def test_positive_create_with_repo_by_id( + self, module_org, sync_repo, content_view, module_target_sat + ): """Create new content view filter and assign it to existing content view that has repository assigned to it. Use that repository id for proper filter assignment. @@ -316,7 +337,7 @@ def test_positive_create_with_repo_by_id(self, module_org, sync_repo, content_vi :CaseImportance: Critical """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'inclusion': 'true', @@ -326,14 +347,16 @@ def test_positive_create_with_repo_by_id(self, module_org, sync_repo, content_vi 'type': 'rpm', }, ) - cvf = ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) # Check that only one, specified above, repository is displayed assert len(cvf['repositories']) == 1 assert cvf['repositories'][0]['name'] == sync_repo['name'] @pytest.mark.tier1 def test_positive_create_with_repo_by_name( - self, module_org, module_product, sync_repo, content_view + self, module_org, module_product, sync_repo, content_view, module_target_sat ): """Create new content view filter and assign it to existing content view that has repository assigned to it. Use that repository name for @@ -349,7 +372,7 @@ def test_positive_create_with_repo_by_name( :CaseImportance: Critical """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'inclusion': 'false', @@ -360,13 +383,15 @@ def test_positive_create_with_repo_by_name( 'type': 'rpm', }, ) - cvf = ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) # Check that only one, specified above, repository is displayed assert len(cvf['repositories']) == 1 assert cvf['repositories'][0]['name'] == sync_repo['name'] @pytest.mark.tier1 - def test_positive_create_with_original_pkgs(self, sync_repo, content_view): + def test_positive_create_with_original_pkgs(self, sync_repo, content_view, module_target_sat): """Create new content view filter and assign it to existing content view that has repository assigned to it. Enable 'original packages' option for that filter @@ -379,7 +404,7 @@ def test_positive_create_with_original_pkgs(self, sync_repo, content_view): :CaseImportance: Critical """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'inclusion': 'true', @@ -389,12 +414,14 @@ def test_positive_create_with_original_pkgs(self, sync_repo, content_view): 'type': 'rpm', }, ) - cvf = ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) assert cvf['repositories'][0]['name'] == sync_repo['name'] @pytest.mark.tier2 def test_positive_create_with_repos_yum_and_docker( - self, module_org, module_product, sync_repo, content_view + self, module_org, module_product, sync_repo, content_view, module_target_sat ): """Create new docker repository and add to content view that has yum repo already assigned to it. Create new content view filter and assign @@ -406,7 +433,7 @@ def test_positive_create_with_repos_yum_and_docker( :expectedresults: Content view filter created successfully and has both repositories affected (yum and docker) """ - docker_repository = make_repository( + docker_repository = module_target_sat.cli_factory.make_repository( { 'content-type': 'docker', 'docker-upstream-name': 'busybox', @@ -416,12 +443,12 @@ def test_positive_create_with_repos_yum_and_docker( }, ) - ContentView.add_repository( + module_target_sat.cli.ContentView.add_repository( {'id': content_view['id'], 'repository-id': docker_repository['id']} ) repos = [sync_repo['id'], docker_repository['id']] cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'inclusion': 'true', @@ -431,14 +458,18 @@ def test_positive_create_with_repos_yum_and_docker( 'type': 'rpm', }, ) - cvf = ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) assert len(cvf['repositories']) == 2 for repo in cvf['repositories']: assert repo['id'] in repos @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) - def test_negative_create_with_invalid_name(self, name, module_org, content_view): + def test_negative_create_with_invalid_name( + self, name, module_org, content_view, module_target_sat + ): """Try to create content view filter using invalid names only :id: f3497a23-6e34-4fee-9964-f95762fc737c @@ -450,7 +481,7 @@ def test_negative_create_with_invalid_name(self, name, module_org, content_view) :CaseImportance: Low """ with pytest.raises(CLIReturnCodeError): - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': name, @@ -460,7 +491,7 @@ def test_negative_create_with_invalid_name(self, name, module_org, content_view) ) @pytest.mark.tier1 - def test_negative_create_with_same_name(self, module_org, content_view): + def test_negative_create_with_same_name(self, module_org, content_view, module_target_sat): """Try to create content view filter using same name twice :id: 7e7444f4-e2b5-406d-a210-49b4008c88d9 @@ -470,7 +501,7 @@ def test_negative_create_with_same_name(self, module_org, content_view): :CaseImportance: Low """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': cvf_name, @@ -479,7 +510,7 @@ def test_negative_create_with_same_name(self, module_org, content_view): }, ) with pytest.raises(CLIReturnCodeError): - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': cvf_name, @@ -489,7 +520,7 @@ def test_negative_create_with_same_name(self, module_org, content_view): ) @pytest.mark.tier1 - def test_negative_create_without_type(self, module_org, content_view): + def test_negative_create_without_type(self, module_org, content_view, module_target_sat): """Try to create content view filter without providing required parameter 'type' @@ -500,7 +531,7 @@ def test_negative_create_without_type(self, module_org, content_view): :CaseImportance: Critical """ with pytest.raises(CLIReturnCodeError): - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': gen_string('utf8'), @@ -509,7 +540,7 @@ def test_negative_create_without_type(self, module_org, content_view): ) @pytest.mark.tier1 - def test_negative_create_without_cv(self): + def test_negative_create_without_cv(self, module_target_sat): """Try to create content view filter without providing content view information which should be used as basis for filter @@ -520,10 +551,14 @@ def test_negative_create_without_cv(self): :CaseImportance: Critical """ with pytest.raises(CLIReturnCodeError): - ContentView.filter.create({'name': gen_string('utf8'), 'type': 'rpm'}) + module_target_sat.cli.ContentView.filter.create( + {'name': gen_string('utf8'), 'type': 'rpm'} + ) @pytest.mark.tier1 - def test_negative_create_with_invalid_repo_id(self, module_org, content_view): + def test_negative_create_with_invalid_repo_id( + self, module_org, content_view, module_target_sat + ): """Try to create content view filter using incorrect repository :id: 21fdbeca-ad0a-4e29-93dc-f850b5639f4f @@ -533,7 +568,7 @@ def test_negative_create_with_invalid_repo_id(self, module_org, content_view): :CaseImportance: Critical """ with pytest.raises(CLIReturnCodeError): - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': gen_string('utf8'), @@ -545,7 +580,7 @@ def test_negative_create_with_invalid_repo_id(self, module_org, content_view): @pytest.mark.tier2 @pytest.mark.parametrize('new_name', **parametrized(valid_data_list())) - def test_positive_update_name(self, new_name, module_org, content_view): + def test_positive_update_name(self, new_name, module_org, content_view, module_target_sat): """Create new content view filter and assign it to existing content view by id. Try to update that filter using different value types as a name @@ -562,7 +597,7 @@ def test_positive_update_name(self, new_name, module_org, content_view): :CaseImportance: Critical """ cvf_name = gen_string('utf8') - cvf = ContentView.filter.create( + cvf = module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': cvf_name, @@ -570,19 +605,21 @@ def test_positive_update_name(self, new_name, module_org, content_view): 'type': 'rpm', }, ) - ContentView.filter.update( + module_target_sat.cli.ContentView.filter.update( { 'content-view-id': content_view['id'], 'id': cvf['filter-id'], 'new-name': new_name, } ) - cvf = ContentView.filter.info({'content-view-id': content_view['id'], 'name': new_name}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': new_name} + ) assert cvf['name'] == new_name @pytest.mark.tier2 def test_positive_update_repo_with_same_type( - self, module_org, module_product, sync_repo, content_view + self, module_org, module_product, sync_repo, content_view, module_target_sat ): """Create new content view filter and apply it to existing content view that has repository assigned to it. Try to update that filter and @@ -598,7 +635,7 @@ def test_positive_update_repo_with_same_type( :CaseImportance: Critical """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': cvf_name, @@ -606,16 +643,20 @@ def test_positive_update_repo_with_same_type( 'type': 'rpm', }, ) - cvf = ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) assert len(cvf['repositories']) == 1 assert cvf['repositories'][0]['name'] == sync_repo['name'] - new_repo = make_repository( + new_repo = module_target_sat.cli_factory.make_repository( {'organization-id': module_org.id, 'product-id': module_product.id}, ) - ContentView.add_repository({'id': content_view['id'], 'repository-id': new_repo['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': content_view['id'], 'repository-id': new_repo['id']} + ) - ContentView.filter.update( + module_target_sat.cli.ContentView.filter.update( { 'content-view-id': content_view['id'], 'name': cvf_name, @@ -623,7 +664,9 @@ def test_positive_update_repo_with_same_type( } ) - cvf = ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) assert len(cvf['repositories']) == 1 assert cvf['repositories'][0]['name'] != sync_repo['name'] assert cvf['repositories'][0]['name'] == new_repo['name'] @@ -631,7 +674,7 @@ def test_positive_update_repo_with_same_type( @pytest.mark.tier2 @pytest.mark.upgrade def test_positive_update_repo_with_different_type( - self, module_org, module_product, sync_repo, content_view + self, module_org, module_product, sync_repo, content_view, module_target_sat ): """Create new content view filter and apply it to existing content view that has repository assigned to it. Try to update that filter and @@ -646,7 +689,7 @@ def test_positive_update_repo_with_different_type( :CaseLevel: Integration """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': cvf_name, @@ -654,10 +697,12 @@ def test_positive_update_repo_with_different_type( 'type': 'rpm', }, ) - cvf = ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) assert len(cvf['repositories']) == 1 assert cvf['repositories'][0]['name'] == sync_repo['name'] - docker_repo = make_repository( + docker_repo = module_target_sat.cli_factory.make_repository( { 'content-type': 'docker', 'docker-upstream-name': 'busybox', @@ -666,21 +711,25 @@ def test_positive_update_repo_with_different_type( 'url': CONTAINER_REGISTRY_HUB, }, ) - ContentView.add_repository({'id': content_view['id'], 'repository-id': docker_repo['id']}) - ContentView.filter.update( + module_target_sat.cli.ContentView.add_repository( + {'id': content_view['id'], 'repository-id': docker_repo['id']} + ) + module_target_sat.cli.ContentView.filter.update( { 'content-view-id': content_view['id'], 'name': cvf_name, 'repository-ids': docker_repo['id'], } ) - cvf = ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) assert len(cvf['repositories']) == 1 assert cvf['repositories'][0]['name'] != sync_repo['name'] assert cvf['repositories'][0]['name'] == docker_repo['name'] @pytest.mark.tier2 - def test_positive_update_inclusion(self, module_org, content_view): + def test_positive_update_inclusion(self, module_org, content_view, module_target_sat): """Create new content view filter and assign it to existing content view by id. Try to update that filter and assign opposite inclusion value for it @@ -693,7 +742,7 @@ def test_positive_update_inclusion(self, module_org, content_view): :CaseLevel: Integration """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'inclusion': 'true', @@ -702,21 +751,25 @@ def test_positive_update_inclusion(self, module_org, content_view): 'type': 'rpm', }, ) - cvf = ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) assert cvf['inclusion'] == 'true' - ContentView.filter.update( + module_target_sat.cli.ContentView.filter.update( { 'content-view-id': content_view['id'], 'name': cvf_name, 'inclusion': 'false', } ) - cvf = ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) assert cvf['inclusion'] == 'false' @pytest.mark.tier1 @pytest.mark.parametrize('new_name', **parametrized(invalid_values_list())) - def test_negative_update_with_name(self, new_name, content_view): + def test_negative_update_with_name(self, new_name, content_view, module_target_sat): """Try to update content view filter using invalid names only :id: 6c40e452-f786-4e28-9f03-b1935b55b33a @@ -730,11 +783,11 @@ def test_negative_update_with_name(self, new_name, content_view): :CaseImportance: Critical """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( {'content-view-id': content_view['id'], 'name': cvf_name, 'type': 'rpm'} ) with pytest.raises(CLIReturnCodeError): - ContentView.filter.update( + module_target_sat.cli.ContentView.filter.update( { 'content-view-id': content_view['id'], 'name': cvf_name, @@ -742,10 +795,12 @@ def test_negative_update_with_name(self, new_name, content_view): } ) with pytest.raises(CLIReturnCodeError): - ContentView.filter.info({'content-view-id': content_view['id'], 'name': new_name}) + module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': new_name} + ) @pytest.mark.tier1 - def test_negative_update_with_same_name(self, module_org, content_view): + def test_negative_update_with_same_name(self, module_org, content_view, module_target_sat): """Try to update content view filter using name of already existing entity @@ -756,7 +811,7 @@ def test_negative_update_with_same_name(self, module_org, content_view): :CaseImportance: Critical """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': cvf_name, @@ -765,7 +820,7 @@ def test_negative_update_with_same_name(self, module_org, content_view): }, ) new_name = gen_string('alpha', 100) - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': new_name, @@ -774,7 +829,7 @@ def test_negative_update_with_same_name(self, module_org, content_view): }, ) with pytest.raises(CLIReturnCodeError): - ContentView.filter.update( + module_target_sat.cli.ContentView.filter.update( { 'content-view-id': content_view['id'], 'name': new_name, @@ -783,7 +838,7 @@ def test_negative_update_with_same_name(self, module_org, content_view): ) @pytest.mark.tier1 - def test_negative_update_inclusion(self, module_org, content_view): + def test_negative_update_inclusion(self, module_org, content_view, module_target_sat): """Try to update content view filter and assign incorrect inclusion value for it @@ -794,7 +849,7 @@ def test_negative_update_inclusion(self, module_org, content_view): :CaseImportance: Critical """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'inclusion': 'true', @@ -804,18 +859,22 @@ def test_negative_update_inclusion(self, module_org, content_view): }, ) with pytest.raises(CLIReturnCodeError): - ContentView.filter.update( + module_target_sat.cli.ContentView.filter.update( { 'content-view-id': content_view['id'], 'inclusion': 'wrong_value', 'name': cvf_name, } ) - cvf = ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) assert cvf['inclusion'] == 'true' @pytest.mark.tier1 - def test_negative_update_with_non_existent_repo_id(self, sync_repo, content_view): + def test_negative_update_with_non_existent_repo_id( + self, sync_repo, content_view, module_target_sat + ): """Try to update content view filter using non-existing repository ID :id: 457af8c2-fb32-4164-9e19-98676f4ea063 @@ -825,7 +884,7 @@ def test_negative_update_with_non_existent_repo_id(self, sync_repo, content_view :CaseImportance: Critical """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': cvf_name, @@ -834,7 +893,7 @@ def test_negative_update_with_non_existent_repo_id(self, sync_repo, content_view }, ) with pytest.raises(CLIReturnCodeError): - ContentView.filter.update( + module_target_sat.cli.ContentView.filter.update( { 'content-view-id': content_view['id'], 'name': cvf_name, @@ -844,7 +903,7 @@ def test_negative_update_with_non_existent_repo_id(self, sync_repo, content_view @pytest.mark.tier1 def test_negative_update_with_invalid_repo_id( - self, module_org, module_product, sync_repo, content_view + self, module_org, module_product, sync_repo, content_view, module_target_sat ): """Try to update filter and assign repository which does not belong to filter content view @@ -856,7 +915,7 @@ def test_negative_update_with_invalid_repo_id( :CaseImportance: Critical """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': cvf_name, @@ -864,11 +923,11 @@ def test_negative_update_with_invalid_repo_id( 'type': 'rpm', }, ) - new_repo = make_repository( + new_repo = module_target_sat.cli_factory.make_repository( {'organization-id': module_org.id, 'product-id': module_product.id}, ) with pytest.raises(CLIReturnCodeError): - ContentView.filter.update( + module_target_sat.cli.ContentView.filter.update( { 'content-view-id': content_view['id'], 'name': cvf_name, @@ -878,7 +937,7 @@ def test_negative_update_with_invalid_repo_id( @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(valid_data_list())) - def test_positive_delete_by_name(self, name, module_org, content_view): + def test_positive_delete_by_name(self, name, module_org, content_view, module_target_sat): """Create new content view filter and assign it to existing content view by id. Try to delete that filter using different value types as a name @@ -891,7 +950,7 @@ def test_positive_delete_by_name(self, name, module_org, content_view): :CaseImportance: Critical """ - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': name, @@ -899,14 +958,20 @@ def test_positive_delete_by_name(self, name, module_org, content_view): 'type': 'rpm', }, ) - ContentView.filter.info({'content-view-id': content_view['id'], 'name': name}) - ContentView.filter.delete({'content-view-id': content_view['id'], 'name': name}) + module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': name} + ) + module_target_sat.cli.ContentView.filter.delete( + {'content-view-id': content_view['id'], 'name': name} + ) with pytest.raises(CLIReturnCodeError): - ContentView.filter.info({'content-view-id': content_view['id'], 'name': name}) + module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': name} + ) @pytest.mark.tier1 @pytest.mark.upgrade - def test_positive_delete_by_id(self, module_org, content_view): + def test_positive_delete_by_id(self, module_org, content_view, module_target_sat): """Create new content view filter and assign it to existing content view by id. Try to delete that filter using its id as a parameter @@ -917,7 +982,7 @@ def test_positive_delete_by_id(self, module_org, content_view): :CaseImportance: Critical """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': cvf_name, @@ -925,13 +990,17 @@ def test_positive_delete_by_id(self, module_org, content_view): 'type': 'rpm', }, ) - cvf = ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) - ContentView.filter.delete({'id': cvf['filter-id']}) + cvf = module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) + module_target_sat.cli.ContentView.filter.delete({'id': cvf['filter-id']}) with pytest.raises(CLIReturnCodeError): - ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) @pytest.mark.tier1 - def test_positive_delete_by_org_name(self, module_org, content_view): + def test_positive_delete_by_org_name(self, module_org, content_view, module_target_sat): """Create new content view filter and assign it to existing content view by id. Try to delete that filter using organization and content view names where that filter was applied @@ -943,7 +1012,7 @@ def test_positive_delete_by_org_name(self, module_org, content_view): :CaseImportance: Critical """ cvf_name = gen_string('utf8') - ContentView.filter.create( + module_target_sat.cli.ContentView.filter.create( { 'content-view-id': content_view['id'], 'name': cvf_name, @@ -951,8 +1020,10 @@ def test_positive_delete_by_org_name(self, module_org, content_view): 'type': 'rpm', }, ) - ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) - ContentView.filter.delete( + module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) + module_target_sat.cli.ContentView.filter.delete( { 'content-view': content_view['name'], 'name': cvf_name, @@ -960,10 +1031,12 @@ def test_positive_delete_by_org_name(self, module_org, content_view): } ) with pytest.raises(CLIReturnCodeError): - ContentView.filter.info({'content-view-id': content_view['id'], 'name': cvf_name}) + module_target_sat.cli.ContentView.filter.info( + {'content-view-id': content_view['id'], 'name': cvf_name} + ) @pytest.mark.tier1 - def test_negative_delete_by_name(self, content_view): + def test_negative_delete_by_name(self, content_view, module_target_sat): """Try to delete non-existent filter using generated name :id: 84509061-6652-4594-b68a-4566c04bc289 @@ -973,7 +1046,7 @@ def test_negative_delete_by_name(self, content_view): :CaseImportance: Critical """ with pytest.raises(CLIReturnCodeError): - ContentView.filter.delete( + module_target_sat.cli.ContentView.filter.delete( {'content-view-id': content_view['id'], 'name': gen_string('utf8')} ) diff --git a/tests/foreman/cli/test_discoveryrule.py b/tests/foreman/cli/test_discoveryrule.py index 8657e010b87..4daea9571e4 100644 --- a/tests/foreman/cli/test_discoveryrule.py +++ b/tests/foreman/cli/test_discoveryrule.py @@ -25,8 +25,7 @@ import pytest from requests import HTTPError -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import CLIFactoryError, make_discoveryrule +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.logging import logger from robottelo.utils.datafactory import ( filtered_datapoint, @@ -62,7 +61,7 @@ class TestDiscoveryRule: """Implements Foreman discovery Rules tests in CLI.""" @pytest.fixture - def discoveryrule_factory(self, class_org, class_location, class_hostgroup): + def discoveryrule_factory(self, class_org, class_location, class_hostgroup, target_sat): def _create_discoveryrule(org, loc, hostgroup, options=None): """Makes a new discovery rule and asserts its success""" options = options or {} @@ -89,7 +88,7 @@ def _create_discoveryrule(org, loc, hostgroup, options=None): # create a simple object from the dictionary that the CLI factory provides # This allows for consistent attributized access of all fixture entities in the tests - return Box(make_discoveryrule(options)) + return Box(target_sat.cli_factory.discoveryrule(options)) return partial( _create_discoveryrule, org=class_org, loc=class_location, hostgroup=class_hostgroup @@ -173,7 +172,7 @@ def test_positive_create_and_update_with_org_loc_id( new_org = target_sat.cli_factory.make_org() new_loc = target_sat.cli_factory.make_location() - new_hostgroup = target_sat.cli_factory.make_hostgroup( + new_hostgroup = target_sat.cli_factory.hostgroup( {'organization-ids': new_org.id, 'location-ids': new_loc.id} ) target_sat.cli.DiscoveryRule.update( @@ -213,7 +212,7 @@ def test_positive_create_and_update_with_org_loc_name( new_org = target_sat.cli_factory.make_org() new_loc = target_sat.cli_factory.make_location() - new_hostgroup = target_sat.cli_factory.make_hostgroup( + new_hostgroup = target_sat.cli_factory.hostgroup( {'organization-ids': new_org.id, 'location-ids': new_loc.id} ) @@ -366,7 +365,7 @@ def test_positive_update_discovery_params(self, discoveryrule_factory, class_org new_query = 'model = KVM' new_hostname = gen_string('alpha') new_limit = '10' - new_hostgroup = target_sat.cli_factory.make_hostgroup({'organization-ids': class_org.id}) + new_hostgroup = target_sat.cli_factory.hostgroup({'organization-ids': class_org.id}) target_sat.cli.DiscoveryRule.update( { diff --git a/tests/foreman/cli/test_docker.py b/tests/foreman/cli/test_docker.py index 9e7ca7b5ed4..e745f44af8c 100644 --- a/tests/foreman/cli/test_docker.py +++ b/tests/foreman/cli/test_docker.py @@ -17,20 +17,6 @@ from fauxfactory import gen_string, gen_url import pytest -from robottelo.cli.activationkey import ActivationKey -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.contentview import ContentView -from robottelo.cli.docker import Docker -from robottelo.cli.factory import ( - make_activation_key, - make_content_view, - make_lifecycle_environment, - make_product_wait, - make_repository, -) -from robottelo.cli.lifecycleenvironment import LifecycleEnvironment -from robottelo.cli.product import Product -from robottelo.cli.repository import Repository from robottelo.config import settings from robottelo.constants import ( CONTAINER_REGISTRY_HUB, @@ -38,6 +24,7 @@ CONTAINER_UPSTREAM_NAME, REPO_TYPE, ) +from robottelo.exceptions import CLIReturnCodeError from robottelo.utils.datafactory import ( invalid_docker_upstream_names, parametrized, @@ -46,7 +33,7 @@ ) -def _repo(product_id, name=None, upstream_name=None, url=None): +def _repo(sat, product_id, name=None, upstream_name=None, url=None): """Creates a Docker-based repository. :param product_id: ID of the ``Product``. @@ -58,7 +45,7 @@ def _repo(product_id, name=None, upstream_name=None, url=None): CONTAINER_REGISTRY_HUB constant. :return: A ``Repository`` object. """ - return make_repository( + return sat.cli_factory.make_repository( { 'content-type': REPO_TYPE['docker'], 'docker-upstream-name': upstream_name or CONTAINER_UPSTREAM_NAME, @@ -69,39 +56,41 @@ def _repo(product_id, name=None, upstream_name=None, url=None): ) -def _content_view(repo_id, org_id): +def _content_view(sat, repo_id, org_id): """Create a content view and link it to the given repository.""" - content_view = make_content_view({'composite': False, 'organization-id': org_id}) - ContentView.add_repository({'id': content_view['id'], 'repository-id': repo_id}) - return ContentView.info({'id': content_view['id']}) + content_view = sat.cli_factory.make_content_view( + {'composite': False, 'organization-id': org_id} + ) + sat.cli.ContentView.add_repository({'id': content_view['id'], 'repository-id': repo_id}) + return sat.cli.ContentView.info({'id': content_view['id']}) @pytest.fixture -def repo(module_product): - return _repo(module_product.id) +def repo(module_product, target_sat): + return _repo(target_sat, module_product.id) @pytest.fixture -def content_view(module_org, repo): - return _content_view(repo['id'], module_org.id) +def content_view(module_org, target_sat, repo): + return _content_view(target_sat, repo['id'], module_org.id) @pytest.fixture -def content_view_publish(content_view): - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) - return ContentView.version_info({'id': content_view['versions'][0]['id']}) +def content_view_publish(content_view, target_sat): + target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = target_sat.cli.ContentView.info({'id': content_view['id']}) + return target_sat.cli.ContentView.version_info({'id': content_view['versions'][0]['id']}) @pytest.fixture -def content_view_promote(content_view_publish, module_lce): - ContentView.version_promote( +def content_view_promote(content_view_publish, module_lce, target_sat): + target_sat.cli.ContentView.version_promote( { 'id': content_view_publish['id'], 'to-lifecycle-environment-id': module_lce.id, } ) - return ContentView.version_info({'id': content_view_publish['id']}) + return target_sat.cli.ContentView.version_info({'id': content_view_publish['id']}) class TestDockerManifest: @@ -113,7 +102,7 @@ class TestDockerManifest: """ @pytest.mark.tier2 - def test_positive_read_docker_tags(self, repo): + def test_positive_read_docker_tags(self, repo, module_target_sat): """docker manifest displays tags information for a docker manifest :id: 59b605b5-ac2d-46e3-a85e-a259e78a07a8 @@ -125,18 +114,18 @@ def test_positive_read_docker_tags(self, repo): :BZ: 1658274 """ - Repository.synchronize({'id': repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) # Grab all available manifests related to repository - manifests_list = Docker.manifest.list({'repository-id': repo['id']}) + manifests_list = module_target_sat.cli.Docker.manifest.list({'repository-id': repo['id']}) # Some manifests do not have tags associated with it, ignore those # because we want to check the tag information manifests = [m_iter for m_iter in manifests_list if not m_iter['tags'] == ''] assert manifests - tags_list = Docker.tag.list({'repository-id': repo['id']}) + tags_list = module_target_sat.cli.Docker.tag.list({'repository-id': repo['id']}) # Extract tag names for the repository out of docker tag list repo_tag_names = [tag['tag'] for tag in tags_list] for manifest in manifests: - manifest_info = Docker.manifest.info({'id': manifest['id']}) + manifest_info = module_target_sat.cli.Docker.manifest.info({'id': manifest['id']}) # Check that manifest's tag is listed in tags for the repository for t_iter in manifest_info['tags']: assert t_iter['name'] in repo_tag_names @@ -152,7 +141,7 @@ class TestDockerRepository: @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(valid_docker_repository_names())) - def test_positive_create_with_name(self, module_org, module_product, name): + def test_positive_create_with_name(self, module_product, name, module_target_sat): """Create one Docker-type repository :id: e82a36c8-3265-4c10-bafe-c7e07db3be78 @@ -164,13 +153,15 @@ def test_positive_create_with_name(self, module_org, module_product, name): :CaseImportance: Critical """ - repo = _repo(module_product.id, name) + repo = _repo(module_target_sat, module_product.id, name) assert repo['name'] == name assert repo['upstream-repository-name'] == CONTAINER_UPSTREAM_NAME assert repo['content-type'] == REPO_TYPE['docker'] @pytest.mark.tier2 - def test_positive_create_repos_using_same_product(self, module_org, module_product): + def test_positive_create_repos_using_same_product( + self, module_org, module_product, module_target_sat + ): """Create multiple Docker-type repositories :id: 6dd25cf4-f8b6-4958-976a-c116daf27b44 @@ -182,13 +173,15 @@ def test_positive_create_repos_using_same_product(self, module_org, module_produ """ repo_names = set() for _ in range(randint(2, 5)): - repo = _repo(module_product.id) + repo = _repo(module_target_sat, module_product.id) repo_names.add(repo['name']) - product = Product.info({'id': module_product.id, 'organization-id': module_org.id}) + product = module_target_sat.cli.Product.info( + {'id': module_product.id, 'organization-id': module_org.id} + ) assert repo_names.issubset({repo_['repo-name'] for repo_ in product['content']}) @pytest.mark.tier2 - def test_positive_create_repos_using_multiple_products(self, module_org): + def test_positive_create_repos_using_multiple_products(self, module_org, module_target_sat): """Create multiple Docker-type repositories on multiple products. @@ -201,16 +194,20 @@ def test_positive_create_repos_using_multiple_products(self, module_org): :CaseLevel: Integration """ for _ in range(randint(2, 5)): - product = make_product_wait({'organization-id': module_org.id}) + product = module_target_sat.cli_factory.make_product_wait( + {'organization-id': module_org.id} + ) repo_names = set() for _ in range(randint(2, 3)): - repo = _repo(product['id']) + repo = _repo(module_target_sat, product['id']) repo_names.add(repo['name']) - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) + product = module_target_sat.cli.Product.info( + {'id': product['id'], 'organization-id': module_org.id} + ) assert repo_names == {repo_['repo-name'] for repo_ in product['content']} @pytest.mark.tier1 - def test_positive_sync(self, repo): + def test_positive_sync(self, repo, module_target_sat): """Create and sync a Docker-type repository :id: bff1d40e-181b-48b2-8141-8c86e0db62a2 @@ -221,13 +218,13 @@ def test_positive_sync(self, repo): :CaseImportance: Critical """ assert int(repo['content-counts']['container-image-manifests']) == 0 - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert int(repo['content-counts']['container-image-manifests']) > 0 @pytest.mark.tier1 @pytest.mark.parametrize('new_name', **parametrized(valid_docker_repository_names())) - def test_positive_update_name(self, repo, new_name): + def test_positive_update_name(self, repo, new_name, module_target_sat): """Create a Docker-type repository and update its name. :id: 8b3a8496-e9bd-44f1-916f-6763a76b9b1b @@ -239,13 +236,15 @@ def test_positive_update_name(self, repo, new_name): :CaseImportance: Critical """ - Repository.update({'id': repo['id'], 'new-name': new_name, 'url': repo['url']}) - repo = Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.update( + {'id': repo['id'], 'new-name': new_name, 'url': repo['url']} + ) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert repo['name'] == new_name @pytest.mark.tier1 @pytest.mark.parametrize('new_upstream_name', **parametrized(valid_docker_upstream_names())) - def test_positive_update_upstream_name(self, repo, new_upstream_name): + def test_positive_update_upstream_name(self, repo, new_upstream_name, module_target_sat): """Create a Docker-type repository and update its upstream name. :id: 1a6985ed-43ec-4ea6-ba27-e3870457ac56 @@ -257,19 +256,19 @@ def test_positive_update_upstream_name(self, repo, new_upstream_name): :CaseImportance: Critical """ - Repository.update( + module_target_sat.cli.Repository.update( { 'docker-upstream-name': new_upstream_name, 'id': repo['id'], 'url': repo['url'], } ) - repo = Repository.info({'id': repo['id']}) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert repo['upstream-repository-name'] == new_upstream_name @pytest.mark.tier1 @pytest.mark.parametrize('new_upstream_name', **parametrized(invalid_docker_upstream_names())) - def test_negative_update_upstream_name(self, repo, new_upstream_name): + def test_negative_update_upstream_name(self, repo, new_upstream_name, module_target_sat): """Attempt to update upstream name for a Docker-type repository. :id: 798651af-28b2-4907-b3a7-7c560bf66c7c @@ -283,7 +282,7 @@ def test_negative_update_upstream_name(self, repo, new_upstream_name): :CaseImportance: Critical """ with pytest.raises(CLIReturnCodeError, match='Validation failed: Docker upstream name'): - Repository.update( + module_target_sat.cli.Repository.update( { 'docker-upstream-name': new_upstream_name, 'id': repo['id'], @@ -293,7 +292,7 @@ def test_negative_update_upstream_name(self, repo, new_upstream_name): @pytest.mark.skip_if_not_set('docker') @pytest.mark.tier1 - def test_positive_create_with_long_upstream_name(self, module_product): + def test_positive_create_with_long_upstream_name(self, module_product, module_target_sat): """Create a docker repository with upstream name longer than 30 characters @@ -308,6 +307,7 @@ def test_positive_create_with_long_upstream_name(self, module_product): :CaseImportance: Critical """ repo = _repo( + module_target_sat, module_product.id, upstream_name=CONTAINER_RH_REGISTRY_UPSTREAM_NAME, url=settings.docker.external_registry_1, @@ -316,7 +316,7 @@ def test_positive_create_with_long_upstream_name(self, module_product): @pytest.mark.skip_if_not_set('docker') @pytest.mark.tier1 - def test_positive_update_with_long_upstream_name(self, repo): + def test_positive_update_with_long_upstream_name(self, repo, module_target_sat): """Create a docker repository and update its upstream name with longer than 30 characters value @@ -328,18 +328,18 @@ def test_positive_update_with_long_upstream_name(self, repo): :CaseImportance: Critical """ - Repository.update( + module_target_sat.cli.Repository.update( { 'docker-upstream-name': CONTAINER_RH_REGISTRY_UPSTREAM_NAME, 'id': repo['id'], 'url': settings.docker.external_registry_1, } ) - repo = Repository.info({'id': repo['id']}) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert repo['upstream-repository-name'] == CONTAINER_RH_REGISTRY_UPSTREAM_NAME @pytest.mark.tier2 - def test_positive_update_url(self, repo): + def test_positive_update_url(self, repo, module_target_sat): """Create a Docker-type repository and update its URL. :id: 73caacd4-7f17-42a7-8d93-3dee8b9341fa @@ -348,12 +348,12 @@ def test_positive_update_url(self, repo): repository and that its URL can be updated. """ new_url = gen_url() - Repository.update({'id': repo['id'], 'url': new_url}) - repo = Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.update({'id': repo['id'], 'url': new_url}) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert repo['url'] == new_url @pytest.mark.tier1 - def test_positive_delete_by_id(self, repo): + def test_positive_delete_by_id(self, repo, module_target_sat): """Create and delete a Docker-type repository :id: ab1e8228-92a8-45dc-a863-7181711f2745 @@ -363,12 +363,12 @@ def test_positive_delete_by_id(self, repo): :CaseImportance: Critical """ - Repository.delete({'id': repo['id']}) + module_target_sat.cli.Repository.delete({'id': repo['id']}) with pytest.raises(CLIReturnCodeError): - Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.info({'id': repo['id']}) @pytest.mark.tier2 - def test_positive_delete_random_repo_by_id(self, module_org): + def test_positive_delete_random_repo_by_id(self, module_org, module_target_sat): """Create Docker-type repositories on multiple products and delete a random repository from a random product. @@ -378,22 +378,23 @@ def test_positive_delete_random_repo_by_id(self, module_org): without altering the other products. """ products = [ - make_product_wait({'organization-id': module_org.id}) for _ in range(randint(2, 5)) + module_target_sat.cli_factory.make_product_wait({'organization-id': module_org.id}) + for _ in range(randint(2, 5)) ] repos = [] for product in products: for _ in range(randint(2, 3)): - repos.append(_repo(product['id'])) + repos.append(_repo(module_target_sat, product['id'])) # Select random repository and delete it repo = choice(repos) repos.remove(repo) - Repository.delete({'id': repo['id']}) + module_target_sat.cli.Repository.delete({'id': repo['id']}) with pytest.raises(CLIReturnCodeError): - Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.info({'id': repo['id']}) # Verify other repositories were not touched product_ids = [product['id'] for product in products] for repo in repos: - result = Repository.info({'id': repo['id']}) + result = module_target_sat.cli.Repository.info({'id': repo['id']}) assert result['product']['id'] in product_ids @@ -408,7 +409,7 @@ class TestDockerContentView: """ @pytest.mark.tier2 - def test_positive_add_docker_repo_by_id(self, module_org, repo): + def test_positive_add_docker_repo_by_id(self, module_org, repo, module_target_sat): """Add one Docker-type repository to a non-composite content view :id: 87d6c7bb-92f8-4a32-8ad2-2a1af896500b @@ -416,13 +417,17 @@ def test_positive_add_docker_repo_by_id(self, module_org, repo): :expectedresults: A repository is created with a Docker repository and the product is added to a non-composite content view """ - content_view = make_content_view({'composite': False, 'organization-id': module_org.id}) - ContentView.add_repository({'id': content_view['id'], 'repository-id': repo['id']}) - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'composite': False, 'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.add_repository( + {'id': content_view['id'], 'repository-id': repo['id']} + ) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert repo['id'] in [repo_['id'] for repo_ in content_view['container-image-repositories']] @pytest.mark.tier2 - def test_positive_add_docker_repos_by_id(self, module_org, module_product): + def test_positive_add_docker_repos_by_id(self, module_org, module_product, module_target_sat): """Add multiple Docker-type repositories to a non-composite CV. :id: 2eb19e28-a633-4c21-9469-75a686c83b34 @@ -431,18 +436,22 @@ def test_positive_add_docker_repos_by_id(self, module_org, module_product): repositories and the product is added to a non-composite content view. """ - repos = [_repo(module_product.id) for _ in range(randint(2, 5))] - content_view = make_content_view({'composite': False, 'organization-id': module_org.id}) + repos = [_repo(module_target_sat, module_product.id) for _ in range(randint(2, 5))] + content_view = module_target_sat.cli_factory.make_content_view( + {'composite': False, 'organization-id': module_org.id} + ) for repo in repos: - ContentView.add_repository({'id': content_view['id'], 'repository-id': repo['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': content_view['id'], 'repository-id': repo['id']} + ) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert {repo['id'] for repo in repos} == { repo['id'] for repo in content_view['container-image-repositories'] } @pytest.mark.tier2 - def test_positive_add_synced_docker_repo_by_id(self, module_org, repo): + def test_positive_add_synced_docker_repo_by_id(self, module_org, repo, module_target_sat): """Create and sync a Docker-type repository :id: 6f51d268-ed23-48ab-9dea-cd3571daa647 @@ -450,17 +459,23 @@ def test_positive_add_synced_docker_repo_by_id(self, module_org, repo): :expectedresults: A repository is created with a Docker repository and it is synchronized. """ - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert int(repo['content-counts']['container-image-manifests']) > 0 - content_view = make_content_view({'composite': False, 'organization-id': module_org.id}) - ContentView.add_repository({'id': content_view['id'], 'repository-id': repo['id']}) - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'composite': False, 'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.add_repository( + {'id': content_view['id'], 'repository-id': repo['id']} + ) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert repo['id'] in [repo_['id'] for repo_ in content_view['container-image-repositories']] @pytest.mark.tier2 - def test_positive_add_docker_repo_by_id_to_ccv(self, module_org, content_view): + def test_positive_add_docker_repo_by_id_to_ccv( + self, module_org, content_view, module_target_sat + ): """Add one Docker-type repository to a composite content view :id: 8e2ef5ba-3cdf-4ef9-a22a-f1701e20a5d5 @@ -471,23 +486,27 @@ def test_positive_add_docker_repo_by_id_to_ccv(self, module_org, content_view): :BZ: 1359665 """ - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert len(content_view['versions']) == 1 - comp_content_view = make_content_view({'composite': True, 'organization-id': module_org.id}) - ContentView.update( + comp_content_view = module_target_sat.cli_factory.make_content_view( + {'composite': True, 'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.update( { 'id': comp_content_view['id'], 'component-ids': content_view['versions'][0]['id'], } ) - comp_content_view = ContentView.info({'id': comp_content_view['id']}) + comp_content_view = module_target_sat.cli.ContentView.info({'id': comp_content_view['id']}) assert content_view['versions'][0]['id'] in [ component['id'] for component in comp_content_view['components'] ] @pytest.mark.tier2 - def test_positive_add_docker_repos_by_id_to_ccv(self, module_org, module_product): + def test_positive_add_docker_repos_by_id_to_ccv( + self, module_org, module_product, module_target_sat + ): """Add multiple Docker-type repositories to a composite content view. :id: b79cbc97-3dba-4059-907d-19316684d569 @@ -500,27 +519,33 @@ def test_positive_add_docker_repos_by_id_to_ccv(self, module_org, module_product """ cv_versions = [] for _ in range(randint(2, 5)): - content_view = make_content_view({'composite': False, 'organization-id': module_org.id}) - repo = _repo(module_product.id) - ContentView.add_repository({'id': content_view['id'], 'repository-id': repo['id']}) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'composite': False, 'organization-id': module_org.id} + ) + repo = _repo(module_target_sat, module_product.id) + module_target_sat.cli.ContentView.add_repository( + {'id': content_view['id'], 'repository-id': repo['id']} + ) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert len(content_view['versions']) == 1 cv_versions.append(content_view['versions'][0]) - comp_content_view = make_content_view({'composite': True, 'organization-id': module_org.id}) - ContentView.update( + comp_content_view = module_target_sat.cli_factory.make_content_view( + {'composite': True, 'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.update( { 'component-ids': [cv_version['id'] for cv_version in cv_versions], 'id': comp_content_view['id'], } ) - comp_content_view = ContentView.info({'id': comp_content_view['id']}) + comp_content_view = module_target_sat.cli.ContentView.info({'id': comp_content_view['id']}) comp_ids = [component['id'] for component in comp_content_view['components']] for cv_version in cv_versions: assert cv_version['id'] in comp_ids @pytest.mark.tier2 - def test_positive_publish_with_docker_repo(self, content_view): + def test_positive_publish_with_docker_repo(self, content_view, module_target_sat): """Add Docker-type repository to content view and publish it once. :id: 28480de3-ffb5-4b8e-8174-fffffeef6af4 @@ -530,12 +555,14 @@ def test_positive_publish_with_docker_repo(self, content_view): published only once. """ assert len(content_view['versions']) == 0 - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert len(content_view['versions']) == 1 @pytest.mark.tier2 - def test_positive_publish_with_docker_repo_composite(self, content_view, module_org): + def test_positive_publish_with_docker_repo_composite( + self, content_view, module_org, module_target_sat + ): """Add Docker-type repository to composite CV and publish it once. :id: 2d75419b-73ed-4f29-ae0d-9af8d9624c87 @@ -549,28 +576,30 @@ def test_positive_publish_with_docker_repo_composite(self, content_view, module_ """ assert len(content_view['versions']) == 0 - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert len(content_view['versions']) == 1 - comp_content_view = make_content_view({'composite': True, 'organization-id': module_org.id}) - ContentView.update( + comp_content_view = module_target_sat.cli_factory.make_content_view( + {'composite': True, 'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.update( { 'component-ids': content_view['versions'][0]['id'], 'id': comp_content_view['id'], } ) - comp_content_view = ContentView.info({'id': comp_content_view['id']}) + comp_content_view = module_target_sat.cli.ContentView.info({'id': comp_content_view['id']}) assert content_view['versions'][0]['id'] in [ component['id'] for component in comp_content_view['components'] ] - ContentView.publish({'id': comp_content_view['id']}) - comp_content_view = ContentView.info({'id': comp_content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': comp_content_view['id']}) + comp_content_view = module_target_sat.cli.ContentView.info({'id': comp_content_view['id']}) assert len(comp_content_view['versions']) == 1 @pytest.mark.tier2 - def test_positive_publish_multiple_with_docker_repo(self, content_view): + def test_positive_publish_multiple_with_docker_repo(self, content_view, module_target_sat): """Add Docker-type repository to content view and publish it multiple times. @@ -584,12 +613,14 @@ def test_positive_publish_multiple_with_docker_repo(self, content_view): publish_amount = randint(2, 5) for _ in range(publish_amount): - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert len(content_view['versions']) == publish_amount @pytest.mark.tier2 - def test_positive_publish_multiple_with_docker_repo_composite(self, module_org, content_view): + def test_positive_publish_multiple_with_docker_repo_composite( + self, module_org, content_view, module_target_sat + ): """Add Docker-type repository to content view and publish it multiple times. @@ -604,30 +635,34 @@ def test_positive_publish_multiple_with_docker_repo_composite(self, module_org, """ assert len(content_view['versions']) == 0 - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert len(content_view['versions']) == 1 - comp_content_view = make_content_view({'composite': True, 'organization-id': module_org.id}) - ContentView.update( + comp_content_view = module_target_sat.cli_factory.make_content_view( + {'composite': True, 'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.update( { 'component-ids': content_view['versions'][0]['id'], 'id': comp_content_view['id'], } ) - comp_content_view = ContentView.info({'id': comp_content_view['id']}) + comp_content_view = module_target_sat.cli.ContentView.info({'id': comp_content_view['id']}) assert content_view['versions'][0]['id'] in [ component['id'] for component in comp_content_view['components'] ] publish_amount = randint(2, 5) for _ in range(publish_amount): - ContentView.publish({'id': comp_content_view['id']}) - comp_content_view = ContentView.info({'id': comp_content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': comp_content_view['id']}) + comp_content_view = module_target_sat.cli.ContentView.info({'id': comp_content_view['id']}) assert len(comp_content_view['versions']) == publish_amount @pytest.mark.tier2 - def test_positive_promote_with_docker_repo(self, module_org, module_lce, content_view): + def test_positive_promote_with_docker_repo( + self, module_org, module_lce, content_view, module_target_sat + ): """Add Docker-type repository to content view and publish it. Then promote it to the next available lifecycle-environment. @@ -636,20 +671,28 @@ def test_positive_promote_with_docker_repo(self, module_org, module_lce, content :expectedresults: Docker-type repository is promoted to content view found in the specific lifecycle-environment. """ - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert len(content_view['versions']) == 1 - cvv = ContentView.version_info({'id': content_view['versions'][0]['id']}) + cvv = module_target_sat.cli.ContentView.version_info( + {'id': content_view['versions'][0]['id']} + ) assert len(cvv['lifecycle-environments']) == 1 - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': module_lce.id}) - cvv = ContentView.version_info({'id': content_view['versions'][0]['id']}) + module_target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': module_lce.id} + ) + cvv = module_target_sat.cli.ContentView.version_info( + {'id': content_view['versions'][0]['id']} + ) assert len(cvv['lifecycle-environments']) == 2 @pytest.mark.tier2 @pytest.mark.upgrade - def test_positive_promote_multiple_with_docker_repo(self, module_org, content_view): + def test_positive_promote_multiple_with_docker_repo( + self, module_org, content_view, module_target_sat + ): """Add Docker-type repository to content view and publish it. Then promote it to multiple available lifecycle-environments. @@ -658,26 +701,32 @@ def test_positive_promote_multiple_with_docker_repo(self, module_org, content_vi :expectedresults: Docker-type repository is promoted to content view found in the specific lifecycle-environments. """ - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert len(content_view['versions']) == 1 - cvv = ContentView.version_info({'id': content_view['versions'][0]['id']}) + cvv = module_target_sat.cli.ContentView.version_info( + {'id': content_view['versions'][0]['id']} + ) assert len(cvv['lifecycle-environments']) == 1 lces = [ - make_lifecycle_environment({'organization-id': module_org.id}) + module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) for _ in range(1, randint(3, 6)) ] for expected_lces, lce in enumerate(lces, start=2): - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': lce['id']}) - cvv = ContentView.version_info({'id': cvv['id']}) + module_target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': lce['id']} + ) + cvv = module_target_sat.cli.ContentView.version_info({'id': cvv['id']}) assert len(cvv['lifecycle-environments']) == expected_lces @pytest.mark.tier2 def test_positive_promote_with_docker_repo_composite( - self, module_org, module_lce, content_view + self, module_org, module_lce, content_view, module_target_sat ): """Add Docker-type repository to composite content view and publish it. Then promote it to the next available lifecycle-environment. @@ -689,39 +738,47 @@ def test_positive_promote_with_docker_repo_composite( :BZ: 1359665 """ - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert len(content_view['versions']) == 1 - comp_content_view = make_content_view({'composite': True, 'organization-id': module_org.id}) - ContentView.update( + comp_content_view = module_target_sat.cli_factory.make_content_view( + {'composite': True, 'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.update( { 'component-ids': content_view['versions'][0]['id'], 'id': comp_content_view['id'], } ) - comp_content_view = ContentView.info({'id': comp_content_view['id']}) + comp_content_view = module_target_sat.cli.ContentView.info({'id': comp_content_view['id']}) assert content_view['versions'][0]['id'] in [ component['id'] for component in comp_content_view['components'] ] - ContentView.publish({'id': comp_content_view['id']}) - comp_content_view = ContentView.info({'id': comp_content_view['id']}) - cvv = ContentView.version_info({'id': comp_content_view['versions'][0]['id']}) + module_target_sat.cli.ContentView.publish({'id': comp_content_view['id']}) + comp_content_view = module_target_sat.cli.ContentView.info({'id': comp_content_view['id']}) + cvv = module_target_sat.cli.ContentView.version_info( + {'id': comp_content_view['versions'][0]['id']} + ) assert len(cvv['lifecycle-environments']) == 1 - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( { 'id': comp_content_view['versions'][0]['id'], 'to-lifecycle-environment-id': module_lce.id, } ) - cvv = ContentView.version_info({'id': comp_content_view['versions'][0]['id']}) + cvv = module_target_sat.cli.ContentView.version_info( + {'id': comp_content_view['versions'][0]['id']} + ) assert len(cvv['lifecycle-environments']) == 2 @pytest.mark.tier2 @pytest.mark.upgrade - def test_positive_promote_multiple_with_docker_repo_composite(self, content_view, module_org): + def test_positive_promote_multiple_with_docker_repo_composite( + self, content_view, module_org, module_target_sat + ): """Add Docker-type repository to composite content view and publish it. Then promote it to the multiple available lifecycle-environments. @@ -732,45 +789,51 @@ def test_positive_promote_multiple_with_docker_repo_composite(self, content_view :BZ: 1359665 """ - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert len(content_view['versions']) == 1 - comp_content_view = make_content_view({'composite': True, 'organization-id': module_org.id}) - ContentView.update( + comp_content_view = module_target_sat.cli_factory.make_content_view( + {'composite': True, 'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.update( { 'component-ids': content_view['versions'][0]['id'], 'id': comp_content_view['id'], } ) - comp_content_view = ContentView.info({'id': comp_content_view['id']}) + comp_content_view = module_target_sat.cli.ContentView.info({'id': comp_content_view['id']}) assert content_view['versions'][0]['id'] in [ component['id'] for component in comp_content_view['components'] ] - ContentView.publish({'id': comp_content_view['id']}) - comp_content_view = ContentView.info({'id': comp_content_view['id']}) - cvv = ContentView.version_info({'id': comp_content_view['versions'][0]['id']}) + module_target_sat.cli.ContentView.publish({'id': comp_content_view['id']}) + comp_content_view = module_target_sat.cli.ContentView.info({'id': comp_content_view['id']}) + cvv = module_target_sat.cli.ContentView.version_info( + {'id': comp_content_view['versions'][0]['id']} + ) assert len(cvv['lifecycle-environments']) == 1 lces = [ - make_lifecycle_environment({'organization-id': module_org.id}) + module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) for _ in range(1, randint(3, 6)) ] for expected_lces, lce in enumerate(lces, start=2): - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( { 'id': cvv['id'], 'to-lifecycle-environment-id': lce['id'], } ) - cvv = ContentView.version_info({'id': cvv['id']}) + cvv = module_target_sat.cli.ContentView.version_info({'id': cvv['id']}) assert len(cvv['lifecycle-environments']) == expected_lces @pytest.mark.tier2 @pytest.mark.upgrade - def test_positive_name_pattern_change(self, module_org): + def test_positive_name_pattern_change(self, module_org, module_target_sat): """Promote content view with Docker repository to lifecycle environment. Change registry name pattern for that environment. Verify that repository name on product changed according to new pattern. @@ -780,7 +843,9 @@ def test_positive_name_pattern_change(self, module_org): :expectedresults: Container repository name is changed according to new pattern. """ - lce = make_lifecycle_environment({'organization-id': module_org.id}) + lce = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) pattern_prefix = gen_string('alpha', 5) docker_upstream_name = 'hello-world' new_pattern = ( @@ -788,37 +853,49 @@ def test_positive_name_pattern_change(self, module_org): ) repo = _repo( - make_product_wait({'organization-id': module_org.id})['id'], + module_target_sat, + module_target_sat.cli_factory.make_product_wait({'organization-id': module_org.id})[ + 'id' + ], name=gen_string('alpha', 5), upstream_name=docker_upstream_name, ) - Repository.synchronize({'id': repo['id']}) - content_view = make_content_view({'composite': False, 'organization-id': module_org.id}) - ContentView.add_repository({'id': content_view['id'], 'repository-id': repo['id']}) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'composite': False, 'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.add_repository( + {'id': content_view['id'], 'repository-id': repo['id']} + ) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': content_view['versions'][0]['id'], 'to-lifecycle-environment-id': lce['id']} ) - LifecycleEnvironment.update( + module_target_sat.cli.LifecycleEnvironment.update( { 'registry-name-pattern': new_pattern, 'id': lce['id'], 'organization-id': module_org.id, } ) - lce = LifecycleEnvironment.info({'id': lce['id'], 'organization-id': module_org.id}) + lce = module_target_sat.cli.LifecycleEnvironment.info( + {'id': lce['id'], 'organization-id': module_org.id} + ) assert lce['registry-name-pattern'] == new_pattern - repo = Repository.list( + repo = module_target_sat.cli.Repository.list( {'name': repo['name'], 'environment-id': lce['id'], 'organization-id': module_org.id} )[0] expected_name = f'{pattern_prefix}-{content_view["label"]}/{docker_upstream_name}'.lower() - assert Repository.info({'id': repo['id']})['container-repository-name'] == expected_name + assert ( + module_target_sat.cli.Repository.info({'id': repo['id']})['container-repository-name'] + == expected_name + ) @pytest.mark.tier2 - def test_positive_product_name_change_after_promotion(self, module_org): + def test_positive_product_name_change_after_promotion(self, module_org, module_target_sat): """Promote content view with Docker repository to lifecycle environment. Change product name. Verify that repository name on product changed according to new pattern. @@ -833,45 +910,63 @@ def test_positive_product_name_change_after_promotion(self, module_org): docker_upstream_name = 'hello-world' new_pattern = '<%= content_view.label %>/<%= product.name %>' - lce = make_lifecycle_environment({'organization-id': module_org.id}) - prod = make_product_wait({'organization-id': module_org.id, 'name': old_prod_name}) - repo = _repo(prod['id'], name=gen_string('alpha', 5), upstream_name=docker_upstream_name) - Repository.synchronize({'id': repo['id']}) - content_view = make_content_view({'composite': False, 'organization-id': module_org.id}) - ContentView.add_repository({'id': content_view['id'], 'repository-id': repo['id']}) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) - LifecycleEnvironment.update( + lce = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + prod = module_target_sat.cli_factory.make_product_wait( + {'organization-id': module_org.id, 'name': old_prod_name} + ) + repo = _repo( + module_target_sat, + prod['id'], + name=gen_string('alpha', 5), + upstream_name=docker_upstream_name, + ) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'composite': False, 'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.add_repository( + {'id': content_view['id'], 'repository-id': repo['id']} + ) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) + module_target_sat.cli.LifecycleEnvironment.update( { 'registry-name-pattern': new_pattern, 'id': lce['id'], 'organization-id': module_org.id, } ) - lce = LifecycleEnvironment.info({'id': lce['id'], 'organization-id': module_org.id}) + lce = module_target_sat.cli.LifecycleEnvironment.info( + {'id': lce['id'], 'organization-id': module_org.id} + ) assert lce['registry-name-pattern'] == new_pattern - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': content_view['versions'][0]['id'], 'to-lifecycle-environment-id': lce['id']} ) - Product.update({'name': new_prod_name, 'id': prod['id']}) + module_target_sat.cli.Product.update({'name': new_prod_name, 'id': prod['id']}) - repo = Repository.list( + repo = module_target_sat.cli.Repository.list( {'name': repo['name'], 'environment-id': lce['id'], 'organization-id': module_org.id} )[0] expected_name = f'{content_view["label"]}/{old_prod_name}'.lower() - assert Repository.info({'id': repo['id']})['container-repository-name'] == expected_name + assert ( + module_target_sat.cli.Repository.info({'id': repo['id']})['container-repository-name'] + == expected_name + ) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) - ContentView.version_promote( + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.version_promote( { 'id': content_view['versions'][-1]['id'], 'to-lifecycle-environment-id': lce['id'], } ) - repo = Repository.list( + repo = module_target_sat.cli.Repository.list( { 'name': repo['name'], 'environment-id': lce['id'], @@ -879,10 +974,13 @@ def test_positive_product_name_change_after_promotion(self, module_org): } )[0] expected_name = f'{content_view["label"]}/{new_prod_name}'.lower() - assert Repository.info({'id': repo['id']})['container-repository-name'] == expected_name + assert ( + module_target_sat.cli.Repository.info({'id': repo['id']})['container-repository-name'] + == expected_name + ) @pytest.mark.tier2 - def test_positive_repo_name_change_after_promotion(self, module_org): + def test_positive_repo_name_change_after_promotion(self, module_org, module_target_sat): """Promote content view with Docker repository to lifecycle environment. Change repository name. Verify that Docker repository name on product changed according to new pattern. @@ -897,27 +995,37 @@ def test_positive_repo_name_change_after_promotion(self, module_org): docker_upstream_name = 'hello-world' new_pattern = '<%= content_view.label %>/<%= repository.name %>' - lce = make_lifecycle_environment({'organization-id': module_org.id}) - prod = make_product_wait({'organization-id': module_org.id}) - repo = _repo(prod['id'], name=old_repo_name, upstream_name=docker_upstream_name) - Repository.synchronize({'id': repo['id']}) - content_view = make_content_view({'composite': False, 'organization-id': module_org.id}) - ContentView.add_repository({'id': content_view['id'], 'repository-id': repo['id']}) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) - LifecycleEnvironment.update( + lce = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + prod = module_target_sat.cli_factory.make_product_wait({'organization-id': module_org.id}) + repo = _repo( + module_target_sat, prod['id'], name=old_repo_name, upstream_name=docker_upstream_name + ) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'composite': False, 'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.add_repository( + {'id': content_view['id'], 'repository-id': repo['id']} + ) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) + module_target_sat.cli.LifecycleEnvironment.update( { 'registry-name-pattern': new_pattern, 'id': lce['id'], 'organization-id': module_org.id, } ) - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': content_view['versions'][0]['id'], 'to-lifecycle-environment-id': lce['id']} ) - Repository.update({'name': new_repo_name, 'id': repo['id'], 'product-id': prod['id']}) + module_target_sat.cli.Repository.update( + {'name': new_repo_name, 'id': repo['id'], 'product-id': prod['id']} + ) - repo = Repository.list( + repo = module_target_sat.cli.Repository.list( { 'name': new_repo_name, 'environment-id': lce['id'], @@ -925,18 +1033,21 @@ def test_positive_repo_name_change_after_promotion(self, module_org): } )[0] expected_name = f'{content_view["label"]}/{old_repo_name}'.lower() - assert Repository.info({'id': repo['id']})['container-repository-name'] == expected_name + assert ( + module_target_sat.cli.Repository.info({'id': repo['id']})['container-repository-name'] + == expected_name + ) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) - ContentView.version_promote( + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) + module_target_sat.cli.ContentView.version_promote( { 'id': content_view['versions'][-1]['id'], 'to-lifecycle-environment-id': lce['id'], } ) - repo = Repository.list( + repo = module_target_sat.cli.Repository.list( { 'name': new_repo_name, 'environment-id': lce['id'], @@ -944,10 +1055,13 @@ def test_positive_repo_name_change_after_promotion(self, module_org): } )[0] expected_name = f'{content_view["label"]}/{new_repo_name}'.lower() - assert Repository.info({'id': repo['id']})['container-repository-name'] == expected_name + assert ( + module_target_sat.cli.Repository.info({'id': repo['id']})['container-repository-name'] + == expected_name + ) @pytest.mark.tier2 - def test_negative_set_non_unique_name_pattern_and_promote(self, module_org): + def test_negative_set_non_unique_name_pattern_and_promote(self, module_org, module_target_sat): """Set registry name pattern to one that does not guarantee uniqueness. Try to promote content view with multiple Docker repositories to lifecycle environment. Verify that content has not been promoted. @@ -959,26 +1073,32 @@ def test_negative_set_non_unique_name_pattern_and_promote(self, module_org): docker_upstream_names = ['hello-world', 'alpine'] new_pattern = '<%= organization.label %>' - lce = make_lifecycle_environment( + lce = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id, 'registry-name-pattern': new_pattern} ) - prod = make_product_wait({'organization-id': module_org.id}) - content_view = make_content_view({'composite': False, 'organization-id': module_org.id}) + prod = module_target_sat.cli_factory.make_product_wait({'organization-id': module_org.id}) + content_view = module_target_sat.cli_factory.make_content_view( + {'composite': False, 'organization-id': module_org.id} + ) for docker_name in docker_upstream_names: - repo = _repo(prod['id'], upstream_name=docker_name) - Repository.synchronize({'id': repo['id']}) - ContentView.add_repository({'id': content_view['id'], 'repository-id': repo['id']}) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + repo = _repo(module_target_sat, prod['id'], upstream_name=docker_name) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': content_view['id'], 'repository-id': repo['id']} + ) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) with pytest.raises(CLIReturnCodeError): - ContentView.version_promote( + module_target_sat.cli.ContentView.version_promote( {'id': content_view['versions'][0]['id'], 'to-lifecycle-environment-id': lce['id']} ) @pytest.mark.tier2 - def test_negative_promote_and_set_non_unique_name_pattern(self, module_org, module_product): + def test_negative_promote_and_set_non_unique_name_pattern( + self, module_org, module_product, module_target_sat + ): """Promote content view with multiple Docker repositories to lifecycle environment. Set registry name pattern to one that does not guarantee uniqueness. Verify that pattern has not been @@ -991,20 +1111,26 @@ def test_negative_promote_and_set_non_unique_name_pattern(self, module_org, modu docker_upstream_names = ['hello-world', 'alpine'] new_pattern = '<%= organization.label %>' - content_view = make_content_view({'composite': False, 'organization-id': module_org.id}) + content_view = module_target_sat.cli_factory.make_content_view( + {'composite': False, 'organization-id': module_org.id} + ) for docker_name in docker_upstream_names: - repo = _repo(module_product.id, upstream_name=docker_name) - Repository.synchronize({'id': repo['id']}) - ContentView.add_repository({'id': content_view['id'], 'repository-id': repo['id']}) - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) - lce = make_lifecycle_environment({'organization-id': module_org.id}) - ContentView.version_promote( + repo = _repo(module_target_sat, module_product.id, upstream_name=docker_name) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + module_target_sat.cli.ContentView.add_repository( + {'id': content_view['id'], 'repository-id': repo['id']} + ) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) + lce = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.version_promote( {'id': content_view['versions'][0]['id'], 'to-lifecycle-environment-id': lce['id']} ) with pytest.raises(CLIReturnCodeError): - LifecycleEnvironment.update( + module_target_sat.cli.LifecycleEnvironment.update( { 'registry-name-pattern': new_pattern, 'id': lce['id'], @@ -1024,7 +1150,9 @@ class TestDockerActivationKey: """ @pytest.mark.tier2 - def test_positive_add_docker_repo_cv(self, module_org, module_lce, content_view_promote): + def test_positive_add_docker_repo_cv( + self, module_org, module_lce, content_view_promote, module_target_sat + ): """Add Docker-type repository to a non-composite content view and publish it. Then create an activation key and associate it with the Docker content view. @@ -1034,7 +1162,7 @@ def test_positive_add_docker_repo_cv(self, module_org, module_lce, content_view_ :expectedresults: Docker-based content view can be added to activation key """ - activation_key = make_activation_key( + activation_key = module_target_sat.cli_factory.make_activation_key( { 'content-view-id': content_view_promote['content-view-id'], 'lifecycle-environment-id': module_lce.id, @@ -1044,7 +1172,9 @@ def test_positive_add_docker_repo_cv(self, module_org, module_lce, content_view_ assert activation_key['content-view'] == content_view_promote['content-view-name'] @pytest.mark.tier2 - def test_positive_remove_docker_repo_cv(self, module_org, module_lce, content_view_promote): + def test_positive_remove_docker_repo_cv( + self, module_org, module_lce, content_view_promote, module_target_sat + ): """Add Docker-type repository to a non-composite content view and publish it. Create an activation key and associate it with the Docker content view. Then remove this content view from the activation @@ -1055,7 +1185,7 @@ def test_positive_remove_docker_repo_cv(self, module_org, module_lce, content_vi :expectedresults: Docker-based content view can be added and then removed from the activation key. """ - activation_key = make_activation_key( + activation_key = module_target_sat.cli_factory.make_activation_key( { 'content-view-id': content_view_promote['content-view-id'], 'lifecycle-environment-id': module_lce.id, @@ -1065,14 +1195,16 @@ def test_positive_remove_docker_repo_cv(self, module_org, module_lce, content_vi assert activation_key['content-view'] == content_view_promote['content-view-name'] # Create another content view replace with - another_cv = make_content_view({'composite': False, 'organization-id': module_org.id}) - ContentView.publish({'id': another_cv['id']}) - another_cv = ContentView.info({'id': another_cv['id']}) - ContentView.version_promote( + another_cv = module_target_sat.cli_factory.make_content_view( + {'composite': False, 'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.publish({'id': another_cv['id']}) + another_cv = module_target_sat.cli.ContentView.info({'id': another_cv['id']}) + module_target_sat.cli.ContentView.version_promote( {'id': another_cv['versions'][0]['id'], 'to-lifecycle-environment-id': module_lce.id} ) - ActivationKey.update( + module_target_sat.cli.ActivationKey.update( { 'id': activation_key['id'], 'organization-id': module_org.id, @@ -1080,11 +1212,13 @@ def test_positive_remove_docker_repo_cv(self, module_org, module_lce, content_vi 'lifecycle-environment-id': module_lce.id, } ) - activation_key = ActivationKey.info({'id': activation_key['id']}) + activation_key = module_target_sat.cli.ActivationKey.info({'id': activation_key['id']}) assert activation_key['content-view'] != content_view_promote['content-view-name'] @pytest.mark.tier2 - def test_positive_add_docker_repo_ccv(self, module_org, module_lce, content_view_publish): + def test_positive_add_docker_repo_ccv( + self, module_org, module_lce, content_view_publish, module_target_sat + ): """Add Docker-type repository to a non-composite content view and publish it. Then add this content view to a composite content view and publish it. Create an activation key and associate it with the @@ -1097,25 +1231,29 @@ def test_positive_add_docker_repo_ccv(self, module_org, module_lce, content_view :BZ: 1359665 """ - comp_content_view = make_content_view({'composite': True, 'organization-id': module_org.id}) - ContentView.update( + comp_content_view = module_target_sat.cli_factory.make_content_view( + {'composite': True, 'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.update( { 'component-ids': content_view_publish['id'], 'id': comp_content_view['id'], } ) - comp_content_view = ContentView.info({'id': comp_content_view['id']}) + comp_content_view = module_target_sat.cli.ContentView.info({'id': comp_content_view['id']}) assert content_view_publish['id'] in [ component['id'] for component in comp_content_view['components'] ] - ContentView.publish({'id': comp_content_view['id']}) - comp_content_view = ContentView.info({'id': comp_content_view['id']}) - comp_cvv = ContentView.version_info({'id': comp_content_view['versions'][0]['id']}) - ContentView.version_promote( + module_target_sat.cli.ContentView.publish({'id': comp_content_view['id']}) + comp_content_view = module_target_sat.cli.ContentView.info({'id': comp_content_view['id']}) + comp_cvv = module_target_sat.cli.ContentView.version_info( + {'id': comp_content_view['versions'][0]['id']} + ) + module_target_sat.cli.ContentView.version_promote( {'id': comp_cvv['id'], 'to-lifecycle-environment-id': module_lce.id} ) - activation_key = make_activation_key( + activation_key = module_target_sat.cli_factory.make_activation_key( { 'content-view-id': comp_content_view['id'], 'lifecycle-environment-id': module_lce.id, @@ -1125,7 +1263,9 @@ def test_positive_add_docker_repo_ccv(self, module_org, module_lce, content_view assert activation_key['content-view'] == comp_content_view['name'] @pytest.mark.tier2 - def test_positive_remove_docker_repo_ccv(self, module_org, module_lce, content_view_publish): + def test_positive_remove_docker_repo_ccv( + self, module_org, module_lce, content_view_publish, module_target_sat + ): """Add Docker-type repository to a non-composite content view and publish it. Then add this content view to a composite content view and publish it. Create an activation key and associate it with the @@ -1139,25 +1279,29 @@ def test_positive_remove_docker_repo_ccv(self, module_org, module_lce, content_v :BZ: 1359665 """ - comp_content_view = make_content_view({'composite': True, 'organization-id': module_org.id}) - ContentView.update( + comp_content_view = module_target_sat.cli_factory.make_content_view( + {'composite': True, 'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.update( { 'component-ids': content_view_publish['id'], 'id': comp_content_view['id'], } ) - comp_content_view = ContentView.info({'id': comp_content_view['id']}) + comp_content_view = module_target_sat.cli.ContentView.info({'id': comp_content_view['id']}) assert content_view_publish['id'] in [ component['id'] for component in comp_content_view['components'] ] - ContentView.publish({'id': comp_content_view['id']}) - comp_content_view = ContentView.info({'id': comp_content_view['id']}) - comp_cvv = ContentView.version_info({'id': comp_content_view['versions'][0]['id']}) - ContentView.version_promote( + module_target_sat.cli.ContentView.publish({'id': comp_content_view['id']}) + comp_content_view = module_target_sat.cli.ContentView.info({'id': comp_content_view['id']}) + comp_cvv = module_target_sat.cli.ContentView.version_info( + {'id': comp_content_view['versions'][0]['id']} + ) + module_target_sat.cli.ContentView.version_promote( {'id': comp_cvv['id'], 'to-lifecycle-environment-id': module_lce.id} ) - activation_key = make_activation_key( + activation_key = module_target_sat.cli_factory.make_activation_key( { 'content-view-id': comp_content_view['id'], 'lifecycle-environment-id': module_lce.id, @@ -1167,14 +1311,16 @@ def test_positive_remove_docker_repo_ccv(self, module_org, module_lce, content_v assert activation_key['content-view'] == comp_content_view['name'] # Create another content view replace with - another_cv = make_content_view({'composite': False, 'organization-id': module_org.id}) - ContentView.publish({'id': another_cv['id']}) - another_cv = ContentView.info({'id': another_cv['id']}) - ContentView.version_promote( + another_cv = module_target_sat.cli_factory.make_content_view( + {'composite': False, 'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.publish({'id': another_cv['id']}) + another_cv = module_target_sat.cli.ContentView.info({'id': another_cv['id']}) + module_target_sat.cli.ContentView.version_promote( {'id': another_cv['versions'][0]['id'], 'to-lifecycle-environment-id': module_lce.id} ) - ActivationKey.update( + module_target_sat.cli.ActivationKey.update( { 'id': activation_key['id'], 'organization-id': module_org.id, @@ -1182,5 +1328,5 @@ def test_positive_remove_docker_repo_ccv(self, module_org, module_lce, content_v 'lifecycle-environment-id': module_lce.id, } ) - activation_key = ActivationKey.info({'id': activation_key['id']}) + activation_key = module_target_sat.cli.ActivationKey.info({'id': activation_key['id']}) assert activation_key['content-view'] != comp_content_view['name'] diff --git a/tests/foreman/cli/test_domain.py b/tests/foreman/cli/test_domain.py index b0d6aeca207..ad48827e8f1 100644 --- a/tests/foreman/cli/test_domain.py +++ b/tests/foreman/cli/test_domain.py @@ -19,9 +19,7 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.domain import Domain -from robottelo.cli.factory import CLIFactoryError, make_domain, make_location, make_org +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.utils.datafactory import ( filtered_datapoint, invalid_id_list, @@ -113,7 +111,7 @@ def valid_delete_params(): @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_create_update_delete_domain(): +def test_positive_create_update_delete_domain(module_target_sat): """Create domain, update and delete domain and set parameters :id: 018740bf-1551-4162-b88e-4d4905af097b @@ -123,9 +121,9 @@ def test_positive_create_update_delete_domain(): :CaseImportance: Critical """ options = valid_create_params()[0] - location = make_location() - org = make_org() - domain = make_domain( + location = module_target_sat.cli_factory.make_location() + org = module_target_sat.cli_factory.make_org() + domain = module_target_sat.cli_factory.make_domain( { 'name': options['name'], 'description': options['description'], @@ -141,8 +139,8 @@ def test_positive_create_update_delete_domain(): # set parameter parameter_options = valid_set_params()[0] parameter_options['domain-id'] = domain['id'] - Domain.set_parameter(parameter_options) - domain = Domain.info({'id': domain['id']}) + module_target_sat.cli.Domain.set_parameter(parameter_options) + domain = module_target_sat.cli.Domain.info({'id': domain['id']}) parameter = { # Satellite applies lower to parameter's name parameter_options['name'].lower(): parameter_options['value'] @@ -151,27 +149,29 @@ def test_positive_create_update_delete_domain(): # update domain options = valid_update_params()[0] - Domain.update(dict(options, id=domain['id'])) + module_target_sat.cli.Domain.update(dict(options, id=domain['id'])) # check - domain updated - domain = Domain.info({'id': domain['id']}) + domain = module_target_sat.cli.Domain.info({'id': domain['id']}) for key, val in options.items(): assert domain[key] == val # delete parameter - Domain.delete_parameter({'name': parameter_options['name'], 'domain-id': domain['id']}) + module_target_sat.cli.Domain.delete_parameter( + {'name': parameter_options['name'], 'domain-id': domain['id']} + ) # check - parameter not set - domain = Domain.info({'name': domain['name']}) + domain = module_target_sat.cli.Domain.info({'name': domain['name']}) assert len(domain['parameters']) == 0 # delete domain - Domain.delete({'id': domain['id']}) + module_target_sat.cli.Domain.delete({'id': domain['id']}) with pytest.raises(CLIReturnCodeError): - Domain.info({'id': domain['id']}) + module_target_sat.cli.Domain.info({'id': domain['id']}) @pytest.mark.tier2 @pytest.mark.parametrize('options', **parametrized(invalid_create_params())) -def test_negative_create(options): +def test_negative_create(options, module_target_sat): """Create domain with invalid values :id: 6d3aec19-75dc-41ca-89af-fef0ca37082d @@ -183,11 +183,11 @@ def test_negative_create(options): :CaseImportance: Medium """ with pytest.raises(CLIFactoryError): - make_domain(options) + module_target_sat.cli_factory.make_domain(options) @pytest.mark.tier2 -def test_negative_create_with_invalid_dns_id(): +def test_negative_create_with_invalid_dns_id(module_target_sat): """Attempt to register a domain with invalid id :id: 4aa52167-368a-41ad-87b7-41d468ad41a8 @@ -201,7 +201,7 @@ def test_negative_create_with_invalid_dns_id(): :CaseImportance: Medium """ with pytest.raises(CLIFactoryError) as context: - make_domain({'name': gen_string('alpha'), 'dns-id': -1}) + module_target_sat.cli_factory.make_domain({'name': gen_string('alpha'), 'dns-id': -1}) valid_messages = ['Invalid smart-proxy id', 'Invalid capsule id'] exception_string = str(context.value) messages = [message for message in valid_messages if message in exception_string] @@ -210,7 +210,7 @@ def test_negative_create_with_invalid_dns_id(): @pytest.mark.tier2 @pytest.mark.parametrize('options', **parametrized(invalid_update_params())) -def test_negative_update(module_domain, options): +def test_negative_update(module_domain, options, module_target_sat): """Update domain with invalid values :id: 9fc708dc-20f9-4d7c-af53-863826462981 @@ -222,16 +222,16 @@ def test_negative_update(module_domain, options): :CaseImportance: Medium """ with pytest.raises(CLIReturnCodeError): - Domain.update(dict(options, id=module_domain.id)) + module_target_sat.cli.Domain.update(dict(options, id=module_domain.id)) # check - domain not updated - result = Domain.info({'id': module_domain.id}) + result = module_target_sat.cli.Domain.info({'id': module_domain.id}) for key in options.keys(): assert result[key] == getattr(module_domain, key) @pytest.mark.tier2 @pytest.mark.parametrize('options', **parametrized(invalid_set_params())) -def test_negative_set_parameter(module_domain, options): +def test_negative_set_parameter(module_domain, options, module_target_sat): """Domain set-parameter with invalid values :id: 991fb849-83be-48f4-a12b-81eabb2bd8d3 @@ -245,15 +245,15 @@ def test_negative_set_parameter(module_domain, options): options['domain-id'] = module_domain.id # set parameter with pytest.raises(CLIReturnCodeError): - Domain.set_parameter(options) + module_target_sat.cli.Domain.set_parameter(options) # check - parameter not set - domain = Domain.info({'id': module_domain.id}) + domain = module_target_sat.cli.Domain.info({'id': module_domain.id}) assert len(domain['parameters']) == 0 @pytest.mark.tier2 @pytest.mark.parametrize('entity_id', **parametrized(invalid_id_list())) -def test_negative_delete_by_id(entity_id): +def test_negative_delete_by_id(entity_id, module_target_sat): """Create Domain then delete it by wrong ID :id: 0e4ef107-f006-4433-abc3-f872613e0b91 @@ -265,4 +265,4 @@ def test_negative_delete_by_id(entity_id): :CaseImportance: Medium """ with pytest.raises(CLIReturnCodeError): - Domain.delete({'id': entity_id}) + module_target_sat.cli.Domain.delete({'id': entity_id}) diff --git a/tests/foreman/cli/test_environment.py b/tests/foreman/cli/test_environment.py index ae6e0d7e947..10481c743c6 100644 --- a/tests/foreman/cli/test_environment.py +++ b/tests/foreman/cli/test_environment.py @@ -21,8 +21,8 @@ from fauxfactory import gen_alphanumeric, gen_string import pytest -from robottelo.cli.base import CLIReturnCodeError from robottelo.config import settings +from robottelo.exceptions import CLIReturnCodeError from robottelo.utils.datafactory import ( invalid_id_list, invalid_values_list, diff --git a/tests/foreman/cli/test_errata.py b/tests/foreman/cli/test_errata.py index d5adf0ccc66..d8e8bd93969 100644 --- a/tests/foreman/cli/test_errata.py +++ b/tests/foreman/cli/test_errata.py @@ -23,24 +23,6 @@ from nailgun import entities import pytest -from robottelo.cli.activationkey import ActivationKey -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.contentview import ContentView, ContentViewFilter -from robottelo.cli.erratum import Erratum -from robottelo.cli.factory import ( - make_content_view_filter, - make_content_view_filter_rule, - make_host_collection, - make_repository, - setup_org_for_a_custom_repo, - setup_org_for_a_rh_repo, -) -from robottelo.cli.host import Host -from robottelo.cli.hostcollection import HostCollection -from robottelo.cli.job_invocation import JobInvocation -from robottelo.cli.package import Package -from robottelo.cli.repository import Repository -from robottelo.cli.repository_set import RepositorySet from robottelo.config import settings from robottelo.constants import ( DEFAULT_ARCHITECTURE, @@ -59,6 +41,7 @@ REPOS, REPOSET, ) +from robottelo.exceptions import CLIReturnCodeError from robottelo.hosts import ContentHost PER_PAGE = 10 @@ -128,7 +111,7 @@ def orgs(): @pytest.fixture(scope='module') -def products_with_repos(orgs): +def products_with_repos(orgs, module_target_sat): """Create and return a list of products. For each product, create and sync a single repo.""" products = [] # Create one product for each org, and a second product for the last org. @@ -138,7 +121,7 @@ def products_with_repos(orgs): # with the one we already have. product.organization = org products.append(product) - repo = make_repository( + repo = module_target_sat.cli_factory.make_repository( { 'download-policy': 'immediate', 'organization-id': product.organization.id, @@ -146,15 +129,17 @@ def products_with_repos(orgs): 'url': params['url'], } ) - Repository.synchronize({'id': repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) return products @pytest.fixture(scope='module') -def rh_repo(module_entitlement_manifest_org, module_lce, module_cv, module_ak_cv_lce): +def rh_repo( + module_entitlement_manifest_org, module_lce, module_cv, module_ak_cv_lce, module_target_sat +): """Add a subscription for the Satellite Tools repo to activation key.""" - setup_org_for_a_rh_repo( + module_target_sat.cli_factory.setup_org_for_a_rh_repo( { 'product': PRDS['rhel'], 'repository-set': REPOSET['rhst7'], @@ -168,9 +153,11 @@ def rh_repo(module_entitlement_manifest_org, module_lce, module_cv, module_ak_cv @pytest.fixture(scope='module') -def custom_repo(module_entitlement_manifest_org, module_lce, module_cv, module_ak_cv_lce): +def custom_repo( + module_entitlement_manifest_org, module_lce, module_cv, module_ak_cv_lce, module_target_sat +): """Create custom repo and add a subscription to activation key.""" - setup_org_for_a_custom_repo( + module_target_sat.cli_factory.setup_org_for_a_custom_repo( { 'url': REPO_WITH_ERRATA['url'], 'organization-id': module_entitlement_manifest_org.id, @@ -230,18 +217,24 @@ def errata_hosts(register_hosts): @pytest.fixture(scope='module') -def host_collection(module_entitlement_manifest_org, module_ak_cv_lce, register_hosts): +def host_collection( + module_entitlement_manifest_org, module_ak_cv_lce, register_hosts, module_target_sat +): """Create and setup host collection.""" - host_collection = make_host_collection({'organization-id': module_entitlement_manifest_org.id}) - host_ids = [Host.info({'name': host.hostname})['id'] for host in register_hosts] - HostCollection.add_host( + host_collection = module_target_sat.cli_factory.make_host_collection( + {'organization-id': module_entitlement_manifest_org.id} + ) + host_ids = [ + module_target_sat.cli.Host.info({'name': host.hostname})['id'] for host in register_hosts + ] + module_target_sat.cli.HostCollection.add_host( { 'id': host_collection['id'], 'organization-id': module_entitlement_manifest_org.id, 'host-ids': host_ids, } ) - ActivationKey.add_host_collection( + module_target_sat.cli.ActivationKey.add_host_collection( { 'id': module_ak_cv_lce.id, 'host-collection-id': host_collection['id'], @@ -262,7 +255,7 @@ def is_rpm_installed(host, rpm=None): return not host.execute(f'rpm -q {rpm}').status -def get_sorted_errata_info_by_id(errata_ids, sort_by='issued', sort_reversed=False): +def get_sorted_errata_info_by_id(sat, errata_ids, sort_by='issued', sort_reversed=False): """Query hammer for erratum ids info :param errata_ids: a list of errata id @@ -278,14 +271,17 @@ def get_sorted_errata_info_by_id(errata_ids, sort_by='issued', sort_reversed=Fal if len(errata_ids) > PER_PAGE: raise Exception('Errata ids length exceeded') errata_info = [ - Erratum.info(options={'id': errata_id}, output_format='json') for errata_id in errata_ids + sat.cli.Erratum.info(options={'id': errata_id}, output_format='json') + for errata_id in errata_ids ] return sorted(errata_info, key=itemgetter(sort_by), reverse=sort_reversed) -def get_errata_ids(*params): +def get_errata_ids(sat, *params): """Return list of sets of errata ids corresponding to the provided params.""" - errata_ids = [{errata['errata-id'] for errata in Erratum.list(param)} for param in params] + errata_ids = [ + {errata['errata-id'] for errata in sat.cli.Erratum.list(param)} for param in params + ] return errata_ids[0] if len(errata_ids) == 1 else errata_ids @@ -300,7 +296,7 @@ def check_errata(errata_ids, by_org=False): assert repo_with_errata['errata_id'] in ids -def filter_sort_errata(org, sort_by_date='issued', filter_by_org=None): +def filter_sort_errata(sat, org, sort_by_date='issued', filter_by_org=None): """Compare the list of errata returned by `hammer erratum {list|info}` to the expected values, subject to the date sort and organization filter options. @@ -323,13 +319,13 @@ def filter_sort_errata(org, sort_by_date='issued', filter_by_org=None): sort_reversed = True if sort_order == 'DESC' else False - errata_list = Erratum.list(list_param) + errata_list = sat.cli.Erratum.list(list_param) assert len(errata_list) > 0 # Build a sorted errata info list, which also contains the sort field. errata_internal_ids = [errata['id'] for errata in errata_list] sorted_errata_info = get_sorted_errata_info_by_id( - errata_internal_ids, sort_by=sort_by_date, sort_reversed=sort_reversed + sat, errata_internal_ids, sort_by=sort_by_date, sort_reversed=sort_reversed ) sort_field_values = [errata[sort_by_date] for errata in sorted_errata_info] @@ -340,7 +336,7 @@ def filter_sort_errata(org, sort_by_date='issued', filter_by_org=None): assert errata_ids == sorted_errata_ids -def cv_publish_promote(cv, org, lce): +def cv_publish_promote(sat, cv, org, lce): """Publish and promote a new version into the given lifecycle environment. :param cv: content view @@ -350,27 +346,27 @@ def cv_publish_promote(cv, org, lce): :param lce: lifecycle environment :type lce: entities.LifecycleEnvironment """ - ContentView.publish({'id': cv.id}) - cvv = ContentView.info({'id': cv.id})['versions'][-1] - ContentView.version_promote( + sat.cli.ContentView.publish({'id': cv.id}) + sat.cli.ContentView.info({'id': cv.id})['versions'][-1] + sat.cli.ContentView.version_promote( { - 'id': cvv['id'], - 'organization-id': org.id, + 'id': cv.id, + 'organization-id': org, 'to-lifecycle-environment-id': lce.id, } ) -def cv_filter_cleanup(filter_id, cv, org, lce): +def cv_filter_cleanup(sat, filter_id, cv, org, lce): """Delete the cv filter, then publish and promote an unfiltered version.""" - ContentViewFilter.delete( + sat.cli.ContentViewFilter.delete( { 'content-view-id': cv.id, 'id': filter_id, 'organization-id': org.id, } ) - cv_publish_promote(cv, org, lce) + cv_publish_promote(sat, cv, org, lce) @pytest.mark.tier3 @@ -429,7 +425,7 @@ def test_positive_install_by_host_collection_and_org( organization_key = 'organization-title' organization_value = module_entitlement_manifest_org.title - JobInvocation.create( + target_sat.cli.JobInvocation.create( { 'feature': 'katello_errata_install', 'search-query': host_collection_query, @@ -444,7 +440,7 @@ def test_positive_install_by_host_collection_and_org( @pytest.mark.tier3 def test_negative_install_by_hc_id_without_errata_info( - module_entitlement_manifest_org, host_collection, errata_hosts + module_entitlement_manifest_org, host_collection, errata_hosts, target_sat ): """Attempt to install an erratum on a host collection by host collection id but no errata info specified. @@ -463,7 +459,7 @@ def test_negative_install_by_hc_id_without_errata_info( :CaseLevel: System """ with pytest.raises(CLIReturnCodeError, match="Error: Option '--errata' is required"): - HostCollection.erratum_install( + target_sat.cli.HostCollection.erratum_install( { 'id': host_collection['id'], 'organization-id': module_entitlement_manifest_org.id, @@ -473,7 +469,7 @@ def test_negative_install_by_hc_id_without_errata_info( @pytest.mark.tier3 def test_negative_install_by_hc_name_without_errata_info( - module_entitlement_manifest_org, host_collection, errata_hosts + module_entitlement_manifest_org, host_collection, errata_hosts, target_sat ): """Attempt to install an erratum on a host collection by host collection name but no errata info specified. @@ -492,7 +488,7 @@ def test_negative_install_by_hc_name_without_errata_info( :CaseLevel: System """ with pytest.raises(CLIReturnCodeError, match="Error: Option '--errata' is required"): - HostCollection.erratum_install( + target_sat.cli.HostCollection.erratum_install( { 'name': host_collection['name'], 'organization-id': module_entitlement_manifest_org.id, @@ -501,7 +497,9 @@ def test_negative_install_by_hc_name_without_errata_info( @pytest.mark.tier3 -def test_negative_install_without_hc_info(module_entitlement_manifest_org, host_collection): +def test_negative_install_without_hc_info( + module_entitlement_manifest_org, host_collection, module_target_sat +): """Attempt to install an erratum on a host collection without specifying host collection info. This test only works with two or more host collections (BZ#1928281). We have the one from the fixture, just need to create one more at the start of the test. @@ -521,9 +519,11 @@ def test_negative_install_without_hc_info(module_entitlement_manifest_org, host_ :CaseLevel: System """ - make_host_collection({'organization-id': module_entitlement_manifest_org.id}) + module_target_sat.cli_factory.make_host_collection( + {'organization-id': module_entitlement_manifest_org.id} + ) with pytest.raises(CLIReturnCodeError): - HostCollection.erratum_install( + module_target_sat.cli.HostCollection.erratum_install( { 'organization-id': module_entitlement_manifest_org.id, 'errata': [REPO_WITH_ERRATA['errata'][0]['id']], @@ -533,7 +533,7 @@ def test_negative_install_without_hc_info(module_entitlement_manifest_org, host_ @pytest.mark.tier3 def test_negative_install_by_hc_id_without_org_info( - module_entitlement_manifest_org, host_collection + module_entitlement_manifest_org, host_collection, module_target_sat ): """Attempt to install an erratum on a host collection by host collection id but without specifying any org info. @@ -551,14 +551,14 @@ def test_negative_install_by_hc_id_without_org_info( :CaseLevel: System """ with pytest.raises(CLIReturnCodeError, match='Error: Could not find organization'): - HostCollection.erratum_install( + module_target_sat.cli.HostCollection.erratum_install( {'id': host_collection['id'], 'errata': [REPO_WITH_ERRATA['errata'][0]['id']]} ) @pytest.mark.tier3 def test_negative_install_by_hc_name_without_org_info( - module_entitlement_manifest_org, host_collection + module_entitlement_manifest_org, host_collection, module_target_sat ): """Attempt to install an erratum on a host collection by host collection name but without specifying any org info. @@ -576,14 +576,14 @@ def test_negative_install_by_hc_name_without_org_info( :CaseLevel: System """ with pytest.raises(CLIReturnCodeError, match='Error: Could not find organization'): - HostCollection.erratum_install( + module_target_sat.cli.HostCollection.erratum_install( {'name': host_collection['name'], 'errata': [REPO_WITH_ERRATA['errata'][0]['id']]} ) @pytest.mark.tier3 @pytest.mark.upgrade -def test_positive_list_affected_chosts(module_entitlement_manifest_org, errata_hosts): +def test_positive_list_affected_chosts(module_entitlement_manifest_org, errata_hosts, target_sat): """View a list of affected content hosts for an erratum. :id: 3b592253-52c0-4165-9a48-ba55287e9ee9 @@ -598,7 +598,7 @@ def test_positive_list_affected_chosts(module_entitlement_manifest_org, errata_h :CaseAutomation: Automated """ - result = Host.list( + result = target_sat.cli.Host.list( { 'search': f'applicable_errata = {REPO_WITH_ERRATA["errata"][0]["id"]}', 'organization-id': module_entitlement_manifest_org.id, @@ -647,7 +647,7 @@ def test_install_errata_to_one_host( # Add ssh keys for host in errata_hosts: host.add_rex_key(satellite=target_sat) - Host.errata_recalculate({'host-id': host.nailgun_host.id}) + target_sat.cli.Host.errata_recalculate({'host-id': host.nailgun_host.id}) timestamp = (datetime.utcnow() - timedelta(minutes=1)).strftime(TIMESTAMP_FMT) target_sat.wait_for_tasks( search_query=( @@ -668,7 +668,7 @@ def test_install_errata_to_one_host( @pytest.mark.tier3 @pytest.mark.e2e def test_positive_list_affected_chosts_by_erratum_restrict_flag( - request, module_entitlement_manifest_org, module_cv, module_lce, errata_hosts + request, module_entitlement_manifest_org, module_cv, module_lce, errata_hosts, target_sat ): """View a list of affected content hosts for an erratum filtered with restrict flags. Applicability is calculated using the Library, @@ -718,7 +718,7 @@ def test_positive_list_affected_chosts_by_erratum_restrict_flag( 'organization-id': module_entitlement_manifest_org.id, 'per-page': PER_PAGE_LARGE, } - errata_ids = get_errata_ids(param) + errata_ids = get_errata_ids(target_sat, param) assert errata['id'] in errata_ids, 'Errata not found in list of installable errata' assert not set(uninstallable) & set(errata_ids), 'Unexpected errata found' @@ -730,7 +730,7 @@ def test_positive_list_affected_chosts_by_erratum_restrict_flag( 'organization-id': module_entitlement_manifest_org.id, 'per-page': PER_PAGE_LARGE, } - errata_ids = get_errata_ids(param) + errata_ids = get_errata_ids(target_sat, param) assert set(REPO_WITH_ERRATA['errata_ids']).issubset( errata_ids ), 'Errata not found in list of installable errata' @@ -741,7 +741,7 @@ def test_positive_list_affected_chosts_by_erratum_restrict_flag( 'organization-id': module_entitlement_manifest_org.id, 'per-page': PER_PAGE_LARGE, } - errata_ids = get_errata_ids(param) + errata_ids = get_errata_ids(target_sat, param) assert errata['id'] in errata_ids, 'Errata not found in list of applicable errata' # Check search of errata is not affected by applicable=0 restrict flag @@ -750,14 +750,14 @@ def test_positive_list_affected_chosts_by_erratum_restrict_flag( 'organization-id': module_entitlement_manifest_org.id, 'per-page': PER_PAGE_LARGE, } - errata_ids = get_errata_ids(param) + errata_ids = get_errata_ids(target_sat, param) assert set(REPO_WITH_ERRATA['errata_ids']).issubset( errata_ids ), 'Errata not found in list of applicable errata' # Apply a filter and rule to the CV to hide the RPM, thus making erratum not installable # Make RPM exclude filter - cv_filter = make_content_view_filter( + cv_filter = target_sat.cli_factory.make_content_view_filter( { 'content-view-id': module_cv.id, 'name': 'erratum_restrict_test', @@ -771,6 +771,7 @@ def test_positive_list_affected_chosts_by_erratum_restrict_flag( @request.addfinalizer def cleanup(): cv_filter_cleanup( + target_sat, cv_filter['filter-id'], module_cv, module_entitlement_manifest_org, @@ -778,7 +779,7 @@ def cleanup(): ) # Make rule to hide the RPM that creates the need for the installable erratum - make_content_view_filter_rule( + target_sat.cli_factory.content_view_filter_rule( { 'content-view-id': module_cv.id, 'content-view-filter-id': cv_filter['filter-id'], @@ -797,7 +798,7 @@ def cleanup(): 'organization-id': module_entitlement_manifest_org.id, 'per-page': PER_PAGE_LARGE, } - errata_ids = get_errata_ids(param) + errata_ids = get_errata_ids(target_sat, param) assert errata['id'] not in errata_ids, 'Errata not found in list of installable errata' # Check errata still applicable @@ -806,7 +807,7 @@ def cleanup(): 'organization-id': module_entitlement_manifest_org.id, 'per-page': PER_PAGE_LARGE, } - errata_ids = get_errata_ids(param) + errata_ids = get_errata_ids(target_sat, param) assert errata['id'] in errata_ids, 'Errata not found in list of applicable errata' @@ -858,7 +859,7 @@ def test_host_errata_search_commands( for host in errata_hosts: timestamp = (datetime.utcnow() - timedelta(minutes=1)).strftime(TIMESTAMP_FMT) - Host.errata_recalculate({'host-id': host.nailgun_host.id}) + target_sat.cli.Host.errata_recalculate({'host-id': host.nailgun_host.id}) # Wait for upload profile event (in case Satellite system slow) target_sat.wait_for_tasks( search_query=( @@ -870,7 +871,7 @@ def test_host_errata_search_commands( ) # Step 1: Search for hosts that require bugfix advisories - result = Host.list( + result = target_sat.cli.Host.list( { 'search': 'errata_status = errata_needed', 'organization-id': module_entitlement_manifest_org.id, @@ -882,7 +883,7 @@ def test_host_errata_search_commands( assert errata_hosts[1].hostname not in result # Step 2: Search for hosts that require security advisories - result = Host.list( + result = target_sat.cli.Host.list( { 'search': 'errata_status = security_needed', 'organization-id': module_entitlement_manifest_org.id, @@ -894,7 +895,7 @@ def test_host_errata_search_commands( assert errata_hosts[1].hostname in result # Step 3: Search for hosts that require the specified bugfix advisory - result = Host.list( + result = target_sat.cli.Host.list( { 'search': f'applicable_errata = {errata[1]["id"]}', 'organization-id': module_entitlement_manifest_org.id, @@ -906,7 +907,7 @@ def test_host_errata_search_commands( assert errata_hosts[1].hostname not in result # Step 4: Search for hosts that require the specified security advisory - result = Host.list( + result = target_sat.cli.Host.list( { 'search': f'applicable_errata = {errata[0]["id"]}', 'organization-id': module_entitlement_manifest_org.id, @@ -918,7 +919,7 @@ def test_host_errata_search_commands( assert errata_hosts[1].hostname in result # Step 5: Search for hosts that require the specified bugfix package - result = Host.list( + result = target_sat.cli.Host.list( { 'search': f'applicable_rpms = {errata[1]["new_package"]}', 'organization-id': module_entitlement_manifest_org.id, @@ -930,7 +931,7 @@ def test_host_errata_search_commands( assert errata_hosts[1].hostname not in result # Step 6: Search for hosts that require the specified security package - result = Host.list( + result = target_sat.cli.Host.list( { 'search': f'applicable_rpms = {errata[0]["new_package"]}', 'organization-id': module_entitlement_manifest_org.id, @@ -943,7 +944,7 @@ def test_host_errata_search_commands( # Step 7: Apply filter and rule to CV to hide RPM, thus making erratum not installable # Make RPM exclude filter - cv_filter = make_content_view_filter( + cv_filter = target_sat.cli_factory.make_content_view_filter( { 'content-view-id': module_cv.id, 'name': 'erratum_search_test', @@ -957,6 +958,7 @@ def test_host_errata_search_commands( @request.addfinalizer def cleanup(): cv_filter_cleanup( + target_sat, cv_filter['filter-id'], module_cv, module_entitlement_manifest_org, @@ -964,7 +966,7 @@ def cleanup(): ) # Make rule to exclude the specified bugfix package - make_content_view_filter_rule( + target_sat.cli_factory.content_view_filter_rule( { 'content-view-id': module_cv.id, 'content-view-filter-id': cv_filter['filter-id'], @@ -977,7 +979,7 @@ def cleanup(): # Step 8: Run tests again. Applicable should still be true, installable should now be false. # Search for hosts that require the bugfix package. - result = Host.list( + result = target_sat.cli.Host.list( { 'search': f'applicable_rpms = {errata[1]["new_package"]}', 'organization-id': module_entitlement_manifest_org.id, @@ -989,7 +991,7 @@ def cleanup(): assert errata_hosts[1].hostname not in result # Search for hosts that require the specified bugfix advisory. - result = Host.list( + result = target_sat.cli.Host.list( { 'search': f'installable_errata = {errata[1]["id"]}', 'organization-id': module_entitlement_manifest_org.id, @@ -1009,7 +1011,7 @@ def cleanup(): ids=('org_id', 'org_name', 'org_label', 'no_org_filter'), ) def test_positive_list_filter_by_org_sort_by_date( - module_entitlement_manifest_org, rh_repo, custom_repo, filter_by_org, sort_by_date + module_entitlement_manifest_org, rh_repo, custom_repo, filter_by_org, sort_by_date, target_sat ): """Filter by organization and sort by date. @@ -1026,6 +1028,7 @@ def test_positive_list_filter_by_org_sort_by_date( :expectedresults: Errata are filtered by org and sorted by date. """ filter_sort_errata( + target_sat, module_entitlement_manifest_org, sort_by_date=sort_by_date, filter_by_org=filter_by_org, @@ -1095,7 +1098,7 @@ def test_positive_list_filter_by_product_and_org( @pytest.mark.tier3 -def test_negative_list_filter_by_product_name(products_with_repos): +def test_negative_list_filter_by_product_name(products_with_repos, module_target_sat): """Attempt to Filter errata by product name :id: c7a5988b-668f-4c48-bc1e-97cb968a2563 @@ -1113,7 +1116,9 @@ def test_negative_list_filter_by_product_name(products_with_repos): :CaseLevel: System """ with pytest.raises(CLIReturnCodeError): - Erratum.list({'product': products_with_repos[0].name, 'per-page': PER_PAGE_LARGE}) + module_target_sat.cli.Erratum.list( + {'product': products_with_repos[0].name, 'per-page': PER_PAGE_LARGE} + ) @pytest.mark.tier3 @@ -1153,7 +1158,7 @@ def test_positive_list_filter_by_org(products_with_repos, filter_by_org): @pytest.mark.run_in_one_thread @pytest.mark.tier3 -def test_positive_list_filter_by_cve(module_entitlement_manifest_org, rh_repo): +def test_positive_list_filter_by_cve(module_entitlement_manifest_org, rh_repo, target_sat): """Filter errata by CVE :id: 7791137c-95a7-4518-a56b-766a5680c5fb @@ -1165,7 +1170,7 @@ def test_positive_list_filter_by_cve(module_entitlement_manifest_org, rh_repo): :expectedresults: Errata is filtered by CVE. """ - RepositorySet.enable( + target_sat.cli.RepositorySet.enable( { 'name': REPOSET['rhva6'], 'organization-id': module_entitlement_manifest_org.id, @@ -1174,14 +1179,14 @@ def test_positive_list_filter_by_cve(module_entitlement_manifest_org, rh_repo): 'basearch': 'x86_64', } ) - Repository.synchronize( + target_sat.cli.Repository.synchronize( { 'name': REPOS['rhva6']['name'], 'organization-id': module_entitlement_manifest_org.id, 'product': PRDS['rhel'], } ) - repository_info = Repository.info( + repository_info = target_sat.cli.Repository.info( { 'name': REPOS['rhva6']['name'], 'organization-id': module_entitlement_manifest_org.id, @@ -1190,17 +1195,18 @@ def test_positive_list_filter_by_cve(module_entitlement_manifest_org, rh_repo): ) assert REAL_4_ERRATA_ID in { - errata['errata-id'] for errata in Erratum.list({'repository-id': repository_info['id']}) + errata['errata-id'] + for errata in target_sat.cli.Erratum.list({'repository-id': repository_info['id']}) } for errata_cve in REAL_4_ERRATA_CVES: assert REAL_4_ERRATA_ID in { - errata['errata-id'] for errata in Erratum.list({'cve': errata_cve}) + errata['errata-id'] for errata in target_sat.cli.Erratum.list({'cve': errata_cve}) } @pytest.mark.tier3 -def test_positive_check_errata_dates(module_entitlement_manifest_org): +def test_positive_check_errata_dates(module_entitlement_manifest_org, module_target_sat): """Check for errata dates in `hammer erratum list` :id: b19286ae-bdb4-4319-87d0-5d3ff06c5f38 @@ -1214,19 +1220,19 @@ def test_positive_check_errata_dates(module_entitlement_manifest_org): :BZ: 1695163 """ product = entities.Product(organization=module_entitlement_manifest_org).create() - repo = make_repository( + repo = module_target_sat.cli_factory.make_repository( {'content-type': 'yum', 'product-id': product.id, 'url': REPO_WITH_ERRATA['url']} ) # Synchronize custom repository - Repository.synchronize({'id': repo['id']}) - result = Erratum.list(options={'per-page': '5', 'fields': 'Issued'}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + result = module_target_sat.cli.Erratum.list(options={'per-page': '5', 'fields': 'Issued'}) assert 'issued' in result[0] # Verify any errata ISSUED date from stdout validate_issued_date = datetime.strptime(result[0]['issued'], '%Y-%m-%d').date() assert isinstance(validate_issued_date, date) - result = Erratum.list(options={'per-page': '5', 'fields': 'Updated'}) + result = module_target_sat.cli.Erratum.list(options={'per-page': '5', 'fields': 'Updated'}) assert 'updated' in result[0] # Verify any errata UPDATED date from stdout @@ -1328,11 +1334,13 @@ def test_apply_errata_using_default_content_view(errata_host, target_sat): :CaseImportance: High """ # check that package errata is applicable - erratum = Host.errata_list({'host': errata_host.hostname, 'search': f'id = {REAL_0_ERRATA_ID}'}) + erratum = target_sat.cli.Host.errata_list( + {'host': errata_host.hostname, 'search': f'id = {REAL_0_ERRATA_ID}'} + ) assert len(erratum) == 1 assert erratum[0]['installable'] == 'true' # Update errata from Library, i.e. Default CV - result = JobInvocation.create( + result = target_sat.cli.JobInvocation.create( { 'feature': 'katello_errata_install', 'search-query': f'name = {errata_host.hostname}', @@ -1342,7 +1350,7 @@ def test_apply_errata_using_default_content_view(errata_host, target_sat): )[1]['id'] assert 'success' in result timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime(TIMESTAMP_FMT) - Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) + target_sat.cli.Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) target_sat.wait_for_tasks( search_query=( 'label = Actions::Katello::Applicability::Hosts::BulkGenerate' @@ -1353,7 +1361,9 @@ def test_apply_errata_using_default_content_view(errata_host, target_sat): ) # Assert that the erratum is no longer applicable - erratum = Host.errata_list({'host': errata_host.hostname, 'search': f'id = {REAL_0_ERRATA_ID}'}) + erratum = target_sat.cli.Host.errata_list( + {'host': errata_host.hostname, 'search': f'id = {REAL_0_ERRATA_ID}'} + ) assert len(erratum) == 0 @@ -1379,7 +1389,7 @@ def test_update_applicable_package_using_default_content_view(errata_host, targe :CaseImportance: High """ # check that package is applicable - applicable_packages = Package.list( + applicable_packages = target_sat.cli.Package.list( { 'host-id': errata_host.nailgun_host.id, 'packages-restrict-applicable': 'true', @@ -1387,7 +1397,7 @@ def test_update_applicable_package_using_default_content_view(errata_host, targe } ) timestamp = (datetime.utcnow()).strftime(TIMESTAMP_FMT) - Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) + target_sat.cli.Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) target_sat.wait_for_tasks( search_query=( 'label = Actions::Katello::Applicability::Hosts::BulkGenerate' @@ -1399,7 +1409,7 @@ def test_update_applicable_package_using_default_content_view(errata_host, targe assert len(applicable_packages) == 1 assert REAL_RHEL7_0_2_PACKAGE_NAME in applicable_packages[0]['filename'] # Update package from Library, i.e. Default CV - result = JobInvocation.create( + result = target_sat.cli.JobInvocation.create( { 'feature': 'katello_errata_install', 'search-query': f'name = {errata_host.hostname}', @@ -1410,7 +1420,7 @@ def test_update_applicable_package_using_default_content_view(errata_host, targe assert 'success' in result # note time for later wait_for_tasks include 2 mins margin of safety. timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime(TIMESTAMP_FMT) - Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) + target_sat.cli.Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) # Wait for upload profile event (in case Satellite system slow) target_sat.wait_for_tasks( @@ -1423,8 +1433,8 @@ def test_update_applicable_package_using_default_content_view(errata_host, targe ) # Assert that the package is no longer applicable - Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) - applicable_packages = Package.list( + target_sat.cli.Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) + applicable_packages = target_sat.cli.Package.list( { 'host-id': errata_host.nailgun_host.id, 'packages-restrict-applicable': 'true', @@ -1459,7 +1469,7 @@ def test_downgrade_applicable_package_using_default_content_view(errata_host, ta # Update package from Library, i.e. Default CV errata_host.run(f'yum -y update {REAL_RHEL7_0_2_PACKAGE_NAME}') # Assert that the package is not applicable - applicable_packages = Package.list( + applicable_packages = target_sat.cli.Package.list( { 'host-id': errata_host.nailgun_host.id, 'packages-restrict-applicable': 'true', @@ -1472,7 +1482,7 @@ def test_downgrade_applicable_package_using_default_content_view(errata_host, ta errata_host.run(f'curl -O {settings.repos.epel_repo.url}/{PSUTIL_RPM}') timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime(TIMESTAMP_FMT) errata_host.run(f'yum -y downgrade {PSUTIL_RPM}') - Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) + target_sat.cli.Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) # Wait for upload profile event (in case Satellite system slow) target_sat.wait_for_tasks( search_query=( @@ -1483,7 +1493,7 @@ def test_downgrade_applicable_package_using_default_content_view(errata_host, ta max_tries=10, ) # check that package is applicable - applicable_packages = Package.list( + applicable_packages = target_sat.cli.Package.list( { 'host-id': errata_host.nailgun_host.id, 'packages-restrict-applicable': 'true', @@ -1515,8 +1525,8 @@ def test_install_applicable_package_to_registerd_host(chost, target_sat): :CaseImportance: Medium """ # Assert that the package is not applicable - Host.errata_recalculate({'host-id': chost.nailgun_host.id}) - applicable_packages = Package.list( + target_sat.cli.Host.errata_recalculate({'host-id': chost.nailgun_host.id}) + applicable_packages = target_sat.cli.Package.list( { 'host-id': chost.nailgun_host.id, 'packages-restrict-applicable': 'true', @@ -1538,10 +1548,10 @@ def test_install_applicable_package_to_registerd_host(chost, target_sat): search_rate=15, max_tries=10, ) - Host.errata_recalculate({'host-id': chost.nailgun_host.id}) + target_sat.cli.Host.errata_recalculate({'host-id': chost.nailgun_host.id}) # check that package is applicable - applicable_packages = Package.list( + applicable_packages = target_sat.cli.Package.list( { 'host-id': chost.nailgun_host.id, 'packages-restrict-applicable': 'true', @@ -1579,7 +1589,7 @@ def test_downgrading_package_shows_errata_from_library( # Update package from Library, i.e. Default CV errata_host.run(f'yum -y update {REAL_RHEL7_0_2_PACKAGE_NAME}') # Assert that the package is not applicable - applicable_packages = Package.list( + applicable_packages = target_sat.cli.Package.list( { 'host-id': errata_host.nailgun_host.id, 'packages-restrict-applicable': 'true', @@ -1593,7 +1603,7 @@ def test_downgrading_package_shows_errata_from_library( errata_host.run(f'curl -O {settings.repos.epel_repo.url}/{PSUTIL_RPM}') errata_host.run(f'yum -y downgrade {PSUTIL_RPM}') # Wait for upload profile event (in case Satellite system slow) - Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) + target_sat.cli.Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) # Wait for upload profile event (in case Satellite system slow) target_sat.wait_for_tasks( search_query=( @@ -1615,7 +1625,7 @@ def test_downgrading_package_shows_errata_from_library( @pytest.mark.skip_if_open('BZ:1785146') @pytest.mark.tier2 -def test_errata_list_by_contentview_filter(module_entitlement_manifest_org): +def test_errata_list_by_contentview_filter(module_entitlement_manifest_org, module_target_sat): """Hammer command to list errata should take filter ID into consideration. :id: e9355a92-8354-4853-a806-d388ed32d73e @@ -1636,17 +1646,17 @@ def test_errata_list_by_contentview_filter(module_entitlement_manifest_org): :BZ: 1785146 """ product = entities.Product(organization=module_entitlement_manifest_org).create() - repo = make_repository( + repo = module_target_sat.cli_factory.make_repository( {'content-type': 'yum', 'product-id': product.id, 'url': REPO_WITH_ERRATA['url']} ) - Repository.synchronize({'id': repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) lce = entities.LifecycleEnvironment(organization=module_entitlement_manifest_org).create() cv = entities.ContentView( organization=module_entitlement_manifest_org, repository=[repo['id']] ).create() cv_publish_promote(cv, module_entitlement_manifest_org, lce) errata_count = len( - Erratum.list( + module_target_sat.cli.Erratum.list( { 'organization-id': module_entitlement_manifest_org.id, 'content-view-id': cv.id, @@ -1661,7 +1671,7 @@ def test_errata_list_by_contentview_filter(module_entitlement_manifest_org): cv.publish() cv_version_info = cv.read().version[1].read() errata_count_cvf = len( - Erratum.list( + module_target_sat.cli.Erratum.list( { 'organization-id': module_entitlement_manifest_org.id, 'content-view-id': cv.id, diff --git a/tests/foreman/cli/test_fact.py b/tests/foreman/cli/test_fact.py index 96fee50c126..1b1dd658250 100644 --- a/tests/foreman/cli/test_fact.py +++ b/tests/foreman/cli/test_fact.py @@ -19,8 +19,6 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.fact import Fact - pytestmark = [pytest.mark.tier1] @@ -29,7 +27,7 @@ @pytest.mark.parametrize( 'fact', ['uptime', 'os::family', 'uptime_seconds', 'memorysize', 'ipaddress'] ) -def test_positive_list_by_name(fact): +def test_positive_list_by_name(fact, module_target_sat): """Test Fact List :id: 83794d97-d21b-4482-9522-9b41053e595f @@ -40,12 +38,12 @@ def test_positive_list_by_name(fact): :BZ: 2161294 """ - facts = Fact().list(options={'search': f'fact={fact}'}) + facts = module_target_sat.cli.Fact().list(options={'search': f'fact={fact}'}) assert facts[0]['fact'] == fact @pytest.mark.parametrize('fact', ['uptime_days', 'memoryfree']) -def test_negative_list_ignored_by_name(fact): +def test_negative_list_ignored_by_name(fact, module_target_sat): """Test Fact List :id: b6375f39-b8c3-4807-b04b-b0e43644441f @@ -54,14 +52,16 @@ def test_negative_list_ignored_by_name(fact): :parametrized: yes """ - assert Fact().list(options={'search': f'fact={fact}'}) == [] + assert module_target_sat.cli.Fact().list(options={'search': f'fact={fact}'}) == [] -def test_negative_list_by_name(): +def test_negative_list_by_name(module_target_sat): """Test Fact List failure :id: bd56d27e-59c0-4f35-bd53-2999af7c6946 :expectedresults: Fact List is not displayed """ - assert Fact().list(options={'search': f'fact={gen_string("alpha")}'}) == [] + assert ( + module_target_sat.cli.Fact().list(options={'search': f'fact={gen_string("alpha")}'}) == [] + ) diff --git a/tests/foreman/cli/test_filter.py b/tests/foreman/cli/test_filter.py index 6881d8b8b01..434b91e3980 100644 --- a/tests/foreman/cli/test_filter.py +++ b/tests/foreman/cli/test_filter.py @@ -18,30 +18,29 @@ """ import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_filter, make_location, make_org, make_role -from robottelo.cli.filter import Filter -from robottelo.cli.role import Role +from robottelo.exceptions import CLIReturnCodeError @pytest.fixture(scope='module') -def module_perms(): +def module_perms(module_target_sat): """Search for provisioning template permissions. Set ``cls.ct_perms``.""" perms = [ permission['name'] - for permission in Filter.available_permissions({"search": "resource_type=User"}) + for permission in module_target_sat.cli.Filter.available_permissions( + {"search": "resource_type=User"} + ) ] return perms @pytest.fixture -def function_role(): +def function_role(target_sat): """Create a role that a filter would be assigned""" - return make_role() + return target_sat.cli_factory.make_role() @pytest.mark.tier1 -def test_positive_create_with_permission(module_perms, function_role): +def test_positive_create_with_permission(module_perms, function_role, target_sat): """Create a filter and assign it some permissions. :id: 6da6c5d3-2727-4eb7-aa15-9f7b6f91d3b2 @@ -51,12 +50,14 @@ def test_positive_create_with_permission(module_perms, function_role): :CaseImportance: Critical """ # Assign filter to created role - filter_ = make_filter({'role-id': function_role['id'], 'permissions': module_perms}) + filter_ = target_sat.cli_factory.make_filter( + {'role-id': function_role['id'], 'permissions': module_perms} + ) assert set(filter_['permissions'].split(", ")) == set(module_perms) @pytest.mark.tier1 -def test_positive_create_with_org(module_perms, function_role): +def test_positive_create_with_org(module_perms, function_role, target_sat): """Create a filter and assign it some permissions. :id: f6308192-0e1f-427b-a296-b285f6684691 @@ -67,9 +68,9 @@ def test_positive_create_with_org(module_perms, function_role): :CaseImportance: Critical """ - org = make_org() + org = target_sat.cli_factory.make_org() # Assign filter to created role - filter_ = make_filter( + filter_ = target_sat.cli_factory.make_filter( { 'role-id': function_role['id'], 'permissions': module_perms, @@ -82,7 +83,7 @@ def test_positive_create_with_org(module_perms, function_role): @pytest.mark.tier1 -def test_positive_create_with_loc(module_perms, function_role): +def test_positive_create_with_loc(module_perms, function_role, module_target_sat): """Create a filter and assign it some permissions. :id: d7d1969a-cb30-4e97-a9a3-3a4aaf608795 @@ -93,9 +94,9 @@ def test_positive_create_with_loc(module_perms, function_role): :CaseImportance: Critical """ - loc = make_location() + loc = module_target_sat.cli_factory.make_location() # Assign filter to created role - filter_ = make_filter( + filter_ = module_target_sat.cli_factory.make_filter( { 'role-id': function_role['id'], 'permissions': module_perms, @@ -108,7 +109,7 @@ def test_positive_create_with_loc(module_perms, function_role): @pytest.mark.tier1 -def test_positive_delete(module_perms, function_role): +def test_positive_delete(module_perms, function_role, module_target_sat): """Create a filter and delete it afterwards. :id: 97d1093c-0d49-454b-86f6-f5be87b32775 @@ -117,15 +118,17 @@ def test_positive_delete(module_perms, function_role): :CaseImportance: Critical """ - filter_ = make_filter({'role-id': function_role['id'], 'permissions': module_perms}) - Filter.delete({'id': filter_['id']}) + filter_ = module_target_sat.cli_factory.make_filter( + {'role-id': function_role['id'], 'permissions': module_perms} + ) + module_target_sat.cli.Filter.delete({'id': filter_['id']}) with pytest.raises(CLIReturnCodeError): - Filter.info({'id': filter_['id']}) + module_target_sat.cli.Filter.info({'id': filter_['id']}) @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_delete_role(module_perms, function_role): +def test_positive_delete_role(module_perms, function_role, target_sat): """Create a filter and delete the role it points at. :id: e2adb6a4-e408-4912-a32d-2bf2c43187d9 @@ -134,19 +137,21 @@ def test_positive_delete_role(module_perms, function_role): :CaseImportance: Critical """ - filter_ = make_filter({'role-id': function_role['id'], 'permissions': module_perms}) + filter_ = target_sat.cli_factory.make_filter( + {'role-id': function_role['id'], 'permissions': module_perms} + ) # A filter depends on a role. Deleting a role implicitly deletes the # filter pointing at it. - Role.delete({'id': function_role['id']}) + target_sat.cli.Role.delete({'id': function_role['id']}) with pytest.raises(CLIReturnCodeError): - Role.info({'id': function_role['id']}) + target_sat.cli.Role.info({'id': function_role['id']}) with pytest.raises(CLIReturnCodeError): - Filter.info({'id': filter_['id']}) + target_sat.cli.Filter.info({'id': filter_['id']}) @pytest.mark.tier1 -def test_positive_update_permissions(module_perms, function_role): +def test_positive_update_permissions(module_perms, function_role, target_sat): """Create a filter and update its permissions. :id: 3d6a52d8-2f8f-4f97-a155-9b52888af16e @@ -155,18 +160,22 @@ def test_positive_update_permissions(module_perms, function_role): :CaseImportance: Critical """ - filter_ = make_filter({'role-id': function_role['id'], 'permissions': module_perms}) + filter_ = target_sat.cli_factory.make_filter( + {'role-id': function_role['id'], 'permissions': module_perms} + ) new_perms = [ permission['name'] - for permission in Filter.available_permissions({"search": "resource_type=User"}) + for permission in target_sat.cli.Filter.available_permissions( + {"search": "resource_type=User"} + ) ] - Filter.update({'id': filter_['id'], 'permissions': new_perms}) - filter_ = Filter.info({'id': filter_['id']}) + target_sat.cli.Filter.update({'id': filter_['id'], 'permissions': new_perms}) + filter_ = target_sat.cli.Filter.info({'id': filter_['id']}) assert set(filter_['permissions'].split(", ")) == set(new_perms) @pytest.mark.tier1 -def test_positive_update_role(module_perms, function_role): +def test_positive_update_role(module_perms, function_role, target_sat): """Create a filter and assign it to another role. :id: 2950b3a1-2bce-447f-9df2-869b1d10eaf5 @@ -175,16 +184,18 @@ def test_positive_update_role(module_perms, function_role): :CaseImportance: Critical """ - filter_ = make_filter({'role-id': function_role['id'], 'permissions': module_perms}) + filter_ = target_sat.cli_factory.make_filter( + {'role-id': function_role['id'], 'permissions': module_perms} + ) # Update with another role - new_role = make_role() - Filter.update({'id': filter_['id'], 'role-id': new_role['id']}) - filter_ = Filter.info({'id': filter_['id']}) + new_role = target_sat.cli_factory.make_role() + target_sat.cli.Filter.update({'id': filter_['id'], 'role-id': new_role['id']}) + filter_ = target_sat.cli.Filter.info({'id': filter_['id']}) assert filter_['role'] == new_role['name'] @pytest.mark.tier1 -def test_positive_update_org_loc(module_perms, function_role): +def test_positive_update_org_loc(module_perms, function_role, target_sat): """Create a filter and assign it to another organization and location. :id: 9bb59109-9701-4ef3-95c6-81f387d372da @@ -195,9 +206,9 @@ def test_positive_update_org_loc(module_perms, function_role): :CaseImportance: Critical """ - org = make_org() - loc = make_location() - filter_ = make_filter( + org = target_sat.cli_factory.make_org() + loc = target_sat.cli_factory.make_location() + filter_ = target_sat.cli_factory.make_filter( { 'role-id': function_role['id'], 'permissions': module_perms, @@ -207,9 +218,9 @@ def test_positive_update_org_loc(module_perms, function_role): } ) # Update org and loc - new_org = make_org() - new_loc = make_location() - Filter.update( + new_org = target_sat.cli_factory.make_org() + new_loc = target_sat.cli_factory.make_location() + target_sat.cli.Filter.update( { 'id': filter_['id'], 'permissions': module_perms, @@ -218,7 +229,7 @@ def test_positive_update_org_loc(module_perms, function_role): 'override': 1, } ) - filter_ = Filter.info({'id': filter_['id']}) + filter_ = target_sat.cli.Filter.info({'id': filter_['id']}) # We expect here only one organization and location assert filter_['organizations'][0] == new_org['name'] assert filter_['locations'][0] == new_loc['name'] diff --git a/tests/foreman/cli/test_globalparam.py b/tests/foreman/cli/test_globalparam.py index 4fb6b1949cb..2a5d49cf966 100644 --- a/tests/foreman/cli/test_globalparam.py +++ b/tests/foreman/cli/test_globalparam.py @@ -21,13 +21,11 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.globalparam import GlobalParameter - pytestmark = [pytest.mark.tier1] @pytest.mark.upgrade -def test_positive_list_delete_by_name(): +def test_positive_list_delete_by_name(module_target_sat): """Test Global Param List :id: 8dd6c4e8-4ec9-4bee-8a04-f5788960973a @@ -40,13 +38,13 @@ def test_positive_list_delete_by_name(): value = f'val-{alphastring()} {alphastring()}' # Create - GlobalParameter().set({'name': name, 'value': value}) + module_target_sat.cli.GlobalParameter().set({'name': name, 'value': value}) # List by name - result = GlobalParameter().list({'search': name}) + result = module_target_sat.cli.GlobalParameter().list({'search': name}) assert len(result) == 1 assert result[0]['value'] == value # Delete - GlobalParameter().delete({'name': name}) - assert len(GlobalParameter().list({'search': name})) == 0 + module_target_sat.cli.GlobalParameter().delete({'name': name}) + assert len(module_target_sat.cli.GlobalParameter().list({'search': name})) == 0 diff --git a/tests/foreman/cli/test_host.py b/tests/foreman/cli/test_host.py index f687b785231..14d0e023c30 100644 --- a/tests/foreman/cli/test_host.py +++ b/tests/foreman/cli/test_host.py @@ -24,19 +24,6 @@ from wait_for import TimedOutError, wait_for import yaml -from robottelo.cli.activationkey import ActivationKey -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import ( - CLIFactoryError, - add_role_permissions, - make_fake_host, - make_host, - setup_org_for_a_rh_repo, -) -from robottelo.cli.host import Host, HostInterface, HostTraces -from robottelo.cli.job_invocation import JobInvocation -from robottelo.cli.package import Package -from robottelo.cli.user import User from robottelo.config import settings from robottelo.constants import ( DEFAULT_SUBSCRIPTION_NAME, @@ -51,6 +38,7 @@ REPOSET, SM_OVERALL_STATUS, ) +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.hosts import ContentHostError from robottelo.logging import logger from robottelo.utils.datafactory import ( @@ -72,7 +60,7 @@ def function_host(target_sat): host_template = target_sat.api.Host() host_template.create_missing() # using CLI to create host - host = make_host( + host = target_sat.cli_factory.make_host( { 'architecture-id': host_template.architecture.id, 'domain-id': host_template.domain.id, @@ -87,7 +75,7 @@ def function_host(target_sat): } ) yield host - Host.delete({'id': host['id']}) + target_sat.cli.Host.delete({'id': host['id']}) @pytest.fixture @@ -261,7 +249,7 @@ def parse_cli_entity_list_help_message(help_message): return parsed_dict -def test_positive_search_all_field_sets(): +def test_positive_search_all_field_sets(module_target_sat): """All fields in predefined field sets from hammer host list --help message are shown when specified as --fields in hammer host list command Note: host was created, so we there will always be at least 1 host @@ -283,12 +271,14 @@ def test_positive_search_all_field_sets(): :customerscenario: true """ - new_host = make_fake_host() - host_help_yaml = Host.list(options={'help': ''}, output_format='yaml') + new_host = module_target_sat.cli_factory.make_fake_host() + host_help_yaml = module_target_sat.cli.Host.list(options={'help': ''}, output_format='yaml') host_help = yaml.load(host_help_yaml, yaml.SafeLoader) parsed_dict = parse_cli_entity_list_help_message(host_help[':message']) help_field_sets = parsed_dict['Predefined field sets'] - output_field_sets = Host.list(options={'fields': ','.join(help_field_sets)}) + output_field_sets = module_target_sat.cli.Host.list( + options={'fields': ','.join(help_field_sets)} + ) # get list index of the created host in the output_field_sets [host_idx] = [idx for idx, host in enumerate(output_field_sets) if new_host['id'] == host['id']] @@ -324,7 +314,7 @@ def test_positive_create_and_delete(target_sat, module_lce_library, module_publi 'type=interface,mac={},identifier=eth0,name={},domain_id={},' 'ip={},primary=true,provision=true' ).format(host.mac, gen_string('alpha'), host.domain.id, gen_ipaddr()) - new_host = make_host( + new_host = target_sat.cli_factory.make_host( { 'architecture-id': host.architecture.id, 'content-view-id': module_published_cv.id, @@ -347,14 +337,14 @@ def test_positive_create_and_delete(target_sat, module_lce_library, module_publi assert ( new_host['content-information']['lifecycle-environment']['name'] == module_lce_library.name ) - host_interface = HostInterface.info( + host_interface = target_sat.cli.HostInterface.info( {'host-id': new_host['id'], 'id': new_host['network-interfaces'][0]['id']} ) assert host_interface['domain'] == host.domain.read().name - Host.delete({'id': new_host['id']}) + target_sat.cli.Host.delete({'id': new_host['id']}) with pytest.raises(CLIReturnCodeError): - Host.info({'id': new_host['id']}) + target_sat.cli.Host.info({'id': new_host['id']}) @pytest.mark.e2e @@ -373,14 +363,14 @@ def test_positive_crud_interface_by_id(target_sat, default_location, default_org domain = target_sat.api.Domain(location=[default_location], organization=[default_org]).create() mac = gen_mac(multicast=False) - host = make_fake_host({'domain-id': domain.id}) - number_of_interfaces = len(HostInterface.list({'host-id': host['id']})) + host = target_sat.cli_factory.make_fake_host({'domain-id': domain.id}) + number_of_interfaces = len(target_sat.cliHostInterface.list({'host-id': host['id']})) - HostInterface.create( + target_sat.cliHostInterface.create( {'host-id': host['id'], 'domain-id': domain.id, 'mac': mac, 'type': 'interface'} ) - host = Host.info({'id': host['id']}) - host_interface = HostInterface.info( + host = target_sat.cli.Host.info({'id': host['id']}) + host_interface = target_sat.cliHostInterface.info( { 'host-id': host['id'], 'id': [ni for ni in host['network-interfaces'] if ni['mac-address'] == mac][0]['id'], @@ -388,13 +378,15 @@ def test_positive_crud_interface_by_id(target_sat, default_location, default_org ) assert host_interface['domain'] == domain.name assert host_interface['mac-address'] == mac - assert len(HostInterface.list({'host-id': host['id']})) == number_of_interfaces + 1 + assert ( + len(target_sat.cliHostInterface.list({'host-id': host['id']})) == number_of_interfaces + 1 + ) new_domain = target_sat.api.Domain( location=[default_location], organization=[default_org] ).create() new_mac = gen_mac(multicast=False) - HostInterface.update( + target_sat.cliHostInterface.update( { 'host-id': host['id'], 'id': host_interface['id'], @@ -402,7 +394,7 @@ def test_positive_crud_interface_by_id(target_sat, default_location, default_org 'mac': new_mac, } ) - host_interface = HostInterface.info( + host_interface = target_sat.cliHostInterface.info( { 'host-id': host['id'], 'id': [ni for ni in host['network-interfaces'] if ni['mac-address'] == mac][0]['id'], @@ -411,15 +403,17 @@ def test_positive_crud_interface_by_id(target_sat, default_location, default_org assert host_interface['domain'] == new_domain.name assert host_interface['mac-address'] == new_mac - HostInterface.delete({'host-id': host['id'], 'id': host_interface['id']}) - assert len(HostInterface.list({'host-id': host['id']})) == number_of_interfaces + target_sat.cliHostInterface.delete({'host-id': host['id'], 'id': host_interface['id']}) + assert len(target_sat.cliHostInterface.list({'host-id': host['id']})) == number_of_interfaces with pytest.raises(CLIReturnCodeError): - HostInterface.info({'host-id': host['id'], 'id': host_interface['id']}) + target_sat.cliHostInterface.info({'host-id': host['id'], 'id': host_interface['id']}) @pytest.mark.cli_host_create @pytest.mark.tier2 -def test_negative_create_with_content_source(module_lce_library, module_org, module_published_cv): +def test_negative_create_with_content_source( + module_lce_library, module_org, module_published_cv, module_target_sat +): """Attempt to create a host with invalid content source specified :id: d92d6aff-4ad3-467c-88a8-5a5e56614f58 @@ -431,7 +425,7 @@ def test_negative_create_with_content_source(module_lce_library, module_org, mod :CaseImportance: Medium """ with pytest.raises(CLIFactoryError): - make_fake_host( + module_target_sat.cli_factory.make_fake_host( { 'content-source-id': gen_integer(10000, 99999), 'content-view-id': module_published_cv.id, @@ -444,7 +438,7 @@ def test_negative_create_with_content_source(module_lce_library, module_org, mod @pytest.mark.cli_host_create @pytest.mark.tier2 def test_negative_update_content_source( - module_default_proxy, module_lce_library, module_org, module_published_cv + module_default_proxy, module_lce_library, module_org, module_published_cv, module_target_sat ): """Attempt to update host's content source with invalid value @@ -459,7 +453,7 @@ def test_negative_update_content_source( :CaseImportance: Medium """ - host = make_fake_host( + host = module_target_sat.cli_factory.make_fake_host( { 'content-source-id': module_default_proxy['id'], 'content-view-id': module_published_cv.id, @@ -468,14 +462,18 @@ def test_negative_update_content_source( } ) with pytest.raises(CLIReturnCodeError): - Host.update({'id': host['id'], 'content-source-id': gen_integer(10000, 99999)}) - host = Host.info({'id': host['id']}) + module_target_sat.cli.Host.update( + {'id': host['id'], 'content-source-id': gen_integer(10000, 99999)} + ) + host = module_target_sat.cli.Host.info({'id': host['id']}) assert host['content-information']['content-source']['name'] == module_default_proxy['name'] @pytest.mark.cli_host_create @pytest.mark.tier1 -def test_positive_create_with_lce_and_cv(module_lce, module_org, module_promoted_cv): +def test_positive_create_with_lce_and_cv( + module_lce, module_org, module_promoted_cv, module_target_sat +): """Check if host can be created with new lifecycle and new content view @@ -488,7 +486,7 @@ def test_positive_create_with_lce_and_cv(module_lce, module_org, module_promoted :CaseImportance: Critical """ - new_host = make_fake_host( + new_host = module_target_sat.cli_factory.make_fake_host( { 'content-view-id': module_promoted_cv.id, 'lifecycle-environment-id': module_lce.id, @@ -501,7 +499,9 @@ def test_positive_create_with_lce_and_cv(module_lce, module_org, module_promoted @pytest.mark.cli_host_create @pytest.mark.tier2 -def test_positive_create_with_openscap_proxy_id(module_default_proxy, module_org): +def test_positive_create_with_openscap_proxy_id( + module_default_proxy, module_org, module_target_sat +): """Check if host can be created with OpenSCAP Proxy id :id: 3774ba08-3b18-4e64-b07f-53f6aa0504f3 @@ -510,7 +510,7 @@ def test_positive_create_with_openscap_proxy_id(module_default_proxy, module_org :CaseImportance: Medium """ - host = make_fake_host( + host = module_target_sat.cli_factory.make_fake_host( {'organization-id': module_org.id, 'openscap-proxy-id': module_default_proxy['id']} ) assert host['openscap-proxy'] == module_default_proxy['id'] @@ -518,7 +518,9 @@ def test_positive_create_with_openscap_proxy_id(module_default_proxy, module_org @pytest.mark.cli_host_create @pytest.mark.tier1 -def test_negative_create_with_name(module_lce_library, module_org, module_published_cv): +def test_negative_create_with_name( + module_lce_library, module_org, module_published_cv, module_target_sat +): """Check if host can be created with random long names :id: f92b6070-b2d1-4e3e-975c-39f1b1096697 @@ -529,7 +531,7 @@ def test_negative_create_with_name(module_lce_library, module_org, module_publis """ name = gen_choice(invalid_values_list()) with pytest.raises(CLIFactoryError): - make_fake_host( + module_target_sat.cli_factory.make_fake_host( { 'name': name, 'organization-id': module_org.id, @@ -541,7 +543,7 @@ def test_negative_create_with_name(module_lce_library, module_org, module_publis @pytest.mark.cli_host_create @pytest.mark.tier1 -def test_negative_create_with_unpublished_cv(module_lce, module_org, module_cv): +def test_negative_create_with_unpublished_cv(module_lce, module_org, module_cv, module_target_sat): """Check if host can be created using unpublished cv :id: 9997383d-3c27-4f14-94f9-4b8b51180eb6 @@ -551,7 +553,7 @@ def test_negative_create_with_unpublished_cv(module_lce, module_org, module_cv): :CaseImportance: Critical """ with pytest.raises(CLIFactoryError): - make_fake_host( + module_target_sat.cli_factory.make_fake_host( { 'content-view-id': module_cv.id, 'lifecycle-environment-id': module_lce.id, @@ -563,7 +565,7 @@ def test_negative_create_with_unpublished_cv(module_lce, module_org, module_cv): @pytest.mark.cli_host_create @pytest.mark.tier3 @pytest.mark.upgrade -def test_positive_katello_and_openscap_loaded(): +def test_positive_katello_and_openscap_loaded(target_sat): """Verify that command line arguments from both Katello and foreman_openscap plugins are loaded and available at the same time @@ -582,7 +584,7 @@ def test_positive_katello_and_openscap_loaded(): :BZ: 1671148 """ - help_output = Host.execute('host update --help') + help_output = target_sat.cli.Host.execute('host update --help') for arg in ['lifecycle-environment[-id]', 'openscap-proxy-id']: assert any( f'--{arg}' in line for line in help_output.split('\n') @@ -607,11 +609,11 @@ def test_positive_list_and_unregister( """ rhel7_contenthost.register(module_org, None, module_ak_with_cv.name, target_sat) assert rhel7_contenthost.subscribed - hosts = Host.list({'organization-id': module_org.id}) + hosts = target_sat.cli.Host.list({'organization-id': module_org.id}) assert rhel7_contenthost.hostname in [host['name'] for host in hosts] result = rhel7_contenthost.unregister() assert result.status == 0 - hosts = Host.list({'organization-id': module_org.id}) + hosts = target_sat.cli.Host.list({'organization-id': module_org.id}) assert rhel7_contenthost.hostname in [host['name'] for host in hosts] @@ -640,7 +642,9 @@ def test_positive_list_by_last_checkin( lce=f'{module_lce.label}/{module_promoted_cv.label}', ) assert rhel7_contenthost.subscribed - hosts = Host.list({'search': 'last_checkin = "Today" or last_checkin = "Yesterday"'}) + hosts = target_sat.cli.Host.list( + {'search': 'last_checkin = "Today" or last_checkin = "Yesterday"'} + ) assert len(hosts) >= 1 assert rhel7_contenthost.hostname in [host['name'] for host in hosts] @@ -666,15 +670,15 @@ def test_positive_list_infrastructure_hosts( lce=f'{module_lce.label}/{module_promoted_cv.label}', ) assert rhel7_contenthost.subscribed - Host.update({'name': target_sat.hostname, 'new-organization-id': module_org.id}) + target_sat.cli.Host.update({'name': target_sat.hostname, 'new-organization-id': module_org.id}) # list satellite hosts - hosts = Host.list({'search': 'infrastructure_facet.foreman=true'}) + hosts = target_sat.cli.Host.list({'search': 'infrastructure_facet.foreman=true'}) assert len(hosts) == 2 if is_open('BZ:1994685') else len(hosts) == 1 hostnames = [host['name'] for host in hosts] assert rhel7_contenthost.hostname not in hostnames assert target_sat.hostname in hostnames # list capsule hosts - hosts = Host.list({'search': 'infrastructure_facet.smart_proxy_id=1'}) + hosts = target_sat.cli.Host.list({'search': 'infrastructure_facet.smart_proxy_id=1'}) hostnames = [host['name'] for host in hosts] assert len(hosts) == 2 if is_open('BZ:1994685') else len(hosts) == 1 assert rhel7_contenthost.hostname not in hostnames @@ -703,7 +707,9 @@ def test_positive_create_inherit_lce_cv( lifecycle_environment=module_lce_library, organization=[module_org], ).create() - host = make_fake_host({'hostgroup-id': hostgroup.id, 'organization-id': module_org.id}) + host = target_sat.cli_factory.make_fake_host( + {'hostgroup-id': hostgroup.id, 'organization-id': module_org.id} + ) assert ( int(host['content-information']['lifecycle-environment']['id']) == hostgroup.lifecycle_environment.id @@ -758,7 +764,7 @@ def test_positive_create_inherit_nested_hostgroup(target_sat): ).create() nested_hostgroups.append(nested_hg) - host = make_host( + host = target_sat.cli_factory.make_host( { 'hostgroup-title': f'{parent_hostgroups[0].name}/{nested_hostgroups[0].name}', 'location-id': options.location.id, @@ -812,7 +818,7 @@ def test_positive_list_with_nested_hostgroup(target_sat): organization=[options.organization], parent=parent_hg, ).create() - make_host( + target_sat.cli_factory.make_host( { 'hostgroup-id': nested_hg.id, 'location-id': options.location.id, @@ -820,9 +826,9 @@ def test_positive_list_with_nested_hostgroup(target_sat): 'name': host_name, } ) - hosts = Host.list({'organization-id': options.organization.id}) + hosts = target_sat.cli.Host.list({'organization-id': options.organization.id}) assert f'{parent_hg_name}/{nested_hg_name}' == hosts[0]['host-group'] - host = Host.info({'id': hosts[0]['id']}) + host = target_sat.cli.Host.info({'id': hosts[0]['id']}) logger.info(f'Host info: {host}') assert host['operating-system']['medium'] == options.medium.name assert host['operating-system']['partition-table'] == options.ptable.name # inherited @@ -910,7 +916,7 @@ def test_positive_update_parameters_by_name( organization=[organization], operatingsystem=[new_os], ).create() - Host.update( + target_sat.cli.Host.update( { 'architecture': module_architecture.name, 'domain': new_domain.name, @@ -922,7 +928,7 @@ def test_positive_update_parameters_by_name( 'new-location-id': new_loc.id, } ) - host = Host.info({'id': function_host['id']}) + host = target_sat.cli.Host.info({'id': function_host['id']}) assert '{}.{}'.format(new_name, host['network']['domain']) == host['name'] assert host['location'] == new_loc.name assert host['network']['mac'] == new_mac @@ -934,7 +940,7 @@ def test_positive_update_parameters_by_name( @pytest.mark.tier1 @pytest.mark.cli_host_update -def test_negative_update_name(function_host): +def test_negative_update_name(function_host, target_sat): """A host can not be updated with invalid or empty name :id: e8068d2a-6a51-4627-908b-60a516c67032 @@ -945,14 +951,14 @@ def test_negative_update_name(function_host): """ new_name = gen_choice(invalid_values_list()) with pytest.raises(CLIReturnCodeError): - Host.update({'id': function_host['id'], 'new-name': new_name}) - host = Host.info({'id': function_host['id']}) + target_sat.cli.Host.update({'id': function_host['id'], 'new-name': new_name}) + host = target_sat.cli.Host.info({'id': function_host['id']}) assert '{}.{}'.format(new_name, host['network']['domain']).lower() != host['name'] @pytest.mark.tier1 @pytest.mark.cli_host_update -def test_negative_update_mac(function_host): +def test_negative_update_mac(function_host, target_sat): """A host can not be updated with invalid or empty MAC address :id: 2f03032d-789d-419f-9ff2-a6f3561444da @@ -963,14 +969,14 @@ def test_negative_update_mac(function_host): """ new_mac = gen_choice(invalid_values_list()) with pytest.raises(CLIReturnCodeError): - Host.update({'id': function_host['id'], 'mac': new_mac}) - host = Host.info({'id': function_host['id']}) + target_sat.cli.Host.update({'id': function_host['id'], 'mac': new_mac}) + host = target_sat.cli.Host.info({'id': function_host['id']}) assert host['network']['mac'] != new_mac @pytest.mark.tier2 @pytest.mark.cli_host_update -def test_negative_update_arch(function_host, module_architecture): +def test_negative_update_arch(function_host, module_architecture, target_sat): """A host can not be updated with a architecture, which does not belong to host's operating system @@ -981,8 +987,10 @@ def test_negative_update_arch(function_host, module_architecture): :CaseLevel: Integration """ with pytest.raises(CLIReturnCodeError): - Host.update({'architecture': module_architecture.name, 'id': function_host['id']}) - host = Host.info({'id': function_host['id']}) + target_sat.cli.Host.update( + {'architecture': module_architecture.name, 'id': function_host['id']} + ) + host = target_sat.cli.Host.info({'id': function_host['id']}) assert host['operating-system']['architecture'] != module_architecture.name @@ -1007,14 +1015,14 @@ def test_negative_update_os(target_sat, function_host, module_architecture): ptable=[p_table.id], ).create() with pytest.raises(CLIReturnCodeError): - Host.update( + target_sat.cli.Host.update( { 'architecture': module_architecture.name, 'id': function_host['id'], 'operatingsystem': new_os.title, } ) - host = Host.info({'id': function_host['id']}) + host = target_sat.cli.Host.info({'id': function_host['id']}) assert host['operating-system']['operating-system'] != new_os.title @@ -1042,25 +1050,27 @@ def test_hammer_host_info_output(target_sat, module_user): user = target_sat.api.User().search( query={'search': f'login={settings.server.admin_username}'} )[0] - Host.update({'owner': settings.server.admin_username, 'owner-type': 'User', 'id': '1'}) - result_info = Host.info(options={'id': '1', 'fields': 'Additional info'}) + target_sat.cli.Host.update( + {'owner': settings.server.admin_username, 'owner-type': 'User', 'id': '1'} + ) + result_info = target_sat.cli.Host.info(options={'id': '1', 'fields': 'Additional info'}) assert int(result_info['additional-info']['owner-id']) == user.id - host = Host.info({'id': '1'}) - User.update( + host = target_sat.cli.Host.info({'id': '1'}) + target_sat.cli.User.update( { 'id': module_user.id, 'organizations': [host['organization']], 'locations': [host['location']], } ) - Host.update({'owner-id': module_user.id, 'id': '1'}) - result_info = Host.info(options={'id': '1', 'fields': 'Additional info'}) + target_sat.cli.Host.update({'owner-id': module_user.id, 'id': '1'}) + result_info = target_sat.cli.Host.info(options={'id': '1', 'fields': 'Additional info'}) assert int(result_info['additional-info']['owner-id']) == module_user.id @pytest.mark.cli_host_parameter @pytest.mark.tier1 -def test_positive_parameter_crud(function_host): +def test_positive_parameter_crud(function_host, target_sat): """Add, update and remove host parameter with valid name. :id: 76034424-cf18-4ced-916b-ee9798c311bc @@ -1072,26 +1082,28 @@ def test_positive_parameter_crud(function_host): """ name = next(iter(valid_data_list())) value = valid_data_list()[name] - Host.set_parameter({'host-id': function_host['id'], 'name': name, 'value': value}) - host = Host.info({'id': function_host['id']}) + target_sat.cli.Host.set_parameter( + {'host-id': function_host['id'], 'name': name, 'value': value} + ) + host = target_sat.cli.Host.info({'id': function_host['id']}) assert name in host['parameters'].keys() assert value == host['parameters'][name] new_value = valid_data_list()[name] - Host.set_parameter({'host-id': host['id'], 'name': name, 'value': new_value}) - host = Host.info({'id': host['id']}) + target_sat.cli.Host.set_parameter({'host-id': host['id'], 'name': name, 'value': new_value}) + host = target_sat.cli.Host.info({'id': host['id']}) assert name in host['parameters'].keys() assert new_value == host['parameters'][name] - Host.delete_parameter({'host-id': host['id'], 'name': name}) - host = Host.info({'id': host['id']}) + target_sat.cli.Host.delete_parameter({'host-id': host['id'], 'name': name}) + host = target_sat.cli.Host.info({'id': host['id']}) assert name not in host['parameters'].keys() # -------------------------- HOST PARAMETER SCENARIOS ------------------------- @pytest.mark.cli_host_parameter @pytest.mark.tier1 -def test_negative_add_parameter(function_host): +def test_negative_add_parameter(function_host, target_sat): """Try to add host parameter with different invalid names. :id: 473f8c3f-b66e-4526-88af-e139cc3dabcb @@ -1103,14 +1115,14 @@ def test_negative_add_parameter(function_host): """ name = gen_choice(invalid_values_list()).lower() with pytest.raises(CLIReturnCodeError): - Host.set_parameter( + target_sat.cli.Host.set_parameter( { 'host-id': function_host['id'], 'name': name, 'value': gen_string('alphanumeric'), } ) - host = Host.info({'id': function_host['id']}) + host = target_sat.cli.Host.info({'id': function_host['id']}) assert name not in host['parameters'].keys() @@ -1138,19 +1150,21 @@ def test_negative_view_parameter_by_non_admin_user(target_sat, function_host, fu """ param_name = gen_string('alpha').lower() param_value = gen_string('alphanumeric') - Host.set_parameter({'host-id': function_host['id'], 'name': param_name, 'value': param_value}) - host = Host.info({'id': function_host['id']}) + target_sat.cli.Host.set_parameter( + {'host-id': function_host['id'], 'name': param_name, 'value': param_value} + ) + host = target_sat.cli.Host.info({'id': function_host['id']}) assert host['parameters'][param_name] == param_value role = target_sat.api.Role(name=gen_string('alphanumeric')).create() - add_role_permissions( + target_sat.cli_factory.add_role_permissions( role.id, resource_permissions={ 'Host': {'permissions': ['view_hosts']}, 'Organization': {'permissions': ['view_organizations']}, }, ) - User.add_role({'id': function_user['user'].id, 'role-id': role.id}) - host = Host.with_user( + target_sat.cli.User.add_role({'id': function_user['user'].id, 'role-id': role.id}) + host = target_sat.cli.Host.with_user( username=function_user['user'].login, password=function_user['password'] ).info({'id': host['id']}) assert not host.get('parameters') @@ -1181,11 +1195,13 @@ def test_positive_view_parameter_by_non_admin_user(target_sat, function_host, fu """ param_name = gen_string('alpha').lower() param_value = gen_string('alphanumeric') - Host.set_parameter({'host-id': function_host['id'], 'name': param_name, 'value': param_value}) - host = Host.info({'id': function_host['id']}) + target_sat.cli.Host.set_parameter( + {'host-id': function_host['id'], 'name': param_name, 'value': param_value} + ) + host = target_sat.cli.Host.info({'id': function_host['id']}) assert host['parameters'][param_name] == param_value role = target_sat.api.Role(name=gen_string('alphanumeric')).create() - add_role_permissions( + target_sat.cli_factory.add_role_permissions( role.id, resource_permissions={ 'Host': {'permissions': ['view_hosts']}, @@ -1193,8 +1209,8 @@ def test_positive_view_parameter_by_non_admin_user(target_sat, function_host, fu 'Parameter': {'permissions': ['view_params']}, }, ) - User.add_role({'id': function_user['user'].id, 'role-id': role.id}) - host = Host.with_user( + target_sat.cli.User.add_role({'id': function_user['user'].id, 'role-id': role.id}) + host = target_sat.cli.Host.with_user( username=function_user['user'].login, password=function_user['password'] ).info({'id': host['id']}) assert param_name in host['parameters'] @@ -1226,11 +1242,13 @@ def test_negative_edit_parameter_by_non_admin_user(target_sat, function_host, fu """ param_name = gen_string('alpha').lower() param_value = gen_string('alphanumeric') - Host.set_parameter({'host-id': function_host['id'], 'name': param_name, 'value': param_value}) - host = Host.info({'id': function_host['id']}) + target_sat.cli.Host.set_parameter( + {'host-id': function_host['id'], 'name': param_name, 'value': param_value} + ) + host = target_sat.cli.Host.info({'id': function_host['id']}) assert host['parameters'][param_name] == param_value role = target_sat.api.Role(name=gen_string('alphanumeric')).create() - add_role_permissions( + target_sat.cli_factory.add_role_permissions( role.id, resource_permissions={ 'Host': {'permissions': ['view_hosts']}, @@ -1238,21 +1256,21 @@ def test_negative_edit_parameter_by_non_admin_user(target_sat, function_host, fu 'Parameter': {'permissions': ['view_params']}, }, ) - User.add_role({'id': function_user['user'].id, 'role-id': role.id}) + target_sat.cli.User.add_role({'id': function_user['user'].id, 'role-id': role.id}) param_new_value = gen_string('alphanumeric') with pytest.raises(CLIReturnCodeError): - Host.with_user( + target_sat.cli.Host.with_user( username=function_user['user'].login, password=function_user['password'] ).set_parameter( {'host-id': function_host['id'], 'name': param_name, 'value': param_new_value} ) - host = Host.info({'id': function_host['id']}) + host = target_sat.cli.Host.info({'id': function_host['id']}) assert host['parameters'][param_name] == param_value @pytest.mark.cli_host_parameter @pytest.mark.tier2 -def test_positive_set_multi_line_and_with_spaces_parameter_value(function_host): +def test_positive_set_multi_line_and_with_spaces_parameter_value(function_host, target_sat): """Check that host parameter value with multi-line and spaces is correctly restored from yaml format @@ -1276,15 +1294,17 @@ def test_positive_set_multi_line_and_with_spaces_parameter_value(function_host): 'account include password-auth' ) # count parameters of a host - response = Host.info( + response = target_sat.cli.Host.info( {'id': function_host['id']}, output_format='yaml', return_raw_response=True ) assert response.status == 0 yaml_content = yaml.load(response.stdout, yaml.SafeLoader) host_initial_params = yaml_content.get('Parameters') # set parameter - Host.set_parameter({'host-id': function_host['id'], 'name': param_name, 'value': param_value}) - response = Host.info( + target_sat.cli.Host.set_parameter( + {'host-id': function_host['id'], 'name': param_name, 'value': param_value} + ) + response = target_sat.cli.Host.info( {'id': function_host['id']}, output_format='yaml', return_raw_response=True ) assert response.status == 0 @@ -1570,7 +1590,9 @@ def yum_security_plugin(katello_host_tools_host): @pytest.mark.e2e @pytest.mark.cli_katello_host_tools @pytest.mark.tier3 -def test_positive_report_package_installed_removed(katello_host_tools_host, setup_custom_repo): +def test_positive_report_package_installed_removed( + katello_host_tools_host, setup_custom_repo, target_sat +): """Ensure installed/removed package is reported to satellite :id: fa5dc238-74c3-4c8a-aa6f-e0a91ba543e3 @@ -1596,18 +1618,18 @@ def test_positive_report_package_installed_removed(katello_host_tools_host, setu :CaseLevel: System """ client = katello_host_tools_host - host_info = Host.info({'name': client.hostname}) + host_info = target_sat.cli.Host.info({'name': client.hostname}) client.run(f'yum install -y {setup_custom_repo["package"]}') result = client.run(f'rpm -q {setup_custom_repo["package"]}') assert result.status == 0 - installed_packages = Host.package_list( + installed_packages = target_sat.cli.Host.package_list( {'host-id': host_info['id'], 'search': f'name={setup_custom_repo["package_name"]}'} ) assert len(installed_packages) == 1 assert installed_packages[0]['nvra'] == setup_custom_repo["package"] result = client.run(f'yum remove -y {setup_custom_repo["package"]}') assert result.status == 0 - installed_packages = Host.package_list( + installed_packages = target_sat.cli.Host.package_list( {'host-id': host_info['id'], 'search': f'name={setup_custom_repo["package_name"]}'} ) assert len(installed_packages) == 0 @@ -1615,7 +1637,7 @@ def test_positive_report_package_installed_removed(katello_host_tools_host, setu @pytest.mark.cli_katello_host_tools @pytest.mark.tier3 -def test_positive_package_applicability(katello_host_tools_host, setup_custom_repo): +def test_positive_package_applicability(katello_host_tools_host, setup_custom_repo, target_sat): """Ensure packages applicability is functioning properly :id: d283b65b-19c1-4eba-87ea-f929b0ee4116 @@ -1642,12 +1664,12 @@ def test_positive_package_applicability(katello_host_tools_host, setup_custom_re :CaseLevel: System """ client = katello_host_tools_host - host_info = Host.info({'name': client.hostname}) + host_info = target_sat.cli.Host.info({'name': client.hostname}) client.run(f'yum install -y {setup_custom_repo["package"]}') result = client.run(f'rpm -q {setup_custom_repo["package"]}') assert result.status == 0 applicable_packages, _ = wait_for( - lambda: Package.list( + lambda: target_sat.cli.Package.list( { 'host-id': host_info['id'], 'packages-restrict-applicable': 'true', @@ -1665,7 +1687,7 @@ def test_positive_package_applicability(katello_host_tools_host, setup_custom_re client.run(f'yum install -y {setup_custom_repo["new_package"]}') result = client.run(f'rpm -q {setup_custom_repo["new_package"]}') assert result.status == 0 - applicable_packages = Package.list( + applicable_packages = target_sat.cli.Package.list( { 'host-id': host_info['id'], 'packages-restrict-applicable': 'true', @@ -1681,7 +1703,7 @@ def test_positive_package_applicability(katello_host_tools_host, setup_custom_re @pytest.mark.pit_server @pytest.mark.tier3 def test_positive_erratum_applicability( - katello_host_tools_host, setup_custom_repo, yum_security_plugin + katello_host_tools_host, setup_custom_repo, yum_security_plugin, target_sat ): """Ensure erratum applicability is functioning properly @@ -1706,12 +1728,12 @@ def test_positive_erratum_applicability( :CaseLevel: System """ client = katello_host_tools_host - host_info = Host.info({'name': client.hostname}) + host_info = target_sat.cli.Host.info({'name': client.hostname}) client.run(f'yum install -y {setup_custom_repo["package"]}') result = client.run(f'rpm -q {setup_custom_repo["package"]}') client.subscription_manager_list_repos() applicable_errata, _ = wait_for( - lambda: Host.errata_list({'host-id': host_info['id']}), + lambda: target_sat.cli.Host.errata_list({'host-id': host_info['id']}), handle_exception=True, fail_condition=[], timeout=120, @@ -1733,7 +1755,7 @@ def test_positive_erratum_applicability( lambda: setup_custom_repo["security_errata"] not in [ errata['erratum-id'] - for errata in Host.errata_list({'host-id': host_info['id']}) + for errata in target_sat.cli.Host.errata_list({'host-id': host_info['id']}) if errata['installable'] == 'true' ], handle_exception=True, @@ -1749,7 +1771,7 @@ def test_positive_erratum_applicability( @pytest.mark.cli_katello_host_tools @pytest.mark.tier3 -def test_positive_apply_security_erratum(katello_host_tools_host, setup_custom_repo): +def test_positive_apply_security_erratum(katello_host_tools_host, setup_custom_repo, target_sat): """Apply security erratum to a host :id: 4d1095c8-d354-42ac-af44-adf6dbb46deb @@ -1766,12 +1788,12 @@ def test_positive_apply_security_erratum(katello_host_tools_host, setup_custom_r :parametrized: yes """ client = katello_host_tools_host - host_info = Host.info({'name': client.hostname}) + host_info = target_sat.cli.Host.info({'name': client.hostname}) client.run(f'yum install -y {setup_custom_repo["new_package"]}') client.run(f'yum downgrade -y {setup_custom_repo["package_name"]}') # Check that host has applicable errata host_erratum, _ = wait_for( - lambda: Host.errata_list({'host-id': host_info['id']})[0], + lambda: target_sat.cli.Host.errata_list({'host-id': host_info['id']})[0], handle_exception=True, timeout=120, delay=5, @@ -1802,10 +1824,10 @@ def test_positive_install_package_via_rex( :parametrized: yes """ client = katello_host_tools_host - host_info = Host.info({'name': client.hostname}) + host_info = target_sat.cli.Host.info({'name': client.hostname}) client.configure_rex(satellite=target_sat, org=module_org, register=False) # Apply errata to the host collection using job invocation - JobInvocation.create( + target_sat.cli.JobInvocation.create( { 'feature': 'katello_package_install', 'search-query': f'name ~ {client.hostname}', @@ -1815,7 +1837,7 @@ def test_positive_install_package_via_rex( ) result = client.run(f'rpm -q {setup_custom_repo["package"]}') assert result.status == 0 - installed_packages = Host.package_list( + installed_packages = target_sat.cli.Host.package_list( {'host-id': host_info['id'], 'search': f'name={setup_custom_repo["package_name"]}'} ) assert len(installed_packages) == 1 @@ -1847,7 +1869,12 @@ def ak_with_subscription( @pytest.mark.cli_host_subscription @pytest.mark.tier3 def test_positive_register( - module_org, module_promoted_cv, module_lce, module_ak_with_cv, host_subscription_client + module_org, + module_promoted_cv, + module_lce, + module_ak_with_cv, + host_subscription_client, + target_sat, ): """Attempt to register a host @@ -1859,14 +1886,14 @@ def test_positive_register( :CaseLevel: System """ - hosts = Host.list( + hosts = target_sat.cli.Host.list( { 'organization-id': module_org.id, 'search': host_subscription_client.hostname, } ) assert len(hosts) == 0 - Host.subscription_register( + target_sat.cli.Host.subscription_register( { 'organization-id': module_org.id, 'content-view-id': module_promoted_cv.id, @@ -1874,18 +1901,18 @@ def test_positive_register( 'name': host_subscription_client.hostname, } ) - hosts = Host.list( + hosts = target_sat.cli.Host.list( { 'organization-id': module_org.id, 'search': host_subscription_client.hostname, } ) assert len(hosts) > 0 - host = Host.info({'id': hosts[0]['id']}) + host = target_sat.cli.Host.info({'id': hosts[0]['id']}) assert host['name'] == host_subscription_client.hostname # note: when not registered the following command lead to exception, # see unregister - host_subscriptions = ActivationKey.subscriptions( + host_subscriptions = target_sat.cli.ActivationKey.subscriptions( { 'organization-id': module_org.id, 'id': module_ak_with_cv.id, @@ -1906,6 +1933,7 @@ def test_positive_attach( module_rhst_repo, default_subscription, host_subscription_client, + target_sat, ): """Attempt to attach a subscription to host @@ -1924,7 +1952,7 @@ def test_positive_attach( """ # create an activation key without subscriptions # register the client host - Host.subscription_register( + target_sat.cli.Host.subscription_register( { 'organization-id': module_org.id, 'content-view-id': module_promoted_cv.id, @@ -1932,13 +1960,13 @@ def test_positive_attach( 'name': host_subscription_client.hostname, } ) - host = Host.info({'name': host_subscription_client.hostname}) + host = target_sat.cli.Host.info({'name': host_subscription_client.hostname}) host_subscription_client.register_contenthost( module_org.name, activation_key=module_ak_with_cv.name ) assert host_subscription_client.subscribed # attach the subscription to host - Host.subscription_attach( + target_sat.cli.Host.subscription_attach( { 'host-id': host['id'], 'subscription-id': default_subscription.id, @@ -1962,6 +1990,7 @@ def test_positive_attach_with_lce( module_rhst_repo, default_subscription, host_subscription_client, + target_sat, ): """Attempt to attach a subscription to host, registered by lce @@ -1984,8 +2013,8 @@ def test_positive_attach_with_lce( auto_attach=False, ) assert host_subscription_client.subscribed - host = Host.info({'name': host_subscription_client.hostname}) - Host.subscription_attach( + host = target_sat.cli.Host.info({'name': host_subscription_client.hostname}) + target_sat.cli.Host.subscription_attach( { 'host-id': host['id'], 'subscription-id': default_subscription.id, @@ -2003,7 +2032,7 @@ def test_positive_attach_with_lce( @pytest.mark.cli_host_subscription @pytest.mark.tier3 def test_negative_without_attach( - module_org, module_promoted_cv, module_lce, host_subscription_client + module_org, module_promoted_cv, module_lce, host_subscription_client, target_sat ): """Register content host from satellite, register client to uuid of that content host, as there was no attach on the client, @@ -2017,7 +2046,7 @@ def test_negative_without_attach( :CaseLevel: System """ - Host.subscription_register( + target_sat.cli.Host.subscription_register( { 'organization-id': module_org.id, 'content-view-id': module_promoted_cv.id, @@ -2025,7 +2054,7 @@ def test_negative_without_attach( 'name': host_subscription_client.hostname, } ) - host = Host.info({'name': host_subscription_client.hostname}) + host = target_sat.cli.Host.info({'name': host_subscription_client.hostname}) host_subscription_client.register_contenthost( module_org.name, lce=None, # required, to jump into right branch in register_contenthost method @@ -2061,7 +2090,7 @@ def test_negative_without_attach_with_lce( environment=function_lce, organization=function_org, ).create() - setup_org_for_a_rh_repo( + target_sat.cli_factory.setup_org_for_a_rh_repo( { 'product': PRDS['rhel'], 'repository-set': REPOSET['rhst7'], @@ -2108,6 +2137,7 @@ def test_positive_remove( ak_with_subscription, default_subscription, host_subscription_client, + target_sat, ): """Attempt to remove a subscription from content host @@ -2119,7 +2149,7 @@ def test_positive_remove( :CaseLevel: System """ - Host.subscription_register( + target_sat.cli.Host.subscription_register( { 'organization-id': module_org.id, 'content-view-id': module_promoted_cv.id, @@ -2127,8 +2157,8 @@ def test_positive_remove( 'name': host_subscription_client.hostname, } ) - host = Host.info({'name': host_subscription_client.hostname}) - host_subscriptions = ActivationKey.subscriptions( + host = target_sat.cli.Host.info({'name': host_subscription_client.hostname}) + host_subscriptions = target_sat.cli.ActivationKey.subscriptions( { 'organization-id': module_org.id, 'id': ak_with_subscription.id, @@ -2140,13 +2170,13 @@ def test_positive_remove( host_subscription_client.register_contenthost( module_org.name, activation_key=ak_with_subscription.name ) - Host.subscription_attach( + target_sat.cli.Host.subscription_attach( { 'host-id': host['id'], 'subscription-id': default_subscription.id, } ) - host_subscriptions = ActivationKey.subscriptions( + host_subscriptions = target_sat.cli.ActivationKey.subscriptions( { 'organization-id': module_org.id, 'id': ak_with_subscription.id, @@ -2155,13 +2185,13 @@ def test_positive_remove( output_format='json', ) assert default_subscription.name in [sub['name'] for sub in host_subscriptions] - Host.subscription_remove( + target_sat.cli.Host.subscription_remove( { 'host-id': host['id'], 'subscription-id': default_subscription.id, } ) - host_subscriptions = ActivationKey.subscriptions( + host_subscriptions = target_sat.cli.ActivationKey.subscriptions( { 'organization-id': module_org.id, 'id': ak_with_subscription.id, @@ -2181,6 +2211,7 @@ def test_positive_auto_attach( module_rhst_repo, ak_with_subscription, host_subscription_client, + target_sat, ): """Attempt to auto attach a subscription to content host @@ -2193,7 +2224,7 @@ def test_positive_auto_attach( :CaseLevel: System """ - Host.subscription_register( + target_sat.cli.Host.subscription_register( { 'organization-id': module_org.id, 'content-view-id': module_promoted_cv.id, @@ -2201,11 +2232,11 @@ def test_positive_auto_attach( 'name': host_subscription_client.hostname, } ) - host = Host.info({'name': host_subscription_client.hostname}) + host = target_sat.cli.Host.info({'name': host_subscription_client.hostname}) host_subscription_client.register_contenthost( module_org.name, activation_key=ak_with_subscription.name ) - Host.subscription_auto_attach({'host-id': host['id']}) + target_sat.cli.Host.subscription_auto_attach({'host-id': host['id']}) host_subscription_client.enable_repo(module_rhst_repo) # ensure that katello agent can be installed try: @@ -2217,7 +2248,7 @@ def test_positive_auto_attach( @pytest.mark.cli_host_subscription @pytest.mark.tier3 def test_positive_unregister_host_subscription( - module_org, module_rhst_repo, ak_with_subscription, host_subscription_client + module_org, module_rhst_repo, ak_with_subscription, host_subscription_client, target_sat ): """Attempt to unregister host subscription @@ -2238,8 +2269,8 @@ def test_positive_unregister_host_subscription( host_subscription_client.run('subscription-manager attach --auto') host_subscription_client.enable_repo(module_rhst_repo) assert host_subscription_client.subscribed - host = Host.info({'name': host_subscription_client.hostname}) - host_subscriptions = ActivationKey.subscriptions( + host = target_sat.cli.Host.info({'name': host_subscription_client.hostname}) + host_subscriptions = target_sat.cli.ActivationKey.subscriptions( { 'organization-id': module_org.id, 'id': ak_with_subscription.id, @@ -2248,11 +2279,11 @@ def test_positive_unregister_host_subscription( output_format='json', ) assert len(host_subscriptions) > 0 - Host.subscription_unregister({'host': host_subscription_client.hostname}) + target_sat.cli.Host.subscription_unregister({'host': host_subscription_client.hostname}) with pytest.raises(CLIReturnCodeError): # raise error that the host was not registered by # subscription-manager register - ActivationKey.subscriptions( + target_sat.cli.ActivationKey.subscriptions( { 'organization-id': module_org.id, 'id': ak_with_subscription.id, @@ -2298,7 +2329,7 @@ def test_syspurpose_end_to_end( purpose_usage="test-usage", service_level="Self-Support", ).create() - ActivationKey.add_subscription( + target_sat.cli.ActivationKey.add_subscription( { 'organization-id': module_org.id, 'id': activation_key.id, @@ -2312,14 +2343,14 @@ def test_syspurpose_end_to_end( assert host_subscription_client.subscribed host_subscription_client.run('subscription-manager attach --auto') host_subscription_client.enable_repo(module_rhst_repo) - host = Host.info({'name': host_subscription_client.hostname}) + host = target_sat.cli.Host.info({'name': host_subscription_client.hostname}) # Assert system purpose values are set in the host as expected assert host['subscription-information']['system-purpose']['purpose-addons'] == purpose_addons assert host['subscription-information']['system-purpose']['purpose-role'] == "test-role" assert host['subscription-information']['system-purpose']['purpose-usage'] == "test-usage" assert host['subscription-information']['system-purpose']['service-level'] == "Self-Support" # Change system purpose values in the host - Host.update( + target_sat.cli.Host.update( { 'purpose-addons': "test-addon3", 'purpose-role': "test-role2", @@ -2328,13 +2359,13 @@ def test_syspurpose_end_to_end( 'id': host['id'], } ) - host = Host.info({'id': host['id']}) + host = target_sat.cli.Host.info({'id': host['id']}) # Assert system purpose values have been updated in the host as expected assert host['subscription-information']['system-purpose']['purpose-addons'] == "test-addon3" assert host['subscription-information']['system-purpose']['purpose-role'] == "test-role2" assert host['subscription-information']['system-purpose']['purpose-usage'] == "test-usage2" assert host['subscription-information']['system-purpose']['service-level'] == "Self-Support2" - host_subscriptions = ActivationKey.subscriptions( + host_subscriptions = target_sat.cli.ActivationKey.subscriptions( { 'organization-id': module_org.id, 'id': activation_key.id, @@ -2345,11 +2376,11 @@ def test_syspurpose_end_to_end( assert len(host_subscriptions) > 0 assert host_subscriptions[0]['name'] == default_subscription.name # Unregister host - Host.subscription_unregister({'host': host_subscription_client.hostname}) + target_sat.cli.Host.subscription_unregister({'host': host_subscription_client.hostname}) with pytest.raises(CLIReturnCodeError): # raise error that the host was not registered by # subscription-manager register - ActivationKey.subscriptions( + target_sat.cli.ActivationKey.subscriptions( { 'organization-id': module_org.id, 'id': activation_key.id, @@ -2372,8 +2403,8 @@ def test_positive_errata_list_of_sat_server(target_sat): :CaseImportance: Critical """ hostname = target_sat.execute('hostname').stdout.strip() - host = Host.info({'name': hostname}) - assert isinstance(Host.errata_list({'host-id': host['id']}), list) + host = target_sat.cli.Host.info({'name': hostname}) + assert isinstance(target_sat.cli.Host.errata_list({'host-id': host['id']}), list) # -------------------------- HOST ENC SUBCOMMAND SCENARIOS ------------------------- @@ -2391,7 +2422,7 @@ def test_positive_dump_enc_yaml(target_sat): :CaseImportance: Critical """ - enc_dump = Host.enc_dump({'name': target_sat.hostname}) + enc_dump = target_sat.cli.Host.enc_dump({'name': target_sat.hostname}) assert f'fqdn: {target_sat.hostname}' in enc_dump assert f'ip: {target_sat.ip_addr}' in enc_dump assert 'ssh-rsa' in enc_dump @@ -2400,7 +2431,7 @@ def test_positive_dump_enc_yaml(target_sat): # -------------------------- HOST TRACE SUBCOMMAND SCENARIOS ------------------------- @pytest.mark.tier3 @pytest.mark.rhel_ver_match('[^6].*') -def test_positive_tracer_list_and_resolve(tracer_host): +def test_positive_tracer_list_and_resolve(tracer_host, target_sat): """Install tracer on client, downgrade the service, check from the satellite that tracer shows and resolves the problem. The test works with a package specified in settings. This package is expected to install a systemd service which is expected @@ -2424,7 +2455,7 @@ def test_positive_tracer_list_and_resolve(tracer_host): """ client = tracer_host package = settings.repos["MOCK_SERVICE_RPM"] - host_info = Host.info({'name': client.hostname}) + host_info = target_sat.cli.Host.info({'name': client.hostname}) # mark the service log messages for later comparison and downgrade the pkg version service_ver_log_old = tracer_host.execute(f'cat /var/log/{package}/service.log') @@ -2432,12 +2463,12 @@ def test_positive_tracer_list_and_resolve(tracer_host): assert package_downgrade.status == 0 # tracer should detect a new trace - traces = HostTraces.list({'host-id': host_info['id']})[0] + traces = target_sat.cli.HostTraces.list({'host-id': host_info['id']})[0] assert package == traces['application'] # resolve traces and make sure that they disappear - HostTraces.resolve({'host-id': host_info['id'], 'trace-ids': traces['trace-id']}) - traces = HostTraces.list({'host-id': host_info['id']}) + target_sat.cli.HostTraces.resolve({'host-id': host_info['id'], 'trace-ids': traces['trace-id']}) + traces = target_sat.cli.HostTraces.list({'host-id': host_info['id']}) assert not traces # verify on the host end, that the service was really restarted @@ -2490,15 +2521,15 @@ def test_positive_host_with_puppet( location=[host_template.location], ).update(['location', 'organization']) - session_puppet_enabled_sat.cli.Host.update( + session_puppet_enabled_sat.cli.target_sat.cli.Host.update( { 'name': host.name, 'puppet-environment': module_puppet_environment.name, } ) - host = session_puppet_enabled_sat.cli.Host.info({'id': host['id']}) + host = session_puppet_enabled_sat.cli.target_sat.cli.Host.info({'id': host['id']}) assert host['puppet-environment'] == module_puppet_environment.name - session_puppet_enabled_sat.cli.Host.delete({'id': host['id']}) + session_puppet_enabled_sat.cli.target_sat.cli.Host.delete({'id': host['id']}) @pytest.fixture @@ -2525,7 +2556,7 @@ def function_host_content_source( } ) yield host - session_puppet_enabled_sat.cli.Host.delete({'id': host['id']}) + session_puppet_enabled_sat.cli.target_sat.cli.Host.delete({'id': host['id']}) @pytest.mark.tier2 @@ -2572,7 +2603,9 @@ class are listed scp_id = choice(sc_params_list)['id'] session_puppet_enabled_sat.cli.SmartClassParameter.update({'id': scp_id, 'override': 1}) # Verify that affected sc-param is listed - host_scparams = session_puppet_enabled_sat.cli.Host.sc_params({'host': host['name']}) + host_scparams = session_puppet_enabled_sat.cli.target_sat.cli.Host.sc_params( + {'host': host['name']} + ) assert scp_id in [scp['id'] for scp in host_scparams] @@ -2607,7 +2640,9 @@ def test_positive_create_with_puppet_class_name( 'puppet-proxy-id': session_puppet_enabled_proxy.id, } ) - host_classes = session_puppet_enabled_sat.cli.Host.puppetclasses({'host': host['name']}) + host_classes = session_puppet_enabled_sat.cli.target_sat.cli.Host.puppetclasses( + {'host': host['name']} + ) assert module_puppet_classes[0].name in [puppet['name'] for puppet in host_classes] @@ -2648,17 +2683,21 @@ def test_positive_update_host_owner_and_verify_puppet_class_name( 'puppet-proxy-id': session_puppet_enabled_proxy.id, } ) - host_classes = session_puppet_enabled_sat.cli.Host.puppetclasses({'host': host['name']}) + host_classes = session_puppet_enabled_sat.cli.target_sat.cli.Host.puppetclasses( + {'host': host['name']} + ) assert module_puppet_classes[0].name in [puppet['name'] for puppet in host_classes] - session_puppet_enabled_sat.cli.Host.update( + session_puppet_enabled_sat.cli.target_sat.cli.Host.update( {'id': host['id'], 'owner': module_puppet_user.login, 'owner-type': 'User'} ) - host = session_puppet_enabled_sat.cli.Host.info({'id': host['id']}) + host = session_puppet_enabled_sat.cli.target_sat.cli.Host.info({'id': host['id']}) assert int(host['additional-info']['owner-id']) == module_puppet_user.id assert host['additional-info']['owner-type'] == 'User' - host_classes = session_puppet_enabled_sat.cli.Host.puppetclasses({'host': host['name']}) + host_classes = session_puppet_enabled_sat.cli.target_sat.cli.Host.puppetclasses( + {'host': host['name']} + ) assert module_puppet_classes[0].name in [puppet['name'] for puppet in host_classes] @@ -2690,8 +2729,8 @@ def test_positive_create_and_update_with_content_source( host['content-information']['content-source']['name'] == session_puppet_enabled_proxy.name ) new_content_source = function_proxy - session_puppet_enabled_sat.cli.Host.update( + session_puppet_enabled_sat.cli.target_sat.cli.Host.update( {'id': host['id'], 'content-source-id': new_content_source.id} ) - host = session_puppet_enabled_sat.cli.Host.info({'id': host['id']}) + host = session_puppet_enabled_sat.cli.target_sat.cli.Host.info({'id': host['id']}) assert host['content-information']['content-source']['name'] == new_content_source.name diff --git a/tests/foreman/cli/test_hostcollection.py b/tests/foreman/cli/test_hostcollection.py index fa41aa9067e..a7710f79fc6 100644 --- a/tests/foreman/cli/test_hostcollection.py +++ b/tests/foreman/cli/test_hostcollection.py @@ -20,19 +20,8 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.activationkey import ActivationKey -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.contentview import ContentView -from robottelo.cli.factory import ( - CLIFactoryError, - make_fake_host, - make_host_collection, - make_org, -) -from robottelo.cli.host import Host -from robottelo.cli.hostcollection import HostCollection -from robottelo.cli.lifecycleenvironment import LifecycleEnvironment from robottelo.constants import DEFAULT_CV, ENVIRONMENT +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.hosts import ContentHost from robottelo.utils.datafactory import ( invalid_values_list, @@ -41,11 +30,15 @@ ) -def _make_fake_host_helper(module_org): +def _make_fake_host_helper(module_org, module_target_sat): """Make a new fake host""" - library = LifecycleEnvironment.info({'organization-id': module_org.id, 'name': ENVIRONMENT}) - default_cv = ContentView.info({'organization-id': module_org.id, 'name': DEFAULT_CV}) - return make_fake_host( + library = module_target_sat.cli.LifecycleEnvironment.info( + {'organization-id': module_org.id, 'name': ENVIRONMENT} + ) + default_cv = module_target_sat.cli.ContentView.info( + {'organization-id': module_org.id, 'name': DEFAULT_CV} + ) + return module_target_sat.cli_factory.make_fake_host( { 'content-view-id': default_cv['id'], 'lifecycle-environment-id': library['id'], @@ -58,7 +51,7 @@ def _make_fake_host_helper(module_org): @pytest.mark.upgrade @pytest.mark.tier2 @pytest.mark.e2e -def test_positive_end_to_end(module_org): +def test_positive_end_to_end(module_org, module_target_sat): """Check if host collection can be created with name and description, content host can be added and removed, host collection can be listed, updated and deleted @@ -73,56 +66,70 @@ def test_positive_end_to_end(module_org): """ name = list(valid_data_list().values())[0] desc = list(valid_data_list().values())[0] - new_host_col = make_host_collection( + new_host_col = module_target_sat.cli_factory.make_host_collection( {'description': desc, 'name': name, 'organization-id': module_org.id} ) assert new_host_col['name'] == name assert new_host_col['description'] == desc # add host - new_system = _make_fake_host_helper(module_org) + new_system = _make_fake_host_helper(module_org, module_target_sat) no_of_content_host = new_host_col['total-hosts'] - HostCollection.add_host({'host-ids': new_system['id'], 'id': new_host_col['id']}) - result = HostCollection.info({'id': new_host_col['id']}) + module_target_sat.cli.HostCollection.add_host( + {'host-ids': new_system['id'], 'id': new_host_col['id']} + ) + result = module_target_sat.cli.HostCollection.info({'id': new_host_col['id']}) assert result['total-hosts'] > no_of_content_host # list hosts - result = HostCollection.hosts({'name': name, 'organization-id': module_org.id}) + result = module_target_sat.cli.HostCollection.hosts( + {'name': name, 'organization-id': module_org.id} + ) assert new_system['name'].lower() == result[0]['name'] # List all host collections within organization - result = HostCollection.list({'organization': module_org.name}) + result = module_target_sat.cli.HostCollection.list({'organization': module_org.name}) assert len(result) >= 1 # Filter list by name - result = HostCollection.list({'name': name, 'organization-id': module_org.id}) + result = module_target_sat.cli.HostCollection.list( + {'name': name, 'organization-id': module_org.id} + ) assert len(result) == 1 assert result[0]['id'] == new_host_col['id'] # Filter list by associated host name - result = HostCollection.list({'organization': module_org.name, 'host': new_system['name']}) + result = module_target_sat.cli.HostCollection.list( + {'organization': module_org.name, 'host': new_system['name']} + ) assert len(result) == 1 assert result[0]['name'] == new_host_col['name'] # remove host - no_of_content_host = HostCollection.info({'id': new_host_col['id']})['total-hosts'] - HostCollection.remove_host({'host-ids': new_system['id'], 'id': new_host_col['id']}) - result = HostCollection.info({'id': new_host_col['id']}) + no_of_content_host = module_target_sat.cli.HostCollection.info({'id': new_host_col['id']})[ + 'total-hosts' + ] + module_target_sat.cli.HostCollection.remove_host( + {'host-ids': new_system['id'], 'id': new_host_col['id']} + ) + result = module_target_sat.cli.HostCollection.info({'id': new_host_col['id']}) assert no_of_content_host > result['total-hosts'] # update new_name = list(valid_data_list().values())[0] new_desc = list(valid_data_list().values())[0] - HostCollection.update({'description': new_desc, 'id': new_host_col['id'], 'new-name': new_name}) - result = HostCollection.info({'id': new_host_col['id']}) + module_target_sat.cli.HostCollection.update( + {'description': new_desc, 'id': new_host_col['id'], 'new-name': new_name} + ) + result = module_target_sat.cli.HostCollection.info({'id': new_host_col['id']}) assert result['name'] == new_name assert result['description'] == new_desc # delete - HostCollection.delete({'id': new_host_col['id']}) + module_target_sat.cli.HostCollection.delete({'id': new_host_col['id']}) with pytest.raises(CLIReturnCodeError): - HostCollection.info({'id': new_host_col['id']}) + module_target_sat.cli.HostCollection.info({'id': new_host_col['id']}) @pytest.mark.tier1 -def test_positive_create_with_limit(module_org): +def test_positive_create_with_limit(module_org, module_target_sat): """Check if host collection can be created with correct limits :id: 682b5624-1095-48e6-a0dd-c76e70ca6540 @@ -132,12 +139,14 @@ def test_positive_create_with_limit(module_org): :CaseImportance: Critical """ for limit in ('1', '3', '5', '10', '20'): - new_host_col = make_host_collection({'max-hosts': limit, 'organization-id': module_org.id}) + new_host_col = module_target_sat.cli_factory.make_host_collection( + {'max-hosts': limit, 'organization-id': module_org.id} + ) assert new_host_col['limit'] == limit @pytest.mark.tier1 -def test_positive_update_to_unlimited_hosts(module_org): +def test_positive_update_to_unlimited_hosts(module_org, module_target_sat): """Create Host Collection with a limit and update it to unlimited hosts :id: d688fd4a-88eb-484e-9e90-854e0595edd0 @@ -146,24 +155,24 @@ def test_positive_update_to_unlimited_hosts(module_org): :CaseImportance: High """ - host_collection = make_host_collection( + host_collection = module_target_sat.cli_factory.make_host_collection( { 'max-hosts': 1, 'organization-id': module_org.id, } ) - result = HostCollection.info( + result = module_target_sat.cli.HostCollection.info( {'name': host_collection['name'], 'organization-id': module_org.id} ) assert result['limit'] == '1' - HostCollection.update( + module_target_sat.cli.HostCollection.update( { 'name': host_collection['name'], 'organization-id': module_org.id, 'unlimited-hosts': True, } ) - result = HostCollection.info( + result = module_target_sat.cli.HostCollection.info( {'name': host_collection['name'], 'organization-id': module_org.id} ) assert result['limit'] == 'None' @@ -171,7 +180,7 @@ def test_positive_update_to_unlimited_hosts(module_org): @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) @pytest.mark.tier1 -def test_negative_create_with_name(module_org, name): +def test_negative_create_with_name(module_org, name, module_target_sat): """Attempt to create host collection with invalid name of different types @@ -184,11 +193,13 @@ def test_negative_create_with_name(module_org, name): :CaseImportance: Critical """ with pytest.raises(CLIFactoryError): - make_host_collection({'name': name, 'organization-id': module_org.id}) + module_target_sat.cli_factory.make_host_collection( + {'name': name, 'organization-id': module_org.id} + ) @pytest.mark.tier1 -def test_positive_update_limit(module_org): +def test_positive_update_limit(module_org, module_target_sat): """Check if host collection limits can be updated :id: 4c0e0c3b-82ac-4aa2-8378-6adc7946d4ec @@ -199,15 +210,17 @@ def test_positive_update_limit(module_org): :CaseImportance: Critical """ - new_host_col = make_host_collection({'organization-id': module_org.id}) + new_host_col = module_target_sat.cli_factory.make_host_collection( + {'organization-id': module_org.id} + ) for limit in ('3', '6', '9', '12', '15', '17', '19'): - HostCollection.update({'id': new_host_col['id'], 'max-hosts': limit}) - result = HostCollection.info({'id': new_host_col['id']}) + module_target_sat.cli.HostCollection.update({'id': new_host_col['id'], 'max-hosts': limit}) + result = module_target_sat.cli.HostCollection.info({'id': new_host_col['id']}) assert result['limit'] == limit @pytest.mark.tier2 -def test_positive_list_by_org_id(module_org): +def test_positive_list_by_org_id(module_org, module_target_sat): """Check if host collection list can be filtered by organization id :id: afbe077a-0de1-432c-a0c4-082129aab92e @@ -217,19 +230,21 @@ def test_positive_list_by_org_id(module_org): :CaseLevel: Integration """ # Create two host collections within different organizations - make_host_collection({'organization-id': module_org.id}) - new_org = make_org() - new_host_col = make_host_collection({'organization-id': new_org['id']}) + module_target_sat.cli_factory.make_host_collection({'organization-id': module_org.id}) + new_org = module_target_sat.cli_factory.make_org() + new_host_col = module_target_sat.cli_factory.make_host_collection( + {'organization-id': new_org['id']} + ) # List all host collections - assert len(HostCollection.list()) >= 2 + assert len(module_target_sat.cli.HostCollection.list()) >= 2 # Filter list by org id - result = HostCollection.list({'organization-id': new_org['id']}) + result = module_target_sat.cli.HostCollection.list({'organization-id': new_org['id']}) assert len(result) == 1 assert result[0]['id'] == new_host_col['id'] @pytest.mark.tier2 -def test_positive_host_collection_host_pagination(module_org): +def test_positive_host_collection_host_pagination(module_org, module_target_sat): """Check if pagination configured on per-page param defined in hammer host-collection hosts command overrides global configuration defined on /etc/hammer/cli_config.yml, which default is 20 per page @@ -243,11 +258,17 @@ def test_positive_host_collection_host_pagination(module_org): :CaseLevel: Integration """ - host_collection = make_host_collection({'organization-id': module_org.id}) - host_ids = ','.join(_make_fake_host_helper(module_org)['id'] for _ in range(2)) - HostCollection.add_host({'host-ids': host_ids, 'id': host_collection['id']}) + host_collection = module_target_sat.cli_factory.make_host_collection( + {'organization-id': module_org.id} + ) + host_ids = ','.join( + _make_fake_host_helper((module_org)['id'] for _ in range(2)), module_target_sat + ) + module_target_sat.cli.HostCollection.add_host( + {'host-ids': host_ids, 'id': host_collection['id']} + ) for number in range(1, 3): - listed_hosts = HostCollection.hosts( + listed_hosts = module_target_sat.cli.HostCollection.hosts( { 'id': host_collection['id'], 'organization-id': module_org.id, @@ -258,7 +279,7 @@ def test_positive_host_collection_host_pagination(module_org): @pytest.mark.tier2 -def test_positive_copy_by_id(module_org): +def test_positive_copy_by_id(module_org, module_target_sat): """Check if host collection can be cloned by id :id: fd7cea50-bc56-4938-a81d-4f7a60711814 @@ -271,12 +292,14 @@ def test_positive_copy_by_id(module_org): :CaseLevel: Integration """ - host_collection = make_host_collection( + host_collection = module_target_sat.cli_factory.make_host_collection( {'name': gen_string('alpha', 15), 'organization-id': module_org.id} ) new_name = gen_string('numeric') - new_host_collection = HostCollection.copy({'id': host_collection['id'], 'new-name': new_name}) - result = HostCollection.info({'id': new_host_collection[0]['id']}) + new_host_collection = module_target_sat.cli.HostCollection.copy( + {'id': host_collection['id'], 'new-name': new_name} + ) + result = module_target_sat.cli.HostCollection.info({'id': new_host_collection[0]['id']}) assert result['name'] == new_name @@ -295,8 +318,8 @@ def test_positive_register_host_ak_with_host_collection(module_org, module_ak_wi """ host_info = _make_fake_host_helper(module_org) - hc = make_host_collection({'organization-id': module_org.id}) - ActivationKey.add_host_collection( + hc = target_sat.cli_factory.make_host_collection({'organization-id': module_org.id}) + target_sat.cli.ActivationKey.add_host_collection( { 'id': module_ak_with_cv.id, 'organization-id': module_org.id, @@ -304,7 +327,7 @@ def test_positive_register_host_ak_with_host_collection(module_org, module_ak_wi } ) # add the registered instance host to collection - HostCollection.add_host( + target_sat.cli.HostCollection.add_host( {'id': hc['id'], 'organization-id': module_org.id, 'host-ids': host_info['id']} ) @@ -314,8 +337,10 @@ def test_positive_register_host_ak_with_host_collection(module_org, module_ak_wi client.register_contenthost(module_org.name, activation_key=module_ak_with_cv.name) assert client.subscribed # note: when registering the host, it should be automatically added to the host-collection - client_host = Host.info({'name': client.hostname}) - hosts = HostCollection.hosts({'id': hc['id'], 'organization-id': module_org.id}) + client_host = target_sat.cli.Host.info({'name': client.hostname}) + hosts = target_sat.cli.HostCollection.hosts( + {'id': hc['id'], 'organization-id': module_org.id} + ) assert len(hosts) == 2 expected_hosts_ids = {host_info['id'], client_host['id']} hosts_ids = {host['id'] for host in hosts} diff --git a/tests/foreman/cli/test_hostgroup.py b/tests/foreman/cli/test_hostgroup.py index d1fa7190b5a..3f5ebbf0ed0 100644 --- a/tests/foreman/cli/test_hostgroup.py +++ b/tests/foreman/cli/test_hostgroup.py @@ -20,25 +20,8 @@ from nailgun import entities import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.contentview import ContentView -from robottelo.cli.factory import ( - CLIFactoryError, - make_architecture, - make_content_view, - make_domain, - make_environment, - make_hostgroup, - make_lifecycle_environment, - make_location, - make_medium, - make_os, - make_partition_table, - make_subnet, -) -from robottelo.cli.hostgroup import HostGroup -from robottelo.cli.proxy import Proxy from robottelo.config import settings +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.utils.datafactory import ( invalid_id_list, invalid_values_list, @@ -92,20 +75,20 @@ def puppet_content_source(session_puppet_enabled_sat): @pytest.fixture(scope='module') def content_source(module_target_sat): """Return the proxy.""" - return Proxy.list({'search': f'url = {module_target_sat.url}:9090'})[0] + return module_target_sat.cli.Proxy.list({'search': f'url = {module_target_sat.url}:9090'})[0] @pytest.fixture(scope='module') -def hostgroup(content_source, module_org): +def hostgroup(content_source, module_org, module_target_sat): """Create a host group.""" - return make_hostgroup( + return module_target_sat.cli_factory.hostgroup( {'content-source-id': content_source['id'], 'organization-ids': module_org.id} ) @pytest.mark.tier2 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) -def test_negative_create_with_name(name): +def test_negative_create_with_name(name, module_target_sat): """Don't create an HostGroup with invalid data. :id: 853a6d43-129a-497b-94f0-08dc622862f8 @@ -115,7 +98,7 @@ def test_negative_create_with_name(name): :expectedresults: HostGroup is not created. """ with pytest.raises(CLIReturnCodeError): - HostGroup.create({'name': name}) + module_target_sat.cli.HostGroup.create({'name': name}) @pytest.mark.e2e @@ -140,27 +123,39 @@ def test_positive_create_with_multiple_entities_and_delete( with session_puppet_enabled_sat: # Common entities name = valid_hostgroups_list()[0] - loc = make_location() + loc = session_puppet_enabled_sat.cli_factory.make_location() org_2 = entities.Organization().create() orgs = [module_puppet_org, org_2] - env = make_environment({'location-ids': loc['id'], 'organization-ids': org_2.id}) - lce = make_lifecycle_environment({'organization-id': org_2.id}) + env = session_puppet_enabled_sat.cli_factory.make_environment( + {'location-ids': loc['id'], 'organization-ids': org_2.id} + ) + lce = session_puppet_enabled_sat.cli_factory.make_lifecycle_environment( + {'organization-id': org_2.id} + ) # Content View should be promoted to be used with LC Env - cv = make_content_view({'organization-id': org_2.id}) - ContentView.publish({'id': cv['id']}) - cv = ContentView.info({'id': cv['id']}) - ContentView.version_promote( + cv = session_puppet_enabled_sat.cli_factory.make_content_view({'organization-id': org_2.id}) + session_puppet_enabled_sat.cli.ContentView.publish({'id': cv['id']}) + cv = session_puppet_enabled_sat.cli.ContentView.info({'id': cv['id']}) + session_puppet_enabled_sat.cli.ContentView.version_promote( {'id': cv['versions'][0]['id'], 'to-lifecycle-environment-id': lce['id']} ) # Network - domain = make_domain({'location-ids': loc['id'], 'organization-ids': org_2.id}) - subnet = make_subnet({'domain-ids': domain['id'], 'organization-ids': org_2.id}) + domain = session_puppet_enabled_sat.cli_factory.make_domain( + {'location-ids': loc['id'], 'organization-ids': org_2.id} + ) + subnet = session_puppet_enabled_sat.cli_factory.make_subnet( + {'domain-ids': domain['id'], 'organization-ids': org_2.id} + ) # Operating System - arch = make_architecture() - ptable = make_partition_table({'location-ids': loc['id'], 'organization-ids': org_2.id}) - os = make_os({'architecture-ids': arch['id'], 'partition-table-ids': ptable['id']}) + arch = session_puppet_enabled_sat.cli_factory.make_architecture() + ptable = session_puppet_enabled_sat.cli_factory.make_partition_table( + {'location-ids': loc['id'], 'organization-ids': org_2.id} + ) + os = session_puppet_enabled_sat.cli_factory.make_os( + {'architecture-ids': arch['id'], 'partition-table-ids': ptable['id']} + ) os_full_name = "{} {}.{}".format(os['name'], os['major-version'], os['minor-version']) - media = make_medium( + media = session_puppet_enabled_sat.cli_factory.make_medium( { 'operatingsystem-ids': os['id'], 'location-ids': loc['id'], @@ -188,7 +183,7 @@ def test_positive_create_with_multiple_entities_and_delete( 'puppet-classes': puppet_classes[0]['name'], 'query-organization': org_2.name, } - hostgroup = make_hostgroup(make_hostgroup_params) + hostgroup = session_puppet_enabled_sat.cli_factory.hostgroup(make_hostgroup_params) assert hostgroup['name'] == name assert {org.name for org in orgs} == set(hostgroup['organizations']) assert loc['name'] in hostgroup['locations'] @@ -206,13 +201,13 @@ def test_positive_create_with_multiple_entities_and_delete( assert puppet_content_source['name'] == hostgroup['content-source']['name'] assert puppet_classes[0]['name'] in hostgroup['puppetclasses'] # delete hostgroup - HostGroup.delete({'id': hostgroup['id']}) + session_puppet_enabled_sat.cli.HostGroup.delete({'id': hostgroup['id']}) with pytest.raises(CLIReturnCodeError): - HostGroup.info({'id': hostgroup['id']}) + session_puppet_enabled_sat.cli.HostGroup.info({'id': hostgroup['id']}) @pytest.mark.tier2 -def test_negative_create_with_content_source(module_org): +def test_negative_create_with_content_source(module_org, module_target_sat): """Attempt to create a hostgroup with invalid content source specified :id: 9fc1b777-36a3-4940-a9c8-aed7ff725371 @@ -224,7 +219,7 @@ def test_negative_create_with_content_source(module_org): :CaseLevel: Integration """ with pytest.raises(CLIFactoryError): - make_hostgroup( + module_target_sat.cli_factory.hostgroup( { 'content-source-id': gen_integer(10000, 99999), 'organization-ids': module_org.id, @@ -257,7 +252,7 @@ def test_positive_update_hostgroup_with_puppet( :CaseLevel: Integration """ with session_puppet_enabled_sat as puppet_sat: - hostgroup = make_hostgroup( + hostgroup = puppet_sat.cli_factory.hostgroup( { 'content-source-id': puppet_content_source['id'], 'organization-ids': module_puppet_org.id, @@ -270,13 +265,13 @@ def test_positive_update_hostgroup_with_puppet( @request.addfinalizer def _cleanup(): with session_puppet_enabled_sat: - HostGroup.delete({'id': hostgroup['id']}) + session_puppet_enabled_sat.cli.HostGroup.delete({'id': hostgroup['id']}) session_puppet_enabled_sat.cli.Proxy.delete({'id': new_content_source['id']}) assert len(hostgroup['puppetclasses']) == 0 new_name = valid_hostgroups_list()[0] puppet_class_names = [puppet['name'] for puppet in puppet_classes] - HostGroup.update( + session_puppet_enabled_sat.cli.HostGroup.update( { 'new-name': new_name, 'id': hostgroup['id'], @@ -284,7 +279,7 @@ def _cleanup(): 'puppet-classes': puppet_class_names, } ) - hostgroup = HostGroup.info({'id': hostgroup['id']}) + hostgroup = session_puppet_enabled_sat.cli.HostGroup.info({'id': hostgroup['id']}) assert hostgroup['name'] == new_name assert hostgroup['content-source']['name'] == new_content_source['name'] for puppet_class_name in puppet_class_names: @@ -311,7 +306,7 @@ def test_positive_update_hostgroup( :CaseLevel: Integration """ - hostgroup = make_hostgroup( + hostgroup = module_target_sat.cli_factory.hostgroup( { 'content-source-id': content_source['id'], 'organization-ids': module_org.id, @@ -321,20 +316,20 @@ def test_positive_update_hostgroup( new_content_source = module_target_sat.cli_factory.make_proxy() new_name = valid_hostgroups_list()[0] - HostGroup.update( + module_target_sat.cli.HostGroup.update( { 'new-name': new_name, 'id': hostgroup['id'], 'content-source-id': new_content_source['id'], } ) - hostgroup = HostGroup.info({'id': hostgroup['id']}) + hostgroup = module_target_sat.cli.HostGroup.info({'id': hostgroup['id']}) assert hostgroup['name'] == new_name assert hostgroup['content-source']['name'] == new_content_source['name'] @pytest.mark.tier2 -def test_negative_update_content_source(hostgroup, content_source): +def test_negative_update_content_source(hostgroup, content_source, module_target_sat): """Attempt to update hostgroup's content source with invalid value :id: 4ffe6d18-3899-4bf1-acb2-d55ea09b7a26 @@ -347,13 +342,15 @@ def test_negative_update_content_source(hostgroup, content_source): :CaseLevel: Integration """ with pytest.raises(CLIReturnCodeError): - HostGroup.update({'id': hostgroup['id'], 'content-source-id': gen_integer(10000, 99999)}) - hostgroup = HostGroup.info({'id': hostgroup['id']}) + module_target_sat.cli.HostGroup.update( + {'id': hostgroup['id'], 'content-source-id': gen_integer(10000, 99999)} + ) + hostgroup = module_target_sat.cli.HostGroup.info({'id': hostgroup['id']}) assert hostgroup['content-source']['name'] == content_source['name'] @pytest.mark.tier2 -def test_negative_update_name(hostgroup): +def test_negative_update_name(hostgroup, module_target_sat): """Create HostGroup then fail to update its name :id: 42d208a4-f518-4ff2-9b7a-311adb460abd @@ -362,13 +359,13 @@ def test_negative_update_name(hostgroup): """ new_name = invalid_values_list()[0] with pytest.raises(CLIReturnCodeError): - HostGroup.update({'id': hostgroup['id'], 'new-name': new_name}) - result = HostGroup.info({'id': hostgroup['id']}) + module_target_sat.cli.HostGroup.update({'id': hostgroup['id'], 'new-name': new_name}) + result = module_target_sat.cli.HostGroup.info({'id': hostgroup['id']}) assert hostgroup['name'] == result['name'] @pytest.mark.tier2 -def test_negative_delete_by_id(): +def test_negative_delete_by_id(module_target_sat): """Create HostGroup then delete it by wrong ID :id: 047c9f1a-4dd6-4fdc-b7ed-37cc725c68d3 @@ -379,11 +376,11 @@ def test_negative_delete_by_id(): """ entity_id = invalid_id_list()[0] with pytest.raises(CLIReturnCodeError): - HostGroup.delete({'id': entity_id}) + module_target_sat.cli.HostGroup.delete({'id': entity_id}) @pytest.mark.tier2 -def test_positive_created_nested_hostgroup(module_org): +def test_positive_created_nested_hostgroup(module_org, module_target_sat): """Create a nested host group using multiple parent hostgroup paths. e.g. ` hostgroup create --organization 'org_name' --name new3 --parent-title new_1/new_2` @@ -396,9 +393,11 @@ def test_positive_created_nested_hostgroup(module_org): :CaseImportance: Low """ - parent_hg = make_hostgroup({'organization-ids': module_org.id}) - nested = make_hostgroup({'organization-ids': module_org.id, 'parent': parent_hg['name']}) - sub_nested = make_hostgroup( + parent_hg = module_target_sat.cli_factory.hostgroup({'organization-ids': module_org.id}) + nested = module_target_sat.cli_factory.hostgroup( + {'organization-ids': module_org.id, 'parent': parent_hg['name']} + ) + sub_nested = module_target_sat.cli_factory.hostgroup( {'organization-ids': module_org.id, 'parent-title': f'{parent_hg["name"]}/{nested["name"]}'} ) assert sub_nested['title'] == f"{parent_hg['name']}/{nested['name']}/{sub_nested['name']}" diff --git a/tests/foreman/cli/test_http_proxy.py b/tests/foreman/cli/test_http_proxy.py index 50c71313a89..8fb4d6af0a4 100644 --- a/tests/foreman/cli/test_http_proxy.py +++ b/tests/foreman/cli/test_http_proxy.py @@ -19,13 +19,9 @@ from fauxfactory import gen_integer, gen_string, gen_url import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_product, make_repository -from robottelo.cli.http_proxy import HttpProxy -from robottelo.cli.product import Product -from robottelo.cli.repository import Repository from robottelo.config import settings from robottelo.constants import FAKE_0_YUM_REPO_PACKAGES_COUNT +from robottelo.exceptions import CLIReturnCodeError @pytest.mark.tier1 @@ -206,7 +202,7 @@ def test_positive_environment_variable_unset_set(): @pytest.mark.e2e @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_assign_http_proxy_to_products(module_org): +def test_positive_assign_http_proxy_to_products(module_org, module_target_sat): """Assign http_proxy to Products and perform product sync. :id: 6af7b2b8-15d5-4d9f-9f87-e76b404a966f @@ -217,14 +213,14 @@ def test_positive_assign_http_proxy_to_products(module_org): :CaseImportance: High """ # create HTTP proxies - http_proxy_a = HttpProxy.create( + http_proxy_a = module_target_sat.cli.HttpProxy.create( { 'name': gen_string('alpha', 15), 'url': settings.http_proxy.un_auth_proxy_url, 'organization-id': module_org.id, }, ) - http_proxy_b = HttpProxy.create( + http_proxy_b = module_target_sat.cli.HttpProxy.create( { 'name': gen_string('alpha', 15), 'url': settings.http_proxy.auth_proxy_url, @@ -234,9 +230,9 @@ def test_positive_assign_http_proxy_to_products(module_org): }, ) # Create products and repositories - product_a = make_product({'organization-id': module_org.id}) - product_b = make_product({'organization-id': module_org.id}) - repo_a1 = make_repository( + product_a = module_target_sat.cli_factory.make_product({'organization-id': module_org.id}) + product_b = module_target_sat.cli_factory.make_product({'organization-id': module_org.id}) + repo_a1 = module_target_sat.cli_factory.make_repository( { 'organization-id': module_org.id, 'product-id': product_a['id'], @@ -244,7 +240,7 @@ def test_positive_assign_http_proxy_to_products(module_org): 'http-proxy-policy': 'none', }, ) - repo_a2 = make_repository( + repo_a2 = module_target_sat.cli_factory.make_repository( { 'organization-id': module_org.id, 'product-id': product_a['id'], @@ -253,7 +249,7 @@ def test_positive_assign_http_proxy_to_products(module_org): 'http-proxy-id': http_proxy_a['id'], }, ) - repo_b1 = make_repository( + repo_b1 = module_target_sat.cli_factory.make_repository( { 'organization-id': module_org.id, 'product-id': product_b['id'], @@ -261,7 +257,7 @@ def test_positive_assign_http_proxy_to_products(module_org): 'http-proxy-policy': 'none', }, ) - repo_b2 = make_repository( + repo_b2 = module_target_sat.cli_factory.make_repository( { 'organization-id': module_org.id, 'product-id': product_b['id'], @@ -269,7 +265,7 @@ def test_positive_assign_http_proxy_to_products(module_org): }, ) # Add http_proxy to products - Product.update_proxy( + module_target_sat.cli.Product.update_proxy( { 'ids': f"{product_a['id']},{product_b['id']}", 'http-proxy-policy': 'use_selected_http_proxy', @@ -277,18 +273,22 @@ def test_positive_assign_http_proxy_to_products(module_org): } ) for repo in repo_a1, repo_a2, repo_b1, repo_b2: - result = Repository.info({'id': repo['id']}) + result = module_target_sat.cli.Repository.info({'id': repo['id']}) assert result['http-proxy']['http-proxy-policy'] == 'use_selected_http_proxy' assert result['http-proxy']['id'] == http_proxy_b['id'] # Perform sync and verify packages count - Product.synchronize({'id': product_a['id'], 'organization-id': module_org.id}) - Product.synchronize({'id': product_b['id'], 'organization-id': module_org.id}) + module_target_sat.cli.Product.synchronize( + {'id': product_a['id'], 'organization-id': module_org.id} + ) + module_target_sat.cli.Product.synchronize( + {'id': product_b['id'], 'organization-id': module_org.id} + ) - Product.update_proxy( + module_target_sat.cli.Product.update_proxy( {'ids': f"{product_a['id']},{product_b['id']}", 'http-proxy-policy': 'none'} ) for repo in repo_a1, repo_a2, repo_b1, repo_b2: - result = Repository.info({'id': repo['id']}) + result = module_target_sat.cli.Repository.info({'id': repo['id']}) assert result['http-proxy']['http-proxy-policy'] == 'none' assert int(result['content-counts']['packages']) == FAKE_0_YUM_REPO_PACKAGES_COUNT diff --git a/tests/foreman/cli/test_jobtemplate.py b/tests/foreman/cli/test_jobtemplate.py index 916fdea7a33..7d52d3486b8 100644 --- a/tests/foreman/cli/test_jobtemplate.py +++ b/tests/foreman/cli/test_jobtemplate.py @@ -20,9 +20,7 @@ import pytest from robottelo import ssh -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import CLIFactoryError, make_job_template -from robottelo.cli.job_template import JobTemplate +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.utils.datafactory import invalid_values_list, parametrized TEMPLATE_FILE = 'template_file.txt' @@ -36,7 +34,7 @@ def module_template(): @pytest.mark.tier1 -def test_positive_create_job_template(module_org): +def test_positive_create_job_template(module_org, module_target_sat): """Create a simple Job Template :id: a5a67b10-61b0-4362-b671-9d9f095c452c @@ -46,19 +44,19 @@ def test_positive_create_job_template(module_org): :CaseImportance: Critical """ template_name = gen_string('alpha', 7) - make_job_template( + module_target_sat.cli_factory.job_template( { 'organizations': module_org.name, 'name': template_name, 'file': TEMPLATE_FILE, } ) - assert JobTemplate.info({'name': template_name}) is not None + assert module_target_sat.cli.JobTemplate.info({'name': template_name}) is not None @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) -def test_negative_create_job_template_with_invalid_name(module_org, name): +def test_negative_create_job_template_with_invalid_name(module_org, name, module_target_sat): """Create Job Template with invalid name :id: eb51afd4-e7b3-42c3-81c3-6e18ef3d7efe @@ -71,7 +69,7 @@ def test_negative_create_job_template_with_invalid_name(module_org, name): :CaseImportance: Critical """ with pytest.raises(CLIFactoryError, match='Could not create the job template:'): - make_job_template( + module_target_sat.cli_factory.job_template( { 'organizations': module_org.name, 'name': name, @@ -81,7 +79,7 @@ def test_negative_create_job_template_with_invalid_name(module_org, name): @pytest.mark.tier1 -def test_negative_create_job_template_with_same_name(module_org): +def test_negative_create_job_template_with_same_name(module_org, module_target_sat): """Create Job Template with duplicate name :id: 66100c82-97f5-4300-a0c9-8cf041f7789f @@ -91,7 +89,7 @@ def test_negative_create_job_template_with_same_name(module_org): :CaseImportance: Critical """ template_name = gen_string('alpha', 7) - make_job_template( + module_target_sat.cli_factory.job_template( { 'organizations': module_org.name, 'name': template_name, @@ -99,7 +97,7 @@ def test_negative_create_job_template_with_same_name(module_org): } ) with pytest.raises(CLIFactoryError, match='Could not create the job template:'): - make_job_template( + module_target_sat.cli_factory.job_template( { 'organizations': module_org.name, 'name': template_name, @@ -109,7 +107,7 @@ def test_negative_create_job_template_with_same_name(module_org): @pytest.mark.tier1 -def test_negative_create_empty_job_template(module_org): +def test_negative_create_empty_job_template(module_org, module_target_sat): """Create Job Template with empty template file :id: 749be863-94ae-4008-a242-c23f353ca404 @@ -120,7 +118,7 @@ def test_negative_create_empty_job_template(module_org): """ template_name = gen_string('alpha', 7) with pytest.raises(CLIFactoryError, match='Could not create the job template:'): - make_job_template( + module_target_sat.cli_factory.job_template( { 'organizations': module_org.name, 'name': template_name, @@ -131,7 +129,7 @@ def test_negative_create_empty_job_template(module_org): @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_delete_job_template(module_org): +def test_positive_delete_job_template(module_org, module_target_sat): """Delete a job template :id: 33104c04-20e9-47aa-99da-4bf3414ea31a @@ -141,20 +139,20 @@ def test_positive_delete_job_template(module_org): :CaseImportance: Critical """ template_name = gen_string('alpha', 7) - make_job_template( + module_target_sat.cli_factory.job_template( { 'organizations': module_org.name, 'name': template_name, 'file': TEMPLATE_FILE, } ) - JobTemplate.delete({'name': template_name}) + module_target_sat.cli.JobTemplate.delete({'name': template_name}) with pytest.raises(CLIReturnCodeError): - JobTemplate.info({'name': template_name}) + module_target_sat.cli.JobTemplate.info({'name': template_name}) @pytest.mark.tier2 -def test_positive_view_dump(module_org): +def test_positive_view_dump(module_org, module_target_sat): """Export contents of a job template :id: 25fcfcaa-fc4c-425e-919e-330e36195c4a @@ -163,12 +161,12 @@ def test_positive_view_dump(module_org): """ template_name = gen_string('alpha', 7) - make_job_template( + module_target_sat.cli_factory.job_template( { 'organizations': module_org.name, 'name': template_name, 'file': TEMPLATE_FILE, } ) - dumped_content = JobTemplate.dump({'name': template_name}) + dumped_content = module_target_sat.cli.JobTemplate.dump({'name': template_name}) assert len(dumped_content) > 0 diff --git a/tests/foreman/cli/test_ldapauthsource.py b/tests/foreman/cli/test_ldapauthsource.py index be218a95796..8f363f5606c 100644 --- a/tests/foreman/cli/test_ldapauthsource.py +++ b/tests/foreman/cli/test_ldapauthsource.py @@ -20,17 +20,8 @@ from nailgun import entities import pytest -from robottelo.cli.auth import Auth -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import ( - make_ldap_auth_source, - make_usergroup, - make_usergroup_external, -) -from robottelo.cli.ldapauthsource import LDAPAuthSource -from robottelo.cli.role import Role -from robottelo.cli.usergroup import UserGroup, UserGroupExternal from robottelo.constants import LDAP_ATTR, LDAP_SERVER_TYPE +from robottelo.exceptions import CLIReturnCodeError from robottelo.utils.datafactory import generate_strings_list, parametrized @@ -57,7 +48,7 @@ class TestADAuthSource: @pytest.mark.upgrade @pytest.mark.parametrize('server_name', **parametrized(generate_strings_list())) @pytest.mark.usefixtures("ldap_tear_down") - def test_positive_create_with_ad(self, ad_data, server_name): + def test_positive_create_with_ad(self, ad_data, server_name, module_target_sat): """Create/update/delete LDAP authentication with AD using names of different types :id: 093f6abc-91e7-4449-b484-71e4a14ac808 @@ -69,7 +60,7 @@ def test_positive_create_with_ad(self, ad_data, server_name): :CaseImportance: Critical """ ad_data = ad_data() - auth = make_ldap_auth_source( + auth = module_target_sat.cli_factory.ldap_auth_source( { 'name': server_name, 'onthefly-register': 'true', @@ -89,17 +80,17 @@ def test_positive_create_with_ad(self, ad_data, server_name): assert auth['server']['server'] == ad_data['ldap_hostname'] assert auth['server']['server-type'] == LDAP_SERVER_TYPE['CLI']['ad'] new_name = gen_string('alpha') - LDAPAuthSource.update({'name': server_name, 'new-name': new_name}) - updated_auth = LDAPAuthSource.info({'id': auth['server']['id']}) + module_target_sat.cli.LDAPAuthSource.update({'name': server_name, 'new-name': new_name}) + updated_auth = module_target_sat.cli.LDAPAuthSource.info({'id': auth['server']['id']}) assert updated_auth['server']['name'] == new_name - LDAPAuthSource.delete({'name': new_name}) + module_target_sat.cli.LDAPAuthSource.delete({'name': new_name}) with pytest.raises(CLIReturnCodeError): - LDAPAuthSource.info({'name': new_name}) + module_target_sat.cli.LDAPAuthSource.info({'name': new_name}) @pytest.mark.tier1 @pytest.mark.parametrize('member_group', ['foobargroup', 'foobar.group']) @pytest.mark.usefixtures("ldap_tear_down") - def test_positive_refresh_usergroup_with_ad(self, member_group, ad_data): + def test_positive_refresh_usergroup_with_ad(self, member_group, ad_data, module_target_sat): """Verify the usergroup-sync functionality in AD Auth Source :id: 2e913e76-49c3-11eb-b4c6-d46d6dd3b5b2 @@ -117,7 +108,7 @@ def test_positive_refresh_usergroup_with_ad(self, member_group, ad_data): """ ad_data = ad_data() LOGEDIN_MSG = "Using configured credentials for user '{0}'." - auth_source = make_ldap_auth_source( + auth_source = module_target_sat.cli_factory.ldap_auth_source( { 'name': gen_string('alpha'), 'onthefly-register': 'true', @@ -132,24 +123,28 @@ def test_positive_refresh_usergroup_with_ad(self, member_group, ad_data): 'base-dn': ad_data['base_dn'], } ) - viewer_role = Role.info({'name': 'Viewer'}) - user_group = make_usergroup() - make_usergroup_external( + viewer_role = module_target_sat.cli.Role.info({'name': 'Viewer'}) + user_group = module_target_sat.cli_factory.usergroup() + module_target_sat.cli_factory.usergroup_external( { 'auth-source-id': auth_source['server']['id'], 'user-group-id': user_group['id'], 'name': member_group, } ) - UserGroup.add_role({'id': user_group['id'], 'role-id': viewer_role['id']}) - user_group = UserGroup.info({'id': user_group['id']}) - result = Auth.with_user( + module_target_sat.cli.UserGroup.add_role( + {'id': user_group['id'], 'role-id': viewer_role['id']} + ) + user_group = module_target_sat.cli.UserGroup.info({'id': user_group['id']}) + result = module_target_sat.cli.Auth.with_user( username=ad_data['ldap_user_name'], password=ad_data['ldap_user_passwd'] ).status() assert LOGEDIN_MSG.format(ad_data['ldap_user_name']) in result[0]['message'] - UserGroupExternal.refresh({'user-group-id': user_group['id'], 'name': member_group}) - user_group = UserGroup.info({'id': user_group['id']}) - list = Role.with_user( + module_target_sat.cli.UserGroupExternal.refresh( + {'user-group-id': user_group['id'], 'name': member_group} + ) + user_group = module_target_sat.cli.UserGroup.info({'id': user_group['id']}) + list = module_target_sat.cli.Role.with_user( username=ad_data['ldap_user_name'], password=ad_data['ldap_user_passwd'] ).list() assert len(list) > 1 @@ -176,7 +171,7 @@ def _clean_up_previous_ldap(self): @pytest.mark.upgrade @pytest.mark.e2e @pytest.mark.usefixtures("ldap_tear_down") - def test_positive_end_to_end_with_ipa(self, default_ipa_host, server_name): + def test_positive_end_to_end_with_ipa(self, default_ipa_host, server_name, module_target_sat): """CRUD LDAP authentication with FreeIPA :id: 6cb54405-b579-4020-bf99-cb811a6aa28b @@ -188,7 +183,7 @@ def test_positive_end_to_end_with_ipa(self, default_ipa_host, server_name): :CaseImportance: High """ - auth = make_ldap_auth_source( + auth = module_target_sat.cli_factory.ldap_auth_source( { 'name': server_name, 'onthefly-register': 'true', @@ -208,16 +203,16 @@ def test_positive_end_to_end_with_ipa(self, default_ipa_host, server_name): assert auth['server']['server'] == default_ipa_host.hostname assert auth['server']['server-type'] == LDAP_SERVER_TYPE['CLI']['ipa'] new_name = gen_string('alpha') - LDAPAuthSource.update({'name': server_name, 'new-name': new_name}) - updated_auth = LDAPAuthSource.info({'id': auth['server']['id']}) + module_target_sat.cli.LDAPAuthSource.update({'name': server_name, 'new-name': new_name}) + updated_auth = module_target_sat.cli.LDAPAuthSource.info({'id': auth['server']['id']}) assert updated_auth['server']['name'] == new_name - LDAPAuthSource.delete({'name': new_name}) + module_target_sat.cli.LDAPAuthSource.delete({'name': new_name}) with pytest.raises(CLIReturnCodeError): - LDAPAuthSource.info({'name': new_name}) + module_target_sat.cli.LDAPAuthSource.info({'name': new_name}) @pytest.mark.tier3 @pytest.mark.usefixtures("ldap_tear_down") - def test_usergroup_sync_with_refresh(self, default_ipa_host): + def test_usergroup_sync_with_refresh(self, default_ipa_host, module_target_sat): """Verify the refresh functionality in Ldap Auth Source :id: c905eb80-2bd0-11ea-abc3-ddb7dbb3c930 @@ -233,7 +228,7 @@ def test_usergroup_sync_with_refresh(self, default_ipa_host): member_group = 'foreman_group' LOGEDIN_MSG = "Using configured credentials for user '{0}'." auth_source_name = gen_string('alpha') - auth_source = make_ldap_auth_source( + auth_source = module_target_sat.cli_factory.ldap_auth_source( { 'name': auth_source_name, 'onthefly-register': 'true', @@ -250,55 +245,61 @@ def test_usergroup_sync_with_refresh(self, default_ipa_host): 'groups-base': ipa_group_base_dn, } ) - auth_source = LDAPAuthSource.info({'id': auth_source['server']['id']}) + auth_source = module_target_sat.cli.LDAPAuthSource.info({'id': auth_source['server']['id']}) # Adding User in IPA UserGroup default_ipa_host.add_user_to_usergroup(member_username, member_group) - viewer_role = Role.info({'name': 'Viewer'}) - user_group = make_usergroup() - ext_user_group = make_usergroup_external( + viewer_role = module_target_sat.cli.Role.info({'name': 'Viewer'}) + user_group = module_target_sat.cli_factory.usergroup() + ext_user_group = module_target_sat.cli_factory.usergroup_external( { 'auth-source-id': auth_source['server']['id'], 'user-group-id': user_group['id'], 'name': member_group, } ) - UserGroup.add_role({'id': user_group['id'], 'role-id': viewer_role['id']}) + module_target_sat.cli.UserGroup.add_role( + {'id': user_group['id'], 'role-id': viewer_role['id']} + ) assert ext_user_group['auth-source'] == auth_source['server']['name'] - user_group = UserGroup.info({'id': user_group['id']}) + user_group = module_target_sat.cli.UserGroup.info({'id': user_group['id']}) assert len(user_group['users']) == 0 - result = Auth.with_user( + result = module_target_sat.cli.Auth.with_user( username=member_username, password=default_ipa_host.ldap_user_passwd ).status() assert LOGEDIN_MSG.format(member_username) in result[0]['message'] with pytest.raises(CLIReturnCodeError) as error: - Role.with_user( + module_target_sat.cli.Role.with_user( username=member_username, password=default_ipa_host.ldap_user_passwd ).list() assert 'Missing one of the required permissions' in error.value.message - UserGroupExternal.refresh({'user-group-id': user_group['id'], 'name': member_group}) - list = Role.with_user( + module_target_sat.cli.UserGroupExternal.refresh( + {'user-group-id': user_group['id'], 'name': member_group} + ) + list = module_target_sat.cli.Role.with_user( username=member_username, password=default_ipa_host.ldap_user_passwd ).list() assert len(list) > 1 - user_group = UserGroup.info({'id': user_group['id']}) + user_group = module_target_sat.cli.UserGroup.info({'id': user_group['id']}) assert len(user_group['users']) == 1 assert user_group['users'][0] == member_username # Removing User in IPA UserGroup default_ipa_host.remove_user_from_usergroup(member_username, member_group) - UserGroupExternal.refresh({'user-group-id': user_group['id'], 'name': member_group}) - user_group = UserGroup.info({'id': user_group['id']}) + module_target_sat.cli.UserGroupExternal.refresh( + {'user-group-id': user_group['id'], 'name': member_group} + ) + user_group = module_target_sat.cli.UserGroup.info({'id': user_group['id']}) assert len(user_group['users']) == 0 with pytest.raises(CLIReturnCodeError) as error: - Role.with_user( + module_target_sat.cli.Role.with_user( username=member_username, password=default_ipa_host.ldap_user_passwd ).list() assert 'Missing one of the required permissions' in error.value.message @pytest.mark.tier3 @pytest.mark.usefixtures("ldap_tear_down") - def test_usergroup_with_usergroup_sync(self, default_ipa_host): + def test_usergroup_with_usergroup_sync(self, default_ipa_host, module_target_sat): """Verify the usergroup-sync functionality in Ldap Auth Source :id: 2b63e886-2c53-11ea-9da5-db3ae0527554 @@ -314,7 +315,7 @@ def test_usergroup_with_usergroup_sync(self, default_ipa_host): member_group = 'foreman_group' LOGEDIN_MSG = "Using configured credentials for user '{0}'." auth_source_name = gen_string('alpha') - auth_source = make_ldap_auth_source( + auth_source = module_target_sat.cli_factory.ldap_auth_source( { 'name': auth_source_name, 'onthefly-register': 'true', @@ -331,43 +332,45 @@ def test_usergroup_with_usergroup_sync(self, default_ipa_host): 'groups-base': ipa_group_base_dn, } ) - auth_source = LDAPAuthSource.info({'id': auth_source['server']['id']}) + auth_source = module_target_sat.cli.LDAPAuthSource.info({'id': auth_source['server']['id']}) # Adding User in IPA UserGroup default_ipa_host.add_user_to_usergroup(member_username, member_group) - viewer_role = Role.info({'name': 'Viewer'}) - user_group = make_usergroup() - ext_user_group = make_usergroup_external( + viewer_role = module_target_sat.cli.Role.info({'name': 'Viewer'}) + user_group = module_target_sat.cli_factory.usergroup() + ext_user_group = module_target_sat.cli_factory.usergroup_external( { 'auth-source-id': auth_source['server']['id'], 'user-group-id': user_group['id'], 'name': member_group, } ) - UserGroup.add_role({'id': user_group['id'], 'role-id': viewer_role['id']}) + module_target_sat.cli.UserGroup.add_role( + {'id': user_group['id'], 'role-id': viewer_role['id']} + ) assert ext_user_group['auth-source'] == auth_source['server']['name'] - user_group = UserGroup.info({'id': user_group['id']}) + user_group = module_target_sat.cli.UserGroup.info({'id': user_group['id']}) assert len(user_group['users']) == 0 - result = Auth.with_user( + result = module_target_sat.cli.Auth.with_user( username=member_username, password=default_ipa_host.ldap_user_passwd ).status() assert LOGEDIN_MSG.format(member_username) in result[0]['message'] - list = Role.with_user( + list = module_target_sat.cli.Role.with_user( username=member_username, password=default_ipa_host.ldap_user_passwd ).list() assert len(list) > 1 - user_group = UserGroup.info({'id': user_group['id']}) + user_group = module_target_sat.cli.UserGroup.info({'id': user_group['id']}) assert len(user_group['users']) == 1 assert user_group['users'][0] == member_username # Removing User in IPA UserGroup default_ipa_host.remove_user_from_usergroup(member_username, member_group) with pytest.raises(CLIReturnCodeError) as error: - Role.with_user( + module_target_sat.cli.Role.with_user( username=member_username, password=default_ipa_host.ldap_user_passwd ).list() assert 'Missing one of the required permissions' in error.value.message - user_group = UserGroup.info({'id': user_group['id']}) + user_group = module_target_sat.cli.UserGroup.info({'id': user_group['id']}) assert len(user_group['users']) == 0 @@ -379,7 +382,9 @@ class TestOpenLdapAuthSource: @pytest.mark.e2e @pytest.mark.parametrize('server_name', **parametrized(generate_strings_list())) @pytest.mark.upgrade - def test_positive_end_to_end_with_open_ldap(self, open_ldap_data, server_name): + def test_positive_end_to_end_with_open_ldap( + self, open_ldap_data, server_name, module_target_sat + ): """CRUD LDAP Operations with OpenLDAP :id: f84db334-0189-11eb-846c-d46d6dd3b5b2 @@ -390,7 +395,7 @@ def test_positive_end_to_end_with_open_ldap(self, open_ldap_data, server_name): :CaseImportance: High """ - auth = make_ldap_auth_source( + auth = module_target_sat.cli_factory.ldap_auth_source( { 'name': server_name, 'onthefly-register': 'true', @@ -409,9 +414,9 @@ def test_positive_end_to_end_with_open_ldap(self, open_ldap_data, server_name): assert auth['server']['server'] == open_ldap_data['ldap_hostname'] assert auth['server']['server-type'] == LDAP_SERVER_TYPE['CLI']['posix'] new_name = gen_string('alpha') - LDAPAuthSource.update({'name': server_name, 'new-name': new_name}) - updated_auth = LDAPAuthSource.info({'id': auth['server']['id']}) + module_target_sat.cli.LDAPAuthSource.update({'name': server_name, 'new-name': new_name}) + updated_auth = module_target_sat.cli.LDAPAuthSource.info({'id': auth['server']['id']}) assert updated_auth['server']['name'] == new_name - LDAPAuthSource.delete({'name': new_name}) + module_target_sat.cli.LDAPAuthSource.delete({'name': new_name}) with pytest.raises(CLIReturnCodeError): - LDAPAuthSource.info({'name': new_name}) + module_target_sat.cli.LDAPAuthSource.info({'name': new_name}) diff --git a/tests/foreman/cli/test_lifecycleenvironment.py b/tests/foreman/cli/test_lifecycleenvironment.py index b9a77b5220f..93001a70fe8 100644 --- a/tests/foreman/cli/test_lifecycleenvironment.py +++ b/tests/foreman/cli/test_lifecycleenvironment.py @@ -21,15 +21,13 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_lifecycle_environment, make_org -from robottelo.cli.lifecycleenvironment import LifecycleEnvironment from robottelo.constants import ENVIRONMENT +from robottelo.exceptions import CLIReturnCodeError @pytest.fixture(scope='class') -def module_lce(module_org): - return make_lifecycle_environment( +def module_lce(module_org, class_target_sat): + return class_target_sat.cli_factory.make_lifecycle_environment( { 'name': module_org.name, 'organization-id': module_org.id, @@ -40,7 +38,7 @@ def module_lce(module_org): # Issues validation @pytest.mark.tier2 -def test_positive_list_subcommand(module_org): +def test_positive_list_subcommand(module_org, module_target_sat): """List subcommand returns standard output :id: cca249d0-fb77-422b-aae3-3361887269db @@ -55,12 +53,14 @@ def test_positive_list_subcommand(module_org): # List available lifecycle environments using default Table # output cmd = 'lifecycle-environment list --organization-id="%s"' - result = LifecycleEnvironment.execute(cmd % module_org.id, None, None, False) + result = module_target_sat.cli.LifecycleEnvironment.execute( + cmd % module_org.id, None, None, False + ) assert len(result) > 0 @pytest.mark.tier2 -def test_positive_search_lce_via_UTF8(module_org): +def test_positive_search_lce_via_UTF8(module_org, module_target_sat): """Search lifecycle environment via its name containing UTF-8 chars @@ -74,15 +74,18 @@ def test_positive_search_lce_via_UTF8(module_org): """ test_data = {'name': gen_string('utf8', 15), 'organization-id': module_org.id} # Can we find the new object - result = LifecycleEnvironment.info( - {'name': make_lifecycle_environment(test_data)['name'], 'organization-id': module_org.id} + result = module_target_sat.cli.LifecycleEnvironment.info( + { + 'name': module_target_sat.cli_factory.make_lifecycle_environment(test_data)['name'], + 'organization-id': module_org.id, + } ) assert result['name'] == test_data['name'] # CRUD @pytest.mark.tier1 -def test_positive_lce_crud(module_org): +def test_positive_lce_crud(module_org, module_target_sat): """CRUD test case for lifecycle environment for name, description, label, registry name pattern, and unauthenticated pull @@ -103,7 +106,7 @@ def test_positive_lce_crud(module_org): ).format(gen_string('alpha', 5)) # create - lce = make_lifecycle_environment( + lce = module_target_sat.cli_factory.make_lifecycle_environment( { 'organization': org_name, 'organization-id': module_org.id, @@ -120,7 +123,7 @@ def test_positive_lce_crud(module_org): assert lce['organization'] == org_name # update - LifecycleEnvironment.update( + module_target_sat.cli.LifecycleEnvironment.update( { 'id': lce['id'], 'new-name': new_name, @@ -129,19 +132,23 @@ def test_positive_lce_crud(module_org): 'registry-name-pattern': registry_name_pattern, } ) - lce = LifecycleEnvironment.info({'id': lce['id'], 'organization-id': module_org.id}) + lce = module_target_sat.cli.LifecycleEnvironment.info( + {'id': lce['id'], 'organization-id': module_org.id} + ) assert lce['name'] == new_name assert lce['registry-name-pattern'] == registry_name_pattern assert lce['unauthenticated-pull'] == 'true' # delete - LifecycleEnvironment.delete({'id': lce['id']}) + module_target_sat.cli.LifecycleEnvironment.delete({'id': lce['id']}) with pytest.raises(CLIReturnCodeError): - LifecycleEnvironment.info({'id': lce['id'], 'organization-id': module_org.id}) + module_target_sat.cli.LifecycleEnvironment.info( + {'id': lce['id'], 'organization-id': module_org.id} + ) @pytest.mark.tier1 -def test_positive_create_with_organization_label(module_org): +def test_positive_create_with_organization_label(module_org, module_target_sat): """Create lifecycle environment, specifying organization label :id: eb5cfc71-c83d-45ca-ba34-9ef79197691d @@ -152,14 +159,14 @@ def test_positive_create_with_organization_label(module_org): :CaseImportance: Critical """ - new_lce = make_lifecycle_environment( - {'name': gen_string('alpha'), 'organization-label': module_org.label} + new_lce = module_target_sat.cli_factory.make_lifecycle_environment( + {'name': gen_string('alpha'), 'organization-id': module_org.id} ) assert new_lce['organization'] == module_org.label @pytest.mark.tier1 -def test_positve_list_paths(module_org): +def test_positve_list_paths(module_org, module_target_sat): """List the environment paths under a given organization :id: 71600d6b-1ef4-4b88-8e9b-eb2481ee1fe2 @@ -169,9 +176,11 @@ def test_positve_list_paths(module_org): :CaseImportance: Critical """ - lc_env = make_lifecycle_environment({'organization-id': module_org.id}) + lc_env = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) # Add paths to lifecycle environments - result = LifecycleEnvironment.paths( + result = module_target_sat.cli.LifecycleEnvironment.paths( {'organization-id': module_org.id, 'permission-type': 'readable'} ) assert f"Library >> {lc_env['name']}" in result @@ -180,27 +189,27 @@ def test_positve_list_paths(module_org): class LifeCycleEnvironmentPaginationTestCase: """Test class for LifeCycle Environment pagination tests""" - @classmethod - def setUpClass(cls): + @pytest.fixture(scope='class', autouse=True) + def class_setup(self, target_sat): """Create organization and lifecycle environments to reuse in tests""" super().setUpClass() - cls.lces_count = 25 - cls.org = make_org() + self.lces_count = 25 + self.org = target_sat.cli_factory.make_org() env_base_name = gen_string('alpha') last_env_name = ENVIRONMENT - cls.env_names = [last_env_name] - for env_index in range(cls.lces_count): + self.env_names = [last_env_name] + for env_index in range(self.lces_count): env_name = f'{env_base_name}-{env_index}' - make_lifecycle_environment( - {'name': env_name, 'organization-id': cls.org['id'], 'prior': last_env_name} + target_sat.cli_factory.make_lifecycle_environment( + {'name': env_name, 'organization-id': self.org['id'], 'prior': last_env_name} ) last_env_name = env_name - cls.env_names.append(env_name) + self.env_names.append(env_name) - cls.lces_count += 1 # include default 'Library' lce + self.lces_count += 1 # include default 'Library' lce @pytest.mark.tier2 - def test_positive_list_all_with_per_page(self): + def test_positive_list_all_with_per_page(self, target_sat): """Attempt to list more than 20 lifecycle environment with per-page option. @@ -213,7 +222,7 @@ def test_positive_list_all_with_per_page(self): """ per_page_count = self.lces_count + 5 - lifecycle_environments = LifecycleEnvironment.list( + lifecycle_environments = target_sat.cli.LifecycleEnvironment.list( {'organization-id': self.org['id'], 'per-page': per_page_count} ) @@ -222,7 +231,7 @@ def test_positive_list_all_with_per_page(self): assert env_name_set == set(self.env_names) @pytest.mark.tier2 - def test_positive_list_with_pagination(self): + def test_positive_list_with_pagination(self, target_sat): """Make sure lces list can be displayed with different items per page value @@ -240,14 +249,14 @@ def test_positive_list_with_pagination(self): # Verify the first page contains exactly the same items count # as `per-page` value with self.subTest(per_page): - lces = LifecycleEnvironment.list( + lces = target_sat.cli.LifecycleEnvironment.list( {'organization-id': self.org['id'], 'per-page': per_page} ) assert len(lces) == per_page # Verify pagination and total amount of pages by checking the # items count on the last page last_page = ceil(self.lces_count / per_page) - lces = LifecycleEnvironment.list( + lces = target_sat.cli.LifecycleEnvironment.list( {'organization-id': self.org['id'], 'page': last_page, 'per-page': per_page} ) assert len(lces) == self.lces_count % per_page or per_page diff --git a/tests/foreman/cli/test_location.py b/tests/foreman/cli/test_location.py index ee157c4ef76..98a38c6a324 100644 --- a/tests/foreman/cli/test_location.py +++ b/tests/foreman/cli/test_location.py @@ -19,29 +19,7 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.computeresource import ComputeResource -from robottelo.cli.domain import Domain -from robottelo.cli.environment import Environment -from robottelo.cli.factory import ( - CLIFactoryError, - make_compute_resource, - make_domain, - make_environment, - make_hostgroup, - make_location, - make_medium, - make_subnet, - make_template, - make_user, -) -from robottelo.cli.hostgroup import HostGroup -from robottelo.cli.location import Location -from robottelo.cli.medium import Medium -from robottelo.cli.proxy import Proxy -from robottelo.cli.subnet import Subnet -from robottelo.cli.template import Template -from robottelo.cli.user import User +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError def _proxy(request, target_sat): @@ -50,107 +28,107 @@ def _proxy(request, target_sat): @request.addfinalizer def _cleanup(): - if Proxy.exists(search=('name', proxy['name'])): - Proxy.delete(options={'id': proxy['id']}) + if target_sat.cli.Proxy.exists(search=('name', proxy['name'])): + target_sat.cli.Proxy.delete(options={'id': proxy['id']}) return proxy -def _location(request, options=None): - location = make_location(options=options) +def _location(request, target_sat, options=None): + location = target_sat.cli_factory.make_location(options=options) @request.addfinalizer def _cleanup(): - if Location.exists(search=('id', location['id'])): - Location.delete(options={'id': location['id']}) + if target_sat.cli.Location.exists(search=('id', location['id'])): + target_sat.cli.Location.delete(options={'id': location['id']}) return location -def _subnet(request): - subnet = make_subnet() +def _subnet(request, target_sat): + subnet = target_sat.cli_factory.make_subnet() @request.addfinalizer def _cleanup(): - if Subnet.exists(search=('name', subnet['name'])): - Subnet.delete(options={'id': subnet['id']}) + if target_sat.cli.Subnet.exists(search=('name', subnet['name'])): + target_sat.cli.Subnet.delete(options={'id': subnet['id']}) return subnet -def _environment(request): - environment = make_environment() +def _environment(request, target_sat): + environment = target_sat.cli_factory.make_environment() @request.addfinalizer def _cleanup(): - if Environment.exists(search=('name', environment['name'])): - Environment.delete(options={'id': environment['id']}) + if target_sat.cli.Environment.exists(search=('name', environment['name'])): + target_sat.cli.Environment.delete(options={'id': environment['id']}) return environment -def _domain(request): - domain = make_domain() +def _domain(request, target_sat): + domain = target_sat.cli_factory.make_domain() @request.addfinalizer def _cleanup(): - if Domain.exists(search=('name', domain['name'])): - Domain.delete(options={'id': domain['id']}) + if target_sat.cli.Domain.exists(search=('name', domain['name'])): + target_sat.cli.Domain.delete(options={'id': domain['id']}) return domain -def _medium(request): - medium = make_medium() +def _medium(request, target_sat): + medium = target_sat.cli_factory.make_medium() @request.addfinalizer def _cleanup(): - if Medium.exists(search=('name', medium['name'])): - Medium.delete(options={'id': medium['id']}) + if target_sat.cli.Medium.exists(search=('name', medium['name'])): + target_sat.cli.Medium.delete(options={'id': medium['id']}) return medium -def _host_group(request): - host_group = make_hostgroup() +def _host_group(request, target_sat): + host_group = target_sat.cli_factory.hostgroup() @request.addfinalizer def _cleanup(): - if HostGroup.exists(search=('id', host_group['id'])): - HostGroup.delete(options={'id': host_group['id']}) + if target_sat.cli.HostGroup.exists(search=('id', host_group['id'])): + target_sat.cli.HostGroup.delete(options={'id': host_group['id']}) return host_group -def _compute_resource(request): - compute_resource = make_compute_resource() +def _compute_resource(request, target_sat): + compute_resource = target_sat.cli_factory.compute_resource() @request.addfinalizer def _cleanup(): - if ComputeResource.exists(search=('id', compute_resource['id'])): - ComputeResource.delete(options={'id': compute_resource['id']}) + if target_sat.cli.ComputeResource.exists(search=('id', compute_resource['id'])): + target_sat.cli.ComputeResource.delete(options={'id': compute_resource['id']}) return compute_resource -def _template(request): - template = make_template() +def _template(request, target_sat): + template = target_sat.cli_factory.make_template() @request.addfinalizer def _cleanup(): - if Template.exists(search=('name', template['name'])): - Template.delete(options={'id': template['id']}) + if target_sat.cli.Template.exists(search=('name', template['name'])): + target_sat.cli.Template.delete(options={'id': template['id']}) return template -def _user(request): - user = make_user() +def _user(request, target_sat): + user = target_sat.cli_factory.user() @request.addfinalizer def _cleanup(): - if User.exists(search=('login', user['login'])): - User.delete(options={'id': user['id']}) + if target_sat.cli.User.exists(search=('login', user['login'])): + target_sat.cli.User.delete(options={'id': user['id']}) return user @@ -161,7 +139,7 @@ class TestLocation: @pytest.mark.e2e @pytest.mark.tier2 @pytest.mark.upgrade - def test_positive_create_update_delete(self, request): + def test_positive_create_update_delete(self, request, target_sat): """Create new location with attributes, update and delete it :id: e1844d9d-ec4a-44b3-9743-e932cc70020d @@ -176,8 +154,8 @@ def test_positive_create_update_delete(self, request): """ # Create description = gen_string('utf8') - subnet = _subnet(request) - domains = [_domain(request) for _ in range(0, 2)] + subnet = _subnet(request, target_sat) + domains = [_domain(request, target_sat) for _ in range(0, 2)] host_groups = [_host_group(request) for _ in range(0, 3)] medium = _medium(request) compute_resource = _compute_resource(request) @@ -221,25 +199,25 @@ def test_positive_create_update_delete(self, request): assert location['users'][0] == user['login'] # Update - Location.update( + target_sat.cli.Location.update( { 'id': location['id'], 'domain-ids': domains[1]['id'], 'hostgroup-ids': [host_groups[1]['id'], host_groups[2]['id']], } ) - location = Location.info({'id': location['id']}) + location = target_sat.cli.Location.info({'id': location['id']}) assert host_groups[1]['name'] in location['hostgroups'] assert host_groups[2]['name'] in location['hostgroups'] assert location['domains'][0] == domains[1]['name'] # Delete - Location.delete({'id': location['id']}) + target_sat.cli.Location.delete({'id': location['id']}) with pytest.raises(CLIReturnCodeError): - Location.info({'id': location['id']}) + target_sat.cli.Location.info({'id': location['id']}) @pytest.mark.tier1 - def test_positive_create_with_parent(self, request): + def test_positive_create_with_parent(self, request, target_sat): """Create new location with parent location specified :id: 49b34733-103a-4fee-818b-6a3386253af1 @@ -252,12 +230,12 @@ def test_positive_create_with_parent(self, request): expected parent location set """ - parent_location = _location(request) + parent_location = _location(request, target_sat) location = _location(request, {'parent-id': parent_location['id']}) assert location['parent'] == parent_location['name'] @pytest.mark.tier1 - def test_negative_create_with_same_name(self, request): + def test_negative_create_with_same_name(self, request, target_sat): """Try to create location using same name twice :id: 4fbaea41-9775-40a2-85a5-4dc05cc95134 @@ -267,13 +245,13 @@ def test_negative_create_with_same_name(self, request): :CaseImportance: Critical """ name = gen_string('utf8') - location = _location(request, options={'name': name}) + location = _location(request, target_sat, options={'name': name}) assert location['name'] == name with pytest.raises(CLIFactoryError): - _location(request, options={'name': name}) + _location(request, target_sat, options={'name': name}) @pytest.mark.tier1 - def test_negative_create_with_user_by_name(self, request): + def test_negative_create_with_user_by_name(self, request, target_sat): """Try to create new location with incorrect user assigned to it Use user login as a parameter @@ -284,7 +262,7 @@ def test_negative_create_with_user_by_name(self, request): :CaseImportance: Critical """ with pytest.raises(CLIFactoryError): - _location(request, options={'users': gen_string('utf8', 80)}) + _location(request, target_sat, options={'users': gen_string('utf8', 80)}) @pytest.mark.run_in_one_thread @pytest.mark.tier2 @@ -300,19 +278,23 @@ def test_positive_add_and_remove_capsule(self, request, target_sat): :CaseLevel: Integration """ - location = _location(request) + location = _location(request, target_sat) proxy = _proxy(request, target_sat) - Location.add_smart_proxy({'name': location['name'], 'smart-proxy-id': proxy['id']}) - location = Location.info({'name': location['name']}) + target_sat.cli.Location.add_smart_proxy( + {'name': location['name'], 'smart-proxy-id': proxy['id']} + ) + location = target_sat.cli.Location.info({'name': location['name']}) assert proxy['name'] in location['smart-proxies'] - Location.remove_smart_proxy({'name': location['name'], 'smart-proxy': proxy['name']}) - location = Location.info({'name': location['name']}) + target_sat.cli.Location.remove_smart_proxy( + {'name': location['name'], 'smart-proxy': proxy['name']} + ) + location = target_sat.cli.Location.info({'name': location['name']}) assert proxy['name'] not in location['smart-proxies'] @pytest.mark.tier1 - def test_positive_add_update_remove_parameter(self, request): + def test_positive_add_update_remove_parameter(self, request, target_sat): """Add, update and remove parameter to location :id: 61b564f2-a42a-48de-833d-bec3a127d0f5 @@ -325,30 +307,30 @@ def test_positive_add_update_remove_parameter(self, request): param_name = gen_string('alpha') param_value = gen_string('alpha') param_new_value = gen_string('alpha') - location = _location(request) - Location.set_parameter( + location = _location(request, target_sat) + target_sat.cli.Location.set_parameter( {'name': param_name, 'value': param_value, 'location-id': location['id']} ) - location = Location.info({'id': location['id']}) + location = target_sat.cli.Location.info({'id': location['id']}) assert len(location['parameters']) == 1 assert param_value == location['parameters'][param_name.lower()] # Update - Location.set_parameter( + target_sat.cli.Location.set_parameter( {'name': param_name, 'value': param_new_value, 'location': location['name']} ) - location = Location.info({'id': location['id']}) + location = target_sat.cli.Location.info({'id': location['id']}) assert len(location['parameters']) == 1 assert param_new_value == location['parameters'][param_name.lower()] # Remove - Location.delete_parameter({'name': param_name, 'location': location['name']}) - location = Location.info({'id': location['id']}) + target_sat.cli.Location.delete_parameter({'name': param_name, 'location': location['name']}) + location = target_sat.cli.Location.info({'id': location['id']}) assert len(location['parameters']) == 0 assert param_name.lower() not in location['parameters'] @pytest.mark.tier2 - def test_positive_update_parent(self, request): + def test_positive_update_parent(self, request, target_sat): """Update location's parent location :id: 34522d1a-1190-48d8-9285-fc9a9bcf6c6a @@ -361,16 +343,16 @@ def test_positive_update_parent(self, request): :CaseImportance: High """ - parent_location = _location(request) + parent_location = _location(request, target_sat) location = _location(request, {'parent-id': parent_location['id']}) - parent_location_2 = _location(request) - Location.update({'id': location['id'], 'parent-id': parent_location_2['id']}) - location = Location.info({'id': location['id']}) + parent_location_2 = _location(request, target_sat) + target_sat.cli.Location.update({'id': location['id'], 'parent-id': parent_location_2['id']}) + location = target_sat.cli.Location.info({'id': location['id']}) assert location['parent'] == parent_location_2['name'] @pytest.mark.tier1 - def test_negative_update_parent_with_child(self, request): + def test_negative_update_parent_with_child(self, request, target_sat): """Attempt to set child location as a parent and vice versa :id: fd4cb1cf-377f-4b48-b7f4-d4f6ca56f544 @@ -383,17 +365,19 @@ def test_negative_update_parent_with_child(self, request): :CaseImportance: High """ - parent_location = _location(request) + parent_location = _location(request, target_sat) location = _location(request, {'parent-id': parent_location['id']}) # set parent as child with pytest.raises(CLIReturnCodeError): - Location.update({'id': parent_location['id'], 'parent-id': location['id']}) - parent_location = Location.info({'id': parent_location['id']}) + target_sat.cli.Location.update( + {'id': parent_location['id'], 'parent-id': location['id']} + ) + parent_location = target_sat.cli.Location.info({'id': parent_location['id']}) assert parent_location.get('parent') is None # set child as parent with pytest.raises(CLIReturnCodeError): - Location.update({'id': location['id'], 'parent-id': location['id']}) - location = Location.info({'id': location['id']}) + target_sat.cli.Location.update({'id': location['id'], 'parent-id': location['id']}) + location = target_sat.cli.Location.info({'id': location['id']}) assert location['parent'] == parent_location['name'] diff --git a/tests/foreman/cli/test_logging.py b/tests/foreman/cli/test_logging.py index 45692e2e2a2..61ead1bee0e 100644 --- a/tests/foreman/cli/test_logging.py +++ b/tests/foreman/cli/test_logging.py @@ -22,9 +22,6 @@ from nailgun import entities import pytest -from robottelo.cli.factory import make_product, make_repository -from robottelo.cli.product import Product -from robottelo.cli.repository import Repository from robottelo.config import settings from robottelo.logging import logger @@ -238,10 +235,10 @@ def test_positive_logging_from_pulp3(module_org, target_sat): name = product_name label = product_name desc = product_name - product = make_product( + product = target_sat.cli_factory.make_product( {'description': desc, 'label': label, 'name': name, 'organization-id': module_org.id}, ) - repo = make_repository( + repo = target_sat.cli_factory.make_repository( { 'organization-id': module_org.id, 'product-id': product['id'], @@ -249,8 +246,8 @@ def test_positive_logging_from_pulp3(module_org, target_sat): }, ) # Synchronize the repository - Product.synchronize({'id': product['id'], 'organization-id': module_org.id}) - Repository.synchronize({'id': repo['id']}) + target_sat.cli.Product.synchronize({'id': product['id'], 'organization-id': module_org.id}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) # Get the id of repository sync from task task_out = target_sat.execute( "hammer task list | grep -F \'Synchronize repository {\"text\"=>\"repository\'" diff --git a/tests/foreman/cli/test_medium.py b/tests/foreman/cli/test_medium.py index d9f835fdb0b..5ded3225e84 100644 --- a/tests/foreman/cli/test_medium.py +++ b/tests/foreman/cli/test_medium.py @@ -19,9 +19,7 @@ from fauxfactory import gen_alphanumeric import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_location, make_medium, make_org, make_os -from robottelo.cli.medium import Medium +from robottelo.exceptions import CLIReturnCodeError from robottelo.utils.datafactory import parametrized, valid_data_list URL = "http://mirror.fakeos.org/%s/$major.$minor/os/$arch" @@ -33,7 +31,7 @@ class TestMedium: @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(valid_data_list().values())) - def test_positive_crud_with_name(self, name): + def test_positive_crud_with_name(self, name, module_target_sat): """Check if Medium can be created, updated, deleted :id: 66b749b2-0248-47a8-b78f-3366f3804b29 @@ -45,18 +43,18 @@ def test_positive_crud_with_name(self, name): :CaseImportance: Critical """ - medium = make_medium({'name': name}) + medium = module_target_sat.cli_factory.make_medium({'name': name}) assert medium['name'] == name new_name = gen_alphanumeric(6) - Medium.update({'name': medium['name'], 'new-name': new_name}) - medium = Medium.info({'id': medium['id']}) + module_target_sat.cli.Medium.update({'name': medium['name'], 'new-name': new_name}) + medium = module_target_sat.cli.Medium.info({'id': medium['id']}) assert medium['name'] == new_name - Medium.delete({'id': medium['id']}) + module_target_sat.cli.Medium.delete({'id': medium['id']}) with pytest.raises(CLIReturnCodeError): - Medium.info({'id': medium['id']}) + module_target_sat.cli.Medium.info({'id': medium['id']}) @pytest.mark.tier1 - def test_positive_create_with_location(self): + def test_positive_create_with_location(self, module_target_sat): """Check if medium with location can be created :id: cbc6c586-fae7-4bb9-aeb1-e30158f16a98 @@ -66,12 +64,12 @@ def test_positive_create_with_location(self): :CaseImportance: Medium """ - location = make_location() - medium = make_medium({'location-ids': location['id']}) + location = module_target_sat.cli_factory.make_location() + medium = module_target_sat.cli_factory.make_medium({'location-ids': location['id']}) assert location['name'] in medium['locations'] @pytest.mark.tier1 - def test_positive_create_with_organization_by_id(self): + def test_positive_create_with_organization_by_id(self, module_target_sat): """Check if medium with organization can be created :id: 631bb6ed-e42b-482a-83f0-f6ce0f20729a @@ -81,13 +79,13 @@ def test_positive_create_with_organization_by_id(self): :CaseImportance: Medium """ - org = make_org() - medium = make_medium({'organization-ids': org['id']}) + org = module_target_sat.cli_factory.make_org() + medium = module_target_sat.cli_factory.make_medium({'organization-ids': org['id']}) assert org['name'] in medium['organizations'] @pytest.mark.tier2 @pytest.mark.upgrade - def test_positive_remove_os(self): + def test_positive_remove_os(self, module_target_sat): """Check if Medium can be associated with operating system and then removed from media :id: 23b5b55b-3624-440c-8001-75c7c5a5a004 @@ -97,11 +95,15 @@ def test_positive_remove_os(self): :CaseLevel: Integration """ - medium = make_medium() - os = make_os() - Medium.add_operating_system({'id': medium['id'], 'operatingsystem-id': os['id']}) - medium = Medium.info({'id': medium['id']}) + medium = module_target_sat.cli_factory.make_medium() + os = module_target_sat.cli_factory.make_os() + module_target_sat.cli.Medium.add_operating_system( + {'id': medium['id'], 'operatingsystem-id': os['id']} + ) + medium = module_target_sat.cli.Medium.info({'id': medium['id']}) assert os['title'] in medium['operating-systems'] - Medium.remove_operating_system({'id': medium['id'], 'operatingsystem-id': os['id']}) - medium = Medium.info({'id': medium['id']}) + module_target_sat.cli.Medium.remove_operating_system( + {'id': medium['id'], 'operatingsystem-id': os['id']} + ) + medium = module_target_sat.cli.Medium.info({'id': medium['id']}) assert os['name'] not in medium['operating-systems'] diff --git a/tests/foreman/cli/test_model.py b/tests/foreman/cli/test_model.py index ddfd4dfe19c..0dfc2cb3ba1 100644 --- a/tests/foreman/cli/test_model.py +++ b/tests/foreman/cli/test_model.py @@ -19,9 +19,7 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_model -from robottelo.cli.model import Model +from robottelo.exceptions import CLIReturnCodeError from robottelo.utils.datafactory import ( invalid_id_list, invalid_values_list, @@ -34,9 +32,9 @@ class TestModel: """Test class for Model CLI""" @pytest.fixture - def class_model(self): + def class_model(self, target_sat): """Shared model for tests""" - return make_model() + return target_sat.cli_factory.make_model() @pytest.mark.tier1 @pytest.mark.upgrade @@ -44,7 +42,7 @@ def class_model(self): ('name', 'new_name'), **parametrized(list(zip(valid_data_list().values(), valid_data_list().values()))) ) - def test_positive_crud_with_name(self, name, new_name): + def test_positive_crud_with_name(self, name, new_name, module_target_sat): """Successfully creates, updates and deletes a Model. :id: 9ca9d5ff-750a-4d60-91b2-4c4375f0e35f @@ -55,17 +53,17 @@ def test_positive_crud_with_name(self, name, new_name): :CaseImportance: High """ - model = make_model({'name': name}) + model = module_target_sat.cli_factory.make_model({'name': name}) assert model['name'] == name - Model.update({'id': model['id'], 'new-name': new_name}) - model = Model.info({'id': model['id']}) + module_target_sat.cli.Model.update({'id': model['id'], 'new-name': new_name}) + model = module_target_sat.cli.Model.info({'id': model['id']}) assert model['name'] == new_name - Model.delete({'id': model['id']}) + module_target_sat.cli.Model.delete({'id': model['id']}) with pytest.raises(CLIReturnCodeError): - Model.info({'id': model['id']}) + module_target_sat.cli.Model.info({'id': model['id']}) @pytest.mark.tier1 - def test_positive_create_with_vendor_class(self): + def test_positive_create_with_vendor_class(self, module_target_sat): """Check if Model can be created with specific vendor class :id: c36d3490-cd12-4f5f-a453-2ae5d0404496 @@ -75,12 +73,12 @@ def test_positive_create_with_vendor_class(self): :CaseImportance: Medium """ vendor_class = gen_string('utf8') - model = make_model({'vendor-class': vendor_class}) + model = module_target_sat.cli_factory.make_model({'vendor-class': vendor_class}) assert model['vendor-class'] == vendor_class @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) - def test_negative_create_with_name(self, name): + def test_negative_create_with_name(self, name, module_target_sat): """Don't create an Model with invalid data. :id: b2eade66-b612-47e7-bfcc-6e363023f498 @@ -92,11 +90,11 @@ def test_negative_create_with_name(self, name): :CaseImportance: High """ with pytest.raises(CLIReturnCodeError): - Model.create({'name': name}) + module_target_sat.cli.Model.create({'name': name}) @pytest.mark.tier1 @pytest.mark.parametrize('new_name', **parametrized(invalid_values_list())) - def test_negative_update_name(self, class_model, new_name): + def test_negative_update_name(self, class_model, new_name, module_target_sat): """Fail to update shared model name :id: 98020a4a-1789-4df3-929c-6c132b57f5a1 @@ -108,13 +106,13 @@ def test_negative_update_name(self, class_model, new_name): :CaseImportance: Medium """ with pytest.raises(CLIReturnCodeError): - Model.update({'id': class_model['id'], 'new-name': new_name}) - result = Model.info({'id': class_model['id']}) + module_target_sat.cli.Model.update({'id': class_model['id'], 'new-name': new_name}) + result = module_target_sat.cli.Model.info({'id': class_model['id']}) assert class_model['name'] == result['name'] @pytest.mark.tier1 @pytest.mark.parametrize('entity_id', **parametrized(invalid_id_list())) - def test_negative_delete_by_id(self, entity_id): + def test_negative_delete_by_id(self, entity_id, module_target_sat): """Delete model by wrong ID :id: f8b0d428-1b3d-4fc9-9ca1-1eb30c8ac20a @@ -126,4 +124,4 @@ def test_negative_delete_by_id(self, entity_id): :CaseImportance: High """ with pytest.raises(CLIReturnCodeError): - Model.delete({'id': entity_id}) + module_target_sat.cli.Model.delete({'id': entity_id}) diff --git a/tests/foreman/cli/test_operatingsystem.py b/tests/foreman/cli/test_operatingsystem.py index 587ceeb67a0..7b5e0bd60a5 100644 --- a/tests/foreman/cli/test_operatingsystem.py +++ b/tests/foreman/cli/test_operatingsystem.py @@ -19,14 +19,8 @@ from fauxfactory import gen_alphanumeric, gen_string import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import ( - make_architecture, - make_medium, - make_partition_table, - make_template, -) from robottelo.constants import DEFAULT_ORG +from robottelo.exceptions import CLIReturnCodeError from robottelo.utils.datafactory import ( filtered_datapoint, invalid_values_list, @@ -114,10 +108,10 @@ def test_positive_end_to_end_os(self, target_sat): new_pass_hash = 'SHA256' new_minor_version = gen_string('numeric', 1) new_major_version = gen_string('numeric', 1) - new_architecture = make_architecture() - new_medium = make_medium() - new_ptable = make_partition_table() - new_template = make_template() + new_architecture = target_sat.cli_factory.make_architecture() + new_medium = target_sat.cli_factory.make_medium() + new_ptable = target_sat.cli_factory.make_partition_table() + new_template = target_sat.cli_factory.make_template() os = target_sat.cli.OperatingSys.update( { 'id': os['id'], diff --git a/tests/foreman/cli/test_organization.py b/tests/foreman/cli/test_organization.py index 2d4a8b8b8b8..a34bea5f7fc 100644 --- a/tests/foreman/cli/test_organization.py +++ b/tests/foreman/cli/test_organization.py @@ -19,25 +19,9 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import ( - CLIFactoryError, - make_compute_resource, - make_domain, - make_hostgroup, - make_lifecycle_environment, - make_location, - make_medium, - make_org, - make_subnet, - make_template, - make_user, -) -from robottelo.cli.lifecycleenvironment import LifecycleEnvironment -from robottelo.cli.org import Org -from robottelo.cli.user import User from robottelo.config import settings from robottelo.constants import FOREMAN_PROVIDERS +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.utils.datafactory import ( filtered_datapoint, invalid_values_list, @@ -73,7 +57,7 @@ def proxy(target_sat): @pytest.mark.tier2 -def test_positive_no_duplicate_lines(): +def test_positive_no_duplicate_lines(module_target_sat): """hammer organization --help types information doubled @@ -86,7 +70,7 @@ def test_positive_no_duplicate_lines(): :CaseImportance: Low """ # org list --help: - result = Org.list({'help': True}, output_format=None) + result = module_target_sat.cli.Org.list({'help': True}, output_format=None) # get list of lines and check they all are unique lines = [ line @@ -98,7 +82,7 @@ def test_positive_no_duplicate_lines(): @pytest.mark.e2e @pytest.mark.tier1 -def test_positive_CRD(): +def test_positive_CRD(module_target_sat): """Create organization with valid name, label and description :id: 35840da7-668e-4f78-990a-738aa688d586 @@ -113,14 +97,16 @@ def test_positive_CRD(): name = valid_org_names_list()[0] label = valid_labels_list()[0] desc = list(valid_data_list().values())[0] - org = make_org({'name': name, 'label': label, 'description': desc}) + org = module_target_sat.cli_factory.make_org( + {'name': name, 'label': label, 'description': desc} + ) assert org['name'] == name assert org['label'] == label assert org['description'] == desc # List - result = Org.list({'search': f'name={name}'}) + result = module_target_sat.cli.Org.list({'search': f'name={name}'}) assert len(result) == 1 assert result[0]['name'] == name @@ -130,30 +116,30 @@ def test_positive_CRD(): f'description ~ {desc[:-5]}', f'name ^ "{name}"', ]: - result = Org.list({'search': query}) + result = module_target_sat.cli.Org.list({'search': query}) assert len(result) == 1 assert result[0]['name'] == name # Search by name and label - result = Org.exists(search=('name', name)) + result = module_target_sat.cli.Org.exists(search=('name', name)) assert result['name'] == name - result = Org.exists(search=('label', label)) + result = module_target_sat.cli.Org.exists(search=('label', label)) assert result['name'] == name # Info by name and label - result = Org.info({'label': label}) + result = module_target_sat.cli.Org.info({'label': label}) assert result['id'] == org['id'] - result = Org.info({'name': name}) + result = module_target_sat.cli.Org.info({'name': name}) assert org['id'] == result['id'] # Delete - Org.delete({'id': org['id']}) + module_target_sat.cli.Org.delete({'id': org['id']}) with pytest.raises(CLIReturnCodeError): - result = Org.info({'id': org['id']}) + result = module_target_sat.cli.Org.info({'id': org['id']}) @pytest.mark.tier2 -def test_positive_create_with_system_admin_user(): +def test_positive_create_with_system_admin_user(module_target_sat): """Create organization using user with system admin role :id: 1482ab6e-18c7-4a62-81a2-cc969ac373fe @@ -165,16 +151,16 @@ def test_positive_create_with_system_admin_user(): login = gen_string('alpha') password = gen_string('alpha') org_name = gen_string('alpha') - make_user({'login': login, 'password': password}) - User.add_role({'login': login, 'role': 'System admin'}) - make_org({'user': login, 'password': password, 'name': org_name}) - result = Org.info({'name': org_name}) + module_target_sat.cli_factory.user({'login': login, 'password': password}) + module_target_sat.cli.User.add_role({'login': login, 'role': 'System admin'}) + module_target_sat.cli_factory.make_org({'user': login, 'password': password, 'name': org_name}) + result = module_target_sat.cli.Org.info({'name': org_name}) assert result['name'] == org_name @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_add_and_remove_subnets(module_org): +def test_positive_add_and_remove_subnets(module_org, module_target_sat): """add and remove a subnet from organization :id: adb5310b-76c5-4aca-8220-fdf0fe605cb0 @@ -189,19 +175,21 @@ def test_positive_add_and_remove_subnets(module_org): :CaseLevel: Integration """ - subnets = [make_subnet() for _ in range(0, 2)] - Org.add_subnet({'name': module_org.name, 'subnet': subnets[0]['name']}) - Org.add_subnet({'name': module_org.name, 'subnet-id': subnets[1]['id']}) - org_info = Org.info({'id': module_org.id}) + subnets = [module_target_sat.cli_factory.make_subnet() for _ in range(0, 2)] + module_target_sat.cli.Org.add_subnet({'name': module_org.name, 'subnet': subnets[0]['name']}) + module_target_sat.cli.Org.add_subnet({'name': module_org.name, 'subnet-id': subnets[1]['id']}) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert len(org_info['subnets']) == 2, "Failed to add subnets" - Org.remove_subnet({'name': module_org.name, 'subnet': subnets[0]['name']}) - Org.remove_subnet({'name': module_org.name, 'subnet-id': subnets[1]['id']}) - org_info = Org.info({'id': module_org.id}) + module_target_sat.cli.Org.remove_subnet({'name': module_org.name, 'subnet': subnets[0]['name']}) + module_target_sat.cli.Org.remove_subnet( + {'name': module_org.name, 'subnet-id': subnets[1]['id']} + ) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert len(org_info['subnets']) == 0, "Failed to remove subnets" @pytest.mark.tier2 -def test_positive_add_and_remove_users(module_org): +def test_positive_add_and_remove_users(module_org, module_target_sat): """Add and remove (admin) user to organization :id: c35b2e88-a65f-4eea-ba55-89cef59f30be @@ -218,39 +206,39 @@ def test_positive_add_and_remove_users(module_org): :CaseLevel: Integration """ - user = make_user() - admin_user = make_user({'admin': '1'}) + user = module_target_sat.cli_factory.user() + admin_user = module_target_sat.cli_factory.user({'admin': '1'}) assert admin_user['admin'] == 'yes' # add and remove user and admin user by name - Org.add_user({'name': module_org.name, 'user': user['login']}) - Org.add_user({'name': module_org.name, 'user': admin_user['login']}) - org_info = Org.info({'name': module_org.name}) + module_target_sat.cli.Org.add_user({'name': module_org.name, 'user': user['login']}) + module_target_sat.cli.Org.add_user({'name': module_org.name, 'user': admin_user['login']}) + org_info = module_target_sat.cli.Org.info({'name': module_org.name}) assert user['login'] in org_info['users'], "Failed to add user by name" assert admin_user['login'] in org_info['users'], "Failed to add admin user by name" - Org.remove_user({'name': module_org.name, 'user': user['login']}) - Org.remove_user({'name': module_org.name, 'user': admin_user['login']}) - org_info = Org.info({'name': module_org.name}) + module_target_sat.cli.Org.remove_user({'name': module_org.name, 'user': user['login']}) + module_target_sat.cli.Org.remove_user({'name': module_org.name, 'user': admin_user['login']}) + org_info = module_target_sat.cli.Org.info({'name': module_org.name}) assert user['login'] not in org_info['users'], "Failed to remove user by name" assert admin_user['login'] not in org_info['users'], "Failed to remove admin user by name" # add and remove user and admin user by id - Org.add_user({'id': module_org.id, 'user-id': user['id']}) - Org.add_user({'id': module_org.id, 'user-id': admin_user['id']}) - org_info = Org.info({'id': module_org.id}) + module_target_sat.cli.Org.add_user({'id': module_org.id, 'user-id': user['id']}) + module_target_sat.cli.Org.add_user({'id': module_org.id, 'user-id': admin_user['id']}) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert user['login'] in org_info['users'], "Failed to add user by id" assert admin_user['login'] in org_info['users'], "Failed to add admin user by id" - Org.remove_user({'id': module_org.id, 'user-id': user['id']}) - Org.remove_user({'id': module_org.id, 'user-id': admin_user['id']}) - org_info = Org.info({'id': module_org.id}) + module_target_sat.cli.Org.remove_user({'id': module_org.id, 'user-id': user['id']}) + module_target_sat.cli.Org.remove_user({'id': module_org.id, 'user-id': admin_user['id']}) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert user['login'] not in org_info['users'], "Failed to remove user by id" assert admin_user['login'] not in org_info['users'], "Failed to remove admin user by id" @pytest.mark.tier2 -def test_positive_add_and_remove_hostgroups(module_org): +def test_positive_add_and_remove_hostgroups(module_org, module_target_sat): """add and remove a hostgroup from an organization :id: 34e2c7c8-dc20-4709-a5a9-83c0dee9d84d @@ -265,16 +253,24 @@ def test_positive_add_and_remove_hostgroups(module_org): :CaseLevel: Integration """ - hostgroups = [make_hostgroup() for _ in range(0, 2)] + hostgroups = [module_target_sat.cli_factory.hostgroup() for _ in range(0, 2)] - Org.add_hostgroup({'hostgroup-id': hostgroups[0]['id'], 'id': module_org.id}) - Org.add_hostgroup({'hostgroup': hostgroups[1]['name'], 'name': module_org.name}) - org_info = Org.info({'name': module_org.name}) + module_target_sat.cli.Org.add_hostgroup( + {'hostgroup-id': hostgroups[0]['id'], 'id': module_org.id} + ) + module_target_sat.cli.Org.add_hostgroup( + {'hostgroup': hostgroups[1]['name'], 'name': module_org.name} + ) + org_info = module_target_sat.cli.Org.info({'name': module_org.name}) assert hostgroups[0]['name'] in org_info['hostgroups'], "Failed to add hostgroup by id" assert hostgroups[1]['name'] in org_info['hostgroups'], "Failed to add hostgroup by name" - Org.remove_hostgroup({'hostgroup-id': hostgroups[1]['id'], 'id': module_org.id}) - Org.remove_hostgroup({'hostgroup': hostgroups[0]['name'], 'name': module_org.name}) - org_info = Org.info({'id': module_org.id}) + module_target_sat.cli.Org.remove_hostgroup( + {'hostgroup-id': hostgroups[1]['id'], 'id': module_org.id} + ) + module_target_sat.cli.Org.remove_hostgroup( + {'hostgroup': hostgroups[0]['name'], 'name': module_org.name} + ) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert hostgroups[0]['name'] not in org_info['hostgroups'], "Failed to remove hostgroup by name" assert hostgroups[1]['name'] not in org_info['hostgroups'], "Failed to remove hostgroup by id" @@ -283,7 +279,7 @@ def test_positive_add_and_remove_hostgroups(module_org): @pytest.mark.tier2 @pytest.mark.libvirt_discovery @pytest.mark.upgrade -def test_positive_add_and_remove_compute_resources(module_org): +def test_positive_add_and_remove_compute_resources(module_org, module_target_sat): """Add and remove a compute resource from organization :id: 415c14ab-f879-4ed8-9ba7-8af4ada2e277 @@ -299,7 +295,7 @@ def test_positive_add_and_remove_compute_resources(module_org): :CaseLevel: Integration """ compute_resources = [ - make_compute_resource( + module_target_sat.cli_factory.compute_resource( { 'provider': FOREMAN_PROVIDERS['libvirt'], 'url': f'qemu+ssh://root@{settings.libvirt.libvirt_hostname}/system', @@ -307,21 +303,21 @@ def test_positive_add_and_remove_compute_resources(module_org): ) for _ in range(0, 2) ] - Org.add_compute_resource( + module_target_sat.cli.Org.add_compute_resource( {'compute-resource-id': compute_resources[0]['id'], 'id': module_org.id} ) - Org.add_compute_resource( + module_target_sat.cli.Org.add_compute_resource( {'compute-resource': compute_resources[1]['name'], 'name': module_org.name} ) - org_info = Org.info({'id': module_org.id}) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert len(org_info['compute-resources']) == 2, "Failed to add compute resources" - Org.remove_compute_resource( + module_target_sat.cli.Org.remove_compute_resource( {'compute-resource-id': compute_resources[0]['id'], 'id': module_org.id} ) - Org.remove_compute_resource( + module_target_sat.cli.Org.remove_compute_resource( {'compute-resource': compute_resources[1]['name'], 'name': module_org.name} ) - org_info = Org.info({'id': module_org.id}) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert ( compute_resources[0]['name'] not in org_info['compute-resources'] ), "Failed to remove compute resource by id" @@ -331,7 +327,7 @@ def test_positive_add_and_remove_compute_resources(module_org): @pytest.mark.tier2 -def test_positive_add_and_remove_media(module_org): +def test_positive_add_and_remove_media(module_org, module_target_sat): """Add and remove medium to organization :id: c2943a81-c8f7-44c4-926b-388055d7c290 @@ -346,15 +342,15 @@ def test_positive_add_and_remove_media(module_org): :CaseLevel: Integration """ - media = [make_medium() for _ in range(0, 2)] - Org.add_medium({'id': module_org.id, 'medium-id': media[0]['id']}) - Org.add_medium({'name': module_org.name, 'medium': media[1]['name']}) - org_info = Org.info({'id': module_org.id}) + media = [module_target_sat.cli_factory.make_medium() for _ in range(0, 2)] + module_target_sat.cli.Org.add_medium({'id': module_org.id, 'medium-id': media[0]['id']}) + module_target_sat.cli.Org.add_medium({'name': module_org.name, 'medium': media[1]['name']}) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert media[0]['name'] in org_info['installation-media'], "Failed to add medium by id" assert media[1]['name'] in org_info['installation-media'], "Failed to add medium by name" - Org.remove_medium({'name': module_org.name, 'medium': media[0]['name']}) - Org.remove_medium({'id': module_org.id, 'medium-id': media[1]['id']}) - org_info = Org.info({'id': module_org.id}) + module_target_sat.cli.Org.remove_medium({'name': module_org.name, 'medium': media[0]['name']}) + module_target_sat.cli.Org.remove_medium({'id': module_org.id, 'medium-id': media[1]['id']}) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert media[0]['name'] not in org_info['installation-media'], "Failed to remove medium by name" assert media[1]['name'] not in org_info['installation-media'], "Failed to remove medium by id" @@ -362,7 +358,7 @@ def test_positive_add_and_remove_media(module_org): @pytest.mark.tier2 @pytest.mark.skip_if_open("BZ:1845860") @pytest.mark.skip_if_open("BZ:1886876") -def test_positive_add_and_remove_templates(module_org): +def test_positive_add_and_remove_templates(module_org, module_target_sat): """Add and remove provisioning templates to organization :id: bd46a192-488f-4da0-bf47-1f370ae5f55c @@ -380,43 +376,47 @@ def test_positive_add_and_remove_templates(module_org): # create and remove templates by name name = list(valid_data_list().values())[0] - template = make_template({'content': gen_string('alpha'), 'name': name}) + template = module_target_sat.cli_factory.make_template( + {'content': gen_string('alpha'), 'name': name} + ) # Add provisioning-template - Org.add_provisioning_template( + module_target_sat.cli.Org.add_provisioning_template( {'name': module_org.name, 'provisioning-template': template['name']} ) - org_info = Org.info({'name': module_org.name}) + org_info = module_target_sat.cli.Org.info({'name': module_org.name}) assert ( f"{template['name']} ({template['type']})" in org_info['templates'] ), "Failed to add template by name" # Remove provisioning-template - Org.remove_provisioning_template( + module_target_sat.cli.Org.remove_provisioning_template( {'provisioning-template': template['name'], 'name': module_org.name} ) - org_info = Org.info({'name': module_org.name}) + org_info = module_target_sat.cli.Org.info({'name': module_org.name}) assert ( f"{template['name']} ({template['type']})" not in org_info['templates'] ), "Failed to remove template by name" # add and remove templates by id # Add provisioning-template - Org.add_provisioning_template({'provisioning-template-id': template['id'], 'id': module_org.id}) - org_info = Org.info({'id': module_org.id}) + module_target_sat.cli.Org.add_provisioning_template( + {'provisioning-template-id': template['id'], 'id': module_org.id} + ) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert ( f"{template['name']} ({template['type']})" in org_info['templates'] ), "Failed to add template by name" # Remove provisioning-template - Org.remove_provisioning_template( + module_target_sat.cli.Org.remove_provisioning_template( {'provisioning-template-id': template['id'], 'id': module_org.id} ) - org_info = Org.info({'id': module_org.id}) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert ( f"{template['name']} ({template['type']})" not in org_info['templates'] ), "Failed to remove template by id" @pytest.mark.tier2 -def test_positive_add_and_remove_domains(module_org): +def test_positive_add_and_remove_domains(module_org, module_target_sat): """Add and remove domains to organization :id: 97359ffe-4ce6-4e44-9e3f-583d3fdebbc8 @@ -431,22 +431,22 @@ def test_positive_add_and_remove_domains(module_org): :CaseLevel: Integration """ - domains = [make_domain() for _ in range(0, 2)] - Org.add_domain({'domain-id': domains[0]['id'], 'name': module_org.name}) - Org.add_domain({'domain': domains[1]['name'], 'name': module_org.name}) - org_info = Org.info({'id': module_org.id}) + domains = [module_target_sat.cli_factory.make_domain() for _ in range(0, 2)] + module_target_sat.cli.Org.add_domain({'domain-id': domains[0]['id'], 'name': module_org.name}) + module_target_sat.cli.Org.add_domain({'domain': domains[1]['name'], 'name': module_org.name}) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert len(org_info['domains']) == 2, "Failed to add domains" assert domains[0]['name'] in org_info['domains'] assert domains[1]['name'] in org_info['domains'] - Org.remove_domain({'domain': domains[0]['name'], 'name': module_org.name}) - Org.remove_domain({'domain-id': domains[1]['id'], 'id': module_org.id}) - org_info = Org.info({'id': module_org.id}) + module_target_sat.cli.Org.remove_domain({'domain': domains[0]['name'], 'name': module_org.name}) + module_target_sat.cli.Org.remove_domain({'domain-id': domains[1]['id'], 'id': module_org.id}) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert len(org_info['domains']) == 0, "Failed to remove domains" @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_add_and_remove_lce(module_org): +def test_positive_add_and_remove_lce(module_org, module_target_sat): """Remove a lifecycle environment from organization :id: bfa9198e-6078-4f10-b79a-3d7f51b835fd @@ -460,23 +460,25 @@ def test_positive_add_and_remove_lce(module_org): :CaseLevel: Integration """ # Create a lifecycle environment. - lc_env_name = make_lifecycle_environment({'organization-id': module_org.id})['name'] + lc_env_name = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + )['name'] lc_env_attrs = {'name': lc_env_name, 'organization-id': module_org.id} # Read back information about the lifecycle environment. Verify the # sanity of that information. - response = LifecycleEnvironment.list(lc_env_attrs) + response = module_target_sat.cli.LifecycleEnvironment.list(lc_env_attrs) assert response[0]['name'] == lc_env_name # Delete it. - LifecycleEnvironment.delete(lc_env_attrs) + module_target_sat.cli.LifecycleEnvironment.delete(lc_env_attrs) # We should get a zero-length response when searching for the LC env. - response = LifecycleEnvironment.list(lc_env_attrs) + response = module_target_sat.cli.LifecycleEnvironment.list(lc_env_attrs) assert len(response) == 0 @pytest.mark.run_in_one_thread @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_add_and_remove_capsules(proxy, module_org): +def test_positive_add_and_remove_capsules(proxy, module_org, module_target_sat): """Add and remove a capsule from organization :id: 71af64ec-5cbb-4dd8-ba90-652e302305ec @@ -489,23 +491,29 @@ def test_positive_add_and_remove_capsules(proxy, module_org): :CaseLevel: Integration """ - Org.add_smart_proxy({'id': module_org.id, 'smart-proxy-id': proxy['id']}) - org_info = Org.info({'name': module_org.name}) + module_target_sat.cli.Org.add_smart_proxy({'id': module_org.id, 'smart-proxy-id': proxy['id']}) + org_info = module_target_sat.cli.Org.info({'name': module_org.name}) assert proxy['name'] in org_info['smart-proxies'], "Failed to add capsule by id" - Org.remove_smart_proxy({'id': module_org.id, 'smart-proxy-id': proxy['id']}) - org_info = Org.info({'id': module_org.id}) + module_target_sat.cli.Org.remove_smart_proxy( + {'id': module_org.id, 'smart-proxy-id': proxy['id']} + ) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert proxy['name'] not in org_info['smart-proxies'], "Failed to remove capsule by id" - Org.add_smart_proxy({'name': module_org.name, 'smart-proxy': proxy['name']}) - org_info = Org.info({'name': module_org.name}) + module_target_sat.cli.Org.add_smart_proxy( + {'name': module_org.name, 'smart-proxy': proxy['name']} + ) + org_info = module_target_sat.cli.Org.info({'name': module_org.name}) assert proxy['name'] in org_info['smart-proxies'], "Failed to add capsule by name" - Org.remove_smart_proxy({'name': module_org.name, 'smart-proxy': proxy['name']}) - org_info = Org.info({'name': module_org.name}) + module_target_sat.cli.Org.remove_smart_proxy( + {'name': module_org.name, 'smart-proxy': proxy['name']} + ) + org_info = module_target_sat.cli.Org.info({'name': module_org.name}) assert proxy['name'] not in org_info['smart-proxies'], "Failed to add capsule by name" @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_add_and_remove_locations(module_org): +def test_positive_add_and_remove_locations(module_org, module_target_sat): """Add and remove a locations from organization :id: 37b63e5c-8fd5-439c-9540-972b597b590a @@ -520,22 +528,30 @@ def test_positive_add_and_remove_locations(module_org): :CaseLevel: Integration """ - locations = [make_location() for _ in range(0, 2)] - Org.add_location({'location-id': locations[0]['id'], 'name': module_org.name}) - Org.add_location({'location': locations[1]['name'], 'name': module_org.name}) - org_info = Org.info({'id': module_org.id}) + locations = [module_target_sat.cli_factory.make_location() for _ in range(0, 2)] + module_target_sat.cli.Org.add_location( + {'location-id': locations[0]['id'], 'name': module_org.name} + ) + module_target_sat.cli.Org.add_location( + {'location': locations[1]['name'], 'name': module_org.name} + ) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert len(org_info['locations']) == 2, "Failed to add locations" assert locations[0]['name'] in org_info['locations'] assert locations[1]['name'] in org_info['locations'] - Org.remove_location({'location-id': locations[0]['id'], 'id': module_org.id}) - Org.remove_location({'location': locations[1]['name'], 'id': module_org.id}) - org_info = Org.info({'id': module_org.id}) + module_target_sat.cli.Org.remove_location( + {'location-id': locations[0]['id'], 'id': module_org.id} + ) + module_target_sat.cli.Org.remove_location( + {'location': locations[1]['name'], 'id': module_org.id} + ) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert not org_info.get('locations'), "Failed to remove locations" @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_add_and_remove_parameter(module_org): +def test_positive_add_and_remove_parameter(module_org, module_target_sat): """Remove a parameter from organization :id: e4099279-4e73-4c14-9e7c-912b3787b99f @@ -547,34 +563,36 @@ def test_positive_add_and_remove_parameter(module_org): param_name = gen_string('alpha') param_new_value = gen_string('alpha') - org_info = Org.info({'id': module_org.id}) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert len(org_info['parameters']) == 0 # Create parameter - Org.set_parameter( + module_target_sat.cli.Org.set_parameter( {'name': param_name, 'value': gen_string('alpha'), 'organization-id': module_org.id} ) - org_info = Org.info({'id': module_org.id}) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert len(org_info['parameters']) == 1 # Update - Org.set_parameter( + module_target_sat.cli.Org.set_parameter( {'name': param_name, 'value': param_new_value, 'organization': module_org.name} ) - org_info = Org.info({'id': module_org.id}) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert len(org_info['parameters']) == 1 assert param_new_value == org_info['parameters'][param_name.lower()] # Delete parameter - Org.delete_parameter({'name': param_name, 'organization': module_org.name}) - org_info = Org.info({'id': module_org.id}) + module_target_sat.cli.Org.delete_parameter( + {'name': param_name, 'organization': module_org.name} + ) + org_info = module_target_sat.cli.Org.info({'id': module_org.id}) assert len(org_info['parameters']) == 0 assert param_name.lower() not in org_info['parameters'] @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) -def test_negative_create_with_invalid_name(name): +def test_negative_create_with_invalid_name(name, module_target_sat): """Try to create an organization with invalid name, but valid label and description @@ -586,7 +604,7 @@ def test_negative_create_with_invalid_name(name): """ with pytest.raises(CLIFactoryError): - make_org( + module_target_sat.cli_factory.make_org( { 'description': gen_string('alpha'), 'label': gen_string('alpha'), @@ -596,7 +614,7 @@ def test_negative_create_with_invalid_name(name): @pytest.mark.tier1 -def test_negative_create_same_name(module_org): +def test_negative_create_same_name(module_org, module_target_sat): """Create a new organization with same name, description, and label. :id: 07924e1f-1eff-4bae-b0db-e41b84966bc1 @@ -606,7 +624,7 @@ def test_negative_create_same_name(module_org): :CaseImportance: Critical """ with pytest.raises(CLIFactoryError): - make_org( + module_target_sat.cli_factory.make_org( { 'description': module_org.description, 'label': module_org.label, @@ -616,7 +634,7 @@ def test_negative_create_same_name(module_org): @pytest.mark.tier1 -def test_positive_update(module_org): +def test_positive_update(module_org, module_target_sat): """Update organization name and description :id: 66581003-f5d9-443c-8cd6-00f68087e8e9 @@ -630,19 +648,19 @@ def test_positive_update(module_org): new_desc = list(valid_data_list().values())[0] # upgrade name - Org.update({'id': module_org.id, 'new-name': new_name}) - org = Org.info({'id': module_org.id}) + module_target_sat.cli.Org.update({'id': module_org.id, 'new-name': new_name}) + org = module_target_sat.cli.Org.info({'id': module_org.id}) assert org['name'] == new_name # upgrade description - Org.update({'description': new_desc, 'id': org['id']}) - org = Org.info({'id': org['id']}) + module_target_sat.cli.Org.update({'description': new_desc, 'id': org['id']}) + org = module_target_sat.cli.Org.info({'id': org['id']}) assert org['description'] == new_desc @pytest.mark.tier1 @pytest.mark.parametrize('new_name', **parametrized(invalid_values_list())) -def test_negative_update_name(new_name, module_org): +def test_negative_update_name(new_name, module_org, module_target_sat): """Fail to update organization name for invalid values. :id: 582d41b8-370d-45ed-9b7b-8096608e1324 @@ -653,11 +671,11 @@ def test_negative_update_name(new_name, module_org): """ with pytest.raises(CLIReturnCodeError): - Org.update({'id': module_org.id, 'new-name': new_name}) + module_target_sat.cli.Org.update({'id': module_org.id, 'new-name': new_name}) @pytest.mark.tier2 -def test_positive_create_user_with_timezone(module_org): +def test_positive_create_user_with_timezone(module_org, module_target_sat): """Create and remove user with valid timezone in an organization :id: b9b92c00-ee99-4da2-84c5-0a576a862100 @@ -686,11 +704,11 @@ def test_positive_create_user_with_timezone(module_org): 'Samoa', ] for timezone in users_timezones: - user = make_user({'timezone': timezone, 'admin': '1'}) - Org.add_user({'name': module_org.name, 'user': user['login']}) - org_info = Org.info({'name': module_org.name}) + user = module_target_sat.cli_factory.user({'timezone': timezone, 'admin': '1'}) + module_target_sat.cli.Org.add_user({'name': module_org.name, 'user': user['login']}) + org_info = module_target_sat.cli.Org.info({'name': module_org.name}) assert user['login'] in org_info['users'] assert user['timezone'] == timezone - Org.remove_user({'id': module_org.id, 'user-id': user['id']}) - org_info = Org.info({'name': module_org.name}) + module_target_sat.cli.Org.remove_user({'id': module_org.id, 'user-id': user['id']}) + org_info = module_target_sat.cli.Org.info({'name': module_org.name}) assert user['login'] not in org_info['users'] diff --git a/tests/foreman/cli/test_oscap.py b/tests/foreman/cli/test_oscap.py index 5088d25fb60..f2430235a57 100644 --- a/tests/foreman/cli/test_oscap.py +++ b/tests/foreman/cli/test_oscap.py @@ -20,19 +20,9 @@ from nailgun import entities import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import ( - CLIFactoryError, - make_hostgroup, - make_scap_policy, - make_scapcontent, - make_tailoringfile, -) -from robottelo.cli.host import Host -from robottelo.cli.scap_policy import Scappolicy -from robottelo.cli.scapcontent import Scapcontent from robottelo.config import settings from robottelo.constants import OSCAP_DEFAULT_CONTENT, OSCAP_PERIOD, OSCAP_WEEKDAY +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.utils.datafactory import ( invalid_names_list, parametrized, @@ -44,20 +34,20 @@ class TestOpenScap: """Tests related to the oscap cli hammer plugin""" @classmethod - def fetch_scap_and_profile_id(cls, scap_name): + def fetch_scap_and_profile_id(cls, scap_name, sat): """Extracts the scap ID and scap profile id :param scap_name: Scap title :returns: scap_id and scap_profile_id """ - default_content = Scapcontent.info({'title': scap_name}, output_format='json') + default_content = sat.cli.Scapcontent.info({'title': scap_name}, output_format='json') scap_id = default_content['id'] scap_profile_ids = default_content['scap-content-profiles'][0]['id'] return scap_id, scap_profile_ids @pytest.mark.tier1 - def test_positive_list_default_content_with_admin(self): + def test_positive_list_default_content_with_admin(self, module_target_sat): """List the default scap content with admin account :id: 32c41c22-6aef-424e-8e69-a65c00f1c811 @@ -81,13 +71,13 @@ def test_positive_list_default_content_with_admin(self): :CaseImportance: Medium """ - scap_contents = [content['title'] for content in Scapcontent.list()] + scap_contents = [content['title'] for content in module_target_sat.cli.Scapcontent.list()] for title in OSCAP_DEFAULT_CONTENT.values(): assert title in scap_contents @pytest.mark.tier1 def test_negative_list_default_content_with_viewer_role( - self, scap_content, default_viewer_role + self, scap_content, default_viewer_role, module_target_sat ): """List the default scap content by user with viewer role @@ -110,17 +100,17 @@ def test_negative_list_default_content_with_viewer_role( :CaseImportance: Medium """ - result = Scapcontent.with_user( + result = module_target_sat.cli.Scapcontent.with_user( default_viewer_role.login, default_viewer_role.password ).list() assert len(result) == 0 with pytest.raises(CLIReturnCodeError): - Scapcontent.with_user(default_viewer_role.login, default_viewer_role.password).info( - {'title': scap_content['title']} - ) + module_target_sat.cli.Scapcontent.with_user( + default_viewer_role.login, default_viewer_role.password + ).info({'title': scap_content['title']}) @pytest.mark.tier1 - def test_positive_view_scap_content_info_admin(self): + def test_positive_view_scap_content_info_admin(self, module_target_sat): """View info of scap content with admin account :id: 539ea982-0701-43f5-bb91-e566e6687e35 @@ -142,12 +132,14 @@ def test_positive_view_scap_content_info_admin(self): :CaseImportance: Medium """ title = gen_string('alpha') - make_scapcontent({'title': title, 'scap-file': settings.oscap.content_path}) - result = Scapcontent.info({'title': title}) + module_target_sat.cli_factory.scapcontent( + {'title': title, 'scap-file': settings.oscap.content_path} + ) + result = module_target_sat.cli.Scapcontent.info({'title': title}) assert result['title'] == title @pytest.mark.tier1 - def test_negative_info_scap_content(self): + def test_negative_info_scap_content(self, module_target_sat): """View info of scap content with invalid ID as parameter :id: 86f44fb1-2e2b-4004-83c1-4a62162ebea9 @@ -170,11 +162,11 @@ def test_negative_info_scap_content(self): """ invalid_scap_id = gen_string('alpha') with pytest.raises(CLIReturnCodeError): - Scapcontent.info({'id': invalid_scap_id}) + module_target_sat.cli.Scapcontent.info({'id': invalid_scap_id}) @pytest.mark.parametrize('title', **parametrized(valid_data_list())) @pytest.mark.tier1 - def test_positive_create_scap_content_with_valid_title(self, title): + def test_positive_create_scap_content_with_valid_title(self, title, module_target_sat): """Create scap-content with valid title :id: 68e9fbe2-e3c3-48e7-a774-f1260a3b7f4f @@ -199,11 +191,13 @@ def test_positive_create_scap_content_with_valid_title(self, title): :CaseImportance: Medium """ - scap_content = make_scapcontent({'title': title, 'scap-file': settings.oscap.content_path}) + scap_content = module_target_sat.cli_factory.scapcontent( + {'title': title, 'scap-file': settings.oscap.content_path} + ) assert scap_content['title'] == title @pytest.mark.tier1 - def test_negative_create_scap_content_with_same_title(self): + def test_negative_create_scap_content_with_same_title(self, module_target_sat): """Create scap-content with same title :id: a8cbacc9-456a-4f6f-bd0e-4d1167a8b401 @@ -231,14 +225,18 @@ def test_negative_create_scap_content_with_same_title(self): :CaseImportance: Medium """ title = gen_string('alpha') - scap_content = make_scapcontent({'title': title, 'scap-file': settings.oscap.content_path}) + scap_content = module_target_sat.cli_factory.scapcontent( + {'title': title, 'scap-file': settings.oscap.content_path} + ) assert scap_content['title'] == title with pytest.raises(CLIFactoryError): - make_scapcontent({'title': title, 'scap-file': settings.oscap.content_path}) + module_target_sat.cli_factory.scapcontent( + {'title': title, 'scap-file': settings.oscap.content_path} + ) @pytest.mark.parametrize('title', **parametrized(invalid_names_list())) @pytest.mark.tier1 - def test_negative_create_scap_content_with_invalid_title(self, title): + def test_negative_create_scap_content_with_invalid_title(self, title, module_target_sat): """Create scap-content with invalid title :id: 90a2590e-a6ff-41f1-9e0a-67d4b16435c0 @@ -262,11 +260,15 @@ def test_negative_create_scap_content_with_invalid_title(self, title): :CaseImportance: Medium """ with pytest.raises(CLIFactoryError): - make_scapcontent({'title': title, 'scap-file': settings.oscap.content_path}) + module_target_sat.cli_factory.scapcontent( + {'title': title, 'scap-file': settings.oscap.content_path} + ) @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 - def test_positive_create_scap_content_with_valid_originalfile_name(self, name): + def test_positive_create_scap_content_with_valid_originalfile_name( + self, name, module_target_sat + ): """Create scap-content with valid original file name :id: 25441174-11cb-4d9b-9ec5-b1c69411b5bc @@ -289,14 +291,16 @@ def test_positive_create_scap_content_with_valid_originalfile_name(self, name): :CaseImportance: Medium """ - scap_content = make_scapcontent( + scap_content = module_target_sat.cli_factory.scapcontent( {'original-filename': name, 'scap-file': settings.oscap.content_path} ) assert scap_content['original-filename'] == name @pytest.mark.parametrize('name', **parametrized(invalid_names_list())) @pytest.mark.tier1 - def test_negative_create_scap_content_with_invalid_originalfile_name(self, name): + def test_negative_create_scap_content_with_invalid_originalfile_name( + self, name, module_target_sat + ): """Create scap-content with invalid original file name :id: 83feb67a-a6bf-4a99-923d-889e8d1013fa @@ -322,11 +326,13 @@ def test_negative_create_scap_content_with_invalid_originalfile_name(self, name) :BZ: 1482395 """ with pytest.raises(CLIFactoryError): - make_scapcontent({'original-filename': name, 'scap-file': settings.oscap.content_path}) + module_target_sat.cli_factory.scapcontent( + {'original-filename': name, 'scap-file': settings.oscap.content_path} + ) @pytest.mark.parametrize('title', **parametrized(valid_data_list())) @pytest.mark.tier1 - def test_negative_create_scap_content_without_dsfile(self, title): + def test_negative_create_scap_content_without_dsfile(self, title, module_target_sat): """Create scap-content without scap data stream xml file :id: ea811994-12cd-4382-9382-37fa806cc26f @@ -349,10 +355,10 @@ def test_negative_create_scap_content_without_dsfile(self, title): :CaseImportance: Medium """ with pytest.raises(CLIFactoryError): - make_scapcontent({'title': title}) + module_target_sat.cli_factory.scapcontent({'title': title}) @pytest.mark.tier1 - def test_positive_update_scap_content_with_newtitle(self): + def test_positive_update_scap_content_with_newtitle(self, module_target_sat): """Update scap content title :id: 2c32e94a-237d-40b9-8a3b-fca2ef26fe79 @@ -376,14 +382,16 @@ def test_positive_update_scap_content_with_newtitle(self): """ title = gen_string('alpha') new_title = gen_string('alpha') - scap_content = make_scapcontent({'title': title, 'scap-file': settings.oscap.content_path}) + scap_content = module_target_sat.cli_factory.scapcontent( + {'title': title, 'scap-file': settings.oscap.content_path} + ) assert scap_content['title'] == title - Scapcontent.update({'title': title, 'new-title': new_title}) - result = Scapcontent.info({'title': new_title}, output_format='json') + module_target_sat.cli.Scapcontent.update({'title': title, 'new-title': new_title}) + result = module_target_sat.cli.Scapcontent.info({'title': new_title}, output_format='json') assert result['title'] == new_title @pytest.mark.tier1 - def test_positive_delete_scap_content_with_id(self): + def test_positive_delete_scap_content_with_id(self, module_target_sat): """Delete a scap content with id as parameter :id: 11ae7652-65e0-4751-b1e0-246b27919238 @@ -403,13 +411,15 @@ def test_positive_delete_scap_content_with_id(self): :CaseImportance: Medium """ - scap_content = make_scapcontent({'scap-file': settings.oscap.content_path}) - Scapcontent.delete({'id': scap_content['id']}) + scap_content = module_target_sat.cli_factory.scapcontent( + {'scap-file': settings.oscap.content_path} + ) + module_target_sat.cli.Scapcontent.delete({'id': scap_content['id']}) with pytest.raises(CLIReturnCodeError): - Scapcontent.info({'id': scap_content['id']}) + module_target_sat.cli.Scapcontent.info({'id': scap_content['id']}) @pytest.mark.tier1 - def test_positive_delete_scap_content_with_title(self): + def test_positive_delete_scap_content_with_title(self, module_target_sat): """Delete a scap content with title as parameter :id: aa4ca830-3250-4517-b40c-0256cdda5e0a @@ -431,14 +441,18 @@ def test_positive_delete_scap_content_with_title(self): :CaseImportance: Medium """ - scap_content = make_scapcontent({'scap-file': settings.oscap.content_path}) - Scapcontent.delete({'title': scap_content['title']}) + scap_content = module_target_sat.cli_factory.scapcontent( + {'scap-file': settings.oscap.content_path} + ) + module_target_sat.cli.Scapcontent.delete({'title': scap_content['title']}) with pytest.raises(CLIReturnCodeError): - Scapcontent.info({'title': scap_content['title']}) + module_target_sat.cli.Scapcontent.info({'title': scap_content['title']}) @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier2 - def test_postive_create_scap_policy_with_valid_name(self, name, scap_content): + def test_postive_create_scap_policy_with_valid_name( + self, name, scap_content, module_target_sat + ): """Create scap policy with valid name :id: c9327675-62b2-4e22-933a-02818ef68c11 @@ -460,7 +474,7 @@ def test_postive_create_scap_policy_with_valid_name(self, name, scap_content): :CaseImportance: Medium """ - scap_policy = make_scap_policy( + scap_policy = module_target_sat.cli_factory.make_scap_policy( { 'name': name, 'deploy-by': 'ansible', @@ -472,13 +486,15 @@ def test_postive_create_scap_policy_with_valid_name(self, name, scap_content): ) assert scap_policy['name'] == name # Deleting policy which created for all valid input (ex- latin1, cjk, utf-8, etc.) - Scappolicy.delete({'name': scap_policy['name']}) + module_target_sat.cli.Scappolicy.delete({'name': scap_policy['name']}) with pytest.raises(CLIReturnCodeError): - Scappolicy.info({'name': scap_policy['name']}) + module_target_sat.cli.Scappolicy.info({'name': scap_policy['name']}) @pytest.mark.parametrize('name', **parametrized(invalid_names_list())) @pytest.mark.tier2 - def test_negative_create_scap_policy_with_invalid_name(self, name, scap_content): + def test_negative_create_scap_policy_with_invalid_name( + self, name, scap_content, module_target_sat + ): """Create scap policy with invalid name :id: 0d163968-7759-4cfd-9c4d-98533d8db925 @@ -501,7 +517,7 @@ def test_negative_create_scap_policy_with_invalid_name(self, name, scap_content) :CaseImportance: Medium """ with pytest.raises(CLIFactoryError): - make_scap_policy( + module_target_sat.cli_factory.make_scap_policy( { 'name': name, 'deploy-by': 'ansible', @@ -513,7 +529,7 @@ def test_negative_create_scap_policy_with_invalid_name(self, name, scap_content) ) @pytest.mark.tier2 - def test_negative_create_scap_policy_without_content(self, scap_content): + def test_negative_create_scap_policy_without_content(self, scap_content, module_target_sat): """Create scap policy without scap content :id: 88a8fba3-f45a-4e22-9ee1-f0d701f1135f @@ -534,7 +550,7 @@ def test_negative_create_scap_policy_without_content(self, scap_content): :CaseImportance: Medium """ with pytest.raises(CLIFactoryError): - make_scap_policy( + module_target_sat.cli_factory.make_scap_policy( { 'deploy-by': 'ansible', 'scap-content-profile-id': scap_content["scap_profile_id"], @@ -544,7 +560,7 @@ def test_negative_create_scap_policy_without_content(self, scap_content): ) @pytest.mark.tier2 - def test_positive_associate_scap_policy_with_hostgroups(self, scap_content): + def test_positive_associate_scap_policy_with_hostgroups(self, scap_content, module_target_sat): """Associate hostgroups to scap policy :id: 916403a0-572d-4cf3-9155-3e3d0373577f @@ -566,9 +582,9 @@ def test_positive_associate_scap_policy_with_hostgroups(self, scap_content): :CaseImportance: Medium """ - hostgroup = make_hostgroup() + hostgroup = module_target_sat.cli_factory.hostgroup() name = gen_string('alphanumeric') - scap_policy = make_scap_policy( + scap_policy = module_target_sat.cli_factory.make_scap_policy( { 'name': name, 'deploy-by': 'ansible', @@ -582,7 +598,9 @@ def test_positive_associate_scap_policy_with_hostgroups(self, scap_content): assert scap_policy['hostgroups'][0] == hostgroup['name'] @pytest.mark.tier2 - def test_positive_associate_scap_policy_with_hostgroup_via_ansible(self, scap_content): + def test_positive_associate_scap_policy_with_hostgroup_via_ansible( + self, scap_content, module_target_sat + ): """Associate hostgroup to scap policy via ansible :id: 2df303c6-bff5-4977-a865-a3afabfb8726 @@ -604,9 +622,9 @@ def test_positive_associate_scap_policy_with_hostgroup_via_ansible(self, scap_co :expectedresults: The policy is created via ansible deploy option and associated successfully. """ - hostgroup = make_hostgroup() + hostgroup = module_target_sat.cli_factory.hostgroup() name = gen_string('alphanumeric') - scap_policy = make_scap_policy( + scap_policy = module_target_sat.cli_factory.make_scap_policy( { 'name': name, 'deploy-by': 'ansible', @@ -624,7 +642,7 @@ def test_positive_associate_scap_policy_with_hostgroup_via_ansible(self, scap_co @pytest.mark.upgrade @pytest.mark.tier2 def test_positive_associate_scap_policy_with_tailoringfiles( - self, deploy, scap_content, tailoring_file_path + self, deploy, scap_content, tailoring_file_path, module_target_sat ): """Associate tailoring file by name/id to scap policy with all deployments @@ -641,12 +659,16 @@ def test_positive_associate_scap_policy_with_tailoringfiles( :expectedresults: The policy is created and associated successfully. """ - tailoring_file_a = make_tailoringfile({'scap-file': tailoring_file_path['satellite']}) + tailoring_file_a = module_target_sat.cli_factory.tailoringfile( + {'scap-file': tailoring_file_path['satellite']} + ) tailoring_file_profile_a_id = tailoring_file_a['tailoring-file-profiles'][0]['id'] - tailoring_file_b = make_tailoringfile({'scap-file': tailoring_file_path['satellite']}) + tailoring_file_b = module_target_sat.cli_factory.tailoringfile( + {'scap-file': tailoring_file_path['satellite']} + ) tailoring_file_profile_b_id = tailoring_file_b['tailoring-file-profiles'][0]['id'] - scap_policy = make_scap_policy( + scap_policy = module_target_sat.cli_factory.make_scap_policy( { 'scap-content-id': scap_content["scap_id"], 'deploy-by': deploy, @@ -661,22 +683,22 @@ def test_positive_associate_scap_policy_with_tailoringfiles( assert scap_policy['tailoring-file-id'] == tailoring_file_a['id'] assert scap_policy['tailoring-file-profile-id'] == tailoring_file_profile_a_id - Scappolicy.update( + module_target_sat.cli.Scappolicy.update( { 'name': scap_policy['name'], 'tailoring-file': tailoring_file_b['name'], 'tailoring-file-profile-id': tailoring_file_profile_b_id, } ) - scap_info = Scappolicy.info({'name': scap_policy['name']}) + scap_info = module_target_sat.cli.Scappolicy.info({'name': scap_policy['name']}) assert scap_info['tailoring-file-id'] == tailoring_file_b['id'] assert scap_info['tailoring-file-profile-id'] == tailoring_file_profile_b_id - Scappolicy.delete({'name': scap_policy['name']}) + module_target_sat.cli.Scappolicy.delete({'name': scap_policy['name']}) with pytest.raises(CLIReturnCodeError): - Scapcontent.info({'name': scap_policy['name']}) + module_target_sat.cli.Scapcontent.info({'name': scap_policy['name']}) - scap_policy = make_scap_policy( + scap_policy = module_target_sat.cli_factory.make_scap_policy( { 'scap-content-id': scap_content["scap_id"], 'deploy-by': deploy, @@ -691,26 +713,26 @@ def test_positive_associate_scap_policy_with_tailoringfiles( assert scap_policy['tailoring-file-id'] == tailoring_file_a['id'] assert scap_policy['tailoring-file-profile-id'] == tailoring_file_profile_a_id - Scappolicy.update( + module_target_sat.cli.Scappolicy.update( { 'id': scap_policy['id'], 'tailoring-file-id': tailoring_file_b['id'], 'tailoring-file-profile-id': tailoring_file_profile_b_id, } ) - scap_info = Scappolicy.info({'id': scap_policy['id']}) + scap_info = module_target_sat.cli.Scappolicy.info({'id': scap_policy['id']}) assert scap_info['tailoring-file-id'] == tailoring_file_b['id'] assert scap_info['tailoring-file-profile-id'] == tailoring_file_profile_b_id - Scappolicy.delete({'id': scap_policy['id']}) + module_target_sat.cli.Scappolicy.delete({'id': scap_policy['id']}) with pytest.raises(CLIReturnCodeError): - Scapcontent.info({'name': scap_policy['name']}) + module_target_sat.cli.Scapcontent.info({'name': scap_policy['name']}) @pytest.mark.parametrize('deploy', **parametrized(['manual', 'ansible'])) @pytest.mark.upgrade @pytest.mark.tier2 @pytest.mark.e2e - def test_positive_scap_policy_end_to_end(self, deploy, scap_content): + def test_positive_scap_policy_end_to_end(self, deploy, scap_content, module_target_sat): """List all scap policies and read info using id, name :id: d14ab43e-c7a9-4eee-b61c-420b07ca1da9 @@ -735,9 +757,9 @@ def test_positive_scap_policy_end_to_end(self, deploy, scap_content): :CaseImportance: Critical """ - hostgroup = make_hostgroup() + hostgroup = module_target_sat.cli_factory.hostgroup() name = gen_string('alphanumeric') - scap_policy = make_scap_policy( + scap_policy = module_target_sat.cli_factory.make_scap_policy( { 'name': name, 'deploy-by': deploy, @@ -748,28 +770,31 @@ def test_positive_scap_policy_end_to_end(self, deploy, scap_content): 'hostgroups': hostgroup['name'], } ) - result = Scappolicy.list() + result = module_target_sat.cli.Scappolicy.list() assert name in [policy['name'] for policy in result] - assert Scappolicy.info({'id': scap_policy['id']})['id'] == scap_policy['id'] - assert Scappolicy.info({'name': scap_policy['name']})['name'] == name + assert ( + module_target_sat.cli.Scappolicy.info({'id': scap_policy['id']})['id'] + == scap_policy['id'] + ) + assert module_target_sat.cli.Scappolicy.info({'name': scap_policy['name']})['name'] == name - Scappolicy.update( + module_target_sat.cli.Scappolicy.update( { 'id': scap_policy['id'], 'period': OSCAP_PERIOD['monthly'].lower(), 'day-of-month': 15, } ) - scap_info = Scappolicy.info({'name': name}) + scap_info = module_target_sat.cli.Scappolicy.info({'name': name}) assert scap_info['period'] == OSCAP_PERIOD['monthly'].lower() assert scap_info['day-of-month'] == '15' - Scappolicy.delete({'id': scap_policy['id']}) + module_target_sat.cli.Scappolicy.delete({'id': scap_policy['id']}) with pytest.raises(CLIReturnCodeError): - Scappolicy.info({'id': scap_policy['id']}) + module_target_sat.cli.Scappolicy.info({'id': scap_policy['id']}) @pytest.mark.upgrade @pytest.mark.tier2 - def test_positive_update_scap_policy_with_hostgroup(self, scap_content): + def test_positive_update_scap_policy_with_hostgroup(self, scap_content, module_target_sat): """Update scap policy by addition of hostgroup :id: 21b9b82b-7c6c-4944-bc2f-67631e1d4086 @@ -790,9 +815,9 @@ def test_positive_update_scap_policy_with_hostgroup(self, scap_content): :CaseImportance: Medium """ - hostgroup = make_hostgroup() + hostgroup = module_target_sat.cli_factory.hostgroup() name = gen_string('alphanumeric') - scap_policy = make_scap_policy( + scap_policy = module_target_sat.cli_factory.make_scap_policy( { 'name': name, 'deploy-by': 'ansible', @@ -805,17 +830,17 @@ def test_positive_update_scap_policy_with_hostgroup(self, scap_content): ) assert scap_policy['hostgroups'][0] == hostgroup['name'] assert scap_policy['deployment-option'] == 'ansible' - new_hostgroup = make_hostgroup() - Scappolicy.update( + new_hostgroup = module_target_sat.cli_factory.hostgroup() + module_target_sat.cli.Scappolicy.update( {'id': scap_policy['id'], 'deploy-by': 'ansible', 'hostgroups': new_hostgroup['name']} ) - scap_info = Scappolicy.info({'name': name}) + scap_info = module_target_sat.cli.Scappolicy.info({'name': name}) assert scap_info['hostgroups'][0] == new_hostgroup['name'] # Assert if the deployment is updated assert scap_info['deployment-option'] == 'ansible' @pytest.mark.tier2 - def test_positive_update_scap_policy_period(self, scap_content): + def test_positive_update_scap_policy_period(self, scap_content, module_target_sat): """Update scap policy by updating the period strategy from monthly to weekly @@ -838,7 +863,7 @@ def test_positive_update_scap_policy_period(self, scap_content): :CaseImportance: Medium """ name = gen_string('alphanumeric') - scap_policy = make_scap_policy( + scap_policy = module_target_sat.cli_factory.make_scap_policy( { 'name': name, 'deploy-by': 'ansible', @@ -849,20 +874,20 @@ def test_positive_update_scap_policy_period(self, scap_content): } ) assert scap_policy['period'] == OSCAP_PERIOD['weekly'].lower() - Scappolicy.update( + module_target_sat.cli.Scappolicy.update( { 'id': scap_policy['id'], 'period': OSCAP_PERIOD['monthly'].lower(), 'day-of-month': 15, } ) - scap_info = Scappolicy.info({'name': name}) + scap_info = module_target_sat.cli.Scappolicy.info({'name': name}) assert scap_info['period'] == OSCAP_PERIOD['monthly'].lower() assert scap_info['day-of-month'] == '15' @pytest.mark.tier2 @pytest.mark.upgrade - def test_positive_update_scap_policy_with_content(self, scap_content): + def test_positive_update_scap_policy_with_content(self, scap_content, module_target_sat): """Update the scap policy by updating the scap content associated with the policy @@ -885,7 +910,7 @@ def test_positive_update_scap_policy_with_content(self, scap_content): :CaseImportance: Medium """ name = gen_string('alphanumeric') - scap_policy = make_scap_policy( + scap_policy = module_target_sat.cli_factory.make_scap_policy( { 'name': name, 'deploy-by': 'ansible', @@ -897,17 +922,19 @@ def test_positive_update_scap_policy_with_content(self, scap_content): ) assert scap_policy['scap-content-id'] == scap_content["scap_id"] scap_id, scap_profile_id = self.fetch_scap_and_profile_id( - OSCAP_DEFAULT_CONTENT['rhel_firefox'] + OSCAP_DEFAULT_CONTENT['rhel_firefox'], module_target_sat ) - Scappolicy.update( + module_target_sat.cli.Scappolicy.update( {'name': name, 'scap-content-id': scap_id, 'scap-content-profile-id': scap_profile_id} ) - scap_info = Scappolicy.info({'name': name}) + scap_info = module_target_sat.cli.Scappolicy.info({'name': name}) assert scap_info['scap-content-id'] == scap_id assert scap_info['scap-content-profile-id'] == scap_profile_id @pytest.mark.tier2 - def test_positive_associate_scap_policy_with_single_server(self, scap_content): + def test_positive_associate_scap_policy_with_single_server( + self, scap_content, module_target_sat + ): """Assign an audit policy to a single server :id: 30566c27-f466-4b4d-beaf-0a5bfda98b89 @@ -931,7 +958,7 @@ def test_positive_associate_scap_policy_with_single_server(self, scap_content): host = entities.Host() host.create() name = gen_string('alpha') - scap_policy = make_scap_policy( + scap_policy = module_target_sat.cli_factory.make_scap_policy( { 'name': name, 'deploy-by': 'ansible', @@ -942,8 +969,10 @@ def test_positive_associate_scap_policy_with_single_server(self, scap_content): } ) host_name = host.name + "." + host.domain.name - Scappolicy.update({'id': scap_policy['id'], 'hosts': host_name}) - hosts = Host.list({'search': 'compliance_policy_id = {}'.format(scap_policy['id'])}) + module_target_sat.cli.Scappolicy.update({'id': scap_policy['id'], 'hosts': host_name}) + hosts = module_target_sat.cli.Host.list( + {'search': 'compliance_policy_id = {}'.format(scap_policy['id'])} + ) assert host_name in [host['name'] for host in hosts] @pytest.mark.stubbed diff --git a/tests/foreman/cli/test_oscap_tailoringfiles.py b/tests/foreman/cli/test_oscap_tailoringfiles.py index 285a907df3b..3f36bff3288 100644 --- a/tests/foreman/cli/test_oscap_tailoringfiles.py +++ b/tests/foreman/cli/test_oscap_tailoringfiles.py @@ -19,10 +19,8 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import CLIFactoryError, make_tailoringfile -from robottelo.cli.scap_tailoring_files import TailoringFiles from robottelo.constants import SNIPPET_DATA_FILE, DataFile +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.utils.datafactory import ( invalid_names_list, parametrized, @@ -35,7 +33,7 @@ class TestTailoringFiles: @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 - def test_positive_create(self, tailoring_file_path, name): + def test_positive_create(self, tailoring_file_path, name, module_target_sat): """Create new Tailoring Files using different values types as name :id: e1bb4de2-1b64-4904-bc7c-f0befa9dbd6f @@ -48,17 +46,17 @@ def test_positive_create(self, tailoring_file_path, name): :parametrized: yes """ - tailoring_file = make_tailoringfile( + tailoring_file = module_target_sat.cli_factory.tailoringfile( {'name': name, 'scap-file': tailoring_file_path['satellite']} ) assert tailoring_file['name'] == name # Delete tailoring files which created for all valid input (ex- latin1, cjk, utf-8, etc.) - TailoringFiles.delete({'id': tailoring_file['id']}) + module_target_sat.cli.TailoringFiles.delete({'id': tailoring_file['id']}) with pytest.raises(CLIReturnCodeError): - TailoringFiles.info({'id': tailoring_file['id']}) + module_target_sat.cli.TailoringFiles.info({'id': tailoring_file['id']}) @pytest.mark.tier1 - def test_positive_create_with_space(self, tailoring_file_path): + def test_positive_create_with_space(self, tailoring_file_path, module_target_sat): """Create tailoring files with space in name :id: c98ef4e7-41c5-4a8b-8a0b-8d53100b75a8 @@ -72,13 +70,13 @@ def test_positive_create_with_space(self, tailoring_file_path): :CaseImportance: Medium """ name = gen_string('alphanumeric') + ' ' + gen_string('alphanumeric') - tailoring_file = make_tailoringfile( + tailoring_file = module_target_sat.cli_factory.tailoringfile( {'name': name, 'scap-file': tailoring_file_path['satellite']} ) assert tailoring_file['name'] == name @pytest.mark.tier1 - def test_positive_get_info_of_tailoring_file(self, tailoring_file_path): + def test_positive_get_info_of_tailoring_file(self, tailoring_file_path, module_target_sat): """Get information of tailoring file :id: bc201194-e8c8-4385-a577-09f3455f5a4d @@ -96,12 +94,14 @@ def test_positive_get_info_of_tailoring_file(self, tailoring_file_path): :CaseImportance: Medium """ name = gen_string('alphanumeric') - make_tailoringfile({'name': name, 'scap-file': tailoring_file_path['satellite']}) - result = TailoringFiles.info({'name': name}) + module_target_sat.cli_factory.tailoringfile( + {'name': name, 'scap-file': tailoring_file_path['satellite']} + ) + result = module_target_sat.cli.TailoringFiles.info({'name': name}) assert result['name'] == name @pytest.mark.tier1 - def test_positive_list_tailoring_file(self, tailoring_file_path): + def test_positive_list_tailoring_file(self, tailoring_file_path, module_target_sat): """List all created tailoring files :id: 2ea63c4b-eebe-468d-8153-807e86d1b6a2 @@ -118,8 +118,10 @@ def test_positive_list_tailoring_file(self, tailoring_file_path): :CaseImportance: Medium """ name = gen_string('utf8', length=5) - make_tailoringfile({'name': name, 'scap-file': tailoring_file_path['satellite']}) - result = TailoringFiles.list() + module_target_sat.cli_factory.tailoringfile( + {'name': name, 'scap-file': tailoring_file_path['satellite']} + ) + result = module_target_sat.cli.TailoringFiles.list() assert name in [tailoringfile['name'] for tailoringfile in result] @pytest.mark.tier1 @@ -139,11 +141,13 @@ def test_negative_create_with_invalid_file(self, target_sat): target_sat.put(DataFile.SNIPPET_DATA_FILE, f'/tmp/{SNIPPET_DATA_FILE}') name = gen_string('alphanumeric') with pytest.raises(CLIFactoryError): - make_tailoringfile({'name': name, 'scap-file': f'/tmp/{SNIPPET_DATA_FILE}'}) + target_sat.cli_factory.tailoringfile( + {'name': name, 'scap-file': f'/tmp/{SNIPPET_DATA_FILE}'} + ) @pytest.mark.parametrize('name', **parametrized(invalid_names_list())) @pytest.mark.tier1 - def test_negative_create_with_invalid_name(self, tailoring_file_path, name): + def test_negative_create_with_invalid_name(self, tailoring_file_path, name, module_target_sat): """Create Tailoring files with invalid name :id: 973eee82-9735-49bb-b534-0de619aa0279 @@ -159,7 +163,9 @@ def test_negative_create_with_invalid_name(self, tailoring_file_path, name): :CaseImportance: Medium """ with pytest.raises(CLIFactoryError): - make_tailoringfile({'name': name, 'scap-file': tailoring_file_path['satellite']}) + module_target_sat.cli_factory.tailoringfile( + {'name': name, 'scap-file': tailoring_file_path['satellite']} + ) @pytest.mark.stubbed @pytest.mark.tier2 @@ -202,11 +208,13 @@ def test_positive_download_tailoring_file(self, tailoring_file_path, target_sat) """ name = gen_string('alphanumeric') file_path = f'/var{tailoring_file_path["satellite"]}' - tailoring_file = make_tailoringfile( + tailoring_file = target_sat.cli_factory.tailoringfile( {'name': name, 'scap-file': tailoring_file_path['satellite']} ) assert tailoring_file['name'] == name - result = TailoringFiles.download_tailoring_file({'name': name, 'path': '/var/tmp/'}) + result = target_sat.cli.TailoringFiles.download_tailoring_file( + {'name': name, 'path': '/var/tmp/'} + ) assert file_path in result result = target_sat.execute(f'find {file_path} 2> /dev/null') assert result.status == 0 @@ -214,7 +222,7 @@ def test_positive_download_tailoring_file(self, tailoring_file_path, target_sat) @pytest.mark.tier1 @pytest.mark.upgrade - def test_positive_delete_tailoring_file(self, tailoring_file_path): + def test_positive_delete_tailoring_file(self, tailoring_file_path, module_target_sat): """Delete tailoring file :id: 8bab5478-1ef1-484f-aafd-98e5cba7b1e7 @@ -228,10 +236,12 @@ def test_positive_delete_tailoring_file(self, tailoring_file_path): :CaseImportance: Medium """ - tailoring_file = make_tailoringfile({'scap-file': tailoring_file_path['satellite']}) - TailoringFiles.delete({'id': tailoring_file['id']}) + tailoring_file = module_target_sat.cli_factory.tailoringfile( + {'scap-file': tailoring_file_path['satellite']} + ) + module_target_sat.cli.TailoringFiles.delete({'id': tailoring_file['id']}) with pytest.raises(CLIReturnCodeError): - TailoringFiles.info({'id': tailoring_file['id']}) + module_target_sat.cli.TailoringFiles.info({'id': tailoring_file['id']}) @pytest.mark.stubbed @pytest.mark.tier4 diff --git a/tests/foreman/cli/test_ostreebranch.py b/tests/foreman/cli/test_ostreebranch.py index 23516360e48..ccb35ea0bae 100644 --- a/tests/foreman/cli/test_ostreebranch.py +++ b/tests/foreman/cli/test_ostreebranch.py @@ -21,15 +21,6 @@ from nailgun import entities import pytest -from robottelo.cli.contentview import ContentView -from robottelo.cli.factory import ( - make_content_view, - make_org_with_credentials, - make_product_with_credentials, - make_repository_with_credentials, -) -from robottelo.cli.ostreebranch import OstreeBranch -from robottelo.cli.repository import Repository from robottelo.config import settings from robottelo.constants.repos import OSTREE_REPO @@ -49,15 +40,17 @@ def ostree_user_credentials(): @pytest.fixture(scope='module') -def ostree_repo_with_user(ostree_user_credentials): +def ostree_repo_with_user(ostree_user_credentials, module_target_sat): """Create an user, organization, product and ostree repo, sync ostree repo for particular user, create content view and publish it for particular user """ - org = make_org_with_credentials(credentials=ostree_user_credentials) - product = make_product_with_credentials({'organization-id': org['id']}, ostree_user_credentials) + org = module_target_sat.cli_factory.org_with_credentials(credentials=ostree_user_credentials) + product = module_target_sat.cli_factory.product_with_credentials( + {'organization-id': org['id']}, ostree_user_credentials + ) # Create new custom ostree repo - ostree_repo = make_repository_with_credentials( + ostree_repo = module_target_sat.cli_factory.repository_with_credentials( { 'product-id': product['id'], 'content-type': 'ostree', @@ -66,30 +59,36 @@ def ostree_repo_with_user(ostree_user_credentials): }, ostree_user_credentials, ) - Repository.with_user(*ostree_user_credentials).synchronize({'id': ostree_repo['id']}) - cv = make_content_view( + module_target_sat.cli.Repository.with_user(*ostree_user_credentials).synchronize( + {'id': ostree_repo['id']} + ) + cv = module_target_sat.cli_factory.make_content_view( {'organization-id': org['id'], 'repository-ids': [ostree_repo['id']]}, ostree_user_credentials, ) - ContentView.with_user(*ostree_user_credentials).publish({'id': cv['id']}) - cv = ContentView.with_user(*ostree_user_credentials).info({'id': cv['id']}) + module_target_sat.cli.ContentView.with_user(*ostree_user_credentials).publish({'id': cv['id']}) + cv = module_target_sat.cli.ContentView.with_user(*ostree_user_credentials).info( + {'id': cv['id']} + ) return {'cv': cv, 'org': org, 'ostree_repo': ostree_repo, 'product': product} @pytest.mark.skip_if_open("BZ:1625783") -def test_positive_list(ostree_user_credentials, ostree_repo_with_user): +def test_positive_list(ostree_user_credentials, ostree_repo_with_user, module_target_sat): """List Ostree Branches :id: 0f5e7e63-c0e3-43fc-8238-caf19a478a46 :expectedresults: Ostree Branch List is displayed """ - result = OstreeBranch.with_user(*ostree_user_credentials).list() + result = module_target_sat.cli.OstreeBranch.with_user(*ostree_user_credentials).list() assert len(result) > 0 @pytest.mark.upgrade -def test_positive_list_by_repo_id(ostree_repo_with_user, ostree_user_credentials): +def test_positive_list_by_repo_id( + ostree_repo_with_user, ostree_user_credentials, module_target_sat +): """List Ostree branches by repo id :id: 8cf1a973-031c-4c02-af14-0faba22ab60b @@ -98,41 +97,43 @@ def test_positive_list_by_repo_id(ostree_repo_with_user, ostree_user_credentials """ - branch = OstreeBranch.with_user(*ostree_user_credentials) + branch = module_target_sat.cli.OstreeBranch.with_user(*ostree_user_credentials) result = branch.list({'repository-id': ostree_repo_with_user['ostree_repo']['id']}) assert len(result) > 0 @pytest.mark.skip_if_open("BZ:1625783") -def test_positive_list_by_product_id(ostree_repo_with_user, ostree_user_credentials): +def test_positive_list_by_product_id( + ostree_repo_with_user, ostree_user_credentials, module_target_sat +): """List Ostree branches by product id :id: e7b9d04d-cace-4271-b166-214017200c53 :expectedresults: Ostree Branch List is displayed """ - result = OstreeBranch.with_user(*ostree_user_credentials).list( + result = module_target_sat.cli.OstreeBranch.with_user(*ostree_user_credentials).list( {'product-id': ostree_repo_with_user['product']['id']} ) assert len(result) > 0 @pytest.mark.skip_if_open("BZ:1625783") -def test_positive_list_by_org_id(ostree_repo_with_user, ostree_user_credentials): +def test_positive_list_by_org_id(ostree_repo_with_user, ostree_user_credentials, module_target_sat): """List Ostree branches by org id :id: 5b169619-305f-4934-b363-068193330701 :expectedresults: Ostree Branch List is displayed """ - result = OstreeBranch.with_user(*ostree_user_credentials).list( + result = module_target_sat.cli.OstreeBranch.with_user(*ostree_user_credentials).list( {'organization-id': ostree_repo_with_user['org']['id']} ) assert len(result) > 0 @pytest.mark.skip_if_open("BZ:1625783") -def test_positive_list_by_cv_id(ostree_repo_with_user, ostree_user_credentials): +def test_positive_list_by_cv_id(ostree_repo_with_user, ostree_user_credentials, module_target_sat): """List Ostree branches by cv id :id: 3654f107-44ee-4af2-a9e4-f9fd8c68491e @@ -140,23 +141,25 @@ def test_positive_list_by_cv_id(ostree_repo_with_user, ostree_user_credentials): :expectedresults: Ostree Branch List is displayed """ - result = OstreeBranch.with_user(*ostree_user_credentials).list( + result = module_target_sat.cli.OstreeBranch.with_user(*ostree_user_credentials).list( {'content-view-id': ostree_repo_with_user['cv']['id']} ) assert len(result) > 0 @pytest.mark.skip_if_open("BZ:1625783") -def test_positive_info_by_id(ostree_user_credentials, ostree_repo_with_user): +def test_positive_info_by_id(ostree_user_credentials, ostree_repo_with_user, module_target_sat): """Get info for Ostree branch by id :id: 7838c9a8-56da-44de-883c-28571ecfa75c :expectedresults: Ostree Branch Info is displayed """ - result = OstreeBranch.with_user(*ostree_user_credentials).list() + result = module_target_sat.cli.OstreeBranch.with_user(*ostree_user_credentials).list() assert len(result) > 0 # Grab a random branch branch = random.choice(result) - result = OstreeBranch.with_user(*ostree_user_credentials).info({'id': branch['id']}) + result = module_target_sat.cli.OstreeBranch.with_user(*ostree_user_credentials).info( + {'id': branch['id']} + ) assert branch['id'] == result['id'] diff --git a/tests/foreman/cli/test_partitiontable.py b/tests/foreman/cli/test_partitiontable.py index fa8af557d36..fe863cfd42e 100644 --- a/tests/foreman/cli/test_partitiontable.py +++ b/tests/foreman/cli/test_partitiontable.py @@ -21,9 +21,7 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_os, make_partition_table -from robottelo.cli.partitiontable import PartitionTable +from robottelo.exceptions import CLIReturnCodeError from robottelo.utils.datafactory import generate_strings_list, parametrized @@ -32,7 +30,7 @@ class TestPartitionTable: @pytest.mark.tier1 @pytest.mark.parametrize('name', **parametrized(generate_strings_list(length=1))) - def test_positive_create_with_one_character_name(self, name): + def test_positive_create_with_one_character_name(self, name, target_sat): """Create Partition table with 1 character in name :id: cfec857c-ed6e-4472-93bb-70e1d4f39bae @@ -45,7 +43,7 @@ def test_positive_create_with_one_character_name(self, name): :CaseImportance: Medium """ - ptable = make_partition_table({'name': name}) + ptable = target_sat.cli_factory.make_partition_table({'name': name}) assert ptable['name'] == name @pytest.mark.tier1 @@ -61,7 +59,7 @@ def test_positive_create_with_one_character_name(self, name): ) ) ) - def test_positive_crud_with_name(self, name, new_name): + def test_positive_crud_with_name(self, name, new_name, module_target_sat): """Create, read, update and delete Partition Tables with different names :id: ce512fef-fbf2-4365-b70b-d30221111d96 @@ -72,17 +70,17 @@ def test_positive_crud_with_name(self, name, new_name): :CaseImportance: Critical """ - ptable = make_partition_table({'name': name}) + ptable = module_target_sat.cli_factory.make_partition_table({'name': name}) assert ptable['name'] == name - PartitionTable.update({'id': ptable['id'], 'new-name': new_name}) - ptable = PartitionTable.info({'id': ptable['id']}) + module_target_sat.cli.PartitionTable.update({'id': ptable['id'], 'new-name': new_name}) + ptable = module_target_sat.cli.PartitionTable.info({'id': ptable['id']}) assert ptable['name'] == new_name - PartitionTable.delete({'name': ptable['name']}) + module_target_sat.cli.PartitionTable.delete({'name': ptable['name']}) with pytest.raises(CLIReturnCodeError): - PartitionTable.info({'name': ptable['name']}) + module_target_sat.cli.PartitionTable.info({'name': ptable['name']}) @pytest.mark.tier1 - def test_positive_create_with_content(self): + def test_positive_create_with_content(self, module_target_sat): """Create a Partition Table with content :id: 28bfbd8b-2ada-44d0-89f3-63885cfb3495 @@ -92,13 +90,13 @@ def test_positive_create_with_content(self): :CaseImportance: Critical """ content = 'Fake ptable' - ptable = make_partition_table({'content': content}) - ptable_content = PartitionTable().dump({'id': ptable['id']}) + ptable = module_target_sat.cli_factory.make_partition_table({'content': content}) + ptable_content = module_target_sat.cli.PartitionTable().dump({'id': ptable['id']}) assert content in ptable_content @pytest.mark.tier1 @pytest.mark.upgrade - def test_positive_create_with_content_length(self): + def test_positive_create_with_content_length(self, module_target_sat): """Create a Partition Table with content length more than 4096 chars :id: 59e6f9ef-85c2-4229-8831-00edb41b19f4 @@ -108,12 +106,12 @@ def test_positive_create_with_content_length(self): :BZ: 1270181 """ content = gen_string('alpha', 5000) - ptable = make_partition_table({'content': content}) - ptable_content = PartitionTable().dump({'id': ptable['id']}) + ptable = module_target_sat.cli_factory.make_partition_table({'content': content}) + ptable_content = module_target_sat.cli.PartitionTable().dump({'id': ptable['id']}) assert content in ptable_content @pytest.mark.tier1 - def test_positive_delete_by_id(self): + def test_positive_delete_by_id(self, module_target_sat): """Create a Partition Table then delete it by its ID :id: 4d2369eb-4dc1-4ab5-96d4-c872c39f4ff5 @@ -122,13 +120,13 @@ def test_positive_delete_by_id(self): :CaseImportance: Critical """ - ptable = make_partition_table() - PartitionTable.delete({'id': ptable['id']}) + ptable = module_target_sat.cli_factory.make_partition_table() + module_target_sat.cli.PartitionTable.delete({'id': ptable['id']}) with pytest.raises(CLIReturnCodeError): - PartitionTable.info({'id': ptable['id']}) + module_target_sat.cli.PartitionTable.info({'id': ptable['id']}) @pytest.mark.tier2 - def test_positive_add_remove_os_by_id(self): + def test_positive_add_remove_os_by_id(self, module_target_sat): """Create a partition table then add and remove an operating system to it using IDs for association @@ -138,18 +136,22 @@ def test_positive_add_remove_os_by_id(self): :CaseLevel: Integration """ - ptable = make_partition_table() - os = make_os() - PartitionTable.add_operating_system({'id': ptable['id'], 'operatingsystem-id': os['id']}) - ptable = PartitionTable.info({'id': ptable['id']}) + ptable = module_target_sat.cli_factory.make_partition_table() + os = module_target_sat.cli_factory.make_os() + module_target_sat.cli.PartitionTable.add_operating_system( + {'id': ptable['id'], 'operatingsystem-id': os['id']} + ) + ptable = module_target_sat.cli.PartitionTable.info({'id': ptable['id']}) assert os['title'] in ptable['operating-systems'] - PartitionTable.remove_operating_system({'id': ptable['id'], 'operatingsystem-id': os['id']}) - ptable = PartitionTable.info({'id': ptable['id']}) + module_target_sat.cli.PartitionTable.remove_operating_system( + {'id': ptable['id'], 'operatingsystem-id': os['id']} + ) + ptable = module_target_sat.cli.PartitionTable.info({'id': ptable['id']}) assert os['title'] not in ptable['operating-systems'] @pytest.mark.tier2 @pytest.mark.upgrade - def test_positive_add_remove_os_by_name(self): + def test_positive_add_remove_os_by_name(self, module_target_sat): """Create a partition table then add and remove an operating system to it using names for association @@ -159,15 +161,15 @@ def test_positive_add_remove_os_by_name(self): :CaseLevel: Integration """ - ptable = make_partition_table() - os = make_os() - PartitionTable.add_operating_system( + ptable = module_target_sat.cli_factory.make_partition_table() + os = module_target_sat.cli_factory.make_os() + module_target_sat.cli.PartitionTable.add_operating_system( {'name': ptable['name'], 'operatingsystem': os['title']} ) - ptable = PartitionTable.info({'name': ptable['name']}) + ptable = module_target_sat.cli.PartitionTable.info({'name': ptable['name']}) assert os['title'] in ptable['operating-systems'] - PartitionTable.remove_operating_system( + module_target_sat.cli.PartitionTable.remove_operating_system( {'name': ptable['name'], 'operatingsystem': os['title']} ) - ptable = PartitionTable.info({'name': ptable['name']}) + ptable = module_target_sat.cli.PartitionTable.info({'name': ptable['name']}) assert os['title'] not in ptable['operating-systems'] diff --git a/tests/foreman/cli/test_product.py b/tests/foreman/cli/test_product.py index e11b09f6367..5a61c2dfb31 100644 --- a/tests/foreman/cli/test_product.py +++ b/tests/foreman/cli/test_product.py @@ -19,21 +19,9 @@ from fauxfactory import gen_alphanumeric, gen_integer, gen_string, gen_url import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.defaults import Defaults -from robottelo.cli.factory import ( - CLIFactoryError, - make_content_credential, - make_org, - make_product, - make_repository, - make_sync_plan, -) -from robottelo.cli.package import Package -from robottelo.cli.product import Product -from robottelo.cli.repository import Repository from robottelo.config import settings from robottelo.constants import FAKE_0_YUM_REPO_PACKAGES_COUNT +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.utils.datafactory import ( invalid_values_list, parametrized, @@ -57,11 +45,11 @@ def test_positive_CRUD(module_org, target_sat): :CaseImportance: Critical """ desc = list(valid_data_list().values())[0] - gpg_key = make_content_credential({'organization-id': module_org.id}) + gpg_key = target_sat.cli_factory.make_content_credential({'organization-id': module_org.id}) name = list(valid_data_list().values())[0] label = valid_labels_list()[0] - sync_plan = make_sync_plan({'organization-id': module_org.id}) - product = make_product( + sync_plan = target_sat.cli_factory.sync_plan({'organization-id': module_org.id}) + product = target_sat.cli_factory.make_product( { 'description': desc, 'gpg-key-id': gpg_key['id'], @@ -80,10 +68,10 @@ def test_positive_CRUD(module_org, target_sat): # update desc = list(valid_data_list().values())[0] - new_gpg_key = make_content_credential({'organization-id': module_org.id}) - new_sync_plan = make_sync_plan({'organization-id': module_org.id}) + new_gpg_key = target_sat.cli_factory.make_content_credential({'organization-id': module_org.id}) + new_sync_plan = target_sat.cli_factory.sync_plan({'organization-id': module_org.id}) new_prod_name = gen_string('alpha', 8) - Product.update( + target_sat.cli.Product.update( { 'description': desc, 'id': product['id'], @@ -92,7 +80,7 @@ def test_positive_CRUD(module_org, target_sat): 'name': new_prod_name, } ) - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) + product = target_sat.cli.Product.info({'id': product['id'], 'organization-id': module_org.id}) assert product['name'] == new_prod_name assert product['description'] == desc assert product['gpg']['gpg-key-id'] == new_gpg_key['id'] @@ -101,36 +89,36 @@ def test_positive_CRUD(module_org, target_sat): assert product['sync-plan-id'] != sync_plan['id'] # synchronize - repo = make_repository( + repo = target_sat.cli_factory.make_repository( { 'organization-id': module_org.id, 'product-id': product['id'], 'url': settings.repos.yum_0.url, }, ) - Product.synchronize({'id': product['id'], 'organization-id': module_org.id}) - packages = Package.list({'product-id': product['id']}) - repo = Repository.info({'id': repo['id']}) + target_sat.cli.Product.synchronize({'id': product['id'], 'organization-id': module_org.id}) + packages = target_sat.cli.Package.list({'product-id': product['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert int(repo['content-counts']['packages']) == len(packages) assert len(packages) == FAKE_0_YUM_REPO_PACKAGES_COUNT # delete - Product.remove_sync_plan({'id': product['id']}) - product = Product.info({'id': product['id'], 'organization-id': module_org.id}) + target_sat.cli.Product.remove_sync_plan({'id': product['id']}) + product = target_sat.cli.Product.info({'id': product['id'], 'organization-id': module_org.id}) assert len(product['sync-plan-id']) == 0 - Product.delete({'id': product['id']}) + target_sat.cli.Product.delete({'id': product['id']}) target_sat.wait_for_tasks( search_query="label = Actions::Katello::Product::Destroy" f" and resource_id = {product['id']}", max_tries=10, ) with pytest.raises(CLIReturnCodeError): - Product.info({'id': product['id'], 'organization-id': module_org.id}) + target_sat.cli.Product.info({'id': product['id'], 'organization-id': module_org.id}) @pytest.mark.tier2 @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) -def test_negative_create_with_name(name, module_org): +def test_negative_create_with_name(name, module_org, module_target_sat): """Check that only valid names can be used :id: 2da26ab2-8d79-47ea-b4d2-defcd98a0649 @@ -142,14 +130,14 @@ def test_negative_create_with_name(name, module_org): :CaseImportance: High """ with pytest.raises(CLIFactoryError): - make_product({'name': name, 'organization-id': module_org.id}) + module_target_sat.cli_factory.make_product({'name': name, 'organization-id': module_org.id}) @pytest.mark.tier2 @pytest.mark.parametrize( 'label', **parametrized([gen_string(e, 15) for e in ('latin1', 'utf8', 'html')]) ) -def test_negative_create_with_label(label, module_org): +def test_negative_create_with_label(label, module_org, module_target_sat): """Check that only valid labels can be used :id: 7cf970aa-48dc-425b-ae37-1e15dfab0626 @@ -161,7 +149,7 @@ def test_negative_create_with_label(label, module_org): :CaseImportance: High """ with pytest.raises(CLIFactoryError): - make_product( + module_target_sat.cli_factory.make_product( { 'label': label, 'name': gen_alphanumeric(), @@ -189,13 +177,15 @@ def test_product_list_with_default_settings(module_org, target_sat): org_id = str(module_org.id) default_product_name = gen_string('alpha') non_default_product_name = gen_string('alpha') - non_default_org = make_org() - default_product = make_product({'name': default_product_name, 'organization-id': org_id}) - non_default_product = make_product( + non_default_org = target_sat.cli_factory.make_org() + default_product = target_sat.cli_factory.make_product( + {'name': default_product_name, 'organization-id': org_id} + ) + non_default_product = target_sat.cli_factory.make_product( {'name': non_default_product_name, 'organization-id': non_default_org['id']} ) for product in default_product, non_default_product: - make_repository( + target_sat.cli_factory.make_repository( { 'organization-id': org_id, 'product-id': product['id'], @@ -203,7 +193,7 @@ def test_product_list_with_default_settings(module_org, target_sat): }, ) - Defaults.add({'param-name': 'organization_id', 'param-value': org_id}) + target_sat.cli.Defaults.add({'param-name': 'organization_id', 'param-value': org_id}) result = target_sat.cli.Defaults.list(per_page=False) assert any([res['value'] == org_id for res in result if res['parameter'] == 'organization_id']) @@ -215,20 +205,20 @@ def test_product_list_with_default_settings(module_org, target_sat): assert any([res['product'] == default_product_name for res in result]) # verify that defaults setting should not affect other entities - product_list = Product.list({'organization-id': non_default_org['id']}) + product_list = target_sat.cli.Product.list({'organization-id': non_default_org['id']}) assert non_default_product_name == product_list[0]['name'] - repository_list = Repository.list({'organization-id': non_default_org['id']}) + repository_list = target_sat.cli.Repository.list({'organization-id': non_default_org['id']}) assert non_default_product_name == repository_list[0]['product'] finally: - Defaults.delete({'param-name': 'organization_id'}) + target_sat.cli.Defaults.delete({'param-name': 'organization_id'}) result = target_sat.cli.Defaults.list(per_page=False) assert not [res for res in result if res['parameter'] == 'organization_id'] @pytest.mark.tier2 @pytest.mark.skip_if_open('BZ:1999541') -def test_positive_product_sync_state(module_org): +def test_positive_product_sync_state(module_org, module_target_sat): """hammer product info shows correct sync state. :id: 58af6239-85d7-4b8b-bd2d-ab4cd4f29840 @@ -246,8 +236,8 @@ def test_positive_product_sync_state(module_org): :expectedresults: hammer should show 'Sync Incomplete' in both cases. """ - product = make_product({'organization-id': module_org.id}) - repo_a1 = make_repository( + product = module_target_sat.cli_factory.make_product({'organization-id': module_org.id}) + repo_a1 = module_target_sat.cli_factory.make_repository( { 'organization-id': module_org.id, 'product-id': product['id'], @@ -257,13 +247,15 @@ def test_positive_product_sync_state(module_org): ) with pytest.raises(CLIReturnCodeError): - Repository.synchronize({'id': repo_a1['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo_a1['id']}) - product_info = Product.info({'id': product['id'], 'organization-id': module_org.id}) - product_list = Product.list({'organization-id': module_org.id}) + product_info = module_target_sat.cli.Product.info( + {'id': product['id'], 'organization-id': module_org.id} + ) + product_list = module_target_sat.cli.Product.list({'organization-id': module_org.id}) assert product_info['sync-state-(last)'] in [p.get('sync-state') for p in product_list] - repo_a2 = make_repository( + repo_a2 = module_target_sat.cli_factory.make_repository( { 'organization-id': module_org.id, 'product-id': product['id'], @@ -272,7 +264,9 @@ def test_positive_product_sync_state(module_org): }, ) - Repository.synchronize({'id': repo_a2['id']}) - product_info = Product.info({'id': product['id'], 'organization-id': module_org.id}) - product_list = Product.list({'organization-id': module_org.id}) + module_target_sat.cli.Repository.synchronize({'id': repo_a2['id']}) + product_info = module_target_sat.cli.Product.info( + {'id': product['id'], 'organization-id': module_org.id} + ) + product_list = module_target_sat.cli.Product.list({'organization-id': module_org.id}) assert product_info['sync-state-(last)'] in [p.get('sync-state') for p in product_list] diff --git a/tests/foreman/cli/test_provisioningtemplate.py b/tests/foreman/cli/test_provisioningtemplate.py index 71214e84615..663aa64a1e2 100644 --- a/tests/foreman/cli/test_provisioningtemplate.py +++ b/tests/foreman/cli/test_provisioningtemplate.py @@ -23,7 +23,7 @@ import pytest from robottelo import constants -from robottelo.cli.base import CLIReturnCodeError +from robottelo.exceptions import CLIReturnCodeError @pytest.fixture(scope='module') diff --git a/tests/foreman/cli/test_realm.py b/tests/foreman/cli/test_realm.py index 595a123e9e8..e488e150cc6 100644 --- a/tests/foreman/cli/test_realm.py +++ b/tests/foreman/cli/test_realm.py @@ -21,13 +21,11 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import CLIFactoryError, make_realm -from robottelo.cli.realm import Realm +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError @pytest.mark.tier1 -def test_negative_create_name_only(): +def test_negative_create_name_only(module_target_sat): """Create a realm with just a name parameter :id: 5606279f-0707-4d36-a307-b204ebb981ad @@ -35,11 +33,11 @@ def test_negative_create_name_only(): :expectedresults: Realm creation fails, requires proxy_id and type """ with pytest.raises(CLIFactoryError): - make_realm({'name': gen_string('alpha', random.randint(1, 30))}) + module_target_sat.cli_factory.realm({'name': gen_string('alpha', random.randint(1, 30))}) @pytest.mark.tier1 -def test_negative_create_invalid_id(): +def test_negative_create_invalid_id(module_target_sat): """Create a realm with an invalid proxy ID :id: 916bd1fb-4649-469c-b511-b0b07301a990 @@ -47,7 +45,7 @@ def test_negative_create_invalid_id(): :expectedresults: Realm creation fails, proxy_id must be numeric """ with pytest.raises(CLIFactoryError): - make_realm( + module_target_sat.cli_factory.realm( { 'name': gen_string('alpha', random.randint(1, 30)), 'realm-proxy-id': gen_string('alphanumeric'), @@ -57,7 +55,7 @@ def test_negative_create_invalid_id(): @pytest.mark.tier1 -def test_negative_create_invalid_realm_type(): +def test_negative_create_invalid_realm_type(module_target_sat): """Create a realm with an invalid type :id: 423a0969-9311-48d2-9220-040a42159a89 @@ -66,7 +64,7 @@ def test_negative_create_invalid_realm_type(): e.g. Red Hat Identity Management or Active Directory """ with pytest.raises(CLIFactoryError): - make_realm( + module_target_sat.cli_factory.realm( { 'name': gen_string('alpha', random.randint(1, 30)), 'realm-proxy-id': '1', @@ -76,7 +74,7 @@ def test_negative_create_invalid_realm_type(): @pytest.mark.tier1 -def test_negative_create_invalid_location(): +def test_negative_create_invalid_location(module_target_sat): """Create a realm with an invalid location :id: 95335c3a-413f-4156-b727-91b525738171 @@ -84,7 +82,7 @@ def test_negative_create_invalid_location(): :expectedresults: Realm creation fails, location not found """ with pytest.raises(CLIFactoryError): - make_realm( + module_target_sat.cli_factory.realm( { 'name': gen_string('alpha', random.randint(1, 30)), 'realm-proxy-id': '1', @@ -95,7 +93,7 @@ def test_negative_create_invalid_location(): @pytest.mark.tier1 -def test_negative_create_invalid_organization(): +def test_negative_create_invalid_organization(module_target_sat): """Create a realm with an invalid organization :id: c0ffbc6d-a2da-484b-9627-5454687a3abb @@ -103,7 +101,7 @@ def test_negative_create_invalid_organization(): :expectedresults: Realm creation fails, organization not found """ with pytest.raises(CLIFactoryError): - make_realm( + module_target_sat.cli_factory.realm( { 'name': gen_string('alpha', random.randint(1, 30)), 'realm-proxy-id': '1', @@ -114,7 +112,7 @@ def test_negative_create_invalid_organization(): @pytest.mark.tier2 -def test_negative_delete_nonexistent_realm_name(): +def test_negative_delete_nonexistent_realm_name(module_target_sat): """Delete a realm with a name that does not exist :id: 616db509-9643-4817-ba6b-f05cdb1cecb0 @@ -122,11 +120,11 @@ def test_negative_delete_nonexistent_realm_name(): :expectedresults: Realm not found """ with pytest.raises(CLIReturnCodeError): - Realm.delete({'name': gen_string('alpha', random.randint(1, 30))}) + module_target_sat.cli.Realm.delete({'name': gen_string('alpha', random.randint(1, 30))}) @pytest.mark.tier2 -def test_negative_delete_nonexistent_realm_id(): +def test_negative_delete_nonexistent_realm_id(module_target_sat): """Delete a realm with an ID that does not exist :id: 70bb9d4e-7e71-479a-8c82-e6fcff88ea14 @@ -134,11 +132,11 @@ def test_negative_delete_nonexistent_realm_id(): :expectedresults: Realm not found """ with pytest.raises(CLIReturnCodeError): - Realm.delete({'id': 0}) + module_target_sat.cli.Realm.delete({'id': 0}) @pytest.mark.tier2 -def test_negative_info_nonexistent_realm_name(): +def test_negative_info_nonexistent_realm_name(module_target_sat): """Get info for a realm with a name that does not exist :id: 24e4fbfa-7141-4f90-8c5d-eb88b162bd64 @@ -146,11 +144,11 @@ def test_negative_info_nonexistent_realm_name(): :expectedresults: Realm not found """ with pytest.raises(CLIReturnCodeError): - Realm.info({'name': gen_string('alpha', random.randint(1, 30))}) + module_target_sat.cli.Realm.info({'name': gen_string('alpha', random.randint(1, 30))}) @pytest.mark.tier2 -def test_negative_info_nonexistent_realm_id(): +def test_negative_info_nonexistent_realm_id(module_target_sat): """Get info for a realm with an ID that does not exist :id: db8382eb-6d0b-4d6a-a9bf-38a462389f7b @@ -158,4 +156,4 @@ def test_negative_info_nonexistent_realm_id(): :expectedresults: Realm not found """ with pytest.raises(CLIReturnCodeError): - Realm.info({'id': 0}) + module_target_sat.cli.Realm.info({'id': 0}) diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index 3493245a4df..7ae6c303f9c 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -25,23 +25,7 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.factory import ( - make_filter, - make_job_invocation, - make_job_invocation_with_credentials, - make_job_template, - make_role, - make_user, -) -from robottelo.cli.filter import Filter -from robottelo.cli.globalparam import GlobalParameter from robottelo.cli.host import Host -from robottelo.cli.job_invocation import JobInvocation -from robottelo.cli.recurring_logic import RecurringLogic -from robottelo.cli.repository import Repository -from robottelo.cli.repository_set import RepositorySet -from robottelo.cli.task import Task -from robottelo.cli.user import User from robottelo.config import settings from robottelo.constants import PRDS, REPOS, REPOSET from robottelo.hosts import ContentHost @@ -72,33 +56,39 @@ def infra_host(request, target_sat, module_capsule_configured): return infra_hosts[request.param] -def assert_job_invocation_result(invocation_command_id, client_hostname, expected_result='success'): +def assert_job_invocation_result( + sat, invocation_command_id, client_hostname, expected_result='success' +): """Asserts the job invocation finished with the expected result and fetches job output when error occurs. Result is one of: success, pending, error, warning""" - result = JobInvocation.info({'id': invocation_command_id}) + result = sat.cli.JobInvocation.info({'id': invocation_command_id}) try: assert result[expected_result] == '1' except AssertionError: raise AssertionError( 'host output: {}'.format( ' '.join( - JobInvocation.get_output({'id': invocation_command_id, 'host': client_hostname}) + sat.cli.JobInvocation.get_output( + {'id': invocation_command_id, 'host': client_hostname} + ) ) ) ) -def assert_job_invocation_status(invocation_command_id, client_hostname, status): +def assert_job_invocation_status(sat, invocation_command_id, client_hostname, status): """Asserts the job invocation status and fetches job output when error occurs. Status is one of: queued, stopped, running, paused""" - result = JobInvocation.info({'id': invocation_command_id}) + result = sat.cli.JobInvocation.info({'id': invocation_command_id}) try: assert result['status'] == status except AssertionError: raise AssertionError( 'host output: {}'.format( ' '.join( - JobInvocation.get_output({'id': invocation_command_id, 'host': client_hostname}) + sat.cli.JobInvocation.get_output( + {'id': invocation_command_id, 'host': client_hostname} + ) ) ) ) @@ -111,7 +101,9 @@ class TestRemoteExecution: @pytest.mark.pit_client @pytest.mark.pit_server @pytest.mark.rhel_ver_list([8]) - def test_positive_run_default_job_template_by_ip(self, module_org, rex_contenthost): + def test_positive_run_default_job_template_by_ip( + self, module_org, rex_contenthost, module_target_sat + ): """Run default template on host connected by ip and list task :id: 811c7747-bec6-4a2d-8e5c-b5045d3fbc0d @@ -127,18 +119,18 @@ def test_positive_run_default_job_template_by_ip(self, module_org, rex_contentho """ client = rex_contenthost command = f'echo {gen_string("alpha")}' - invocation_command = make_job_invocation( + invocation_command = module_target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': f'command={command}', 'search-query': f"name ~ {client.hostname}", } ) - assert_job_invocation_result(invocation_command['id'], client.hostname) - task = Task.list_tasks({'search': command})[0] - search = Task.list_tasks({'search': f'id={task["id"]}'}) + assert_job_invocation_result(module_target_sat, invocation_command['id'], client.hostname) + task = module_target_sat.cli.Task.list_tasks({'search': command})[0] + search = module_target_sat.cli.Task.list_tasks({'search': f'id={task["id"]}'}) assert search[0]['action'] == task['action'] - out = JobInvocation.get_output( + out = module_target_sat.cli.JobInvocation.get_output( { 'id': invocation_command['id'], 'host': client.hostname, @@ -152,7 +144,7 @@ def test_positive_run_default_job_template_by_ip(self, module_org, rex_contentho @pytest.mark.pit_client @pytest.mark.pit_server @pytest.mark.rhel_ver_list([7, 8, 9]) - def test_positive_run_job_effective_user_by_ip(self, rex_contenthost): + def test_positive_run_job_effective_user_by_ip(self, rex_contenthost, module_target_sat): """Run default job template as effective user on a host by ip :id: 0cd75cab-f699-47e6-94d3-4477d2a94bb7 @@ -168,16 +160,16 @@ def test_positive_run_job_effective_user_by_ip(self, rex_contenthost): # create a user on client via remote job username = gen_string('alpha') filename = gen_string('alpha') - make_user_job = make_job_invocation( + make_user_job = module_target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': f"command=useradd -m {username}", 'search-query': f"name ~ {client.hostname}", } ) - assert_job_invocation_result(make_user_job['id'], client.hostname) + assert_job_invocation_result(module_target_sat, make_user_job['id'], client.hostname) # create a file as new user - invocation_command = make_job_invocation( + invocation_command = module_target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': f"command=touch /home/{username}/{filename}", @@ -185,7 +177,7 @@ def test_positive_run_job_effective_user_by_ip(self, rex_contenthost): 'effective-user': f'{username}', } ) - assert_job_invocation_result(invocation_command['id'], client.hostname) + assert_job_invocation_result(module_target_sat, invocation_command['id'], client.hostname) # check the file owner result = client.execute( f'''stat -c '%U' /home/{username}/{filename}''', @@ -216,20 +208,20 @@ def test_positive_run_custom_job_template_by_ip(self, rex_contenthost, module_or template_file = 'template_file.txt' target_sat.execute(f'echo "echo Enforcing" > {template_file}') template_name = gen_string('alpha', 7) - make_job_template( + target_sat.cli_factory.job_template( {'organizations': self.org.name, 'name': template_name, 'file': template_file} ) - invocation_command = make_job_invocation( + invocation_command = target_sat.cli_factory.job_invocation( {'job-template': template_name, 'search-query': f'name ~ {client.hostname}'} ) - assert_job_invocation_result(invocation_command['id'], client.hostname) + assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) @pytest.mark.tier3 @pytest.mark.upgrade @pytest.mark.no_containers @pytest.mark.rhel_ver_list([8]) def test_positive_run_default_job_template_multiple_hosts_by_ip( - self, registered_hosts, module_org + self, registered_hosts, module_target_sat ): """Run default job template against multiple hosts by ip @@ -240,7 +232,7 @@ def test_positive_run_default_job_template_multiple_hosts_by_ip( :parametrized: yes """ clients = registered_hosts - invocation_command = make_job_invocation( + invocation_command = module_target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': 'command=ls', @@ -254,13 +246,13 @@ def test_positive_run_default_job_template_multiple_hosts_by_ip( 'host output from {}: {}'.format( vm.hostname, ' '.join( - JobInvocation.get_output( + module_target_sat.cli.JobInvocation.get_output( {'id': invocation_command['id'], 'host': vm.hostname} ) ), ) ) - result = JobInvocation.info({'id': invocation_command['id']}) + result = module_target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) assert result['success'] == '2', output_msgs @pytest.mark.tier3 @@ -290,20 +282,23 @@ def test_positive_install_multiple_packages_with_a_job_by_ip( target_sat, repo=settings.repos.yum_3.url, ) - invocation_command = make_job_invocation( + # Install packages + invocation_command = target_sat.cli_factory.job_invocation( { 'job-template': 'Install Package - Katello Script Default', 'inputs': 'package={} {} {}'.format(*packages), 'search-query': f'name ~ {client.hostname}', } ) - assert_job_invocation_result(invocation_command['id'], client.hostname) + assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) result = client.run(f'rpm -q {" ".join(packages)}') assert result.status == 0 @pytest.mark.tier3 @pytest.mark.rhel_ver_list([8]) - def test_positive_run_recurring_job_with_max_iterations_by_ip(self, rex_contenthost): + def test_positive_run_recurring_job_with_max_iterations_by_ip( + self, rex_contenthost, target_sat + ): """Run default job template multiple times with max iteration by ip :id: 0a3d1627-95d9-42ab-9478-a908f2a7c509 @@ -314,7 +309,7 @@ def test_positive_run_recurring_job_with_max_iterations_by_ip(self, rex_contenth :parametrized: yes """ client = rex_contenthost - invocation_command = make_job_invocation( + invocation_command = target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': 'command=ls', @@ -323,16 +318,18 @@ def test_positive_run_recurring_job_with_max_iterations_by_ip(self, rex_contenth 'max-iteration': 2, # just two runs } ) - result = JobInvocation.info({'id': invocation_command['id']}) - assert_job_invocation_status(invocation_command['id'], client.hostname, 'queued') + result = target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) + assert_job_invocation_status( + target_sat, invocation_command['id'], client.hostname, 'queued' + ) sleep(150) - rec_logic = RecurringLogic.info({'id': result['recurring-logic-id']}) + rec_logic = target_sat.cli.RecurringLogic.info({'id': result['recurring-logic-id']}) assert rec_logic['state'] == 'finished' assert rec_logic['iteration'] == '2' @pytest.mark.tier3 @pytest.mark.rhel_ver_list([8]) - def test_positive_time_expressions(self, rex_contenthost): + def test_positive_time_expressions(self, rex_contenthost, target_sat): """Test various expressions for extended cronline syntax :id: 584e7b27-9484-436a-b850-11acb900a7d8 @@ -397,7 +394,7 @@ def test_positive_time_expressions(self, rex_contenthost): ], ] for exp in fugit_expressions: - invocation_command = make_job_invocation( + invocation_command = target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': 'command=ls', @@ -406,9 +403,11 @@ def test_positive_time_expressions(self, rex_contenthost): 'max-iteration': 1, } ) - result = JobInvocation.info({'id': invocation_command['id']}) - assert_job_invocation_status(invocation_command['id'], client.hostname, 'queued') - rec_logic = RecurringLogic.info({'id': result['recurring-logic-id']}) + result = target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) + assert_job_invocation_status( + target_sat, invocation_command['id'], client.hostname, 'queued' + ) + rec_logic = target_sat.cli.RecurringLogic.info({'id': result['recurring-logic-id']}) assert ( rec_logic['next-occurrence'] == exp[1] ), f'Job was not scheduled as expected using {exp[0]}' @@ -437,7 +436,7 @@ def test_positive_run_scheduled_job_template_by_ip(self, rex_contenthost, target 'parameter-type': 'boolean', } ) - invocation_command = make_job_invocation( + invocation_command = target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': 'command=ls', @@ -448,10 +447,10 @@ def test_positive_run_scheduled_job_template_by_ip(self, rex_contenthost, target # Wait until the job runs pending_state = '1' while pending_state != '0': - invocation_info = JobInvocation.info({'id': invocation_command['id']}) + invocation_info = target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) pending_state = invocation_info['pending'] sleep(30) - assert_job_invocation_result(invocation_command['id'], client.hostname) + assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) class TestAnsibleREX: @@ -462,7 +461,7 @@ class TestAnsibleREX: @pytest.mark.pit_client @pytest.mark.pit_server @pytest.mark.rhel_ver_list([7, 8, 9]) - def test_positive_run_effective_user_job(self, rex_contenthost): + def test_positive_run_effective_user_job(self, rex_contenthost, target_sat): """Tests Ansible REX job having effective user runs successfully :id: a5fa20d8-c2bd-4bbf-a6dc-bf307b59dd8c @@ -489,16 +488,16 @@ def test_positive_run_effective_user_job(self, rex_contenthost): # create a user on client via remote job username = gen_string('alpha') filename = gen_string('alpha') - make_user_job = make_job_invocation( + make_user_job = target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Ansible Default', 'inputs': f"command=useradd -m {username}", 'search-query': f"name ~ {client.hostname}", } ) - assert_job_invocation_result(make_user_job['id'], client.hostname) + assert_job_invocation_result(target_sat, make_user_job['id'], client.hostname) # create a file as new user - invocation_command = make_job_invocation( + invocation_command = target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Ansible Default', 'inputs': f"command=touch /home/{username}/{filename}", @@ -506,7 +505,7 @@ def test_positive_run_effective_user_job(self, rex_contenthost): 'effective-user': f'{username}', } ) - assert_job_invocation_result(invocation_command['id'], client.hostname) + assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) # check the file owner result = client.execute( f'''stat -c '%U' /home/{username}/{filename}''', @@ -517,7 +516,7 @@ def test_positive_run_effective_user_job(self, rex_contenthost): @pytest.mark.tier3 @pytest.mark.upgrade @pytest.mark.rhel_ver_list([8]) - def test_positive_run_reccuring_job(self, rex_contenthost): + def test_positive_run_reccuring_job(self, rex_contenthost, target_sat): """Tests Ansible REX reccuring job runs successfully multiple times :id: 49b0d31d-58f9-47f1-aa5d-561a1dcb0d66 @@ -543,7 +542,7 @@ def test_positive_run_reccuring_job(self, rex_contenthost): :parametrized: yes """ client = rex_contenthost - invocation_command = make_job_invocation( + invocation_command = target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Ansible Default', 'inputs': 'command=ls', @@ -552,9 +551,9 @@ def test_positive_run_reccuring_job(self, rex_contenthost): 'max-iteration': 2, # just two runs } ) - result = JobInvocation.info({'id': invocation_command['id']}) + result = target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) sleep(150) - rec_logic = RecurringLogic.info({'id': result['recurring-logic-id']}) + rec_logic = target_sat.cli.RecurringLogic.info({'id': result['recurring-logic-id']}) assert rec_logic['state'] == 'finished' assert rec_logic['iteration'] == '2' # 2129432 @@ -569,7 +568,7 @@ def test_positive_run_reccuring_job(self, rex_contenthost): @pytest.mark.tier3 @pytest.mark.no_containers - def test_positive_run_concurrent_jobs(self, registered_hosts, module_org): + def test_positive_run_concurrent_jobs(self, registered_hosts, target_sat): """Tests Ansible REX concurent jobs without batch trigger :id: ad0f108c-03f2-49c7-8732-b1056570567b @@ -593,10 +592,10 @@ def test_positive_run_concurrent_jobs(self, registered_hosts, module_org): :parametrized: yes """ param_name = 'foreman_tasks_proxy_batch_trigger' - GlobalParameter().set({'name': param_name, 'value': 'false'}) + target_sat.cli.GlobalParameter().set({'name': param_name, 'value': 'false'}) clients = registered_hosts output_msgs = [] - invocation_command = make_job_invocation( + invocation_command = target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Ansible Default', 'inputs': 'command=ls', @@ -609,16 +608,16 @@ def test_positive_run_concurrent_jobs(self, registered_hosts, module_org): 'host output from {}: {}'.format( vm.hostname, ' '.join( - JobInvocation.get_output( + target_sat.cli.JobInvocation.get_output( {'id': invocation_command['id'], 'host': vm.hostname} ) ), ) ) - result = JobInvocation.info({'id': invocation_command['id']}) + result = target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) assert result['success'] == '2', output_msgs - GlobalParameter().delete({'name': param_name}) - assert len(GlobalParameter().list({'search': param_name})) == 0 + target_sat.cli.GlobalParameter().delete({'name': param_name}) + assert len(target_sat.cli.GlobalParameter().list({'search': param_name})) == 0 @pytest.mark.tier3 @pytest.mark.upgrade @@ -672,39 +671,39 @@ def test_positive_run_packages_and_services_job( repo=settings.repos.yum_3.url, ) # install package - invocation_command = make_job_invocation( + invocation_command = target_sat.cli_factory.job_invocation( { 'job-template': 'Package Action - Ansible Default', 'inputs': 'state=latest, name={}'.format(*packages), 'search-query': f'name ~ {client.hostname}', } ) - assert_job_invocation_result(invocation_command['id'], client.hostname) + assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) result = client.run(f'rpm -q {" ".join(packages)}') assert result.status == 0 # stop a service service = "rsyslog" - invocation_command = make_job_invocation( + invocation_command = target_sat.cli_factory.job_invocation( { 'job-template': 'Service Action - Ansible Default', 'inputs': f'state=stopped, name={service}', 'search-query': f"name ~ {client.hostname}", } ) - assert_job_invocation_result(invocation_command['id'], client.hostname) + assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) result = client.execute(f'systemctl status {service}') assert result.status == 3 # start it again - invocation_command = make_job_invocation( + invocation_command = target_sat.cli_factory.job_invocation( { 'job-template': 'Service Action - Ansible Default', 'inputs': f'state=started, name={service}', 'search-query': f'name ~ {client.hostname}', } ) - assert_job_invocation_result(invocation_command['id'], client.hostname) + assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) result = client.execute(f'systemctl status {service}') assert result.status == 0 @@ -713,7 +712,7 @@ def test_positive_run_packages_and_services_job( 'fixture_sca_vmsetup', [{'nick': 'rhel8'}], ids=['rhel8'], indirect=True ) def test_positive_install_ansible_collection( - self, fixture_sca_vmsetup, module_sca_manifest_org + self, fixture_sca_vmsetup, module_sca_manifest_org, target_sat ): """Test whether Ansible collection can be installed via REX @@ -732,7 +731,7 @@ def test_positive_install_ansible_collection( :Team: Rocket """ # Configure repository to prepare for installing ansible on host - RepositorySet.enable( + target_sat.cli.RepositorySet.enable( { 'basearch': 'x86_64', 'name': REPOSET['rhae2.9_el8'], @@ -741,7 +740,7 @@ def test_positive_install_ansible_collection( 'releasever': '8', } ) - Repository.synchronize( + target_sat.cli.Repository.synchronize( { 'name': REPOS['rhae2.9_el8']['name'], 'organization-id': module_sca_manifest_org.id, @@ -752,27 +751,27 @@ def test_positive_install_ansible_collection( client.execute('subscription-manager refresh') client.execute(f'subscription-manager repos --enable {REPOS["rhae2.9_el8"]["id"]}') client.execute('dnf -y install ansible') - collection_job = make_job_invocation( + collection_job = target_sat.cli_factory.job_invocation( { 'job-template': 'Ansible Collection - Install from Galaxy', 'inputs': 'ansible_collections_list="oasis_roles.system"', 'search-query': f'name ~ {client.hostname}', } ) - result = JobInvocation.info({'id': collection_job['id']}) + result = target_sat.cli.JobInvocation.info({'id': collection_job['id']}) assert result['success'] == '1' collection_path = client.execute('ls /etc/ansible/collections/ansible_collections').stdout assert 'oasis_roles' in collection_path # Extend test with custom collections_path advanced input field - collection_job = make_job_invocation( + collection_job = target_sat.cli_factory.job_invocation( { 'job-template': 'Ansible Collection - Install from Galaxy', 'inputs': 'ansible_collections_list="oasis_roles.system", collections_path="~/"', 'search-query': f'name ~ {client.hostname}', } ) - result = JobInvocation.info({'id': collection_job['id']}) + result = target_sat.cli.JobInvocation.info({'id': collection_job['id']}) assert result['success'] == '1' collection_path = client.execute('ls ~/ansible_collections').stdout assert 'oasis_roles' in collection_path @@ -782,38 +781,50 @@ class TestRexUsers: """Tests related to remote execution users""" @pytest.fixture(scope='class') - def class_rexmanager_user(self, module_org): + def class_rexmanager_user(self, module_org, class_target_sat): """Creates a user with Remote Execution Manager role""" password = gen_string('alpha') rexmanager = gen_string('alpha') - make_user({'login': rexmanager, 'password': password, 'organization-ids': module_org.id}) - User.add_role({'login': rexmanager, 'role': 'Remote Execution Manager'}) + class_target_sat.cli_factory.user( + {'login': rexmanager, 'password': password, 'organization-ids': module_org.id} + ) + class_target_sat.cli.User.add_role( + {'login': rexmanager, 'role': 'Remote Execution Manager'} + ) return (rexmanager, password) @pytest.fixture(scope='class') - def class_rexinfra_user(self, module_org): + def class_rexinfra_user(self, module_org, class_target_sat): """Creates a user with all Remote Execution related permissions""" password = gen_string('alpha') rexinfra = gen_string('alpha') - make_user({'login': rexinfra, 'password': password, 'organization-ids': module_org.id}) - role = make_role({'organization-ids': module_org.id}) + class_target_sat.cli_factory.user( + {'login': rexinfra, 'password': password, 'organization-ids': module_org.id} + ) + role = class_target_sat.cli_factory.make_role({'organization-ids': module_org.id}) invocation_permissions = [ permission['name'] - for permission in Filter.available_permissions( + for permission in class_target_sat.cli.Filter.available_permissions( {'search': 'resource_type=JobInvocation'} ) ] template_permissions = [ permission['name'] - for permission in Filter.available_permissions({'search': 'resource_type=JobTemplate'}) + for permission in class_target_sat.cli.Filter.available_permissions( + {'search': 'resource_type=JobTemplate'} + ) ] permissions = ','.join(invocation_permissions) - make_filter({'role-id': role['id'], 'permissions': permissions}) + class_target_sat.cli_factory.make_filter( + {'role-id': role['id'], 'permissions': permissions} + ) permissions = ','.join(template_permissions) # needs execute_jobs_on_infrastructure_host permission - make_filter({'role-id': role['id'], 'permissions': permissions}) - User.add_role({'login': rexinfra, 'role': role['name']}) - User.add_role({'login': rexinfra, 'role': 'Remote Execution Manager'}) + class_target_sat.cli_factory.make_filter( + {'role-id': role['id'], 'permissions': permissions} + ) + class_target_sat.cli.User.add_role({'login': rexinfra, 'role': role['name']}) + class_target_sat.cli.User.add_role({'login': rexinfra, 'role': 'Remote Execution Manager'}) return (rexinfra, password) @pytest.mark.tier3 @@ -856,11 +867,13 @@ def test_positive_rex_against_infra_hosts( """ client = rex_contenthost infra_host.add_rex_key(satellite=target_sat) - Host.update({'name': infra_host.hostname, 'new-organization-id': module_org.id}) + target_sat.cli.Host.update( + {'name': infra_host.hostname, 'new-organization-id': module_org.id} + ) # run job as admin command = f"echo {gen_string('alpha')}" - invocation_command = make_job_invocation( + invocation_command = target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': f'command={command}', @@ -871,14 +884,16 @@ def test_positive_rex_against_infra_hosts( hostnames = [client.hostname, infra_host.hostname] for hostname in hostnames: inv_output = ' '.join( - JobInvocation.get_output({'id': invocation_command['id'], 'host': hostname}) + target_sat.cli.JobInvocation.get_output( + {'id': invocation_command['id'], 'host': hostname} + ) ) output_msgs.append(f"host output from {hostname}: { inv_output }") - result = JobInvocation.info({'id': invocation_command['id']}) + result = target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) assert result['success'] == '2', output_msgs # run job as regular rex user on all hosts - invocation_command = make_job_invocation_with_credentials( + invocation_command = target_sat.cli_factory.job_invocation_with_credentials( { 'job-template': 'Run Command - Script Default', 'inputs': f'command={command}', @@ -887,11 +902,11 @@ def test_positive_rex_against_infra_hosts( class_rexmanager_user, ) - result = JobInvocation.info({'id': invocation_command['id']}) + result = target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) assert result['success'] == '1' # run job as regular rex user just on infra hosts - invocation_command = make_job_invocation_with_credentials( + invocation_command = target_sat.cli_factory.job_invocation_with_credentials( { 'job-template': 'Run Command - Script Default', 'inputs': f'command={command}', @@ -899,11 +914,11 @@ def test_positive_rex_against_infra_hosts( }, class_rexmanager_user, ) - result = JobInvocation.info({'id': invocation_command['id']}) + result = target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) assert result['success'] == '0' # run job as rex user on Satellite - invocation_command = make_job_invocation_with_credentials( + invocation_command = target_sat.cli_factory.job_invocation_with_credentials( { 'job-template': 'Run Command - Script Default', 'inputs': f'command={command}', @@ -911,7 +926,7 @@ def test_positive_rex_against_infra_hosts( }, class_rexinfra_user, ) - result = JobInvocation.info({'id': invocation_command['id']}) + result = target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) assert result['success'] == '1' @@ -964,14 +979,16 @@ def test_positive_run_job_on_host_registered_to_async_ssh_provider( assert result.status == 0, f'Failed to register host: {result.stderr}' # run script provider rex command, longer-running command is needed to # verify the connection is not shut down too soon - invocation_command = make_job_invocation( + invocation_command = module_target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': 'command=echo start; sleep 10; echo done', 'search-query': f"name ~ {rhel_contenthost.hostname}", } ) - assert_job_invocation_result(invocation_command['id'], rhel_contenthost.hostname) + assert_job_invocation_result( + module_target_sat, invocation_command['id'], rhel_contenthost.hostname + ) class TestPullProviderRex: @@ -1037,42 +1054,48 @@ def test_positive_run_job_on_host_converted_to_pull_provider( result = rhel_contenthost.execute('systemctl status yggdrasild') assert result.status == 0, f'Failed to start yggdrasil on client: {result.stderr}' # run script provider rex command - invocation_command = make_job_invocation( + invocation_command = module_target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': 'command=ls', 'search-query': f"name ~ {rhel_contenthost.hostname}", } ) - assert_job_invocation_result(invocation_command['id'], rhel_contenthost.hostname) + assert_job_invocation_result( + module_target_sat, invocation_command['id'], rhel_contenthost.hostname + ) # check katello-agent runs along ygdrassil (SAT-1671) result = rhel_contenthost.execute('systemctl status goferd') assert result.status == 0, 'Failed to start goferd on client' # run Ansible rex command to prove ssh provider works, remove katello-agent - invocation_command = make_job_invocation( + invocation_command = module_target_sat.cli_factory.job_invocation( { 'job-template': 'Package Action - Ansible Default', 'inputs': 'state=absent, name=katello-agent', 'search-query': f"name ~ {rhel_contenthost.hostname}", } ) - assert_job_invocation_result(invocation_command['id'], rhel_contenthost.hostname) + assert_job_invocation_result( + module_target_sat, invocation_command['id'], rhel_contenthost.hostname + ) # check katello-agent removal did not influence ygdrassil (SAT-1672) result = rhel_contenthost.execute('systemctl status yggdrasild') assert result.status == 0, f'Failed to start yggdrasil on client: {result.stderr}' result = rhel_contenthost.execute('systemctl status yggdrasild') assert result.status == 0, f'Failed to start yggdrasil on client: {result.stderr}' - invocation_command = make_job_invocation( + invocation_command = module_target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': 'command=ls', 'search-query': f"name ~ {rhel_contenthost.hostname}", } ) - assert_job_invocation_result(invocation_command['id'], rhel_contenthost.hostname) - result = JobInvocation.info({'id': invocation_command['id']}) + assert_job_invocation_result( + module_target_sat, invocation_command['id'], rhel_contenthost.hostname + ) + module_target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) @pytest.mark.tier3 @pytest.mark.upgrade @@ -1087,6 +1110,7 @@ def test_positive_run_job_on_host_registered_to_pull_provider( module_ak_with_cv, module_capsule_configured_mqtt, rhel_contenthost, + target_sat, ): """Run custom template on host registered to mqtt, check effective user setting @@ -1131,27 +1155,31 @@ def test_positive_run_job_on_host_registered_to_pull_provider( result = rhel_contenthost.execute('systemctl status yggdrasild') assert result.status == 0, f'Failed to start yggdrasil on client: {result.stderr}' # run script provider rex command - invocation_command = make_job_invocation( + invocation_command = target_sat.cli_factory.job_invocation( { 'job-template': 'Service Action - Script Default', 'inputs': 'action=status, service=yggdrasild', 'search-query': f"name ~ {rhel_contenthost.hostname}", } ) - assert_job_invocation_result(invocation_command['id'], rhel_contenthost.hostname) + assert_job_invocation_result( + target_sat, invocation_command['id'], rhel_contenthost.hostname + ) # create user on host username = gen_string('alpha') filename = gen_string('alpha') - make_user_job = make_job_invocation( + make_user_job = target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': f"command=useradd -m {username}", 'search-query': f"name ~ {rhel_contenthost.hostname}", } ) - assert_job_invocation_result(make_user_job['id'], rhel_contenthost.hostname) + assert_job_invocation_result( + module_target_sat, make_user_job['id'], rhel_contenthost.hostname + ) # create a file as new user - invocation_command = make_job_invocation( + invocation_command = module_target_sat.make_job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': f"command=touch /home/{username}/{filename}", @@ -1159,7 +1187,9 @@ def test_positive_run_job_on_host_registered_to_pull_provider( 'effective-user': f'{username}', } ) - assert_job_invocation_result(invocation_command['id'], rhel_contenthost.hostname) + assert_job_invocation_result( + module_target_sat, invocation_command['id'], rhel_contenthost.hostname + ) # check the file owner result = rhel_contenthost.execute( f'''stat -c '%U' /home/{username}/{filename}''', @@ -1224,7 +1254,7 @@ def test_positive_run_pull_job_on_offline_host( result = rhel_contenthost.execute('systemctl stop yggdrasild') assert result.status == 0, f'Failed to stop yggdrasil on client: {result.stderr}' # run script provider rex command - invocation_command = make_job_invocation( + invocation_command = module_target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': 'command=ls', @@ -1233,10 +1263,14 @@ def test_positive_run_pull_job_on_offline_host( } ) # assert the job is waiting to be picked up by client - assert_job_invocation_status(invocation_command['id'], rhel_contenthost.hostname, 'running') + assert_job_invocation_status( + module_target_sat, invocation_command['id'], rhel_contenthost.hostname, 'running' + ) # start client on host result = rhel_contenthost.execute('systemctl start yggdrasild') assert result.status == 0, f'Failed to start yggdrasil on client: {result.stderr}' # wait twice the mqtt_resend_interval (set in module_capsule_configured_mqtt) sleep(60) - assert_job_invocation_result(invocation_command['id'], rhel_contenthost.hostname) + assert_job_invocation_result( + module_target_sat, invocation_command['id'], rhel_contenthost.hostname + ) diff --git a/tests/foreman/cli/test_report.py b/tests/foreman/cli/test_report.py index b2d84d3adb8..94946b88e66 100644 --- a/tests/foreman/cli/test_report.py +++ b/tests/foreman/cli/test_report.py @@ -20,7 +20,7 @@ import pytest -from robottelo.cli.base import CLIReturnCodeError +from robottelo.exceptions import CLIReturnCodeError @pytest.fixture(scope='module') diff --git a/tests/foreman/cli/test_reporttemplates.py b/tests/foreman/cli/test_reporttemplates.py index f5027c028f1..d0d94693f95 100644 --- a/tests/foreman/cli/test_reporttemplates.py +++ b/tests/foreman/cli/test_reporttemplates.py @@ -19,38 +19,6 @@ from fauxfactory import gen_alpha import pytest -from robottelo.cli.activationkey import ActivationKey -from robottelo.cli.base import Base, CLIReturnCodeError -from robottelo.cli.contentview import ContentView -from robottelo.cli.factory import ( - CLIFactoryError, - make_activation_key, - make_architecture, - make_content_view, - make_fake_host, - make_filter, - make_lifecycle_environment, - make_medium, - make_os, - make_partition_table, - make_product, - make_report_template, - make_repository, - make_role, - make_template_input, - make_user, - setup_org_for_a_custom_repo, - setup_org_for_a_rh_repo, -) -from robottelo.cli.filter import Filter -from robottelo.cli.host import Host -from robottelo.cli.location import Location -from robottelo.cli.org import Org -from robottelo.cli.report_template import ReportTemplate -from robottelo.cli.repository import Repository -from robottelo.cli.settings import Settings -from robottelo.cli.subscription import Subscription -from robottelo.cli.user import User from robottelo.config import settings from robottelo.constants import ( DEFAULT_LOC, @@ -65,41 +33,50 @@ REPOS, REPOSET, ) +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.hosts import ContentHost @pytest.fixture(scope='module') -def local_environment(module_entitlement_manifest_org): +def local_environment(module_entitlement_manifest_org, module_target_sat): """Create a lifecycle environment with CLI factory""" - return make_lifecycle_environment({'organization-id': module_entitlement_manifest_org.id}) + return module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_entitlement_manifest_org.id} + ) @pytest.fixture(scope='module') -def local_content_view(module_entitlement_manifest_org): +def local_content_view(module_entitlement_manifest_org, module_target_sat): """Create content view, repository, and product""" - new_product = make_product({'organization-id': module_entitlement_manifest_org.id}) - new_repo = make_repository({'product-id': new_product['id']}) - Repository.synchronize({'id': new_repo['id']}) - content_view = make_content_view({'organization-id': module_entitlement_manifest_org.id}) - ContentView.add_repository( + new_product = module_target_sat.cli_factory.make_product( + {'organization-id': module_entitlement_manifest_org.id} + ) + new_repo = module_target_sat.cli_factory.make_repository({'product-id': new_product['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_entitlement_manifest_org.id} + ) + module_target_sat.cli.ContentView.add_repository( { 'id': content_view['id'], 'organization-id': module_entitlement_manifest_org.id, 'repository-id': new_repo['id'], } ) - ContentView.publish({'id': content_view['id']}) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) return content_view @pytest.fixture(scope='module') -def local_ak(module_entitlement_manifest_org, local_environment, local_content_view): +def local_ak( + module_entitlement_manifest_org, local_environment, local_content_view, module_target_sat +): """Promote a content view version and create an activation key with CLI Factory""" - cvv = ContentView.info({'id': local_content_view['id']})['versions'][0] - ContentView.version_promote( + cvv = module_target_sat.cli.ContentView.info({'id': local_content_view['id']})['versions'][0] + module_target_sat.cli.ContentView.version_promote( {'id': cvv['id'], 'to-lifecycle-environment-id': local_environment['id']} ) - return make_activation_key( + return module_target_sat.cli_factory.make_activation_key( { 'lifecycle-environment-id': local_environment['id'], 'content-view': local_content_view['name'], @@ -110,19 +87,21 @@ def local_ak(module_entitlement_manifest_org, local_environment, local_content_v @pytest.fixture(scope='module') -def local_subscription(module_entitlement_manifest_org, local_ak): - for subscription in Subscription.list( +def local_subscription(module_entitlement_manifest_org, local_ak, module_target_sat): + for subscription in module_target_sat.cli.Subscription.list( {'organization-id': module_entitlement_manifest_org.id}, per_page=False ): if subscription['name'] == DEFAULT_SUBSCRIPTION_NAME: break - ActivationKey.add_subscription({'id': local_ak['id'], 'subscription-id': subscription['id']}) + module_target_sat.cli.ActivationKey.add_subscription( + {'id': local_ak['id'], 'subscription-id': subscription['id']} + ) return subscription @pytest.mark.tier2 -def test_positive_report_help(): +def test_positive_report_help(module_target_sat): """hammer level of help included in test: Base level hammer help includes report-templates, Command level hammer help contains usage details, @@ -143,9 +122,9 @@ def test_positive_report_help(): report-templates create command details are displayed """ - command_output = Base().execute('--help') + command_output = module_target_sat.cli.Base().execute('--help') assert 'report-template' in command_output - command_output = Base().execute('report-template --help') + command_output = module_target_sat.cli.Base().execute('report-template --help') assert all( [ phrase in command_output @@ -158,7 +137,7 @@ def test_positive_report_help(): ] ] ) - command_output = Base().execute('report-template create --help') + command_output = module_target_sat.cli.Base().execute('report-template create --help') assert all( [ phrase in command_output @@ -169,7 +148,7 @@ def test_positive_report_help(): @pytest.mark.tier1 @pytest.mark.e2e -def test_positive_end_to_end_crud_and_list(): +def test_positive_end_to_end_crud_and_list(target_sat): """CRUD test + list test for report templates :id: 2a143ddf-683f-49e2-badb-f9a387cfc53c @@ -197,34 +176,36 @@ def test_positive_end_to_end_crud_and_list(): """ # create name = gen_alpha() - report_template = make_report_template({'name': name}) + report_template = target_sat.cli_factory.report_template({'name': name}) assert report_template['name'] == name # list - create second template tmp_name = gen_alpha() - tmp_report_template = make_report_template({'name': tmp_name}) - result_list = ReportTemplate.list() + tmp_report_template = target_sat.cli_factory.report_template({'name': tmp_name}) + result_list = target_sat.cli.ReportTemplate.list() assert name in [rt['name'] for rt in result_list] # info - result = ReportTemplate.info({'id': report_template['id']}) + result = target_sat.cli.ReportTemplate.info({'id': report_template['id']}) assert name == result['name'] # update new_name = gen_alpha() - result = ReportTemplate.update({'name': report_template['name'], 'new-name': new_name}) + result = target_sat.cli.ReportTemplate.update( + {'name': report_template['name'], 'new-name': new_name} + ) assert new_name == result[0]['name'] - rt_list = ReportTemplate.list() + rt_list = target_sat.cli.ReportTemplate.list() assert name not in [rt['name'] for rt in rt_list] # delete tmp - ReportTemplate.delete({'name': tmp_report_template['name']}) + target_sat.cli.ReportTemplate.delete({'name': tmp_report_template['name']}) with pytest.raises(CLIReturnCodeError): - ReportTemplate.info({'id': tmp_report_template['id']}) + target_sat.cli.ReportTemplate.info({'id': tmp_report_template['id']}) @pytest.mark.tier1 -def test_positive_generate_report_nofilter_and_with_filter(): +def test_positive_generate_report_nofilter_and_with_filter(module_target_sat): """Generate Host Status report without filter and with filter :id: 5af03399-b918-468a-9306-1c76dda6a369 @@ -243,21 +224,23 @@ def test_positive_generate_report_nofilter_and_with_filter(): :CaseImportance: Critical """ host_name = gen_alpha() - host1 = make_fake_host({'name': host_name}) + host1 = module_target_sat.cli_factory.make_fake_host({'name': host_name}) host_name_2 = gen_alpha() - host2 = make_fake_host({'name': host_name_2}) + host2 = module_target_sat.cli_factory.make_fake_host({'name': host_name_2}) - result_list = ReportTemplate.list() + result_list = module_target_sat.cli.ReportTemplate.list() assert 'Host - Statuses' in [rt['name'] for rt in result_list] - rt_host_statuses = ReportTemplate.info({'name': 'Host - Statuses'}) - result_no_filter = ReportTemplate.generate({'name': rt_host_statuses['name']}) + rt_host_statuses = module_target_sat.cli.ReportTemplate.info({'name': 'Host - Statuses'}) + result_no_filter = module_target_sat.cli.ReportTemplate.generate( + {'name': rt_host_statuses['name']} + ) assert host1['name'] in [item.split(',')[0] for item in result_no_filter.splitlines()] assert host2['name'] in [item.split(',')[0] for item in result_no_filter.splitlines()] - result = ReportTemplate.generate( + result = module_target_sat.cli.ReportTemplate.generate( { 'name': rt_host_statuses['name'], 'inputs': ( @@ -270,7 +253,7 @@ def test_positive_generate_report_nofilter_and_with_filter(): @pytest.mark.tier2 -def test_positive_lock_and_unlock_report(): +def test_positive_lock_and_unlock_report(module_target_sat): """Lock and unlock report template :id: df306515-8798-4ce3-9430-6bc3bf9b9b33 @@ -287,19 +270,23 @@ def test_positive_lock_and_unlock_report(): :CaseImportance: Medium """ name = gen_alpha() - report_template = make_report_template({'name': name}) - ReportTemplate.update({'name': report_template['name'], 'locked': 1}) + report_template = module_target_sat.cli_factory.report_template({'name': name}) + module_target_sat.cli.ReportTemplate.update({'name': report_template['name'], 'locked': 1}) new_name = gen_alpha() with pytest.raises(CLIReturnCodeError): - ReportTemplate.update({'name': report_template['name'], 'new-name': new_name}) + module_target_sat.cli.ReportTemplate.update( + {'name': report_template['name'], 'new-name': new_name} + ) - ReportTemplate.update({'name': report_template['name'], 'locked': 0}) - result = ReportTemplate.update({'name': report_template['name'], 'new-name': new_name}) + module_target_sat.cli.ReportTemplate.update({'name': report_template['name'], 'locked': 0}) + result = module_target_sat.cli.ReportTemplate.update( + {'name': report_template['name'], 'new-name': new_name} + ) assert result[0]['name'] == new_name @pytest.mark.tier2 -def test_positive_report_add_userinput(): +def test_positive_report_add_userinput(module_target_sat): """Add user input to template :id: 84b577db-144e-4761-a46e-e83887464986 @@ -314,17 +301,17 @@ def test_positive_report_add_userinput(): """ name = gen_alpha() - report_template = make_report_template({'name': name}) + report_template = module_target_sat.cli_factory.report_template({'name': name}) ti_name = gen_alpha() - template_input = make_template_input( + template_input = module_target_sat.cli_factory.template_input( {'name': ti_name, 'input-type': 'user', 'template-id': report_template['id']} ) - result = ReportTemplate.info({'name': report_template['name']}) + result = module_target_sat.cli.ReportTemplate.info({'name': report_template['name']}) assert result['template-inputs'][0]['name'] == template_input['name'] @pytest.mark.tier2 -def test_positive_dump_report(): +def test_positive_dump_report(module_target_sat): """Export report template :id: 84b577db-144e-4761-a42e-a83887464986 @@ -341,13 +328,15 @@ def test_positive_dump_report(): """ name = gen_alpha() content = gen_alpha() - report_template = make_report_template({'name': name, 'content': content}) - result = ReportTemplate.dump({'id': report_template['id']}) + report_template = module_target_sat.cli_factory.report_template( + {'name': name, 'content': content} + ) + result = module_target_sat.cli.ReportTemplate.dump({'id': report_template['id']}) assert content in result @pytest.mark.tier2 -def test_positive_clone_locked_report(): +def test_positive_clone_locked_report(module_target_sat): """Clone locked report template :id: cc843731-b9c2-4fc9-9e15-d1ee5d967cda @@ -364,19 +353,21 @@ def test_positive_clone_locked_report(): """ name = gen_alpha() - report_template = make_report_template({'name': name}) - ReportTemplate.update({'name': report_template['name'], 'locked': 1, 'default': 1}) + report_template = module_target_sat.cli_factory.report_template({'name': name}) + module_target_sat.cli.ReportTemplate.update( + {'name': report_template['name'], 'locked': 1, 'default': 1} + ) new_name = gen_alpha() - ReportTemplate.clone({'id': report_template['id'], 'new-name': new_name}) - result_list = ReportTemplate.list() + module_target_sat.cli.ReportTemplate.clone({'id': report_template['id'], 'new-name': new_name}) + result_list = module_target_sat.cli.ReportTemplate.list() assert new_name in [rt['name'] for rt in result_list] - result_info = ReportTemplate.info({'id': report_template['id']}) + result_info = module_target_sat.cli.ReportTemplate.info({'id': report_template['id']}) assert result_info['locked'] == 'yes' assert result_info['default'] == 'yes' @pytest.mark.tier2 -def test_positive_generate_report_sanitized(): +def test_positive_generate_report_sanitized(module_target_sat): """Generate report template where there are values in comma outputted which might brake CSV format @@ -395,10 +386,10 @@ def test_positive_generate_report_sanitized(): """ # create a name that has a comma in it, some randomized text, and no spaces. os_name = gen_alpha(start='test', separator=',').replace(' ', '') - architecture = make_architecture() - partition_table = make_partition_table() - medium = make_medium() - os = make_os( + architecture = module_target_sat.cli_factory.make_architecture() + partition_table = module_target_sat.cli_factory.make_partition_table() + medium = module_target_sat.cli_factory.make_medium() + os = module_target_sat.cli_factory.make_os( { 'name': os_name, 'architecture-ids': architecture['id'], @@ -408,7 +399,7 @@ def test_positive_generate_report_sanitized(): ) host_name = gen_alpha() - host = make_fake_host( + host = module_target_sat.cli_factory.make_fake_host( { 'name': host_name, 'architecture-id': architecture['id'], @@ -418,9 +409,11 @@ def test_positive_generate_report_sanitized(): } ) - report_template = make_report_template({'content': REPORT_TEMPLATE_FILE}) + report_template = module_target_sat.cli_factory.report_template( + {'content': REPORT_TEMPLATE_FILE} + ) - result = ReportTemplate.generate({'name': report_template['name']}) + result = module_target_sat.cli.ReportTemplate.generate({'name': report_template['name']}) assert 'Name,Operating System' in result # verify header of custom template assert f'{host["name"]},"{host["operating-system"]["operating-system"]}"' in result @@ -494,7 +487,7 @@ def test_positive_generate_email_uncompressed(): @pytest.mark.tier2 -def test_negative_create_report_without_name(): +def test_negative_create_report_without_name(module_target_sat): """Try to create a report template with empty name :id: 84b577db-144e-4771-a42e-e93887464986 @@ -510,11 +503,11 @@ def test_negative_create_report_without_name(): :CaseImportance: Medium """ with pytest.raises(CLIFactoryError): - make_report_template({'name': ''}) + module_target_sat.cli_factory.report_template({'name': ''}) @pytest.mark.tier2 -def test_negative_delete_locked_report(): +def test_negative_delete_locked_report(module_target_sat): """Try to delete a locked report template :id: 84b577db-144e-4871-a42e-e93887464986 @@ -530,16 +523,16 @@ def test_negative_delete_locked_report(): :CaseImportance: Medium """ name = gen_alpha() - report_template = make_report_template({'name': name}) + report_template = module_target_sat.cli_factory.report_template({'name': name}) - ReportTemplate.update({'name': report_template['name'], 'locked': 1}) + module_target_sat.cli.ReportTemplate.update({'name': report_template['name'], 'locked': 1}) with pytest.raises(CLIReturnCodeError): - ReportTemplate.delete({'name': report_template['name']}) + module_target_sat.cli.ReportTemplate.delete({'name': report_template['name']}) @pytest.mark.tier2 -def test_negative_bad_email(): +def test_negative_bad_email(module_target_sat): """Report can't be generated when incorrectly formed mail specified :id: a4ba77db-144e-4871-a42e-e93887464986 @@ -555,14 +548,16 @@ def test_negative_bad_email(): :CaseImportance: Medium """ name = gen_alpha() - report_template = make_report_template({'name': name}) + report_template = module_target_sat.cli_factory.report_template({'name': name}) with pytest.raises(CLIReturnCodeError): - ReportTemplate.schedule({'name': report_template['name'], 'mail-to': gen_alpha()}) + module_target_sat.cli.ReportTemplate.schedule( + {'name': report_template['name'], 'mail-to': gen_alpha()} + ) @pytest.mark.tier3 -def test_negative_nonauthor_of_report_cant_download_it(): +def test_negative_nonauthor_of_report_cant_download_it(module_target_sat): """The resulting report should only be downloadable by the user that generated it or admin. Check. @@ -581,10 +576,10 @@ def test_negative_nonauthor_of_report_cant_download_it(): uname_viewer2 = gen_alpha() password = gen_alpha() - loc = Location.info({'name': DEFAULT_LOC}) - org = Org.info({'name': DEFAULT_ORG}) + loc = module_target_sat.cli.Location.info({'name': DEFAULT_LOC}) + org = module_target_sat.cli.Org.info({'name': DEFAULT_ORG}) - user1 = make_user( + user1 = module_target_sat.cli_factory.user( { 'login': uname_viewer, 'password': password, @@ -593,7 +588,7 @@ def test_negative_nonauthor_of_report_cant_download_it(): } ) - user2 = make_user( + user2 = module_target_sat.cli_factory.user( { 'login': uname_viewer2, 'password': password, @@ -602,62 +597,72 @@ def test_negative_nonauthor_of_report_cant_download_it(): } ) - role = make_role() + role = module_target_sat.cli_factory.make_role() # Pick permissions by its resource type permissions_org = [ permission['name'] - for permission in Filter.available_permissions({'search': 'resource_type=Organization'}) + for permission in module_target_sat.cli.Filter.available_permissions( + {'search': 'resource_type=Organization'} + ) ] permissions_loc = [ permission['name'] - for permission in Filter.available_permissions({'search': 'resource_type=Location'}) + for permission in module_target_sat.cli.Filter.available_permissions( + {'search': 'resource_type=Location'} + ) ] permissions_rt = [ permission['name'] - for permission in Filter.available_permissions({'search': 'resource_type=ReportTemplate'}) + for permission in module_target_sat.cli.Filter.available_permissions( + {'search': 'resource_type=ReportTemplate'} + ) ] permissions_pt = [ permission['name'] - for permission in Filter.available_permissions( + for permission in module_target_sat.cli.Filter.available_permissions( {'search': 'resource_type=ProvisioningTemplate'} ) ] permissions_jt = [ permission['name'] - for permission in Filter.available_permissions({'search': 'resource_type=JobTemplate'}) + for permission in module_target_sat.cli.Filter.available_permissions( + {'search': 'resource_type=JobTemplate'} + ) ] # Assign filters to created role for perm in [permissions_org, permissions_loc, permissions_rt, permissions_pt, permissions_jt]: - make_filter({'role-id': role['id'], 'permissions': perm}) - User.add_role({'login': user1['login'], 'role-id': role['id']}) - User.add_role({'login': user2['login'], 'role-id': role['id']}) + module_target_sat.cli_factory.make_filter({'role-id': role['id'], 'permissions': perm}) + module_target_sat.cli.User.add_role({'login': user1['login'], 'role-id': role['id']}) + module_target_sat.cli.User.add_role({'login': user2['login'], 'role-id': role['id']}) name = gen_alpha() content = gen_alpha() - report_template = ReportTemplate.with_user(username=user1['login'], password=password).create( + report_template = module_target_sat.cli.ReportTemplate.with_user( + username=user1['login'], password=password + ).create( {'name': name, 'organization-id': org['id'], 'location-id': loc['id'], 'file': content} ) - schedule = ReportTemplate.with_user(username=user1['login'], password=password).schedule( - {'name': report_template['name']} - ) + schedule = module_target_sat.cli.ReportTemplate.with_user( + username=user1['login'], password=password + ).schedule({'name': report_template['name']}) job_id = schedule.split('Job ID: ', 1)[1].strip() - report_data = ReportTemplate.with_user(username=user1['login'], password=password).report_data( - {'id': report_template['name'], 'job-id': job_id} - ) + report_data = module_target_sat.cli.ReportTemplate.with_user( + username=user1['login'], password=password + ).report_data({'id': report_template['name'], 'job-id': job_id}) assert content in report_data with pytest.raises(CLIReturnCodeError): - ReportTemplate.with_user(username=user2['login'], password=password).report_data( - {'id': report_template['name'], 'job-id': job_id} - ) + module_target_sat.cli.ReportTemplate.with_user( + username=user2['login'], password=password + ).report_data({'id': report_template['name'], 'job-id': job_id}) @pytest.mark.tier2 @pytest.mark.skip_if_open('BZ:1750924') -def test_positive_generate_with_name_and_org(): +def test_positive_generate_with_name_and_org(module_target_sat): """Generate Host Status report, specifying template name and organization :id: 5af03399-b918-468a-1306-1c76dda6f369 @@ -680,16 +685,18 @@ def test_positive_generate_with_name_and_org(): :BZ: 1750924 """ host_name = gen_alpha() - host = make_fake_host({'name': host_name}) + host = module_target_sat.cli_factory.make_fake_host({'name': host_name}) - result = ReportTemplate.generate({'name': 'Host - Statuses', 'organization': DEFAULT_ORG}) + result = module_target_sat.cli.ReportTemplate.generate( + {'name': 'Host - Statuses', 'organization': DEFAULT_ORG} + ) assert host['name'] in [item.split(',')[0] for item in result.split('\n')] @pytest.mark.tier2 @pytest.mark.skip_if_open('BZ:1782807') -def test_positive_generate_ansible_template(): +def test_positive_generate_ansible_template(module_target_sat): """Report template named 'Ansible Inventory' (default name is specified in settings) must be present in Satellite 6.7 and later in order to provide enhanced functionality for Ansible Tower inventory synchronization with Satellite. @@ -710,19 +717,19 @@ def test_positive_generate_ansible_template(): :CaseImportance: Medium """ - settings = Settings.list({'search': 'name=ansible_inventory_template'}) + settings = module_target_sat.cli.Settings.list({'search': 'name=ansible_inventory_template'}) assert 1 == len(settings) template_name = settings[0]['value'] - report_list = ReportTemplate.list() + report_list = module_target_sat.cli.ReportTemplate.list() assert template_name in [rt['name'] for rt in report_list] login = gen_alpha().lower() password = gen_alpha().lower() - loc = Location.info({'name': DEFAULT_LOC}) - org = Org.info({'name': DEFAULT_ORG}) + loc = module_target_sat.cli.Location.info({'name': DEFAULT_LOC}) + org = module_target_sat.cli.Org.info({'name': DEFAULT_ORG}) - user = make_user( + user = module_target_sat.cli_factory.user( { 'login': login, 'password': password, @@ -731,19 +738,21 @@ def test_positive_generate_ansible_template(): } ) - User.add_role({'login': user['login'], 'role': 'Ansible Tower Inventory Reader'}) + module_target_sat.cli.User.add_role( + {'login': user['login'], 'role': 'Ansible Tower Inventory Reader'} + ) host_name = gen_alpha().lower() - host = make_fake_host({'name': host_name}) + host = module_target_sat.cli_factory.make_fake_host({'name': host_name}) - schedule = ReportTemplate.with_user(username=user['login'], password=password).schedule( - {'name': template_name} - ) + schedule = module_target_sat.cli.ReportTemplate.with_user( + username=user['login'], password=password + ).schedule({'name': template_name}) job_id = schedule.split('Job ID: ', 1)[1].strip() - report_data = ReportTemplate.with_user(username=user['login'], password=password).report_data( - {'name': template_name, 'job-id': job_id} - ) + report_data = module_target_sat.cli.ReportTemplate.with_user( + username=user['login'], password=password + ).report_data({'name': template_name, 'job-id': job_id}) assert host['name'] in [item.split(',')[1] for item in report_data.split('\n') if len(item) > 0] @@ -776,7 +785,7 @@ def test_positive_generate_entitlements_report_multiple_formats( client.install_katello_ca(target_sat) client.register_contenthost(module_entitlement_manifest_org.label, local_ak['name']) assert client.subscribed - result_html = ReportTemplate.generate( + result_html = target_sat.cli.ReportTemplate.generate( { 'organization': module_entitlement_manifest_org.name, 'name': 'Subscription - Entitlement Report', @@ -786,7 +795,7 @@ def test_positive_generate_entitlements_report_multiple_formats( ) assert client.hostname in result_html assert local_subscription['name'] in result_html - result_yaml = ReportTemplate.generate( + result_yaml = target_sat.cli.ReportTemplate.generate( { 'organization': module_entitlement_manifest_org.name, 'name': 'Subscription - Entitlement Report', @@ -799,7 +808,7 @@ def test_positive_generate_entitlements_report_multiple_formats( assert client.hostname in entry elif 'Subscription Name:' in entry: assert local_subscription['name'] in entry - result_csv = ReportTemplate.generate( + result_csv = target_sat.cli.ReportTemplate.generate( { 'organization': module_entitlement_manifest_org.name, 'name': 'Subscription - Entitlement Report', @@ -838,7 +847,7 @@ def test_positive_schedule_entitlements_report( client.install_katello_ca(target_sat) client.register_contenthost(module_entitlement_manifest_org.label, local_ak['name']) assert client.subscribed - scheduled_csv = ReportTemplate.schedule( + scheduled_csv = target_sat.cli.ReportTemplate.schedule( { 'name': 'Subscription - Entitlement Report', 'organization': module_entitlement_manifest_org.name, @@ -846,7 +855,7 @@ def test_positive_schedule_entitlements_report( 'inputs': 'Days from Now=no limit', } ) - data_csv = ReportTemplate.report_data( + data_csv = target_sat.cli.ReportTemplate.report_data( { 'name': 'Subscription - Entitlement Report', 'job-id': scheduled_csv.split('\n', 1)[0].split('Job ID: ', 1)[1], @@ -879,7 +888,7 @@ def test_positive_generate_hostpkgcompare( :BZ: 1860430 """ # Add subscription to Satellite Tools repo to activation key - setup_org_for_a_rh_repo( + target_sat.cli_factory.setup_org_for_a_rh_repo( { 'product': PRDS['rhel'], 'repository-set': REPOSET['rhst7'], @@ -890,7 +899,7 @@ def test_positive_generate_hostpkgcompare( 'activationkey-id': local_ak['id'], } ) - setup_org_for_a_custom_repo( + target_sat.cli_factory.setup_org_for_a_custom_repo( { 'url': settings.repos.yum_6.url, 'organization-id': module_entitlement_manifest_org.id, @@ -912,7 +921,7 @@ def test_positive_generate_hostpkgcompare( client.enable_repo(REPOS['rhst7']['id']) client.install_katello_agent() clients.sort(key=lambda client: client.hostname) - hosts_info = [Host.info({'name': client.hostname}) for client in clients] + hosts_info = [target_sat.cli.Host.info({'name': client.hostname}) for client in clients] host1, host2 = hosts_info res = clients[0].execute( @@ -922,7 +931,7 @@ def test_positive_generate_hostpkgcompare( res = clients[1].execute(f'yum -y install {FAKE_2_CUSTOM_PACKAGE}') assert not res.status - result = ReportTemplate.generate( + result = target_sat.cli.ReportTemplate.generate( { 'name': 'Host - compare content hosts packages', 'inputs': f'Host 1 = {host1["name"]}, ' f'Host 2 = {host2["name"]}', @@ -963,7 +972,7 @@ def test_positive_generate_hostpkgcompare( @pytest.mark.tier3 -def test_negative_generate_hostpkgcompare_nonexistent_host(): +def test_negative_generate_hostpkgcompare_nonexistent_host(module_target_sat): """Try to generate 'Host - compare content hosts packages' report with nonexistent hosts inputs @@ -982,7 +991,7 @@ def test_negative_generate_hostpkgcompare_nonexistent_host(): :BZ: 1860351 """ with pytest.raises(CLIReturnCodeError) as cm: - ReportTemplate.generate( + module_target_sat.cli.ReportTemplate.generate( { 'name': 'Host - compare content hosts packages', 'inputs': 'Host 1 = nonexistent1, ' 'Host 2 = nonexistent2', @@ -1021,7 +1030,7 @@ def test_positive_generate_installed_packages_report( :customerscenario: true """ - setup_org_for_a_custom_repo( + target_sat.cli_factory.setup_org_for_a_custom_repo( { 'url': settings.repos.yum_6.url, 'organization-id': module_entitlement_manifest_org.id, diff --git a/tests/foreman/cli/test_repository.py b/tests/foreman/cli/test_repository.py index 85a9d751bd4..440dd4f128b 100644 --- a/tests/foreman/cli/test_repository.py +++ b/tests/foreman/cli/test_repository.py @@ -25,36 +25,6 @@ import requests from wait_for import wait_for -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.content_export import ContentExport -from robottelo.cli.content_import import ContentImport -from robottelo.cli.contentview import ContentView -from robottelo.cli.factory import ( - CLIFactoryError, - make_content_credential, - make_content_view, - make_filter, - make_lifecycle_environment, - make_location, - make_org, - make_product, - make_repository, - make_role, - make_user, -) -from robottelo.cli.file import File -from robottelo.cli.filter import Filter -from robottelo.cli.module_stream import ModuleStream -from robottelo.cli.org import Org -from robottelo.cli.package import Package -from robottelo.cli.product import Product -from robottelo.cli.repository import Repository -from robottelo.cli.repository_set import RepositorySet -from robottelo.cli.role import Role -from robottelo.cli.settings import Settings -from robottelo.cli.srpm import Srpm -from robottelo.cli.task import Task -from robottelo.cli.user import User from robottelo.config import settings from robottelo.constants import ( CONTAINER_REGISTRY_HUB, @@ -79,6 +49,7 @@ FAKE_YUM_MD5_REPO, FAKE_YUM_SRPM_REPO, ) +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.logging import logger from robottelo.utils.datafactory import ( invalid_values_list, @@ -87,9 +58,7 @@ valid_docker_repository_names, valid_http_credentials, ) - -# from robottelo.constants.repos import FEDORA_OSTREE_REPO - +from tests.foreman.api.test_contentview import content_view YUM_REPOS = ( settings.repos.yum_0.url, @@ -107,23 +76,26 @@ ) -def _get_image_tags_count(repo): - return Repository.info({'id': repo['id']}) +def _get_image_tags_count(repo, sat): + return sat.cli.Repository.info({'id': repo['id']}) -def _validated_image_tags_count(repo): +def _validated_image_tags_count(repo, sat): """Wrapper around Repository.info(), that returns once container-image-tags in repo is greater than 0. Needed due to BZ#1664631 (container-image-tags is not populated immediately after synchronization), which was CLOSED WONTFIX """ wait_for( - lambda: int(_get_image_tags_count(repo=repo)['content-counts']['container-image-tags']) > 0, + lambda: int( + _get_image_tags_count(repo=repo, sat=sat)['content-counts']['container-image-tags'] + ) + > 0, timeout=30, delay=2, logger=logger, ) - return _get_image_tags_count(repo=repo) + return _get_image_tags_count(repo=repo, sat=sat) @pytest.fixture @@ -136,15 +108,15 @@ def repo_options(request, module_org, module_product): @pytest.fixture -def repo(repo_options): +def repo(repo_options, target_sat): """create a new repository.""" - return make_repository(repo_options) + return target_sat.cli_factory.make_repository(repo_options) @pytest.fixture -def gpg_key(module_org): +def gpg_key(module_org, module_target_sat): """Create a new GPG key.""" - return make_content_credential({'organization-id': module_org.id}) + return module_target_sat.cli_factory.make_content_credential({'organization-id': module_org.id}) class TestRepository: @@ -350,7 +322,7 @@ def test_positive_mirroring_policy(self, repo_options, repo): @pytest.mark.parametrize( 'repo_options', **parametrized([{'content-type': 'yum'}]), indirect=True ) - def test_positive_create_with_default_download_policy(self, repo_options, repo): + def test_positive_create_with_default_download_policy(self, repo_options, repo, target_sat): """Verify if the default download policy is assigned when creating a YUM repo without `--download-policy` @@ -362,7 +334,7 @@ def test_positive_create_with_default_download_policy(self, repo_options, repo): :CaseImportance: Critical """ - default_dl_policy = Settings.list({'search': 'name=default_download_policy'}) + default_dl_policy = target_sat.cli.Settings.list({'search': 'name=default_download_policy'}) assert default_dl_policy assert repo.get('download-policy') == default_dl_policy[0]['value'] @@ -370,7 +342,7 @@ def test_positive_create_with_default_download_policy(self, repo_options, repo): @pytest.mark.parametrize( 'repo_options', **parametrized([{'content-type': 'yum'}]), indirect=True ) - def test_positive_create_immediate_update_to_on_demand(self, repo_options, repo): + def test_positive_create_immediate_update_to_on_demand(self, repo_options, repo, target_sat): """Update `immediate` download policy to `on_demand` for a newly created YUM repository @@ -385,8 +357,8 @@ def test_positive_create_immediate_update_to_on_demand(self, repo_options, repo) :BZ: 1732056 """ assert repo.get('download-policy') == 'immediate' - Repository.update({'id': repo['id'], 'download-policy': 'on_demand'}) - result = Repository.info({'id': repo['id']}) + target_sat.cli.Repository.update({'id': repo['id'], 'download-policy': 'on_demand'}) + result = target_sat.cli.Repository.info({'id': repo['id']}) assert result.get('download-policy') == 'on_demand' @pytest.mark.tier1 @@ -395,7 +367,7 @@ def test_positive_create_immediate_update_to_on_demand(self, repo_options, repo) **parametrized([{'content-type': 'yum', 'download-policy': 'on_demand'}]), indirect=True, ) - def test_positive_create_on_demand_update_to_immediate(self, repo_options, repo): + def test_positive_create_on_demand_update_to_immediate(self, repo_options, repo, target_sat): """Update `on_demand` download policy to `immediate` for a newly created YUM repository @@ -407,13 +379,13 @@ def test_positive_create_on_demand_update_to_immediate(self, repo_options, repo) :CaseImportance: Critical """ - Repository.update({'id': repo['id'], 'download-policy': 'immediate'}) - result = Repository.info({'id': repo['id']}) + target_sat.cli.Repository.update({'id': repo['id'], 'download-policy': 'immediate'}) + result = target_sat.cli.Repository.info({'id': repo['id']}) assert result['download-policy'] == 'immediate' @pytest.mark.tier1 @pytest.mark.upgrade - def test_positive_create_with_gpg_key_by_id(self, repo_options, gpg_key): + def test_positive_create_with_gpg_key_by_id(self, repo_options, gpg_key, target_sat): """Check if repository can be created with gpg key ID :id: 6d22f0ea-2d27-4827-9b7a-3e1550a47285 @@ -425,7 +397,7 @@ def test_positive_create_with_gpg_key_by_id(self, repo_options, gpg_key): :CaseImportance: Critical """ repo_options['gpg-key-id'] = gpg_key['id'] - repo = make_repository(repo_options) + repo = target_sat.cli_factory.make_repository(repo_options) assert repo['gpg-key']['id'] == gpg_key['id'] assert repo['gpg-key']['name'] == gpg_key['name'] @@ -590,16 +562,18 @@ def test_positive_create_repo_with_new_organization_and_location(self, target_sa :CaseImportance: High """ - new_org = make_org() - new_location = make_location() - new_product = make_product( + new_org = target_sat.cli_factory.make_org() + new_location = target_sat.cli_factory.make_location() + new_product = target_sat.cli_factory.make_product( {'organization-id': new_org['id'], 'description': 'test_product'} ) - Org.add_location({'location-id': new_location['id'], 'name': new_org['name']}) - assert new_location['name'] in Org.info({'id': new_org['id']})['locations'] - make_repository( + target_sat.cli.Org.add_location( + {'location-id': new_location['id'], 'name': new_org['name']} + ) + assert new_location['name'] in target_sat.cli.Org.info({'id': new_org['id']})['locations'] + target_sat.cli_factory.make_repository( { - 'location-id': new_location['id'], + 'content-type': 'yum', 'organization-id': new_org['id'], 'product-id': new_product['id'], } @@ -617,7 +591,7 @@ def test_positive_create_repo_with_new_organization_and_location(self, target_sa **parametrized([{'name': name} for name in invalid_values_list()]), indirect=True, ) - def test_negative_create_with_name(self, repo_options): + def test_negative_create_with_name(self, repo_options, target_sat): """Repository name cannot be 300-characters long :id: af0652d3-012d-4846-82ac-047918f74722 @@ -629,7 +603,7 @@ def test_negative_create_with_name(self, repo_options): :CaseImportance: Critical """ with pytest.raises(CLIFactoryError): - make_repository(repo_options) + target_sat.cli_factory.make_repository(repo_options) @pytest.mark.tier1 @pytest.mark.parametrize( @@ -637,7 +611,9 @@ def test_negative_create_with_name(self, repo_options): **parametrized([{'url': f'http://{gen_string("alpha")}{punctuation}.com'}]), indirect=True, ) - def test_negative_create_with_url_with_special_characters(self, repo_options): + def test_negative_create_with_url_with_special_characters( + self, repo_options, module_target_sat + ): """Verify that repository URL cannot contain unquoted special characters :id: 2bd5ee17-0fe5-43cb-9cdc-dc2178c5374c @@ -649,7 +625,7 @@ def test_negative_create_with_url_with_special_characters(self, repo_options): :CaseImportance: Critical """ with pytest.raises(CLIFactoryError): - make_repository(repo_options) + module_target_sat.cli_factory.make_repository(repo_options) @pytest.mark.tier1 @pytest.mark.parametrize( @@ -657,7 +633,7 @@ def test_negative_create_with_url_with_special_characters(self, repo_options): **parametrized([{'content-type': 'yum', 'download-policy': gen_string('alpha', 5)}]), indirect=True, ) - def test_negative_create_with_invalid_download_policy(self, repo_options): + def test_negative_create_with_invalid_download_policy(self, repo_options, module_target_sat): """Verify that YUM repository cannot be created with invalid download policy @@ -671,13 +647,13 @@ def test_negative_create_with_invalid_download_policy(self, repo_options): :CaseImportance: Critical """ with pytest.raises(CLIFactoryError): - make_repository(repo_options) + module_target_sat.cli_factory.make_repository(repo_options) @pytest.mark.tier1 @pytest.mark.parametrize( 'repo_options', **parametrized([{'content-type': 'yum'}]), indirect=True ) - def test_negative_update_to_invalid_download_policy(self, repo_options, repo): + def test_negative_update_to_invalid_download_policy(self, repo_options, repo, target_sat): """Verify that YUM repository cannot be updated to invalid download policy @@ -691,7 +667,9 @@ def test_negative_update_to_invalid_download_policy(self, repo_options, repo): :CaseImportance: Critical """ with pytest.raises(CLIReturnCodeError): - Repository.update({'id': repo['id'], 'download-policy': gen_string('alpha', 5)}) + target_sat.cli.Repository.update( + {'id': repo['id'], 'download-policy': gen_string('alpha', 5)} + ) @pytest.mark.tier1 @pytest.mark.parametrize( @@ -706,7 +684,7 @@ def test_negative_update_to_invalid_download_policy(self, repo_options, repo): ), indirect=True, ) - def test_negative_create_non_yum_with_download_policy(self, repo_options): + def test_negative_create_non_yum_with_download_policy(self, repo_options, module_target_sat): """Verify that non-YUM repositories cannot be created with download policy TODO: Remove ostree from exceptions when ostree is added back in Satellite 7 @@ -725,7 +703,7 @@ def test_negative_create_non_yum_with_download_policy(self, repo_options): CLIFactoryError, match='Download policy Cannot set attribute download_policy for content type', ): - make_repository(repo_options) + module_target_sat.cli_factory.make_repository(repo_options) @pytest.mark.tier1 @pytest.mark.parametrize( @@ -742,7 +720,7 @@ def test_negative_create_non_yum_with_download_policy(self, repo_options): ), indirect=True, ) - def test_positive_synchronize_yum_repo(self, repo_options, repo): + def test_positive_synchronize_yum_repo(self, repo_options, repo, target_sat): """Check if repository can be created and synced :id: e3a62529-edbd-4062-9246-bef5f33bdcf0 @@ -758,9 +736,9 @@ def test_positive_synchronize_yum_repo(self, repo_options, repo): # Repo is not yet synced assert repo['sync']['status'] == 'Not Synced' # Synchronize it - Repository.synchronize({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) # Verify it has finished - repo = Repository.info({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert repo['sync']['status'] == 'Success' @pytest.mark.tier1 @@ -769,7 +747,7 @@ def test_positive_synchronize_yum_repo(self, repo_options, repo): **parametrized([{'content-type': 'file', 'url': CUSTOM_FILE_REPO}]), indirect=True, ) - def test_positive_synchronize_file_repo(self, repo_options, repo): + def test_positive_synchronize_file_repo(self, repo_options, repo, target_sat): """Check if repository can be created and synced :id: eafc421d-153e-41e1-afbd-938e556ef827 @@ -785,9 +763,9 @@ def test_positive_synchronize_file_repo(self, repo_options, repo): # Assertion that repo is not yet synced assert repo['sync']['status'] == 'Not Synced' # Synchronize it - Repository.synchronize({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) # Verify it has finished - repo = Repository.info({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert repo['sync']['status'] == 'Success' assert int(repo['content-counts']['files']) == CUSTOM_FILE_REPO_FILES_COUNT @@ -809,7 +787,7 @@ def test_positive_synchronize_file_repo(self, repo_options, repo): ), indirect=True, ) - def test_positive_synchronize_auth_yum_repo(self, repo): + def test_positive_synchronize_auth_yum_repo(self, repo, target_sat): """Check if secured repository can be created and synced :id: b0db676b-e0f0-428c-adf3-1d7c0c3599f0 @@ -825,9 +803,9 @@ def test_positive_synchronize_auth_yum_repo(self, repo): # Assertion that repo is not yet synced assert repo['sync']['status'] == 'Not Synced' # Synchronize it - Repository.synchronize({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) # Verify it has finished - new_repo = Repository.info({'id': repo['id']}) + new_repo = target_sat.cli.Repository.info({'id': repo['id']}) assert new_repo['sync']['status'] == 'Success' @pytest.mark.skip_if_open("BZ:2035025") @@ -850,7 +828,7 @@ def test_positive_synchronize_auth_yum_repo(self, repo): ), indirect=['repo_options'], ) - def test_negative_synchronize_auth_yum_repo(self, repo): + def test_negative_synchronize_auth_yum_repo(self, repo, target_sat): """Check if secured repo fails to synchronize with invalid credentials :id: 809905ae-fb76-465d-9468-1f99c4274aeb @@ -864,8 +842,10 @@ def test_negative_synchronize_auth_yum_repo(self, repo): :CaseLevel: Integration """ # Try to synchronize it - repo_sync = Repository.synchronize({'id': repo['id'], 'async': True}) - response = Task.progress({'id': repo_sync[0]['id']}, return_raw_response=True) + repo_sync = target_sat.cli.Repository.synchronize({'id': repo['id'], 'async': True}) + response = target_sat.cli.Task.progress( + {'id': repo_sync[0]['id']}, return_raw_response=True + ) assert "Error: 401, message='Unauthorized'" in response.stderr[1].decode('utf-8') @pytest.mark.tier2 @@ -884,7 +864,9 @@ def test_negative_synchronize_auth_yum_repo(self, repo): ), indirect=True, ) - def test_positive_synchronize_docker_repo(self, repo, module_product, module_org): + def test_positive_synchronize_docker_repo( + self, repo, module_product, module_org, module_target_sat + ): """Check if Docker repository can be created, synced, and deleted :id: cb9ae788-743c-4785-98b2-6ae0c161bc9a @@ -900,17 +882,17 @@ def test_positive_synchronize_docker_repo(self, repo, module_product, module_org # Assertion that repo is not yet synced assert repo['sync']['status'] == 'Not Synced' # Synchronize it - Repository.synchronize({'id': repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) # Verify it has finished - new_repo = Repository.info({'id': repo['id']}) + new_repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert new_repo['sync']['status'] == 'Success' # For BZ#1810165, assert repo can be deleted - Repository.delete({'id': repo['id']}) + module_target_sat.cli.Repository.delete({'id': repo['id']}) assert ( new_repo['name'] - not in Product.info({'id': module_product.id, 'organization-id': module_org.id})[ - 'content' - ] + not in module_target_sat.cli.Product.info( + {'id': module_product.id, 'organization-id': module_org.id} + )['content'] ) @pytest.mark.tier2 @@ -929,7 +911,7 @@ def test_positive_synchronize_docker_repo(self, repo, module_product, module_org ), indirect=True, ) - def test_verify_checksum_container_repo(self, repo): + def test_verify_checksum_container_repo(self, repo, target_sat): """Check if Verify Content Checksum can be run on non container repos :id: c8f0eb45-3cb6-41b2-aad9-52ac847d7bf8 @@ -944,8 +926,8 @@ def test_verify_checksum_container_repo(self, repo): :customerscenario: true """ assert repo['sync']['status'] == 'Not Synced' - Repository.synchronize({'id': repo['id'], 'validate-contents': 'true'}) - new_repo = Repository.info({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id'], 'validate-contents': 'true'}) + new_repo = target_sat.cli.Repository.info({'id': repo['id']}) assert new_repo['sync']['status'] == 'Success' @pytest.mark.tier2 @@ -964,7 +946,9 @@ def test_verify_checksum_container_repo(self, repo): ), indirect=True, ) - def test_positive_synchronize_docker_repo_with_tags_whitelist(self, repo_options, repo): + def test_positive_synchronize_docker_repo_with_tags_whitelist( + self, repo_options, repo, target_sat + ): """Check if only whitelisted tags are synchronized :id: aa820c65-2de1-4b32-8890-98bd8b4320dc @@ -973,8 +957,8 @@ def test_positive_synchronize_docker_repo_with_tags_whitelist(self, repo_options :expectedresults: Only whitelisted tag is synchronized """ - Repository.synchronize({'id': repo['id']}) - repo = _validated_image_tags_count(repo=repo) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = _validated_image_tags_count(repo=repo, sat=target_sat) assert repo_options['include-tags'] in repo['container-image-tags-filter'] assert int(repo['content-counts']['container-image-tags']) == 1 @@ -993,7 +977,7 @@ def test_positive_synchronize_docker_repo_with_tags_whitelist(self, repo_options ), indirect=True, ) - def test_positive_synchronize_docker_repo_set_tags_later_additive(self, repo): + def test_positive_synchronize_docker_repo_set_tags_later_additive(self, repo, target_sat): """Verify that adding tags whitelist and re-syncing after synchronizing full repository doesn't remove content that was already pulled in when mirroring policy is set to additive @@ -1005,13 +989,13 @@ def test_positive_synchronize_docker_repo_set_tags_later_additive(self, repo): :expectedresults: Non-whitelisted tags are not removed """ tags = 'latest' - Repository.synchronize({'id': repo['id']}) - repo = _validated_image_tags_count(repo=repo) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = _validated_image_tags_count(repo=repo, sat=target_sat) assert not repo['container-image-tags-filter'] assert int(repo['content-counts']['container-image-tags']) >= 2 - Repository.update({'id': repo['id'], 'include-tags': tags}) - Repository.synchronize({'id': repo['id']}) - repo = _validated_image_tags_count(repo=repo) + target_sat.cli.Repository.update({'id': repo['id'], 'include-tags': tags}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = _validated_image_tags_count(repo=repo, sat=target_sat) assert tags in repo['container-image-tags-filter'] assert int(repo['content-counts']['container-image-tags']) >= 2 @@ -1030,7 +1014,7 @@ def test_positive_synchronize_docker_repo_set_tags_later_additive(self, repo): ), indirect=True, ) - def test_positive_synchronize_docker_repo_set_tags_later_content_only(self, repo): + def test_positive_synchronize_docker_repo_set_tags_later_content_only(self, repo, target_sat): """Verify that adding tags whitelist and re-syncing after synchronizing full repository does remove content that was already pulled in when mirroring policy is set to content only @@ -1043,13 +1027,13 @@ def test_positive_synchronize_docker_repo_set_tags_later_content_only(self, repo :expectedresults: Non-whitelisted tags are removed """ tags = 'latest' - Repository.synchronize({'id': repo['id']}) - repo = _validated_image_tags_count(repo=repo) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = _validated_image_tags_count(repo=repo, sat=target_sat) assert not repo['container-image-tags-filter'] assert int(repo['content-counts']['container-image-tags']) >= 2 - Repository.update({'id': repo['id'], 'include-tags': tags}) - Repository.synchronize({'id': repo['id']}) - repo = _validated_image_tags_count(repo=repo) + target_sat.cli.Repository.update({'id': repo['id'], 'include-tags': tags}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = _validated_image_tags_count(repo=repo, sat=target_sat) assert tags in repo['container-image-tags-filter'] assert int(repo['content-counts']['container-image-tags']) <= 2 @@ -1068,7 +1052,9 @@ def test_positive_synchronize_docker_repo_set_tags_later_content_only(self, repo ), indirect=True, ) - def test_negative_synchronize_docker_repo_with_mix_valid_invalid_tags(self, repo_options, repo): + def test_negative_synchronize_docker_repo_with_mix_valid_invalid_tags( + self, repo_options, repo, target_sat + ): """Set tags whitelist to contain both valid and invalid (non-existing) tags. Check if only whitelisted tags are synchronized @@ -1078,8 +1064,8 @@ def test_negative_synchronize_docker_repo_with_mix_valid_invalid_tags(self, repo :expectedresults: Only whitelisted tag is synchronized """ - Repository.synchronize({'id': repo['id']}) - repo = _validated_image_tags_count(repo=repo) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = _validated_image_tags_count(repo=repo, sat=target_sat) for tag in repo_options['include-tags'].split(','): assert tag in repo['container-image-tags-filter'] assert int(repo['content-counts']['container-image-tags']) == 1 @@ -1099,7 +1085,9 @@ def test_negative_synchronize_docker_repo_with_mix_valid_invalid_tags(self, repo ), indirect=True, ) - def test_negative_synchronize_docker_repo_with_invalid_tags(self, repo_options, repo): + def test_negative_synchronize_docker_repo_with_invalid_tags( + self, repo_options, repo, target_sat + ): """Set tags whitelist to contain only invalid (non-existing) tags. Check that no data is synchronized. @@ -1109,8 +1097,8 @@ def test_negative_synchronize_docker_repo_with_invalid_tags(self, repo_options, :expectedresults: Tags are not synchronized """ - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) for tag in repo_options['include-tags'].split(','): assert tag in repo['container-image-tags-filter'] assert int(repo['content-counts']['container-image-tags']) == 0 @@ -1121,7 +1109,7 @@ def test_negative_synchronize_docker_repo_with_invalid_tags(self, repo_options, **parametrized([{'content-type': 'yum', 'url': settings.repos.yum_1.url}]), indirect=True, ) - def test_positive_resynchronize_rpm_repo(self, repo): + def test_positive_resynchronize_rpm_repo(self, repo, target_sat): """Check that repository content is resynced after packages were removed from repository @@ -1135,20 +1123,20 @@ def test_positive_resynchronize_rpm_repo(self, repo): :CaseLevel: Integration """ - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert repo['sync']['status'] == 'Success' assert repo['content-counts']['packages'] == '32' # Find repo packages and remove them - packages = Package.list({'repository-id': repo['id']}) - Repository.remove_content( + packages = target_sat.cli.Package.list({'repository-id': repo['id']}) + target_sat.cli.Repository.remove_content( {'id': repo['id'], 'ids': [package['id'] for package in packages]} ) - repo = Repository.info({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert repo['content-counts']['packages'] == '0' # Re-synchronize repository - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert repo['sync']['status'] == 'Success' assert repo['content-counts']['packages'] == '32' @@ -1177,7 +1165,7 @@ def test_positive_resynchronize_rpm_repo(self, repo): ), ) @pytest.mark.tier2 - def test_mirror_on_sync_removes_rpm(self, module_org, repo, repo_options_2): + def test_mirror_on_sync_removes_rpm(self, module_org, repo, repo_options_2, module_target_sat): """ Check that a package removed upstream is removed downstream when the repo is next synced if mirror-on-sync is enabled (the default setting). @@ -1198,36 +1186,40 @@ def test_mirror_on_sync_removes_rpm(self, module_org, repo, repo_options_2): :CaseImportance: Medium """ # Add description to repo 1 and its product - Product.update( + module_target_sat.cli.Product.update( { 'id': repo.get('product')['id'], 'organization': module_org.label, 'description': 'Fake Upstream', } ) - Repository.update({'id': repo['id'], 'description': ['Fake Upstream']}) + module_target_sat.cli.Repository.update( + {'id': repo['id'], 'description': ['Fake Upstream']} + ) # Sync repo 1 from the real upstream - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert repo['sync']['status'] == 'Success' assert repo['content-counts']['packages'] == '32' # Make 2nd repo - prod_2 = make_product({'organization-id': module_org.id, 'description': 'Downstream'}) + prod_2 = module_target_sat.cli_factory.make_product( + {'organization-id': module_org.id, 'description': 'Downstream'} + ) repo_options_2['organization-id'] = module_org.id repo_options_2['product-id'] = prod_2['id'] repo_options_2['url'] = repo.get('published-at') - repo_2 = make_repository(repo_options_2) - Repository.update({'id': repo_2['id'], 'description': ['Downstream']}) - repo_2 = Repository.info({'id': repo_2['id']}) - Repository.synchronize({'id': repo_2['id']}) + repo_2 = module_target_sat.cli_factory.make_repository(repo_options_2) + module_target_sat.cli.Repository.update({'id': repo_2['id'], 'description': ['Downstream']}) + repo_2 = module_target_sat.cli.Repository.info({'id': repo_2['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo_2['id']}) # Get list of repo 1's packages and remove one - package = choice(Package.list({'repository-id': repo['id']})) - Repository.remove_content({'id': repo['id'], 'ids': [package['id']]}) - repo = Repository.info({'id': repo['id']}) + package = choice(module_target_sat.cli.Package.list({'repository-id': repo['id']})) + module_target_sat.cli.Repository.remove_content({'id': repo['id'], 'ids': [package['id']]}) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert repo['content-counts']['packages'] == '31' # Re-synchronize repo_2, the downstream repository - Repository.synchronize({'id': repo_2['id']}) - repo_2 = Repository.info({'id': repo_2['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo_2['id']}) + repo_2 = module_target_sat.cli.Repository.info({'id': repo_2['id']}) assert repo_2['sync']['status'] == 'Success' assert repo_2['content-counts']['packages'] == '31' @@ -1261,8 +1253,8 @@ def test_positive_synchronize_rpm_repo_ignore_SRPM( :CaseLevel: Integration """ - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert repo['sync']['status'] == 'Success' assert repo['content-counts']['source-rpms'] == '0', 'content not ignored correctly' @@ -1270,7 +1262,7 @@ def test_positive_synchronize_rpm_repo_ignore_SRPM( @pytest.mark.skipif( (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) - def test_positive_update_url(self, repo): + def test_positive_update_url(self, repo, module_target_sat): """Update the original url for a repository :id: 1a2cf29b-5c30-4d4c-b6d1-2f227b0a0a57 @@ -1282,9 +1274,9 @@ def test_positive_update_url(self, repo): :CaseImportance: Critical """ # Update the url - Repository.update({'id': repo['id'], 'url': settings.repos.yum_2.url}) + module_target_sat.cli.Repository.update({'id': repo['id'], 'url': settings.repos.yum_2.url}) # Fetch it again - result = Repository.info({'id': repo['id']}) + result = module_target_sat.cli.Repository.info({'id': repo['id']}) assert result['url'] == settings.repos.yum_2.url @pytest.mark.tier1 @@ -1292,7 +1284,9 @@ def test_positive_update_url(self, repo): 'new_repo_options', **parametrized([{'url': f'http://{gen_string("alpha")}{punctuation}'}]), ) - def test_negative_update_url_with_special_characters(self, new_repo_options, repo): + def test_negative_update_url_with_special_characters( + self, new_repo_options, repo, module_target_sat + ): """Verify that repository URL cannot be updated to contain the forbidden characters @@ -1305,13 +1299,15 @@ def test_negative_update_url_with_special_characters(self, new_repo_options, rep :CaseImportance: Critical """ with pytest.raises(CLIReturnCodeError): - Repository.update({'id': repo['id'], 'url': new_repo_options['url']}) + module_target_sat.cli.Repository.update( + {'id': repo['id'], 'url': new_repo_options['url']} + ) # Fetch it again, ensure url hasn't changed. - result = Repository.info({'id': repo['id']}) + result = module_target_sat.cli.Repository.info({'id': repo['id']}) assert result['url'] == repo['url'] @pytest.mark.tier1 - def test_positive_update_gpg_key(self, repo_options, module_org, repo, gpg_key): + def test_positive_update_gpg_key(self, repo_options, module_org, repo, gpg_key, target_sat): """Update the original gpg key :id: 367ff375-4f52-4a8c-b974-8c1c54e3fdd3 @@ -1322,11 +1318,13 @@ def test_positive_update_gpg_key(self, repo_options, module_org, repo, gpg_key): :CaseImportance: Critical """ - Repository.update({'id': repo['id'], 'gpg-key-id': gpg_key['id']}) + target_sat.cli.Repository.update({'id': repo['id'], 'gpg-key-id': gpg_key['id']}) - gpg_key_new = make_content_credential({'organization-id': module_org.id}) - Repository.update({'id': repo['id'], 'gpg-key-id': gpg_key_new['id']}) - result = Repository.info({'id': repo['id']}) + gpg_key_new = target_sat.cli_factory.make_content_credential( + {'organization-id': module_org.id} + ) + target_sat.cli.Repository.update({'id': repo['id'], 'gpg-key-id': gpg_key_new['id']}) + result = target_sat.cli.Repository.info({'id': repo['id']}) assert result['gpg-key']['id'] == gpg_key_new['id'] @pytest.mark.tier1 @@ -1335,7 +1333,7 @@ def test_positive_update_gpg_key(self, repo_options, module_org, repo, gpg_key): **parametrized([{'mirroring-policy': policy} for policy in MIRRORING_POLICIES]), indirect=True, ) - def test_positive_update_mirroring_policy(self, repo, repo_options): + def test_positive_update_mirroring_policy(self, repo, repo_options, module_target_sat): """Update the mirroring policy rule for repository :id: 9bab2537-3223-40d7-bc4c-a51b09d2e812 @@ -1346,15 +1344,17 @@ def test_positive_update_mirroring_policy(self, repo, repo_options): :CaseImportance: Critical """ - Repository.update({'id': repo['id'], 'mirroring-policy': repo_options['mirroring-policy']}) - result = Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.update( + {'id': repo['id'], 'mirroring-policy': repo_options['mirroring-policy']} + ) + result = module_target_sat.cli.Repository.info({'id': repo['id']}) assert result['mirroring-policy'] == MIRRORING_POLICIES[repo_options['mirroring-policy']] @pytest.mark.tier1 @pytest.mark.parametrize( 'repo_options', **parametrized([{'publish-via-http': 'no'}]), indirect=True ) - def test_positive_update_publish_method(self, repo): + def test_positive_update_publish_method(self, repo, module_target_sat): """Update the original publishing method :id: e7bd2667-4851-4a64-9c70-1b5eafbc3f71 @@ -1365,8 +1365,8 @@ def test_positive_update_publish_method(self, repo): :CaseImportance: Critical """ - Repository.update({'id': repo['id'], 'publish-via-http': 'yes'}) - result = Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.update({'id': repo['id'], 'publish-via-http': 'yes'}) + result = module_target_sat.cli.Repository.info({'id': repo['id']}) assert result['publish-via-http'] == 'yes' @pytest.mark.tier1 @@ -1376,7 +1376,9 @@ def test_positive_update_publish_method(self, repo): indirect=True, ) @pytest.mark.parametrize('checksum_type', ['sha1', 'sha256']) - def test_positive_update_checksum_type(self, repo_options, repo, checksum_type): + def test_positive_update_checksum_type( + self, repo_options, repo, checksum_type, module_target_sat + ): """Create a YUM repository and update the checksum type :id: 42f14257-d860-443d-b337-36fd355014bc @@ -1389,8 +1391,8 @@ def test_positive_update_checksum_type(self, repo_options, repo, checksum_type): :CaseImportance: Critical """ assert repo['content-type'] == repo_options['content-type'] - Repository.update({'checksum-type': checksum_type, 'id': repo['id']}) - result = Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.update({'checksum-type': checksum_type, 'id': repo['id']}) + result = module_target_sat.cli.Repository.info({'id': repo['id']}) assert result['checksum-type'] == checksum_type @pytest.mark.tier1 @@ -1408,7 +1410,7 @@ def test_positive_update_checksum_type(self, repo_options, repo, checksum_type): ), indirect=True, ) - def test_negative_create_checksum_with_on_demand_policy(self, repo_options): + def test_negative_create_checksum_with_on_demand_policy(self, repo_options, module_target_sat): """Attempt to create repository with checksum and on_demand policy. :id: 33d712e6-e91f-42bb-8c5d-35bdc427182c @@ -1422,7 +1424,7 @@ def test_negative_create_checksum_with_on_demand_policy(self, repo_options): :BZ: 1732056 """ with pytest.raises(CLIFactoryError): - make_repository(repo_options) + module_target_sat.cli_factory.make_repository(repo_options) @pytest.mark.tier1 @pytest.mark.parametrize( @@ -1430,7 +1432,7 @@ def test_negative_create_checksum_with_on_demand_policy(self, repo_options): **parametrized([{'name': name} for name in valid_data_list().values()]), indirect=True, ) - def test_positive_delete_by_id(self, repo): + def test_positive_delete_by_id(self, repo, target_sat): """Check if repository can be created and deleted :id: bcf096db-0033-4138-90a3-cb7355d5dfaf @@ -1441,9 +1443,9 @@ def test_positive_delete_by_id(self, repo): :CaseImportance: Critical """ - Repository.delete({'id': repo['id']}) + target_sat.cli.Repository.delete({'id': repo['id']}) with pytest.raises(CLIReturnCodeError): - Repository.info({'id': repo['id']}) + target_sat.cli.Repository.info({'id': repo['id']}) @pytest.mark.tier1 @pytest.mark.upgrade @@ -1452,7 +1454,7 @@ def test_positive_delete_by_id(self, repo): **parametrized([{'name': name} for name in valid_data_list().values()]), indirect=True, ) - def test_positive_delete_by_name(self, repo_options, repo): + def test_positive_delete_by_name(self, repo_options, repo, module_target_sat): """Check if repository can be created and deleted :id: 463980a4-dbcf-4178-83a6-1863cf59909a @@ -1463,9 +1465,11 @@ def test_positive_delete_by_name(self, repo_options, repo): :CaseImportance: Critical """ - Repository.delete({'name': repo['name'], 'product-id': repo_options['product-id']}) + module_target_sat.cli.Repository.delete( + {'name': repo['name'], 'product-id': repo_options['product-id']} + ) with pytest.raises(CLIReturnCodeError): - Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.info({'id': repo['id']}) @pytest.mark.tier1 @pytest.mark.parametrize( @@ -1473,7 +1477,7 @@ def test_positive_delete_by_name(self, repo_options, repo): **parametrized([{'content-type': 'yum', 'url': settings.repos.yum_1.url}]), indirect=True, ) - def test_positive_delete_rpm(self, repo): + def test_positive_delete_rpm(self, repo, module_target_sat): """Check if rpm repository with packages can be deleted. :id: 1172492f-d595-4c8e-89c1-fabb21eb04ac @@ -1484,14 +1488,14 @@ def test_positive_delete_rpm(self, repo): :CaseImportance: Critical """ - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert repo['sync']['status'] == 'Success' # Check that there is at least one package assert int(repo['content-counts']['packages']) > 0 - Repository.delete({'id': repo['id']}) + module_target_sat.cli.Repository.delete({'id': repo['id']}) with pytest.raises(CLIReturnCodeError): - Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.info({'id': repo['id']}) @pytest.mark.tier1 @pytest.mark.upgrade @@ -1500,7 +1504,9 @@ def test_positive_delete_rpm(self, repo): **parametrized([{'content-type': 'yum', 'url': settings.repos.yum_1.url}]), indirect=True, ) - def test_positive_remove_content_by_repo_name(self, module_org, module_product, repo): + def test_positive_remove_content_by_repo_name( + self, module_org, module_product, repo, module_target_sat + ): """Synchronize and remove rpm content using repo name :id: a8b6f17d-3b13-4185-920a-2558ace59458 @@ -1513,14 +1519,14 @@ def test_positive_remove_content_by_repo_name(self, module_org, module_product, :CaseImportance: Critical """ - Repository.synchronize( + module_target_sat.cli.Repository.synchronize( { 'name': repo['name'], 'product': module_product.name, 'organization': module_org.name, } ) - repo = Repository.info( + repo = module_target_sat.cli.Repository.info( { 'name': repo['name'], 'product': module_product.name, @@ -1530,14 +1536,14 @@ def test_positive_remove_content_by_repo_name(self, module_org, module_product, assert repo['sync']['status'] == 'Success' assert repo['content-counts']['packages'] == '32' # Find repo packages and remove them - packages = Package.list( + packages = module_target_sat.cli.Package.list( { 'repository': repo['name'], 'product': module_product.name, 'organization': module_org.name, } ) - Repository.remove_content( + module_target_sat.cli.Repository.remove_content( { 'name': repo['name'], 'product': module_product.name, @@ -1545,7 +1551,7 @@ def test_positive_remove_content_by_repo_name(self, module_org, module_product, 'ids': [package['id'] for package in packages], } ) - repo = Repository.info({'id': repo['id']}) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert repo['content-counts']['packages'] == '0' @pytest.mark.tier1 @@ -1555,7 +1561,7 @@ def test_positive_remove_content_by_repo_name(self, module_org, module_product, **parametrized([{'content-type': 'yum', 'url': settings.repos.yum_1.url}]), indirect=True, ) - def test_positive_remove_content_rpm(self, repo): + def test_positive_remove_content_rpm(self, repo, module_target_sat): """Synchronize repository and remove rpm content from it :id: c4bcda0e-c0d6-424c-840d-26684ca7c9f1 @@ -1568,16 +1574,16 @@ def test_positive_remove_content_rpm(self, repo): :CaseImportance: Critical """ - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert repo['sync']['status'] == 'Success' assert repo['content-counts']['packages'] == '32' # Find repo packages and remove them - packages = Package.list({'repository-id': repo['id']}) - Repository.remove_content( + packages = module_target_sat.cli.Package.list({'repository-id': repo['id']}) + module_target_sat.cli.Repository.remove_content( {'id': repo['id'], 'ids': [package['id'] for package in packages]} ) - repo = Repository.info({'id': repo['id']}) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert repo['content-counts']['packages'] == '0' @pytest.mark.tier1 @@ -1662,7 +1668,7 @@ def test_positive_upload_content_to_file_repo(self, repo, target_sat): **parametrized([{'content-type': 'yum', 'url': settings.repos.yum_1.url}]), indirect=True, ) - def test_negative_restricted_user_cv_add_repository(self, module_org, repo): + def test_negative_restricted_user_cv_add_repository(self, module_org, repo, module_target_sat): """Attempt to add a product repository to content view with a restricted user, using product name not visible to restricted user. @@ -1731,7 +1737,7 @@ def test_negative_restricted_user_cv_add_repository(self, module_org, repo): content_view_name = f"Test_{gen_string('alpha', 20)}" # Create a non admin user, for the moment without any permissions - user = make_user( + user = module_target_sat.cli_factory.user( { 'admin': False, 'default-organization-id': module_org.id, @@ -1741,9 +1747,9 @@ def test_negative_restricted_user_cv_add_repository(self, module_org, repo): } ) # Create a new role - role = make_role() + role = module_target_sat.cli_factory.make_role() # Get the available permissions - available_permissions = Filter.available_permissions() + available_permissions = module_target_sat.cli.Filter.available_permissions() # group the available permissions by resource type available_rc_permissions = {} for permission in available_permissions: @@ -1764,43 +1770,45 @@ def test_negative_restricted_user_cv_add_repository(self, module_org, repo): # assert that all the required permissions are available assert set(permission_names) == set(available_permission_names) # Create the current resource type role permissions - make_filter({'role-id': role['id'], 'permissions': permission_names, 'search': search}) + module_target_sat.cli_factory.make_filter( + {'role-id': role['id'], 'permissions': permission_names, 'search': search} + ) # Add the created and initiated role with permissions to user - User.add_role({'id': user['id'], 'role-id': role['id']}) + module_target_sat.cli.User.add_role({'id': user['id'], 'role-id': role['id']}) # assert that the user is not an admin one and cannot read the current # role info (note: view_roles is not in the required permissions) with pytest.raises( CLIReturnCodeError, match=r'Access denied\\nMissing one of the required permissions: view_roles', ): - Role.with_user(user_name, user_password).info({'id': role['id']}) + module_target_sat.cli.Role.with_user(user_name, user_password).info({'id': role['id']}) - Repository.synchronize({'id': repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) # Create a content view - content_view = make_content_view( + content_view = module_target_sat.cli_factory.make_content_view( {'organization-id': module_org.id, 'name': content_view_name} ) # assert that the user can read the content view info as per required # permissions - user_content_view = ContentView.with_user(user_name, user_password).info( - {'id': content_view['id']} - ) + user_content_view = module_target_sat.cli.ContentView.with_user( + user_name, user_password + ).info({'id': content_view['id']}) # assert that this is the same content view assert content_view['name'] == user_content_view['name'] # assert admin user is able to view the product - repos = Repository.list({'organization-id': module_org.id}) + repos = module_target_sat.cli.Repository.list({'organization-id': module_org.id}) assert len(repos) == 1 # assert that this is the same repo assert repos[0]['id'] == repo['id'] # assert that restricted user is not able to view the product - repos = Repository.with_user(user_name, user_password).list( + repos = module_target_sat.cli.Repository.with_user(user_name, user_password).list( {'organization-id': module_org.id} ) assert len(repos) == 0 # assert that the user cannot add the product repo to content view with pytest.raises(CLIReturnCodeError): - ContentView.with_user(user_name, user_password).add_repository( + module_target_sat.cli.ContentView.with_user(user_name, user_password).add_repository( { 'id': content_view['id'], 'organization-id': module_org.id, @@ -1808,7 +1816,7 @@ def test_negative_restricted_user_cv_add_repository(self, module_org, repo): } ) # assert that restricted user still not able to view the product - repos = Repository.with_user(user_name, user_password).list( + repos = module_target_sat.cli.Repository.with_user(user_name, user_password).list( {'organization-id': module_org.id} ) assert len(repos) == 0 @@ -1834,7 +1842,7 @@ def test_positive_upload_remove_srpm_content(self, repo, target_sat): remote_path=f"/tmp/{SRPM_TO_UPLOAD}", ) # Upload SRPM - result = Repository.upload_content( + result = target_sat.cli.Repository.upload_content( { 'name': repo['name'], 'organization': repo['organization'], @@ -1844,17 +1852,23 @@ def test_positive_upload_remove_srpm_content(self, repo, target_sat): } ) assert f"Successfully uploaded file '{SRPM_TO_UPLOAD}'" in result[0]['message'] - assert int(Repository.info({'id': repo['id']})['content-counts']['source-rpms']) == 1 + assert ( + int(target_sat.cli.Repository.info({'id': repo['id']})['content-counts']['source-rpms']) + == 1 + ) # Remove uploaded SRPM - Repository.remove_content( + target_sat.cli.Repository.remove_content( { 'id': repo['id'], - 'ids': [Srpm.list({'repository-id': repo['id']})[0]['id']], + 'ids': [target_sat.cli.Srpm.list({'repository-id': repo['id']})[0]['id']], 'content-type': 'srpm', } ) - assert int(Repository.info({'id': repo['id']})['content-counts']['source-rpms']) == 0 + assert ( + int(target_sat.cli.Repository.info({'id': repo['id']})['content-counts']['source-rpms']) + == 0 + ) @pytest.mark.upgrade @pytest.mark.tier2 @@ -1880,7 +1894,7 @@ def test_positive_srpm_list_end_to_end(self, repo, target_sat): remote_path=f"/tmp/{SRPM_TO_UPLOAD}", ) # Upload SRPM - Repository.upload_content( + target_sat.cli.Repository.upload_content( { 'name': repo['name'], 'organization': repo['organization'], @@ -1889,15 +1903,18 @@ def test_positive_srpm_list_end_to_end(self, repo, target_sat): 'content-type': 'srpm', } ) - assert len(Srpm.list()) > 0 - srpm_list = Srpm.list({'repository-id': repo['id']}) + assert len(target_sat.cli.Srpm.list()) > 0 + srpm_list = target_sat.cli.Srpm.list({'repository-id': repo['id']}) assert srpm_list[0]['filename'] == SRPM_TO_UPLOAD assert len(srpm_list) == 1 - assert Srpm.info({'id': srpm_list[0]['id']})[0]['filename'] == SRPM_TO_UPLOAD - assert int(Repository.info({'id': repo['id']})['content-counts']['source-rpms']) == 1 + assert target_sat.cli.Srpm.info({'id': srpm_list[0]['id']})[0]['filename'] == SRPM_TO_UPLOAD + assert ( + int(target_sat.cli.Repository.info({'id': repo['id']})['content-counts']['source-rpms']) + == 1 + ) assert ( len( - Srpm.list( + target_sat.cli.Srpm.list( { 'organization': repo['organization'], 'product-id': repo['product']['id'], @@ -1907,10 +1924,10 @@ def test_positive_srpm_list_end_to_end(self, repo, target_sat): ) > 0 ) - assert len(Srpm.list({'organization': repo['organization']})) > 0 + assert len(target_sat.cli.Srpm.list({'organization': repo['organization']})) > 0 assert ( len( - Srpm.list( + target_sat.cli.Srpm.list( { 'organization': repo['organization'], 'lifecycle-environment': 'Library', @@ -1921,7 +1938,7 @@ def test_positive_srpm_list_end_to_end(self, repo, target_sat): ) assert ( len( - Srpm.list( + target_sat.cli.Srpm.list( { 'content-view': 'Default Organization View', 'lifecycle-environment': 'Library', @@ -1933,16 +1950,16 @@ def test_positive_srpm_list_end_to_end(self, repo, target_sat): ) # Remove uploaded SRPM - Repository.remove_content( + target_sat.cli.Repository.remove_content( { 'id': repo['id'], - 'ids': [Srpm.list({'repository-id': repo['id']})[0]['id']], + 'ids': [target_sat.cli.Srpm.list({'repository-id': repo['id']})[0]['id']], 'content-type': 'srpm', } ) - assert int(Repository.info({'id': repo['id']})['content-counts']['source-rpms']) == len( - Srpm.list({'repository-id': repo['id']}) - ) + assert int( + target_sat.cli.Repository.info({'id': repo['id']})['content-counts']['source-rpms'] + ) == len(target_sat.cli.Srpm.list({'repository-id': repo['id']})) @pytest.mark.tier1 @pytest.mark.parametrize( @@ -1951,7 +1968,7 @@ def test_positive_srpm_list_end_to_end(self, repo, target_sat): indirect=True, ) def test_positive_create_get_update_delete_module_streams( - self, repo_options, module_org, module_product, repo + self, repo_options, module_org, module_product, repo, module_target_sat ): """Check module-stream get for each create, get, update, delete. @@ -1980,34 +1997,34 @@ def test_positive_create_get_update_delete_module_streams( :CaseImportance: Critical """ - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert ( repo['content-counts']['module-streams'] == '7' ), 'Module Streams not synced correctly' # adding repo with same yum url should not change count. - duplicate_repo = make_repository(repo_options) - Repository.synchronize({'id': duplicate_repo['id']}) + duplicate_repo = module_target_sat.cli_factory.make_repository(repo_options) + module_target_sat.cli.Repository.synchronize({'id': duplicate_repo['id']}) - module_streams = ModuleStream.list({'organization-id': module_org.id}) + module_streams = module_target_sat.cli.ModuleStream.list({'organization-id': module_org.id}) assert len(module_streams) == 7, 'Module Streams get worked correctly' - Repository.update( + module_target_sat.cli.Repository.update( { 'product-id': module_product.id, 'id': repo['id'], 'url': settings.repos.module_stream_1.url, } ) - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert ( repo['content-counts']['module-streams'] == '7' ), 'Module Streams not synced correctly' - Repository.delete({'id': repo['id']}) + module_target_sat.cli.Repository.delete({'id': repo['id']}) with pytest.raises(CLIReturnCodeError): - Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.info({'id': repo['id']}) @pytest.mark.tier1 @pytest.mark.parametrize( @@ -2019,7 +2036,9 @@ def test_positive_create_get_update_delete_module_streams( 'repo_options_2', **parametrized([{'content-type': 'yum', 'url': settings.repos.module_stream_1.url}]), ) - def test_module_stream_list_validation(self, module_org, repo, repo_options_2): + def test_module_stream_list_validation( + self, module_org, repo, repo_options_2, module_target_sat + ): """Check module-stream get with list on hammer. :id: 9842a0c3-8532-4b16-a00a-534fc3b0a776ff89f23e-cd00-4d20-84d3-add0ea24abf8 @@ -2038,17 +2057,17 @@ def test_module_stream_list_validation(self, module_org, repo, repo_options_2): :CaseAutomation: Automated """ - Repository.synchronize({'id': repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) - prod_2 = make_product({'organization-id': module_org.id}) + prod_2 = module_target_sat.cli_factory.make_product({'organization-id': module_org.id}) repo_options_2['organization-id'] = module_org.id repo_options_2['product-id'] = prod_2['id'] - repo_2 = make_repository(repo_options_2) + repo_2 = module_target_sat.cli_factory.make_repository(repo_options_2) - Repository.synchronize({'id': repo_2['id']}) - module_streams = ModuleStream.list() + module_target_sat.cli.Repository.synchronize({'id': repo_2['id']}) + module_streams = module_target_sat.cli.ModuleStream.list() assert len(module_streams) > 13, 'Module Streams list failed' - module_streams = ModuleStream.list({'product-id': prod_2['id']}) + module_streams = module_target_sat.cli.ModuleStream.list({'product-id': prod_2['id']}) assert len(module_streams) == 7, 'Module Streams list by product failed' @pytest.mark.tier1 @@ -2057,7 +2076,7 @@ def test_module_stream_list_validation(self, module_org, repo, repo_options_2): **parametrized([{'content-type': 'yum', 'url': settings.repos.module_stream_1.url}]), indirect=True, ) - def test_module_stream_info_validation(self, repo): + def test_module_stream_info_validation(self, repo, module_target_sat): """Check module-stream get with info on hammer. :id: ddbeb49e-d292-4dc4-8fb9-e9b768acc441a2c2e797-02b7-4b12-9f95-cffc93254198 @@ -2076,11 +2095,11 @@ def test_module_stream_info_validation(self, repo): :CaseAutomation: Automated """ - Repository.synchronize({'id': repo['id']}) - module_streams = ModuleStream.list( + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + module_streams = module_target_sat.cli.ModuleStream.list( {'repository-id': repo['id'], 'search': 'name="walrus" and stream="5.21"'} ) - actual_result = ModuleStream.info({'id': module_streams[0]['id']}) + actual_result = module_target_sat.cli.ModuleStream.info({'id': module_streams[0]['id']}) expected_result = { 'module-stream-name': 'walrus', 'stream': '5.21', @@ -2092,7 +2111,7 @@ def test_module_stream_info_validation(self, repo): @pytest.mark.tier1 @pytest.mark.skip_if_open('BZ:2002653') - def test_negative_update_red_hat_repo(self, module_manifest_org): + def test_negative_update_red_hat_repo(self, module_manifest_org, module_target_sat): """Updates to Red Hat products fail. :id: d3ac0ea2-faab-4df4-be66-733e1b7ae6b4 @@ -2109,26 +2128,34 @@ def test_negative_update_red_hat_repo(self, module_manifest_org): :expectedresults: hammer returns error code. The repository is not updated. """ - rh_repo_set_id = RepositorySet.list({'organization-id': module_manifest_org.id})[0]['id'] + rh_repo_set_id = module_target_sat.cli.RepositorySet.list( + {'organization-id': module_manifest_org.id} + )[0]['id'] - RepositorySet.enable( + module_target_sat.cli.RepositorySet.enable( { 'organization-id': module_manifest_org.id, 'basearch': "x86_64", 'id': rh_repo_set_id, } ) - repo_list = Repository.list({'organization-id': module_manifest_org.id}) + repo_list = module_target_sat.cli.Repository.list( + {'organization-id': module_manifest_org.id} + ) - rh_repo_id = Repository.list({'organization-id': module_manifest_org.id})[0]['id'] + rh_repo_id = module_target_sat.cli.Repository.list( + {'organization-id': module_manifest_org.id} + )[0]['id'] - Repository.update( + module_target_sat.cli.Repository.update( { 'id': rh_repo_id, 'url': f'{gen_url(scheme="https")}:{gen_integer(min_value=10, max_value=9999)}', } ) - repo_info = Repository.info({'organization-id': module_manifest_org.id, 'id': rh_repo_id}) + repo_info = module_target_sat.cli.Repository.info( + {'organization-id': module_manifest_org.id, 'id': rh_repo_id} + ) assert repo_info['url'] in [repo.get('url') for repo in repo_list] @pytest.mark.tier1 @@ -2164,7 +2191,7 @@ def test_positive_accessible_content_status( **parametrized([{'content_type': 'yum', 'url': CUSTOM_RPM_SHA}]), indirect=True, ) - def test_positive_sync_sha_repo(self, repo_options): + def test_positive_sync_sha_repo(self, repo_options, module_target_sat): """Sync a 'sha' repo successfully :id: 20579f52-a67b-4d3f-be07-41eec059a891 @@ -2177,10 +2204,10 @@ def test_positive_sync_sha_repo(self, repo_options): :SubComponent: Candlepin """ - sha_repo = make_repository(repo_options) - sha_repo = Repository.info({'id': sha_repo['id']}) - Repository.synchronize({'id': sha_repo['id']}) - sha_repo = Repository.info({'id': sha_repo['id']}) + sha_repo = module_target_sat.cli_factory.make_repository(repo_options) + sha_repo = module_target_sat.cli.Repository.info({'id': sha_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': sha_repo['id']}) + sha_repo = module_target_sat.cli.Repository.info({'id': sha_repo['id']}) assert sha_repo['sync']['status'] == 'Success' @pytest.mark.tier2 @@ -2189,7 +2216,7 @@ def test_positive_sync_sha_repo(self, repo_options): **parametrized([{'content_type': 'yum', 'url': CUSTOM_3RD_PARTY_REPO}]), indirect=True, ) - def test_positive_sync_third_party_repo(self, repo_options): + def test_positive_sync_third_party_repo(self, repo_options, module_target_sat): """Sync third party repo successfully :id: 45936ab8-46b7-4f07-8b71-d7c8a4a2d984 @@ -2202,10 +2229,10 @@ def test_positive_sync_third_party_repo(self, repo_options): :SubComponent: Pulp """ - repo = make_repository(repo_options) - repo = Repository.info({'id': repo['id']}) - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + repo = module_target_sat.cli_factory.make_repository(repo_options) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert repo['sync']['status'] == 'Success' @@ -2413,7 +2440,7 @@ def test_positive_sync(self, repo, module_org, module_product, target_sat): :expectedresults: srpms can be listed in repository """ - Repository.synchronize({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) result = target_sat.execute( f"ls /var/lib/pulp/published/yum/https/repos/{module_org.label}/Library" f"/custom/{module_product.label}/{repo['label']}/Packages/t/ | grep .src.rpm" @@ -2436,10 +2463,10 @@ def test_positive_sync_publish_cv(self, module_org, module_product, repo, target :expectedresults: srpms can be listed in content view """ - Repository.synchronize({'id': repo['id']}) - cv = make_content_view({'organization-id': module_org.id}) - ContentView.add_repository({'id': cv['id'], 'repository-id': repo['id']}) - ContentView.publish({'id': cv['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + cv = target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + target_sat.cli.ContentView.add_repository({'id': cv['id'], 'repository-id': repo['id']}) + target_sat.cli.ContentView.publish({'id': cv['id']}) result = target_sat.execute( f"ls /var/lib/pulp/published/yum/https/repos/{module_org.label}/content_views/" f"{cv['label']}/1.0/custom/{module_product.label}/{repo['label']}/Packages/t/" @@ -2465,14 +2492,16 @@ def test_positive_sync_publish_promote_cv(self, repo, module_org, module_product :expectedresults: srpms can be listed in content view in proper lifecycle environment """ - lce = make_lifecycle_environment({'organization-id': module_org.id}) - Repository.synchronize({'id': repo['id']}) - cv = make_content_view({'organization-id': module_org.id}) - ContentView.add_repository({'id': cv['id'], 'repository-id': repo['id']}) - ContentView.publish({'id': cv['id']}) - content_view = ContentView.info({'id': cv['id']}) + lce = target_sat.cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + cv = target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + target_sat.cli.ContentView.add_repository({'id': cv['id'], 'repository-id': repo['id']}) + target_sat.cli.ContentView.publish({'id': cv['id']}) + target_sat.cli.content_view = target_sat.cli.ContentView.info({'id': cv['id']}) cvv = content_view['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': lce['id']}) + target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': lce['id']} + ) result = target_sat.execute( f"ls /var/lib/pulp/published/yum/https/repos/{module_org.label}/{lce['label']}/" f"{cv['label']}/custom/{module_product.label}/{repo['label']}/Packages/t" @@ -2509,7 +2538,7 @@ class TestAnsibleCollectionRepository: ids=['ansible_galaxy', 'ansible_hub'], indirect=True, ) - def test_positive_sync_ansible_collection(self, repo, module_org, module_product): + def test_positive_sync_ansible_collection(self, repo, module_target_sat): """Sync ansible collection repository from Ansible Galaxy and Hub :id: 4b6a819b-8c3d-4a74-bd97-ee3f34cf5d92 @@ -2523,8 +2552,8 @@ def test_positive_sync_ansible_collection(self, repo, module_org, module_product :parametrized: yes """ - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + module_target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) + repo = module_target_sat.cli_factory.Repository.info({'id': repo['id']}) assert repo['sync']['status'] == 'Success' @pytest.mark.tier2 @@ -2543,7 +2572,7 @@ def test_positive_sync_ansible_collection(self, repo, module_org, module_product ids=['ansible_galaxy'], indirect=True, ) - def test_positive_export_ansible_collection(self, repo, module_org, module_product, target_sat): + def test_positive_export_ansible_collection(self, repo, module_org, target_sat): """Export ansible collection between organizations :id: 4858227e-1669-476d-8da3-4e6bfb6b7e2a @@ -2555,27 +2584,35 @@ def test_positive_export_ansible_collection(self, repo, module_org, module_produ :CaseImportance: High """ - import_org = make_org() - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + import_org = target_sat.cli_factory.make_org() + target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) + repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) assert repo['sync']['status'] == 'Success' # export - result = ContentExport.completeLibrary({'organization-id': module_org.id}) + result = target_sat.cli.ContentExport.completeLibrary({'organization-id': module_org.id}) target_sat.execute(f'cp -r /var/lib/pulp/exports/{module_org.name} /var/lib/pulp/imports/.') target_sat.execute('chown -R pulp:pulp /var/lib/pulp/imports') export_metadata = result['message'].split()[1] # import import_path = export_metadata.replace('/metadata.json', '').replace('exports', 'imports') - ContentImport.library({'organization-id': import_org['id'], 'path': import_path}) - cv = ContentView.info({'name': 'Import-Library', 'organization-label': import_org['label']}) + target_sat.cli.ContentImport.library( + {'organization-id': import_org['id'], 'path': import_path} + ) + cv = target_sat.cli.ContentView.info( + {'name': 'Import-Library', 'organization-label': import_org['label']} + ) assert cv['description'] == 'Content View used for importing into library' - prods = Product.list({'organization-id': import_org['id']}) - prod = Product.info({'id': prods[0]['id'], 'organization-id': import_org['id']}) + prods = target_sat.cli_factory.Product.list({'organization-id': import_org['id']}) + prod = target_sat.cli_factory.Product.info( + {'id': prods[0]['id'], 'organization-id': import_org['id']} + ) ac_content = [ cont for cont in prod['content'] if cont['content-type'] == 'ansible_collection' ] assert len(ac_content) > 0 - repo = Repository.info({'name': ac_content[0]['repo-name'], 'product-id': prod['id']}) + repo = target_sat.cli_factory.Repository.info( + {'name': ac_content[0]['repo-name'], 'product-id': prod['id']} + ) result = target_sat.execute(f'curl {repo["published-at"]}') assert "available_versions" in result.stdout @@ -2595,9 +2632,7 @@ def test_positive_export_ansible_collection(self, repo, module_org, module_produ ids=['ansible_galaxy'], indirect=True, ) - def test_positive_sync_ansible_collection_from_satellite( - self, repo, module_org, module_product, target_sat - ): + def test_positive_sync_ansible_collection_from_satellite(self, repo, target_sat): """Sync ansible collection from another organization :id: f7897a56-d014-4189-b4c7-df8f15aaf30a @@ -2609,16 +2644,16 @@ def test_positive_sync_ansible_collection_from_satellite( :CaseImportance: High """ - import_org = make_org() - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + import_org = target_sat.cli_factory.make_org() + target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) + repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) assert repo['sync']['status'] == 'Success' published_url = repo['published-at'] # sync from different org - prod_2 = make_product( + prod_2 = target_sat.cli_factory.make_product( {'organization-id': import_org['id'], 'description': 'Sync from Satellite'} ) - repo_2 = make_repository( + repo_2 = target_sat.cli_factory.make_repository( { 'organization-id': import_org['id'], 'product-id': prod_2['id'], @@ -2628,8 +2663,8 @@ def test_positive_sync_ansible_collection_from_satellite( [{ name: theforeman.operations, version: "0.1.0"}]}', } ) - Repository.synchronize({'id': repo_2['id']}) - repo_2_status = Repository.info({'id': repo_2['id']}) + target_sat.cli_factory.Repository.synchronize({'id': repo_2['id']}) + repo_2_status = target_sat.cli_factory.Repository.info({'id': repo_2['id']}) assert repo_2_status['sync']['status'] == 'Success' @@ -2641,7 +2676,7 @@ class TestMD5Repository: @pytest.mark.parametrize( 'repo_options', **parametrized([{'url': FAKE_YUM_MD5_REPO}]), indirect=True ) - def test_positive_sync_publish_promote_cv(self, repo, module_org, module_product, target_sat): + def test_positive_sync_publish_promote_cv(self, repo, module_org, target_sat): """Synchronize MD5 signed repository with add repository to content view, publish and promote content view to lifecycle environment @@ -2652,18 +2687,20 @@ def test_positive_sync_publish_promote_cv(self, repo, module_org, module_product :expectedresults: rpms can be listed in content view in proper lifecycle environment """ - lce = make_lifecycle_environment({'organization-id': module_org.id}) - Repository.synchronize({'id': repo['id']}) - synced_repo = Repository.info({'id': repo['id']}) + lce = target_sat.cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) + target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) + synced_repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) assert synced_repo['sync']['status'].lower() == 'success' assert synced_repo['content-counts']['packages'] == '35' - cv = make_content_view({'organization-id': module_org.id}) - ContentView.add_repository({'id': cv['id'], 'repository-id': repo['id']}) - ContentView.publish({'id': cv['id']}) - content_view = ContentView.info({'id': cv['id']}) + cv = target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + target_sat.cli.ContentView.add_repository({'id': cv['id'], 'repository-id': repo['id']}) + target_sat.cli.ContentView.publish({'id': cv['id']}) + content_view = target_sat.cli.ContentView.info({'id': cv['id']}) cvv = content_view['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': lce['id']}) - cv = ContentView.info({'id': cv['id']}) + target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': lce['id']} + ) + cv = target_sat.cli.ContentView.info({'id': cv['id']}) assert synced_repo['id'] in [repo['id'] for repo in cv['yum-repositories']] assert lce['id'] in [lc['id'] for lc in cv['lifecycle-environments']] @@ -2686,7 +2723,7 @@ def test_positive_sync(self, repo, module_org, module_product, target_sat): :expectedresults: drpms can be listed in repository """ - Repository.synchronize({'id': repo['id']}) + target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) result = target_sat.execute( f"ls /var/lib/pulp/published/yum/https/repos/{module_org.label}/Library" f"/custom/{module_product.label}/{repo['label']}/drpms/ | grep .drpm" @@ -2709,10 +2746,10 @@ def test_positive_sync_publish_cv(self, repo, module_org, module_product, target :expectedresults: drpms can be listed in content view """ - Repository.synchronize({'id': repo['id']}) - cv = make_content_view({'organization-id': module_org.id}) - ContentView.add_repository({'id': cv['id'], 'repository-id': repo['id']}) - ContentView.publish({'id': cv['id']}) + target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) + cv = target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + target_sat.cli.ContentView.add_repository({'id': cv['id'], 'repository-id': repo['id']}) + target_sat.cli.ContentView.publish({'id': cv['id']}) result = target_sat.execute( f"ls /var/lib/pulp/published/yum/https/repos/{module_org.label}/content_views/" f"{cv['label']}/1.0/custom/{module_product.label}/{repo['label']}/drpms/ | grep .drpm" @@ -2737,14 +2774,16 @@ def test_positive_sync_publish_promote_cv(self, repo, module_org, module_product :expectedresults: drpms can be listed in content view in proper lifecycle environment """ - lce = make_lifecycle_environment({'organization-id': module_org.id}) - Repository.synchronize({'id': repo['id']}) - cv = make_content_view({'organization-id': module_org.id}) - ContentView.add_repository({'id': cv['id'], 'repository-id': repo['id']}) - ContentView.publish({'id': cv['id']}) - content_view = ContentView.info({'id': cv['id']}) + lce = target_sat.cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) + target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) + cv = target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) + target_sat.cli.ContentView.add_repository({'id': cv['id'], 'repository-id': repo['id']}) + target_sat.cli.ContentView.publish({'id': cv['id']}) + content_view = target_sat.cli.ContentView.info({'id': cv['id']}) cvv = content_view['versions'][0] - ContentView.version_promote({'id': cvv['id'], 'to-lifecycle-environment-id': lce['id']}) + target_sat.cli.ContentView.version_promote( + {'id': cvv['id'], 'to-lifecycle-environment-id': lce['id']} + ) result = target_sat.execute( f"ls /var/lib/pulp/published/yum/https/repos/{module_org.label}/{lce['label']}" f"/{cv['label']}/custom/{module_product.label}/{repo['label']}/drpms/ | grep .drpm" @@ -2972,7 +3011,7 @@ def test_positive_upload_file_to_file_repo(self, repo_options, repo, target_sat) local_path=DataFile.RPM_TO_UPLOAD, remote_path=f"/tmp/{RPM_TO_UPLOAD}", ) - result = Repository.upload_content( + result = target_sat.cli_factory.Repository.upload_content( { 'name': repo['name'], 'organization': repo['organization'], @@ -2981,7 +3020,7 @@ def test_positive_upload_file_to_file_repo(self, repo_options, repo, target_sat) } ) assert f"Successfully uploaded file '{RPM_TO_UPLOAD}'" in result[0]['message'] - repo = Repository.info({'id': repo['id']}) + repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) assert repo['content-counts']['files'] == '1' filesearch = entities.File().search( query={"search": f"name={RPM_TO_UPLOAD} and repository={repo['name']}"} @@ -3037,7 +3076,7 @@ def test_positive_remove_file(self, repo, target_sat): local_path=DataFile.RPM_TO_UPLOAD, remote_path=f"/tmp/{RPM_TO_UPLOAD}", ) - result = Repository.upload_content( + result = target_sat.cli_factory.Repository.upload_content( { 'name': repo['name'], 'organization': repo['organization'], @@ -3046,11 +3085,13 @@ def test_positive_remove_file(self, repo, target_sat): } ) assert f"Successfully uploaded file '{RPM_TO_UPLOAD}'" in result[0]['message'] - repo = Repository.info({'id': repo['id']}) + repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) assert int(repo['content-counts']['files']) > 0 - files = File.list({'repository-id': repo['id']}) - Repository.remove_content({'id': repo['id'], 'ids': [file_['id'] for file_ in files]}) - repo = Repository.info({'id': repo['id']}) + files = target_sat.cli.File.list({'repository-id': repo['id']}) + target_sat.cli_factory.Repository.remove_content( + {'id': repo['id'], 'ids': [file_['id'] for file_ in files]} + ) + repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) assert repo['content-counts']['files'] == '0' @pytest.mark.tier2 @@ -3068,7 +3109,7 @@ def test_positive_remove_file(self, repo, target_sat): ), indirect=True, ) - def test_positive_remote_directory_sync(self, repo): + def test_positive_remote_directory_sync(self, repo, module_target_sat): """Check an entire remote directory can be synced to File Repository through http @@ -3088,8 +3129,8 @@ def test_positive_remote_directory_sync(self, repo): :expectedresults: entire directory is synced over http """ - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + module_target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) + repo = module_target_sat.cli_factory.Repository.info({'id': repo['id']}) assert repo['sync']['status'] == 'Success' assert repo['content-counts']['files'] == '2' @@ -3126,8 +3167,8 @@ def test_positive_file_repo_local_directory_sync(self, repo, target_sat): f'wget -P {CUSTOM_LOCAL_FOLDER} -r -np -nH --cut-dirs=5 -R "index.html*" ' f'{CUSTOM_FILE_REPO}' ) - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) + repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) assert int(repo['content-counts']['files']) > 1 @pytest.mark.tier2 @@ -3166,8 +3207,8 @@ def test_positive_symlinks_sync(self, repo, target_sat): ) target_sat.execute(f'ln -s {CUSTOM_LOCAL_FOLDER} /{gen_string("alpha")}') - Repository.synchronize({'id': repo['id']}) - repo = Repository.info({'id': repo['id']}) + target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) + repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) assert int(repo['content-counts']['files']) > 1 @pytest.mark.tier2 @@ -3200,7 +3241,7 @@ def test_file_repo_contains_only_newer_file(self, repo_options, repo, target_sat """ text_file_name = f'test-{gen_string("alpha", 5)}.txt'.lower() target_sat.execute(f'echo "First File" > /tmp/{text_file_name}') - result = Repository.upload_content( + result = target_sat.cli_factory.Repository.upload_content( { 'name': repo['name'], 'organization': repo['organization'], @@ -3209,7 +3250,7 @@ def test_file_repo_contains_only_newer_file(self, repo_options, repo, target_sat } ) assert f"Successfully uploaded file '{text_file_name}'" in result[0]['message'] - repo = Repository.info({'id': repo['id']}) + repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) # Assert there is only one file assert repo['content-counts']['files'] == '1' filesearch = entities.File().search( @@ -3218,7 +3259,7 @@ def test_file_repo_contains_only_newer_file(self, repo_options, repo, target_sat assert text_file_name == filesearch[0].name # Create new version of the file by changing the text target_sat.execute(f'echo "Second File" > /tmp/{text_file_name}') - result = Repository.upload_content( + result = target_sat.cli_factory.Repository.upload_content( { 'name': repo['name'], 'organization': repo['organization'], @@ -3227,7 +3268,7 @@ def test_file_repo_contains_only_newer_file(self, repo_options, repo, target_sat } ) assert f"Successfully uploaded file '{text_file_name}'" in result[0]['message'] - repo = Repository.info({'id': repo['id']}) + repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) # Assert there is still only one file assert repo['content-counts']['files'] == '1' filesearch = entities.File().search( diff --git a/tests/foreman/cli/test_repository_set.py b/tests/foreman/cli/test_repository_set.py index 0be98f8e32d..317f19e33d5 100644 --- a/tests/foreman/cli/test_repository_set.py +++ b/tests/foreman/cli/test_repository_set.py @@ -18,15 +18,13 @@ """ import pytest -from robottelo.cli.product import Product -from robottelo.cli.repository_set import RepositorySet from robottelo.constants import PRDS, REPOSET pytestmark = [pytest.mark.run_in_one_thread, pytest.mark.tier1] @pytest.fixture -def params(function_entitlement_manifest_org): +def params(function_entitlement_manifest_org, target_sat): PRODUCT_NAME = PRDS['rhel'] REPOSET_NAME = REPOSET['rhva6'] ARCH = 'x86_64' @@ -34,8 +32,10 @@ def params(function_entitlement_manifest_org): RELEASE = '6Server' manifest_org = function_entitlement_manifest_org - product_id = Product.info({'name': PRODUCT_NAME, 'organization-id': manifest_org.id})['id'] - reposet_id = RepositorySet.info( + product_id = target_sat.cli.Product.info( + {'name': PRODUCT_NAME, 'organization-id': manifest_org.id} + )['id'] + reposet_id = target_sat.cli.RepositorySet.info( {'name': REPOSET_NAME, 'organization-id': manifest_org.id, 'product-id': product_id} )['id'] @@ -114,7 +114,7 @@ def match_repos(repos, match_params): @pytest.mark.upgrade -def test_positive_list_available_repositories(params): +def test_positive_list_available_repositories(params, target_sat): """List available repositories for repository-set :id: 987d6b08-acb0-4264-a459-9cef0d2c6f3f @@ -125,39 +125,39 @@ def test_positive_list_available_repositories(params): :CaseImportance: Critical """ # No repos should be enabled by default - result = RepositorySet.available_repositories(params['avail']['id']) + result = target_sat.cli.RepositorySet.available_repositories(params['avail']['id']) assert len(match_repos(result, params['match']['enabled'])) == 0 # Enable repo from Repository Set - RepositorySet.enable(params['enable']['id']) + target_sat.cli.RepositorySet.enable(params['enable']['id']) # Only 1 repo should be enabled, and it should match the arch and releasever - result = RepositorySet.available_repositories(params['avail']['name']) + result = target_sat.cli.RepositorySet.available_repositories(params['avail']['name']) assert len(match_repos(result, params['match']['enabled'])) == 1 # Enable one more repo - RepositorySet.enable(params['enable']['arch_2']) + target_sat.cli.RepositorySet.enable(params['enable']['arch_2']) # 2 repos should be enabled - result = RepositorySet.available_repositories(params['avail']['label']) + result = target_sat.cli.RepositorySet.available_repositories(params['avail']['label']) assert len(match_repos(result, params['match']['enabled'])) == 2 # Disable one repo - RepositorySet.disable(params['enable']['id']) + target_sat.cli.RepositorySet.disable(params['enable']['id']) # There should remain only 1 enabled repo - result = RepositorySet.available_repositories(params['avail']['id']) + result = target_sat.cli.RepositorySet.available_repositories(params['avail']['id']) assert len(match_repos(result, params['match']['enabled'])) == 1 # Disable the last enabled repo - RepositorySet.disable(params['enable']['arch_2']) + target_sat.cli.RepositorySet.disable(params['enable']['arch_2']) # There should be no enabled repos - result = RepositorySet.available_repositories(params['avail']['id']) + result = target_sat.cli.RepositorySet.available_repositories(params['avail']['id']) assert len(match_repos(result, params['match']['enabled'])) == 0 -def test_positive_enable_by_name(params): +def test_positive_enable_by_name(params, target_sat): """Enable repo from reposet by names of reposet, org and product :id: a78537bd-b88d-4f00-8901-e7944e5de729 @@ -166,12 +166,12 @@ def test_positive_enable_by_name(params): :CaseImportance: Critical """ - RepositorySet.enable(params['enable']['name']) - result = RepositorySet.available_repositories(params['avail']['name']) + target_sat.cli.RepositorySet.enable(params['enable']['name']) + result = target_sat.cli.RepositorySet.available_repositories(params['avail']['name']) assert len(match_repos(result, params['match']['enabled_arch_rel'])) == 1 -def test_positive_enable_by_label(params): +def test_positive_enable_by_label(params, target_sat): """Enable repo from reposet by org label, reposet and product names @@ -181,12 +181,12 @@ def test_positive_enable_by_label(params): :CaseImportance: Critical """ - RepositorySet.enable(params['enable']['label']) - result = RepositorySet.available_repositories(params['avail']['label']) + target_sat.cli.RepositorySet.enable(params['enable']['label']) + result = target_sat.cli.RepositorySet.available_repositories(params['avail']['label']) assert len(match_repos(result, params['match']['enabled_arch_rel'])) == 1 -def test_positive_enable_by_id(params): +def test_positive_enable_by_id(params, target_sat): """Enable repo from reposet by IDs of reposet, org and product :id: f7c88534-1d45-45d9-9b87-c50c4e268e8d @@ -195,12 +195,12 @@ def test_positive_enable_by_id(params): :CaseImportance: Critical """ - RepositorySet.enable(params['enable']['ids']) - result = RepositorySet.available_repositories(params['avail']['ids']) + target_sat.cli.RepositorySet.enable(params['enable']['ids']) + result = target_sat.cli.RepositorySet.available_repositories(params['avail']['ids']) assert len(match_repos(result, params['match']['enabled_arch_rel'])) == 1 -def test_positive_disable_by_name(params): +def test_positive_disable_by_name(params, target_sat): """Disable repo from reposet by names of reposet, org and product @@ -210,13 +210,13 @@ def test_positive_disable_by_name(params): :CaseImportance: Critical """ - RepositorySet.enable(params['enable']['name']) - RepositorySet.disable(params['enable']['name']) - result = RepositorySet.available_repositories(params['avail']['name']) + target_sat.cli.RepositorySet.enable(params['enable']['name']) + target_sat.cli.RepositorySet.disable(params['enable']['name']) + result = target_sat.cli.RepositorySet.available_repositories(params['avail']['name']) assert len(match_repos(result, params['match']['enabled'])) == 0 -def test_positive_disable_by_label(params): +def test_positive_disable_by_label(params, target_sat): """Disable repo from reposet by org label, reposet and product names @@ -226,13 +226,13 @@ def test_positive_disable_by_label(params): :CaseImportance: Critical """ - RepositorySet.enable(params['enable']['label']) - RepositorySet.disable(params['enable']['label']) - result = RepositorySet.available_repositories(params['avail']['label']) + target_sat.cli.RepositorySet.enable(params['enable']['label']) + target_sat.cli.RepositorySet.disable(params['enable']['label']) + result = target_sat.cli.RepositorySet.available_repositories(params['avail']['label']) assert len(match_repos(result, params['match']['enabled'])) == 0 -def test_positive_disable_by_id(params): +def test_positive_disable_by_id(params, target_sat): """Disable repo from reposet by IDs of reposet, org and product :id: 0d6102ba-3fb9-4eb8-972e-d537e252a8e6 @@ -241,7 +241,7 @@ def test_positive_disable_by_id(params): :CaseImportance: Critical """ - RepositorySet.enable(params['enable']['ids']) - RepositorySet.disable(params['enable']['ids']) - result = RepositorySet.available_repositories(params['avail']['ids']) + target_sat.cli.RepositorySet.enable(params['enable']['ids']) + target_sat.cli.RepositorySet.disable(params['enable']['ids']) + result = target_sat.cli.RepositorySet.available_repositories(params['avail']['ids']) assert len(match_repos(result, params['match']['enabled'])) == 0 diff --git a/tests/foreman/cli/test_role.py b/tests/foreman/cli/test_role.py index 21900c30523..034d395b609 100644 --- a/tests/foreman/cli/test_role.py +++ b/tests/foreman/cli/test_role.py @@ -23,19 +23,8 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.base import CLIDataBaseError, CLIReturnCodeError -from robottelo.cli.factory import ( - make_filter, - make_location, - make_org, - make_role, - make_user, -) -from robottelo.cli.filter import Filter -from robottelo.cli.role import Role -from robottelo.cli.settings import Settings -from robottelo.cli.user import User from robottelo.constants import PERMISSIONS, ROLES +from robottelo.exceptions import CLIDataBaseError, CLIReturnCodeError from robottelo.utils.datafactory import generate_strings_list, parametrized @@ -49,7 +38,7 @@ class TestRole: list(zip(generate_strings_list(length=10), generate_strings_list(length=10))) ), ) - def test_positive_crud_with_name(self, name, new_name): + def test_positive_crud_with_name(self, name, new_name, module_target_sat): """Create new role with provided name, update name and delete role by ID :id: f77b8e84-e964-4007-b12b-142949134d8b @@ -63,18 +52,18 @@ def test_positive_crud_with_name(self, name, new_name): :CaseImportance: Critical """ - role = make_role({'name': name}) + role = module_target_sat.cli_factory.make_role({'name': name}) assert role['name'] == name - Role.update({'id': role['id'], 'new-name': new_name}) - role = Role.info({'id': role['id']}) + module_target_sat.cli.Role.update({'id': role['id'], 'new-name': new_name}) + role = module_target_sat.cli.Role.info({'id': role['id']}) assert role['name'] == new_name - Role.delete({'id': role['id']}) + module_target_sat.cli.Role.delete({'id': role['id']}) with pytest.raises(CLIReturnCodeError): - Role.info({'id': role['id']}) + module_target_sat.cli.Role.info({'id': role['id']}) @pytest.mark.tier1 @pytest.mark.upgrade - def test_positive_create_with_permission(self): + def test_positive_create_with_permission(self, module_target_sat): """Create new role with a set of permission :id: 7cb2b2e2-ad4d-41e9-b6b2-c0366eb09b9a @@ -83,18 +72,24 @@ def test_positive_create_with_permission(self): :CaseImportance: Critical """ - role = make_role() + role = module_target_sat.cli_factory.make_role() # Pick permissions by its resource type permissions = [ permission['name'] - for permission in Filter.available_permissions({"search": "resource_type=Organization"}) + for permission in module_target_sat.cli.Filter.available_permissions( + {"search": "resource_type=Organization"} + ) ] # Assign filter to created role - make_filter({'role-id': role['id'], 'permissions': permissions}) - assert set(Role.filters({'id': role['id']})[0]['permissions']) == set(permissions) + module_target_sat.cli_factory.make_filter( + {'role-id': role['id'], 'permissions': permissions} + ) + assert set(module_target_sat.cli.Role.filters({'id': role['id']})[0]['permissions']) == set( + permissions + ) @pytest.mark.tier1 - def test_positive_list_filters_by_id(self): + def test_positive_list_filters_by_id(self, module_target_sat): """Create new role with a filter and list it by role id :id: 6979ad8d-629b-481e-9d3a-8f3b3bca53f9 @@ -103,19 +98,23 @@ def test_positive_list_filters_by_id(self): :CaseImportance: Critical """ - role = make_role() + role = module_target_sat.cli_factory.make_role() # Pick permissions by its resource type permissions = [ permission['name'] - for permission in Filter.available_permissions({"search": "resource_type=Organization"}) + for permission in module_target_sat.cli.Filter.available_permissions( + {"search": "resource_type=Organization"} + ) ] # Assign filter to created role - filter_ = make_filter({'role-id': role['id'], 'permissions': permissions}) + filter_ = module_target_sat.cli_factory.make_filter( + {'role-id': role['id'], 'permissions': permissions} + ) assert role['name'] == filter_['role'] - assert Role.filters({'id': role['id']})[0]['id'] == filter_['id'] + assert module_target_sat.cli.Role.filters({'id': role['id']})[0]['id'] == filter_['id'] @pytest.mark.tier1 - def test_positive_list_filters_by_name(self): + def test_positive_list_filters_by_name(self, module_target_sat): """Create new role with a filter and list it by role name :id: bbcb3982-f484-4dde-a3ea-7145fd28ab1f @@ -124,19 +123,23 @@ def test_positive_list_filters_by_name(self): :CaseImportance: Critical """ - role = make_role() + role = module_target_sat.cli_factory.make_role() # Pick permissions by its resource type permissions = [ permission['name'] - for permission in Filter.available_permissions({"search": "resource_type=Organization"}) + for permission in module_target_sat.cli.Filter.available_permissions( + {"search": "resource_type=Organization"} + ) ] # Assign filter to created role - filter_ = make_filter({'role': role['name'], 'permissions': permissions}) + filter_ = module_target_sat.cli_factory.make_filter( + {'role': role['name'], 'permissions': permissions} + ) assert role['name'] == filter_['role'] - assert Role.filters({'name': role['name']})[0]['id'] == filter_['id'] + assert module_target_sat.cli.Role.filters({'name': role['name']})[0]['id'] == filter_['id'] @pytest.mark.tier1 - def test_negative_list_filters_without_parameters(self): + def test_negative_list_filters_without_parameters(self, module_target_sat): """Try to list filter without specifying role id or name :id: 56cafbe0-d1cb-413e-8eac-0e01a3590fd2 @@ -148,28 +151,28 @@ def test_negative_list_filters_without_parameters(self): :BZ: 1296782 """ with pytest.raises(CLIReturnCodeError, CLIDataBaseError) as err: - Role.filters() + module_target_sat.cli.Role.filters() if isinstance(err.type, CLIDataBaseError): pytest.fail(err) assert re.search('At least one of options .* is required', err.value.msg) @pytest.fixture - def make_role_with_permissions(self): + def make_role_with_permissions(self, target_sat): """Create new role with a filter""" - role = make_role() + role = target_sat.cli_factory.make_role() res_types = iter(PERMISSIONS.keys()) permissions = [] # Collect more than 20 different permissions while len(permissions) <= 20: permissions += [ permission['name'] - for permission in Filter.available_permissions( + for permission in target_sat.cli.Filter.available_permissions( {"search": f"resource_type={next(res_types)}"} ) ] # Create a filter for each permission for perm in permissions: - make_filter({'role': role['name'], 'permissions': perm}) + target_sat.cli_factory.make_filter({'role': role['name'], 'permissions': perm}) return { 'role': role, 'permissions': permissions, @@ -178,7 +181,9 @@ def make_role_with_permissions(self): @pytest.mark.tier1 @pytest.mark.upgrade @pytest.mark.parametrize('per_page', [1, 5, 20]) - def test_positive_list_filters_with_pagination(self, make_role_with_permissions, per_page): + def test_positive_list_filters_with_pagination( + self, make_role_with_permissions, per_page, module_target_sat + ): """Make sure filters list can be displayed with different items per page value @@ -196,14 +201,14 @@ def test_positive_list_filters_with_pagination(self, make_role_with_permissions, """ # Verify the first page contains exactly the same items count # as `per-page` value - filters = Role.filters( + filters = module_target_sat.cli.Role.filters( {'name': make_role_with_permissions['role']['name'], 'per-page': per_page} ) assert len(filters) == per_page # Verify pagination and total amount of pages by checking the # items count on the last page last_page = ceil(len(make_role_with_permissions['permissions']) / per_page) - filters = Role.filters( + filters = module_target_sat.cli.Role.filters( { 'name': make_role_with_permissions['role']['name'], 'page': last_page, @@ -216,7 +221,7 @@ def test_positive_list_filters_with_pagination(self, make_role_with_permissions, @pytest.mark.tier1 @pytest.mark.upgrade - def test_positive_delete_cloned_builtin(self): + def test_positive_delete_cloned_builtin(self, module_target_sat): """Clone a builtin role and attempt to delete it :id: 1fd9c636-596a-4cb2-b100-de19238042cc @@ -228,27 +233,29 @@ def test_positive_delete_cloned_builtin(self): :CaseImportance: Critical """ - role_list = Role.list({'search': f'name=\\"{choice(ROLES)}\\"'}) + role_list = module_target_sat.cli.Role.list({'search': f'name=\\"{choice(ROLES)}\\"'}) assert len(role_list) == 1 - cloned_role = Role.clone({'id': role_list[0]['id'], 'new-name': gen_string('alphanumeric')}) - Role.delete({'id': cloned_role['id']}) + cloned_role = module_target_sat.cli.Role.clone( + {'id': role_list[0]['id'], 'new-name': gen_string('alphanumeric')} + ) + module_target_sat.cli.Role.delete({'id': cloned_role['id']}) with pytest.raises(CLIReturnCodeError): - Role.info({'id': cloned_role['id']}) + module_target_sat.cli.Role.info({'id': cloned_role['id']}) class TestSystemAdmin: """Test class for System Admin role end to end CLI""" @pytest.fixture(scope='class', autouse=True) - def tearDown(self): + def tearDown(self, class_target_sat): """Will reset the changed value of settings""" yield - Settings.set({'name': "outofsync_interval", 'value': "30"}) + class_target_sat.cli.Settings.set({'name': "outofsync_interval", 'value': "30"}) @pytest.mark.upgrade @pytest.mark.tier3 @pytest.mark.e2e - def test_system_admin_role_end_to_end(self): + def test_system_admin_role_end_to_end(self, target_sat): """Test System admin role with a end to end workflow :id: da6b3549-d1cf-44fc-869f-08d15d407fa2 @@ -276,27 +283,27 @@ def test_system_admin_role_end_to_end(self): :CaseLevel: System """ - org = make_org() - location = make_location() + org = target_sat.cli_factory.make_org() + location = target_sat.cli_factory.make_location() common_pass = gen_string('alpha') - role = Role.info({'name': 'System admin'}) - system_admin_1 = make_user( + role = target_sat.cli.Role.info({'name': 'System admin'}) + system_admin_1 = target_sat.cli_factory.user( { 'password': common_pass, 'organization-ids': org['id'], 'location-ids': location['id'], } ) - User.add_role({'id': system_admin_1['id'], 'role-id': role['id']}) - Settings.with_user(username=system_admin_1['login'], password=common_pass).set( - {'name': "outofsync_interval", 'value': "32"} - ) - sync_time = Settings.list({'search': 'name=outofsync_interval'})[0] + target_sat.cli.User.add_role({'id': system_admin_1['id'], 'role-id': role['id']}) + target_sat.cli.Settings.with_user( + username=system_admin_1['login'], password=common_pass + ).set({'name': "outofsync_interval", 'value': "32"}) + sync_time = target_sat.cli.Settings.list({'search': 'name=outofsync_interval'})[0] # Asserts if the setting was updated successfully assert '32' == sync_time['value'] # Create another System Admin user using the first one - system_admin = User.with_user( + system_admin = target_sat.cli.User.with_user( username=system_admin_1['login'], password=common_pass ).create( { @@ -312,7 +319,9 @@ def test_system_admin_role_end_to_end(self): } ) # Create the Org Admin user - org_role = Role.with_user(username=system_admin['login'], password=common_pass).clone( + org_role = target_sat.cli.Role.with_user( + username=system_admin['login'], password=common_pass + ).clone( { 'name': 'Organization admin', 'new-name': gen_string('alpha'), @@ -320,7 +329,9 @@ def test_system_admin_role_end_to_end(self): 'location-ids': location['id'], } ) - org_admin = User.with_user(username=system_admin['login'], password=common_pass).create( + org_admin = target_sat.cli.User.with_user( + username=system_admin['login'], password=common_pass + ).create( { 'auth-source-id': 1, 'firstname': gen_string('alpha'), @@ -335,20 +346,20 @@ def test_system_admin_role_end_to_end(self): ) # Assert if the cloning was successful assert org_role['id'] is not None - org_role_filters = Role.filters({'id': org_role['id']}) + org_role_filters = target_sat.cli.Role.filters({'id': org_role['id']}) search_filter = None for arch_filter in org_role_filters: if arch_filter['resource-type'] == 'Architecture': search_filter = arch_filter break - Filter.with_user(username=system_admin['login'], password=common_pass).update( - {'role-id': org_role['id'], 'id': arch_filter['id'], 'search': 'name=x86_64'} - ) + target_sat.cli.Filter.with_user( + username=system_admin['login'], password=common_pass + ).update({'role-id': org_role['id'], 'id': arch_filter['id'], 'search': 'name=x86_64'}) # Asserts if the filter is updated - assert 'name=x86_64' in Filter.info({'id': search_filter['id']}).values() - org_admin = User.with_user(username=system_admin['login'], password=common_pass).info( - {'id': org_admin['id']} - ) + assert 'name=x86_64' in target_sat.cli.Filter.info({'id': search_filter['id']}).values() + org_admin = target_sat.cli.User.with_user( + username=system_admin['login'], password=common_pass + ).info({'id': org_admin['id']}) # Asserts Created Org Admin assert org_role['name'] in org_admin['roles'] assert org['name'] in org_admin['organizations'] diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index 2154535d618..8dd3fae299c 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -24,20 +24,6 @@ import pytest from wait_for import wait_for -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.content_export import ContentExport -from robottelo.cli.content_import import ContentImport -from robottelo.cli.contentview import ContentView -from robottelo.cli.factory import ( - make_content_view, - make_org, - make_product, - make_repository, -) -from robottelo.cli.package import Package -from robottelo.cli.product import Product -from robottelo.cli.repository import Repository -from robottelo.cli.settings import Settings from robottelo.config import settings from robottelo.constants import ( CONTAINER_REGISTRY_HUB, @@ -52,19 +38,34 @@ DataFile, ) from robottelo.constants.repos import ANSIBLE_GALAXY +from robottelo.exceptions import CLIReturnCodeError @pytest.fixture(scope='class') -def config_export_import_settings(): +def config_export_import_settings(module_target_sat): """Check settings and set download policy for export. Reset to original state after import""" - download_policy_value = Settings.info({'name': 'default_download_policy'})['value'] - rh_download_policy_value = Settings.info({'name': 'default_redhat_download_policy'})['value'] - subs_conn_enabled_value = Settings.info({'name': 'subscription_connection_enabled'})['value'] - Settings.set({'name': 'default_redhat_download_policy', 'value': 'immediate'}) + download_policy_value = module_target_sat.cli.Settings.info( + {'name': 'default_download_policy'} + )['value'] + rh_download_policy_value = module_target_sat.cli.Settings.info( + {'name': 'default_redhat_download_policy'} + )['value'] + subs_conn_enabled_value = module_target_sat.cli.Settings.info( + {'name': 'subscription_connection_enabled'} + )['value'] + module_target_sat.cli.Settings.set( + {'name': 'default_redhat_download_policy', 'value': 'immediate'} + ) yield - Settings.set({'name': 'default_download_policy', 'value': download_policy_value}) - Settings.set({'name': 'default_redhat_download_policy', 'value': rh_download_policy_value}) - Settings.set({'name': 'subscription_connection_enabled', 'value': subs_conn_enabled_value}) + module_target_sat.cli.Settings.set( + {'name': 'default_download_policy', 'value': download_policy_value} + ) + module_target_sat.cli.Settings.set( + {'name': 'default_redhat_download_policy', 'value': rh_download_policy_value} + ) + module_target_sat.cli.Settings.set( + {'name': 'subscription_connection_enabled', 'value': subs_conn_enabled_value} + ) @pytest.fixture @@ -100,6 +101,23 @@ def function_import_org_with_manifest(target_sat, function_import_org): return function_import_org +@pytest.fixture(scope='class') +def docker_repo(module_target_sat, module_org): + product = module_target_sat.cli_factory.make_product({'organization-id': module_org.id}) + repo = module_target_sat.cli_factory.make_repository( + { + 'organization-id': module_org.id, + 'product-id': product['id'], + 'content-type': REPO_TYPE['docker'], + 'download-policy': 'immediate', + 'url': 'https://quay.io', + 'docker-upstream-name': 'quay/busybox', + } + ) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + return repo + + @pytest.fixture(scope='module') def module_synced_custom_repo(module_target_sat, module_org, module_product): repo = module_target_sat.cli_factory.make_repository( @@ -355,19 +373,23 @@ def test_positive_export_complete_library_rh_repo( """ # Create cv and publish cv_name = gen_string('alpha') - cv = make_content_view({'name': cv_name, 'organization-id': function_sca_manifest_org.id}) - ContentView.add_repository( + cv = target_sat.cli_factory.make_content_view( + {'name': cv_name, 'organization-id': function_sca_manifest_org.id} + ) + target_sat.cli.ContentView.add_repository( { 'id': cv['id'], 'organization-id': function_sca_manifest_org.id, 'repository-id': function_synced_rhel_repo['id'], } ) - ContentView.publish({'id': cv['id']}) + target_sat.cli.ContentView.publish({'id': cv['id']}) # Verify export directory is empty assert target_sat.validate_pulp_filepath(function_sca_manifest_org, PULP_EXPORT_DIR) == '' # Export content view - ContentExport.completeLibrary({'organization-id': function_sca_manifest_org.id}) + target_sat.cli.ContentExport.completeLibrary( + {'organization-id': function_sca_manifest_org.id} + ) # Verify export directory is not empty assert target_sat.validate_pulp_filepath(function_sca_manifest_org, PULP_EXPORT_DIR) != '' @@ -434,7 +456,9 @@ def test_positive_export_version_docker( """ # Create CV and publish cv_name = gen_string('alpha') - cv = make_content_view({'name': cv_name, 'organization-id': function_org.id}) + cv = target_sat.cli_factory.make_content_view( + {'name': cv_name, 'organization-id': function_org.id} + ) target_sat.cli.ContentView.add_repository( { 'id': cv['id'], @@ -465,12 +489,14 @@ def test_positive_export_version_docker( @pytest.fixture(scope='class') -def class_export_entities(module_org): +def class_export_entities(module_org, module_target_sat): """Setup custom repos for export""" exporting_prod_name = gen_string('alpha') - product = make_product({'organization-id': module_org.id, 'name': exporting_prod_name}) + product = module_target_sat.cli_factory.make_product( + {'organization-id': module_org.id, 'name': exporting_prod_name} + ) exporting_repo_name = gen_string('alpha') - exporting_repo = make_repository( + exporting_repo = module_target_sat.cli_factory.make_repository( { 'name': exporting_repo_name, 'mirror-on-sync': 'no', @@ -478,9 +504,11 @@ def class_export_entities(module_org): 'product-id': product['id'], } ) - Repository.synchronize({'id': exporting_repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': exporting_repo['id']}) exporting_cv_name = gen_string('alpha') - exporting_cv, exporting_cvv_id = _create_cv(exporting_cv_name, exporting_repo, module_org) + exporting_cv, exporting_cvv_id = _create_cv( + exporting_cv_name, exporting_repo, module_org, module_target_sat + ) return { 'exporting_org': module_org, 'exporting_prod_name': exporting_prod_name, @@ -492,7 +520,7 @@ def class_export_entities(module_org): } -def _create_cv(cv_name, repo, module_org, publish=True): +def _create_cv(cv_name, repo, module_org, sat, publish=True): """Creates CV and/or publishes in organization with given name and repository :param cv_name: The name of CV to create @@ -502,21 +530,21 @@ def _create_cv(cv_name, repo, module_org, publish=True): :return: The directory of CV and Content View ID """ description = gen_string('alpha') - content_view = make_content_view( + content_view = sat.cli_factory.make_content_view( {'name': cv_name, 'description': description, 'organization-id': module_org.id} ) - ContentView.add_repository( + sat.cli.ContentView.add_repository( { 'id': content_view['id'], 'organization-id': module_org.id, 'repository-id': repo['id'], } ) - content_view = ContentView.info({'name': cv_name, 'organization-id': module_org.id}) + content_view = sat.cli.ContentView.info({'name': cv_name, 'organization-id': module_org.id}) cvv_id = None if publish: - ContentView.publish({'id': content_view['id']}) - content_view = ContentView.info({'id': content_view['id']}) + sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = sat.cli.ContentView.info({'id': content_view['id']}) cvv_id = content_view['versions'][0]['id'] return content_view, cvv_id @@ -753,10 +781,10 @@ def test_positive_export_import_filtered_cvv( exporting_cv_name, class_export_entities['exporting_repo'], class_export_entities['exporting_org'], - False, + target_sat, ) filter_name = gen_string('alphanumeric') - ContentView.filter.create( + target_sat.cli.ContentView.filter.create( { 'name': filter_name, 'content-view-id': exporting_cv['id'], @@ -764,46 +792,50 @@ def test_positive_export_import_filtered_cvv( 'type': 'rpm', } ) - ContentView.filter.rule.create( + target_sat.cli.ContentView.filter.rule.create( { 'name': 'cat', 'content-view-filter': filter_name, 'content-view-id': exporting_cv['id'], } ) - ContentView.publish( + target_sat.cli.ContentView.publish( { 'id': exporting_cv['id'], 'organization-id': class_export_entities['exporting_org'].id, } ) - exporting_cv = ContentView.info({'id': exporting_cv['id']}) + exporting_cv = target_sat.cli.ContentView.info({'id': exporting_cv['id']}) exporting_cvv_id = exporting_cv['versions'][0]['id'] # Check presence of 1 rpm due to filter - export_packages = Package.list({'content-view-version-id': exporting_cvv_id}) + export_packages = target_sat.cli.Package.list({'content-view-version-id': exporting_cvv_id}) assert len(export_packages) == 1 # Verify export directory is empty assert target_sat.validate_pulp_filepath(module_org, PULP_EXPORT_DIR) == '' # Export cv - export = ContentExport.completeVersion( + export = target_sat.cli.ContentExport.completeVersion( {'id': exporting_cvv_id, 'organization-id': module_org.id} ) import_path = target_sat.move_pulp_archive(module_org, export['message']) # Import section - importing_org = make_org() + importing_org = target_sat.cli_factory.make_org() # set disconnected mode - Settings.set({'name': 'subscription_connection_enabled', 'value': "No"}) + target_sat.cli.Settings.set({'name': 'subscription_connection_enabled', 'value': "No"}) # check that files are present in import_path result = target_sat.execute(f'ls {import_path}') assert result.stdout != '' # Import file and verify content - ContentImport.version({'organization-id': importing_org['id'], 'path': import_path}) - importing_cvv = ContentView.info( + target_sat.cli.ContentImport.version( + {'organization-id': importing_org['id'], 'path': import_path} + ) + importing_cvv = target_sat.cli.ContentView.info( {'name': importing_cvv, 'organization-id': importing_org['id']} )['versions'] assert len(importing_cvv) >= 1 - imported_packages = Package.list({'content-view-version-id': importing_cvv[0]['id']}) + imported_packages = target_sat.cli.Package.list( + {'content-view-version-id': importing_cvv[0]['id']} + ) assert len(imported_packages) == 1 assert len(export_packages) == len(imported_packages) @@ -1146,7 +1178,7 @@ def test_negative_import_same_cv_twice( ) in error.value.message @pytest.mark.tier2 - def test_negative_import_invalid_path(self, module_org): + def test_negative_import_invalid_path(self, module_org, module_target_sat): """Import cv that doesn't exist in path :id: 4cc69666-407f-4d66-b3d2-8fe2ed135a5f @@ -1164,7 +1196,9 @@ def test_negative_import_invalid_path(self, module_org): import_path = f'{PULP_IMPORT_DIR}{export_folder}' # Import section with pytest.raises(CLIReturnCodeError) as error: - ContentImport.version({'organization-id': module_org.id, 'path': import_path}) + module_target_sat.cli.ContentImport.version( + {'organization-id': module_org.id, 'path': import_path} + ) assert ( f'''Error: Unable to find '{import_path}/metadata.json'. ''' 'If the metadata.json file is at a different location provide it to the ' @@ -1229,6 +1263,57 @@ def test_postive_export_cv_with_mixed_content_repos( exported_packages = target_sat.cli.Package.list( {'content-view-version-id': exporting_cvv['id']} ) + product = target_sat.cli_factory.make_product( + { + 'organization-id': function_org.id, + 'name': gen_string('alpha'), + } + ) + nonyum_repo = target_sat.cli_factory.make_repository( + { + 'content-type': 'docker', + 'docker-upstream-name': 'quay/busybox', + 'organization-id': function_org.id, + 'product-id': product['id'], + 'url': CONTAINER_REGISTRY_HUB, + }, + ) + target_sat.cli.Repository.synchronize({'id': nonyum_repo['id']}) + yum_repo = target_sat.cli_factory.make_repository( + { + 'name': gen_string('alpha'), + 'download-policy': 'immediate', + 'mirror-on-sync': 'no', + 'product-id': product['id'], + } + ) + target_sat.cli.Repository.synchronize({'id': yum_repo['id']}) + content_view = target_sat.cli_factory.make_content_view( + {'organization-id': function_org.id} + ) + # Add docker and yum repo + target_sat.cli.ContentView.add_repository( + { + 'id': content_view['id'], + 'organization-id': function_org.id, + 'repository-id': nonyum_repo['id'], + } + ) + target_sat.cli.ContentView.add_repository( + { + 'id': content_view['id'], + 'organization-id': function_org.id, + 'repository-id': yum_repo['id'], + } + ) + target_sat.cli.ContentView.publish({'id': content_view['id']}) + exporting_cv_id = target_sat.cli.ContentView.info({'id': content_view['id']}) + assert len(exporting_cv_id['versions']) == 1 + exporting_cvv_id = exporting_cv_id['versions'][0] + # check packages + exported_packages = target_sat.cli.Package.list( + {'content-view-version-id': exporting_cvv_id['id']} + ) assert len(exported_packages) # Verify export directory is empty assert target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) == '' @@ -1462,7 +1547,9 @@ def test_postive_export_import_ansible_collection_repo( import_product = target_sat.cli.Product.info( { 'organization-id': function_import_org.id, - 'id': Product.list({'organization-id': function_import_org.id})[0]['id'], + 'id': target_sat.cli.Product.list({'organization-id': function_import_org.id})[0][ + 'id' + ], } ) assert import_product['name'] == export_product['name'] diff --git a/tests/foreman/cli/test_settings.py b/tests/foreman/cli/test_settings.py index 3a47b6500a5..4923e8ec5f8 100644 --- a/tests/foreman/cli/test_settings.py +++ b/tests/foreman/cli/test_settings.py @@ -21,9 +21,8 @@ import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.settings import Settings from robottelo.config import settings +from robottelo.exceptions import CLIReturnCodeError from robottelo.utils.datafactory import ( gen_string, generate_strings_list, @@ -51,7 +50,7 @@ def test_negative_update_hostname_with_empty_fact(): @pytest.mark.tier2 @pytest.mark.parametrize('setting_update', ['discovery_prefix'], indirect=True) -def test_positive_update_hostname_prefix_without_value(setting_update): +def test_positive_update_hostname_prefix_without_value(setting_update, module_target_sat): """Update the Hostname_prefix settings without any string(empty values) :id: a84c28ea-6821-4c31-b4ab-8662c22c9135 @@ -64,12 +63,12 @@ def test_positive_update_hostname_prefix_without_value(setting_update): """ with pytest.raises(CLIReturnCodeError): - Settings.set({'name': "discovery_prefix", 'value': ""}) + module_target_sat.cli.Settings.set({'name': "discovery_prefix", 'value': ""}) @pytest.mark.tier2 @pytest.mark.parametrize('setting_update', ['discovery_prefix'], indirect=True) -def test_positive_update_hostname_default_prefix(setting_update): +def test_positive_update_hostname_default_prefix(setting_update, module_target_sat): """Update the default set prefix of hostname_prefix setting :id: a6e46e53-6273-406a-8009-f184d9551d66 @@ -80,8 +79,8 @@ def test_positive_update_hostname_default_prefix(setting_update): """ hostname_prefix_value = gen_string('alpha') - Settings.set({'name': "discovery_prefix", 'value': hostname_prefix_value}) - discovery_prefix = Settings.list({'search': 'name=discovery_prefix'})[0] + module_target_sat.cli.Settings.set({'name': "discovery_prefix", 'value': hostname_prefix_value}) + discovery_prefix = module_target_sat.cli.Settings.list({'search': 'name=discovery_prefix'})[0] assert hostname_prefix_value == discovery_prefix['value'] @@ -116,7 +115,7 @@ def test_negative_discover_host_with_invalid_prefix(): @pytest.mark.tier2 @pytest.mark.parametrize('setting_update', ['login_text'], indirect=True) -def test_positive_update_login_page_footer_text(setting_update): +def test_positive_update_login_page_footer_text(setting_update, module_target_sat): """Updates parameter "login_text" in settings :id: 4d4e1151-5bd6-4fa2-8dbb-e182b43ad7ec @@ -132,14 +131,14 @@ def test_positive_update_login_page_footer_text(setting_update): """ login_text_value = random.choice(list(valid_data_list().values())) - Settings.set({'name': "login_text", 'value': login_text_value}) - login_text = Settings.list({'search': 'name=login_text'})[0] + module_target_sat.cli.Settings.set({'name': "login_text", 'value': login_text_value}) + login_text = module_target_sat.cli.Settings.list({'search': 'name=login_text'})[0] assert login_text["value"] == login_text_value @pytest.mark.tier2 @pytest.mark.parametrize('setting_update', ['login_text'], indirect=True) -def test_positive_update_login_page_footer_text_without_value(setting_update): +def test_positive_update_login_page_footer_text_without_value(setting_update, module_target_sat): """Updates parameter "login_text" without any string (empty value) :id: 01ce95de-2994-42b6-b9f8-f7882981fb69 @@ -154,14 +153,14 @@ def test_positive_update_login_page_footer_text_without_value(setting_update): :expectedresults: Message on login screen should be removed """ - Settings.set({'name': "login_text", 'value': ""}) - login_text = Settings.list({'search': 'name=login_text'})[0] + module_target_sat.cli.Settings.set({'name': "login_text", 'value': ""}) + login_text = module_target_sat.cli.Settings.list({'search': 'name=login_text'})[0] assert login_text['value'] == '' @pytest.mark.tier2 @pytest.mark.parametrize('setting_update', ['login_text'], indirect=True) -def test_positive_update_login_page_footer_text_with_long_string(setting_update): +def test_positive_update_login_page_footer_text_with_long_string(setting_update, module_target_sat): """Attempt to update parameter "Login_page_footer_text" with long length string under General tab @@ -180,8 +179,8 @@ def test_positive_update_login_page_footer_text_with_long_string(setting_update) login_text_value = random.choice( list(generate_strings_list(length=1000, exclude_types=['latin1', 'utf8', 'cjk', 'html'])) ) - Settings.set({'name': "login_text", 'value': login_text_value}) - login_text = Settings.list({'search': 'name=login_text'})[0] + module_target_sat.cli.Settings.set({'name': "login_text", 'value': login_text_value}) + login_text = module_target_sat.cli.Settings.list({'search': 'name=login_text'})[0] assert login_text['value'] == login_text_value @@ -214,7 +213,7 @@ def test_positive_update_email_delivery_method_smtp(): @pytest.mark.tier2 @pytest.mark.parametrize('setting_update', ['delivery_method'], indirect=True) -def test_positive_update_email_delivery_method_sendmail(setting_update): +def test_positive_update_email_delivery_method_sendmail(setting_update, module_target_sat): """Check Updating Sendmail params through settings subcommand :id: 578de898-fde2-4957-b39a-9dd059f490bf @@ -238,13 +237,13 @@ def test_positive_update_email_delivery_method_sendmail(setting_update): 'sendmail_location': '/usr/sbin/sendmail', } for key, value in sendmail_config_params.items(): - Settings.set({'name': f'{key}', 'value': f'{value}'}) - assert Settings.list({'search': f'name={key}'})[0]['value'] == value + module_target_sat.cli.Settings.set({'name': f'{key}', 'value': f'{value}'}) + assert module_target_sat.cli.Settings.list({'search': f'name={key}'})[0]['value'] == value @pytest.mark.tier2 @pytest.mark.parametrize('setting_update', ['email_reply_address'], indirect=True) -def test_positive_update_email_reply_address(setting_update): +def test_positive_update_email_reply_address(setting_update, module_target_sat): """Check email reply address is updated :id: cb0907d1-9cb6-45c4-b2bb-e2790ea55f16 @@ -259,8 +258,8 @@ def test_positive_update_email_reply_address(setting_update): """ email_address = random.choice(list(valid_emails_list())) email_address = email_address.replace('"', r'\"').replace('`', r'\`') - Settings.set({'name': "email_reply_address", 'value': email_address}) - email_reply_address = Settings.list( + module_target_sat.cli.Settings.set({'name': "email_reply_address", 'value': email_address}) + email_reply_address = module_target_sat.cli.Settings.list( {'search': 'name=email_reply_address'}, output_format='json' )[0] updated_email_address = ( @@ -271,7 +270,7 @@ def test_positive_update_email_reply_address(setting_update): @pytest.mark.tier2 @pytest.mark.parametrize('setting_update', ['email_reply_address'], indirect=True) -def test_negative_update_email_reply_address(setting_update): +def test_negative_update_email_reply_address(setting_update, module_target_sat): """Check email reply address is not updated :id: 2a2220c2-badf-47d5-ba3f-e6329930ab39 @@ -288,12 +287,14 @@ def test_negative_update_email_reply_address(setting_update): """ invalid_email_address = random.choice(list(invalid_emails_list())) with pytest.raises(CLIReturnCodeError): - Settings.set({'name': 'email_reply_address', 'value': invalid_email_address}) + module_target_sat.cli.Settings.set( + {'name': 'email_reply_address', 'value': invalid_email_address} + ) @pytest.mark.tier2 @pytest.mark.parametrize('setting_update', ['email_subject_prefix'], indirect=True) -def test_positive_update_email_subject_prefix(setting_update): +def test_positive_update_email_subject_prefix(setting_update, module_target_sat): """Check email subject prefix is updated :id: c8e6b323-7b39-43d6-a9f1-5474f920bba2 @@ -307,14 +308,18 @@ def test_positive_update_email_subject_prefix(setting_update): :CaseImportance: Low """ email_subject_prefix_value = gen_string('alpha') - Settings.set({'name': "email_subject_prefix", 'value': email_subject_prefix_value}) - email_subject_prefix = Settings.list({'search': 'name=email_subject_prefix'})[0] + module_target_sat.cli.Settings.set( + {'name': "email_subject_prefix", 'value': email_subject_prefix_value} + ) + email_subject_prefix = module_target_sat.cli.Settings.list( + {'search': 'name=email_subject_prefix'} + )[0] assert email_subject_prefix_value == email_subject_prefix['value'] @pytest.mark.tier2 @pytest.mark.parametrize('setting_update', ['email_subject_prefix'], indirect=True) -def test_negative_update_email_subject_prefix(setting_update): +def test_negative_update_email_subject_prefix(setting_update, module_target_sat): """Check email subject prefix is not updated :id: 8a638596-248f-4196-af36-ad2982196382 @@ -329,18 +334,26 @@ def test_negative_update_email_subject_prefix(setting_update): :CaseImportance: Low """ - email_subject_prefix_original = Settings.list({'search': 'name=email_subject_prefix'})[0] + email_subject_prefix_original = module_target_sat.cli.Settings.list( + {'search': 'name=email_subject_prefix'} + )[0] email_subject_prefix_value = gen_string('alpha', 256) with pytest.raises(CLIReturnCodeError): - Settings.set({'name': 'email_subject_prefix', 'value': email_subject_prefix_value}) - email_subject_prefix = Settings.list({'search': 'name=email_subject_prefix'})[0] + module_target_sat.cli.Settings.set( + {'name': 'email_subject_prefix', 'value': email_subject_prefix_value} + ) + email_subject_prefix = module_target_sat.cli.Settings.list( + {'search': 'name=email_subject_prefix'} + )[0] assert email_subject_prefix == email_subject_prefix_original @pytest.mark.tier2 @pytest.mark.parametrize('send_welcome_email_value', ["true", "false"]) @pytest.mark.parametrize('setting_update', ['send_welcome_email'], indirect=True) -def test_positive_update_send_welcome_email(setting_update, send_welcome_email_value): +def test_positive_update_send_welcome_email( + setting_update, send_welcome_email_value, module_target_sat +): """Check email send welcome email is updated :id: cdaf6cd0-5eea-4252-87c5-f9ec3ba79ac1 @@ -355,15 +368,19 @@ def test_positive_update_send_welcome_email(setting_update, send_welcome_email_v :CaseImportance: Low """ - Settings.set({'name': 'send_welcome_email', 'value': send_welcome_email_value}) - host_value = Settings.list({'search': 'name=send_welcome_email'})[0]['value'] + module_target_sat.cli.Settings.set( + {'name': 'send_welcome_email', 'value': send_welcome_email_value} + ) + host_value = module_target_sat.cli.Settings.list({'search': 'name=send_welcome_email'})[0][ + 'value' + ] assert send_welcome_email_value == host_value @pytest.mark.tier2 @pytest.mark.parametrize('rss_enable_value', ["true", "false"]) @pytest.mark.parametrize('setting_update', ['rss_enable'], indirect=True) -def test_positive_enable_disable_rssfeed(setting_update, rss_enable_value): +def test_positive_enable_disable_rssfeed(setting_update, rss_enable_value, module_target_sat): """Check if the RSS feed can be enabled or disabled :id: 021cefab-2629-44e2-a30d-49c944d0a234 @@ -376,14 +393,14 @@ def test_positive_enable_disable_rssfeed(setting_update, rss_enable_value): :CaseAutomation: Automated """ - Settings.set({'name': 'rss_enable', 'value': rss_enable_value}) - rss_setting = Settings.list({'search': 'name=rss_enable'})[0] + module_target_sat.cli.Settings.set({'name': 'rss_enable', 'value': rss_enable_value}) + rss_setting = module_target_sat.cli.Settings.list({'search': 'name=rss_enable'})[0] assert rss_setting["value"] == rss_enable_value @pytest.mark.tier2 @pytest.mark.parametrize('setting_update', ['rss_url'], indirect=True) -def test_positive_update_rssfeed_url(setting_update): +def test_positive_update_rssfeed_url(setting_update, module_target_sat): """Check if the RSS feed URL is updated :id: 166ff6f2-e36e-4934-951f-b947139d0d73 @@ -401,14 +418,14 @@ def test_positive_update_rssfeed_url(setting_update): :CaseAutomation: Automated """ test_url = random.choice(list(valid_url_list())) - Settings.set({'name': 'rss_url', 'value': test_url}) - updated_url = Settings.list({'search': 'name=rss_url'})[0] + module_target_sat.cli.Settings.set({'name': 'rss_url', 'value': test_url}) + updated_url = module_target_sat.cli.Settings.list({'search': 'name=rss_url'})[0] assert updated_url['value'] == test_url @pytest.mark.parametrize('value', **xdist_adapter(invalid_boolean_strings())) @pytest.mark.tier2 -def test_negative_update_send_welcome_email(value): +def test_negative_update_send_welcome_email(value, module_target_sat): """Check email send welcome email is updated :id: 2f75775d-72a1-4b2f-86c2-98c36e446099 @@ -426,7 +443,7 @@ def test_negative_update_send_welcome_email(value): :CaseImportance: Low """ with pytest.raises(CLIReturnCodeError): - Settings.set({'name': 'send_welcome_email', 'value': value}) + module_target_sat.cli.Settings.set({'name': 'send_welcome_email', 'value': value}) @pytest.mark.tier3 @@ -461,11 +478,11 @@ def test_positive_failed_login_attempts_limit(setting_update, target_sat): username = settings.server.admin_username password = settings.server.admin_password assert target_sat.execute(f'hammer -u {username} -p {password} user list').status == 0 - Settings.set({'name': 'failed_login_attempts_limit', 'value': '5'}) + target_sat.cli.Settings.set({'name': 'failed_login_attempts_limit', 'value': '5'}) for _ in range(5): assert target_sat.execute(f'hammer -u {username} -p BAD_PASS user list').status == 129 assert target_sat.execute(f'hammer -u {username} -p {password} user list').status == 129 sleep(301) assert target_sat.execute(f'hammer -u {username} -p {password} user list').status == 0 - Settings.set({'name': 'failed_login_attempts_limit', 'value': '0'}) - assert Settings.info({'name': 'failed_login_attempts_limit'})['value'] == '0' + target_sat.cli.Settings.set({'name': 'failed_login_attempts_limit', 'value': '0'}) + assert target_sat.cli.Settings.info({'name': 'failed_login_attempts_limit'})['value'] == '0' diff --git a/tests/foreman/cli/test_subnet.py b/tests/foreman/cli/test_subnet.py index 5eefb2c24ad..eaa2262d13e 100644 --- a/tests/foreman/cli/test_subnet.py +++ b/tests/foreman/cli/test_subnet.py @@ -22,10 +22,8 @@ from fauxfactory import gen_choice, gen_integer, gen_ipaddr import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import CLIFactoryError, make_domain, make_subnet -from robottelo.cli.subnet import Subnet from robottelo.constants import SUBNET_IPAM_TYPES +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.utils.datafactory import ( filtered_datapoint, parametrized, @@ -72,7 +70,7 @@ def invalid_missing_attributes(): @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_CRUD(): +def test_positive_CRUD(module_target_sat): """Create, update and delete subnet :id: d74a52a7-df56-44ef-89a3-081c14e81e43 @@ -89,10 +87,10 @@ def test_positive_CRUD(): from_ip = re.sub(r'\d+$', str(pool[0]), network) to_ip = re.sub(r'\d+$', str(pool[1]), network) domains_amount = random.randint(2, 3) - domains = [make_domain() for _ in range(domains_amount)] + domains = [module_target_sat.cli_factory.make_domain() for _ in range(domains_amount)] gateway = gen_ipaddr(ip3=True) ipam_type = SUBNET_IPAM_TYPES['dhcp'] - subnet = make_subnet( + subnet = module_target_sat.cli_factory.make_subnet( { 'name': name, 'from': from_ip, @@ -105,7 +103,7 @@ def test_positive_CRUD(): } ) # Check if Subnet can be listed - subnets_ids = [subnet_['id'] for subnet_ in Subnet.list()] + subnets_ids = [subnet_['id'] for subnet_ in module_target_sat.cli.Subnet.list()] assert subnet['id'] in subnets_ids assert subnet['name'] == name assert subnet['start-of-ip-range'] == from_ip @@ -125,7 +123,7 @@ def test_positive_CRUD(): ip_from = re.sub(r'\d+$', str(pool[0]), new_network) ip_to = re.sub(r'\d+$', str(pool[1]), new_network) ipam_type = SUBNET_IPAM_TYPES['internal'] - Subnet.update( + module_target_sat.cli.Subnet.update( { 'new-name': new_name, 'from': ip_from, @@ -137,7 +135,7 @@ def test_positive_CRUD(): 'domain-ids': "", # delete domains needed for subnet delete } ) - subnet = Subnet.info({'id': subnet['id']}) + subnet = module_target_sat.cli.Subnet.info({'id': subnet['id']}) assert subnet['name'] == new_name assert subnet['start-of-ip-range'] == ip_from assert subnet['end-of-ip-range'] == ip_to @@ -146,14 +144,14 @@ def test_positive_CRUD(): assert ipam_type in subnet['ipam'] # delete subnet - Subnet.delete({'id': subnet['id']}) + module_target_sat.cli.Subnet.delete({'id': subnet['id']}) with pytest.raises(CLIReturnCodeError): - Subnet.info({'id': subnet['id']}) + module_target_sat.cli.Subnet.info({'id': subnet['id']}) @pytest.mark.tier2 @pytest.mark.parametrize('options', **parametrized(invalid_missing_attributes())) -def test_negative_create_with_attributes(options): +def test_negative_create_with_attributes(options, module_target_sat): """Create subnet with invalid or missing required attributes :id: de468dd3-7ba8-463e-881a-fd1cb3cfc7b6 @@ -165,13 +163,13 @@ def test_negative_create_with_attributes(options): :CaseImportance: Medium """ with pytest.raises(CLIFactoryError, match='Could not create the subnet:'): - make_subnet(options) + module_target_sat.cli_factory.make_subnet(options) @pytest.mark.tier2 @pytest.mark.upgrade @pytest.mark.parametrize('pool', **parametrized(invalid_addr_pools())) -def test_negative_create_with_address_pool(pool): +def test_negative_create_with_address_pool(pool, module_target_sat): """Create subnet with invalid address pool range :parametrized: yes @@ -189,13 +187,13 @@ def test_negative_create_with_address_pool(pool): for key, val in pool.items(): opts[key] = re.sub(r'\d+$', str(val), network) with pytest.raises(CLIFactoryError) as raise_ctx: - make_subnet(opts) + module_target_sat.cli_factory.make_subnet(opts) assert 'Could not create the subnet:' in str(raise_ctx.value) @pytest.mark.tier2 @pytest.mark.parametrize('options', **parametrized(invalid_missing_attributes())) -def test_negative_update_attributes(options): +def test_negative_update_attributes(options, module_target_sat): """Update subnet with invalid or missing required attributes :parametrized: yes @@ -206,19 +204,19 @@ def test_negative_update_attributes(options): :CaseImportance: Medium """ - subnet = make_subnet() + subnet = module_target_sat.cli_factory.make_subnet() options['id'] = subnet['id'] with pytest.raises(CLIReturnCodeError, match='Could not update the subnet:'): - Subnet.update(options) + module_target_sat.cli.Subnet.update(options) # check - subnet is not updated - result = Subnet.info({'id': subnet['id']}) + result = module_target_sat.cli.Subnet.info({'id': subnet['id']}) for key in options.keys(): assert subnet[key] == result[key] @pytest.mark.tier2 @pytest.mark.parametrize('options', **parametrized(invalid_addr_pools())) -def test_negative_update_address_pool(options): +def test_negative_update_address_pool(options, module_target_sat): """Update subnet with invalid address pool :parametrized: yes @@ -229,15 +227,15 @@ def test_negative_update_address_pool(options): :CaseImportance: Medium """ - subnet = make_subnet() + subnet = module_target_sat.cli_factory.make_subnet() opts = {'id': subnet['id']} # generate pool range from network address for key, val in options.items(): opts[key] = re.sub(r'\d+$', str(val), subnet['network-addr']) with pytest.raises(CLIReturnCodeError, match='Could not update the subnet:'): - Subnet.update(opts) + module_target_sat.cli.Subnet.update(opts) # check - subnet is not updated - result = Subnet.info({'id': subnet['id']}) + result = module_target_sat.cli.Subnet.info({'id': subnet['id']}) for key in ['start-of-ip-range', 'end-of-ip-range']: assert result[key] == subnet[key] diff --git a/tests/foreman/cli/test_subscription.py b/tests/foreman/cli/test_subscription.py index d6feeb1381d..f2491479e40 100644 --- a/tests/foreman/cli/test_subscription.py +++ b/tests/foreman/cli/test_subscription.py @@ -20,23 +20,20 @@ from nailgun import entities import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import make_activation_key, make_product, make_repository -from robottelo.cli.host import Host -from robottelo.cli.repository import Repository -from robottelo.cli.repository_set import RepositorySet -from robottelo.cli.subscription import Subscription from robottelo.constants import PRDS, REPOS, REPOSET +from robottelo.exceptions import CLIReturnCodeError pytestmark = [pytest.mark.run_in_one_thread] @pytest.fixture(scope='module') -def golden_ticket_host_setup(request, module_sca_manifest_org): - new_product = make_product({'organization-id': module_sca_manifest_org.id}) - new_repo = make_repository({'product-id': new_product['id']}) - Repository.synchronize({'id': new_repo['id']}) - new_ak = make_activation_key( +def golden_ticket_host_setup(request, module_sca_manifest_org, module_target_sat): + new_product = module_target_sat.cli_factory.make_product( + {'organization-id': module_sca_manifest_org.id} + ) + new_repo = module_target_sat.cli_factory.make_repository({'product-id': new_product['id']}) + module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) + new_ak = module_target_sat.cli_factory.make_activation_key( { 'lifecycle-environment': 'Library', 'content-view': 'Default Organization View', @@ -48,7 +45,7 @@ def golden_ticket_host_setup(request, module_sca_manifest_org): @pytest.mark.tier1 -def test_positive_manifest_upload(function_entitlement_manifest_org): +def test_positive_manifest_upload(function_entitlement_manifest_org, module_target_sat): """upload manifest :id: e5a0e4f8-fed9-4896-87a0-ac33f6baa227 @@ -58,12 +55,14 @@ def test_positive_manifest_upload(function_entitlement_manifest_org): :CaseImportance: Critical """ - Subscription.list({'organization-id': function_entitlement_manifest_org.id}, per_page=False) + module_target_sat.cli.Subscription.list( + {'organization-id': function_entitlement_manifest_org.id}, per_page=False + ) @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_manifest_delete(function_entitlement_manifest_org): +def test_positive_manifest_delete(function_entitlement_manifest_org, module_target_sat): """Delete uploaded manifest :id: 01539c07-00d5-47e2-95eb-c0fd4f39090f @@ -72,14 +71,20 @@ def test_positive_manifest_delete(function_entitlement_manifest_org): :CaseImportance: Critical """ - Subscription.list({'organization-id': function_entitlement_manifest_org.id}, per_page=False) - Subscription.delete_manifest({'organization-id': function_entitlement_manifest_org.id}) - Subscription.list({'organization-id': function_entitlement_manifest_org.id}, per_page=False) + module_target_sat.cli.Subscription.list( + {'organization-id': function_entitlement_manifest_org.id}, per_page=False + ) + module_target_sat.cli.Subscription.delete_manifest( + {'organization-id': function_entitlement_manifest_org.id} + ) + module_target_sat.cli.Subscription.list( + {'organization-id': function_entitlement_manifest_org.id}, per_page=False + ) @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_enable_manifest_reposet(function_entitlement_manifest_org): +def test_positive_enable_manifest_reposet(function_entitlement_manifest_org, module_target_sat): """enable repository set :id: cc0f8f40-5ea6-4fa7-8154-acdc2cb56b45 @@ -91,8 +96,10 @@ def test_positive_enable_manifest_reposet(function_entitlement_manifest_org): :CaseImportance: Critical """ - Subscription.list({'organization-id': function_entitlement_manifest_org.id}, per_page=False) - RepositorySet.enable( + module_target_sat.cli.Subscription.list( + {'organization-id': function_entitlement_manifest_org.id}, per_page=False + ) + module_target_sat.cli.RepositorySet.enable( { 'basearch': 'x86_64', 'name': REPOSET['rhva6'], @@ -101,7 +108,7 @@ def test_positive_enable_manifest_reposet(function_entitlement_manifest_org): 'releasever': '6Server', } ) - Repository.synchronize( + module_target_sat.cli.Repository.synchronize( { 'name': REPOS['rhva6']['name'], 'organization-id': function_entitlement_manifest_org.id, @@ -111,7 +118,7 @@ def test_positive_enable_manifest_reposet(function_entitlement_manifest_org): @pytest.mark.tier3 -def test_positive_manifest_history(function_entitlement_manifest_org): +def test_positive_manifest_history(function_entitlement_manifest_org, module_target_sat): """upload manifest and check history :id: 000ab0a0-ec1b-497a-84ff-3969a965b52c @@ -121,14 +128,14 @@ def test_positive_manifest_history(function_entitlement_manifest_org): :CaseImportance: Medium """ org = function_entitlement_manifest_org - Subscription.list({'organization-id': org.id}, per_page=None) - history = Subscription.manifest_history({'organization-id': org.id}) + module_target_sat.cli.Subscription.list({'organization-id': org.id}, per_page=None) + history = module_target_sat.cli.Subscription.manifest_history({'organization-id': org.id}) assert f'{org.name} file imported successfully.' in ''.join(history) @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_manifest_refresh(function_entitlement_manifest_org): +def test_positive_manifest_refresh(function_entitlement_manifest_org, module_target_sat): """upload manifest and refresh :id: 579bbbf7-11cf-4d78-a3b1-16d73bd4ca57 @@ -137,13 +144,19 @@ def test_positive_manifest_refresh(function_entitlement_manifest_org): :CaseImportance: Critical """ - Subscription.list({'organization-id': function_entitlement_manifest_org.id}, per_page=False) - Subscription.refresh_manifest({'organization-id': function_entitlement_manifest_org.id}) - Subscription.delete_manifest({'organization-id': function_entitlement_manifest_org.id}) + module_target_sat.cli.Subscription.list( + {'organization-id': function_entitlement_manifest_org.id}, per_page=False + ) + module_target_sat.cli.Subscription.refresh_manifest( + {'organization-id': function_entitlement_manifest_org.id} + ) + module_target_sat.cli.Subscription.delete_manifest( + {'organization-id': function_entitlement_manifest_org.id} + ) @pytest.mark.tier2 -def test_positive_subscription_list(function_entitlement_manifest_org): +def test_positive_subscription_list(function_entitlement_manifest_org, module_target_sat): """Verify that subscription list contains start and end date :id: 4861bcbc-785a-436d-98ce-14cfef7d6907 @@ -156,7 +169,7 @@ def test_positive_subscription_list(function_entitlement_manifest_org): :CaseImportance: Medium """ - subscription_list = Subscription.list( + subscription_list = module_target_sat.cli.Subscription.list( {'organization-id': function_entitlement_manifest_org.id}, per_page=False ) for column in ['start-date', 'end-date']: @@ -189,14 +202,14 @@ def test_positive_delete_manifest_as_another_user(target_sat, function_entitleme ).create() # use the first admin to upload a manifest target_sat.put(f'{function_entitlement_manifest.path}', f'{function_entitlement_manifest.name}') - Subscription.with_user(username=user1.login, password=user1_password).upload( + target_sat.cli.Subscription.with_user(username=user1.login, password=user1_password).upload( {'file': f'{function_entitlement_manifest.name}', 'organization-id': f'{org.id}'} ) # try to search and delete the manifest with another admin - Subscription.with_user(username=user2.login, password=user2_password).delete_manifest( - {'organization-id': org.id} - ) - assert len(Subscription.list({'organization-id': org.id})) == 0 + target_sat.cli.Subscription.with_user( + username=user2.login, password=user2_password + ).delete_manifest({'organization-id': org.id}) + assert len(target_sat.cli.Subscription.list({'organization-id': org.id})) == 0 @pytest.mark.tier2 @@ -266,8 +279,8 @@ def test_positive_auto_attach_disabled_golden_ticket( rhel7_contenthost_class.install_katello_ca(target_sat) rhel7_contenthost_class.register_contenthost(module_org.label, golden_ticket_host_setup['name']) assert rhel7_contenthost_class.subscribed - host = Host.list({'search': rhel7_contenthost_class.hostname}) + host = target_sat.cli.Host.list({'search': rhel7_contenthost_class.hostname}) host_id = host[0]['id'] with pytest.raises(CLIReturnCodeError) as context: - Host.subscription_auto_attach({'host-id': host_id}) + target_sat.cli.Host.subscription_auto_attach({'host-id': host_id}) assert "This host's organization is in Simple Content Access mode" in str(context.value) diff --git a/tests/foreman/cli/test_syncplan.py b/tests/foreman/cli/test_syncplan.py index c22dcbe198e..b6dc18e9bc6 100644 --- a/tests/foreman/cli/test_syncplan.py +++ b/tests/foreman/cli/test_syncplan.py @@ -23,18 +23,8 @@ from nailgun import entities import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import ( - CLIFactoryError, - make_product, - make_repository, - make_sync_plan, -) -from robottelo.cli.product import Product -from robottelo.cli.repository import Repository -from robottelo.cli.repository_set import RepositorySet -from robottelo.cli.syncplan import SyncPlan from robottelo.constants import PRDS, REPOS, REPOSET +from robottelo.exceptions import CLIFactoryError, CLIReturnCodeError from robottelo.logging import logger from robottelo.utils.datafactory import ( filtered_datapoint, @@ -113,7 +103,7 @@ def validate_task_status(sat, repo_id, org_id, max_tries=10): ) -def validate_repo_content(repo, content_types, after_sync=True): +def validate_repo_content(sat, repo, content_types, after_sync=True): """Check whether corresponding content is present in repository before or after synchronization is performed @@ -123,7 +113,7 @@ def validate_repo_content(repo, content_types, after_sync=True): :param bool after_sync: Specify whether you perform validation before synchronization procedure is happened or after """ - repo = Repository.info({'id': repo['id']}) + repo = sat.cli.Repository.info({'id': repo['id']}) for content in content_types: count = int(repo['content-counts'][content]) assert count > 0 if after_sync else count == 0 @@ -131,7 +121,7 @@ def validate_repo_content(repo, content_types, after_sync=True): @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_create_with_name(module_org, name): +def test_positive_create_with_name(module_org, name, module_target_sat): """Check if syncplan can be created with random names :id: dc0a86f7-4219-427e-92fd-29352dbdbfce @@ -142,14 +132,16 @@ def test_positive_create_with_name(module_org, name): :CaseImportance: Critical """ - sync_plan = make_sync_plan({'enabled': 'false', 'name': name, 'organization-id': module_org.id}) - result = SyncPlan.info({'id': sync_plan['id']}) + sync_plan = module_target_sat.cli_factory.sync_plan( + {'enabled': 'false', 'name': name, 'organization-id': module_org.id} + ) + result = module_target_sat.cli.SyncPlan.info({'id': sync_plan['id']}) assert result['name'] == name @pytest.mark.parametrize('desc', **parametrized(valid_data_list())) @pytest.mark.tier1 -def test_positive_create_with_description(module_org, desc): +def test_positive_create_with_description(module_org, desc, module_target_sat): """Check if syncplan can be created with random description :id: a1bbe81b-60f5-4a19-b400-a02a23fa1dfa @@ -160,16 +152,16 @@ def test_positive_create_with_description(module_org, desc): :CaseImportance: Critical """ - new_sync_plan = make_sync_plan( + new_sync_plan = module_target_sat.cli_factory.sync_plan( {'enabled': 'false', 'description': desc, 'organization-id': module_org.id} ) - result = SyncPlan.info({'id': new_sync_plan['id']}) + result = module_target_sat.cli.SyncPlan.info({'id': new_sync_plan['id']}) assert result['description'] == desc @pytest.mark.parametrize('test_data', **parametrized(valid_name_interval_create_tests())) @pytest.mark.tier1 -def test_positive_create_with_interval(module_org, test_data): +def test_positive_create_with_interval(module_org, test_data, module_target_sat): """Check if syncplan can be created with varied intervals :id: 32eb0c1d-0c9a-4fb5-a185-68d0d705fbce @@ -180,7 +172,7 @@ def test_positive_create_with_interval(module_org, test_data): :CaseImportance: Critical """ - new_sync_plan = make_sync_plan( + new_sync_plan = module_target_sat.cli_factory.sync_plan( { 'enabled': 'false', 'interval': test_data['interval'], @@ -188,14 +180,14 @@ def test_positive_create_with_interval(module_org, test_data): 'organization-id': module_org.id, } ) - result = SyncPlan.info({'id': new_sync_plan['id']}) + result = module_target_sat.cli.SyncPlan.info({'id': new_sync_plan['id']}) assert result['name'] == test_data['name'] assert result['interval'] == test_data['interval'] @pytest.mark.parametrize('name', **parametrized(invalid_values_list())) @pytest.mark.tier1 -def test_negative_create_with_name(module_org, name): +def test_negative_create_with_name(module_org, name, module_target_sat): """Check if syncplan can be created with random invalid names :id: 4c1aee35-271e-4ed8-9369-d2abfea8cfd9 @@ -207,12 +199,14 @@ def test_negative_create_with_name(module_org, name): :CaseImportance: Critical """ with pytest.raises(CLIFactoryError, match='Could not create the sync plan:'): - make_sync_plan({'enabled': 'false', 'name': name, 'organization-id': module_org.id}) + module_target_sat.cli_factory.sync_plan( + {'enabled': 'false', 'name': name, 'organization-id': module_org.id} + ) @pytest.mark.parametrize('new_desc', **parametrized(valid_data_list())) @pytest.mark.tier2 -def test_positive_update_description(module_org, new_desc): +def test_positive_update_description(module_org, new_desc, module_target_sat): """Check if syncplan description can be updated :id: 00a279cd-1f49-4ebb-a59a-6f0b4e4cb83c @@ -221,9 +215,11 @@ def test_positive_update_description(module_org, new_desc): :expectedresults: Sync plan is created and description is updated """ - new_sync_plan = make_sync_plan({'enabled': 'false', 'organization-id': module_org.id}) - SyncPlan.update({'description': new_desc, 'id': new_sync_plan['id']}) - result = SyncPlan.info({'id': new_sync_plan['id']}) + new_sync_plan = module_target_sat.cli_factory.sync_plan( + {'enabled': 'false', 'organization-id': module_org.id} + ) + module_target_sat.cli.SyncPlan.update({'description': new_desc, 'id': new_sync_plan['id']}) + result = module_target_sat.cli.SyncPlan.info({'id': new_sync_plan['id']}) assert result['description'] == new_desc @@ -240,7 +236,7 @@ def test_positive_update_interval(module_org, test_data, request, target_sat): :CaseImportance: Critical """ - new_sync_plan = make_sync_plan( + new_sync_plan = target_sat.cli_factory.sync_plan( { 'enabled': 'false', 'interval': test_data['interval'], @@ -250,8 +246,10 @@ def test_positive_update_interval(module_org, test_data, request, target_sat): ) sync_plan = entities.SyncPlan(organization=module_org.id, id=new_sync_plan['id']).read() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) - SyncPlan.update({'id': new_sync_plan['id'], 'interval': test_data['new-interval']}) - result = SyncPlan.info({'id': new_sync_plan['id']}) + target_sat.cli.SyncPlan.update( + {'id': new_sync_plan['id'], 'interval': test_data['new-interval']} + ) + result = target_sat.cli.SyncPlan.info({'id': new_sync_plan['id']}) assert result['interval'] == test_data['new-interval'] @@ -271,7 +269,7 @@ def test_positive_update_sync_date(module_org, request, target_sat): # Set the sync date to today/right now today = datetime.now() sync_plan_name = gen_string('alphanumeric') - new_sync_plan = make_sync_plan( + new_sync_plan = target_sat.cli_factory.sync_plan( { 'name': sync_plan_name, 'sync-date': today.strftime(SYNC_DATE_FMT), @@ -285,9 +283,11 @@ def test_positive_update_sync_date(module_org, request, target_sat): # Set sync date 5 days in the future future_date = today + timedelta(days=5) # Update sync interval - SyncPlan.update({'id': new_sync_plan['id'], 'sync-date': future_date.strftime(SYNC_DATE_FMT)}) + target_sat.cli.SyncPlan.update( + {'id': new_sync_plan['id'], 'sync-date': future_date.strftime(SYNC_DATE_FMT)} + ) # Fetch it - result = SyncPlan.info({'id': new_sync_plan['id']}) + result = target_sat.cli.SyncPlan.info({'id': new_sync_plan['id']}) assert result['start-date'] != new_sync_plan['start-date'] assert datetime.strptime(result['start-date'], '%Y/%m/%d %H:%M:%S') > datetime.strptime( new_sync_plan['start-date'], '%Y/%m/%d %H:%M:%S' @@ -297,7 +297,7 @@ def test_positive_update_sync_date(module_org, request, target_sat): @pytest.mark.parametrize('name', **parametrized(valid_data_list())) @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_delete_by_id(module_org, name): +def test_positive_delete_by_id(module_org, module_target_sat, name): """Check if syncplan can be created and deleted :id: b5d97c6b-aead-422b-8d9f-4a192bbe4a3b @@ -308,10 +308,12 @@ def test_positive_delete_by_id(module_org, name): :CaseImportance: Critical """ - new_sync_plan = make_sync_plan({'name': name, 'organization-id': module_org.id}) - SyncPlan.delete({'id': new_sync_plan['id']}) + new_sync_plan = module_target_sat.cli_factory.sync_plan( + {'name': name, 'organization-id': module_org.id} + ) + module_target_sat.cli.SyncPlan.delete({'id': new_sync_plan['id']}) with pytest.raises(CLIReturnCodeError): - SyncPlan.info({'id': new_sync_plan['id']}) + module_target_sat.cli.SyncPlan.info({'id': new_sync_plan['id']}) @pytest.mark.tier1 @@ -324,16 +326,16 @@ def test_positive_info_enabled_field_is_displayed(module_org, request, target_sa :CaseImportance: Critical """ - new_sync_plan = make_sync_plan({'organization-id': module_org.id}) + new_sync_plan = target_sat.cli_factory.sync_plan({'organization-id': module_org.id}) sync_plan = entities.SyncPlan(organization=module_org.id, id=new_sync_plan['id']).read() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) - result = SyncPlan.info({'id': new_sync_plan['id']}) + result = target_sat.cli.SyncPlan.info({'id': new_sync_plan['id']}) assert result.get('enabled') is not None @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_info_with_assigned_product(module_org): +def test_positive_info_with_assigned_product(module_org, module_target_sat): """Verify that sync plan info command returns list of products which are assigned to that sync plan @@ -352,7 +354,7 @@ def test_positive_info_with_assigned_product(module_org): """ prod1 = gen_string('alpha') prod2 = gen_string('alpha') - sync_plan = make_sync_plan( + sync_plan = module_target_sat.cli_factory.sync_plan( { 'enabled': 'false', 'organization-id': module_org.id, @@ -360,9 +362,13 @@ def test_positive_info_with_assigned_product(module_org): } ) for prod_name in [prod1, prod2]: - product = make_product({'organization-id': module_org.id, 'name': prod_name}) - Product.set_sync_plan({'id': product['id'], 'sync-plan-id': sync_plan['id']}) - updated_plan = SyncPlan.info({'id': sync_plan['id']}) + product = module_target_sat.cli_factory.make_product( + {'organization-id': module_org.id, 'name': prod_name} + ) + module_target_sat.cli.Product.set_sync_plan( + {'id': product['id'], 'sync-plan-id': sync_plan['id']} + ) + updated_plan = module_target_sat.info({'id': sync_plan['id']}) assert len(updated_plan['products']) == 2 assert {prod['name'] for prod in updated_plan['products']} == {prod1, prod2} @@ -381,7 +387,7 @@ def test_negative_synchronize_custom_product_past_sync_date(module_org, request, :CaseLevel: System """ - new_sync_plan = make_sync_plan( + new_sync_plan = target_sat.cli_factory.sync_plan( { 'enabled': 'true', 'organization-id': module_org.id, @@ -390,9 +396,9 @@ def test_negative_synchronize_custom_product_past_sync_date(module_org, request, ) sync_plan = entities.SyncPlan(organization=module_org.id, id=new_sync_plan['id']).read() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) - product = make_product({'organization-id': module_org.id}) - repo = make_repository({'product-id': product['id']}) - Product.set_sync_plan({'id': product['id'], 'sync-plan-id': new_sync_plan['id']}) + product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) + repo = target_sat.cli_factory.make_repository({'product-id': product['id']}) + target_sat.cli.Product.set_sync_plan({'id': product['id'], 'sync-plan-id': new_sync_plan['id']}) with pytest.raises(AssertionError): validate_task_status(target_sat, repo['id'], module_org.id, max_tries=2) @@ -414,9 +420,9 @@ def test_positive_synchronize_custom_product_past_sync_date(module_org, request, """ interval = 60 * 60 # 'hourly' sync interval in seconds delay = 2 * 60 - product = make_product({'organization-id': module_org.id}) - repo = make_repository({'product-id': product['id']}) - new_sync_plan = make_sync_plan( + product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) + repo = target_sat.cli_factory.make_repository({'product-id': product['id']}) + new_sync_plan = target_sat.cli_factory.sync_plan( { 'enabled': 'true', 'interval': 'hourly', @@ -429,7 +435,7 @@ def test_positive_synchronize_custom_product_past_sync_date(module_org, request, sync_plan = entities.SyncPlan(organization=module_org.id, id=new_sync_plan['id']).read() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) # Associate sync plan with product - Product.set_sync_plan({'id': product['id'], 'sync-plan-id': new_sync_plan['id']}) + target_sat.cli.Product.set_sync_plan({'id': product['id'], 'sync-plan-id': new_sync_plan['id']}) # Wait quarter of expected time logger.info( f"Waiting {(delay / 4)} seconds to check product {product['name']}" @@ -439,7 +445,7 @@ def test_positive_synchronize_custom_product_past_sync_date(module_org, request, # Verify product has not been synced yet with pytest.raises(AssertionError): validate_task_status(target_sat, repo['id'], module_org.id, max_tries=1) - validate_repo_content(repo, ['errata', 'packages'], after_sync=False) + validate_repo_content(target_sat, repo, ['errata', 'packages'], after_sync=False) # Wait until the first recurrence logger.info( f"Waiting {(delay * 3 / 4)} seconds to check product {product['name']}" @@ -448,7 +454,7 @@ def test_positive_synchronize_custom_product_past_sync_date(module_org, request, sleep(delay * 3 / 4) # Verify product was synced successfully validate_task_status(target_sat, repo['id'], module_org.id) - validate_repo_content(repo, ['errata', 'package-groups', 'packages']) + validate_repo_content(target_sat, repo, ['errata', 'package-groups', 'packages']) @pytest.mark.tier4 @@ -468,12 +474,12 @@ def test_positive_synchronize_custom_product_future_sync_date(module_org, reques cron_multiple = 5 # sync event is on every multiple of this value, starting from 00 mins delay = (cron_multiple) * 60 # delay for sync date in seconds guardtime = 180 # do not start test less than 3 mins before the next sync event - product = make_product({'organization-id': module_org.id}) - repo = make_repository({'product-id': product['id']}) + product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) + repo = target_sat.cli_factory.make_repository({'product-id': product['id']}) # if < 3 mins before the target event rather wait 3 mins for the next test window if int(datetime.utcnow().strftime('%M')) % (cron_multiple) > int(guardtime / 60): sleep(guardtime) - new_sync_plan = make_sync_plan( + new_sync_plan = target_sat.cli_factory.sync_plan( { 'enabled': 'true', 'organization-id': module_org.id, @@ -486,9 +492,9 @@ def test_positive_synchronize_custom_product_future_sync_date(module_org, reques sync_plan = entities.SyncPlan(organization=module_org.id, id=new_sync_plan['id']).read() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) # Verify product is not synced and doesn't have any content - validate_repo_content(repo, ['errata', 'packages'], after_sync=False) + validate_repo_content(target_sat, repo, ['errata', 'packages'], after_sync=False) # Associate sync plan with product - Product.set_sync_plan({'id': product['id'], 'sync-plan-id': new_sync_plan['id']}) + target_sat.cli.Product.set_sync_plan({'id': product['id'], 'sync-plan-id': new_sync_plan['id']}) # Wait quarter of expected time logger.info( f"Waiting {(delay / 4)} seconds to check product {product['name']}" @@ -498,7 +504,7 @@ def test_positive_synchronize_custom_product_future_sync_date(module_org, reques # Verify product has not been synced yet with pytest.raises(AssertionError): validate_task_status(target_sat, repo['id'], module_org.id, max_tries=1) - validate_repo_content(repo, ['errata', 'packages'], after_sync=False) + validate_repo_content(target_sat, repo, ['errata', 'packages'], after_sync=False) # Wait the rest of expected time logger.info( f"Waiting {(delay * 3 / 4)} seconds to check product {product['name']}" @@ -507,7 +513,7 @@ def test_positive_synchronize_custom_product_future_sync_date(module_org, reques sleep(delay * 3 / 4) # Verify product was synced successfully validate_task_status(target_sat, repo['id'], module_org.id) - validate_repo_content(repo, ['errata', 'package-groups', 'packages']) + validate_repo_content(target_sat, repo, ['errata', 'package-groups', 'packages']) @pytest.mark.tier4 @@ -527,14 +533,18 @@ def test_positive_synchronize_custom_products_future_sync_date(module_org, reque cron_multiple = 5 # sync event is on every multiple of this value, starting from 00 mins delay = (cron_multiple) * 60 # delay for sync date in seconds guardtime = 210 # do not start test less than 3.5 mins before the next sync event - products = [make_product({'organization-id': module_org.id}) for _ in range(2)] + products = [ + target_sat.cli_factory.make_product({'organization-id': module_org.id}) for _ in range(2) + ] repos = [ - make_repository({'product-id': product['id']}) for product in products for _ in range(2) + target_sat.cli_factory.make_repository({'product-id': product['id']}) + for product in products + for _ in range(2) ] # if < 3 mins before the target event rather wait 3 mins for the next test window if int(datetime.utcnow().strftime('%M')) % (cron_multiple) > int(guardtime / 60): sleep(guardtime) - new_sync_plan = make_sync_plan( + new_sync_plan = target_sat.cli_factory.sync_plan( { 'enabled': 'true', 'organization-id': module_org.id, @@ -556,7 +566,9 @@ def test_positive_synchronize_custom_products_future_sync_date(module_org, reque validate_task_status(target_sat, repo['id'], module_org.id, max_tries=1) # Associate sync plan with products for product in products: - Product.set_sync_plan({'id': product['id'], 'sync-plan-id': new_sync_plan['id']}) + target_sat.cli.Product.set_sync_plan( + {'id': product['id'], 'sync-plan-id': new_sync_plan['id']} + ) # Wait fifth of expected time logger.info( f"Waiting {(delay / 5)} seconds to check products {products[0]['name']}" @@ -576,7 +588,7 @@ def test_positive_synchronize_custom_products_future_sync_date(module_org, reque # Verify products were synced successfully for repo in repos: validate_task_status(target_sat, repo['id'], module_org.id) - validate_repo_content(repo, ['errata', 'package-groups', 'packages']) + validate_repo_content(target_sat, repo, ['errata', 'package-groups', 'packages']) @pytest.mark.run_in_one_thread @@ -600,7 +612,7 @@ def test_positive_synchronize_rh_product_past_sync_date( interval = 60 * 60 # 'hourly' sync interval in seconds delay = 2 * 60 org = function_entitlement_manifest_org - RepositorySet.enable( + target_sat.cli.RepositorySet.enable( { 'name': REPOSET['rhva6'], 'organization-id': org.id, @@ -609,11 +621,11 @@ def test_positive_synchronize_rh_product_past_sync_date( 'basearch': 'x86_64', } ) - product = Product.info({'name': PRDS['rhel'], 'organization-id': org.id}) - repo = Repository.info( + product = target_sat.cli.Product.info({'name': PRDS['rhel'], 'organization-id': org.id}) + repo = target_sat.cli.Repository.info( {'name': REPOS['rhva6']['name'], 'product': product['name'], 'organization-id': org.id} ) - new_sync_plan = make_sync_plan( + new_sync_plan = target_sat.cli_factory.sync_plan( { 'enabled': 'true', 'interval': 'hourly', @@ -626,7 +638,7 @@ def test_positive_synchronize_rh_product_past_sync_date( sync_plan = entities.SyncPlan(organization=org.id, id=new_sync_plan['id']).read() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) # Associate sync plan with product - Product.set_sync_plan({'id': product['id'], 'sync-plan-id': new_sync_plan['id']}) + target_sat.cli.Product.set_sync_plan({'id': product['id'], 'sync-plan-id': new_sync_plan['id']}) # Wait quarter of expected time logger.info( f"Waiting {(delay / 4)} seconds to check product {product['name']}" @@ -636,7 +648,7 @@ def test_positive_synchronize_rh_product_past_sync_date( # Verify product has not been synced yet with pytest.raises(AssertionError): validate_task_status(target_sat, repo['id'], org.id, max_tries=1) - validate_repo_content(repo, ['errata', 'packages'], after_sync=False) + validate_repo_content(target_sat, repo, ['errata', 'packages'], after_sync=False) # Wait the rest of expected time logger.info( f"Waiting {(delay * 3 / 4)} seconds to check product {product['name']}" @@ -645,7 +657,7 @@ def test_positive_synchronize_rh_product_past_sync_date( sleep(delay * 3 / 4) # Verify product was synced successfully validate_task_status(target_sat, repo['id'], org.id) - validate_repo_content(repo, ['errata', 'packages']) + validate_repo_content(target_sat, repo, ['errata', 'packages']) @pytest.mark.run_in_one_thread @@ -669,7 +681,7 @@ def test_positive_synchronize_rh_product_future_sync_date( delay = (cron_multiple) * 60 # delay for sync date in seconds guardtime = 180 # do not start test less than 2 mins before the next sync event org = function_entitlement_manifest_org - RepositorySet.enable( + target_sat.cli.RepositorySet.enable( { 'name': REPOSET['rhva6'], 'organization-id': org.id, @@ -678,14 +690,14 @@ def test_positive_synchronize_rh_product_future_sync_date( 'basearch': 'x86_64', } ) - product = Product.info({'name': PRDS['rhel'], 'organization-id': org.id}) - repo = Repository.info( + product = target_sat.cli.Product.info({'name': PRDS['rhel'], 'organization-id': org.id}) + repo = target_sat.cli.Repository.info( {'name': REPOS['rhva6']['name'], 'product': product['name'], 'organization-id': org.id} ) # if < 3 mins before the target event rather wait 3 mins for the next test window if int(datetime.utcnow().strftime('%M')) % (cron_multiple) > int(guardtime / 60): sleep(guardtime) - new_sync_plan = make_sync_plan( + new_sync_plan = target_sat.cli_factory.sync_plan( { 'enabled': 'true', 'organization-id': org.id, @@ -700,9 +712,9 @@ def test_positive_synchronize_rh_product_future_sync_date( # Verify product is not synced and doesn't have any content with pytest.raises(AssertionError): validate_task_status(target_sat, repo['id'], org.id, max_tries=1) - validate_repo_content(repo, ['errata', 'packages'], after_sync=False) + validate_repo_content(target_sat, repo, ['errata', 'packages'], after_sync=False) # Associate sync plan with product - Product.set_sync_plan({'id': product['id'], 'sync-plan-id': new_sync_plan['id']}) + target_sat.cli.Product.set_sync_plan({'id': product['id'], 'sync-plan-id': new_sync_plan['id']}) # Wait fifth of expected time logger.info( f"Waiting {(delay / 5)} seconds to check product {product['name']}" @@ -738,10 +750,10 @@ def test_positive_synchronize_custom_product_daily_recurrence(module_org, reques :CaseLevel: System """ delay = 2 * 60 - product = make_product({'organization-id': module_org.id}) - repo = make_repository({'product-id': product['id']}) + product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) + repo = target_sat.cli_factory.make_repository({'product-id': product['id']}) start_date = datetime.utcnow() - timedelta(days=1) + timedelta(seconds=delay) - new_sync_plan = make_sync_plan( + new_sync_plan = target_sat.cli_factory.sync_plan( { 'enabled': 'true', 'interval': 'daily', @@ -752,7 +764,7 @@ def test_positive_synchronize_custom_product_daily_recurrence(module_org, reques sync_plan = entities.SyncPlan(organization=module_org.id, id=new_sync_plan['id']).read() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) # Associate sync plan with product - Product.set_sync_plan({'id': product['id'], 'sync-plan-id': new_sync_plan['id']}) + target_sat.cli.Product.set_sync_plan({'id': product['id'], 'sync-plan-id': new_sync_plan['id']}) # Wait quarter of expected time logger.info( f"Waiting {(delay / 4)} seconds to check product {product['name']}" @@ -762,7 +774,7 @@ def test_positive_synchronize_custom_product_daily_recurrence(module_org, reques # Verify product has not been synced yet with pytest.raises(AssertionError): validate_task_status(target_sat, repo['id'], module_org.id, max_tries=1) - validate_repo_content(repo, ['errata', 'packages'], after_sync=False) + validate_repo_content(target_sat, repo, ['errata', 'packages'], after_sync=False) # Wait until the first recurrence logger.info( f"Waiting {(delay * 3 / 4)} seconds to check product {product['name']}" @@ -771,7 +783,7 @@ def test_positive_synchronize_custom_product_daily_recurrence(module_org, reques sleep(delay * 3 / 4) # Verify product was synced successfully validate_task_status(target_sat, repo['id'], module_org.id) - validate_repo_content(repo, ['errata', 'package-groups', 'packages']) + validate_repo_content(target_sat, repo, ['errata', 'package-groups', 'packages']) @pytest.mark.tier3 @@ -789,10 +801,10 @@ def test_positive_synchronize_custom_product_weekly_recurrence(module_org, reque :CaseLevel: System """ delay = 2 * 60 - product = make_product({'organization-id': module_org.id}) - repo = make_repository({'product-id': product['id']}) + product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) + repo = target_sat.cli_factory.make_repository({'product-id': product['id']}) start_date = datetime.utcnow() - timedelta(weeks=1) + timedelta(seconds=delay) - new_sync_plan = make_sync_plan( + new_sync_plan = target_sat.cli_factory.sync_plan( { 'enabled': 'true', 'interval': 'weekly', @@ -803,7 +815,7 @@ def test_positive_synchronize_custom_product_weekly_recurrence(module_org, reque sync_plan = entities.SyncPlan(organization=module_org.id, id=new_sync_plan['id']).read() request.addfinalizer(lambda: target_sat.api_factory.disable_syncplan(sync_plan)) # Associate sync plan with product - Product.set_sync_plan({'id': product['id'], 'sync-plan-id': new_sync_plan['id']}) + target_sat.cli.Product.set_sync_plan({'id': product['id'], 'sync-plan-id': new_sync_plan['id']}) # Wait quarter of expected time logger.info( f"Waiting {(delay / 4)} seconds to check product {product['name']}" @@ -813,7 +825,7 @@ def test_positive_synchronize_custom_product_weekly_recurrence(module_org, reque # Verify product has not been synced yet with pytest.raises(AssertionError): validate_task_status(target_sat, repo['id'], module_org.id, max_tries=1) - validate_repo_content(repo, ['errata', 'packages'], after_sync=False) + validate_repo_content(target_sat, repo, ['errata', 'packages'], after_sync=False) # Wait until the first recurrence logger.info( f"Waiting {(delay * 3 / 4)} seconds to check product {product['name']}" @@ -822,4 +834,4 @@ def test_positive_synchronize_custom_product_weekly_recurrence(module_org, reque sleep(delay * 3 / 4) # Verify product was synced successfully validate_task_status(target_sat, repo['id'], module_org.id) - validate_repo_content(repo, ['errata', 'package-groups', 'packages']) + validate_repo_content(target_sat, repo, ['errata', 'package-groups', 'packages']) diff --git a/tests/foreman/cli/test_templatesync.py b/tests/foreman/cli/test_templatesync.py index c1fa8ab41dc..2facc9139de 100644 --- a/tests/foreman/cli/test_templatesync.py +++ b/tests/foreman/cli/test_templatesync.py @@ -21,8 +21,6 @@ import pytest import requests -from robottelo.cli.template import Template -from robottelo.cli.template_sync import TemplateSync from robottelo.config import settings from robottelo.constants import ( FOREMAN_TEMPLATE_IMPORT_URL, @@ -83,7 +81,7 @@ def test_positive_import_force_locked_template( """ prefix = gen_string('alpha') _, dir_path = create_import_export_local_dir - TemplateSync.imports( + target_sat.cli.TemplateSync.imports( {'repo': dir_path, 'prefix': prefix, 'organization-ids': module_org.id, 'lock': 'true'} ) ptemplate = entities.ProvisioningTemplate().search( @@ -93,11 +91,13 @@ def test_positive_import_force_locked_template( assert ptemplate[0].read().locked update_txt = 'updated a little' target_sat.execute(f"echo {update_txt} >> {dir_path}/example_template.erb") - TemplateSync.imports( + target_sat.cli.TemplateSync.imports( {'repo': dir_path, 'prefix': prefix, 'organization-id': module_org.id} ) - assert update_txt not in Template.dump({'name': f'{prefix}example template'}) - TemplateSync.imports( + assert update_txt not in target_sat.cli.Template.dump( + {'name': f'{prefix}example template'} + ) + target_sat.cli.TemplateSync.imports( { 'repo': dir_path, 'prefix': prefix, @@ -105,7 +105,7 @@ def test_positive_import_force_locked_template( 'force': 'true', } ) - assert update_txt in Template.dump({'name': f'{prefix}example template'}) + assert update_txt in target_sat.cli.Template.dump({'name': f'{prefix}example template'}) else: pytest.fail('The template is not imported for force test') @@ -126,7 +126,9 @@ def test_positive_import_force_locked_template( indirect=True, ids=['non_empty_repo'], ) - def test_positive_update_templates_in_git(self, module_org, git_repository, git_branch, url): + def test_positive_update_templates_in_git( + self, module_org, git_repository, git_branch, url, module_target_sat + ): """Assure only templates with a given filter are pushed to git repository and existing template file is updated. @@ -159,7 +161,7 @@ def test_positive_update_templates_in_git(self, module_org, git_repository, git_ assert res.status_code == 201 # export template to git url = f'{url}/{git.username}/{git_repository["name"]}' - output = TemplateSync.exports( + output = module_target_sat.cli.TemplateSync.exports( { 'repo': url, 'branch': git_branch, @@ -192,7 +194,7 @@ def test_positive_update_templates_in_git(self, module_org, git_repository, git_ ids=['non_empty_repo', 'empty_repo'], ) def test_positive_export_filtered_templates_to_git( - self, module_org, git_repository, git_branch, url + self, module_org, git_repository, git_branch, url, module_target_sat ): """Assure only templates with a given filter regex are pushed to git repository. @@ -213,7 +215,7 @@ def test_positive_export_filtered_templates_to_git( """ dirname = 'export' url = f'{url}/{git.username}/{git_repository["name"]}' - output = TemplateSync.exports( + output = module_target_sat.cli.TemplateSync.exports( { 'repo': url, 'branch': git_branch, @@ -247,7 +249,7 @@ def test_positive_export_filtered_templates_to_temp_dir(self, module_org, target :CaseImportance: Medium """ dir_path = '/tmp' - output = TemplateSync.exports( + output = target_sat.cli.TemplateSync.exports( {'repo': dir_path, 'organization-id': module_org.id, 'filter': 'ansible'} ).split('\n') exported_count = [row == 'Exported: true' for row in output].count(True) diff --git a/tests/foreman/cli/test_user.py b/tests/foreman/cli/test_user.py index 8946837d2aa..9c9ebd67822 100644 --- a/tests/foreman/cli/test_user.py +++ b/tests/foreman/cli/test_user.py @@ -30,19 +30,9 @@ from nailgun import entities import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import ( - make_filter, - make_location, - make_org, - make_role, - make_user, -) -from robottelo.cli.filter import Filter -from robottelo.cli.org import Org -from robottelo.cli.user import User from robottelo.config import settings from robottelo.constants import LOCALES +from robottelo.exceptions import CLIReturnCodeError from robottelo.utils import gen_ssh_keypairs from robottelo.utils.datafactory import ( parametrized, @@ -56,7 +46,7 @@ class TestUser: """Implements Users tests in CLI""" @pytest.fixture(scope='module') - def module_roles(self): + def module_roles(self, module_target_sat): """ Initializes class attribute ``dct_roles`` with several random roles saved on sat. roles is a dict so keys are role's id respective value is @@ -69,14 +59,14 @@ def roles_helper(): tests """ for role_name in valid_usernames_list() + include_list: - yield make_role({'name': role_name}) + yield module_target_sat.cli_factory.make_role({'name': role_name}) stubbed_roles = {role['id']: role for role in roles_helper()} return stubbed_roles @pytest.mark.parametrize('email', **parametrized(valid_emails_list())) @pytest.mark.tier2 - def test_positive_CRUD(self, email): + def test_positive_CRUD(self, email, module_target_sat): """Create User with various parameters, updating and deleting :id: 2d430243-8512-46ee-8d21-7ccf0c7af807 @@ -99,7 +89,7 @@ def test_positive_CRUD(self, email): 'mail': mail.replace('"', r'\"').replace('`', r'\`'), 'description': random.choice(list(valid_data_list().values())), } - user = make_user(user_params) + user = module_target_sat.cli_factory.user(user_params) user['firstname'], user['lastname'] = user['name'].split() user_params.pop('mail') user_params['email'] = mail @@ -107,14 +97,18 @@ def test_positive_CRUD(self, email): assert user_params[key] == user[key], f'values for key "{key}" do not match' # list by firstname and lastname - result = User.list({'search': 'firstname = {}'.format(user_params['firstname'])}) + result = module_target_sat.cli.User.list( + {'search': 'firstname = {}'.format(user_params['firstname'])} + ) # make sure user is in list result assert {user['id'], user['login'], user['name']} == { result[0]['id'], result[0]['login'], result[0]['name'], } - result = User.list({'search': 'lastname = {}'.format(user_params['lastname'])}) + result = module_target_sat.cli.User.list( + {'search': 'lastname = {}'.format(user_params['lastname'])} + ) # make sure user is in list result assert {user['id'], user['login'], user['name']} == { result[0]['id'], @@ -130,21 +124,21 @@ def test_positive_CRUD(self, email): 'description': random.choice(list(valid_data_list().values())), } user_params.update({'id': user['id']}) - User.update(user_params) - user = User.info({'login': user['login']}) + module_target_sat.cli.User.update(user_params) + user = module_target_sat.cli.User.info({'login': user['login']}) user['firstname'], user['lastname'] = user['name'].split() user_params.pop('mail') user_params['email'] = new_mail for key in user_params: assert user_params[key] == user[key], f'values for key "{key}" do not match' # delete - User.delete({'login': user['login']}) + module_target_sat.cli.User.delete({'login': user['login']}) with pytest.raises(CLIReturnCodeError): - User.info({'login': user['login']}) + module_target_sat.cli.User.info({'login': user['login']}) @pytest.mark.tier1 @pytest.mark.upgrade - def test_positive_CRUD_admin(self): + def test_positive_CRUD_admin(self, target_sat): """Create an Admin user :id: 0d0384ad-d85a-492e-8630-7f48912a4fd5 @@ -153,23 +147,23 @@ def test_positive_CRUD_admin(self): :CaseImportance: Critical """ - user = make_user({'admin': '1'}) + user = target_sat.cli_factory.user({'admin': '1'}) assert user['admin'] == 'yes' # update to non admin by id - User.update({'id': user['id'], 'admin': '0'}) - user = User.info({'id': user['id']}) + target_sat.cli.User.update({'id': user['id'], 'admin': '0'}) + user = target_sat.cli.User.info({'id': user['id']}) assert user['admin'] == 'no' # update back to admin by name - User.update({'login': user['login'], 'admin': '1'}) - user = User.info({'login': user['login']}) + target_sat.cli.User.update({'login': user['login'], 'admin': '1'}) + user = target_sat.cli.User.info({'login': user['login']}) assert user['admin'] == 'yes' # delete user - User.delete({'login': user['login']}) + target_sat.cli.User.delete({'login': user['login']}) with pytest.raises(CLIReturnCodeError): - User.info({'id': user['id']}) + target_sat.cli.User.info({'id': user['id']}) @pytest.mark.tier1 - def test_positive_create_with_default_loc(self): + def test_positive_create_with_default_loc(self, target_sat): """Check if user with default location can be created :id: efe7256d-8c8f-444c-8d59-43500e1319c3 @@ -178,13 +172,15 @@ def test_positive_create_with_default_loc(self): :CaseImportance: Critical """ - location = make_location() - user = make_user({'default-location-id': location['id'], 'location-ids': location['id']}) + location = target_sat.cli_factory.make_location() + user = target_sat.cli_factory.user( + {'default-location-id': location['id'], 'location-ids': location['id']} + ) assert location['name'] in user['locations'] assert location['name'] == user['default-location'] @pytest.mark.tier1 - def test_positive_create_with_defaut_org(self): + def test_positive_create_with_defaut_org(self, module_target_sat): """Check if user with default organization can be created :id: cc692b6f-2519-429b-8ecb-c4bb51ed3544 @@ -194,13 +190,15 @@ def test_positive_create_with_defaut_org(self): :CaseImportance: Critical """ - org = make_org() - user = make_user({'default-organization-id': org['id'], 'organization-ids': org['id']}) + org = module_target_sat.cli_factory.make_org() + user = module_target_sat.cli_factory.user( + {'default-organization-id': org['id'], 'organization-ids': org['id']} + ) assert org['name'] in user['organizations'] assert org['name'] == user['default-organization'] @pytest.mark.tier2 - def test_positive_create_with_orgs_and_update(self): + def test_positive_create_with_orgs_and_update(self, module_target_sat): """Create User associated to multiple Organizations, update them :id: f537296c-a8a8-45ef-8996-c1d32b8f64de @@ -210,19 +208,21 @@ def test_positive_create_with_orgs_and_update(self): :CaseLevel: Integration """ orgs_amount = 2 - orgs = [make_org() for _ in range(orgs_amount)] - user = make_user({'organization-ids': [org['id'] for org in orgs]}) + orgs = [module_target_sat.cli_factory.make_org() for _ in range(orgs_amount)] + user = module_target_sat.cli_factory.user({'organization-ids': [org['id'] for org in orgs]}) assert len(user['organizations']) == orgs_amount for org in orgs: assert org['name'] in user['organizations'] - orgs = [make_org() for _ in range(orgs_amount)] - User.update({'id': user['id'], 'organization-ids': [org['id'] for org in orgs]}) - user = User.info({'id': user['id']}) + orgs = [module_target_sat.cli_factory.make_org() for _ in range(orgs_amount)] + module_target_sat.cli.User.update( + {'id': user['id'], 'organization-ids': [org['id'] for org in orgs]} + ) + user = module_target_sat.cli.User.info({'id': user['id']}) for org in orgs: assert org['name'] in user['organizations'] @pytest.mark.tier1 - def test_negative_delete_internal_admin(self): + def test_negative_delete_internal_admin(self, module_target_sat): """Attempt to delete internal admin user :id: 4fc92958-9e75-4bd2-bcbe-32f906e432f5 @@ -232,11 +232,11 @@ def test_negative_delete_internal_admin(self): :CaseImportance: Critical """ with pytest.raises(CLIReturnCodeError): - User.delete({'login': settings.server.admin_username}) - assert User.info({'login': settings.server.admin_username}) + module_target_sat.cli.User.delete({'login': settings.server.admin_username}) + assert module_target_sat.cli.User.info({'login': settings.server.admin_username}) @pytest.mark.tier2 - def test_positive_last_login_for_new_user(self): + def test_positive_last_login_for_new_user(self, module_target_sat): """Create new user with admin role and check last login updated for that user :id: 967282d3-92d0-42ce-9ef3-e542d2883408 @@ -253,17 +253,19 @@ def test_positive_last_login_for_new_user(self): password = gen_string('alpha') org_name = gen_string('alpha') - make_user({'login': login, 'password': password}) - User.add_role({'login': login, 'role': 'System admin'}) - result_before_login = User.list({'search': f'login = {login}'}) + module_target_sat.cli_factory.user({'login': login, 'password': password}) + module_target_sat.cli.User.add_role({'login': login, 'role': 'System admin'}) + result_before_login = module_target_sat.cli.User.list({'search': f'login = {login}'}) # this is because satellite uses the UTC timezone before_login_time = datetime.datetime.utcnow() assert result_before_login[0]['login'] == login assert result_before_login[0]['last-login'] == "" - Org.with_user(username=login, password=password).create({'name': org_name}) - result_after_login = User.list({'search': f'login = {login}'}) + module_target_sat.cli.Org.with_user(username=login, password=password).create( + {'name': org_name} + ) + result_after_login = module_target_sat.cli.User.list({'search': f'login = {login}'}) # checking user last login should not be empty assert result_after_login[0]['last-login'] != "" @@ -273,7 +275,7 @@ def test_positive_last_login_for_new_user(self): assert after_login_time > before_login_time @pytest.mark.tier1 - def test_positive_update_all_locales(self): + def test_positive_update_all_locales(self, module_target_sat): """Update Language in My Account :id: f0993495-5117-461d-a116-44867b820139 @@ -286,14 +288,14 @@ def test_positive_update_all_locales(self): :CaseImportance: Critical """ - user = make_user() + user = module_target_sat.cli_factory.user() for locale in LOCALES: - User.update({'id': user['id'], 'locale': locale}) - assert locale == User.info({'id': user['id']})['locale'] + module_target_sat.cli.User.update({'id': user['id'], 'locale': locale}) + assert locale == module_target_sat.cli.User.info({'id': user['id']})['locale'] @pytest.mark.tier2 @pytest.mark.upgrade - def test_positive_add_and_delete_roles(self, module_roles): + def test_positive_add_and_delete_roles(self, module_roles, module_target_sat): """Add multiple roles to User, then delete them For now add-role user sub command does not allow multiple role ids @@ -309,15 +311,15 @@ def test_positive_add_and_delete_roles(self, module_roles): :CaseLevel: Integration """ - user = make_user() + user = module_target_sat.cli_factory.user() original_role_names = set(user['roles']) expected_role_names = set(original_role_names) for role_id, role in module_roles.items(): - User.add_role({'login': user['login'], 'role-id': role_id}) + module_target_sat.cli.User.add_role({'login': user['login'], 'role-id': role_id}) expected_role_names.add(role['name']) - user_roles = User.info({'id': user['id']})['roles'] + user_roles = module_target_sat.cli.User.info({'id': user['id']})['roles'] assert len(expected_role_names) == len(user_roles) for role in expected_role_names: assert role in user_roles @@ -325,11 +327,11 @@ def test_positive_add_and_delete_roles(self, module_roles): roles_to_remove = expected_role_names - original_role_names for role_name in roles_to_remove: user_credentials = {'login': user['login'], 'role': role_name} - User.remove_role(user_credentials) - user = User.info({'id': user['id']}) + module_target_sat.cli.User.remove_role(user_credentials) + user = module_target_sat.cli.User.info({'id': user['id']}) assert role_name not in user['roles'] - user_roles = User.info({'id': user['id']})['roles'] + user_roles = module_target_sat.cli.User.info({'id': user['id']})['roles'] assert len(original_role_names) == len(user_roles) for role in original_role_names: assert role in user_roles @@ -346,7 +348,7 @@ def module_user(self): return entities.User().create() @pytest.mark.tier1 - def test_positive_CRD_ssh_key(self, module_user): + def test_positive_CRD_ssh_key(self, module_user, module_target_sat): """SSH Key can be added to a User, listed and deletd :id: 57304fca-8e0d-454a-be31-34423345c8b2 @@ -357,13 +359,19 @@ def test_positive_CRD_ssh_key(self, module_user): :CaseImportance: Critical """ ssh_name = gen_string('alpha') - User.ssh_keys_add({'user': module_user.login, 'key': self.ssh_key, 'name': ssh_name}) - result = User.ssh_keys_list({'user-id': module_user.id}) + module_target_sat.cli.User.ssh_keys_add( + {'user': module_user.login, 'key': self.ssh_key, 'name': ssh_name} + ) + result = module_target_sat.cli.User.ssh_keys_list({'user-id': module_user.id}) assert ssh_name in [i['name'] for i in result] - result = User.ssh_keys_info({'user-id': module_user.id, 'name': ssh_name}) + result = module_target_sat.cli.User.ssh_keys_info( + {'user-id': module_user.id, 'name': ssh_name} + ) assert self.ssh_key in result[0]['public-key'] - result = User.ssh_keys_delete({'user-id': module_user.id, 'name': ssh_name}) - result = User.ssh_keys_list({'user-id': module_user.id}) + result = module_target_sat.cli.User.ssh_keys_delete( + {'user-id': module_user.id, 'name': ssh_name} + ) + result = module_target_sat.cli.User.ssh_keys_list({'user-id': module_user.id}) assert ssh_name not in [i['name'] for i in result] @pytest.mark.tier1 @@ -380,10 +388,12 @@ def test_positive_create_ssh_key_super_admin_from_file(self, target_sat): ssh_name = gen_string('alpha') result = target_sat.execute(f"echo '{self.ssh_key}' > test_key.pub") assert result.status == 0, 'key file not created' - User.ssh_keys_add({'user': 'admin', 'key-file': 'test_key.pub', 'name': ssh_name}) - result = User.ssh_keys_list({'user': 'admin'}) + target_sat.cli.User.ssh_keys_add( + {'user': 'admin', 'key-file': 'test_key.pub', 'name': ssh_name} + ) + result = target_sat.cli.User.ssh_keys_list({'user': 'admin'}) assert ssh_name in [i['name'] for i in result] - result = User.ssh_keys_info({'user': 'admin', 'name': ssh_name}) + result = target_sat.cli.User.ssh_keys_info({'user': 'admin', 'name': ssh_name}) assert self.ssh_key == result[0]['public-key'] @@ -409,9 +419,9 @@ def test_personal_access_token_admin_user(self, target_sat): :CaseImportance: High """ - user = make_user({'admin': '1'}) + user = target_sat.cli_factory.user({'admin': '1'}) token_name = gen_alphanumeric() - result = User.access_token( + result = target_sat.cli.User.access_token( action="create", options={'name': token_name, 'user-id': user['id']} ) token_value = result[0]['message'].split(':')[-1] @@ -419,7 +429,9 @@ def test_personal_access_token_admin_user(self, target_sat): command_output = target_sat.execute(curl_command) assert user['login'] in command_output.stdout assert user['email'] in command_output.stdout - User.access_token(action="revoke", options={'name': token_name, 'user-id': user['id']}) + target_sat.cli.User.access_token( + action="revoke", options={'name': token_name, 'user-id': user['id']} + ) command_output = target_sat.execute(curl_command) assert f'Unable to authenticate user {user["login"]}' in command_output.stdout @@ -445,10 +457,10 @@ def test_positive_personal_access_token_user_with_role(self, target_sat): :CaseImportance: High """ - user = make_user() - User.add_role({'login': user['login'], 'role': 'Viewer'}) + user = target_sat.cli_factory.user() + target_sat.cli.User.add_role({'login': user['login'], 'role': 'Viewer'}) token_name = gen_alphanumeric() - result = User.access_token( + result = target_sat.cli.User.access_token( action="create", options={'name': token_name, 'user-id': user['id']} ) token_value = result[0]['message'].split(':')[-1] @@ -479,13 +491,13 @@ def test_expired_personal_access_token(self, target_sat): :CaseImportance: Medium """ - user = make_user() - User.add_role({'login': user['login'], 'role': 'Viewer'}) + user = target_sat.cli_factory.user() + target_sat.cli.User.add_role({'login': user['login'], 'role': 'Viewer'}) token_name = gen_alphanumeric() datetime_now = datetime.datetime.utcnow() datetime_expire = datetime_now + datetime.timedelta(seconds=20) datetime_expire = datetime_expire.strftime("%Y-%m-%d %H:%M:%S") - result = User.access_token( + result = target_sat.cli.User.access_token( action="create", options={'name': token_name, 'user-id': user['id'], 'expires-at': datetime_expire}, ) @@ -521,20 +533,20 @@ def test_custom_personal_access_token_role(self, target_sat): :BZ: 1974685, 1996048 """ - role = make_role() + role = target_sat.cli_factory.make_role() permissions = [ permission['name'] - for permission in Filter.available_permissions( + for permission in target_sat.cli.Filter.available_permissions( {'search': 'resource_type=PersonalAccessToken'} ) ] permissions = ','.join(permissions) - make_filter({'role-id': role['id'], 'permissions': permissions}) - make_filter({'role-id': role['id'], 'permissions': 'view_users'}) - user = make_user() - User.add_role({'login': user['login'], 'role': role['name']}) + target_sat.cli_factory.make_filter({'role-id': role['id'], 'permissions': permissions}) + target_sat.cli_factory.make_filter({'role-id': role['id'], 'permissions': 'view_users'}) + user = target_sat.cli_factory.user() + target_sat.cli.User.add_role({'login': user['login'], 'role': role['name']}) token_name = gen_alphanumeric() - result = User.access_token( + result = target_sat.cli.User.access_token( action="create", options={'name': token_name, 'user-id': user['id']} ) token_value = result[0]['message'].split(':')[-1] @@ -543,7 +555,9 @@ def test_custom_personal_access_token_role(self, target_sat): ) assert user['login'] in command_output.stdout assert user['email'] in command_output.stdout - User.access_token(action="revoke", options={'name': token_name, 'user-id': user['id']}) + target_sat.cli.User.access_token( + action="revoke", options={'name': token_name, 'user-id': user['id']} + ) command_output = target_sat.execute( f'curl -k -u {user["login"]}:{token_value} {target_sat.url}/api/v2/users' ) diff --git a/tests/foreman/cli/test_usergroup.py b/tests/foreman/cli/test_usergroup.py index 61feba18d2f..81e412de415 100644 --- a/tests/foreman/cli/test_usergroup.py +++ b/tests/foreman/cli/test_usergroup.py @@ -20,29 +20,19 @@ import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.factory import ( - make_role, - make_user, - make_usergroup, - make_usergroup_external, -) -from robottelo.cli.ldapauthsource import LDAPAuthSource -from robottelo.cli.task import Task -from robottelo.cli.user import User -from robottelo.cli.usergroup import UserGroup, UserGroupExternal +from robottelo.exceptions import CLIReturnCodeError from robottelo.utils.datafactory import valid_usernames_list @pytest.fixture -def function_user_group(): +def function_user_group(target_sat): """Create new usergroup per each test""" - user_group = make_usergroup() + user_group = target_sat.cli_factory.usergroup() return user_group @pytest.mark.tier1 -def test_positive_CRUD(): +def test_positive_CRUD(module_target_sat): """Create new user group with valid elements that attached group. List the user group, update and delete it. @@ -53,14 +43,14 @@ def test_positive_CRUD(): :CaseImportance: Critical """ - user = make_user() + user = module_target_sat.cli_factory.user() ug_name = random.choice(valid_usernames_list()) role_name = random.choice(valid_usernames_list()) - role = make_role({'name': role_name}) - sub_user_group = make_usergroup() + role = module_target_sat.cli_factory.make_role({'name': role_name}) + sub_user_group = module_target_sat.cli_factory.usergroup() # Create - user_group = make_usergroup( + user_group = module_target_sat.cli_factory.usergroup( { 'user-ids': user['id'], 'name': ug_name, @@ -76,24 +66,26 @@ def test_positive_CRUD(): assert user_group['user-groups'][0]['usergroup'] == sub_user_group['name'] # List - result_list = UserGroup.list({'search': 'name={}'.format(user_group['name'])}) + result_list = module_target_sat.cli.UserGroup.list( + {'search': 'name={}'.format(user_group['name'])} + ) assert len(result_list) > 0 - assert UserGroup.exists(search=('name', user_group['name'])) + assert module_target_sat.cli.UserGroup.exists(search=('name', user_group['name'])) # Update new_name = random.choice(valid_usernames_list()) - UserGroup.update({'id': user_group['id'], 'new-name': new_name}) - user_group = UserGroup.info({'id': user_group['id']}) + module_target_sat.cli.UserGroup.update({'id': user_group['id'], 'new-name': new_name}) + user_group = module_target_sat.cli.UserGroup.info({'id': user_group['id']}) assert user_group['name'] == new_name # Delete - UserGroup.delete({'name': user_group['name']}) + module_target_sat.cli.UserGroup.delete({'name': user_group['name']}) with pytest.raises(CLIReturnCodeError): - UserGroup.info({'name': user_group['name']}) + module_target_sat.cli.UserGroup.info({'name': user_group['name']}) @pytest.mark.tier1 -def test_positive_create_with_multiple_elements(): +def test_positive_create_with_multiple_elements(module_target_sat): """Create new user group using multiple users, roles and user groups attached to that group. @@ -105,17 +97,19 @@ def test_positive_create_with_multiple_elements(): :CaseImportance: Critical """ count = 2 - users = [make_user()['login'] for _ in range(count)] - roles = [make_role()['name'] for _ in range(count)] - sub_user_groups = [make_usergroup()['name'] for _ in range(count)] - user_group = make_usergroup({'users': users, 'roles': roles, 'user-groups': sub_user_groups}) + users = [module_target_sat.cli_factory.user()['login'] for _ in range(count)] + roles = [module_target_sat.cli_factory.make_role()['name'] for _ in range(count)] + sub_user_groups = [module_target_sat.cli_factory.usergroup()['name'] for _ in range(count)] + user_group = module_target_sat.cli_factory.usergroup( + {'users': users, 'roles': roles, 'user-groups': sub_user_groups} + ) assert sorted(users) == sorted(user_group['users']) assert sorted(roles) == sorted(user_group['roles']) assert sorted(sub_user_groups) == sorted(ug['usergroup'] for ug in user_group['user-groups']) @pytest.mark.tier2 -def test_positive_add_and_remove_elements(): +def test_positive_add_and_remove_elements(module_target_sat): """Create new user group. Add and remove several element from the group. :id: a4ce8724-d3c8-4c00-9421-aaa40394134d @@ -127,17 +121,19 @@ def test_positive_add_and_remove_elements(): :CaseLevel: Integration """ - role = make_role() - user_group = make_usergroup() - user = make_user() - sub_user_group = make_usergroup() + role = module_target_sat.cli_factory.make_role() + user_group = module_target_sat.cli_factory.usergroup() + user = module_target_sat.cli_factory.user() + sub_user_group = module_target_sat.cli_factory.usergroup() # Add elements by id - UserGroup.add_role({'id': user_group['id'], 'role-id': role['id']}) - UserGroup.add_user({'id': user_group['id'], 'user-id': user['id']}) - UserGroup.add_user_group({'id': user_group['id'], 'user-group-id': sub_user_group['id']}) + module_target_sat.cli.UserGroup.add_role({'id': user_group['id'], 'role-id': role['id']}) + module_target_sat.cli.UserGroup.add_user({'id': user_group['id'], 'user-id': user['id']}) + module_target_sat.cli.UserGroup.add_user_group( + {'id': user_group['id'], 'user-group-id': sub_user_group['id']} + ) - user_group = UserGroup.info({'id': user_group['id']}) + user_group = module_target_sat.cli.UserGroup.info({'id': user_group['id']}) assert len(user_group['roles']) == 1 assert user_group['roles'][0] == role['name'] assert len(user_group['users']) == 1 @@ -146,11 +142,13 @@ def test_positive_add_and_remove_elements(): assert user_group['user-groups'][0]['usergroup'] == sub_user_group['name'] # Remove elements by name - UserGroup.remove_role({'id': user_group['id'], 'role': role['name']}) - UserGroup.remove_user({'id': user_group['id'], 'user': user['login']}) - UserGroup.remove_user_group({'id': user_group['id'], 'user-group': sub_user_group['name']}) + module_target_sat.cli.UserGroup.remove_role({'id': user_group['id'], 'role': role['name']}) + module_target_sat.cli.UserGroup.remove_user({'id': user_group['id'], 'user': user['login']}) + module_target_sat.cli.UserGroup.remove_user_group( + {'id': user_group['id'], 'user-group': sub_user_group['name']} + ) - user_group = UserGroup.info({'id': user_group['id']}) + user_group = module_target_sat.cli.UserGroup.info({'id': user_group['id']}) assert len(user_group['roles']) == 0 assert len(user_group['users']) == 0 assert len(user_group['user-groups']) == 0 @@ -158,7 +156,7 @@ def test_positive_add_and_remove_elements(): @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_remove_user_assigned_to_usergroup(): +def test_positive_remove_user_assigned_to_usergroup(module_target_sat): """Create new user and assign it to user group. Then remove that user. :id: 2a2623ce-4723-4402-aae7-8675473fd8bd @@ -171,17 +169,17 @@ def test_positive_remove_user_assigned_to_usergroup(): :BZ: 1667704 """ - user = make_user() - user_group = make_usergroup() - UserGroup.add_user({'id': user_group['id'], 'user-id': user['id']}) - User.delete({'id': user['id']}) - user_group = UserGroup.info({'id': user_group['id']}) + user = module_target_sat.cli_factory.user() + user_group = module_target_sat.cli_factory.usergroup() + module_target_sat.cli.UserGroup.add_user({'id': user_group['id'], 'user-id': user['id']}) + module_target_sat.cli.User.delete({'id': user['id']}) + user_group = module_target_sat.cli.UserGroup.info({'id': user_group['id']}) assert user['login'] not in user_group['users'] @pytest.mark.tier2 @pytest.mark.parametrize("ldap_auth_source", ["AD"], indirect=True) -def test_positive_automate_bz1426957(ldap_auth_source, function_user_group): +def test_positive_automate_bz1426957(ldap_auth_source, function_user_group, target_sat): """Verify role is properly reflected on AD user. :id: 1c1209a6-5bb8-489c-a151-bb2fce4dbbfc @@ -196,7 +194,7 @@ def test_positive_automate_bz1426957(ldap_auth_source, function_user_group): :BZ: 1426957, 1667704 """ - ext_user_group = make_usergroup_external( + ext_user_group = target_sat.cli_factory.usergroup_external( { 'auth-source-id': ldap_auth_source[1].id, 'user-group-id': function_user_group['id'], @@ -204,23 +202,26 @@ def test_positive_automate_bz1426957(ldap_auth_source, function_user_group): } ) assert ext_user_group['auth-source'] == ldap_auth_source[1].name - role = make_role() - UserGroup.add_role({'id': function_user_group['id'], 'role-id': role['id']}) - Task.with_user( + role = target_sat.cli_factory.make_role() + target_sat.cli.UserGroup.add_role({'id': function_user_group['id'], 'role-id': role['id']}) + target_sat.cli.Task.with_user( username=ldap_auth_source[0]['ldap_user_name'], password=ldap_auth_source[0]['ldap_user_passwd'], ).list() - UserGroupExternal.refresh({'user-group-id': function_user_group['id'], 'name': 'foobargroup'}) + target_sat.cli.UserGroupExternal.refresh( + {'user-group-id': function_user_group['id'], 'name': 'foobargroup'} + ) assert ( - role['name'] in User.info({'login': ldap_auth_source[0]['ldap_user_name']})['user-groups'] + role['name'] + in target_sat.cli.User.info({'login': ldap_auth_source[0]['ldap_user_name']})['user-groups'] ) - User.delete({'login': ldap_auth_source[0]['ldap_user_name']}) - LDAPAuthSource.delete({'id': ldap_auth_source[1].id}) + target_sat.cli.User.delete({'login': ldap_auth_source[0]['ldap_user_name']}) + target_sat.cli.LDAPAuthSource.delete({'id': ldap_auth_source[1].id}) @pytest.mark.tier2 @pytest.mark.parametrize("ldap_auth_source", ["AD"], indirect=True) -def test_negative_automate_bz1437578(ldap_auth_source, function_user_group): +def test_negative_automate_bz1437578(ldap_auth_source, function_user_group, module_target_sat): """Verify error message on usergroup create with 'Domain Users' on AD user. :id: d4caf33e-b9eb-4281-9e04-fbe1d5b035dc @@ -234,7 +235,7 @@ def test_negative_automate_bz1437578(ldap_auth_source, function_user_group): :BZ: 1437578 """ with pytest.raises(CLIReturnCodeError): - result = UserGroupExternal.create( + result = module_target_sat.cli.UserGroupExternal.create( { 'auth-source-id': ldap_auth_source[1].id, 'user-group-id': function_user_group['id'], diff --git a/tests/foreman/cli/test_vm_install_products_package.py b/tests/foreman/cli/test_vm_install_products_package.py index 1e16997457d..53777235341 100644 --- a/tests/foreman/cli/test_vm_install_products_package.py +++ b/tests/foreman/cli/test_vm_install_products_package.py @@ -19,7 +19,6 @@ from broker import Broker import pytest -from robottelo.cli.factory import make_lifecycle_environment from robottelo.config import settings from robottelo.constants import ( CONTAINER_REGISTRY_HUB, @@ -31,8 +30,10 @@ @pytest.fixture -def lce(function_entitlement_manifest_org): - return make_lifecycle_environment({'organization-id': function_entitlement_manifest_org.id}) +def lce(function_entitlement_manifest_org, target_sat): + return target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': function_entitlement_manifest_org.id} + ) @pytest.mark.tier4 diff --git a/tests/foreman/cli/test_webhook.py b/tests/foreman/cli/test_webhook.py index 29c08189449..516df792b63 100644 --- a/tests/foreman/cli/test_webhook.py +++ b/tests/foreman/cli/test_webhook.py @@ -23,13 +23,12 @@ from fauxfactory import gen_alphanumeric import pytest -from robottelo.cli.base import CLIReturnCodeError -from robottelo.cli.webhook import Webhook from robottelo.constants import WEBHOOK_EVENTS, WEBHOOK_METHODS +from robottelo.exceptions import CLIReturnCodeError @pytest.fixture -def webhook_factory(request, class_org, class_location): +def webhook_factory(request, class_org, class_location, class_target_sat): def _create_webhook(org, loc, options=None): """Function for creating a new Webhook @@ -49,7 +48,7 @@ def _create_webhook(org, loc, options=None): if options.get('target-url') is None: options['target-url'] = 'http://localhost/some-path' - return Box(Webhook.create(options)) + return Box(class_target_sat.cli.Webhook.create(options)) return partial(_create_webhook, org=class_org, loc=class_location) @@ -63,7 +62,7 @@ def assert_created(options, hook): class TestWebhook: @pytest.mark.tier3 @pytest.mark.e2e - def test_positive_end_to_end(self, webhook_factory): + def test_positive_end_to_end(self, webhook_factory, class_target_sat): """Test creation, list, update and removal of webhook :id: d893d176-cbe9-421b-8631-7c7a1a462ea5 @@ -81,22 +80,28 @@ def test_positive_end_to_end(self, webhook_factory): assert webhook_options['event'] == webhook_item['event'].rsplit('.', 2)[0] # Find webhook by name - webhook_search = Webhook.info({'name': webhook_options['name']}) + webhook_search = class_target_sat.cli.Webhook.info({'name': webhook_options['name']}) # A non empty dict has been returned assert webhook_search # Test that webhook gets updated different_url = 'http://localhost/different-path' - Webhook.update({'name': webhook_options['name'], 'target-url': different_url}) - webhook_search_after_update = Webhook.info({'name': webhook_options['name']}) + class_target_sat.cli.Webhook.update( + {'name': webhook_options['name'], 'target-url': different_url} + ) + webhook_search_after_update = class_target_sat.cli.Webhook.info( + {'name': webhook_options['name']} + ) assert webhook_search_after_update['target-url'] == different_url # Test that webhook is deleted - Webhook.delete({'name': webhook_options['name']}) - webhook_deleted_search = Webhook.list({'search': webhook_options['name']}) + class_target_sat.cli.Webhook.delete({'name': webhook_options['name']}) + webhook_deleted_search = class_target_sat.cli.Webhook.list( + {'search': webhook_options['name']} + ) assert len(webhook_deleted_search) == 0 - def test_webhook_disabled_enabled(self, webhook_factory): + def test_webhook_disabled_enabled(self, webhook_factory, class_target_sat): """Test disable/enable the webhook :id: 4fef4320-0655-440d-90e7-150ffcdcd043 @@ -106,17 +111,17 @@ def test_webhook_disabled_enabled(self, webhook_factory): hook = webhook_factory() # The new webhook is enabled by default on creation - assert Webhook.info({'name': hook.name})['enabled'] == 'yes' + assert class_target_sat.cli.Webhook.info({'name': hook.name})['enabled'] == 'yes' - Webhook.update({'name': hook.name, 'enabled': 'no'}) + class_target_sat.cli.Webhook.update({'name': hook.name, 'enabled': 'no'}) # The webhook should be disabled now - assert Webhook.info({'name': hook.name})['enabled'] == 'no' + assert class_target_sat.cli.Webhook.info({'name': hook.name})['enabled'] == 'no' - Webhook.update({'name': hook.name, 'enabled': 'yes'}) + class_target_sat.cli.Webhook.update({'name': hook.name, 'enabled': 'yes'}) # The webhook should be enabled again - assert Webhook.info({'name': hook.name})['enabled'] == 'yes' + assert class_target_sat.cli.Webhook.info({'name': hook.name})['enabled'] == 'yes' - def test_negative_update_invalid_url(self, webhook_factory): + def test_negative_update_invalid_url(self, webhook_factory, class_target_sat): """Test webhook negative update - invalid target URL fails :id: 7a6c87f5-0e6c-4a55-b495-b1bfb24607bd @@ -129,4 +134,4 @@ def test_negative_update_invalid_url(self, webhook_factory): invalid_url = '$%^##@***' with pytest.raises(CLIReturnCodeError): - Webhook.update({'name': hook.name, 'target-url': invalid_url}) + class_target_sat.cli.Webhook.update({'name': hook.name, 'target-url': invalid_url}) diff --git a/tests/foreman/destructive/test_ldap_authentication.py b/tests/foreman/destructive/test_ldap_authentication.py index 65ef31aa2cf..bc35a10bd83 100644 --- a/tests/foreman/destructive/test_ldap_authentication.py +++ b/tests/foreman/destructive/test_ldap_authentication.py @@ -23,9 +23,9 @@ import pyotp import pytest -from robottelo.cli.base import CLIReturnCodeError from robottelo.config import settings from robottelo.constants import CERT_PATH, HAMMER_CONFIG, HAMMER_SESSIONS, LDAP_ATTR +from robottelo.exceptions import CLIReturnCodeError from robottelo.logging import logger from robottelo.utils.datafactory import gen_string @@ -475,9 +475,9 @@ def test_user_permissions_rhsso_user_after_group_delete( default_sso_host.update_rhsso_user(username, group_name=group_name) # creating satellite external group - user_group = module_target_sat.cli_factory.make_usergroup({'admin': 1, 'name': group_name}) + user_group = module_target_sat.cli_factory.usergroup({'admin': 1, 'name': group_name}) external_auth_source = module_target_sat.cli.ExternalAuthSource.info({'name': "External"}) - module_target_sat.cli_factory.make_usergroup_external( + module_target_sat.cli_factory.usergroup_external( { 'auth-source-id': external_auth_source['id'], 'user-group-id': user_group['id'], @@ -555,8 +555,8 @@ def test_user_permissions_rhsso_user_multiple_group( argument['name'] = group_name # creating satellite external groups - user_group = module_target_sat.cli_factory.make_usergroup(argument) - module_target_sat.cli_factory.make_usergroup_external( + user_group = module_target_sat.cli_factory.usergroup(argument) + module_target_sat.cli_factory.usergroup_external( { 'auth-source-id': external_auth_source['id'], 'user-group-id': user_group['id'], diff --git a/tests/foreman/destructive/test_ldapauthsource.py b/tests/foreman/destructive/test_ldapauthsource.py index 58e3ceb3e84..03e09384db8 100644 --- a/tests/foreman/destructive/test_ldapauthsource.py +++ b/tests/foreman/destructive/test_ldapauthsource.py @@ -20,9 +20,9 @@ import pytest -from robottelo.cli.base import CLIReturnCodeError from robottelo.config import settings from robottelo.constants import HAMMER_CONFIG +from robottelo.exceptions import CLIReturnCodeError pytestmark = [pytest.mark.destructive] diff --git a/tests/foreman/destructive/test_realm.py b/tests/foreman/destructive/test_realm.py index de5a9b1ebc3..6d429e483c0 100644 --- a/tests/foreman/destructive/test_realm.py +++ b/tests/foreman/destructive/test_realm.py @@ -21,7 +21,7 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.base import CLIReturnCodeError +from robottelo.exceptions import CLIReturnCodeError pytestmark = [pytest.mark.run_in_one_thread, pytest.mark.destructive] @@ -39,7 +39,7 @@ def test_positive_delete_by_name( :expectedresults: Realm is deleted """ - realm = module_target_sat.cli_factory.make_realm( + realm = module_target_sat.cli_factory.realm( {'realm-proxy-id': module_fake_proxy.id, 'realm-type': 'Active Directory'} ) module_target_sat.cli.Realm.delete({'name': realm['name']}) @@ -60,7 +60,7 @@ def test_positive_delete_by_id( :expectedresults: Realm is deleted """ - realm = module_target_sat.cli_factory.make_realm( + realm = module_target_sat.cli_factory.realm( {'realm-proxy-id': module_fake_proxy.id, 'realm-type': 'Active Directory'} ) module_target_sat.cli.Realm.delete({'id': realm['id']}) @@ -82,7 +82,7 @@ def test_positive_realm_info_name( :expectedresults: Realm information obtained by name is correct """ - realm = module_target_sat.cli_factory.make_realm( + realm = module_target_sat.cli_factory.realm( { 'name': gen_string('alpha', random.randint(1, 30)), 'realm-proxy-id': module_fake_proxy.id, @@ -110,7 +110,7 @@ def test_positive_realm_info_id( :expectedresults: Realm information obtained by ID is correct """ - realm = module_target_sat.cli_factory.make_realm( + realm = module_target_sat.cli_factory.realm( { 'name': gen_string('alpha', random.randint(1, 30)), 'realm-proxy-id': module_fake_proxy.id, @@ -142,7 +142,7 @@ def test_positive_realm_update_name( """ realm_name = gen_string('alpha', random.randint(1, 30)) new_realm_name = gen_string('alpha', random.randint(1, 30)) - realm = module_target_sat.cli_factory.make_realm( + realm = module_target_sat.cli_factory.realm( { 'name': realm_name, 'realm-proxy-id': module_fake_proxy.id, @@ -174,7 +174,7 @@ def test_negative_realm_update_invalid_type( """ realm_type = 'Red Hat Identity Management' new_realm_type = gen_string('alpha') - realm = module_target_sat.cli_factory.make_realm( + realm = module_target_sat.cli_factory.realm( { 'name': gen_string('alpha', random.randint(1, 30)), 'realm-proxy-id': module_fake_proxy.id, diff --git a/tests/foreman/destructive/test_remoteexecution.py b/tests/foreman/destructive/test_remoteexecution.py index 83962c4bf18..23cee12cff1 100644 --- a/tests/foreman/destructive/test_remoteexecution.py +++ b/tests/foreman/destructive/test_remoteexecution.py @@ -121,7 +121,7 @@ def test_positive_use_alternate_directory( assert result.status == 0 command = f'echo {gen_string("alpha")}' - invocation_command = target_sat.cli_factory.make_job_invocation( + invocation_command = target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': f'command={command}', diff --git a/tests/foreman/endtoend/test_cli_endtoend.py b/tests/foreman/endtoend/test_cli_endtoend.py index 0001e482043..2a63dd53924 100644 --- a/tests/foreman/endtoend/test_cli_endtoend.py +++ b/tests/foreman/endtoend/test_cli_endtoend.py @@ -20,22 +20,6 @@ import pytest from robottelo import constants -from robottelo.cli.activationkey import ActivationKey -from robottelo.cli.computeresource import ComputeResource -from robottelo.cli.contentview import ContentView -from robottelo.cli.domain import Domain -from robottelo.cli.factory import make_user -from robottelo.cli.host import Host -from robottelo.cli.hostgroup import HostGroup -from robottelo.cli.lifecycleenvironment import LifecycleEnvironment -from robottelo.cli.location import Location -from robottelo.cli.org import Org -from robottelo.cli.product import Product -from robottelo.cli.repository import Repository -from robottelo.cli.repository_set import RepositorySet -from robottelo.cli.subnet import Subnet -from robottelo.cli.subscription import Subscription -from robottelo.cli.user import User from robottelo.config import setting_is_set, settings from robottelo.constants.repos import CUSTOM_RPM_REPO @@ -47,40 +31,40 @@ def fake_manifest_is_set(): @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_cli_find_default_org(): +def test_positive_cli_find_default_org(module_target_sat): """Check if 'Default Organization' is present :id: 95ffeb7a-134e-4273-bccc-fe8a3a336b2a :expectedresults: 'Default Organization' is found """ - result = Org.info({'name': constants.DEFAULT_ORG}) + result = module_target_sat.cli.Org.info({'name': constants.DEFAULT_ORG}) assert result['name'] == constants.DEFAULT_ORG @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_cli_find_default_loc(): +def test_positive_cli_find_default_loc(module_target_sat): """Check if 'Default Location' is present :id: 11cf0d06-78ff-47e8-9d50-407a2ea31988 :expectedresults: 'Default Location' is found """ - result = Location.info({'name': constants.DEFAULT_LOC}) + result = module_target_sat.cli.Location.info({'name': constants.DEFAULT_LOC}) assert result['name'] == constants.DEFAULT_LOC @pytest.mark.tier1 @pytest.mark.upgrade -def test_positive_cli_find_admin_user(): +def test_positive_cli_find_admin_user(module_target_sat): """Check if Admin User is present :id: f6755189-05a6-4d2f-a3b8-98be0cfacaee :expectedresults: Admin User is found and has Admin role """ - result = User.info({'login': 'admin'}) + result = module_target_sat.cli.User.info({'login': 'admin'}) assert result['login'] == 'admin' assert result['admin'] == 'yes' @@ -126,34 +110,36 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel """ # step 1: Create a new user with admin permissions password = gen_alphanumeric() - user = make_user({'admin': 'true', 'password': password}) + user = target_sat.cli_factory.user({'admin': 'true', 'password': password}) user['password'] = password # step 2.1: Create a new organization - org = _create(user, Org, {'name': gen_alphanumeric()}) + org = _create(user, target_sat.cli.Org, {'name': gen_alphanumeric()}) target_sat.cli.SimpleContentAccess.disable({'organization-id': org['id']}) # step 2.2: Clone and upload manifest target_sat.put(f'{function_entitlement_manifest.path}', f'{function_entitlement_manifest.name}') - Subscription.upload( + target_sat.cli.Subscription.upload( {'file': f'{function_entitlement_manifest.name}', 'organization-id': org['id']} ) # step 2.3: Create a new lifecycle environment lifecycle_environment = _create( user, - LifecycleEnvironment, + target_sat.cli.LifecycleEnvironment, {'name': gen_alphanumeric(), 'organization-id': org['id'], 'prior': 'Library'}, ) # step 2.4: Create a custom product - product = _create(user, Product, {'name': gen_alphanumeric(), 'organization-id': org['id']}) + product = _create( + user, target_sat.cli.Product, {'name': gen_alphanumeric(), 'organization-id': org['id']} + ) repositories = [] # step 2.5: Create custom YUM repository custom_repo = _create( user, - Repository, + target_sat.cli.Repository, { 'content-type': 'yum', 'name': gen_alphanumeric(), @@ -165,7 +151,7 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel repositories.append(custom_repo) # step 2.6: Enable a Red Hat repository - RepositorySet.enable( + target_sat.cli.RepositorySet.enable( { 'basearch': 'x86_64', 'name': constants.REPOSET['rhst7'], @@ -174,7 +160,7 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel 'releasever': None, } ) - rhel_repo = Repository.info( + rhel_repo = target_sat.cli.Repository.info( { 'name': constants.REPOS['rhst7']['name'], 'organization-id': org['id'], @@ -185,16 +171,18 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel # step 2.7: Synchronize these two repositories for repo in repositories: - Repository.with_user(user['login'], user['password']).synchronize({'id': repo['id']}) + target_sat.cli.Repository.with_user(user['login'], user['password']).synchronize( + {'id': repo['id']} + ) # step 2.8: Create content view content_view = _create( - user, ContentView, {'name': gen_alphanumeric(), 'organization-id': org['id']} + user, target_sat.cli.ContentView, {'name': gen_alphanumeric(), 'organization-id': org['id']} ) # step 2.9: Associate the YUM and Red Hat repositories to new content view for repo in repositories: - ContentView.add_repository( + target_sat.cli.ContentView.add_repository( { 'id': content_view['id'], 'organization-id': org['id'], @@ -203,26 +191,28 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel ) # step 2.10: Publish content view - ContentView.with_user(user['login'], user['password']).publish({'id': content_view['id']}) + target_sat.cli.ContentView.with_user(user['login'], user['password']).publish( + {'id': content_view['id']} + ) # step 2.11: Promote content view to the lifecycle environment - content_view = ContentView.with_user(user['login'], user['password']).info( + content_view = target_sat.cli.ContentView.with_user(user['login'], user['password']).info( {'id': content_view['id']} ) assert len(content_view['versions']) == 1 - cv_version = ContentView.with_user(user['login'], user['password']).version_info( + cv_version = target_sat.cli.ContentView.with_user(user['login'], user['password']).version_info( {'id': content_view['versions'][0]['id']} ) assert len(cv_version['lifecycle-environments']) == 1 - ContentView.with_user(user['login'], user['password']).version_promote( + target_sat.cli.ContentView.with_user(user['login'], user['password']).version_promote( {'id': cv_version['id'], 'to-lifecycle-environment-id': lifecycle_environment['id']} ) # check that content view exists in lifecycle - content_view = ContentView.with_user(user['login'], user['password']).info( + content_view = target_sat.cli.ContentView.with_user(user['login'], user['password']).info( {'id': content_view['id']} ) assert len(content_view['versions']) == 1 - cv_version = ContentView.with_user(user['login'], user['password']).version_info( + cv_version = target_sat.cli.ContentView.with_user(user['login'], user['password']).version_info( {'id': content_view['versions'][0]['id']} ) assert len(cv_version['lifecycle-environments']) == 2 @@ -231,7 +221,7 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel # step 2.12: Create a new activation key activation_key = _create( user, - ActivationKey, + target_sat.cli.ActivationKey, { 'content-view-id': content_view['id'], 'lifecycle-environment-id': lifecycle_environment['id'], @@ -241,12 +231,14 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel ) # step 2.13: Add the products to the activation key - subscription_list = Subscription.with_user(user['login'], user['password']).list( + subscription_list = target_sat.cli.Subscription.with_user(user['login'], user['password']).list( {'organization-id': org['id']}, per_page=False ) for subscription in subscription_list: if subscription['name'] == constants.DEFAULT_SUBSCRIPTION_NAME: - ActivationKey.with_user(user['login'], user['password']).add_subscription( + target_sat.cli.ActivationKey.with_user( + user['login'], user['password'] + ).add_subscription( { 'id': activation_key['id'], 'quantity': 1, @@ -255,7 +247,7 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel ) # step 2.13.1: Enable product content - ActivationKey.with_user(user['login'], user['password']).content_override( + target_sat.cli.ActivationKey.with_user(user['login'], user['password']).content_override( { 'content-label': constants.REPOS['rhst7']['id'], 'id': activation_key['id'], @@ -267,7 +259,9 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel # BONUS: Create a content host and associate it with promoted # content view and last lifecycle where it exists content_host_name = gen_alphanumeric() - content_host = Host.with_user(user['login'], user['password']).subscription_register( + content_host = target_sat.cli.Host.with_user( + user['login'], user['password'] + ).subscription_register( { 'content-view-id': content_view['id'], 'lifecycle-environment-id': lifecycle_environment['id'], @@ -276,7 +270,9 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel } ) - content_host = Host.with_user(user['login'], user['password']).info({'id': content_host['id']}) + content_host = target_sat.cli.Host.with_user(user['login'], user['password']).info( + {'id': content_host['id']} + ) # check that content view matches what we passed assert content_host['content-information']['content-view']['name'] == content_view['name'] @@ -289,7 +285,7 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel # step 2.14: Create a new libvirt compute resource _create( user, - ComputeResource, + target_sat.cli.ComputeResource, { 'name': gen_alphanumeric(), 'provider': 'Libvirt', @@ -300,7 +296,7 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel # step 2.15: Create a new subnet subnet = _create( user, - Subnet, + target_sat.cli.Subnet, { 'name': gen_alphanumeric(), 'network': gen_ipaddr(ip3=True), @@ -309,15 +305,15 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel ) # step 2.16: Create a new domain - domain = _create(user, Domain, {'name': gen_alphanumeric()}) + domain = _create(user, target_sat.cli.Domain, {'name': gen_alphanumeric()}) # step 2.17: Create a new hostgroup and associate previous entities to it host_group = _create( user, - HostGroup, + target_sat.cli.HostGroup, {'domain-id': domain['id'], 'name': gen_alphanumeric(), 'subnet-id': subnet['id']}, ) - HostGroup.with_user(user['login'], user['password']).update( + target_sat.cli.HostGroup.with_user(user['login'], user['password']).update( { 'id': host_group['id'], 'organization-ids': org['id'], diff --git a/tests/foreman/longrun/test_oscap.py b/tests/foreman/longrun/test_oscap.py index 3d292092c86..b4ef1571975 100644 --- a/tests/foreman/longrun/test_oscap.py +++ b/tests/foreman/longrun/test_oscap.py @@ -21,13 +21,6 @@ from nailgun import entities import pytest -from robottelo.cli.ansible import Ansible -from robottelo.cli.arfreport import Arfreport -from robottelo.cli.factory import make_hostgroup, make_scap_policy -from robottelo.cli.host import Host -from robottelo.cli.job_invocation import JobInvocation -from robottelo.cli.proxy import Proxy -from robottelo.cli.scapcontent import Scapcontent from robottelo.config import settings from robottelo.constants import ( OSCAP_DEFAULT_CONTENT, @@ -48,7 +41,7 @@ } -def fetch_scap_and_profile_id(scap_name, scap_profile): +def fetch_scap_and_profile_id(scap_name, scap_profile, sat): """Extracts the scap ID and scap profile id :param scap_name: Scap title @@ -57,7 +50,7 @@ def fetch_scap_and_profile_id(scap_name, scap_profile): :returns: scap_id and scap_profile_id """ - default_content = Scapcontent.info({'title': scap_name}, output_format='json') + default_content = sat.cli.Scapcontent.info({'title': scap_name}, output_format='json') scap_id = default_content['id'] scap_profile_ids = [ profile['id'] @@ -70,7 +63,7 @@ def fetch_scap_and_profile_id(scap_name, scap_profile): @pytest.fixture(scope='module') def default_proxy(module_target_sat): """Returns default capsule/proxy id""" - proxy = Proxy.list({'search': module_target_sat.hostname})[0] + proxy = module_target_sat.cli.Proxy.list({'search': module_target_sat.hostname})[0] p_features = set(proxy.get('features').split(', ')) if {'Ansible', 'Openscap'}.issubset(p_features): proxy_id = proxy.get('id') @@ -173,7 +166,7 @@ def test_positive_oscap_run_via_ansible( hgrp_name = gen_string('alpha') policy_name = gen_string('alpha') # Creates host_group for rhel7 - make_hostgroup( + target_sat.cli_factory.hostgroup( { 'content-source-id': default_proxy, 'name': hgrp_name, @@ -181,11 +174,11 @@ def test_positive_oscap_run_via_ansible( } ) # Creates oscap_policy. - scap_id, scap_profile_id = fetch_scap_and_profile_id(content, profile) - Ansible.roles_import({'proxy-id': default_proxy}) - Ansible.variables_import({'proxy-id': default_proxy}) - role_id = Ansible.roles_list({'search': 'foreman_scap_client'})[0].get('id') - make_scap_policy( + scap_id, scap_profile_id = fetch_scap_and_profile_id(target_sat, content, profile) + target_sat.cli.Ansible.roles_import({'proxy-id': default_proxy}) + target_sat.cli.Ansible.variables_import({'proxy-id': default_proxy}) + role_id = target_sat.cli.Ansible.roles_list({'search': 'foreman_scap_client'})[0].get('id') + target_sat.cli_factory.make_scap_policy( { 'scap-content-id': scap_id, 'hostgroups': hgrp_name, @@ -204,7 +197,7 @@ def test_positive_oscap_run_via_ansible( vm.create_custom_repos(**rhel_repo) else: vm.create_custom_repos(**{distro: rhel_repo}) - Host.update( + target_sat.cli.Host.update( { 'name': vm.hostname.lower(), 'lifecycle-environment': lifecycle_env.name, @@ -215,15 +208,17 @@ def test_positive_oscap_run_via_ansible( 'ansible-role-ids': role_id, } ) - job_id = Host.ansible_roles_play({'name': vm.hostname.lower()})[0].get('id') + job_id = target_sat.cli.Host.ansible_roles_play({'name': vm.hostname.lower()})[0].get('id') target_sat.wait_for_tasks( f'resource_type = JobInvocation and resource_id = {job_id} and action ~ "hosts job"' ) try: - result = JobInvocation.info({'id': job_id})['success'] + result = target_sat.cli.JobInvocation.info({'id': job_id})['success'] assert result == '1' except AssertionError: - output = ' '.join(JobInvocation.get_output({'id': job_id, 'host': vm.hostname})) + output = ' '.join( + target_sat.cli.JobInvocation.get_output({'id': job_id, 'host': vm.hostname}) + ) result = f'host output: {output}' raise AssertionError(result) result = vm.run('cat /etc/foreman_scap_client/config.yaml | grep profile') @@ -233,7 +228,7 @@ def test_positive_oscap_run_via_ansible( vm.execute_foreman_scap_client() # Assert whether oscap reports are uploaded to # Satellite6. - result = Arfreport.list({'search': f'host={vm.hostname.lower()}'}) + result = target_sat.cli.Arfreport.list({'search': f'host={vm.hostname.lower()}'}) assert result is not None @@ -270,7 +265,7 @@ def test_positive_oscap_run_via_ansible_bz_1814988( hgrp_name = gen_string('alpha') policy_name = gen_string('alpha') # Creates host_group for rhel7 - make_hostgroup( + target_sat.cli_factory.hostgroup( { 'content-source-id': default_proxy, 'name': hgrp_name, @@ -279,12 +274,12 @@ def test_positive_oscap_run_via_ansible_bz_1814988( ) # Creates oscap_policy. scap_id, scap_profile_id = fetch_scap_and_profile_id( - OSCAP_DEFAULT_CONTENT['rhel7_content'], OSCAP_PROFILE['dsrhel7'] + target_sat, OSCAP_DEFAULT_CONTENT['rhel7_content'], OSCAP_PROFILE['dsrhel7'] ) - Ansible.roles_import({'proxy-id': default_proxy}) - Ansible.variables_import({'proxy-id': default_proxy}) - role_id = Ansible.roles_list({'search': 'foreman_scap_client'})[0].get('id') - make_scap_policy( + target_sat.cli.Ansible.roles_import({'proxy-id': default_proxy}) + target_sat.cli.Ansible.variables_import({'proxy-id': default_proxy}) + role_id = target_sat.cli.Ansible.roles_list({'search': 'foreman_scap_client'})[0].get('id') + target_sat.cli_factory.make_scap_policy( { 'scap-content-id': scap_id, 'hostgroups': hgrp_name, @@ -307,7 +302,7 @@ def test_positive_oscap_run_via_ansible_bz_1814988( '--fetch-remote-resources --results-arf results.xml ' '/usr/share/xml/scap/ssg/content/ssg-rhel7-ds.xml', ) - Host.update( + target_sat.cli.Host.update( { 'name': vm.hostname.lower(), 'lifecycle-environment': lifecycle_env.name, @@ -318,15 +313,17 @@ def test_positive_oscap_run_via_ansible_bz_1814988( 'ansible-role-ids': role_id, } ) - job_id = Host.ansible_roles_play({'name': vm.hostname.lower()})[0].get('id') + job_id = target_sat.cli.Host.ansible_roles_play({'name': vm.hostname.lower()})[0].get('id') target_sat.wait_for_tasks( f'resource_type = JobInvocation and resource_id = {job_id} and action ~ "hosts job"' ) try: - result = JobInvocation.info({'id': job_id})['success'] + result = target_sat.cli.JobInvocation.info({'id': job_id})['success'] assert result == '1' except AssertionError: - output = ' '.join(JobInvocation.get_output({'id': job_id, 'host': vm.hostname})) + output = ' '.join( + target_sat.cli.JobInvocation.get_output({'id': job_id, 'host': vm.hostname}) + ) result = f'host output: {output}' raise AssertionError(result) result = vm.run('cat /etc/foreman_scap_client/config.yaml | grep profile') @@ -336,7 +333,7 @@ def test_positive_oscap_run_via_ansible_bz_1814988( vm.execute_foreman_scap_client() # Assert whether oscap reports are uploaded to # Satellite6. - result = Arfreport.list({'search': f'host={vm.hostname.lower()}'}) + result = target_sat.cli.Arfreport.list({'search': f'host={vm.hostname.lower()}'}) assert result is not None @@ -497,7 +494,7 @@ def test_positive_oscap_run_via_local_files( hgrp_name = gen_string('alpha') policy_name = gen_string('alpha') - module_target_sat.cli_factory.make_hostgroup( + module_target_sat.cli_factory.hostgroup( { 'content-source-id': default_proxy, 'name': hgrp_name, @@ -505,7 +502,7 @@ def test_positive_oscap_run_via_local_files( } ) # Creates oscap_policy. - scap_id, scap_profile_id = fetch_scap_and_profile_id(content, profile) + scap_id, scap_profile_id = fetch_scap_and_profile_id(module_target_sat, content, profile) with Broker( nick=distro, host_class=ContentHost, diff --git a/tests/foreman/ui/test_activationkey.py b/tests/foreman/ui/test_activationkey.py index 5216cf97540..19e163c6cb8 100644 --- a/tests/foreman/ui/test_activationkey.py +++ b/tests/foreman/ui/test_activationkey.py @@ -25,7 +25,6 @@ import pytest from robottelo import constants -from robottelo.cli.factory import setup_org_for_a_custom_repo from robottelo.config import settings from robottelo.hosts import ContentHost from robottelo.utils.datafactory import parametrized, valid_data_list @@ -1051,7 +1050,7 @@ def test_positive_host_associations(session, target_sat): :CaseLevel: System """ org = entities.Organization().create() - org_entities = setup_org_for_a_custom_repo( + org_entities = target_sat.cli_factory.setup_org_for_a_custom_repo( {'url': settings.repos.yum_1.url, 'organization-id': org.id} ) ak1 = entities.ActivationKey(id=org_entities['activationkey-id']).read() @@ -1115,7 +1114,7 @@ def test_positive_service_level_subscription_with_custom_product( :CaseLevel: System """ org = function_entitlement_manifest_org - entities_ids = setup_org_for_a_custom_repo( + entities_ids = target_sat.cli_factory.setup_org_for_a_custom_repo( {'url': settings.repos.yum_1.url, 'organization-id': org.id} ) product = entities.Product(id=entities_ids['product-id']).read() diff --git a/tests/foreman/ui/test_contenthost.py b/tests/foreman/ui/test_contenthost.py index 7fce51f0055..80a0eedf3a9 100644 --- a/tests/foreman/ui/test_contenthost.py +++ b/tests/foreman/ui/test_contenthost.py @@ -25,7 +25,6 @@ from nailgun import entities import pytest -from robottelo.cli.factory import CLIFactoryError, make_fake_host, make_virt_who_config from robottelo.config import setting_is_set, settings from robottelo.constants import ( DEFAULT_SYSPURPOSE_ATTRIBUTES, @@ -41,6 +40,7 @@ VDC_SUBSCRIPTION_NAME, VIRT_WHO_HYPERVISOR_TYPES, ) +from robottelo.exceptions import CLIFactoryError from robottelo.utils.issue_handlers import is_open from robottelo.utils.virtwho import create_fake_hypervisor_content @@ -902,7 +902,7 @@ def test_positive_virt_who_hypervisor_subscription_status( # TODO move this to either hack around virt-who service or use an env-* compute resource provisioning_server = settings.libvirt.libvirt_hostname # Create a new virt-who config - virt_who_config = make_virt_who_config( + virt_who_config = target_sat.cli_factory.virt_who_config( { 'organization-id': org.id, 'hypervisor-type': VIRT_WHO_HYPERVISOR_TYPES['libvirt'], @@ -1726,7 +1726,7 @@ def test_syspurpose_mismatched(session, default_location, vm_module_streams): @pytest.mark.tier3 -def test_pagination_multiple_hosts_multiple_pages(session, module_host_template): +def test_pagination_multiple_hosts_multiple_pages(session, module_host_template, target_sat): """Create hosts to fill more than one page, sort on OS, check pagination. Search for hosts based on operating system and assert that more than one page @@ -1751,7 +1751,7 @@ def test_pagination_multiple_hosts_multiple_pages(session, module_host_template) # Create more than one page of fake hosts. Need two digits in name to ensure sort order. for count in range(host_num): host_name = f'test-{count + 1:0>2}' - make_fake_host( + target_sat.cli_factory.make_fake_host( { 'name': host_name, 'organization-id': module_host_template.organization.id, diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index 193eebff940..4e6a929f38b 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -1000,7 +1000,7 @@ def test_positive_validate_inherited_cv_lce_ansiblerole(session, target_sat, mod target_sat.cli.Ansible.roles_sync( {'role-names': SELECTED_ROLE, 'proxy-id': target_sat.nailgun_smart_proxy.id} ) - hostgroup = target_sat.cli_factory.make_hostgroup( + hostgroup = target_sat.cli_factory.hostgroup( { 'content-view-id': cv.id, 'lifecycle-environment-id': lce.id, @@ -1209,7 +1209,7 @@ def test_positive_global_registration_end_to_end( ).create() # run insights-client via REX command = "insights-client --status" - invocation_command = target_sat.cli_factory.make_job_invocation( + invocation_command = target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': f'command={command}', diff --git a/tests/foreman/ui/test_settings.py b/tests/foreman/ui/test_settings.py index 923d542443d..e32c1c8c518 100644 --- a/tests/foreman/ui/test_settings.py +++ b/tests/foreman/ui/test_settings.py @@ -23,7 +23,6 @@ from nailgun import entities import pytest -from robottelo.cli.user import User from robottelo.config import settings from robottelo.utils.datafactory import filtered_datapoint, gen_string @@ -252,7 +251,7 @@ def test_positive_update_login_page_footer_text(session, setting_update): @pytest.mark.tier3 -def test_negative_settings_access_to_non_admin(): +def test_negative_settings_access_to_non_admin(module_target_sat): """Check non admin users can't access Administer -> Settings tab :id: 34bb9376-c5fe-431a-ac0d-ef030c0ab50e @@ -282,7 +281,7 @@ def test_negative_settings_access_to_non_admin(): 'from a Satellite administrator: view_settings Back' ) finally: - User.delete({'login': login}) + module_target_sat.cli.User.delete({'login': login}) @pytest.mark.stubbed diff --git a/tests/foreman/ui/test_subscription.py b/tests/foreman/ui/test_subscription.py index 473753fa28b..c1fdd1f3c5c 100644 --- a/tests/foreman/ui/test_subscription.py +++ b/tests/foreman/ui/test_subscription.py @@ -24,7 +24,6 @@ from nailgun import entities import pytest -from robottelo.cli.factory import make_virt_who_config from robottelo.config import settings from robottelo.constants import ( DEFAULT_SUBSCRIPTION_NAME, @@ -370,7 +369,7 @@ def test_positive_view_vdc_guest_subscription_products( rh_product_repository = target_sat.cli_factory.RHELAnsibleEngineRepository(cdn=True) product_name = rh_product_repository.data['product'] # Create a new virt-who config - virt_who_config = make_virt_who_config( + virt_who_config = target_sat.cli_factory.virt_who_config( { 'organization-id': org.id, 'hypervisor-type': VIRT_WHO_HYPERVISOR_TYPES['libvirt'], diff --git a/tests/foreman/virtwho/cli/test_esx.py b/tests/foreman/virtwho/cli/test_esx.py index c5d5a5b1364..b8e110af4f4 100644 --- a/tests/foreman/virtwho/cli/test_esx.py +++ b/tests/foreman/virtwho/cli/test_esx.py @@ -22,7 +22,6 @@ import pytest import requests -from robottelo.cli.user import User from robottelo.config import settings from robottelo.utils.virtwho import ( ETC_VIRTWHO_CONFIG, @@ -292,7 +291,7 @@ def test_positive_rhsm_option(self, default_org, form_data_cli, virtwho_config_c command, form_data_cli['hypervisor-type'], org=default_org.label ) rhsm_username = get_configure_option('rhsm_username', config_file) - assert not User.exists(search=('login', rhsm_username)) + assert not target_sat.cli.User.exists(search=('login', rhsm_username)) assert get_configure_option('rhsm_hostname', config_file) == target_sat.hostname assert get_configure_option('rhsm_prefix', config_file) == '/rhsm' diff --git a/tests/foreman/virtwho/cli/test_esx_sca.py b/tests/foreman/virtwho/cli/test_esx_sca.py index ab942933bbe..1a54ae9d569 100644 --- a/tests/foreman/virtwho/cli/test_esx_sca.py +++ b/tests/foreman/virtwho/cli/test_esx_sca.py @@ -20,7 +20,6 @@ import pytest import requests -from robottelo.cli.user import User from robottelo.config import settings from robottelo.utils.virtwho import ( ETC_VIRTWHO_CONFIG, @@ -366,7 +365,7 @@ def test_positive_rhsm_option( command, form_data_cli['hypervisor-type'], org=module_sca_manifest_org.label ) rhsm_username = get_configure_option('rhsm_username', config_file) - assert not User.exists(search=('login', rhsm_username)) + assert not target_sat.cli.User.exists(search=('login', rhsm_username)) assert get_configure_option('rhsm_hostname', config_file) == target_sat.hostname assert get_configure_option('rhsm_prefix', config_file) == '/rhsm' diff --git a/tests/robottelo/test_cli.py b/tests/robottelo/test_cli.py index 706a1d47785..fcc583eaee0 100644 --- a/tests/robottelo/test_cli.py +++ b/tests/robottelo/test_cli.py @@ -4,8 +4,8 @@ import pytest -from robottelo.cli.base import ( - Base, +from robottelo.cli.base import Base +from robottelo.exceptions import ( CLIBaseError, CLIDataBaseError, CLIError, diff --git a/tests/upgrades/test_usergroup.py b/tests/upgrades/test_usergroup.py index 0420a8a5e8c..aaa337d316f 100644 --- a/tests/upgrades/test_usergroup.py +++ b/tests/upgrades/test_usergroup.py @@ -43,7 +43,7 @@ def test_pre_create_user_group_with_ldap_user(self, ad_data, target_sat, save_te ad_data = ad_data() member_group = 'foobargroup' LOGEDIN_MSG = "Using configured credentials for user '{0}'." - auth_source = target_sat.cli_factory.make_ldap_auth_source( + auth_source = target_sat.cli_factory.ldap_auth_source( { 'name': gen_string('alpha'), 'onthefly-register': 'true', @@ -59,8 +59,8 @@ def test_pre_create_user_group_with_ldap_user(self, ad_data, target_sat, save_te } ) viewer_role = target_sat.cli.Role.info({'name': 'Viewer'}) - user_group = target_sat.cli_factory.make_usergroup() - target_sat.cli_factory.make_usergroup_external( + user_group = target_sat.cli_factory.usergroup() + target_sat.cli_factory.usergroup_external( { 'auth-source-id': auth_source['server']['id'], 'user-group-id': user_group['id'], diff --git a/tests/upgrades/test_virtwho.py b/tests/upgrades/test_virtwho.py index 826e4e1b059..99606f2f062 100644 --- a/tests/upgrades/test_virtwho.py +++ b/tests/upgrades/test_virtwho.py @@ -19,9 +19,6 @@ from fauxfactory import gen_string import pytest -from robottelo.cli.host import Host -from robottelo.cli.subscription import Subscription -from robottelo.cli.virt_who_config import VirtWhoConfig from robottelo.config import settings from robottelo.utils.issue_handlers import is_open from robottelo.utils.virtwho import ( @@ -101,8 +98,10 @@ def test_pre_create_virt_who_configuration( (guest_name, f'product_id={settings.virtwho.sku.vdc_physical} and type=STACK_DERIVED'), ] for hostname, sku in hosts: - host = Host.list({'search': hostname})[0] - subscriptions = Subscription.list({'organization-id': org.id, 'search': sku}) + host = target_sat.cli.Host.list({'search': hostname})[0] + subscriptions = target_sat.cli.Subscription.list( + {'organization-id': org.id, 'search': sku} + ) vdc_id = subscriptions[0]['id'] if 'type=STACK_DERIVED' in sku: for item in subscriptions: @@ -154,8 +153,13 @@ def test_post_crud_virt_who_configuration(self, form_data, pre_upgrade_data, tar if not is_open('BZ:1802395'): assert vhd.status == 'ok' # Verify virt-who status via CLI as we cannot check it via API now - vhd_cli = VirtWhoConfig.exists(search=('name', form_data['name'])) - assert VirtWhoConfig.info({'id': vhd_cli['id']})['general-information']['status'] == 'OK' + vhd_cli = target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) + assert ( + target_sat.cli.VirtWhoConfig.info({'id': vhd_cli['id']})['general-information'][ + 'status' + ] + == 'OK' + ) # Vefify the connection of the guest on Content host hypervisor_name = pre_upgrade_data.get('hypervisor_name') From 12fefc388aadc0539882d16675cb3f9e168c0a30 Mon Sep 17 00:00:00 2001 From: vsedmik <46570670+vsedmik@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:37:59 +0100 Subject: [PATCH 350/586] [6.14.z] Fix capsule download policy update (#13254) Fix capsule download policy update --- robottelo/hosts.py | 6 ++++++ tests/foreman/api/test_capsulecontent.py | 18 ++++-------------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 24cc7e41529..b6c1be14a41 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1656,6 +1656,12 @@ def capsule_setup(self, sat_host=None, capsule_cert_opts=None, **installer_kwarg f'A core service is not running at capsule host\n{result.stdout}' ) + def update_download_policy(self, policy): + """Updates capsule's download policy to desired value""" + proxy = self.nailgun_smart_proxy.read() + proxy.download_policy = policy + proxy.update(['download_policy']) + def set_rex_script_mode_provider(self, mode='ssh'): """Set provider for remote execution script mode. One of: ssh(default), pull-mqtt, ssh-async""" diff --git a/tests/foreman/api/test_capsulecontent.py b/tests/foreman/api/test_capsulecontent.py index 2e72bc8998a..e68872a0f15 100644 --- a/tests/foreman/api/test_capsulecontent.py +++ b/tests/foreman/api/test_capsulecontent.py @@ -43,16 +43,6 @@ class TestCapsuleContentManagement: interactions and use capsule. """ - def update_capsule_download_policy( - self, module_capsule_configured, download_policy, module_target_sat - ): - """Updates capsule's download policy to desired value""" - proxy = module_target_sat.api.SmartProxy( - id=module_capsule_configured.nailgun_capsule.id - ).read() - proxy.download_policy = download_policy - proxy.update(['download_policy']) - @pytest.mark.tier3 @pytest.mark.skip_if_not_set('capsule', 'clients', 'fake_manifest') def test_positive_insights_puppet_package_availability(self, module_capsule_configured): @@ -595,7 +585,7 @@ def test_positive_on_demand_sync( assert function_lce.id in [capsule_lce['id'] for capsule_lce in result['results']] # Update capsule's download policy to on_demand - self.update_capsule_download_policy(module_capsule_configured, 'on_demand') + module_capsule_configured.update_download_policy('on_demand') # Create a content view with the repository cv = target_sat.api.ContentView(organization=function_org, repository=[repo]).create() @@ -669,7 +659,7 @@ def test_positive_update_with_immediate_sync( url=repo_url, ).create() # Update capsule's download policy to on_demand to match repository's policy - self.update_capsule_download_policy(module_capsule_configured, 'on_demand') + module_capsule_configured.update_download_policy('on_demand') # Associate the lifecycle environment with the capsule module_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( data={'environment_id': function_lce.id} @@ -706,7 +696,7 @@ def test_positive_update_with_immediate_sync( assert repo.download_policy == 'immediate' # Update capsule's download policy as well - self.update_capsule_download_policy(module_capsule_configured, 'immediate') + module_capsule_configured.update_download_policy('immediate') # Sync repository once again repo.sync() @@ -808,7 +798,7 @@ def test_positive_sync_kickstart_repo( assert lce.id in [capsule_lce['id'] for capsule_lce in result['results']] # Update capsule's download policy to on_demand - self.update_capsule_download_policy(module_capsule_configured, 'on_demand') + module_capsule_configured.update_download_policy('on_demand') # Create a content view with the repository cv = target_sat.api.ContentView( From e991b1a9f6cad3198ae90cc85b5883ca2e85e7c4 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 5 Dec 2023 01:36:21 -0500 Subject: [PATCH 351/586] [6.14.z] adding customerscenario tag to libvirt e2e provision test (#13258) adding customerscenario tag to libvirt e2e provision test (#13252) (cherry picked from commit 5703ea7d785af76c805e98f12a988b19ca275b25) Co-authored-by: Peter Ondrejka --- tests/foreman/cli/test_computeresource_libvirt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/foreman/cli/test_computeresource_libvirt.py b/tests/foreman/cli/test_computeresource_libvirt.py index 25643439ce3..470398747e7 100644 --- a/tests/foreman/cli/test_computeresource_libvirt.py +++ b/tests/foreman/cli/test_computeresource_libvirt.py @@ -467,6 +467,8 @@ def test_positive_provision_end_to_end( :parametrized: yes :BZ: 2236693 + + :customerscenario: true """ sat = module_libvirt_provisioning_sat.sat cr_name = gen_string('alpha') From 9db25c06aeff71f8e3025ad48de209664420270d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 5 Dec 2023 01:37:39 -0500 Subject: [PATCH 352/586] [6.14.z] Bump navmazing from 1.1.6 to 1.2.2 (#13263) Bump navmazing from 1.1.6 to 1.2.2 (#13259) (cherry picked from commit 7b90a09797211e2d517069169443cf928eca9cd4) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9d02fce25a3..48d2bd4439b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ dynaconf[vault]==3.2.4 fauxfactory==3.1.0 jinja2==3.1.2 manifester==0.0.14 -navmazing==1.1.6 +navmazing==1.2.2 productmd==1.37 pyotp==2.9.0 python-box==7.1.1 From 48e27ffb0831a1a4e49061bdacdb059ee3193258 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 5 Dec 2023 12:25:54 -0500 Subject: [PATCH 353/586] [6.14.z] Add test to verify supported provisioning templates (#13269) --- tests/foreman/ui/test_provisioningtemplate.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/foreman/ui/test_provisioningtemplate.py b/tests/foreman/ui/test_provisioningtemplate.py index 62b9169a729..1b957fb4549 100644 --- a/tests/foreman/ui/test_provisioningtemplate.py +++ b/tests/foreman/ui/test_provisioningtemplate.py @@ -182,3 +182,41 @@ def test_positive_end_to_end(module_org, module_location, template_data, target_ assert not target_sat.api.ProvisioningTemplate().search( query={'search': f'name=={new_name}'} ), f'Provisioning template {new_name} expected to be removed but is included in the search' + + +@pytest.mark.tier2 +def test_positive_verify_supported_templates_rhlogo(target_sat, module_org, module_location): + """Verify presense of RH logo on supported provisioning template + + :id: 2df8550a-fe7d-405f-ab48-2896554cda14 + + :Steps: + 1. Go to Provisioning template UI + 2. Choose a any provisioning template and check if its supported or not + + :expectedresults: Supported templates will have the RH logo and not supported will have no logo. + + :BZ: 2211210, 2238346 + """ + template_name = '"Kickstart default"' + pt = target_sat.api.ProvisioningTemplate().search(query={'search': f'name={template_name}'})[0] + pt_clone = pt.clone(data={'name': f'{pt.name} {gen_string("alpha")}'}) + + random_templates = { + 'ansible_provisioning_callback': {'supported': True, 'locked': True}, + pt.name: {'supported': True, 'locked': True}, + pt_clone['name']: {'supported': False, 'locked': False}, + 'Windows default provision': {'supported': False, 'locked': True}, + } + with target_sat.ui_session() as session: + session.organization.select(org_name=module_org.name) + session.location.select(loc_name=module_location.name) + for template in random_templates.keys(): + assert ( + session.provisioningtemplate.is_locked(template) + == random_templates[template]['locked'] + ) + assert ( + session.provisioningtemplate.is_supported(template) + == random_templates[template]['supported'] + ) From 1d05626ccd042a0caeb8daad3095c9d539b8cd0e Mon Sep 17 00:00:00 2001 From: vsedmik <46570670+vsedmik@users.noreply.github.com> Date: Tue, 5 Dec 2023 21:45:14 +0100 Subject: [PATCH 354/586] [6.14.z] Add coverage for BZ#2092039 and update few docstrings (#13245) * Add coverage for BZ#2092039 and update few docstrings * Use better timeout format * Clear import path before import --- robottelo/host_helpers/satellite_mixins.py | 1 + tests/foreman/cli/test_satellitesync.py | 221 ++++++++++++++++----- 2 files changed, 173 insertions(+), 49 deletions(-) diff --git a/robottelo/host_helpers/satellite_mixins.py b/robottelo/host_helpers/satellite_mixins.py index b17325a8492..751e9607ffc 100644 --- a/robottelo/host_helpers/satellite_mixins.py +++ b/robottelo/host_helpers/satellite_mixins.py @@ -200,6 +200,7 @@ def move_pulp_archive(self, org, export_message): sets ownership, returns import path """ self.execute( + f'rm -rf {PULP_IMPORT_DIR}/{org.name} &&' f'mv {PULP_EXPORT_DIR}/{org.name} {PULP_IMPORT_DIR} && ' f'chown -R pulp:pulp {PULP_IMPORT_DIR}' ) diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index 8dd3fae299c..7fed3bd1586 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -579,8 +579,6 @@ def test_positive_export_import_cv_end_to_end( 1. CV version custom contents has been exported to directory. 2. All The exported custom contents has been imported in org/satellite. - :CaseImportance: High - :CaseLevel: System :BZ: 1832858 @@ -772,8 +770,6 @@ def test_positive_export_import_filtered_cvv( 1. Filtered CV version custom contents has been exported to directory 2. Filtered exported custom contents has been imported in org/satellite - :CaseImportance: High - :CaseLevel: System """ exporting_cv_name = importing_cvv = gen_string('alpha') @@ -953,8 +949,6 @@ def test_positive_export_import_redhat_cv( :customerscenario: true - :CaseImportance: High - :CaseLevel: System """ # Create cv and publish @@ -1184,13 +1178,10 @@ def test_negative_import_invalid_path(self, module_org, module_target_sat): :id: 4cc69666-407f-4d66-b3d2-8fe2ed135a5f :steps: - - 1. Import a cv with a path that doesn't exist + 1. Import a CV with a path that doesn't exist. :expectedresults: - - 1. Error 'Unable to sync repositories, no library repository found' should be - displayed + 1. Error 'Unable to sync repositories, no library repository found' should be displayed. """ export_folder = gen_string('alpha') import_path = f'{PULP_IMPORT_DIR}{export_folder}' @@ -1233,7 +1224,6 @@ def test_postive_export_cv_with_mixed_content_repos( :BZ: 1726457 :customerscenario: true - """ content_view = target_sat.cli_factory.make_content_view( {'organization-id': function_org.id} @@ -1342,9 +1332,9 @@ def test_postive_export_import_cv_with_file_content( 1. Product with synced file-type repository. :steps: - 3. Create CV, add the file repo and publish. - 4. Export the CV and import it into another organization. - 5. Check the imported CV has files in it. + 1. Create CV, add the file repo and publish. + 2. Export the CV and import it into another organization. + 3. Check the imported CV has files in it. :expectedresults: 1. Imported CV should have the files present. @@ -1723,7 +1713,7 @@ def test_positive_import_content_for_disconnected_sat_with_existing_content( 1. Import should fail with correct message when existing CV has 'import-only' set False. 2. Import should succeed when existing CV has 'import-only' set True. - :bz: 2030101 + :BZ: 2030101 :customerscenario: true """ @@ -1903,14 +1893,13 @@ def test_positive_reimport_repo(self): :id: b3a71405-d8f0-4085-b728-8fc3513611c8 :steps: - 1. From upstream Export repo fully and import it in downstream. 2. In upstream delete some packages from repo. 3. Re-export the full repo. 4. In downstream, reimport the repo re-exported. - :expectedresults: Deleted packages from upstream are removed from - downstream. + :expectedresults: + 1. Deleted packages from upstream are removed from downstream. :CaseAutomation: NotAutomated @@ -1924,59 +1913,193 @@ def test_negative_export_repo_from_future_datetime(self): :id: 1e8bc352-198f-4d59-b437-1b184141fab4 - :steps: Export the repo incrementally from the future date time. + :steps: + 1. Export the repo incrementally from the future date time. - :expectedresults: Error is raised for attempting to export from future - datetime. + :expectedresults: + 1. Error is raised for attempting to export from future datetime. :CaseAutomation: NotAutomated :CaseLevel: System """ - @pytest.mark.stubbed @pytest.mark.tier3 - def test_positive_export_redhat_incremental_yum_repo(self): - """Export Red Hat YUM repo in directory incrementally. - - :id: be054636-629a-40a0-b414-da3964154bd1 + @pytest.mark.upgrade + def test_positive_export_import_incremental_yum_repo( + self, + target_sat, + export_import_cleanup_function, + config_export_import_settings, + function_org, + function_import_org, + function_synced_custom_repo, + ): + """Export and import custom YUM repo contents incrementally. - :steps: + :id: 318560d7-71f5-4646-ab5c-12a2ec22d031 - 1. Export whole Red Hat YUM repo. - 2. Add some packages to the earlier exported yum repo. - 3. Incrementally export the yum repo from last exported date. + :setup: + 1. Enabled and synced custom yum repository. - :expectedresults: Red Hat YUM repo contents have been exported - incrementally in separate directory. + :steps: + 1. First, export and import whole custom YUM repo. + 2. Add some packages to the earlier exported YUM repo. + 3. Incrementally export the custom YUM repo. + 4. Import the exported YUM repo contents incrementally. - :CaseAutomation: NotAutomated + :expectedresults: + 1. Complete export and import succeeds, product and repository is created + in the importing organization and content counts match. + 2. Incremental export and import succeeds, content counts match the updated counts. :CaseLevel: System """ + export_cc = target_sat.cli.Repository.info({'id': function_synced_custom_repo.id})[ + 'content-counts' + ] + + # Verify export directory is empty + assert target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) == '' + # Export complete and check the export directory + export = target_sat.cli.ContentExport.completeRepository( + {'id': function_synced_custom_repo['id']} + ) + assert '1.0' in target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) + + # Run import and verify the product and repo is created + # in the importing org and the content counts match. + import_path = target_sat.move_pulp_archive(function_org, export['message']) + target_sat.cli.ContentImport.repository( + {'organization-id': function_import_org.id, 'path': import_path} + ) + import_repo = target_sat.cli.Repository.info( + { + 'organization-id': function_import_org.id, + 'name': function_synced_custom_repo.name, + 'product': function_synced_custom_repo.product.name, + } + ) + assert import_repo['content-counts'] == export_cc, 'Import counts do not match the export.' + + # Upload custom content into the repo + with open(DataFile.RPM_TO_UPLOAD, 'rb') as handle: + result = target_sat.api.Repository(id=function_synced_custom_repo.id).upload_content( + files={'content': handle} + ) + assert 'success' in result['status'] + + # Export incremental and check the export directory + export = target_sat.cli.ContentExport.incrementalRepository( + {'id': function_synced_custom_repo['id']} + ) + assert '2.0' in target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) + + # Run the import and verify the content counts match the updated counts. + import_path = target_sat.move_pulp_archive(function_org, export['message']) + target_sat.cli.ContentImport.repository( + {'organization-id': function_import_org.id, 'path': import_path} + ) + import_repo = target_sat.cli.Repository.info( + { + 'organization-id': function_import_org.id, + 'name': function_synced_custom_repo.name, + 'product': function_synced_custom_repo.product.name, + } + ) + export_cc['packages'] = str(int(export_cc['packages']) + 1) + assert import_repo['content-counts'] == export_cc, 'Import counts do not match the export.' - @pytest.mark.stubbed @pytest.mark.tier3 - @pytest.mark.upgrade - def test_positive_export_import_redhat_incremental_yum_repo(self): - """Import the exported YUM repo contents incrementally. + @pytest.mark.parametrize( + 'function_synced_rhel_repo', + ['rhae2'], + indirect=True, + ) + def test_positive_export_import_mismatch_label( + self, + target_sat, + export_import_cleanup_function, + config_export_import_settings, + function_sca_manifest_org, + function_import_org_with_manifest, + function_synced_rhel_repo, + ): + """Export and import repo with mismatched label - :id: 318560d7-71f5-4646-ab5c-12a2ec22d031 + :id: eb2f3e8e-3ee6-4713-80ab-3811a098e079 + + :setup: + 1. Enabled and synced RH yum repository. :steps: + 1. Export and import a RH yum repo, verify it was imported. + 2. Export the repo again and change the repository label. + 3. Import the changed repository again, it should succeed without errors. - 1. First, Export and Import whole Red Hat YUM repo. - 2. Add some packages to the earlier exported yum repo. - 3. Incrementally export the Red Hat YUM repo from last exported - date. - 4. Import the exported YUM repo contents incrementally. + :expectedresults: + 1. All exports and imports succeed. - :expectedresults: YUM repo contents have been imported incrementally. + :CaseLevel: System - :CaseAutomation: NotAutomated + :CaseImportance: Medium - :CaseLevel: System + :BZ: 2092039 + + :customerscenario: true """ + # Verify export directory is empty + assert target_sat.validate_pulp_filepath(function_sca_manifest_org, PULP_EXPORT_DIR) == '' + # Export the repository and check the export directory + export = target_sat.cli.ContentExport.completeRepository( + {'id': function_synced_rhel_repo['id']} + ) + assert '1.0' in target_sat.validate_pulp_filepath( + function_sca_manifest_org, PULP_EXPORT_DIR + ) + + # Run import and verify the product and repo is created in the importing org + import_path = target_sat.move_pulp_archive(function_sca_manifest_org, export['message']) + target_sat.cli.ContentImport.repository( + {'organization-id': function_import_org_with_manifest.id, 'path': import_path}, + timeout='5m', + ) + import_repo = target_sat.cli.Repository.info( + { + 'name': function_synced_rhel_repo['name'], + 'product': function_synced_rhel_repo['product']['name'], + 'organization-id': function_sca_manifest_org.id, + } + ) + assert import_repo + + # Export again and check the export directory + export = target_sat.cli.ContentExport.completeRepository( + {'id': function_synced_rhel_repo['id']} + ) + assert '2.0' in target_sat.validate_pulp_filepath( + function_sca_manifest_org, PULP_EXPORT_DIR + ) + + # Change the repo label in metadata.json and run the import again + import_path = target_sat.move_pulp_archive(function_sca_manifest_org, export['message']) + target_sat.execute( + f'''sed -i 's/"label":"{function_synced_rhel_repo['label']}"/''' + f'''"label":"{gen_string("alpha")}"/g' {import_path}/metadata.json''' + ) + target_sat.cli.ContentImport.repository( + {'organization-id': function_import_org_with_manifest.id, 'path': import_path}, + timeout='5m', + ) + + # Verify that both import tasks succeeded + tasks = target_sat.cli.Task.list_tasks( + {'search': f"Import Repository organization '{function_import_org_with_manifest.name}'"} + ) + assert len(tasks) == 2, f'Expected 2 import tasks in this Org but found {len(tasks)}' + assert all( + ['success' in task['result'] for task in tasks] + ), 'Not every import task succeeded' @pytest.mark.stubbed @pytest.mark.tier3 @@ -1996,8 +2119,8 @@ def test_positive_install_package_from_imported_repos(self): 5. Attempt to install a package on a client from imported repo of downstream. - :expectedresults: The package is installed on client from imported repo - of downstream satellite. + :expectedresults: + 1. The package is installed on client from imported repo of downstream satellite. :CaseAutomation: NotAutomated From 6509e4d87a7d473f02cf205de0a3107444e225c3 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:36:18 -0500 Subject: [PATCH 355/586] [6.14.z] Fix subnet tests (#13293) --- tests/foreman/cli/test_subnet.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/foreman/cli/test_subnet.py b/tests/foreman/cli/test_subnet.py index eaa2262d13e..a08fe818dfd 100644 --- a/tests/foreman/cli/test_subnet.py +++ b/tests/foreman/cli/test_subnet.py @@ -19,7 +19,7 @@ import random import re -from fauxfactory import gen_choice, gen_integer, gen_ipaddr +from fauxfactory import gen_choice, gen_integer, gen_ipaddr, gen_netmask import pytest from robottelo.constants import SUBNET_IPAM_TYPES @@ -81,7 +81,7 @@ def test_positive_CRUD(module_target_sat): """ name = gen_choice(list(valid_data_list().values())) pool = sorted(valid_addr_pools()[0]) - mask = '255.255.255.0' + mask = gen_netmask() # generate pool range from network address network = gen_ipaddr() from_ip = re.sub(r'\d+$', str(pool[0]), network) @@ -119,7 +119,7 @@ def test_positive_CRUD(module_target_sat): pool = sorted(valid_addr_pools()[0]) # generate pool range from network address new_network = gen_ipaddr() - new_mask = '255.255.192.0' + new_mask = gen_netmask() ip_from = re.sub(r'\d+$', str(pool[0]), new_network) ip_to = re.sub(r'\d+$', str(pool[1]), new_network) ipam_type = SUBNET_IPAM_TYPES['internal'] @@ -180,20 +180,19 @@ def test_negative_create_with_address_pool(pool, module_target_sat): :CaseImportance: Medium """ - mask = '255.255.255.0' + mask = gen_netmask() network = gen_ipaddr() - opts = {'mask': mask, 'network': network} + options = {'mask': mask, 'network': network} # generate pool range from network address for key, val in pool.items(): - opts[key] = re.sub(r'\d+$', str(val), network) - with pytest.raises(CLIFactoryError) as raise_ctx: - module_target_sat.cli_factory.make_subnet(opts) - assert 'Could not create the subnet:' in str(raise_ctx.value) + options[key] = re.sub(r'\d+$', str(val), network) + with pytest.raises(CLIFactoryError, match='Could not create the subnet:'): + module_target_sat.cli_factory.make_subnet(options) @pytest.mark.tier2 @pytest.mark.parametrize('options', **parametrized(invalid_missing_attributes())) -def test_negative_update_attributes(options, module_target_sat): +def test_negative_update_attributes(request, options, module_target_sat): """Update subnet with invalid or missing required attributes :parametrized: yes @@ -206,17 +205,18 @@ def test_negative_update_attributes(options, module_target_sat): """ subnet = module_target_sat.cli_factory.make_subnet() options['id'] = subnet['id'] + request.addfinalizer(lambda: module_target_sat.cli.Subnet.delete({'id': subnet['id']})) with pytest.raises(CLIReturnCodeError, match='Could not update the subnet:'): module_target_sat.cli.Subnet.update(options) # check - subnet is not updated result = module_target_sat.cli.Subnet.info({'id': subnet['id']}) - for key in options.keys(): + for key in ['name', 'network-addr', 'network-mask']: assert subnet[key] == result[key] @pytest.mark.tier2 @pytest.mark.parametrize('options', **parametrized(invalid_addr_pools())) -def test_negative_update_address_pool(options, module_target_sat): +def test_negative_update_address_pool(request, options, module_target_sat): """Update subnet with invalid address pool :parametrized: yes @@ -229,6 +229,7 @@ def test_negative_update_address_pool(options, module_target_sat): """ subnet = module_target_sat.cli_factory.make_subnet() opts = {'id': subnet['id']} + request.addfinalizer(lambda: module_target_sat.cli.Subnet.delete({'id': subnet['id']})) # generate pool range from network address for key, val in options.items(): opts[key] = re.sub(r'\d+$', str(val), subnet['network-addr']) From fb0a0c943ad0515a3a2eb10ca5348e68d8b970c4 Mon Sep 17 00:00:00 2001 From: omkarkhatavkar Date: Fri, 24 Nov 2023 15:06:04 +0530 Subject: [PATCH 356/586] Add custom pytest hook for test result handling and video cleanup (cherry picked from commit 1e58d01c522f605d5d942d91ab7eb8591e116bec) --- conftest.py | 1 + pytest_plugins/video_cleanup.py | 74 +++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 pytest_plugins/video_cleanup.py diff --git a/conftest.py b/conftest.py index 14b8601c4d2..1b4510ec7e4 100644 --- a/conftest.py +++ b/conftest.py @@ -20,6 +20,7 @@ 'pytest_plugins.factory_collection', 'pytest_plugins.requirements.update_requirements', 'pytest_plugins.sanity_plugin', + 'pytest_plugins.video_cleanup', # Fixtures 'pytest_fixtures.core.broker', 'pytest_fixtures.core.sat_cap_factory', diff --git a/pytest_plugins/video_cleanup.py b/pytest_plugins/video_cleanup.py new file mode 100644 index 00000000000..35c6fb5fb13 --- /dev/null +++ b/pytest_plugins/video_cleanup.py @@ -0,0 +1,74 @@ +from urllib.parse import urlparse + +from box import Box +from broker.hosts import Host +import pytest + +from robottelo.config import settings +from robottelo.logging import logger + +test_results = {} +test_directories = [ + 'tests/foreman/destructive', + 'tests/foreman/ui', + 'tests/foreman/sanity', + 'tests/foreman/virtwho', +] + + +def _clean_video(session_id, test): + logger.info(f"cleaning up video files for session: {session_id} and test: {test}") + + if settings.ui.grid_url and session_id: + grid = urlparse(url=settings.ui.grid_url) + infra_grid = Host(hostname=grid.hostname) + infra_grid.execute(command=f'rm -rf /var/www/html/videos/{session_id}') + logger.info(f"video cleanup for session {session_id} is complete") + else: + logger.warning("missing grid_url or session_id. unable to clean video files.") + + +def pytest_addoption(parser): + """Custom pytest option to skip video cleanup on test success. + Args: + parser (object): The pytest command-line option parser. + Options: + --skip-video-cleanup: Skip video cleaning on test success (default: False). + """ + parser.addoption( + "--skip-video-cleanup", + action="store_true", + default=False, + help="Skip video cleaning on test success", + ) + + +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_makereport(item): + """Custom pytest hook to capture test outcomes and perform video cleanup. + Note: + This hook captures test results during 'call' and performs video cleanup + during 'teardown' if the test passed and the '--skip-video-cleanup' option is not set. + """ + outcome = yield + report = outcome.get_result() + skip_video_cleanup = item.config.getoption("--skip-video-cleanup", False) + + if not skip_video_cleanup and any(directory in report.fspath for directory in test_directories): + if report.when == "call": + test_results[item.nodeid] = Box( + { + 'outcome': report.outcome, + 'duration': report.duration, + 'longrepr': str(report.longrepr), + } + ) + if report.when == "teardown": + if item.nodeid in test_results: + result_info = test_results[item.nodeid] + if result_info.outcome == 'passed': + session_id_tuple = next( + (t for t in report.user_properties if t[0] == 'session_id'), None + ) + session_id = session_id_tuple[1] if session_id_tuple else None + _clean_video(session_id, item.nodeid) From 2301ef140ddd7b03fe6bc162121b69b16b9245b3 Mon Sep 17 00:00:00 2001 From: Peter Ondrejka Date: Thu, 23 Nov 2023 10:06:43 +0100 Subject: [PATCH 357/586] fix for remote execution cli tests --- tests/foreman/cli/test_remoteexecution.py | 34 +++++++++++++++++++---- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index 7ae6c303f9c..b3f0cf0eb74 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -293,6 +293,31 @@ def test_positive_install_multiple_packages_with_a_job_by_ip( assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) result = client.run(f'rpm -q {" ".join(packages)}') assert result.status == 0 + # Update packages + pre_versions = result.stdout.splitlines() + result = client.run(f'dnf -y downgrade {" ".join(packages)}') + assert result.status == 0 + invocation_command = target_sat.cli_factory.job_invocation( + { + 'job-template': 'Update Package - Katello Script Default', + 'inputs': f'package={" ".join(packages)}', + 'search-query': f'name ~ {client.hostname}', + } + ) + assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) + post_versions = client.run(f'rpm -q {" ".join(packages)}').stdout.splitlines() + assert set(pre_versions) == set(post_versions) + # Remove packages + invocation_command = target_sat.cli_factory.job_invocation( + { + 'job-template': 'Remove Package - Katello Script Default', + 'inputs': f'package={" ".join(packages)}', + 'search-query': f'name ~ {client.hostname}', + } + ) + assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) + result = client.run(f'rpm -q {" ".join(packages)}') + assert result.status == len(packages) @pytest.mark.tier3 @pytest.mark.rhel_ver_list([8]) @@ -1095,7 +1120,7 @@ def test_positive_run_job_on_host_converted_to_pull_provider( assert_job_invocation_result( module_target_sat, invocation_command['id'], rhel_contenthost.hostname ) - module_target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) + result = module_target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) @pytest.mark.tier3 @pytest.mark.upgrade @@ -1110,7 +1135,6 @@ def test_positive_run_job_on_host_registered_to_pull_provider( module_ak_with_cv, module_capsule_configured_mqtt, rhel_contenthost, - target_sat, ): """Run custom template on host registered to mqtt, check effective user setting @@ -1155,7 +1179,7 @@ def test_positive_run_job_on_host_registered_to_pull_provider( result = rhel_contenthost.execute('systemctl status yggdrasild') assert result.status == 0, f'Failed to start yggdrasil on client: {result.stderr}' # run script provider rex command - invocation_command = target_sat.cli_factory.job_invocation( + invocation_command = module_target_sat.cli_factory.job_invocation( { 'job-template': 'Service Action - Script Default', 'inputs': 'action=status, service=yggdrasild', @@ -1163,12 +1187,12 @@ def test_positive_run_job_on_host_registered_to_pull_provider( } ) assert_job_invocation_result( - target_sat, invocation_command['id'], rhel_contenthost.hostname + module_target_sat, invocation_command['id'], rhel_contenthost.hostname ) # create user on host username = gen_string('alpha') filename = gen_string('alpha') - make_user_job = target_sat.cli_factory.job_invocation( + make_user_job = module_target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': f"command=useradd -m {username}", From e68dc31309a06c9f4447aa683bef89a2bad2179f Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Dec 2023 02:01:48 -0500 Subject: [PATCH 358/586] [6.14.z] Bump pytest-reportportal from 5.3.0 to 5.3.1 (#13300) Bump pytest-reportportal from 5.3.0 to 5.3.1 (#13297) (cherry picked from commit 11d1366e913d797a94bef984b1beeeee34e4411e) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 48d2bd4439b..01904f0c611 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ python-box==7.1.1 pytest==7.4.3 pytest-services==2.2.1 pytest-mock==3.12.0 -pytest-reportportal==5.3.0 +pytest-reportportal==5.3.1 pytest-xdist==3.5.0 pytest-fixturecollection==0.1.1 pytest-ibutsu==2.2.4 From 18e7abc3d594677c96ffb579eae69ba19d6752fc Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Dec 2023 05:48:19 -0500 Subject: [PATCH 359/586] [6.14.z] Update error message in negative settings test (#13308) --- tests/foreman/ui/test_settings.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/foreman/ui/test_settings.py b/tests/foreman/ui/test_settings.py index e32c1c8c518..e0a2ca0cf9c 100644 --- a/tests/foreman/ui/test_settings.py +++ b/tests/foreman/ui/test_settings.py @@ -121,7 +121,7 @@ def test_positive_httpd_proxy_url_update(session, setting_update): @pytest.mark.tier2 -@pytest.mark.parametrize('setting_update', ['foreman_url', 'entries_per_page'], indirect=True) +@pytest.mark.parametrize('setting_update', ['foreman_url'], indirect=True) def test_negative_validate_foreman_url_error_message(session, setting_update): """Updates some settings with invalid values (an exceptional tier2 test) @@ -136,9 +136,10 @@ def test_negative_validate_foreman_url_error_message(session, setting_update): property_name = setting_update.name with session: invalid_value = [invalid_value for invalid_value in invalid_settings_values()][0] + err_msg = 'URL must be valid and schema must be one of http and https, Invalid HTTP(S) URL' with pytest.raises(AssertionError) as context: session.settings.update(f'name = {property_name}', invalid_value) - assert 'Value is invalid: must be integer' in str(context.value) + assert err_msg in str(context.value) @pytest.mark.tier2 From b0ebf4d964dacbc2af918559a17a73c527f7a4f7 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Dec 2023 05:54:08 -0500 Subject: [PATCH 360/586] [6.14.z] Test Coverage for FDI version verification (#13321) Test Coverage for FDI version verification (#13209) * Test Coverage for FDI version verification * Removed unwanted code * Using foreman-maintain for installation * Using Version (cherry picked from commit 18acd551b0d78b8de630998dd570fbf208a16bdd) Co-authored-by: Adarsh dubey --- tests/upgrades/test_discovery.py | 68 ++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/upgrades/test_discovery.py diff --git a/tests/upgrades/test_discovery.py b/tests/upgrades/test_discovery.py new file mode 100644 index 00000000000..157b04cc4b8 --- /dev/null +++ b/tests/upgrades/test_discovery.py @@ -0,0 +1,68 @@ +"""Test Discovery Plugin related Upgrade Scenario's + +:Requirement: UpgradedSatellite + +:CaseAutomation: Automated + +:CaseLevel: Acceptance + +:CaseComponent: DiscoveryImage + +:Team: Rocket + +:TestType: Functional + +:CaseImportance: High + +:Upstream: No +""" +import re + +from packaging.version import Version +import pytest + + +class TestDiscoveryImage: + """Pre-upgrade and post-upgrade scenarios to test Foreman Discovery Image version. + + Test Steps: + 1. Before Satellite upgrade, Check the FDI version on the Satellite + 2. Upgrade satellite. + 3. Check the FDI version on the Satellite after upgrade to make sure its same or greater. + """ + + @pytest.mark.pre_upgrade + def test_pre_upgrade_fdi_version(self, target_sat, save_test_data, request): + """Test FDI version before upgrade. + + :id: preupgrade-8c94841c-6791-4af0-aa9c-e54c8d8b9a92 + + :steps: + 1. Check installed version of FDI + + :expectedresults: Version should be saved and checked post-upgrade + """ + target_sat.register_to_cdn() + target_sat.execute('foreman-maintain packages install -y foreman-discovery-image') + fdi_package = target_sat.execute('rpm -qa *foreman-discovery-image*').stdout + # Note: The regular exp takes care of format digit.digit.digit or digit.digit.digit-digit in the output + pre_upgrade_version = Version(re.search(r'\d+\.\d+\.\d+(-\d+)?', fdi_package).group()) + save_test_data({'pre_upgrade_version': pre_upgrade_version}) + + @pytest.mark.post_upgrade(depend_on=test_pre_upgrade_fdi_version) + def test_post_upgrade_fdi_version(self, target_sat, pre_upgrade_data): + """Test FDI version post upgrade. + + :id: postugrade-38bdecaa-2b50-434b-90b1-4aa2b600d04e + + :steps: + 1. Check installed version of FDI + + :expectedresults: Version should be greater than or equal to pre_upgrade version + """ + pre_upgrade_version = pre_upgrade_data.get('pre_upgrade_version') + fdi_package = target_sat.execute('rpm -qa *foreman-discovery-image*').stdout + # Note: The regular exp takes care of format digit.digit.digit or digit.digit.digit-digit in the output + post_upgrade_version = Version(re.search(r'\d+\.\d+\.\d+(-\d+)?', fdi_package).group()) + assert post_upgrade_version >= pre_upgrade_version + target_sat.unregister() From 71f28f842e56d035e004de1c91775f9e6ca4b6e3 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Dec 2023 07:44:54 -0500 Subject: [PATCH 361/586] [6.14.z] Add closed loop for BZ#1841048 (#13317) Add closed loop for BZ#1841048 (#13282) Signed-off-by: Shubham Ganar (cherry picked from commit a8ab8073c65f00b77e956864fefec50bcbc05106) Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> --- tests/foreman/api/test_registration.py | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/foreman/api/test_registration.py b/tests/foreman/api/test_registration.py index eb74c4afe24..f29da4c8e7a 100644 --- a/tests/foreman/api/test_registration.py +++ b/tests/foreman/api/test_registration.py @@ -18,6 +18,7 @@ """ import uuid +from fauxfactory import gen_ipaddr, gen_mac import pytest from robottelo import constants @@ -158,3 +159,51 @@ def test_positive_update_packages_registration( rhel8_contenthost.create_custom_repos(fake_yum=repo_url) result = rhel8_contenthost.execute(f"yum install -y {package}") assert result.status == 0 + + +@pytest.mark.no_containers +def test_positive_rex_interface_for_global_registration( + module_target_sat, + module_entitlement_manifest_org, + module_location, + rhel8_contenthost, + module_activation_key, +): + """Test remote execution interface is set for global registration + + :id: 982de593-dd1a-4c6c-81fe-728f40a7ad4d + + :steps: + 1. Register host with global registration template to Satellite specifying remote execution interface parameter. + + :expectedresults: remote execution interface passed in the registration command is properly set for the host. + + :BZ: 1841048 + + :customerscenario: true + """ + mac_address = gen_mac(multicast=False) + ip = gen_ipaddr() + # Create eth1 interface on the host + add_interface_command = f'ip link add eth1 type dummy;ifconfig eth1 hw ether {mac_address};ip addr add {ip}/24 brd + dev eth1 label eth1:1;ip link set dev eth1 up' + result = rhel8_contenthost.execute(add_interface_command) + assert result.status == 0 + org = module_entitlement_manifest_org + command = module_target_sat.api.RegistrationCommand( + organization=org, + location=module_location, + activation_keys=[module_activation_key.name], + update_packages=True, + remote_execution_interface='eth1', + ).create() + result = rhel8_contenthost.execute(command) + assert result.status == 0, f'Failed to register host: {result.stderr}' + host = module_target_sat.api.Host().search( + query={'search': f'name={rhel8_contenthost.hostname}'} + )[0] + # Check if eth1 interface is set for remote execution + for interface in host.read_json()['interfaces']: + if 'eth1' in str(interface): + assert interface['execution'] is True + assert interface['ip'] == ip + assert interface['mac'] == mac_address From 6b4a814d6c90cbf53a262633fa8c8b1897c0f04c Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Dec 2023 08:46:23 -0500 Subject: [PATCH 362/586] [6.14.z] Create distinction between the product-ready OS and the product WFs (#13275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create a distinction between the product-ready OS and the product WFs (cherry picked from commit ec88ed968bb3859ee31349b3ac99fa2799cd6491) Co-authored-by: Ondřej Gajdušek --- conf/capsule.yaml.template | 4 ++- conf/dynaconf_hooks.py | 34 +++++++++++++++++++++-- conf/migrations.py | 37 +++++++++++++++++++++++++ conf/server.yaml.template | 4 ++- pytest_fixtures/core/sat_cap_factory.py | 13 +++++---- robottelo/config/validators.py | 8 ++++-- robottelo/hosts.py | 2 +- 7 files changed, 89 insertions(+), 13 deletions(-) create mode 100644 conf/migrations.py diff --git a/conf/capsule.yaml.template b/conf/capsule.yaml.template index e6747faa5eb..9ef9e48a2c2 100644 --- a/conf/capsule.yaml.template +++ b/conf/capsule.yaml.template @@ -10,6 +10,8 @@ CAPSULE: # The base os rhel version where the capsule installed # RHEL_VERSION: # The Ansible Tower workflow used to deploy a capsule - DEPLOY_WORKFLOW: deploy-capsule + DEPLOY_WORKFLOWS: + PRODUCT: deploy-capsule # workflow to deploy OS with product running on top of it + OS: deploy-rhel # workflow to deploy OS that is ready to run the product # Dictionary of arguments which should be passed along to the deploy workflow DEPLOY_ARGUMENTS: diff --git a/conf/dynaconf_hooks.py b/conf/dynaconf_hooks.py index 116207aa165..6d09d6e5bec 100644 --- a/conf/dynaconf_hooks.py +++ b/conf/dynaconf_hooks.py @@ -1,5 +1,9 @@ +from inspect import getmembers, isfunction import json from pathlib import Path +import sys + +from box import Box from robottelo.logging import logger from robottelo.utils.ohsnap import dogfood_repository @@ -22,6 +26,7 @@ def post(settings): ) data = get_repos_config(settings) write_cache(settings_cache_path, data) + config_migrations(settings, data) data['dynaconf_merge'] = True return data @@ -33,7 +38,32 @@ def write_cache(path, data): def read_cache(path): logger.info(f'Using settings cache file: {path}') - return json.loads(path.read_text()) + return Box(json.loads(path.read_text())) + + +def config_migrations(settings, data): + """Run config migrations + + Fetch the config migrations from the conf/migrations.py file and run them. + + :param settings: dynaconf settings object + :type settings: LazySettings + :param data: settings data to be merged with the rest of the settings + :type data: dict + """ + logger.info('Running config migration hooks') + sys.path.append(str(Path(__file__).parent)) + from conf import migrations + + migration_functions = [ + mf for mf in getmembers(migrations, isfunction) if mf[0].startswith('migration_') + ] + # migration_functions is a sorted list of tuples (name, function) + for name, func in migration_functions: + logger.debug(f'Running {name}') + func(settings, data) + logger.debug(f'Finished running {name}') + logger.info('Finished running config migration hooks') def get_repos_config(settings): @@ -46,7 +76,7 @@ def get_repos_config(settings): 'The Ohsnap URL is invalid! Post-configuration hooks will not run. ' 'Default configuration will be used.' ) - return {'REPOS': data} + return Box({'REPOS': data}) def get_ohsnap_repos(settings): diff --git a/conf/migrations.py b/conf/migrations.py new file mode 100644 index 00000000000..b263fa352d6 --- /dev/null +++ b/conf/migrations.py @@ -0,0 +1,37 @@ +"""Robottelo configuration migrations + +This module contains functions that are run after the configuration is loaded. Each function +should be named `migration__` and accept two parameters: `settings` and +`data`. `settings` is a `dynaconf` `Box` object containing the configuration. `data` is a +`dict` that can be used to store settings that will be merged with the rest of the settings. +The functions should not return anything. +""" + +from packaging.version import Version + +from robottelo.logging import logger + + +def migration_231129_deploy_workflow(settings, data): + """Migrates {server,capsule}.deploy_workflow to {server,capsule}.deploy_workflows""" + for product_type in ['server', 'capsule']: + # If the product_type has a deploy_workflow and it is a string, and + # it does not have a deploy_workflows set + if ( + settings[product_type].get('deploy_workflow') + and isinstance(settings[product_type].deploy_workflow, str) + and not settings[product_type].get('deploy_workflows') + ): + sat_rhel_version = settings[product_type].version.rhel_version + data[product_type] = {} + # Set the deploy_workflows to a dict with product and os keys + # Get the OS workflow from the content_host config + data[product_type].deploy_workflows = { + 'product': settings[product_type].deploy_workflow, + 'os': settings.content_host[ + f'rhel{Version(str(sat_rhel_version)).major}' + ].vm.workflow, + } + logger.info( + f'Migrated {product_type}.DEPLOY_WORKFLOW to {product_type}.DEPLOY_WORKFLOWS' + ) diff --git a/conf/server.yaml.template b/conf/server.yaml.template index 7f876bccaba..c5b844e6509 100644 --- a/conf/server.yaml.template +++ b/conf/server.yaml.template @@ -27,7 +27,9 @@ SERVER: # this setting determines if they will be automatically checked in AUTO_CHECKIN: False # The Ansible Tower workflow used to deploy a satellite - DEPLOY_WORKFLOW: "deploy-sat-jenkins" + DEPLOY_WORKFLOWS: + PRODUCT: deploy-satellite # workflow to deploy OS with product running on top of it + OS: deploy-rhel # workflow to deploy OS that is ready to run the product # Dictionary of arguments which should be passed along to the deploy workflow # DEPLOY_ARGUMENTS: # HTTP scheme when building the server URL diff --git a/pytest_fixtures/core/sat_cap_factory.py b/pytest_fixtures/core/sat_cap_factory.py index 8f83704702e..436d8b6d443 100644 --- a/pytest_fixtures/core/sat_cap_factory.py +++ b/pytest_fixtures/core/sat_cap_factory.py @@ -63,7 +63,7 @@ def factory(retry_limit=3, delay=300, workflow=None, **broker_args): vmb = Broker( host_class=Satellite, - workflow=workflow or settings.server.deploy_workflow, + workflow=workflow or settings.server.deploy_workflows.product, **broker_args, ) timeout = (1200 + delay) * retry_limit @@ -95,7 +95,7 @@ def factory(retry_limit=3, delay=300, workflow=None, **broker_args): broker_args.update(settings.capsule.deploy_arguments) vmb = Broker( host_class=Capsule, - workflow=workflow or settings.capsule.deploy_workflow, + workflow=workflow or settings.capsule.deploy_workflows.product, **broker_args, ) timeout = (1200 + delay) * retry_limit @@ -202,7 +202,7 @@ def module_lb_capsule(retry_limit=3, delay=300, **broker_args): timeout = (1200 + delay) * retry_limit hosts = Broker( host_class=Capsule, - workflow=settings.capsule.deploy_workflow, + workflow=settings.capsule.deploy_workflows.product, _count=2, **broker_args, ) @@ -245,12 +245,13 @@ def parametrized_enrolled_sat( def get_deploy_args(request): + """Get deploy arguments for Satellite base OS deployment. Should not be used for Capsule.""" rhel_version = get_sat_rhel_version() deploy_args = { 'deploy_rhel_version': rhel_version.base_version, 'deploy_flavor': settings.flavors.default, 'promtail_config_template_file': 'config_sat.j2', - 'workflow': settings.content_host.get(f'rhel{rhel_version.major}').vm.workflow, + 'workflow': settings.server.deploy_workflows.os, } if hasattr(request, 'param'): if isinstance(request.param, dict): @@ -277,11 +278,11 @@ def module_sat_ready_rhels(request): @pytest.fixture def cap_ready_rhel(): rhel_version = Version(settings.capsule.version.release) - settings.content_host.get(f'rhel{rhel_version.major}').vm.workflow deploy_args = { 'deploy_rhel_version': rhel_version.base_version, 'deploy_flavor': settings.flavors.default, - 'workflow': settings.content_host.get(f'rhel{rhel_version.major}').vm.workflow, + 'promtail_config_template_file': 'config_sat.j2', + 'workflow': settings.capsule.deploy_workflows.os, } with Broker(**deploy_args, host_class=Capsule) as host: yield host diff --git a/robottelo/config/validators.py b/robottelo/config/validators.py index 383abfb7aed..f598821839d 100644 --- a/robottelo/config/validators.py +++ b/robottelo/config/validators.py @@ -23,7 +23,9 @@ ), Validator('server.admin_password', default='changeme'), Validator('server.admin_username', default='admin'), - Validator('server.deploy_workflow', must_exist=True), + Validator('server.deploy_workflows', must_exist=True, is_type_of=dict), + Validator('server.deploy_workflows.product', must_exist=True), + Validator('server.deploy_workflows.os', must_exist=True), Validator('server.deploy_arguments', must_exist=True, is_type_of=dict, default={}), Validator('server.scheme', default='https'), Validator('server.port', default=443), @@ -67,7 +69,9 @@ capsule=[ Validator('capsule.version.release', must_exist=True), Validator('capsule.version.source', must_exist=True), - Validator('capsule.deploy_workflow', must_exist=True), + Validator('capsule.deploy_workflows', must_exist=True, is_type_of=dict), + Validator('capsule.deploy_workflows.product', must_exist=True), + Validator('capsule.deploy_workflows.os', must_exist=True), Validator('capsule.deploy_arguments', must_exist=True, is_type_of=dict, default={}), ], certs=[ diff --git a/robottelo/hosts.py b/robottelo/hosts.py index b6c1be14a41..a975ebeb215 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -73,7 +73,7 @@ def lru_sat_ready_rhel(rhel_ver): 'deploy_rhel_version': rhel_version, 'deploy_flavor': settings.flavors.default, 'promtail_config_template_file': 'config_sat.j2', - 'workflow': settings.content_host.get(f'rhel{Version(rhel_version).major}').vm.workflow, + 'workflow': settings.server.deploy_workflows.os, } sat_ready_rhel = Broker(**deploy_args, host_class=Satellite).checkout() return sat_ready_rhel From 0b037ba7ea58f7fe56be19f1067b635813f80a37 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Dec 2023 09:43:41 -0500 Subject: [PATCH 363/586] [6.14.z] Add default provider/url for make_compute_resource helper (#13313) Add default provider/url for make_compute_resource helper (#13283) Signed-off-by: Gaurav Talreja (cherry picked from commit 8015e0a11e5e0814bb1f8ca86250c99c7cce7843) Co-authored-by: Gaurav Talreja --- robottelo/host_helpers/cli_factory.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/robottelo/host_helpers/cli_factory.py b/robottelo/host_helpers/cli_factory.py index bcf7120eedf..9af5ee4cce4 100644 --- a/robottelo/host_helpers/cli_factory.py +++ b/robottelo/host_helpers/cli_factory.py @@ -179,6 +179,8 @@ def create_object(cli_object, options, values=None, credentials=None): }, 'compute_resource': { 'name': gen_alphanumeric, + 'provider': 'Libvirt', + 'url': 'qemu+tcp://localhost:16509/system', }, 'org': {'_redirect': 'org_with_credentials'}, 'org_with_credentials': { From 3159a9ddce9b54926457ae3e187dd5959e2f1c4a Mon Sep 17 00:00:00 2001 From: Jacob Callahan Date: Fri, 1 Dec 2023 16:44:04 -0500 Subject: [PATCH 364/586] Multiple enhancements to scripts/fixture_cli.py The changes included in this commit are: - Run fixtures in separate tests so failing fixtures don't block others - Add the ability to run fixtures that are indirectly parametrized - Add an option to switch to verbose mode, showing test execution and results. This should help with debugging and generally seeing what' happening - Add an option to run the fixtures in multiple xdist workers (cherry picked from commit e86c4366eaef5e065f7039484f4852e5bcab3171) --- scripts/fixture_cli.py | 54 +++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/scripts/fixture_cli.py b/scripts/fixture_cli.py index f8eb7cf3424..5963394e21f 100644 --- a/scripts/fixture_cli.py +++ b/scripts/fixture_cli.py @@ -4,41 +4,79 @@ import pytest +def fixture_to_test(fixture_name): + """Convert a fixture name to a test name. + + Basic Example: fixture_to_test("module_published_cv") + Returns: "def test_run_module_published_cv(module_published_cv):\n assert True" + + Parametrized Example: fixture_to_test("sat_azure:sat,puppet_sat") + Returns: "@pytest.mark.parametrize('sat_azure', ['sat', 'puppet_sat'], indirect=True)" + "\ndef test_run_sat_azure(sat_azure):\n assert True" + """ + if ":" not in fixture_name: + return f"def test_runfake_{fixture_name}({fixture_name}):\n assert True" + else: + fixture_name, params = fixture_name.split(":") + params = params.split(",") + return ( + f"@pytest.mark.parametrize('{fixture_name}', {params}, indirect=True)\n" + f"def test_runfake_{fixture_name}({fixture_name}):\n assert True" + ) + + @click.command() @click.argument("fixtures", nargs=-1, required=True) @click.option( "--from-file", - "-f", type=click.File("w"), help="Run the fixtures from within a file, inheriting the file's context.", ) -def run_fixtures(fixtures, from_file): +@click.option( + "--verbose", + is_flag=True, + help="Toggle verbose mode (default is quiet).", +) +@click.option( + "--xdist-workers", + "-n", + type=int, + default=1, + help="Run the tests in parallel with xdist.", +) +def run_fixtures(fixtures, from_file, verbose, xdist_workers): """Create a temporary test that depends on each fixture, then run it. You can also run the fixtures from the context of a file, which is useful when testing fixtures that don't live at a global scope. + Indirectly parametrized fixtures are also possible with this syntax: fixture_name:param1,param2,param3 + Examples: python scripts/fixture_cli.py module_published_cv module_subscribe_satellite python scripts/fixture_cli.py module_lce --from-file tests/foreman/api/test_activationkey.py + python scripts/fixture_cli.py sat_azure:sat,puppet_sat """ - fixture_string = ", ".join(filter(None, fixtures)) - test_template = f"def test_fake({fixture_string}):\n assert True" + verbosity = "-v" if verbose else "-qq" + xdist_workers = str(xdist_workers) # pytest expects a string + generated_tests = "import pytest\n\n" + "\n\n".join(map(fixture_to_test, fixtures)) if from_file: from_file = Path(from_file.name) # inject the test at the end of the file with from_file.open("a") as f: eof_pos = f.tell() - f.write(f"\n\n{test_template}") - pytest.main(["-qq", str(from_file.resolve()), "-k", "test_fake"]) + f.write(f"\n\n{generated_tests}") + pytest.main( + [verbosity, "-n", xdist_workers, str(from_file.resolve()), "-k", "test_runfake_"] + ) # remove the test from the file with from_file.open("r+") as f: f.seek(eof_pos) f.truncate() else: temp_file = Path("test_DELETEME.py") - temp_file.write_text(test_template) - pytest.main(["-qq", str(temp_file)]) + temp_file.write_text(generated_tests) + pytest.main([verbosity, "-n", xdist_workers, str(temp_file)]) temp_file.unlink() From ef6edbcddf1136e0fff1d47e07b6808cfd650129 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Dec 2023 23:00:59 -0500 Subject: [PATCH 365/586] [6.14.z] Bump productmd from 1.37 to 1.38 (#13336) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 01904f0c611..2d0acf016ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ fauxfactory==3.1.0 jinja2==3.1.2 manifester==0.0.14 navmazing==1.2.2 -productmd==1.37 +productmd==1.38 pyotp==2.9.0 python-box==7.1.1 pytest==7.4.3 From 6906f00f9a9737378c8d36173ea45a4c84c33408 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Dec 2023 23:01:59 -0500 Subject: [PATCH 366/586] [6.14.z] Bump actions/stale from 8 to 9 (#13337) --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 85daa8f4292..2c5edda7dda 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -16,7 +16,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@v8 + - uses: actions/stale@v9 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-issue-stale: 90 From 86bc15eb1757194bce79a872fbd7dedff3ef5d76 Mon Sep 17 00:00:00 2001 From: vijay sawant Date: Fri, 8 Dec 2023 16:39:41 +0530 Subject: [PATCH 367/586] Fix incremental update longrun 6 14 z (#13286) fix for increamental update test scenario --- tests/foreman/longrun/test_inc_updates.py | 55 ++++++++++++----------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/tests/foreman/longrun/test_inc_updates.py b/tests/foreman/longrun/test_inc_updates.py index d305c9fe180..12c32a61c7c 100644 --- a/tests/foreman/longrun/test_inc_updates.py +++ b/tests/foreman/longrun/test_inc_updates.py @@ -66,19 +66,19 @@ def qe_lce(module_entitlement_manifest_org, dev_lce): @pytest.fixture(scope='module') -def rhel7_sat6tools_repo(module_entitlement_manifest_org, module_target_sat): +def sat_client_repo(module_entitlement_manifest_org, module_target_sat): """Enable Sat tools repository""" - rhel7_sat6tools_repo_id = module_target_sat.api_factory.enable_rhrepo_and_fetchid( + sat_client_repo_id = module_target_sat.api_factory.enable_rhrepo_and_fetchid( basearch=DEFAULT_ARCHITECTURE, org_id=module_entitlement_manifest_org.id, product=PRDS['rhel'], - repo=REPOS['rhst7']['name'], - reposet=REPOSET['rhst7'], + repo=REPOS['rhsclient7']['name'], + reposet=REPOSET['rhsclient7'], releasever=None, ) - rhel7_sat6tools_repo = entities.Repository(id=rhel7_sat6tools_repo_id).read() - assert rhel7_sat6tools_repo.sync()['result'] == 'success' - return rhel7_sat6tools_repo + sat_client_repo = module_target_sat.api.Repository(id=sat_client_repo_id).read() + assert sat_client_repo.sync()['result'] == 'success' + return sat_client_repo @pytest.fixture(scope='module') @@ -93,11 +93,11 @@ def custom_repo(module_entitlement_manifest_org): @pytest.fixture(scope='module') -def module_cv(module_entitlement_manifest_org, rhel7_sat6tools_repo, custom_repo): +def module_cv(module_entitlement_manifest_org, sat_client_repo, custom_repo): """Publish both repos into module CV""" module_cv = entities.ContentView( organization=module_entitlement_manifest_org, - repository=[rhel7_sat6tools_repo.id, custom_repo.id], + repository=[sat_client_repo.id, custom_repo.id], ).create() module_cv.publish() module_cv = module_cv.read() @@ -124,7 +124,7 @@ def module_ak(module_entitlement_manifest_org, module_cv, custom_repo, module_lc assert sub_found # Enable RHEL product content in activation key ak.content_override( - data={'content_overrides': [{'content_label': REPOS['rhst7']['id'], 'value': '1'}]} + data={'content_overrides': [{'content_label': REPOS['rhsclient7']['id'], 'value': '1'}]} ) # Add custom subscription to activation key prod = custom_repo.product.read() @@ -132,6 +132,14 @@ def module_ak(module_entitlement_manifest_org, module_cv, custom_repo, module_lc query={'search': f'name={prod.name}'} ) ak.add_subscriptions(data={'subscription_id': custom_sub[0].id}) + # Enable custom repo in activation key + all_content = ak.product_content(data={'content_access_mode_all': '1'})['results'] + for content in all_content: + if content['name'] == custom_repo.name: + content_label = content['label'] + ak.content_override( + data={'content_overrides': [{'content_label': content_label, 'value': '1'}]} + ) return ak @@ -147,18 +155,16 @@ def host( module_target_sat, ): # Create client machine and register it to satellite with rhel_7_partial_ak - rhel7_contenthost_module.install_katello_ca(module_target_sat) # Register, enable tools repo and install katello-host-tools. - rhel7_contenthost_module.register_contenthost( - module_entitlement_manifest_org.label, module_ak.name + rhel7_contenthost_module.register( + module_entitlement_manifest_org, None, module_ak.name, module_target_sat ) - rhel7_contenthost_module.enable_repo(REPOS['rhst7']['id']) - rhel7_contenthost_module.install_katello_host_tools() + rhel7_contenthost_module.enable_repo(REPOS['rhsclient7']['id']) # make a note of time for later wait_for_tasks, and include 4 mins margin of safety. timestamp = (datetime.utcnow() - timedelta(minutes=4)).strftime('%Y-%m-%d %H:%M') # AK added custom repo for errata package, just install it. rhel7_contenthost_module.execute(f'yum install -y {FAKE_4_CUSTOM_PACKAGE}') - rhel7_contenthost_module.execute('katello-package-upload') + rhel7_contenthost_module.execute('subscription-manager repos') # Wait for applicability update event (in case Satellite system slow) module_target_sat.wait_for_tasks( search_query='label = Actions::Katello::Applicability::Hosts::BulkGenerate' @@ -207,28 +213,25 @@ def test_positive_noapply_api( versions = sorted(module_cv.read().version, key=lambda ver: ver.id) cvv = versions[-1].read() cvv.promote(data={'environment_ids': dev_lce.id}) - # Read CV to pick up LCE ID and next_version - module_cv = module_cv.read() - # Get the content view versions and use the recent one. API always - # returns the versions in ascending order (last in the list is most recent) - cv_versions = module_cv.version # Get the applicable errata errata_list = get_applicable_errata(custom_repo) assert len(errata_list) > 0 # Apply incremental update using the first applicable errata - entities.ContentViewVersion().incremental_update( + outval = entities.ContentViewVersion().incremental_update( data={ 'content_view_version_environments': [ { - 'content_view_version_id': cv_versions[-1].id, + 'content_view_version_id': cvv.id, 'environment_ids': [dev_lce.id], } ], 'add_content': {'errata_ids': [errata_list[0].id]}, } ) - # Re-read the content view to get the latest versions - module_cv = module_cv.read() - assert len(module_cv.version) > len(cv_versions) + assert outval['result'] == 'success' + assert ( + outval['action'] + == 'Incremental Update of 1 Content View Version(s) with 1 Package(s), and 1 Errata' + ) From 69e49bd967cfa6734c53219a45ad66346f11c2c0 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 8 Dec 2023 07:30:05 -0500 Subject: [PATCH 368/586] [6.14.z] Remove unnecessary on_premises_provisioning marker (#13345) Remove unnecessary on_premises_provisioning marker (#13343) Remove unecessary on premise marker (cherry picked from commit fbe723c719e95efbcab9e3c328007095e70393b8) Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> --- tests/foreman/ui/test_location.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/foreman/ui/test_location.py b/tests/foreman/ui/test_location.py index b86484ff392..1142dbe878c 100644 --- a/tests/foreman/ui/test_location.py +++ b/tests/foreman/ui/test_location.py @@ -196,7 +196,6 @@ def test_positive_add_org_hostgroup_template(session): @pytest.mark.skip_if_not_set('libvirt') -@pytest.mark.on_premises_provisioning @pytest.mark.tier2 def test_positive_update_compresource(session): """Add/Remove compute resource from/to location From 62f9121f28d8bc3c85825b4ec94f93997106c97d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 8 Dec 2023 10:47:59 -0500 Subject: [PATCH 369/586] [6.14.z] Use module_extra_rhel_entitlement_manifest for cloud connector test (#13349) --- tests/foreman/ui/test_rhc.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/tests/foreman/ui/test_rhc.py b/tests/foreman/ui/test_rhc.py index 2ffe7b11e12..57095979bca 100644 --- a/tests/foreman/ui/test_rhc.py +++ b/tests/foreman/ui/test_rhc.py @@ -16,9 +16,10 @@ :Upstream: No """ +from datetime import datetime, timedelta + from fauxfactory import gen_string import pytest -from wait_for import wait_for from robottelo import constants from robottelo.config import settings @@ -69,11 +70,13 @@ def fixture_setup_rhc_satellite( request, module_target_sat, module_rhc_org, - module_entitlement_manifest, + module_extra_rhel_entitlement_manifest, ): """Create Organization and activation key after successful test execution""" if settings.rh_cloud.crc_env == 'prod': - module_target_sat.upload_manifest(module_rhc_org.id, module_entitlement_manifest.content) + module_target_sat.upload_manifest( + module_rhc_org.id, module_extra_rhel_entitlement_manifest.content + ) yield if request.node.rep_call.passed: # Enable and sync required repos @@ -145,7 +148,7 @@ def test_positive_configure_cloud_connector( parameters = [{'name': 'source_display_name', 'value': gen_string('alpha')}] host.host_parameters_attributes = parameters host.update(['host_parameters_attributes']) - + timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime('%Y-%m-%d %H:%M') with module_target_sat.ui_session() as session: session.organization.select(org_name=module_rhc_org.name) if session.cloudinventory.is_cloud_connector_configured(): @@ -155,19 +158,19 @@ def test_positive_configure_cloud_connector( 'check if everything is correctly configured from scratch. Skipping.' ) session.cloudinventory.configure_cloud_connector() - template_name = 'Configure Cloud Connector' + module_target_sat.wait_for_tasks( + search_query=( + f'action = "Run hosts job: {template_name}"' f' and started_at >= "{timestamp}"' + ), + search_rate=15, + max_tries=10, + ) invocation_id = ( module_target_sat.api.JobInvocation() .search(query={'search': f'description="{template_name}"'})[0] .id ) - wait_for( - lambda: module_target_sat.api.JobInvocation(id=invocation_id).read().status_label - in ["succeeded", "failed"], - timeout="1500s", - ) - job_output = module_target_sat.cli.JobInvocation.get_output( {'id': invocation_id, 'host': module_target_sat.hostname} ) From d11416f20529d9f9782a12b23cc4cb902b14faf9 Mon Sep 17 00:00:00 2001 From: jyejare Date: Thu, 7 Dec 2023 16:44:06 +0530 Subject: [PATCH 370/586] Test markers as test fields in polarion (cherry picked from commit 0bfd8961120678de2864deedd22967762939818b) --- scripts/polarion-test-case-upload.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/polarion-test-case-upload.sh b/scripts/polarion-test-case-upload.sh index 58df170e361..987a77d6466 100755 --- a/scripts/polarion-test-case-upload.sh +++ b/scripts/polarion-test-case-upload.sh @@ -68,7 +68,7 @@ from betelgeuse import default_config DEFAULT_APPROVERS_VALUE = '${POLARION_USERNAME}:approved' DEFAULT_STATUS_VALUE = 'approved' DEFAULT_SUBTYPE2_VALUE = '-' -TESTCASE_CUSTOM_FIELDS = default_config.TESTCASE_CUSTOM_FIELDS + ('customerscenario',) + ('team',) +TESTCASE_CUSTOM_FIELDS = default_config.TESTCASE_CUSTOM_FIELDS + ('customerscenario',) + ('team',) + ('markers',) REQUIREMENT_CUSTOM_FIELDS = default_config.REQUIREMENT_CUSTOM_FIELDS + ('team',) TRANSFORM_CUSTOMERSCENARIO_VALUE = default_config._transform_to_lower DEFAULT_CUSTOMERSCENARIO_VALUE = 'false' From ddaad7bb962dc3a7f1d886f89d8343eb39bf04f9 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Sun, 10 Dec 2023 22:21:37 -0500 Subject: [PATCH 371/586] [6.14.z] Bump pre-commit from 3.5.0 to 3.6.0 (#13361) --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 8d8abe4d798..24b8b4c6001 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -2,7 +2,7 @@ flake8==6.1.0 pytest-cov==4.1.0 redis==5.0.1 -pre-commit==3.5.0 +pre-commit==3.6.0 # For generating documentation. sphinx==7.2.6 From 299fb63d3352c310c60dc8d98afd1e203c7fb69e Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 11 Dec 2023 10:12:14 -0500 Subject: [PATCH 372/586] [6.14.z] Fix AK update tests - org required (#13366) Fix AK update tests - org required (#13364) (cherry picked from commit 87f501f0d72f29fcd4f5a21b76d59d45e002d905) Co-authored-by: vsedmik <46570670+vsedmik@users.noreply.github.com> --- tests/foreman/api/test_activationkey.py | 30 +++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/tests/foreman/api/test_activationkey.py b/tests/foreman/api/test_activationkey.py index 16e968ed7e8..d909c4b87ab 100644 --- a/tests/foreman/api/test_activationkey.py +++ b/tests/foreman/api/test_activationkey.py @@ -181,7 +181,7 @@ def test_positive_update_limited_host(max_host, target_sat): @pytest.mark.tier2 @pytest.mark.parametrize('new_name', **parametrized(valid_data_list())) -def test_positive_update_name(new_name, target_sat): +def test_positive_update_name(new_name, target_sat, module_org): """Create activation key providing the initial name, then update its name to another valid name. @@ -192,8 +192,10 @@ def test_positive_update_name(new_name, target_sat): :parametrized: yes """ - act_key = target_sat.api.ActivationKey().create() - updated = target_sat.api.ActivationKey(id=act_key.id, name=new_name).update(['name']) + act_key = target_sat.api.ActivationKey(organization=module_org).create() + updated = target_sat.api.ActivationKey( + id=act_key.id, organization=module_org, name=new_name + ).update(['name']) assert new_name == updated.name @@ -227,7 +229,7 @@ def test_negative_update_limit(max_host, target_sat): @pytest.mark.tier3 @pytest.mark.parametrize('new_name', **parametrized(invalid_names_list())) -def test_negative_update_name(new_name, target_sat): +def test_negative_update_name(new_name, target_sat, module_org): """Create activation key then update its name to an invalid name. :id: da85a32c-942b-4ab8-a133-36b028208c4d @@ -239,16 +241,18 @@ def test_negative_update_name(new_name, target_sat): :parametrized: yes """ - act_key = target_sat.api.ActivationKey().create() + act_key = target_sat.api.ActivationKey(organization=module_org).create() with pytest.raises(HTTPError): - target_sat.api.ActivationKey(id=act_key.id, name=new_name).update(['name']) + target_sat.api.ActivationKey(id=act_key.id, organization=module_org, name=new_name).update( + ['name'] + ) new_key = target_sat.api.ActivationKey(id=act_key.id).read() assert new_key.name != new_name assert new_key.name == act_key.name @pytest.mark.tier3 -def test_negative_update_max_hosts(target_sat): +def test_negative_update_max_hosts(target_sat, module_org): """Create an activation key with ``max_hosts == 1``, then update that field with a string value. @@ -258,9 +262,11 @@ def test_negative_update_max_hosts(target_sat): :CaseImportance: Low """ - act_key = target_sat.api.ActivationKey(max_hosts=1).create() + act_key = target_sat.api.ActivationKey(max_hosts=1, organization=module_org).create() with pytest.raises(HTTPError): - target_sat.api.ActivationKey(id=act_key.id, max_hosts='foo').update(['max_hosts']) + target_sat.api.ActivationKey( + id=act_key.id, organization=module_org, max_hosts='foo' + ).update(['max_hosts']) assert act_key.read().max_hosts == 1 @@ -372,7 +378,7 @@ def test_positive_remove_host_collection(module_org, module_target_sat): @pytest.mark.tier1 -def test_positive_update_auto_attach(target_sat): +def test_positive_update_auto_attach(target_sat, module_org): """Create an activation key, then update the auto_attach field with the inverse boolean value. @@ -382,9 +388,9 @@ def test_positive_update_auto_attach(target_sat): :CaseImportance: Critical """ - act_key = target_sat.api.ActivationKey().create() + act_key = target_sat.api.ActivationKey(organization=module_org).create() act_key_2 = target_sat.api.ActivationKey( - id=act_key.id, auto_attach=(not act_key.auto_attach) + id=act_key.id, organization=module_org, auto_attach=(not act_key.auto_attach) ).update(['auto_attach']) assert act_key.auto_attach != act_key_2.auto_attach From 17a7043438fb2272972f0d71edefdb92f7cdef9a Mon Sep 17 00:00:00 2001 From: Samuel Bible Date: Mon, 11 Dec 2023 13:04:54 -0600 Subject: [PATCH 373/586] Fix missing api param in target_sat call in SyncPlan test (#13370) (cherry picked from commit 3a8d82b72a52ff92ce7b865fa1c42ebf583cad12) --- tests/foreman/api/test_syncplan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/api/test_syncplan.py b/tests/foreman/api/test_syncplan.py index 566d06d692a..dec71b6b9c3 100644 --- a/tests/foreman/api/test_syncplan.py +++ b/tests/foreman/api/test_syncplan.py @@ -837,7 +837,7 @@ def test_positive_synchronize_rh_product_past_sync_date( ) product = target_sat.api.Product(name=PRDS['rhel'], organization=org).search()[0] repo = target_sat.api.Repository(id=repo_id).read() - sync_plan = target_sat.SyncPlan( + sync_plan = target_sat.api.SyncPlan( organization=org, enabled=True, interval='hourly', From b19d01f8a12f66aa9b0eeda4b7f5c3b5adad384e Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 12 Dec 2023 03:15:27 -0500 Subject: [PATCH 374/586] [6.14.z] Update yum repo for ACSes (#13386) --- robottelo/constants/repos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robottelo/constants/repos.py b/robottelo/constants/repos.py index bf54b5518bc..9738894a689 100644 --- a/robottelo/constants/repos.py +++ b/robottelo/constants/repos.py @@ -2,7 +2,7 @@ PULP_FIXTURE_ROOT = 'https://fixtures.pulpproject.org/' PULP_SUBPATHS_COMBINED = { - 'yum': ['rpm-zchunk/', 'rpm-modular/'], + 'yum': ['rpm-zstd-metadata/', 'rpm-modular/'], 'file': ['file-large/', 'file-many/'], } CUSTOM_3RD_PARTY_REPO = 'http://repo.calcforge.org/fedora/21/x86_64/' From bc682be76a9340a2d499b8a025cfba6f42932963 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 12 Dec 2023 03:52:34 -0500 Subject: [PATCH 375/586] [6.14.z] Automation for BZ 2119112 - subpaths field is mandatory while creating ACS in the UI (#13380) Automation for BZ 2119112 - subpaths field is mandatory while creating ACS in the UI (#13369) Add test checking that subpath field isn't mandatory (cherry picked from commit a32647357f86d3f63fa25c4ff17d35cfef09c307) Co-authored-by: Samuel Bible --- tests/foreman/ui/test_acs.py | 54 ++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/foreman/ui/test_acs.py b/tests/foreman/ui/test_acs.py index 09809d16075..9705bc0cd8f 100644 --- a/tests/foreman/ui/test_acs.py +++ b/tests/foreman/ui/test_acs.py @@ -64,6 +64,60 @@ def acs_setup(class_target_sat, class_sca_manifest_org): return class_target_sat, class_sca_manifest_org +@pytest.mark.tier2 +def test_acs_subpath_not_required(acs_setup): + """ + This test verifies that the subpath field isn't mandatory for ACS creation. + + :id: 232d944a-a7c1-4387-ab01-7e20b2bbebfa + + :steps: + 1. Create an ACS of each type where subpath field is present in the UI + + :expectedresults: + Subpath field isn't required in the creation of any ACS where it's present, + so ACS should all create successfully + + :BZ: 2119112 + + :customerscenario: True + """ + class_target_sat, class_sca_manifest_org = acs_setup + + with class_target_sat.ui_session() as session: + session.organization.select(org_name=class_sca_manifest_org.name) + + # Create an ACS of each configturation that displays the subpath field in UI + session.acs.create_new_acs( + custom_type=True, + content_type='yum', + name=gen_string('alpha'), + capsules_to_add=class_target_sat.hostname, + use_http_proxies=True, + base_url='https://test.com', + none_auth=True, + ) + + session.acs.create_new_acs( + custom_type=True, + content_type='file', + name=gen_string('alpha'), + capsules_to_add=class_target_sat.hostname, + use_http_proxies=True, + base_url='https://test.com', + none_auth=True, + ) + + session.acs.create_new_acs( + rhui_type=True, + name=gen_string('alpha'), + capsules_to_add=class_target_sat.hostname, + use_http_proxies=True, + base_url='https://rhui-server.example.com/pulp/content', + none_auth=True, + ) + + class TestAllAcsTypes: """ Test class insuring fixture is ran once before From 97070e814b93f6e9340657743fe0dbe35d9e454a Mon Sep 17 00:00:00 2001 From: Peter Ondrejka Date: Tue, 12 Dec 2023 12:07:45 +0100 Subject: [PATCH 376/586] [6.14] added available_remote_execution_features apipath (#13363) added available_remote_execution_features apipath --- tests/foreman/endtoend/test_api_endtoend.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/foreman/endtoend/test_api_endtoend.py b/tests/foreman/endtoend/test_api_endtoend.py index 47c28ffbefd..0b32f10c460 100644 --- a/tests/foreman/endtoend/test_api_endtoend.py +++ b/tests/foreman/endtoend/test_api_endtoend.py @@ -729,6 +729,7 @@ '/api/remote_execution_features', '/api/remote_execution_features/:id', '/api/remote_execution_features/:id', + '/api/api/hosts/:id/available_remote_execution_features', ), 'scap_content_profiles': ('/api/compliance/scap_content_profiles',), 'simple_content_access': ( From 3381ad2ae0cb84fc2c04f1ae3ac55b71cc68fc62 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 12 Dec 2023 06:08:51 -0500 Subject: [PATCH 377/586] [6.14.z] fixed api audit tests (#13377) fixed api audit tests (cherry picked from commit 2809bdb4018fc00974b1144f53d4fdd416e74eb3) Co-authored-by: Peter Ondrejka --- tests/foreman/api/test_audit.py | 66 +++++++++++++++++---------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/tests/foreman/api/test_audit.py b/tests/foreman/api/test_audit.py index 1aa9c28cc41..1eff8e5e599 100644 --- a/tests/foreman/api/test_audit.py +++ b/tests/foreman/api/test_audit.py @@ -39,7 +39,7 @@ def test_positive_create_by_type(target_sat): :CaseImportance: Medium """ for entity_item in [ - {'entity': target_sat.api.Architecture()}, + {'entity': target_sat.api.Architecture(), 'entity_type': 'architecture'}, { 'entity': target_sat.api.AuthSourceLDAP(), 'entity_type': 'auth_source', @@ -51,15 +51,16 @@ def test_positive_create_by_type(target_sat): 'entity_type': 'compute_resource', 'value_template': '{entity.name} (Libvirt)', }, - {'entity': target_sat.api.Domain()}, - {'entity': target_sat.api.Host()}, - {'entity': target_sat.api.HostGroup()}, + {'entity': target_sat.api.Domain(), 'entity_type': 'domain'}, + {'entity': target_sat.api.Host(), 'entity_type': 'host'}, + {'entity': target_sat.api.HostGroup(), 'entity_type': 'hostgroup'}, { 'entity': target_sat.api.Image( compute_resource=target_sat.api.LibvirtComputeResource().create() - ) + ), + 'entity_type': 'image', }, - {'entity': target_sat.api.Location()}, + {'entity': target_sat.api.Location(), 'entity_type': 'location'}, {'entity': target_sat.api.Media(), 'entity_type': 'medium'}, { 'entity': target_sat.api.OperatingSystem(), @@ -67,14 +68,19 @@ def test_positive_create_by_type(target_sat): 'value_template': '{entity.name} {entity.major}', }, {'entity': target_sat.api.PartitionTable(), 'entity_type': 'ptable'}, - {'entity': target_sat.api.Role()}, + {'entity': target_sat.api.Role(), 'entity_type': 'role'}, { 'entity': target_sat.api.Subnet(), + 'entity_type': 'subnet', 'value_template': '{entity.name} ({entity.network}/{entity.cidr})', }, {'entity': target_sat.api.ProvisioningTemplate(), 'entity_type': 'provisioning_template'}, - {'entity': target_sat.api.User(), 'value_template': '{entity.login}'}, - {'entity': target_sat.api.UserGroup()}, + { + 'entity': target_sat.api.User(), + 'value_template': '{entity.login}', + 'entity_type': 'user', + }, + {'entity': target_sat.api.UserGroup(), 'entity_type': 'usergroup'}, {'entity': target_sat.api.ContentView(), 'entity_type': 'katello/content_view'}, {'entity': target_sat.api.LifecycleEnvironment(), 'entity_type': 'katello/kt_environment'}, {'entity': target_sat.api.ActivationKey(), 'entity_type': 'katello/activation_key'}, @@ -86,10 +92,11 @@ def test_positive_create_by_type(target_sat): }, ]: created_entity = entity_item['entity'].create() - entity_type = entity_item.get('entity_type', created_entity.__class__.__name__.lower()) value_template = entity_item.get('value_template', '{entity.name}') entity_value = value_template.format(entity=created_entity) - audits = target_sat.api.Audit().search(query={'search': f'type={entity_type}'}) + audits = target_sat.api.Audit().search( + query={'search': f'type={entity_item["entity_type"]}'} + ) entity_audits = [entry for entry in audits if entry.auditable_name == entity_value] assert entity_audits, ( f'audit not found by name "{entity_value}" for entity: ' @@ -114,21 +121,19 @@ def test_positive_update_by_type(target_sat): :CaseImportance: Medium """ for entity in [ - target_sat.api.Architecture(), - target_sat.api.Domain(), - target_sat.api.HostGroup(), - target_sat.api.Location(), - target_sat.api.Role(), - target_sat.api.UserGroup(), + {'entity': target_sat.api.Architecture(), 'entity_type': 'architecture'}, + {'entity': target_sat.api.Domain(), 'entity_type': 'domain'}, + {'entity': target_sat.api.HostGroup(), 'entity_type': 'hostgroup'}, + {'entity': target_sat.api.Location(), 'entity_type': 'location'}, + {'entity': target_sat.api.Role(), 'entity_type': 'role'}, + {'entity': target_sat.api.UserGroup(), 'entity_type': 'usergroup'}, ]: - created_entity = entity.create() + created_entity = entity['entity'].create() name = created_entity.name new_name = gen_string('alpha') created_entity.name = new_name created_entity = created_entity.update(['name']) - audits = target_sat.api.Audit().search( - query={'search': f'type={created_entity.__class__.__name__.lower()}'} - ) + audits = target_sat.api.Audit().search(query={'search': f'type={entity["entity_type"]}'}) entity_audits = [entry for entry in audits if entry.auditable_name == name] assert entity_audits, f'audit not found by name "{name}"' audit = entity_audits[0] @@ -151,19 +156,16 @@ def test_positive_delete_by_type(target_sat): :CaseImportance: Medium """ for entity in [ - target_sat.api.Architecture(), - target_sat.api.Domain(), - target_sat.api.Host(), - target_sat.api.HostGroup(), - target_sat.api.Location(), - target_sat.api.Role(), - target_sat.api.UserGroup(), + {'entity': target_sat.api.Architecture(), 'entity_type': 'architecture'}, + {'entity': target_sat.api.Domain(), 'entity_type': 'domain'}, + {'entity': target_sat.api.HostGroup(), 'entity_type': 'hostgroup'}, + {'entity': target_sat.api.Location(), 'entity_type': 'location'}, + {'entity': target_sat.api.Role(), 'entity_type': 'role'}, + {'entity': target_sat.api.UserGroup(), 'entity_type': 'usergroup'}, ]: - created_entity = entity.create() + created_entity = entity['entity'].create() created_entity.delete() - audits = target_sat.api.Audit().search( - query={'search': f'type={created_entity.__class__.__name__.lower()}'} - ) + audits = target_sat.api.Audit().search(query={'search': f'type={entity["entity_type"]}'}) entity_audits = [entry for entry in audits if entry.auditable_name == created_entity.name] assert entity_audits, f'audit not found by name "{created_entity.name}"' audit = entity_audits[0] From 55a75586734d3c2935757a363872c14b46dc895c Mon Sep 17 00:00:00 2001 From: Shweta Singh Date: Tue, 12 Dec 2023 17:02:19 +0530 Subject: [PATCH 378/586] [6.14.z] Using SCA enabled manifest in activation key (#13394) Using SCA enabled manifest in activation key --- pytest_fixtures/component/activationkey.py | 9 +++-- tests/foreman/api/test_registration.py | 38 +++++++++++++--------- tests/foreman/cli/test_registration.py | 13 ++++---- tests/foreman/ui/test_host.py | 29 ++--------------- 4 files changed, 35 insertions(+), 54 deletions(-) diff --git a/pytest_fixtures/component/activationkey.py b/pytest_fixtures/component/activationkey.py index 9981d60f13d..fa7b53d0189 100644 --- a/pytest_fixtures/component/activationkey.py +++ b/pytest_fixtures/component/activationkey.py @@ -6,13 +6,12 @@ @pytest.fixture(scope='module') -def module_activation_key(module_entitlement_manifest_org, module_target_sat): +def module_activation_key(module_sca_manifest_org, module_target_sat): """Create activation key using default CV and library environment.""" activation_key = module_target_sat.api.ActivationKey( - auto_attach=True, - content_view=module_entitlement_manifest_org.default_content_view.id, - environment=module_entitlement_manifest_org.library.id, - organization=module_entitlement_manifest_org, + content_view=module_sca_manifest_org.default_content_view.id, + environment=module_sca_manifest_org.library.id, + organization=module_sca_manifest_org, ).create() return activation_key diff --git a/tests/foreman/api/test_registration.py b/tests/foreman/api/test_registration.py index f29da4c8e7a..dab85fa6209 100644 --- a/tests/foreman/api/test_registration.py +++ b/tests/foreman/api/test_registration.py @@ -30,9 +30,9 @@ @pytest.mark.e2e @pytest.mark.no_containers def test_host_registration_end_to_end( - module_org, + module_entitlement_manifest_org, module_location, - module_ak_with_synced_repo, + module_activation_key, module_target_sat, module_capsule_configured, rhel_contenthost, @@ -50,9 +50,10 @@ def test_host_registration_end_to_end( :customerscenario: true """ + org = module_entitlement_manifest_org command = module_target_sat.api.RegistrationCommand( - organization=module_org, - activation_keys=[module_ak_with_synced_repo.name], + organization=org, + activation_keys=[module_activation_key.name], location=module_location, ).create() @@ -66,13 +67,13 @@ def test_host_registration_end_to_end( # Update module_capsule_configured to include module_org/module_location nc = module_capsule_configured.nailgun_smart_proxy - module_target_sat.api.SmartProxy(id=nc.id, organization=[module_org]).update(['organization']) + module_target_sat.api.SmartProxy(id=nc.id, organization=[org]).update(['organization']) module_target_sat.api.SmartProxy(id=nc.id, location=[module_location]).update(['location']) command = module_target_sat.api.RegistrationCommand( smart_proxy=nc, - organization=module_org, - activation_keys=[module_ak_with_synced_repo.name], + organization=org, + activation_keys=[module_activation_key.name], location=module_location, force=True, ).create() @@ -92,7 +93,11 @@ def test_host_registration_end_to_end( @pytest.mark.tier3 @pytest.mark.rhel_ver_match('[^6]') def test_positive_allow_reregistration_when_dmi_uuid_changed( - module_org, rhel_contenthost, target_sat, module_ak_with_synced_repo, module_location + module_sca_manifest_org, + rhel_contenthost, + target_sat, + module_activation_key, + module_location, ): """Register a content host with a custom DMI UUID, unregistering it, change the DMI UUID, and re-registering it again @@ -109,10 +114,11 @@ def test_positive_allow_reregistration_when_dmi_uuid_changed( """ uuid_1 = str(uuid.uuid1()) uuid_2 = str(uuid.uuid4()) + org = module_sca_manifest_org target_sat.execute(f'echo \'{{"dmi.system.uuid": "{uuid_1}"}}\' > /etc/rhsm/facts/uuid.facts') command = target_sat.api.RegistrationCommand( - organization=module_org, - activation_keys=[module_ak_with_synced_repo.name], + organization=org, + activation_keys=[module_activation_key.name], location=module_location, ).create() result = rhel_contenthost.execute(command) @@ -121,8 +127,8 @@ def test_positive_allow_reregistration_when_dmi_uuid_changed( assert result.status == 0 target_sat.execute(f'echo \'{{"dmi.system.uuid": "{uuid_2}"}}\' > /etc/rhsm/facts/uuid.facts') command = target_sat.api.RegistrationCommand( - organization=module_org, - activation_keys=[module_ak_with_synced_repo.name], + organization=org, + activation_keys=[module_activation_key.name], location=module_location, ).create() result = rhel_contenthost.execute(command) @@ -131,7 +137,7 @@ def test_positive_allow_reregistration_when_dmi_uuid_changed( def test_positive_update_packages_registration( module_target_sat, - module_entitlement_manifest_org, + module_sca_manifest_org, module_location, rhel8_contenthost, module_activation_key, @@ -144,7 +150,7 @@ def test_positive_update_packages_registration( :CaseLevel: Component """ - org = module_entitlement_manifest_org + org = module_sca_manifest_org command = module_target_sat.api.RegistrationCommand( organization=org, location=module_location, @@ -164,7 +170,7 @@ def test_positive_update_packages_registration( @pytest.mark.no_containers def test_positive_rex_interface_for_global_registration( module_target_sat, - module_entitlement_manifest_org, + module_sca_manifest_org, module_location, rhel8_contenthost, module_activation_key, @@ -188,7 +194,7 @@ def test_positive_rex_interface_for_global_registration( add_interface_command = f'ip link add eth1 type dummy;ifconfig eth1 hw ether {mac_address};ip addr add {ip}/24 brd + dev eth1 label eth1:1;ip link set dev eth1 up' result = rhel8_contenthost.execute(add_interface_command) assert result.status == 0 - org = module_entitlement_manifest_org + org = module_sca_manifest_org command = module_target_sat.api.RegistrationCommand( organization=org, location=module_location, diff --git a/tests/foreman/cli/test_registration.py b/tests/foreman/cli/test_registration.py index 50102ae2c0e..25b8c0a2135 100644 --- a/tests/foreman/cli/test_registration.py +++ b/tests/foreman/cli/test_registration.py @@ -27,9 +27,9 @@ @pytest.mark.e2e @pytest.mark.no_containers def test_host_registration_end_to_end( - module_org, + module_entitlement_manifest_org, module_location, - module_ak_with_synced_repo, + module_activation_key, module_target_sat, module_capsule_configured, rhel_contenthost, @@ -47,8 +47,9 @@ def test_host_registration_end_to_end( :customerscenario: true """ + org = module_entitlement_manifest_org result = rhel_contenthost.register( - module_org, module_location, module_ak_with_synced_repo.name, module_target_sat + org, module_location, [module_activation_key.name], module_target_sat ) rc = 1 if rhel_contenthost.os_version.major == 6 else 0 @@ -62,14 +63,14 @@ def test_host_registration_end_to_end( module_target_sat.cli.Capsule.update( { 'name': module_capsule_configured.hostname, - 'organization-ids': module_org.id, + 'organization-ids': org.id, 'location-ids': module_location.id, } ) result = rhel_contenthost.register( - module_org, + org, module_location, - module_ak_with_synced_repo.name, + [module_activation_key.name], module_capsule_configured, force=True, ) diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index 4e6a929f38b..c4ae7a73773 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -34,7 +34,6 @@ ANY_CONTEXT, DEFAULT_CV, DEFAULT_LOC, - DEFAULT_SUBSCRIPTION_NAME, ENVIRONMENT, FAKE_1_CUSTOM_PACKAGE, FAKE_7_CUSTOM_PACKAGE, @@ -97,30 +96,6 @@ def module_global_params(module_target_sat): global_parameter.delete() -@pytest.fixture(scope='module') -def module_activation_key(module_entitlement_manifest_org, module_target_sat): - """Create activation key using default CV and library environment.""" - activation_key = module_target_sat.api.ActivationKey( - auto_attach=True, - content_view=module_entitlement_manifest_org.default_content_view.id, - environment=module_entitlement_manifest_org.library.id, - organization=module_entitlement_manifest_org, - ).create() - - # Find the 'Red Hat Employee Subscription' and attach it to the activation key. - for subs in module_target_sat.api.Subscription( - organization=module_entitlement_manifest_org - ).search(): - if subs.name == DEFAULT_SUBSCRIPTION_NAME: - # 'quantity' must be 1, not subscription['quantity']. Greater - # values produce this error: 'RuntimeError: Error: Only pools - # with multi-entitlement product subscriptions can be added to - # the activation key with a quantity greater than one.' - activation_key.add_subscriptions(data={'quantity': 1, 'subscription_id': subs.id}) - break - return activation_key - - @pytest.fixture def tracer_install_host(rex_contenthost, target_sat): """Sets up a contenthost with katello-host-tools-tracer enabled, @@ -2400,7 +2375,7 @@ def test_positive_tracer_enable_reload(tracer_install_host, target_sat): def test_positive_host_registration_with_non_admin_user( test_name, - module_entitlement_manifest_org, + module_sca_manifest_org, module_location, target_sat, rhel8_contenthost, @@ -2416,7 +2391,7 @@ def test_positive_host_registration_with_non_admin_user( :CaseLevel: Component """ user_password = gen_string('alpha') - org = module_entitlement_manifest_org + org = module_sca_manifest_org role = target_sat.api.Role(organization=[org]).create() user_permissions = { From b150b2ffcf68810901d08c9016d7e9c359d193e8 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 12 Dec 2023 06:44:36 -0500 Subject: [PATCH 379/586] [6.14.z] add automation coverage for BZ 2161993 (#13397) --- robottelo/constants/__init__.py | 1 + tests/foreman/cli/test_repository.py | 87 ++++++++++++++++++++++ tests/foreman/data/ant-7.7.7-1.noarch.rpm | Bin 0 -> 7340 bytes 3 files changed, 88 insertions(+) create mode 100644 tests/foreman/data/ant-7.7.7-1.noarch.rpm diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index e1aebb9a66e..e789e5e6bdf 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -1994,3 +1994,4 @@ class DataFile(Box): SNIPPET_DATA_FILE = DATA_DIR.joinpath(SNIPPET_DATA_FILE) PARTITION_SCRIPT_DATA_FILE = DATA_DIR.joinpath(PARTITION_SCRIPT_DATA_FILE) OS_TEMPLATE_DATA_FILE = DATA_DIR.joinpath(OS_TEMPLATE_DATA_FILE) + FAKE_3_YUM_REPO_RPMS_ANT = DATA_DIR.joinpath(FAKE_3_YUM_REPO_RPMS[0]) diff --git a/tests/foreman/cli/test_repository.py b/tests/foreman/cli/test_repository.py index 440dd4f128b..d1f434ce010 100644 --- a/tests/foreman/cli/test_repository.py +++ b/tests/foreman/cli/test_repository.py @@ -32,6 +32,7 @@ CUSTOM_FILE_REPO_FILES_COUNT, CUSTOM_LOCAL_FOLDER, DOWNLOAD_POLICIES, + FAKE_3_YUM_REPO_RPMS, MIRRORING_POLICIES, OS_TEMPLATE_DATA_FILE, REPO_TYPE, @@ -3390,3 +3391,89 @@ def test_positive_syncable_yum_format_repo_import(target_sat, module_org): ) assert repodata['content-counts']['packages'] != 0 assert repodata['sync']['status'] == 'Success' + + +@pytest.mark.rhel_ver_list([9]) +def test_positive_install_uploaded_rpm_on_host( + target_sat, rhel_contenthost, function_org, function_lce +): + """Verify that uploaded rpm successfully install on content host + + :id: 8701782e-2d1e-41b7-a9dc-07325bfeaf1c + + :steps: + 1. Create product, custom repo and upload rpm into repo + 2. Create CV, add repo into it & publish CV + 3. Upload package on to the satellite, rename it then upload it into repo + 4. Register host with satellite and install rpm on it + + :expectedresults: rpm should install successfully + + :customerscenario: true + + :BZ: 2161993 + """ + product = target_sat.cli_factory.make_product({'organization-id': function_org.id}) + repo = target_sat.cli_factory.make_repository( + { + 'content-type': 'yum', + 'name': gen_string('alpha', 5), + 'product-id': product['id'], + } + ) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + sync_status = target_sat.cli.Repository.info({'id': repo['id']})['sync']['status'] + assert sync_status == 'Success', f'Failed to sync {repo["name"]} repo' + + # Upload package + target_sat.put( + local_path=DataFile.FAKE_3_YUM_REPO_RPMS_ANT, + remote_path=f"/tmp/{FAKE_3_YUM_REPO_RPMS[0]}", + ) + # Rename uploaded package name + rpm_new_name = f'{gen_string(str_type="alpha", length=5)}.rpm' + target_sat.execute(f"mv /tmp/{FAKE_3_YUM_REPO_RPMS[0]} /tmp/{rpm_new_name}") + + result = target_sat.cli.Repository.upload_content( + { + 'name': repo['name'], + 'organization': repo['organization'], + 'path': f"/tmp/{rpm_new_name}", + 'product-id': repo['product']['id'], + } + ) + assert f"Successfully uploaded file {rpm_new_name}" == result[0]['message'] + queryinfo = target_sat.execute(f"rpm -q /tmp/{rpm_new_name}") + + content_view = target_sat.cli_factory.make_content_view( + {'organization-id': function_org.id, 'name': gen_string('alpha', 5)} + ) + target_sat.cli.ContentView.add_repository( + {'id': content_view['id'], 'repository-id': repo['id']} + ) + target_sat.cli.ContentView.publish({'id': content_view['id']}) + content_view = target_sat.cli.ContentView.info({'id': content_view['id']}) + target_sat.cli.ContentView.version_promote( + {'id': content_view['versions'][0]['id'], 'to-lifecycle-environment-id': function_lce.id} + ) + activation_key = target_sat.cli_factory.make_activation_key( + { + 'organization-id': function_org.id, + 'lifecycle-environment-id': function_lce.id, + 'content-view-id': content_view['id'], + } + ) + target_sat.cli.ActivationKey.content_override( + {'id': activation_key.id, 'content-label': repo.content_label, 'value': 'true'} + ) + assert ( + rhel_contenthost.register(function_org, None, activation_key.name, target_sat).status == 0 + ) + assert ( + rhel_contenthost.execute(f'yum install -y {FAKE_3_YUM_REPO_RPMS[0].split("-")[0]}').status + == 0 + ) + installed_package_name = rhel_contenthost.execute( + f'rpm -q {FAKE_3_YUM_REPO_RPMS[0].split("-")[0]}' + ) + assert installed_package_name.stdout == queryinfo.stdout diff --git a/tests/foreman/data/ant-7.7.7-1.noarch.rpm b/tests/foreman/data/ant-7.7.7-1.noarch.rpm new file mode 100644 index 0000000000000000000000000000000000000000..70d588254b9e91c173ac906b4f32224b1f4217b7 GIT binary patch literal 7340 zcmeHMe{38_6`u2*6DM)NQA-*(iE=GLNl^Fhc5nA~H?1L#-8ils+r%!h(bj|Sw%CZPT`xt?5}I&2qs4Gxcpsqk&fw}^91?mda6{ssvSD>yyU4j311*$obnVFdf zK?ulc5Hcrkl7ndi|Q-a?p_(i}3)D41To(1RX6#OrMXve+b{L_GF?-%?kAkIH1_&b1T|D53O3VyHP z{}lXSjVEw!tf&dZB`_9KRgVddb0f!`651C6qW?33F9Jk;&I*p_1|IvC;Fxy;|Ja1! z9~Jf!f}_Uh|FYoM*D!|Xg7YRm1_<#DI4`b$10eV}tOkTc%3jpF@CaKTpzyCe9`f)@o}EqDY7>o?;2i|bbe9~68P5YE$x zap0HOE9_4T`P&OFp7)W9LR3>$sjhL^GPAOyF-z7>rZUT7j;d15O#@P(OxLsAR<)dx z$xusG98KmLckG;I5oU9nWteS3E>z}<0~5)LZBSEH6iPFy3fa&c18^v3imF*Aw`}Tg zP0uhntJ{R|td#)=#WWlyGg&pMV>v1_Of%$|s*I|pW}1+wH8jOC zG;QudYkhcr*Gk~~5&4kKoUC^$3|J~Cmk8X$?_2OGG2e6C{95R)gAHuJi?|6kvv6)r zSHd7|xnA0$Vd38*VD_SGt?lqj<$#AFw>P;(-b+h7Tt|crM>^d+52I^i{dBzO)2eg0 znU3=QfIIHkdm+Ny9S-e-P0i-;v#Cr>i~=7n2Pq$L<_ETvXG{wBx)CdIa1YCzg>BU%;J)F3 zrvq*mXe2-?(7@&%w<867EBXAk)kA~wOE5M%oN+?yl zy{>0V5w(iA8?kv-gqL*o2yiSgFlP=YtnB(^Y#-6Z-6YlaELHC-DevJm{y)p?`+$yd z+XZ<*u5r>n(9zwUj5xdo8~sY)aj88|T`w6HI6StH1#UUIC3(#&$;aT?RZl>8sT|=~ zzy~V0ezk0^K~p7iB!gru%g8#mW9aa^jt)N&@r>q}@XL>(D;eFOSzXs`Mb%|aRb5kT zOEy*8p*fy4G`Ozt8?gGFQv2U*X6BtEz+2wc_YG}_JK>FgG=Hao+!VtLylZHE$Loo0 zF|v~^Tl`vreKS}dt+#r5R-OCfqWkXu`QoK7`>%X;&ytmsPwl#Q{DI!)|iIB?rl4GYRT~3 z+g^M7yOpH}{=WIqE%#47=QY2e$QR;YzIf%wM^Bu4F!?zx8%|CGKs}H$1 z?v$?_JAU{~-;T2jR`2@R6JLtGA$@$!sC;ZK+q3_+KehC(oZk89M0VXB2Ml-c_1IrK zhpwFbLg5Fw19#19{>t_fcb|DN*fI3kSMNO8c>3{) Date: Wed, 13 Dec 2023 01:54:02 -0500 Subject: [PATCH 380/586] [Cherrypick]Test for disabling rh repository with basearch 6.14.z (#13409) * updating adding test for rh repos with basearch * removed extra console command * swapped out hammer execute for reposets method * addressing comments --- tests/foreman/cli/test_repositories.py | 44 ++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/foreman/cli/test_repositories.py b/tests/foreman/cli/test_repositories.py index 2a98e9dbd58..222f6938860 100644 --- a/tests/foreman/cli/test_repositories.py +++ b/tests/foreman/cli/test_repositories.py @@ -19,6 +19,8 @@ import pytest from requests.exceptions import HTTPError +from robottelo.constants import DEFAULT_ARCHITECTURE, REPOS, REPOSET + @pytest.mark.rhel_ver_match('[^6]') def test_positive_custom_products_disabled_by_default( @@ -85,3 +87,45 @@ def test_negative_invalid_repo_fails_publish( with pytest.raises(HTTPError) as context: cv.publish() assert 'Remove the invalid repository before publishing again' in context.value.response.text + + +def test_positive_disable_rh_repo_with_basearch(module_target_sat, module_entitlement_manifest_org): + """Verify that users can disable Red Hat Repositories with basearch + + :id: dd3b63b7-1dbf-4d8a-ab66-348de0ad7cf3 + + :steps: + 1. You have the Appstream Kicstart repositories release version + "8" synced in from the release of RHEL 8 + 2. hammer repository-set disable --basearch --name --product-id + --organization --releasever + + + :expectedresults: Users can now disable Red Hat repositories with + basearch + + :customerscenario: true + + :BZ: 1932486 + """ + rh_repo_id = module_target_sat.api_factory.enable_rhrepo_and_fetchid( + basearch=DEFAULT_ARCHITECTURE, + org_id=module_entitlement_manifest_org.id, + product=REPOS['kickstart']['rhel8_aps']['product'], + repo=REPOS['kickstart']['rhel8_aps']['name'], + reposet=REPOS['kickstart']['rhel8_aps']['reposet'], + releasever=REPOS['kickstart']['rhel8_aps']['version'], + ) + repo = module_target_sat.api.Repository(id=rh_repo_id).read() + repo.sync(timeout=600) + disabled_repo = module_target_sat.cli.RepositorySet.disable( + { + 'basearch': DEFAULT_ARCHITECTURE, + 'name': REPOSET['kickstart']['rhel8'], + 'product-id': repo.product.id, + 'organization-id': module_entitlement_manifest_org.id, + 'releasever': REPOS['kickstart']['rhel8_aps']['version'], + 'repository-id': rh_repo_id, + } + ) + assert 'Repository disabled' in disabled_repo[0]['message'] From 121973a67bd2abce099b654cecb3b7ed09cc283d Mon Sep 17 00:00:00 2001 From: Cole Higgins Date: Wed, 13 Dec 2023 02:33:39 -0500 Subject: [PATCH 381/586] [Cherrypick][Repository Rewrite] Adding test for available_repositories endpoint (#13412) * adding test for available repositories endpoint * updated repo to rhel7 extras * replaced PRDS with REPOS * updated product call --- tests/foreman/api/test_repositories.py | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/foreman/api/test_repositories.py b/tests/foreman/api/test_repositories.py index f8639a13367..a8728ab46e1 100644 --- a/tests/foreman/api/test_repositories.py +++ b/tests/foreman/api/test_repositories.py @@ -290,3 +290,44 @@ def test_positive_sync_mulitple_large_repos(module_target_sat, module_entitlemen data={'ids': [rh_product.id]}, timeout=2000 ) assert res['result'] == 'success' + + +def test_positive_available_repositories_endpoint(module_sca_manifest_org, target_sat): + """Attempt to hit the /available_repositories endpoint with no failures + + :id: f4c9d4a0-9a82-4f06-b772-b1f7e3f45e7d + + :Steps: + 1. Enable a Red Hat Repository + 2. Attempt to hit the enpoint: + GET /katello/api/repository_sets/:id/available_repositories + 3. Verify Actions::Katello::RepositorySet::ScanCdn task is run + 4. Verify there are no failures when scanning for repository + + + :expectedresults: Actions::Katello::RepositorySet::ScanCdn task should succeed and + not fail when scanning for repositories + + :customerscenario: true + + :BZ: 2030445 + """ + rh_repo_id = target_sat.api_factory.enable_rhrepo_and_fetchid( + basearch=constants.DEFAULT_ARCHITECTURE, + org_id=module_sca_manifest_org.id, + product=constants.REPOS['rhel7_extra']['product'], + repo=constants.REPOS['rhel7_extra']['name'], + reposet=constants.REPOS['rhel7_extra']['reposet'], + releasever=None, + ) + rh_repo = target_sat.api.Repository(id=rh_repo_id).read() + product = target_sat.api.Product(id=rh_repo.product.id).read() + reposet = target_sat.api.RepositorySet( + name=constants.REPOSET['rhel7_extra'], product=product + ).search()[0] + touch_endpoint = target_sat.api.RepositorySet.available_repositories(reposet) + assert touch_endpoint['total'] != 0 + results = target_sat.execute('tail -15 /var/log/foreman/production.log').stdout + assert 'Actions::Katello::RepositorySet::ScanCdn' in results + assert 'result: success' in results + assert 'Failed at scanning for repository' not in results From 7de9314affb02371256c78ba6b6a67b670fb5df7 Mon Sep 17 00:00:00 2001 From: Lukas Hellebrandt Date: Tue, 13 Jun 2023 12:46:42 +0200 Subject: [PATCH 382/586] bz1699188 --- tests/foreman/api/test_ansible.py | 90 ++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/tests/foreman/api/test_ansible.py b/tests/foreman/api/test_ansible.py index b6435d77f8a..a842ec5c3ca 100644 --- a/tests/foreman/api/test_ansible.py +++ b/tests/foreman/api/test_ansible.py @@ -18,8 +18,10 @@ """ from fauxfactory import gen_string import pytest +from wait_for import wait_for -from robottelo.config import settings +from robottelo.config import settings, user_nailgun_config +from robottelo.utils.issue_handlers import is_open @pytest.mark.e2e @@ -253,3 +255,89 @@ def test_add_and_remove_ansible_role_hostgroup(target_sat): target_sat.api.HostGroup(id=hg.id).remove_ansible_role(data={'ansible_role_id': role}) host_roles = target_sat.api.HostGroup(id=hg.id).list_ansible_roles() assert len(host_roles) == 0 + + +@pytest.fixture +def filtered_user(target_sat, module_org, module_location): + """ + :Steps: + 1. Create a role with a host view filtered + 2. Create a user with that role + 3. Setup a host + """ + api = target_sat.api + role = api.Role( + name=gen_string('alpha'), location=[module_location], organization=[module_org] + ).create() + # assign view_hosts (with a filter, to test BZ 1699188), + # view_hostgroups, view_facts permissions to the role + permission_hosts = api.Permission().search(query={'search': 'name="view_hosts"'}) + permission_hostgroups = api.Permission().search(query={'search': 'name="view_hostgroups"'}) + permission_facts = api.Permission().search(query={'search': 'name="view_facts"'}) + api.Filter(permission=permission_hosts, search='name != nonexistent', role=role).create() + api.Filter(permission=permission_hostgroups, role=role).create() + api.Filter(permission=permission_facts, role=role).create() + + password = gen_string('alpha') + user = api.User( + role=[role], password=password, location=[module_location], organization=[module_org] + ).create() + + return user, password + + +@pytest.fixture +def rex_host_in_org_and_loc(target_sat, module_org, module_location, rex_contenthost): + api = target_sat.api + host = api.Host().search(query={'search': f'name={rex_contenthost.hostname}'})[0] + host_id = host.id + api.Host(id=host_id, organization=[module_org.id]).update(['organization']) + api.Host(id=host_id, location=module_location.id).update(['location']) + return host + + +@pytest.mark.rhel_ver_match('[78]') +@pytest.mark.tier2 +def test_positive_read_facts_with_filter( + target_sat, rex_contenthost, filtered_user, rex_host_in_org_and_loc +): + """ + Read host's Ansible facts as a user with a role that has host filter + + :id: 483d5faf-7a4c-4cb7-b14f-369768ad99b0 + + 1. Run Ansible roles on a host + 2. Using API, read Ansible facts of that host + + :expectedresults: Ansible facts returned + + :BZ: 1699188 + + :customerscenario: true + """ + user, password = filtered_user + host = rex_host_in_org_and_loc + + # gather ansible facts by running ansible roles on the host + host.play_ansible_roles() + if is_open('BZ:2216471'): + host_wait = target_sat.api.Host().search( + query={'search': f'name={rex_contenthost.hostname}'} + )[0] + wait_for( + lambda: len(host_wait.get_facts()) > 0, + timeout=30, + delay=2, + ) + + user_cfg = user_nailgun_config(user.login, password) + host = target_sat.api.Host(server_config=user_cfg).search( + query={'search': f'name={rex_contenthost.hostname}'} + )[0] + # get facts through API + facts = host.get_facts() + assert 'subtotal' in facts + assert facts['subtotal'] == 1 + assert 'results' in facts + assert rex_contenthost.hostname in facts['results'] + assert len(facts['results'][rex_contenthost.hostname]) > 0 From 1c1747463ac26ec6fd4fcec6a2b051242062d9e0 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 13 Dec 2023 04:59:29 -0500 Subject: [PATCH 383/586] [6.14.z] cli location test fixes (#13423) --- tests/foreman/cli/test_location.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/foreman/cli/test_location.py b/tests/foreman/cli/test_location.py index 98a38c6a324..021c3e09714 100644 --- a/tests/foreman/cli/test_location.py +++ b/tests/foreman/cli/test_location.py @@ -35,7 +35,7 @@ def _cleanup(): def _location(request, target_sat, options=None): - location = target_sat.cli_factory.make_location(options=options) + location = target_sat.cli_factory.make_location(options) @request.addfinalizer def _cleanup(): @@ -156,14 +156,15 @@ def test_positive_create_update_delete(self, request, target_sat): description = gen_string('utf8') subnet = _subnet(request, target_sat) domains = [_domain(request, target_sat) for _ in range(0, 2)] - host_groups = [_host_group(request) for _ in range(0, 3)] - medium = _medium(request) - compute_resource = _compute_resource(request) - template = _template(request) - user = _user(request) + host_groups = [_host_group(request, target_sat) for _ in range(0, 3)] + medium = _medium(request, target_sat) + compute_resource = _compute_resource(request, target_sat) + template = _template(request, target_sat) + user = _user(request, target_sat) location = _location( request, + target_sat, { 'description': description, 'subnet-ids': subnet['id'], @@ -231,7 +232,7 @@ def test_positive_create_with_parent(self, request, target_sat): """ parent_location = _location(request, target_sat) - location = _location(request, {'parent-id': parent_location['id']}) + location = _location(request, target_sat, {'parent-id': parent_location['id']}) assert location['parent'] == parent_location['name'] @pytest.mark.tier1 @@ -344,7 +345,7 @@ def test_positive_update_parent(self, request, target_sat): :CaseImportance: High """ parent_location = _location(request, target_sat) - location = _location(request, {'parent-id': parent_location['id']}) + location = _location(request, target_sat, {'parent-id': parent_location['id']}) parent_location_2 = _location(request, target_sat) target_sat.cli.Location.update({'id': location['id'], 'parent-id': parent_location_2['id']}) @@ -366,7 +367,7 @@ def test_negative_update_parent_with_child(self, request, target_sat): :CaseImportance: High """ parent_location = _location(request, target_sat) - location = _location(request, {'parent-id': parent_location['id']}) + location = _location(request, target_sat, {'parent-id': parent_location['id']}) # set parent as child with pytest.raises(CLIReturnCodeError): From bacd62ceee3c7c42c1079b3a4d031104b87382f2 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 13 Dec 2023 06:14:01 -0500 Subject: [PATCH 384/586] [6.14.z] Fix libvirt/ec2 compute resource tests (#13431) --- robottelo/host_helpers/cli_factory.py | 2 -- .../cli/test_computeresource_libvirt.py | 26 +++++++++++++++---- tests/foreman/cli/test_location.py | 7 ++++- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/robottelo/host_helpers/cli_factory.py b/robottelo/host_helpers/cli_factory.py index 9af5ee4cce4..bcf7120eedf 100644 --- a/robottelo/host_helpers/cli_factory.py +++ b/robottelo/host_helpers/cli_factory.py @@ -179,8 +179,6 @@ def create_object(cli_object, options, values=None, credentials=None): }, 'compute_resource': { 'name': gen_alphanumeric, - 'provider': 'Libvirt', - 'url': 'qemu+tcp://localhost:16509/system', }, 'org': {'_redirect': 'org_with_credentials'}, 'org_with_credentials': { diff --git a/tests/foreman/cli/test_computeresource_libvirt.py b/tests/foreman/cli/test_computeresource_libvirt.py index 470398747e7..91baee553d9 100644 --- a/tests/foreman/cli/test_computeresource_libvirt.py +++ b/tests/foreman/cli/test_computeresource_libvirt.py @@ -242,7 +242,13 @@ def test_positive_create_with_loc(libvirt_url, module_target_sat): :CaseLevel: Integration """ location = module_target_sat.cli_factory.make_location() - comp_resource = module_target_sat.cli_factory.compute_resource({'location-ids': location['id']}) + comp_resource = module_target_sat.cli_factory.compute_resource( + { + 'location-ids': location['id'], + 'provider': FOREMAN_PROVIDERS['libvirt'], + 'url': libvirt_url, + } + ) assert len(comp_resource['locations']) == 1 assert comp_resource['locations'][0] == location['name'] @@ -263,7 +269,11 @@ def test_positive_create_with_locs(libvirt_url, module_target_sat): locations_amount = random.randint(3, 5) locations = [module_target_sat.cli_factory.make_location() for _ in range(locations_amount)] comp_resource = module_target_sat.cli_factory.compute_resource( - {'location-ids': [location['id'] for location in locations]} + { + 'location-ids': [location['id'] for location in locations], + 'provider': FOREMAN_PROVIDERS['libvirt'], + 'url': libvirt_url, + } ) assert len(comp_resource['locations']) == locations_amount for location in locations: @@ -310,7 +320,9 @@ def test_negative_create_with_same_name(libvirt_url, module_target_sat): :CaseLevel: Component """ - comp_res = module_target_sat.cli_factory.compute_resource() + comp_res = module_target_sat.cli_factory.compute_resource( + {'provider': FOREMAN_PROVIDERS['libvirt'], 'url': libvirt_url} + ) with pytest.raises(CLIReturnCodeError): module_target_sat.cli.ComputeResource.create( { @@ -339,7 +351,9 @@ def test_positive_update_name(libvirt_url, options, module_target_sat): :parametrized: yes """ - comp_res = module_target_sat.cli_factory.compute_resource() + comp_res = module_target_sat.cli_factory.compute_resource( + {'provider': FOREMAN_PROVIDERS['libvirt'], 'url': libvirt_url} + ) options.update({'name': comp_res['name']}) # update Compute Resource module_target_sat.cli.ComputeResource.update(options) @@ -369,7 +383,9 @@ def test_negative_update(libvirt_url, options, module_target_sat): :parametrized: yes """ - comp_res = module_target_sat.cli_factory.compute_resource() + comp_res = module_target_sat.cli_factory.compute_resource( + {'provider': FOREMAN_PROVIDERS['libvirt'], 'url': libvirt_url} + ) with pytest.raises(CLIReturnCodeError): module_target_sat.cli.ComputeResource.update(dict({'name': comp_res['name']}, **options)) result = module_target_sat.cli.ComputeResource.info({'id': comp_res['id']}) diff --git a/tests/foreman/cli/test_location.py b/tests/foreman/cli/test_location.py index 021c3e09714..4779bf9373d 100644 --- a/tests/foreman/cli/test_location.py +++ b/tests/foreman/cli/test_location.py @@ -101,7 +101,12 @@ def _cleanup(): def _compute_resource(request, target_sat): - compute_resource = target_sat.cli_factory.compute_resource() + compute_resource = target_sat.cli_factory.compute_resource( + { + 'provider': 'Libvirt', + 'url': 'qemu+tcp://localhost:16509/system', + } + ) @request.addfinalizer def _cleanup(): From 677b81c0974ea1a9afbc25287730b08f21b9ced8 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 13 Dec 2023 07:19:36 -0500 Subject: [PATCH 385/586] [6.14.z] Bump EL8, EL9 versions in leapp upgrade test (#13416) Bump EL8, EL9 versions in leapp upgrade test (#13411) Signed-off-by: Gaurav Talreja (cherry picked from commit 55fc8731de8c73929f5eb3befe484ab00c73420f) Co-authored-by: Gaurav Talreja --- tests/foreman/cli/test_leapp_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/foreman/cli/test_leapp_client.py b/tests/foreman/cli/test_leapp_client.py index a6d547f5314..550a1b662a4 100644 --- a/tests/foreman/cli/test_leapp_client.py +++ b/tests/foreman/cli/test_leapp_client.py @@ -27,8 +27,8 @@ synced_repos = pytest.StashKey[dict] RHEL7_VER = '7.9' -RHEL8_VER = '8.8' -RHEL9_VER = '9.2' +RHEL8_VER = '8.9' +RHEL9_VER = '9.3' RHEL_REPOS = { 'rhel7_server': { From e5ee959fb05c00786d079a2da1b1201a0b736c5d Mon Sep 17 00:00:00 2001 From: vijay sawant Date: Wed, 13 Dec 2023 17:50:58 +0530 Subject: [PATCH 386/586] keyword argument server config required (#13427) --- tests/foreman/api/test_subscription.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/foreman/api/test_subscription.py b/tests/foreman/api/test_subscription.py index 5d5a2aedd38..bccb968f479 100644 --- a/tests/foreman/api/test_subscription.py +++ b/tests/foreman/api/test_subscription.py @@ -208,11 +208,11 @@ def test_positive_delete_manifest_as_another_user( ) # use the first admin to upload a manifest with function_entitlement_manifest as manifest: - target_sat.api.Subscription(sc1, organization=function_org).upload( + target_sat.api.Subscription(server_config=sc1, organization=function_org).upload( data={'organization_id': function_org.id}, files={'content': manifest.content} ) # try to search and delete the manifest with another admin - target_sat.api.Subscription(sc2, organization=function_org).delete_manifest( + target_sat.api.Subscription(server_config=sc2, organization=function_org).delete_manifest( data={'organization_id': function_org.id} ) assert len(target_sat.cli.Subscription.list({'organization-id': function_org.id})) == 0 From acaebf34e2719451f5e8a591c54b9f873074a4e9 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 13 Dec 2023 10:39:15 -0500 Subject: [PATCH 387/586] [6.14.z] users and roles test fixes (#13437) --- tests/foreman/api/test_permission.py | 18 ++++---- tests/foreman/api/test_role.py | 61 +++++++++++++++++----------- tests/foreman/cli/test_role.py | 2 +- 3 files changed, 48 insertions(+), 33 deletions(-) diff --git a/tests/foreman/api/test_permission.py b/tests/foreman/api/test_permission.py index 9ea92305b39..30e065d77c3 100644 --- a/tests/foreman/api/test_permission.py +++ b/tests/foreman/api/test_permission.py @@ -261,7 +261,7 @@ def set_taxonomies(self, entity, organization=None, location=None): 'entity_cls', **parametrized([entities.Architecture, entities.Domain, entities.ActivationKey]), ) - def test_positive_check_create(self, entity_cls, class_org, class_location): + def test_positive_check_create(self, entity_cls, class_org, class_location, target_sat): """Check whether the "create_*" role has an effect. :id: e4c92365-58b7-4538-9d1b-93f3cf51fbef @@ -278,14 +278,14 @@ def test_positive_check_create(self, entity_cls, class_org, class_location): """ with pytest.raises(HTTPError): entity_cls(self.cfg).create() - self.give_user_permission(_permission_name(entity_cls, 'create')) + self.give_user_permission(_permission_name(entity_cls, 'create'), target_sat) new_entity = self.set_taxonomies(entity_cls(self.cfg), class_org, class_location) # Entities with both org and loc require # additional permissions to set them. fields = {'organization', 'location'} if fields.issubset(set(new_entity.get_fields())): - self.give_user_permission('assign_organizations') - self.give_user_permission('assign_locations') + self.give_user_permission('assign_organizations', target_sat) + self.give_user_permission('assign_locations', target_sat) new_entity = new_entity.create_json() entity_cls(id=new_entity['id']).read() # As admin user. @@ -294,7 +294,7 @@ def test_positive_check_create(self, entity_cls, class_org, class_location): 'entity_cls', **parametrized([entities.Architecture, entities.Domain, entities.ActivationKey]), ) - def test_positive_check_read(self, entity_cls, class_org, class_location): + def test_positive_check_read(self, entity_cls, class_org, class_location, target_sat): """Check whether the "view_*" role has an effect. :id: 55689121-2646-414f-beb1-dbba5973c523 @@ -312,7 +312,7 @@ def test_positive_check_read(self, entity_cls, class_org, class_location): new_entity = new_entity.create() with pytest.raises(HTTPError): entity_cls(self.cfg, id=new_entity.id).read() - self.give_user_permission(_permission_name(entity_cls, 'read')) + self.give_user_permission(_permission_name(entity_cls, 'read'), target_sat) entity_cls(self.cfg, id=new_entity.id).read() @pytest.mark.upgrade @@ -321,7 +321,7 @@ def test_positive_check_read(self, entity_cls, class_org, class_location): 'entity_cls', **parametrized([entities.Architecture, entities.Domain, entities.ActivationKey]), ) - def test_positive_check_delete(self, entity_cls, class_org, class_location): + def test_positive_check_delete(self, entity_cls, class_org, class_location, target_sat): """Check whether the "destroy_*" role has an effect. :id: 71365147-51ef-4602-948f-78a5e78e32b4 @@ -339,7 +339,7 @@ def test_positive_check_delete(self, entity_cls, class_org, class_location): new_entity = new_entity.create() with pytest.raises(HTTPError): entity_cls(self.cfg, id=new_entity.id).delete() - self.give_user_permission(_permission_name(entity_cls, 'delete')) + self.give_user_permission(_permission_name(entity_cls, 'delete'), target_sat) entity_cls(self.cfg, id=new_entity.id).delete() with pytest.raises(HTTPError): new_entity.read() # As admin user @@ -376,7 +376,7 @@ def test_positive_check_update(self, entity_cls, class_org, class_location, targ update_entity = entity_cls(self.cfg, id=new_entity.id, name=name) with pytest.raises(HTTPError): update_entity.update(['name']) - self.give_user_permission(_permission_name(entity_cls, 'update')) + self.give_user_permission(_permission_name(entity_cls, 'update'), target_sat) # update() calls read() under the hood, which triggers # permission error if entity_cls is entities.ActivationKey: diff --git a/tests/foreman/api/test_role.py b/tests/foreman/api/test_role.py index 1fc5ae89067..df0bc677718 100644 --- a/tests/foreman/api/test_role.py +++ b/tests/foreman/api/test_role.py @@ -90,7 +90,7 @@ def create_org_admin_role(self, target_sat, name=None, orgs=None, locs=None): return target_sat.api.Role(id=org_admin['role']['id']).read() return target_sat.api.Role(id=org_admin['id']).read() - def create_org_admin_user(self, role_taxos, user_taxos, target_sat): + def create_org_admin_user(self, target_sat, role_taxos, user_taxos): """Helper function to create an Org Admin user by assigning org admin role and assign taxonomies to Role and User @@ -526,7 +526,7 @@ def test_positive_create_org_admin_from_clone(self, target_sat): default_org_admin = target_sat.api.Role().search( query={'search': 'name="Organization admin"'} ) - org_admin = self.create_org_admin_role() + org_admin = self.create_org_admin_role(target_sat) default_filters = target_sat.api.Role(id=default_org_admin[0].id).read().filters orgadmin_filters = target_sat.api.Role(id=org_admin.id).read().filters assert len(default_filters) == len(orgadmin_filters) @@ -550,7 +550,7 @@ def test_positive_create_cloned_role_with_taxonomies(self, role_taxonomies, targ :CaseImportance: Critical """ org_admin = self.create_org_admin_role( - orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] + target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) org_admin = target_sat.api.Role(id=org_admin.id).read() assert role_taxonomies['org'].id == org_admin.organization[0].id @@ -578,7 +578,9 @@ def test_negative_access_entities_from_org_admin( :CaseLevel: System """ - user = self.create_org_admin_user(role_taxos=role_taxonomies, user_taxos=filter_taxonomies) + user = self.create_org_admin_user( + target_sat, role_taxos=role_taxonomies, user_taxos=filter_taxonomies + ) domain = self.create_domain( orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) @@ -609,7 +611,9 @@ def test_negative_access_entities_from_user( :CaseLevel: System """ - user = self.create_org_admin_user(role_taxos=role_taxonomies, user_taxos=filter_taxonomies) + user = self.create_org_admin_user( + target_sat, role_taxos=role_taxonomies, user_taxos=filter_taxonomies + ) domain = self.create_domain( orgs=[filter_taxonomies['org'].id], locs=[filter_taxonomies['loc'].id] ) @@ -973,7 +977,7 @@ def test_positive_user_group_users_access_as_org_admin(self, role_taxonomies, ta :CaseLevel: System """ org_admin = self.create_org_admin_role( - orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] + target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) userone_login = gen_string('alpha') userone_pass = gen_string('alphanumeric') @@ -1081,7 +1085,7 @@ def test_negative_assign_org_admin_to_user_group( :CaseLevel: System """ org_admin = self.create_org_admin_role( - orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] + target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) user_one = self.create_simple_user(target_sat, filter_taxos=filter_taxonomies) user_two = self.create_simple_user(target_sat, filter_taxos=filter_taxonomies) @@ -1123,7 +1127,7 @@ def test_negative_assign_taxonomies_by_org_admin( :CaseLevel: Integration """ org_admin = self.create_org_admin_role( - orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] + target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) # Creating resource dom_name = gen_string('alpha') @@ -1168,7 +1172,7 @@ def test_positive_remove_org_admin_role(self, role_taxonomies, target_sat): :CaseImportance: Critical """ org_admin = self.create_org_admin_role( - orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] + target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') @@ -1204,7 +1208,9 @@ def test_positive_taxonomies_control_to_superadmin_with_org_admin( :CaseLevel: Integration """ - user = self.create_org_admin_user(role_taxos=role_taxonomies, user_taxos=role_taxonomies) + user = self.create_org_admin_user( + target_sat, role_taxos=role_taxonomies, user_taxos=role_taxonomies + ) sc = self.user_config(user, target_sat) # Creating resource dom_name = gen_string('alpha') @@ -1247,7 +1253,9 @@ def test_positive_taxonomies_control_to_superadmin_without_org_admin( :CaseLevel: Integration """ - user = self.create_org_admin_user(role_taxos=role_taxonomies, user_taxos=role_taxonomies) + user = self.create_org_admin_user( + target_sat, role_taxos=role_taxonomies, user_taxos=role_taxonomies + ) sc = self.user_config(user, target_sat) # Creating resource dom_name = gen_string('alpha') @@ -1293,7 +1301,7 @@ def test_negative_create_roles_by_org_admin(self, role_taxonomies, target_sat): create new role """ org_admin = self.create_org_admin_role( - orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] + target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') @@ -1333,7 +1341,9 @@ def test_negative_modify_roles_by_org_admin(self, role_taxonomies, target_sat): :expectedresults: Org Admin should not have permissions to update existing roles """ - user = self.create_org_admin_user(role_taxos=role_taxonomies, user_taxos=role_taxonomies) + user = self.create_org_admin_user( + target_sat, role_taxos=role_taxonomies, user_taxos=role_taxonomies + ) test_role = target_sat.api.Role().create() sc = self.user_config(user, target_sat) test_role = target_sat.api.Role(sc, id=test_role.id).read() @@ -1360,7 +1370,7 @@ def test_negative_admin_permissions_to_org_admin(self, role_taxonomies, target_s :CaseLevel: Integration """ org_admin = self.create_org_admin_role( - orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] + target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') @@ -1407,7 +1417,7 @@ def test_positive_create_user_by_org_admin(self, role_taxonomies, target_sat): :CaseLevel: Integration """ org_admin = self.create_org_admin_role( - orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] + target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') @@ -1460,7 +1470,9 @@ def test_positive_access_users_inside_org_admin_taxonomies(self, role_taxonomies :CaseLevel: Integration """ - user = self.create_org_admin_user(role_taxos=role_taxonomies, user_taxos=role_taxonomies) + user = self.create_org_admin_user( + target_sat, role_taxos=role_taxonomies, user_taxos=role_taxonomies + ) test_user = self.create_simple_user(filter_taxos=role_taxonomies) sc = self.user_config(user, target_sat) try: @@ -1498,7 +1510,7 @@ def test_positive_create_nested_location(self, role_taxonomies, target_sat): location=[role_taxonomies['loc']], ).create() org_admin = self.create_org_admin_role( - orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] + target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) user.role = [org_admin] user = user.update(['role']) @@ -1532,7 +1544,9 @@ def test_negative_access_users_outside_org_admin_taxonomies( :CaseLevel: Integration """ - user = self.create_org_admin_user(role_taxos=role_taxonomies, user_taxos=role_taxonomies) + user = self.create_org_admin_user( + target_sat, role_taxos=role_taxonomies, user_taxos=role_taxonomies + ) test_user = self.create_simple_user(filter_taxos=filter_taxonomies) sc = self.user_config(user, target_sat) with pytest.raises(HTTPError): @@ -1557,7 +1571,7 @@ def test_negative_create_taxonomies_by_org_admin(self, role_taxonomies, target_s 1. Org Admin should not have access to create organizations 2. Org Admin should have access to create locations """ - org_admin = self.create_org_admin_role(orgs=[role_taxonomies['org'].id]) + org_admin = self.create_org_admin_role(target_sat, orgs=[role_taxonomies['org'].id]) user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') user = target_sat.api.User( @@ -1603,7 +1617,7 @@ def test_positive_access_all_global_entities_by_org_admin( :expectedresults: Org Admin should have access to all the global target_sat.api in any taxonomies """ - org_admin = self.create_org_admin_role(orgs=[role_taxonomies['org'].id]) + org_admin = self.create_org_admin_role(target_sat, orgs=[role_taxonomies['org'].id]) user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') user = target_sat.api.User( @@ -1658,7 +1672,7 @@ def test_negative_access_entities_from_ldap_org_admin( :CaseAutomation: Automated """ org_admin = self.create_org_admin_role( - orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] + target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) # Creating Domain resource in same taxonomies as Org Admin role to access later domain = self.create_domain( @@ -1705,7 +1719,7 @@ def test_negative_access_entities_from_ldap_user( :CaseAutomation: Automated """ org_admin = self.create_org_admin_role( - orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] + target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) # Creating Domain resource in different taxonomies to access later domain = self.create_domain(orgs=[module_org.id], locs=[module_location.id]) @@ -1753,6 +1767,7 @@ def test_positive_assign_org_admin_to_ldap_user_group( group_name = gen_string("alpha") password = gen_string("alpha") org_admin = self.create_org_admin_role( + target_sat, orgs=[create_ldap['authsource'].organization[0].id], locs=[create_ldap['authsource'].location[0].id], ) @@ -1815,7 +1830,7 @@ def test_negative_assign_org_admin_to_ldap_user_group( group_name = gen_string("alpha") password = gen_string("alpha") org_admin = self.create_org_admin_role( - orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] + target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) # Creating Domain resource in same taxonomies as Org Admin role to access later domain = self.create_domain( diff --git a/tests/foreman/cli/test_role.py b/tests/foreman/cli/test_role.py index 034d395b609..11474b42f1e 100644 --- a/tests/foreman/cli/test_role.py +++ b/tests/foreman/cli/test_role.py @@ -150,7 +150,7 @@ def test_negative_list_filters_without_parameters(self, module_target_sat): :BZ: 1296782 """ - with pytest.raises(CLIReturnCodeError, CLIDataBaseError) as err: + with pytest.raises(CLIReturnCodeError) as err: module_target_sat.cli.Role.filters() if isinstance(err.type, CLIDataBaseError): pytest.fail(err) From 3ffcab8718f2cf423fafdc67f9ec06045d6c5ac0 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:05:58 -0500 Subject: [PATCH 388/586] [6.14.z] Fix host-collections tests (#13447) --- tests/foreman/cli/test_hostcollection.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/foreman/cli/test_hostcollection.py b/tests/foreman/cli/test_hostcollection.py index a7710f79fc6..91bf5c4f563 100644 --- a/tests/foreman/cli/test_hostcollection.py +++ b/tests/foreman/cli/test_hostcollection.py @@ -30,15 +30,13 @@ ) -def _make_fake_host_helper(module_org, module_target_sat): +def _make_fake_host_helper(module_org, sat): """Make a new fake host""" - library = module_target_sat.cli.LifecycleEnvironment.info( + library = sat.cli.LifecycleEnvironment.info( {'organization-id': module_org.id, 'name': ENVIRONMENT} ) - default_cv = module_target_sat.cli.ContentView.info( - {'organization-id': module_org.id, 'name': DEFAULT_CV} - ) - return module_target_sat.cli_factory.make_fake_host( + default_cv = sat.cli.ContentView.info({'organization-id': module_org.id, 'name': DEFAULT_CV}) + return sat.cli_factory.make_fake_host( { 'content-view-id': default_cv['id'], 'lifecycle-environment-id': library['id'], @@ -262,7 +260,7 @@ def test_positive_host_collection_host_pagination(module_org, module_target_sat) {'organization-id': module_org.id} ) host_ids = ','.join( - _make_fake_host_helper((module_org)['id'] for _ in range(2)), module_target_sat + _make_fake_host_helper(module_org, module_target_sat)['id'] for _ in range(2) ) module_target_sat.cli.HostCollection.add_host( {'host-ids': host_ids, 'id': host_collection['id']} @@ -316,7 +314,7 @@ def test_positive_register_host_ak_with_host_collection(module_org, module_ak_wi :CaseLevel: System """ - host_info = _make_fake_host_helper(module_org) + host_info = _make_fake_host_helper(module_org, target_sat) hc = target_sat.cli_factory.make_host_collection({'organization-id': module_org.id}) target_sat.cli.ActivationKey.add_host_collection( From 11f53dc4f257e38f41bcc7415acd8fc4148b9ef5 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:40:51 -0500 Subject: [PATCH 389/586] [6.14.z] SAT-21433: Enable and sync rhel8_bos repo for rhc tests (#13435) SAT-21433: Enable and sync rhel8_bos repo for rhc tests (#13400) Enable and sync rhel8_bos repo for rhc tests (cherry picked from commit eb4c4e77fe48da5bca037199570946020a5de1b6) Co-authored-by: Jameer Pathan <21165044+jameerpathan111@users.noreply.github.com> --- tests/foreman/ui/test_rhc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/foreman/ui/test_rhc.py b/tests/foreman/ui/test_rhc.py index 57095979bca..36d3d64875a 100644 --- a/tests/foreman/ui/test_rhc.py +++ b/tests/foreman/ui/test_rhc.py @@ -86,9 +86,12 @@ def fixture_setup_rhc_satellite( repo2_id = module_target_sat.api_factory.enable_sync_redhat_repo( constants.REPOS['rhel7'], module_rhc_org.id ) + repo3_id = module_target_sat.api_factory.enable_sync_redhat_repo( + constants.REPOS['rhel8_bos'], module_rhc_org.id + ) # Add repos to Content view content_view = module_target_sat.api.ContentView( - organization=module_rhc_org, repository=[repo1_id, repo2_id] + organization=module_rhc_org, repository=[repo1_id, repo2_id, repo3_id] ).create() content_view.publish() # Create Activation key From 1022563912a7aad6a16c191f3b18dd58f8b7d05c Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 14 Dec 2023 02:28:46 -0500 Subject: [PATCH 390/586] [6.14.z] cli org test fixes (#13452) --- tests/foreman/cli/test_organization.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/foreman/cli/test_organization.py b/tests/foreman/cli/test_organization.py index a34bea5f7fc..9b2b3893c8d 100644 --- a/tests/foreman/cli/test_organization.py +++ b/tests/foreman/cli/test_organization.py @@ -153,7 +153,7 @@ def test_positive_create_with_system_admin_user(module_target_sat): org_name = gen_string('alpha') module_target_sat.cli_factory.user({'login': login, 'password': password}) module_target_sat.cli.User.add_role({'login': login, 'role': 'System admin'}) - module_target_sat.cli_factory.make_org({'user': login, 'password': password, 'name': org_name}) + module_target_sat.cli_factory.make_org({'users': login, 'name': org_name}) result = module_target_sat.cli.Org.info({'name': org_name}) assert result['name'] == org_name @@ -536,7 +536,6 @@ def test_positive_add_and_remove_locations(module_org, module_target_sat): {'location': locations[1]['name'], 'name': module_org.name} ) org_info = module_target_sat.cli.Org.info({'id': module_org.id}) - assert len(org_info['locations']) == 2, "Failed to add locations" assert locations[0]['name'] in org_info['locations'] assert locations[1]['name'] in org_info['locations'] module_target_sat.cli.Org.remove_location( From d9c3d03ca1509e43761b06280a547c6c7c757e32 Mon Sep 17 00:00:00 2001 From: David Moore <109112035+damoore044@users.noreply.github.com> Date: Thu, 14 Dec 2023 11:02:00 -0500 Subject: [PATCH 391/586] [6.14.z] CLI::Errata Refactor (#13464) * Updating cli-errata standards * RHEL8 for errata_host fixture --- tests/foreman/cli/test_errata.py | 439 ++++++++++++++++--------------- 1 file changed, 234 insertions(+), 205 deletions(-) diff --git a/tests/foreman/cli/test_errata.py b/tests/foreman/cli/test_errata.py index d8e8bd93969..0b4b3a37f5c 100644 --- a/tests/foreman/cli/test_errata.py +++ b/tests/foreman/cli/test_errata.py @@ -34,10 +34,8 @@ FAKE_4_CUSTOM_PACKAGE_NAME, FAKE_5_CUSTOM_PACKAGE, PRDS, - REAL_0_ERRATA_ID, REAL_4_ERRATA_CVES, REAL_4_ERRATA_ID, - REAL_RHEL7_0_2_PACKAGE_NAME, REPOS, REPOSET, ) @@ -93,6 +91,7 @@ ) TIMESTAMP_FMT = '%Y-%m-%d %H:%M' +TIMESTAMP_FMT_S = '%Y-%m-%d %H:%M:%S' PSUTIL_RPM = 'python2-psutil-5.6.7-1.el7.x86_64.rpm' @@ -105,9 +104,9 @@ @pytest.fixture(scope='module') -def orgs(): +def orgs(module_target_sat): """Create and return a list of three orgs.""" - return [entities.Organization().create() for _ in range(3)] + return [module_target_sat.api.Organization().create() for _ in range(3)] @pytest.fixture(scope='module') @@ -116,7 +115,7 @@ def products_with_repos(orgs, module_target_sat): products = [] # Create one product for each org, and a second product for the last org. for org, params in zip(orgs + orgs[-1:], REPOS_WITH_ERRATA): - product = entities.Product(organization=org).create() + product = module_target_sat.api.Product(organization=org).create() # Replace the organization entity returned by create(), which contains only the id, # with the one we already have. product.organization = org @@ -172,7 +171,7 @@ def custom_repo( def hosts(request): """Deploy hosts via broker.""" num_hosts = getattr(request, 'param', 2) - with Broker(nick='rhel7', host_class=ContentHost, _count=num_hosts) as hosts: + with Broker(nick='rhel8', host_class=ContentHost, _count=num_hosts) as hosts: if not isinstance(hosts, list) or len(hosts) != num_hosts: pytest.fail('Failed to provision the expected number of hosts.') yield hosts @@ -182,24 +181,39 @@ def hosts(request): def register_hosts( hosts, module_entitlement_manifest_org, - module_ak_cv_lce, - rh_repo, - custom_repo, + module_lce, + module_ak, module_target_sat, ): - """Register hosts to Satellite and install katello-agent rpm.""" + """Register hosts to Satellite""" for host in hosts: - host.install_katello_ca(module_target_sat) - host.register_contenthost(module_entitlement_manifest_org.name, module_ak_cv_lce.name) - host.enable_repo(REPOS['rhst7']['id']) - host.install_katello_agent() + module_target_sat.cli_factory.setup_org_for_a_custom_repo( + { + 'url': REPO_WITH_ERRATA['url'], + 'organization-id': module_entitlement_manifest_org.id, + 'lifecycle-environment-id': module_lce.id, + 'activationkey-id': module_ak.id, + } + ) + host.register( + activation_keys=module_ak.name, + target=module_target_sat, + org=module_entitlement_manifest_org, + loc=None, + ) + assert host.subscribed + return hosts @pytest.fixture -def errata_hosts(register_hosts): +def errata_hosts(register_hosts, target_sat): """Ensure that rpm is installed on host.""" for host in register_hosts: + # Enable all custom and rh repositories. + host.execute(r'subscription-manager repos --enable \*') + host.execute(r'yum-config-manager --enable \*') + host.add_rex_key(satellite=target_sat) # Remove all packages. for errata in REPO_WITH_ERRATA['errata']: # Remove package if present, old or new. @@ -213,6 +227,7 @@ def errata_hosts(register_hosts): result = host.execute(f'yum install -y {old_package}') if result.status != 0: pytest.fail(f'Failed to install {old_package}: {result.stdout} {result.stderr}') + return register_hosts @@ -244,6 +259,43 @@ def host_collection( return host_collection +def start_and_wait_errata_recalculate(sat, host): + """Helper to find any in-progress errata applicability task and wait_for completion. + Otherwise, schedule errata recalculation, wait for the task. + Find the successful completed task(s). + + :param sat: Satellite instance to check for task(s) + :param host: ContentHost instance to schedule errata recalculate + + """ + # Find any in-progress task for this host + search = "label = Actions::Katello::Applicability::Hosts::BulkGenerate and result = pending" + applicability_task_running = sat.cli.Task.list_tasks({'search': search}) + # No task in progress, invoke errata recalculate + if len(applicability_task_running) == 0: + sat.cli.Host.errata_recalculate({'host-id': host.nailgun_host.id}) + host.run('subscription-manager repos') + # Note time check for later wait_for_tasks include 30s margin of safety + timestamp = (datetime.utcnow() - timedelta(seconds=30)).strftime(TIMESTAMP_FMT_S) + # Wait for upload profile event (in case Satellite system is slow) + sat.wait_for_tasks( + search_query=( + 'label = Actions::Katello::Applicability::Hosts::BulkGenerate' + f' and started_at >= "{timestamp}"' + ), + search_rate=15, + max_tries=10, + ) + # Find the successful finished task(s) + search = ( + "label = Actions::Katello::Applicability::Hosts::BulkGenerate" + " and result = success" + f" and started_at >= '{timestamp}'" + ) + applicability_task_success = sat.cli.Task.list_tasks({'search': search}) + assert applicability_task_success, f'No successful task found by search: {search}' + + def is_rpm_installed(host, rpm=None): """Return whether the specified rpm is installed. @@ -325,7 +377,7 @@ def filter_sort_errata(sat, org, sort_by_date='issued', filter_by_org=None): # Build a sorted errata info list, which also contains the sort field. errata_internal_ids = [errata['id'] for errata in errata_list] sorted_errata_info = get_sorted_errata_info_by_id( - sat, errata_internal_ids, sort_by=sort_by_date, sort_reversed=sort_reversed + errata_internal_ids, sort_by=sort_by_date, sort_reversed=sort_reversed ) sort_field_values = [errata[sort_by_date] for errata in sorted_errata_info] @@ -336,7 +388,7 @@ def filter_sort_errata(sat, org, sort_by_date='issued', filter_by_org=None): assert errata_ids == sorted_errata_ids -def cv_publish_promote(sat, cv, org, lce): +def cv_publish_promote(sat, cv, org, lce, force=False): """Publish and promote a new version into the given lifecycle environment. :param cv: content view @@ -346,13 +398,15 @@ def cv_publish_promote(sat, cv, org, lce): :param lce: lifecycle environment :type lce: entities.LifecycleEnvironment """ - sat.cli.ContentView.publish({'id': cv.id}) - sat.cli.ContentView.info({'id': cv.id})['versions'][-1] + sat.cli.ContentView.publish({'id': cv.id, 'organization': org}) + # Sort CV Version results by --id, grab last version (latest) + cvv_id = sorted(cvv['id'] for cvv in sat.cli.ContentView.info({'id': cv.id})['versions'])[-1] sat.cli.ContentView.version_promote( { - 'id': cv.id, - 'organization-id': org, + 'id': cvv_id, + 'organization': org, 'to-lifecycle-environment-id': lce.id, + 'force': force, } ) @@ -366,6 +420,7 @@ def cv_filter_cleanup(sat, filter_id, cv, org, lce): 'organization-id': org.id, } ) + cv = cv.read() cv_publish_promote(sat, cv, org, lce) @@ -407,9 +462,6 @@ def test_positive_install_by_host_collection_and_org( """ errata_id = REPO_WITH_ERRATA['errata'][0]['id'] - for host in errata_hosts: - host.add_rex_key(satellite=target_sat) - if filter_by_hc == 'id': host_collection_query = f'host_collection_id = {host_collection["id"]}' elif filter_by_hc == 'name': @@ -644,19 +696,10 @@ def test_install_errata_to_one_host( # Remove the package on first host to remove need for errata. result = errata_hosts[0].execute(f'yum erase -y {errata["package_name"]}') assert result.status == 0, f'Failed to erase the rpm: {result.stdout}' - # Add ssh keys + for host in errata_hosts: - host.add_rex_key(satellite=target_sat) - target_sat.cli.Host.errata_recalculate({'host-id': host.nailgun_host.id}) - timestamp = (datetime.utcnow() - timedelta(minutes=1)).strftime(TIMESTAMP_FMT) - target_sat.wait_for_tasks( - search_query=( - 'label = Actions::Katello::Applicability::Hosts::BulkGenerate' - f' and started_at >= "{timestamp}"' - ), - search_rate=15, - max_tries=10, - ) + start_and_wait_errata_recalculate(target_sat, host) + assert not is_rpm_installed( errata_hosts[0], rpm=errata['package_name'] ), 'Package should not be installed on host.' @@ -668,7 +711,12 @@ def test_install_errata_to_one_host( @pytest.mark.tier3 @pytest.mark.e2e def test_positive_list_affected_chosts_by_erratum_restrict_flag( - request, module_entitlement_manifest_org, module_cv, module_lce, errata_hosts, target_sat + target_sat, + request, + module_entitlement_manifest_org, + module_cv, + module_lce, + errata_hosts, ): """View a list of affected content hosts for an erratum filtered with restrict flags. Applicability is calculated using the Library, @@ -700,32 +748,30 @@ def test_positive_list_affected_chosts_by_erratum_restrict_flag( :CaseAutomation: Automated """ - # Uninstall package so that only the first errata applies. for host in errata_hosts: host.execute(f'yum erase -y {REPO_WITH_ERRATA["errata"][1]["package_name"]}') + start_and_wait_errata_recalculate(target_sat, host) # Create list of uninstallable errata. errata = REPO_WITH_ERRATA['errata'][0] uninstallable = REPO_WITH_ERRATA['errata_ids'].copy() uninstallable.remove(errata['id']) - # Check search for only installable errata param = { 'errata-restrict-installable': 1, - 'content-view-id': module_cv.id, 'lifecycle-environment-id': module_lce.id, 'organization-id': module_entitlement_manifest_org.id, 'per-page': PER_PAGE_LARGE, } errata_ids = get_errata_ids(target_sat, param) + assert errata_ids, 'No installable errata found' assert errata['id'] in errata_ids, 'Errata not found in list of installable errata' assert not set(uninstallable) & set(errata_ids), 'Unexpected errata found' # Check search of errata is not affected by installable=0 restrict flag param = { 'errata-restrict-installable': 0, - 'content-view-id': module_cv.id, 'lifecycle-environment-id': module_lce.id, 'organization-id': module_entitlement_manifest_org.id, 'per-page': PER_PAGE_LARGE, @@ -788,7 +834,7 @@ def cleanup(): ) # Publish and promote a new version with the filter - cv_publish_promote(module_cv, module_entitlement_manifest_org, module_lce) + cv_publish_promote(target_sat, module_cv, module_entitlement_manifest_org, module_lce) # Check that the installable erratum is no longer present in the list param = { @@ -817,7 +863,6 @@ def test_host_errata_search_commands( module_entitlement_manifest_org, module_cv, module_lce, - host_collection, errata_hosts, target_sat, ): @@ -844,11 +889,8 @@ def test_host_errata_search_commands( :expectedresults: The hosts are correctly listed for security and bugfix advisories. """ - # note time for later wait_for_tasks include 2 mins margin of safety. - timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime(TIMESTAMP_FMT) errata = REPO_WITH_ERRATA['errata'] - # Update package on first host so that the security advisory doesn't apply. result = errata_hosts[0].execute(f'yum update -y {errata[0]["new_package"]}') assert result.status == 0, 'Failed to install rpm' @@ -858,17 +900,7 @@ def test_host_errata_search_commands( assert result.status == 0, 'Failed to install rpm' for host in errata_hosts: - timestamp = (datetime.utcnow() - timedelta(minutes=1)).strftime(TIMESTAMP_FMT) - target_sat.cli.Host.errata_recalculate({'host-id': host.nailgun_host.id}) - # Wait for upload profile event (in case Satellite system slow) - target_sat.wait_for_tasks( - search_query=( - 'label = Actions::Katello::Applicability::Hosts::BulkGenerate' - f' and started_at >= "{timestamp}"' - ), - search_rate=20, - max_tries=10, - ) + start_and_wait_errata_recalculate(target_sat, host) # Step 1: Search for hosts that require bugfix advisories result = target_sat.cli.Host.list( @@ -975,7 +1007,7 @@ def cleanup(): ) # Publish and promote a new version with the filter - cv_publish_promote(module_cv, module_entitlement_manifest_org, module_lce) + cv_publish_promote(target_sat, module_cv, module_entitlement_manifest_org, module_lce) # Step 8: Run tests again. Applicable should still be true, installable should now be false. # Search for hosts that require the bugfix package. @@ -1011,7 +1043,7 @@ def cleanup(): ids=('org_id', 'org_name', 'org_label', 'no_org_filter'), ) def test_positive_list_filter_by_org_sort_by_date( - module_entitlement_manifest_org, rh_repo, custom_repo, filter_by_org, sort_by_date, target_sat + module_entitlement_manifest_org, rh_repo, custom_repo, filter_by_org, sort_by_date ): """Filter by organization and sort by date. @@ -1028,7 +1060,6 @@ def test_positive_list_filter_by_org_sort_by_date( :expectedresults: Errata are filtered by org and sorted by date. """ filter_sort_errata( - target_sat, module_entitlement_manifest_org, sort_by_date=sort_by_date, filter_by_org=filter_by_org, @@ -1036,7 +1067,7 @@ def test_positive_list_filter_by_org_sort_by_date( @pytest.mark.tier3 -def test_positive_list_filter_by_product_id(products_with_repos): +def test_positive_list_filter_by_product_id(target_sat, products_with_repos): """Filter errata by product id :id: 7d06950a-c058-48b3-a384-c3565cbd643f @@ -1050,7 +1081,7 @@ def test_positive_list_filter_by_product_id(products_with_repos): params = [ {'product-id': product.id, 'per-page': PER_PAGE_LARGE} for product in products_with_repos ] - errata_ids = get_errata_ids(*params) + errata_ids = get_errata_ids(target_sat, *params) check_errata(errata_ids) @@ -1060,7 +1091,7 @@ def test_positive_list_filter_by_product_id(products_with_repos): 'filter_by_org', ['id', 'name', 'label'], ids=('org_id', 'org_name', 'org_label') ) def test_positive_list_filter_by_product_and_org( - products_with_repos, filter_by_product, filter_by_org + target_sat, products_with_repos, filter_by_product, filter_by_org ): """Filter errata by product id and Org id @@ -1093,7 +1124,7 @@ def test_positive_list_filter_by_product_and_org( params.append(param) - errata_ids = get_errata_ids(*params) + errata_ids = get_errata_ids(target_sat, *params) check_errata(errata_ids) @@ -1125,7 +1156,7 @@ def test_negative_list_filter_by_product_name(products_with_repos, module_target @pytest.mark.parametrize( 'filter_by_org', ['id', 'name', 'label'], ids=('org_id', 'org_name', 'org_label') ) -def test_positive_list_filter_by_org(products_with_repos, filter_by_org): +def test_positive_list_filter_by_org(target_sat, products_with_repos, filter_by_org): """Filter errata by org id, name, or label. :id: de7646be-7ac8-4dbe-8cc3-6959808d78fa @@ -1152,7 +1183,7 @@ def test_positive_list_filter_by_org(products_with_repos, filter_by_org): params.append(param) - errata_ids = get_errata_ids(*params) + errata_ids = get_errata_ids(target_sat, *params) check_errata(errata_ids, by_org=True) @@ -1219,7 +1250,7 @@ def test_positive_check_errata_dates(module_entitlement_manifest_org, module_tar :BZ: 1695163 """ - product = entities.Product(organization=module_entitlement_manifest_org).create() + product = module_target_sat.api.Product(organization=module_entitlement_manifest_org).create() repo = module_target_sat.cli_factory.make_repository( {'content-type': 'yum', 'product-id': product.id, 'url': REPO_WITH_ERRATA['url']} ) @@ -1253,7 +1284,7 @@ def rh_repo_module_manifest(module_entitlement_manifest_org, module_target_sat): releasever=None, ) # Sync step because repo is not synced by default - rh_repo = entities.Repository(id=rh_repo_id).read() + rh_repo = module_target_sat.api.Repository(id=rh_repo_id).read() rh_repo.sync() return rh_repo @@ -1286,36 +1317,98 @@ def new_module_ak(module_entitlement_manifest_org, rh_repo_module_manifest, defa @pytest.fixture def errata_host( - module_entitlement_manifest_org, rhel7_contenthost_module, new_module_ak, target_sat + target_sat, + rhel8_contenthost, + module_entitlement_manifest_org, + module_lce_library, + new_module_ak, ): - """A RHEL77 Content Host that has applicable errata and registered to Library""" - # python-psutil is obsoleted by python2-psutil, so get older python2-psutil for errata test - rhel7_contenthost_module.run(f'rpm -Uvh {settings.repos.epel_repo.url}/{PSUTIL_RPM}') - rhel7_contenthost_module.install_katello_ca(target_sat) - rhel7_contenthost_module.register_contenthost( - module_entitlement_manifest_org.label, new_module_ak.name + """A RHEL8 Content Host that has applicable errata and registered to Library, using Global Registration.""" + org = module_entitlement_manifest_org + cv = target_sat.api.ContentView( + organization=org, + environment=[module_lce_library.id], + ).create() + target_sat.cli_factory.setup_org_for_a_custom_repo( + { + 'url': REPO_WITH_ERRATA['url'], + 'organization-id': org.id, + 'lifecycle-environment-id': module_lce_library.id, + 'activationkey-id': new_module_ak.id, + 'content-view-id': cv.id, + } + ) + rhel8_contenthost.register( + activation_keys=new_module_ak.name, + target=target_sat, + org=org, + loc=None, ) - assert rhel7_contenthost_module.nailgun_host.read_json()['subscription_status'] == 0 - rhel7_contenthost_module.install_katello_host_tools() - rhel7_contenthost_module.add_rex_key(satellite=target_sat) - return rhel7_contenthost_module + rhel8_contenthost.add_rex_key(satellite=target_sat) + assert rhel8_contenthost.subscribed + assert rhel8_contenthost.applicable_errata_count == 0 + + rhel8_contenthost.execute(r'yum-config-manager --enable \*') + rhel8_contenthost.execute(r'subscription-manager repos --enable \*') + for errata in REPO_WITH_ERRATA['errata']: + # Remove package if present, old or new. + package_name = errata['package_name'] + result = rhel8_contenthost.execute(f'yum erase -y {package_name}') + if result.status != 0: + pytest.fail(f'Failed to remove {package_name}: {result.stdout} {result.stderr}') + + # Install old package, so that errata apply. + old_package = errata['old_package'] + result = rhel8_contenthost.execute(f'yum install -y {old_package}') + if result.status != 0: + pytest.fail(f'Failed to install {old_package}: {result.stdout} {result.stderr}') + + start_and_wait_errata_recalculate(target_sat, rhel8_contenthost) + assert rhel8_contenthost.applicable_errata_count == 2 + + return rhel8_contenthost @pytest.fixture -def chost(module_entitlement_manifest_org, rhel7_contenthost_module, new_module_ak, target_sat): +def chost( + module_entitlement_manifest_org, + rhel7_contenthost, + module_lce_library, + new_module_ak, + target_sat, +): """A RHEL77 Content Host registered to Library that does not have applicable errata""" - rhel7_contenthost_module.install_katello_ca(target_sat) - rhel7_contenthost_module.register_contenthost( - module_entitlement_manifest_org.label, new_module_ak.name + module_cv = target_sat.api.ContentView( + organization=module_entitlement_manifest_org, + environment=[module_lce_library.id], + ).create() + target_sat.cli_factory.setup_org_for_a_custom_repo( + { + 'url': REPO_WITH_ERRATA['url'], + 'organization-id': module_entitlement_manifest_org.id, + 'lifecycle-environment-id': module_lce_library.id, + 'activationkey-id': new_module_ak.id, + 'content-view-id': module_cv.id, + } + ) + rhel7_contenthost.register( + activation_keys=new_module_ak.name, + target=target_sat, + org=module_entitlement_manifest_org, + loc=None, ) - assert rhel7_contenthost_module.nailgun_host.read_json()['subscription_status'] == 0 - rhel7_contenthost_module.install_katello_host_tools() - return rhel7_contenthost_module + assert rhel7_contenthost.subscribed + assert rhel7_contenthost.applicable_errata_count == 0 + assert rhel7_contenthost.nailgun_host.read_json()['subscription_status'] == 2 + rhel7_contenthost.execute(r'subscription-manager repos --enable \*') + rhel7_contenthost.execute(r'yum-config-manager --enable \*') @pytest.mark.tier2 @pytest.mark.no_containers -def test_apply_errata_using_default_content_view(errata_host, target_sat): +def test_apply_errata_using_default_content_view( + errata_host, module_entitlement_manifest_org, target_sat +): """Updating an applicable errata on a host attached to the default content view causes the errata to not be applicable. @@ -1333,9 +1426,11 @@ def test_apply_errata_using_default_content_view(errata_host, target_sat): :CaseImportance: High """ - # check that package errata is applicable + assert errata_host.applicable_errata_count > 0 + # Check that package errata is applicable + erratum_id = REPO_WITH_ERRATA['errata'][0]['id'] erratum = target_sat.cli.Host.errata_list( - {'host': errata_host.hostname, 'search': f'id = {REAL_0_ERRATA_ID}'} + {'host': errata_host.hostname, 'search': f'id = {erratum_id}'} ) assert len(erratum) == 1 assert erratum[0]['installable'] == 'true' @@ -1344,25 +1439,16 @@ def test_apply_errata_using_default_content_view(errata_host, target_sat): { 'feature': 'katello_errata_install', 'search-query': f'name = {errata_host.hostname}', - 'inputs': f"errata='{REAL_0_ERRATA_ID}'", - 'organization-id': f'{errata_host.nailgun_host.organization.id}', + 'inputs': f'errata={erratum[0]["erratum-id"]}', + 'organization-id': f'{module_entitlement_manifest_org.id}', } )[1]['id'] assert 'success' in result - timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime(TIMESTAMP_FMT) - target_sat.cli.Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) - target_sat.wait_for_tasks( - search_query=( - 'label = Actions::Katello::Applicability::Hosts::BulkGenerate' - f' and started_at >= "{timestamp}"' - ), - search_rate=15, - max_tries=10, - ) + start_and_wait_errata_recalculate(target_sat, errata_host) # Assert that the erratum is no longer applicable erratum = target_sat.cli.Host.errata_list( - {'host': errata_host.hostname, 'search': f'id = {REAL_0_ERRATA_ID}'} + {'host': errata_host.hostname, 'search': f'id = {erratum_id}'} ) assert len(erratum) == 0 @@ -1389,56 +1475,35 @@ def test_update_applicable_package_using_default_content_view(errata_host, targe :CaseImportance: High """ # check that package is applicable + package_name = REPO_WITH_ERRATA['errata'][0]['package_name'] applicable_packages = target_sat.cli.Package.list( { 'host-id': errata_host.nailgun_host.id, 'packages-restrict-applicable': 'true', - 'search': f'name={REAL_RHEL7_0_2_PACKAGE_NAME}', + 'search': f'name={package_name}', } ) - timestamp = (datetime.utcnow()).strftime(TIMESTAMP_FMT) - target_sat.cli.Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) - target_sat.wait_for_tasks( - search_query=( - 'label = Actions::Katello::Applicability::Hosts::BulkGenerate' - f' and started_at >= "{timestamp}"' - ), - search_rate=20, - max_tries=15, - ) + + start_and_wait_errata_recalculate(target_sat, errata_host) assert len(applicable_packages) == 1 - assert REAL_RHEL7_0_2_PACKAGE_NAME in applicable_packages[0]['filename'] + assert package_name in applicable_packages[0]['filename'] # Update package from Library, i.e. Default CV result = target_sat.cli.JobInvocation.create( { 'feature': 'katello_errata_install', 'search-query': f'name = {errata_host.hostname}', - 'inputs': f"errata='{REAL_0_ERRATA_ID}'", + 'inputs': f'errata={REPO_WITH_ERRATA["errata"][0]["id"]}', 'organization-id': f'{errata_host.nailgun_host.organization.id}', } )[1]['id'] assert 'success' in result - # note time for later wait_for_tasks include 2 mins margin of safety. - timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime(TIMESTAMP_FMT) - target_sat.cli.Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) - - # Wait for upload profile event (in case Satellite system slow) - target_sat.wait_for_tasks( - search_query=( - 'label = Actions::Katello::Applicability::Hosts::BulkGenerate' - f' and started_at >= "{timestamp}"' - ), - search_rate=30, - max_tries=10, - ) - + start_and_wait_errata_recalculate(target_sat, errata_host) # Assert that the package is no longer applicable - target_sat.cli.Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) applicable_packages = target_sat.cli.Package.list( { 'host-id': errata_host.nailgun_host.id, 'packages-restrict-applicable': 'true', - 'search': f'name={REAL_RHEL7_0_2_PACKAGE_NAME}', + 'search': f'name={package_name}', } ) assert len(applicable_packages) == 0 @@ -1467,51 +1532,42 @@ def test_downgrade_applicable_package_using_default_content_view(errata_host, ta :CaseImportance: High """ # Update package from Library, i.e. Default CV - errata_host.run(f'yum -y update {REAL_RHEL7_0_2_PACKAGE_NAME}') + errata_host.run(f'yum -y update {FAKE_2_CUSTOM_PACKAGE_NAME}') + start_and_wait_errata_recalculate(target_sat, errata_host) # Assert that the package is not applicable applicable_packages = target_sat.cli.Package.list( { 'host-id': errata_host.nailgun_host.id, 'packages-restrict-applicable': 'true', - 'search': f'name={REAL_RHEL7_0_2_PACKAGE_NAME}', + 'search': f'name={FAKE_2_CUSTOM_PACKAGE_NAME}', } ) assert len(applicable_packages) == 0 - # note time for later wait_for_tasks include 2 mins margin of safety. - # Downgrade package (we can't get it from Library, so get older one from EPEL) - errata_host.run(f'curl -O {settings.repos.epel_repo.url}/{PSUTIL_RPM}') - timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime(TIMESTAMP_FMT) - errata_host.run(f'yum -y downgrade {PSUTIL_RPM}') - target_sat.cli.Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) - # Wait for upload profile event (in case Satellite system slow) - target_sat.wait_for_tasks( - search_query=( - 'label = Actions::Katello::Applicability::Hosts::BulkGenerate' - f' and started_at >= "{timestamp}"' - ), - search_rate=15, - max_tries=10, - ) + + # Downgrade package + errata_host.run(f'yum -y downgrade {FAKE_1_CUSTOM_PACKAGE}') + start_and_wait_errata_recalculate(target_sat, errata_host) # check that package is applicable applicable_packages = target_sat.cli.Package.list( { 'host-id': errata_host.nailgun_host.id, 'packages-restrict-applicable': 'true', - 'search': f'name={REAL_RHEL7_0_2_PACKAGE_NAME}', + 'search': f'name={FAKE_2_CUSTOM_PACKAGE_NAME}', } ) assert len(applicable_packages) == 1 - assert REAL_RHEL7_0_2_PACKAGE_NAME in applicable_packages[0]['filename'] + assert FAKE_2_CUSTOM_PACKAGE_NAME in applicable_packages[0]['filename'] @pytest.mark.tier2 -def test_install_applicable_package_to_registerd_host(chost, target_sat): +def test_install_applicable_package_to_registerd_host(errata_host, target_sat): """Installing an older package to an already registered host should show the newer package and errata as applicable and installable. :id: 519bfe91-cf86-4d6e-94ef-aaf3e5d40a81 - :setup: Register a host to default org with Library + :setup: Register a host to default org with Library, + Remove the newer package version if present :steps: 1. Ensure package is not applicable @@ -1524,42 +1580,29 @@ def test_install_applicable_package_to_registerd_host(chost, target_sat): :CaseImportance: Medium """ + errata_host.run(f'yum -y remove {FAKE_2_CUSTOM_PACKAGE_NAME}') + start_and_wait_errata_recalculate(target_sat, errata_host) # Assert that the package is not applicable - target_sat.cli.Host.errata_recalculate({'host-id': chost.nailgun_host.id}) applicable_packages = target_sat.cli.Package.list( { - 'host-id': chost.nailgun_host.id, + 'host-id': errata_host.nailgun_host.id, 'packages-restrict-applicable': 'true', - 'search': f'name={REAL_RHEL7_0_2_PACKAGE_NAME}', + 'search': f'name={FAKE_2_CUSTOM_PACKAGE_NAME}', } ) assert len(applicable_packages) == 0 - # python-psutil is obsoleted by python2-psutil, so download older python2-psutil for this test - chost.run(f'curl -O {settings.repos.epel_repo.url}/{PSUTIL_RPM}') - # note time for later wait_for_tasks include 2 mins margin of safety. - timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime(TIMESTAMP_FMT) - chost.run(f'yum -y install {PSUTIL_RPM}') - # Wait for upload profile event (in case Satellite system slow) - target_sat.wait_for_tasks( - search_query=( - 'label = Actions::Katello::Applicability::Hosts::BulkGenerate' - f' and started_at >= "{timestamp}"' - ), - search_rate=15, - max_tries=10, - ) - target_sat.cli.Host.errata_recalculate({'host-id': chost.nailgun_host.id}) - + errata_host.run(f'yum -y install {FAKE_1_CUSTOM_PACKAGE}') + start_and_wait_errata_recalculate(target_sat, errata_host) # check that package is applicable applicable_packages = target_sat.cli.Package.list( { - 'host-id': chost.nailgun_host.id, + 'host-id': errata_host.nailgun_host.id, 'packages-restrict-applicable': 'true', - 'search': f'name={REAL_RHEL7_0_2_PACKAGE_NAME}', + 'search': f'name={FAKE_2_CUSTOM_PACKAGE_NAME}', } ) assert len(applicable_packages) == 1 - assert REAL_RHEL7_0_2_PACKAGE_NAME in applicable_packages[0]['filename'] + assert FAKE_2_CUSTOM_PACKAGE_NAME in applicable_packages[0]['filename'] @pytest.mark.tier2 @@ -1587,43 +1630,27 @@ def test_downgrading_package_shows_errata_from_library( :CaseImportance: High """ # Update package from Library, i.e. Default CV - errata_host.run(f'yum -y update {REAL_RHEL7_0_2_PACKAGE_NAME}') - # Assert that the package is not applicable + errata_host.run(f'yum -y install {FAKE_2_CUSTOM_PACKAGE}') + start_and_wait_errata_recalculate(target_sat, errata_host) applicable_packages = target_sat.cli.Package.list( { 'host-id': errata_host.nailgun_host.id, 'packages-restrict-applicable': 'true', - 'search': f'name={REAL_RHEL7_0_2_PACKAGE_NAME}', + 'search': f'name={FAKE_2_CUSTOM_PACKAGE_NAME}', } ) assert len(applicable_packages) == 0 - # note time for later wait_for_tasks include 2 mins margin of safety. - timestamp = (datetime.utcnow() - timedelta(minutes=2)).strftime(TIMESTAMP_FMT) - # Downgrade package (we can't get it from Library, so get older one from EPEL) - errata_host.run(f'curl -O {settings.repos.epel_repo.url}/{PSUTIL_RPM}') - errata_host.run(f'yum -y downgrade {PSUTIL_RPM}') - # Wait for upload profile event (in case Satellite system slow) - target_sat.cli.Host.errata_recalculate({'host-id': errata_host.nailgun_host.id}) - # Wait for upload profile event (in case Satellite system slow) - target_sat.wait_for_tasks( - search_query=( - 'label = Actions::Katello::Applicability::Hosts::BulkGenerate' - f' and started_at >= "{timestamp}"' - ), - search_rate=15, - max_tries=10, - ) - # check that errata is applicable + # Downgrade package + errata_host.run(f'yum -y downgrade {FAKE_1_CUSTOM_PACKAGE}') + start_and_wait_errata_recalculate(target_sat, errata_host) param = { 'errata-restrict-applicable': 1, - 'organization-id': module_manifest_org.id, 'per-page': PER_PAGE_LARGE, } - errata_ids = get_errata_ids(param) - assert REAL_0_ERRATA_ID in errata_ids + errata_ids = get_errata_ids(target_sat, param) + assert settings.repos.yum_6.errata[2] in errata_ids -@pytest.mark.skip_if_open('BZ:1785146') @pytest.mark.tier2 def test_errata_list_by_contentview_filter(module_entitlement_manifest_org, module_target_sat): """Hammer command to list errata should take filter ID into consideration. @@ -1645,16 +1672,18 @@ def test_errata_list_by_contentview_filter(module_entitlement_manifest_org, modu :BZ: 1785146 """ - product = entities.Product(organization=module_entitlement_manifest_org).create() + product = module_target_sat.api.Product(organization=module_entitlement_manifest_org).create() repo = module_target_sat.cli_factory.make_repository( {'content-type': 'yum', 'product-id': product.id, 'url': REPO_WITH_ERRATA['url']} ) module_target_sat.cli.Repository.synchronize({'id': repo['id']}) - lce = entities.LifecycleEnvironment(organization=module_entitlement_manifest_org).create() - cv = entities.ContentView( + lce = module_target_sat.api.LifecycleEnvironment( + organization=module_entitlement_manifest_org + ).create() + cv = module_target_sat.api.ContentView( organization=module_entitlement_manifest_org, repository=[repo['id']] ).create() - cv_publish_promote(cv, module_entitlement_manifest_org, lce) + cv_publish_promote(module_target_sat, cv, module_entitlement_manifest_org, lce) errata_count = len( module_target_sat.cli.Erratum.list( { @@ -1663,11 +1692,11 @@ def test_errata_list_by_contentview_filter(module_entitlement_manifest_org, modu } ) ) - cvf = entities.ErratumContentViewFilter(content_view=cv, inclusion=True).create() - errata_id = entities.Errata().search( + cvf = module_target_sat.api.ErratumContentViewFilter(content_view=cv, inclusion=True).create() + errata_id = module_target_sat.api.Errata().search( query={'search': f'errata_id="{settings.repos.yum_9.errata[0]}"'} )[0] - entities.ContentViewFilterRule(content_view_filter=cvf, errata=errata_id).create() + module_target_sat.api.ContentViewFilterRule(content_view_filter=cvf, errata=errata_id).create() cv.publish() cv_version_info = cv.read().version[1].read() errata_count_cvf = len( From cd96a260762fa5ac26f7e6c3d683f6ba4c4bddf2 Mon Sep 17 00:00:00 2001 From: Jameer Pathan <21165044+jameerpathan111@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:22:45 +0100 Subject: [PATCH 392/586] Fix test failures from cli.factory refactor (cherry picked from commit 5fac78b29ae8e5a189de455385e9c591c329b5af) --- tests/foreman/cli/test_repository.py | 64 ++++++++++++++-------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/tests/foreman/cli/test_repository.py b/tests/foreman/cli/test_repository.py index d1f434ce010..1ca398b9ff4 100644 --- a/tests/foreman/cli/test_repository.py +++ b/tests/foreman/cli/test_repository.py @@ -2553,8 +2553,8 @@ def test_positive_sync_ansible_collection(self, repo, module_target_sat): :parametrized: yes """ - module_target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) - repo = module_target_sat.cli_factory.Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert repo['sync']['status'] == 'Success' @pytest.mark.tier2 @@ -2586,8 +2586,8 @@ def test_positive_export_ansible_collection(self, repo, module_org, target_sat): """ import_org = target_sat.cli_factory.make_org() - target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) - repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert repo['sync']['status'] == 'Success' # export result = target_sat.cli.ContentExport.completeLibrary({'organization-id': module_org.id}) @@ -2603,15 +2603,15 @@ def test_positive_export_ansible_collection(self, repo, module_org, target_sat): {'name': 'Import-Library', 'organization-label': import_org['label']} ) assert cv['description'] == 'Content View used for importing into library' - prods = target_sat.cli_factory.Product.list({'organization-id': import_org['id']}) - prod = target_sat.cli_factory.Product.info( + prods = target_sat.cli.Product.list({'organization-id': import_org['id']}) + prod = target_sat.cli.Product.info( {'id': prods[0]['id'], 'organization-id': import_org['id']} ) ac_content = [ cont for cont in prod['content'] if cont['content-type'] == 'ansible_collection' ] assert len(ac_content) > 0 - repo = target_sat.cli_factory.Repository.info( + repo = target_sat.cli.Repository.info( {'name': ac_content[0]['repo-name'], 'product-id': prod['id']} ) result = target_sat.execute(f'curl {repo["published-at"]}') @@ -2646,8 +2646,8 @@ def test_positive_sync_ansible_collection_from_satellite(self, repo, target_sat) """ import_org = target_sat.cli_factory.make_org() - target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) - repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert repo['sync']['status'] == 'Success' published_url = repo['published-at'] # sync from different org @@ -2664,8 +2664,8 @@ def test_positive_sync_ansible_collection_from_satellite(self, repo, target_sat) [{ name: theforeman.operations, version: "0.1.0"}]}', } ) - target_sat.cli_factory.Repository.synchronize({'id': repo_2['id']}) - repo_2_status = target_sat.cli_factory.Repository.info({'id': repo_2['id']}) + target_sat.cli.Repository.synchronize({'id': repo_2['id']}) + repo_2_status = target_sat.cli.Repository.info({'id': repo_2['id']}) assert repo_2_status['sync']['status'] == 'Success' @@ -2689,8 +2689,8 @@ def test_positive_sync_publish_promote_cv(self, repo, module_org, target_sat): lifecycle environment """ lce = target_sat.cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) - target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) - synced_repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + synced_repo = target_sat.cli.Repository.info({'id': repo['id']}) assert synced_repo['sync']['status'].lower() == 'success' assert synced_repo['content-counts']['packages'] == '35' cv = target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) @@ -2724,7 +2724,7 @@ def test_positive_sync(self, repo, module_org, module_product, target_sat): :expectedresults: drpms can be listed in repository """ - target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) result = target_sat.execute( f"ls /var/lib/pulp/published/yum/https/repos/{module_org.label}/Library" f"/custom/{module_product.label}/{repo['label']}/drpms/ | grep .drpm" @@ -2747,7 +2747,7 @@ def test_positive_sync_publish_cv(self, repo, module_org, module_product, target :expectedresults: drpms can be listed in content view """ - target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) cv = target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) target_sat.cli.ContentView.add_repository({'id': cv['id'], 'repository-id': repo['id']}) target_sat.cli.ContentView.publish({'id': cv['id']}) @@ -2776,7 +2776,7 @@ def test_positive_sync_publish_promote_cv(self, repo, module_org, module_product lifecycle environment """ lce = target_sat.cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) - target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) cv = target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) target_sat.cli.ContentView.add_repository({'id': cv['id'], 'repository-id': repo['id']}) target_sat.cli.ContentView.publish({'id': cv['id']}) @@ -3012,7 +3012,7 @@ def test_positive_upload_file_to_file_repo(self, repo_options, repo, target_sat) local_path=DataFile.RPM_TO_UPLOAD, remote_path=f"/tmp/{RPM_TO_UPLOAD}", ) - result = target_sat.cli_factory.Repository.upload_content( + result = target_sat.cli.Repository.upload_content( { 'name': repo['name'], 'organization': repo['organization'], @@ -3021,7 +3021,7 @@ def test_positive_upload_file_to_file_repo(self, repo_options, repo, target_sat) } ) assert f"Successfully uploaded file '{RPM_TO_UPLOAD}'" in result[0]['message'] - repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert repo['content-counts']['files'] == '1' filesearch = entities.File().search( query={"search": f"name={RPM_TO_UPLOAD} and repository={repo['name']}"} @@ -3077,7 +3077,7 @@ def test_positive_remove_file(self, repo, target_sat): local_path=DataFile.RPM_TO_UPLOAD, remote_path=f"/tmp/{RPM_TO_UPLOAD}", ) - result = target_sat.cli_factory.Repository.upload_content( + result = target_sat.cli.Repository.upload_content( { 'name': repo['name'], 'organization': repo['organization'], @@ -3086,13 +3086,13 @@ def test_positive_remove_file(self, repo, target_sat): } ) assert f"Successfully uploaded file '{RPM_TO_UPLOAD}'" in result[0]['message'] - repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert int(repo['content-counts']['files']) > 0 files = target_sat.cli.File.list({'repository-id': repo['id']}) - target_sat.cli_factory.Repository.remove_content( + target_sat.cli.Repository.remove_content( {'id': repo['id'], 'ids': [file_['id'] for file_ in files]} ) - repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert repo['content-counts']['files'] == '0' @pytest.mark.tier2 @@ -3130,8 +3130,8 @@ def test_positive_remote_directory_sync(self, repo, module_target_sat): :expectedresults: entire directory is synced over http """ - module_target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) - repo = module_target_sat.cli_factory.Repository.info({'id': repo['id']}) + module_target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = module_target_sat.cli.Repository.info({'id': repo['id']}) assert repo['sync']['status'] == 'Success' assert repo['content-counts']['files'] == '2' @@ -3168,8 +3168,8 @@ def test_positive_file_repo_local_directory_sync(self, repo, target_sat): f'wget -P {CUSTOM_LOCAL_FOLDER} -r -np -nH --cut-dirs=5 -R "index.html*" ' f'{CUSTOM_FILE_REPO}' ) - target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) - repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert int(repo['content-counts']['files']) > 1 @pytest.mark.tier2 @@ -3208,8 +3208,8 @@ def test_positive_symlinks_sync(self, repo, target_sat): ) target_sat.execute(f'ln -s {CUSTOM_LOCAL_FOLDER} /{gen_string("alpha")}') - target_sat.cli_factory.Repository.synchronize({'id': repo['id']}) - repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) + target_sat.cli.Repository.synchronize({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) assert int(repo['content-counts']['files']) > 1 @pytest.mark.tier2 @@ -3242,7 +3242,7 @@ def test_file_repo_contains_only_newer_file(self, repo_options, repo, target_sat """ text_file_name = f'test-{gen_string("alpha", 5)}.txt'.lower() target_sat.execute(f'echo "First File" > /tmp/{text_file_name}') - result = target_sat.cli_factory.Repository.upload_content( + result = target_sat.cli.Repository.upload_content( { 'name': repo['name'], 'organization': repo['organization'], @@ -3251,7 +3251,7 @@ def test_file_repo_contains_only_newer_file(self, repo_options, repo, target_sat } ) assert f"Successfully uploaded file '{text_file_name}'" in result[0]['message'] - repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) # Assert there is only one file assert repo['content-counts']['files'] == '1' filesearch = entities.File().search( @@ -3260,7 +3260,7 @@ def test_file_repo_contains_only_newer_file(self, repo_options, repo, target_sat assert text_file_name == filesearch[0].name # Create new version of the file by changing the text target_sat.execute(f'echo "Second File" > /tmp/{text_file_name}') - result = target_sat.cli_factory.Repository.upload_content( + result = target_sat.cli.Repository.upload_content( { 'name': repo['name'], 'organization': repo['organization'], @@ -3269,7 +3269,7 @@ def test_file_repo_contains_only_newer_file(self, repo_options, repo, target_sat } ) assert f"Successfully uploaded file '{text_file_name}'" in result[0]['message'] - repo = target_sat.cli_factory.Repository.info({'id': repo['id']}) + repo = target_sat.cli.Repository.info({'id': repo['id']}) # Assert there is still only one file assert repo['content-counts']['files'] == '1' filesearch = entities.File().search( From fbef12f7473fe5a932b77a55e4db5d1a9169e02d Mon Sep 17 00:00:00 2001 From: Gaurav Talreja Date: Fri, 15 Dec 2023 15:24:10 +0530 Subject: [PATCH 393/586] [6.14.z] Add test coverage for provisioning with fips_enabled (#13458) Add test coverage for provisioning with fips_enabled (#12695) Signed-off-by: Gaurav Talreja (cherry picked from commit c740d61536d47ce1ac732e726b6519a9dd24a1f1) --- tests/foreman/api/test_provisioning.py | 139 ++++++++++++++++++ .../foreman/api/test_provisioningtemplate.py | 47 ++++++ 2 files changed, 186 insertions(+) diff --git a/tests/foreman/api/test_provisioning.py b/tests/foreman/api/test_provisioning.py index f28e1d12730..b0760d3ff1a 100644 --- a/tests/foreman/api/test_provisioning.py +++ b/tests/foreman/api/test_provisioning.py @@ -25,6 +25,7 @@ from robottelo.config import settings from robottelo.logging import logger from robottelo.utils.installer import InstallerCommand +from robottelo.utils.issue_handlers import is_open def _read_log(ch, pattern): @@ -441,3 +442,141 @@ def test_rhel_httpboot_provisioning( # assert that the host is subscribed and consumes # subsctiption provided by the activation key assert provisioning_host.subscribed, 'Host is not subscribed' + + +@pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) +@pytest.mark.on_premises_provisioning +@pytest.mark.rhel_ver_match('[^6]') +def test_rhel_pxe_provisioning_fips_enabled( + request, + module_provisioning_sat, + module_sca_manifest_org, + module_location, + provisioning_host, + pxe_loader, + module_provisioning_rhel_content, + provisioning_hostgroup, + module_lce_library, + module_default_org_view, +): + """Provision a host with host param fips_enabled set to true + + :id: 9e016e1d-757a-48e7-9159-131bb65dc4ef + + :steps: + 1. Configure satellite for provisioning + 2. Provision a host with host param fips_enabled + 3. Check that resulting host is registered to Satellite + 4. Check host is subscribed to Satellite + + :expectedresults: + 1. Provisioning with host param fips_enabled is successful + 2. Host installs right version of RHEL + 3. Satellite is able to run REX job on the fips_enabled host + 4. Host is registered to Satellite and subscription status is 'Success' + + :parametrized: yes + + :BZ: 2240076 + """ + sat = module_provisioning_sat.sat + host_mac_addr = provisioning_host._broker_args['provisioning_nic_mac_addr'] + # Verify password hashing algorithm SHA256 is set in OS used for provisioning + assert module_provisioning_rhel_content.os.password_hash == 'SHA256' + + host = sat.api.Host( + hostgroup=provisioning_hostgroup, + organization=module_sca_manifest_org, + location=module_location, + name=gen_string('alpha').lower(), + mac=host_mac_addr, + operatingsystem=module_provisioning_rhel_content.os, + subnet=module_provisioning_sat.subnet, + host_parameters_attributes=[ + { + 'name': 'remote_execution_connect_by_ip', + 'value': 'true', + 'parameter_type': 'boolean', + }, + {'name': 'fips_enabled', '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) + + # TODO: Implement Satellite log capturing logic to verify that + # all the events are captured in the logs. + + # 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}' + + # Verify FIPS is enabled on host after provisioning is completed sucessfully + if int(host_os.major) >= 8: + result = provisioning_host.execute('fips-mode-setup --check') + fips_status = 'FIPS mode is disabled' if is_open('BZ:2240076') else 'FIPS mode is enabled' + assert fips_status in result.stdout + else: + result = provisioning_host.execute('cat /proc/sys/crypto/fips_enabled') + assert (0 if is_open('BZ:2240076') else 1) == int(result.stdout) + + # 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 = {sat.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' diff --git a/tests/foreman/api/test_provisioningtemplate.py b/tests/foreman/api/test_provisioningtemplate.py index ac0567181c5..19c4ad981ae 100644 --- a/tests/foreman/api/test_provisioningtemplate.py +++ b/tests/foreman/api/test_provisioningtemplate.py @@ -561,3 +561,50 @@ def test_positive_template_check_aap_snippet( assert 'systemctl enable ansible-callback' in render assert f'"host_config_key":"{config_key}"' in render assert '{"package_install": "zsh"}' in render + + @pytest.mark.parametrize('module_sync_kickstart_content', [7, 8, 9], indirect=True) + def test_positive_template_check_fips_enabled( + self, + module_sync_kickstart_content, + module_target_sat, + module_sca_manifest_org, + module_location, + module_default_org_view, + module_lce_library, + default_architecture, + default_partitiontable, + ): + """Read provision/PXE templates, verify fips packages to install and kernel cmdline option + fips=1, set by kickstart_kernel_options snippet while using host param fips_enabled + is rendered correctly + + :id: 065ef48f-bec5-4535-8be7-d8527fa21565 + + :expectedresults: Rendered template should contain correct FIPS packages and boot parameter + set by snippet while using host param fips_enabled for rhel host + + :parametrized: yes + """ + host_params = [{'name': 'fips_enabled', 'value': 'true', 'parameter_type': 'boolean'}] + host = module_target_sat.api.Host( + organization=module_sca_manifest_org, + location=module_location, + name=gen_string('alpha').lower(), + operatingsystem=module_sync_kickstart_content.os, + architecture=default_architecture, + domain=module_sync_kickstart_content.domain, + root_pass=settings.provisioning.host_root_password, + ptable=default_partitiontable, + content_facet_attributes={ + 'content_source_id': module_target_sat.nailgun_smart_proxy.id, + 'content_view_id': module_default_org_view.id, + 'lifecycle_environment_id': module_lce_library.id, + }, + host_parameters_attributes=host_params, + ).create() + render = host.read_template(data={'template_kind': 'provision'})['template'] + assert 'dracut-fips' in render + assert '-prelink' in render + for kind in ['PXELinux', 'PXEGrub', 'PXEGrub2', 'iPXE', 'kexec']: + render = host.read_template(data={'template_kind': kind})['template'] + assert 'fips=1' in render From 1bcfcdfb625d5726d73421e00e54a5bf73c39a4f Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 15 Dec 2023 07:03:02 -0500 Subject: [PATCH 394/586] [6.14.z] Get rid of deprecated mirror-on-sync (#13472) Get rid of deprecated mirror-on-sync (#13425) (cherry picked from commit 0452011e7ba6e6cf5499b3520883405425eb1786) Co-authored-by: vsedmik <46570670+vsedmik@users.noreply.github.com> --- tests/foreman/cli/test_satellitesync.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index 7fed3bd1586..0251b4901e8 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -499,7 +499,7 @@ def class_export_entities(module_org, module_target_sat): exporting_repo = module_target_sat.cli_factory.make_repository( { 'name': exporting_repo_name, - 'mirror-on-sync': 'no', + 'mirroring-policy': 'mirror_content_only', 'download-policy': 'immediate', 'product-id': product['id'], } @@ -1273,7 +1273,7 @@ def test_postive_export_cv_with_mixed_content_repos( { 'name': gen_string('alpha'), 'download-policy': 'immediate', - 'mirror-on-sync': 'no', + 'mirroring-policy': 'mirror_content_only', 'product-id': product['id'], } ) From 4cece4a55c6977c34f087ae5f0ca06c7142791a4 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 19 Dec 2023 04:04:06 -0500 Subject: [PATCH 395/586] [6.14.z] Fix discovery rule tests (#13483) Fix discovery rule tests (#13426) Signed-off-by: Shubham Ganar (cherry picked from commit f415a64309100985bdea8fe0818bf30a3bdb0c22) Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> --- tests/foreman/api/test_discoveryrule.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/foreman/api/test_discoveryrule.py b/tests/foreman/api/test_discoveryrule.py index 528647a3341..14ead0aabed 100644 --- a/tests/foreman/api/test_discoveryrule.py +++ b/tests/foreman/api/test_discoveryrule.py @@ -164,7 +164,7 @@ def test_positive_update_and_provision_with_rule_priority( @pytest.mark.tier3 def test_positive_multi_provision_with_rule_limit( - module_target_sat, module_discovery_hostgroup, discovery_location, discovery_org + request, module_target_sat, module_discovery_hostgroup, discovery_location, discovery_org ): """Create a discovery rule with certain host limit and try to provision more than the passed limit @@ -176,13 +176,12 @@ def test_positive_multi_provision_with_rule_limit( :CaseImportance: High """ - for _ in range(2): - discovered_host = module_target_sat.api_factory.create_discovered_host() - + discovered_host1 = module_target_sat.api_factory.create_discovered_host() + discovered_host2 = module_target_sat.api_factory.create_discovered_host() rule = module_target_sat.api.DiscoveryRule( max_count=1, hostgroup=module_discovery_hostgroup, - search_=f'name = {discovered_host["name"]}', + search_=f'name = {discovered_host1["name"]}', location=[discovery_location], organization=[discovery_org], priority=1000, @@ -191,6 +190,10 @@ def test_positive_multi_provision_with_rule_limit( assert '1 discovered hosts were provisioned' in result['message'] # Delete discovery rule - rule.delete() - with pytest.raises(HTTPError): - rule.read() + @request.addfinalizer + def _finalize(): + rule.delete() + module_target_sat.api.Host(id=discovered_host1['id']).delete() + module_target_sat.api.DiscoveredHost(id=discovered_host2['id']).delete() + with pytest.raises(HTTPError): + rule.read() From 3d2f73732837e6dc1a05b417d9770544b5c9db54 Mon Sep 17 00:00:00 2001 From: vijaysawant Date: Thu, 14 Dec 2023 19:45:47 +0530 Subject: [PATCH 396/586] fix server config from user and role test module (cherry picked from commit 0665208cc9de112ab04e72e7d330513019768997) --- tests/foreman/api/test_role.py | 50 ++++++++++++++++++---------------- tests/foreman/api/test_user.py | 20 ++++++++------ 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/tests/foreman/api/test_role.py b/tests/foreman/api/test_role.py index df0bc677718..da880c71f59 100644 --- a/tests/foreman/api/test_role.py +++ b/tests/foreman/api/test_role.py @@ -587,7 +587,7 @@ def test_negative_access_entities_from_org_admin( sc = self.user_config(user, target_sat) # Getting the domain from user with pytest.raises(HTTPError): - target_sat.api.Domain(sc, id=domain.id).read() + target_sat.api.Domain(server_config=sc, id=domain.id).read() @pytest.mark.tier3 def test_negative_access_entities_from_user( @@ -620,7 +620,7 @@ def test_negative_access_entities_from_user( sc = self.user_config(user, target_sat) # Getting the domain from user with pytest.raises(HTTPError): - target_sat.api.Domain(sc, id=domain.id).read() + target_sat.api.Domain(server_config=sc, id=domain.id).read() @pytest.mark.tier2 def test_positive_override_cloned_role_filter(self, role_taxonomies, target_sat): @@ -1018,13 +1018,13 @@ def test_positive_user_group_users_access_as_org_admin(self, role_taxonomies, ta auth=(login, password), url=target_sat.url, verify=settings.server.verify_ca ) try: - target_sat.api.Domain(sc).search( + target_sat.api.Domain(server_config=sc).search( query={ 'organization-id': role_taxonomies['org'].id, 'location-id': role_taxonomies['loc'].id, } ) - target_sat.api.Subnet(sc).search( + target_sat.api.Subnet(server_config=sc).search( query={ 'organization-id': role_taxonomies['org'].id, 'location-id': role_taxonomies['loc'].id, @@ -1032,8 +1032,8 @@ def test_positive_user_group_users_access_as_org_admin(self, role_taxonomies, ta ) except HTTPError as err: pytest.fail(str(err)) - assert domain.id in [dom.id for dom in target_sat.api.Domain(sc).search()] - assert subnet.id in [sub.id for sub in target_sat.api.Subnet(sc).search()] + assert domain.id in [dom.id for dom in target_sat.api.Domain(server_config=sc).search()] + assert subnet.id in [sub.id for sub in target_sat.api.Subnet(server_config=sc).search()] @pytest.mark.tier3 def test_positive_user_group_users_access_contradict_as_org_admins(self): @@ -1098,7 +1098,7 @@ def test_negative_assign_org_admin_to_user_group( for user in [user_one, user_two]: sc = self.user_config(user, target_sat) with pytest.raises(HTTPError): - target_sat.api.Domain(sc, id=dom.id).read() + target_sat.api.Domain(server_config=sc, id=dom.id).read() @pytest.mark.tier2 def test_negative_assign_taxonomies_by_org_admin( @@ -1149,7 +1149,7 @@ def test_negative_assign_taxonomies_by_org_admin( auth=(user_login, user_pass), url=target_sat.url, verify=settings.server.verify_ca ) # Getting the domain from user1 - dom = target_sat.api.Domain(sc, id=dom.id).read() + dom = target_sat.api.Domain(server_config=sc, id=dom.id).read() dom.organization = [filter_taxonomies['org']] with pytest.raises(HTTPError): dom.update(['organization']) @@ -1319,7 +1319,7 @@ def test_negative_create_roles_by_org_admin(self, role_taxonomies, target_sat): role_name = gen_string('alpha') with pytest.raises(HTTPError): target_sat.api.Role( - sc, + server_config=sc, name=role_name, organization=[role_taxonomies['org']], location=[role_taxonomies['loc']], @@ -1346,7 +1346,7 @@ def test_negative_modify_roles_by_org_admin(self, role_taxonomies, target_sat): ) test_role = target_sat.api.Role().create() sc = self.user_config(user, target_sat) - test_role = target_sat.api.Role(sc, id=test_role.id).read() + test_role = target_sat.api.Role(server_config=sc, id=test_role.id).read() test_role.organization = [role_taxonomies['org']] test_role.location = [role_taxonomies['loc']] with pytest.raises(HTTPError): @@ -1386,7 +1386,7 @@ def test_negative_admin_permissions_to_org_admin(self, role_taxonomies, target_s auth=(user_login, user_pass), url=target_sat.url, verify=settings.server.verify_ca ) with pytest.raises(HTTPError): - target_sat.api.User(sc, id=1).read() + target_sat.api.User(server_config=sc, id=1).read() @pytest.mark.tier2 @pytest.mark.upgrade @@ -1435,7 +1435,7 @@ def test_positive_create_user_by_org_admin(self, role_taxonomies, target_sat): user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') user = target_sat.api.User( - sc_user, + server_config=sc_user, login=user_login, password=user_pass, role=[org_admin.id], @@ -1476,7 +1476,7 @@ def test_positive_access_users_inside_org_admin_taxonomies(self, role_taxonomies test_user = self.create_simple_user(filter_taxos=role_taxonomies) sc = self.user_config(user, target_sat) try: - target_sat.api.User(sc, id=test_user.id).read() + target_sat.api.User(server_config=sc, id=test_user.id).read() except HTTPError as err: pytest.fail(str(err)) @@ -1518,7 +1518,9 @@ def test_positive_create_nested_location(self, role_taxonomies, target_sat): auth=(user_login, user_pass), url=target_sat.url, verify=settings.server.verify_ca ) name = gen_string('alphanumeric') - location = target_sat.api.Location(sc, name=name, parent=role_taxonomies['loc'].id).create() + location = target_sat.api.Location( + server_config=sc, name=name, parent=role_taxonomies['loc'].id + ).create() assert location.name == name @pytest.mark.tier2 @@ -1550,7 +1552,7 @@ def test_negative_access_users_outside_org_admin_taxonomies( test_user = self.create_simple_user(filter_taxos=filter_taxonomies) sc = self.user_config(user, target_sat) with pytest.raises(HTTPError): - target_sat.api.User(sc, id=test_user.id).read() + target_sat.api.User(server_config=sc, id=test_user.id).read() @pytest.mark.tier1 def test_negative_create_taxonomies_by_org_admin(self, role_taxonomies, target_sat): @@ -1586,11 +1588,11 @@ def test_negative_create_taxonomies_by_org_admin(self, role_taxonomies, target_s auth=(user_login, user_pass), url=target_sat.url, verify=settings.server.verify_ca ) with pytest.raises(HTTPError): - target_sat.api.Organization(sc, name=gen_string('alpha')).create() + target_sat.api.Organization(server_config=sc, name=gen_string('alpha')).create() if not is_open("BZ:1825698"): try: loc_name = gen_string('alpha') - loc = target_sat.api.Location(sc, name=loc_name).create() + loc = target_sat.api.Location(server_config=sc, name=loc_name).create() except HTTPError as err: pytest.fail(str(err)) assert loc_name == loc.name @@ -1643,7 +1645,7 @@ def test_positive_access_all_global_entities_by_org_admin( target_sat.api.Errata, target_sat.api.OperatingSystem, ]: - entity(sc).search() + entity(server_config=sc).search() except HTTPError as err: pytest.fail(str(err)) @@ -1684,7 +1686,7 @@ def test_negative_access_entities_from_ldap_org_admin( verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): - target_sat.api.Architecture(sc).search() + target_sat.api.Architecture(server_config=sc).search() user = target_sat.api.User().search( query={'search': f"login={create_ldap['ldap_user_name']}"} )[0] @@ -1692,7 +1694,7 @@ def test_negative_access_entities_from_ldap_org_admin( user.update(['role']) # Trying to access the domain resource created in org admin role with pytest.raises(HTTPError): - target_sat.api.Domain(sc, id=domain.id).read() + target_sat.api.Domain(server_config=sc, id=domain.id).read() @pytest.mark.tier3 def test_negative_access_entities_from_ldap_user( @@ -1729,7 +1731,7 @@ def test_negative_access_entities_from_ldap_user( verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): - target_sat.api.Architecture(sc).search() + target_sat.api.Architecture(server_config=sc).search() user = target_sat.api.User().search( query={'search': f"login={create_ldap['ldap_user_name']}"} )[0] @@ -1737,7 +1739,7 @@ def test_negative_access_entities_from_ldap_user( user.update(['role']) # Trying to access the Domain resource with pytest.raises(HTTPError): - target_sat.api.Domain(sc, id=domain.id).read() + target_sat.api.Domain(server_config=sc, id=domain.id).read() @pytest.mark.tier3 def test_positive_assign_org_admin_to_ldap_user_group( @@ -1800,7 +1802,7 @@ def test_positive_assign_org_admin_to_ldap_user_group( verify=settings.server.verify_ca, ) # Accessing the Domain resource - target_sat.api.Domain(sc, id=domain.id).read() + target_sat.api.Domain(server_config=sc, id=domain.id).read() @pytest.mark.tier3 def test_negative_assign_org_admin_to_ldap_user_group( @@ -1861,7 +1863,7 @@ def test_negative_assign_org_admin_to_ldap_user_group( ) # Trying to access the Domain resource with pytest.raises(HTTPError): - target_sat.api.Domain(sc, id=domain.id).read() + target_sat.api.Domain(server_config=sc, id=domain.id).read() class TestRoleSearchFilter: diff --git a/tests/foreman/api/test_user.py b/tests/foreman/api/test_user.py index 29442aa3080..de78386805e 100644 --- a/tests/foreman/api/test_user.py +++ b/tests/foreman/api/test_user.py @@ -420,8 +420,12 @@ def test_positive_table_preferences(self, module_target_sat): sc = ServerConfig( auth=(user.login, password), url=module_target_sat.url, verify=settings.server.verify_ca ) - module_target_sat.api.TablePreferences(sc, user=user, name=name, columns=columns).create() - table_preferences = module_target_sat.api.TablePreferences(sc, user=user).search() + module_target_sat.api.TablePreferences( + server_config=sc, user=user, name=name, columns=columns + ).create() + table_preferences = module_target_sat.api.TablePreferences( + server_config=sc, user=user + ).search() assert len(table_preferences) == 1 tp = table_preferences[0] assert hasattr(tp, 'name') @@ -732,7 +736,7 @@ def test_positive_ad_basic_no_roles(self, create_ldap, target_sat): verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): - target_sat.api.Architecture(sc).search() + target_sat.api.Architecture(server_config=sc).search() @pytest.mark.tier3 @pytest.mark.upgrade @@ -783,7 +787,7 @@ def test_positive_access_entities_from_ldap_org_admin(self, create_ldap, module_ verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): - module_target_sat.api.Architecture(sc).search() + module_target_sat.api.Architecture(server_config=sc).search() user = module_target_sat.api.User().search( query={'search': 'login={}'.format(create_ldap['ldap_user_name'])} )[0] @@ -800,7 +804,7 @@ def test_positive_access_entities_from_ldap_org_admin(self, create_ldap, module_ module_target_sat.api.Errata, module_target_sat.api.OperatingSystem, ]: - entity(sc).search() + entity(server_config=sc).search() @pytest.mark.run_in_one_thread @@ -865,7 +869,7 @@ def test_positive_ipa_basic_no_roles(self, create_ldap, target_sat): verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): - target_sat.api.Architecture(sc).search() + target_sat.api.Architecture(server_config=sc).search() @pytest.mark.tier3 @pytest.mark.upgrade @@ -906,7 +910,7 @@ def test_positive_access_entities_from_ipa_org_admin(self, create_ldap, target_s verify=settings.server.verify_ca, ) with pytest.raises(HTTPError): - target_sat.api.Architecture(sc).search() + target_sat.api.Architecture(server_config=sc).search() user = target_sat.api.User().search( query={'search': 'login={}'.format(create_ldap['username'])} )[0] @@ -923,7 +927,7 @@ def test_positive_access_entities_from_ipa_org_admin(self, create_ldap, target_s target_sat.api.Errata, target_sat.api.OperatingSystem, ]: - entity(sc).search() + entity(server_config=sc).search() class TestPersonalAccessToken: From 335ff08e0e442982bdff360ec87f65bc70c4c101 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 19 Dec 2023 14:41:19 -0500 Subject: [PATCH 397/586] [6.14.z] Use rsync to copy /var/lib/pulp after satellite-clone (#13498) --- pytest_fixtures/component/repository.py | 13 +++++++++- tests/foreman/destructive/test_clone.py | 34 ++++++++++++++++--------- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/pytest_fixtures/component/repository.py b/pytest_fixtures/component/repository.py index fc98c32f302..a3a48083d1c 100644 --- a/pytest_fixtures/component/repository.py +++ b/pytest_fixtures/component/repository.py @@ -4,7 +4,8 @@ from nailgun.entity_mixins import call_entity_method_with_timeout import pytest -from robottelo.constants import DEFAULT_ARCHITECTURE, PRDS, REPOS, REPOSET +from robottelo.config import settings +from robottelo.constants import DEFAULT_ARCHITECTURE, DEFAULT_ORG, PRDS, REPOS, REPOSET @pytest.fixture(scope='module') @@ -101,6 +102,16 @@ def module_repository(os_path, module_product, module_target_sat): return repo +@pytest.fixture +def custom_synced_repo(target_sat): + custom_repo = target_sat.api.Repository( + product=target_sat.api.Product(organization=DEFAULT_ORG).create(), + url=settings.repos.yum_0.url, + ).create() + custom_repo.sync() + return custom_repo + + def _simplify_repos(request, repos): """This is a helper function that transforms repos_collection related fixture parameters into a list that can be passed to robottelo.host_helpers.RepositoryMixins.RepositoryCollection diff --git a/tests/foreman/destructive/test_clone.py b/tests/foreman/destructive/test_clone.py index 5ef2f42abdf..77a7147859a 100644 --- a/tests/foreman/destructive/test_clone.py +++ b/tests/foreman/destructive/test_clone.py @@ -29,7 +29,9 @@ @pytest.mark.e2e @pytest.mark.parametrize('backup_type', ['online', 'offline']) @pytest.mark.parametrize('skip_pulp', [False, True], ids=['include_pulp', 'skip_pulp']) -def test_positive_clone_backup(target_sat, sat_ready_rhel, backup_type, skip_pulp): +def test_positive_clone_backup( + target_sat, sat_ready_rhel, backup_type, skip_pulp, custom_synced_repo +): """Make an online/offline backup with/without pulp data of Satellite and clone it (restore it). :id: 5b9182d5-6789-4d2c-bcc3-6641b96ab277 @@ -45,13 +47,15 @@ def test_positive_clone_backup(target_sat, sat_ready_rhel, backup_type, skip_pul :parametrized: yes - :BZ: 2142514 + :BZ: 2142514, 2013776 :customerscenario: true """ rhel_version = sat_ready_rhel._v_major sat_version = target_sat.version + pulp_artifact_len = len(target_sat.execute('ls /var/lib/pulp/media/artifact').stdout) + # SATELLITE PART - SOURCE SERVER # Enabling and starting services assert target_sat.cli.Service.enable().status == 0 @@ -66,16 +70,6 @@ def test_positive_clone_backup(target_sat, sat_ready_rhel, backup_type, skip_pul assert backup_result.status == 0 sat_backup_dir = backup_result.stdout.strip().split()[-2] - if skip_pulp: - # Copying satellite pulp data to target RHEL - assert sat_ready_rhel.execute('mkdir -p /var/lib/pulp').status == 0 - assert ( - target_sat.execute( - f'''sshpass -p "{SSH_PASS}" scp -o StrictHostKeyChecking=no \ - -r /var/lib/pulp root@{sat_ready_rhel.hostname}:/var/lib/pulp/pulp''' - ).status - == 0 - ) # Copying satellite backup to target RHEL assert ( target_sat.execute( @@ -118,6 +112,22 @@ def test_positive_clone_backup(target_sat, sat_ready_rhel, backup_type, skip_pul cloned_sat = Satellite(sat_ready_rhel.hostname) assert cloned_sat.cli.Health.check().status == 0 + # If --skip-pulp-data make sure you can rsync /var/lib/pulp over per BZ#2013776 + if skip_pulp: + # Copying satellite pulp data to target RHEL + assert ( + target_sat.execute( + f'sshpass -p "{SSH_PASS}" rsync -e "ssh -o StrictHostKeyChecking=no" --archive --partial --progress --compress ' + f'/var/lib/pulp root@{sat_ready_rhel.hostname}:/var/lib/' + ).status + == 0 + ) + + # Make sure all of the pulp data that was on the original Satellite is on the clone + assert ( + len(sat_ready_rhel.execute('ls /var/lib/pulp/media/artifact').stdout) == pulp_artifact_len + ) + @pytest.mark.pit_server def test_positive_list_tasks(target_sat): From 2ae89beae6c520451bb50657361c901359a4190b Mon Sep 17 00:00:00 2001 From: Adarsh dubey Date: Wed, 20 Dec 2023 13:51:10 +0530 Subject: [PATCH 398/586] Cherrypick : Check for Default values on Global Registration Template (#13503) --- tests/foreman/ui/test_registration.py | 58 +++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/foreman/ui/test_registration.py diff --git a/tests/foreman/ui/test_registration.py b/tests/foreman/ui/test_registration.py new file mode 100644 index 00000000000..5731e058fcf --- /dev/null +++ b/tests/foreman/ui/test_registration.py @@ -0,0 +1,58 @@ +"""Tests for registration. + +:Requirement: Registration + +:CaseLevel: Acceptance + +:CaseComponent: Registration + +:CaseAutomation: Automated + +:CaseImportance: Critical + +:Team: Rocket + +:TestType: Functional + +:Upstream: No +""" +from robottelo.utils.datafactory import gen_string + + +def test_positive_verify_default_values_for_global_registration( + module_target_sat, + default_org, +): + """Check for all the Default values pre-populated in the global registration template + + :id: 34122bf3-ae23-47ca-ba3d-da0653d8fd33 + + :expectedresults: Default fields in the form should be auto-populated + e.g. organization, location, rex, insights setup, etc + + :CaseLevel: Component + + :steps: + 1. Check for the default values in the global registration template + """ + ak = module_target_sat.cli_factory.make_activation_key( + {'organization-id': default_org.id, 'name': gen_string('alpha')} + ) + with module_target_sat.ui_session() as session: + cmd = session.host.get_register_command( + {'general.activation_keys': ak.name}, + full_read=True, + ) + assert cmd['general']['organization'] == 'Default Organization' + assert cmd['general']['location'] == 'Default Location' + assert cmd['general']['capsule'] == 'Nothing to select.' + assert cmd['general']['activation_keys'][0] == ak.name + assert cmd['general']['host_group'] == 'Nothing to select.' + assert cmd['general']['insecure'] is False + assert cmd['advanced']['setup_rex'] == 'Inherit from host parameter (yes)' + assert cmd['advanced']['setup_insights'] == 'Inherit from host parameter (yes)' + assert cmd['advanced']['token_life_time'] == '4' + assert cmd['advanced']['rex_pull_mode'] == 'Inherit from host parameter (no)' + assert cmd['advanced']['update_packages'] is False + assert cmd['advanced']['ignore_error'] is False + assert cmd['advanced']['force'] is False From 74293d98c4310acf6ae1652d4fd0cc94767ea26d Mon Sep 17 00:00:00 2001 From: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> Date: Wed, 20 Dec 2023 16:50:00 +0530 Subject: [PATCH 399/586] [6.14.z] Component Audit: Check org and loc change on Global Registration form (#13508) Component Audit: Check org and loc change on Global Registration form Signed-off-by: Shubham Ganar --- tests/foreman/ui/test_registration.py | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/foreman/ui/test_registration.py b/tests/foreman/ui/test_registration.py index 5731e058fcf..569b8c03c33 100644 --- a/tests/foreman/ui/test_registration.py +++ b/tests/foreman/ui/test_registration.py @@ -16,6 +16,8 @@ :Upstream: No """ +import pytest + from robottelo.utils.datafactory import gen_string @@ -56,3 +58,53 @@ def test_positive_verify_default_values_for_global_registration( assert cmd['advanced']['update_packages'] is False assert cmd['advanced']['ignore_error'] is False assert cmd['advanced']['force'] is False + + +@pytest.mark.tier2 +def test_positive_org_loc_change_for_registration( + module_activation_key, + module_org, + module_location, + target_sat, +): + """Changing the organization and location to check if correct org and loc is updated on the global registration page as well as in the command + + :id: e83ed6bc-ceae-4021-87fe-3ecde1cbf347 + + :expectedresults: organization and location is updated correctly on the global registration page as well as in the command. + + :CaseLevel: Component + + :CaseImportance: Medium + """ + new_org = target_sat.api.Organization().create() + new_loc = target_sat.api.Location().create() + new_ak = target_sat.api.ActivationKey(organization=new_org).create() + with target_sat.ui_session() as session: + session.organization.select(org_name=module_org.name) + session.location.select(loc_name=module_location.name) + cmd = session.host.get_register_command( + { + 'general.activation_keys': module_activation_key.name, + } + ) + expected_pairs = [ + f'organization_id={module_org.id}', + f'location_id={module_location.id}', + ] + for pair in expected_pairs: + assert pair in cmd + # changing the org and loc to check if correct org and loc is updated on the registration command + session.organization.select(org_name=new_org.name) + session.location.select(loc_name=new_loc.name) + cmd = session.host.get_register_command( + { + 'general.activation_keys': new_ak.name, + } + ) + expected_pairs = [ + f'organization_id={new_org.id}', + f'location_id={new_loc.id}', + ] + for pair in expected_pairs: + assert pair in cmd From ce823b01a84b715513ac9df31f680d5844fa89de Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 20 Dec 2023 07:37:58 -0500 Subject: [PATCH 400/586] [6.14.z] Add Stubbed test for realm provisioning (#13527) Add Stubbed test for realm provisioning (#13524) Signed-off-by: Shubham Ganar (cherry picked from commit bc2f2cb47f1d26caf45543e182507862a466e1cf) Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> --- tests/foreman/api/test_provisioning.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/foreman/api/test_provisioning.py b/tests/foreman/api/test_provisioning.py index b0760d3ff1a..825f9fa188e 100644 --- a/tests/foreman/api/test_provisioning.py +++ b/tests/foreman/api/test_provisioning.py @@ -580,3 +580,24 @@ def test_rhel_pxe_provisioning_fips_enabled( # 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 + + :id: 687e7d71-7e46-46d5-939b-4562f88c4598 + + :steps: + 1. Configure satellite for provisioning + 2. Configure Satellite for Realm support + 3. Provision a Host + 4. Check host is subscribed to Satellite + + :expectedresults: + 1. Provisioning via Realm is successful + 2. Check if the provisioned host is automatically registered to IdM + 3. Host installs right version of RHEL + 4. Satellite is able to run REX job on the host + 5. Host is registered to Satellite and subscription status is 'Success' + """ From 507510f32d1dc33b86a6f8258fc281e30954fcbb Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 20 Dec 2023 08:06:19 -0500 Subject: [PATCH 401/586] [6.14.z] Bring back changes reverted by #11544 (#13522) Bring back changes reverted by #11544 (cherry picked from commit 1f59e24c2f146a012428f577ba24ed4811d74b7b) Co-authored-by: Vladimir Sedmik --- tests/foreman/cli/test_satellitesync.py | 69 +------------------------ 1 file changed, 1 insertion(+), 68 deletions(-) diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index 0251b4901e8..aa660748303 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -101,23 +101,6 @@ def function_import_org_with_manifest(target_sat, function_import_org): return function_import_org -@pytest.fixture(scope='class') -def docker_repo(module_target_sat, module_org): - product = module_target_sat.cli_factory.make_product({'organization-id': module_org.id}) - repo = module_target_sat.cli_factory.make_repository( - { - 'organization-id': module_org.id, - 'product-id': product['id'], - 'content-type': REPO_TYPE['docker'], - 'download-policy': 'immediate', - 'url': 'https://quay.io', - 'docker-upstream-name': 'quay/busybox', - } - ) - module_target_sat.cli.Repository.synchronize({'id': repo['id']}) - return repo - - @pytest.fixture(scope='module') def module_synced_custom_repo(module_target_sat, module_org, module_product): repo = module_target_sat.cli_factory.make_repository( @@ -1224,6 +1207,7 @@ def test_postive_export_cv_with_mixed_content_repos( :BZ: 1726457 :customerscenario: true + """ content_view = target_sat.cli_factory.make_content_view( {'organization-id': function_org.id} @@ -1253,57 +1237,6 @@ def test_postive_export_cv_with_mixed_content_repos( exported_packages = target_sat.cli.Package.list( {'content-view-version-id': exporting_cvv['id']} ) - product = target_sat.cli_factory.make_product( - { - 'organization-id': function_org.id, - 'name': gen_string('alpha'), - } - ) - nonyum_repo = target_sat.cli_factory.make_repository( - { - 'content-type': 'docker', - 'docker-upstream-name': 'quay/busybox', - 'organization-id': function_org.id, - 'product-id': product['id'], - 'url': CONTAINER_REGISTRY_HUB, - }, - ) - target_sat.cli.Repository.synchronize({'id': nonyum_repo['id']}) - yum_repo = target_sat.cli_factory.make_repository( - { - 'name': gen_string('alpha'), - 'download-policy': 'immediate', - 'mirroring-policy': 'mirror_content_only', - 'product-id': product['id'], - } - ) - target_sat.cli.Repository.synchronize({'id': yum_repo['id']}) - content_view = target_sat.cli_factory.make_content_view( - {'organization-id': function_org.id} - ) - # Add docker and yum repo - target_sat.cli.ContentView.add_repository( - { - 'id': content_view['id'], - 'organization-id': function_org.id, - 'repository-id': nonyum_repo['id'], - } - ) - target_sat.cli.ContentView.add_repository( - { - 'id': content_view['id'], - 'organization-id': function_org.id, - 'repository-id': yum_repo['id'], - } - ) - target_sat.cli.ContentView.publish({'id': content_view['id']}) - exporting_cv_id = target_sat.cli.ContentView.info({'id': content_view['id']}) - assert len(exporting_cv_id['versions']) == 1 - exporting_cvv_id = exporting_cv_id['versions'][0] - # check packages - exported_packages = target_sat.cli.Package.list( - {'content-view-version-id': exporting_cvv_id['id']} - ) assert len(exported_packages) # Verify export directory is empty assert target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) == '' From 1c8b5881df5ed59530448bb4a79fc54cecd597bb Mon Sep 17 00:00:00 2001 From: Gaurav Talreja Date: Wed, 20 Dec 2023 20:51:54 +0530 Subject: [PATCH 402/586] Add negative tests for registration without AK (#13490) (#13536) Signed-off-by: Gaurav Talreja (cherry picked from commit 48735b00ba4b7acbd983bd06e9f2342fbfa08163) --- tests/foreman/api/test_registration.py | 14 ++++++++++++++ tests/foreman/cli/test_registration.py | 17 +++++++++++++++++ tests/foreman/ui/test_registration.py | 22 ++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/tests/foreman/api/test_registration.py b/tests/foreman/api/test_registration.py index dab85fa6209..77fd856db11 100644 --- a/tests/foreman/api/test_registration.py +++ b/tests/foreman/api/test_registration.py @@ -20,6 +20,7 @@ from fauxfactory import gen_ipaddr, gen_mac import pytest +from requests import HTTPError from robottelo import constants from robottelo.config import settings @@ -213,3 +214,16 @@ def test_positive_rex_interface_for_global_registration( assert interface['execution'] is True assert interface['ip'] == ip assert interface['mac'] == mac_address + + +@pytest.mark.tier1 +def test_negative_global_registration_without_ak(module_target_sat): + """Attempt to register a host without ActivationKey + + :id: e48a6260-97e0-4234-a69c-77bbbcde85de + + :expectedresults: Generate command is disabled without ActivationKey + """ + with pytest.raises(HTTPError) as context: + module_target_sat.api.RegistrationCommand().create() + assert 'Missing activation key!' in context.value.response.text diff --git a/tests/foreman/cli/test_registration.py b/tests/foreman/cli/test_registration.py index 25b8c0a2135..633fbf3be9f 100644 --- a/tests/foreman/cli/test_registration.py +++ b/tests/foreman/cli/test_registration.py @@ -20,6 +20,7 @@ from robottelo.config import settings from robottelo.constants import CLIENT_PORT +from robottelo.exceptions import CLIReturnCodeError pytestmark = pytest.mark.tier1 @@ -171,3 +172,19 @@ def test_negative_register_twice(module_ak_with_cv, module_org, rhel_contenthost # host being already registered. assert result.status == 1 assert 'This system is already registered' in str(result.stderr) + + +@pytest.mark.tier1 +def test_negative_global_registration_without_ak(module_target_sat): + """Attempt to register a host without ActivationKey + + :id: e48a6260-97e0-4234-a69c-77bbbcde85df + + :expectedresults: Generate command is disabled without ActivationKey + """ + with pytest.raises(CLIReturnCodeError) as context: + module_target_sat.cli.HostRegistration.generate_command(options=None) + assert ( + 'Failed to generate registration command:\n Missing activation key!' + in context.value.message + ) diff --git a/tests/foreman/ui/test_registration.py b/tests/foreman/ui/test_registration.py index 569b8c03c33..9a8eedf359b 100644 --- a/tests/foreman/ui/test_registration.py +++ b/tests/foreman/ui/test_registration.py @@ -16,10 +16,13 @@ :Upstream: No """ +from airgun.exceptions import DisabledWidgetError import pytest from robottelo.utils.datafactory import gen_string +pytestmark = pytest.mark.tier1 + def test_positive_verify_default_values_for_global_registration( module_target_sat, @@ -108,3 +111,22 @@ def test_positive_org_loc_change_for_registration( ] for pair in expected_pairs: assert pair in cmd + + +def test_negative_global_registration_without_ak( + module_target_sat, + module_org, + module_location, +): + """Attempt to register a host without ActivationKey + + :id: 34122bf3-ae23-47ca-ba3d-da0653d8fd36 + + :expectedresults: Generate command is disabled without ActivationKey + """ + with module_target_sat.ui_session() as session: + session.organization.select(org_name=module_org.name) + session.location.select(loc_name=module_location.name) + with pytest.raises(DisabledWidgetError) as context: + session.host.get_register_command() + assert 'Generate registration command button is disabled' in str(context.value) From 07671fa489c84bdbb43468481889c70a85368459 Mon Sep 17 00:00:00 2001 From: Gaurav Talreja Date: Wed, 20 Dec 2023 21:27:49 +0530 Subject: [PATCH 403/586] Add coverage for rex pull mode provisioning snippet (#13533) Add coverage for rex pull mode provisioning snippet (#13518) (cherry picked from commit 80e6ba456b9ae6f4b6171033975d5c94429b3acd) Signed-off-by: Gaurav Talreja --- .../foreman/api/test_provisioningtemplate.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/foreman/api/test_provisioningtemplate.py b/tests/foreman/api/test_provisioningtemplate.py index 19c4ad981ae..42bc807245c 100644 --- a/tests/foreman/api/test_provisioningtemplate.py +++ b/tests/foreman/api/test_provisioningtemplate.py @@ -608,3 +608,64 @@ def test_positive_template_check_fips_enabled( for kind in ['PXELinux', 'PXEGrub', 'PXEGrub2', 'iPXE', 'kexec']: render = host.read_template(data={'template_kind': kind})['template'] assert 'fips=1' in render + + @pytest.mark.parametrize('module_sync_kickstart_content', [7, 8, 9], indirect=True) + def test_positive_template_check_rex_pull_mode_snippet( + self, + module_sync_kickstart_content, + module_target_sat, + module_provisioning_capsule, + module_sca_manifest_org, + module_location, + module_default_org_view, + module_lce_library, + default_architecture, + default_partitiontable, + ): + """Read the provision template and verify the host params and REX pull mode snippet rendered correctly. + + :id: e5212c46-d269-4bce-8e03-9d00c086e69m + + :steps: + 1. Create a host by setting host param enable-remote-execution-pull/host_registration_remote_execution_pull + 2. Read the template to verify the host param and REX pull mode snippet for respective rhel hosts + + :expectedresults: The rendered template has the host params set and correct home directory permissions for the rex user + + :parametrized: yes + """ + host = module_target_sat.api.Host( + organization=module_sca_manifest_org, + location=module_location, + name=gen_string('alpha').lower(), + mac=gen_mac(multicast=False), + operatingsystem=module_sync_kickstart_content.os, + architecture=default_architecture, + domain=module_sync_kickstart_content.domain, + root_pass=settings.provisioning.host_root_password, + ptable=default_partitiontable, + host_parameters_attributes=[ + { + 'name': 'host_registration_remote_execution_pull', + 'value': True, + 'parameter_type': 'boolean', + }, + { + 'name': 'enable-remote-execution-pull', + 'value': True, + 'parameter_type': 'boolean', + }, + ], + ).create() + rex_snippet = host.read_template(data={'template_kind': 'provision'})['template'] + assert 'chmod +x /root/remote_execution_pull_setup.sh' in rex_snippet + + rex_snippet = host.read_template(data={'template_kind': 'host_init_config'})['template'] + assert 'Starting deployment of REX pull provider' in rex_snippet + pkg_manager = 'yum' if module_sync_kickstart_content.rhel_ver < 8 else 'dnf' + assert f'{pkg_manager} -y install foreman_ygg_worker' in rex_snippet + assert 'broker = ["mqtts://$SERVER_NAME:1883"]' in rex_snippet + assert 'systemctl try-restart yggdrasild' in rex_snippet + assert 'systemctl enable --now yggdrasild' in rex_snippet + assert 'yggdrasil status' in rex_snippet + assert 'Remote execution pull provider successfully configured!' in rex_snippet From 7ff7e4753eb78c61969a7e3656172368baf103e8 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 21 Dec 2023 03:53:30 -0500 Subject: [PATCH 404/586] [6.14.z] Configurable Ignore list of markers test field in polarion tests (#13511) Configurable Ignore list of markers test field in polarion tests (#13492) (cherry picked from commit f169757c65d1daa4e7e83ca81a4445dbfa2b406b) Co-authored-by: Jitendra Yejare --- requirements.txt | 2 +- scripts/polarion-test-case-upload.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2d0acf016ab..e2e2cb0a926 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # Version updates managed by dependabot -betelgeuse==1.10.0 +betelgeuse==1.11.0 broker[docker]==0.4.1 cryptography==41.0.7 deepdiff==6.7.1 diff --git a/scripts/polarion-test-case-upload.sh b/scripts/polarion-test-case-upload.sh index 987a77d6466..888bacb04f0 100755 --- a/scripts/polarion-test-case-upload.sh +++ b/scripts/polarion-test-case-upload.sh @@ -74,6 +74,7 @@ TRANSFORM_CUSTOMERSCENARIO_VALUE = default_config._transform_to_lower DEFAULT_CUSTOMERSCENARIO_VALUE = 'false' DEFAULT_REQUIREMENT_TEAM_VALUE = None TRANSFORM_REQUIREMENT_TEAM_VALUE = default_config._transform_to_lower +MARKERS_IGNORE_LIST = ['parametrize', 'skip.*', 'usefixtures', 'rhel_ver_.*'] EOF set -x From 003d84135b8614a1194047968fd7eb9892f3d262 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 21 Dec 2023 08:45:19 -0500 Subject: [PATCH 405/586] [6.14.z] Corrected Req for AuditLog (#13540) Corrected Req for AuditLog (#13516) (cherry picked from commit 8d0d3ff5633437599ba386ab432e7d6aad05ed39) Co-authored-by: Jitendra Yejare --- tests/foreman/api/test_audit.py | 2 +- tests/foreman/ui/test_audit.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/foreman/api/test_audit.py b/tests/foreman/api/test_audit.py index 1eff8e5e599..85bcec9bd70 100644 --- a/tests/foreman/api/test_audit.py +++ b/tests/foreman/api/test_audit.py @@ -1,6 +1,6 @@ """Tests for audit functionality. -:Requirement: Audit +:Requirement: AuditLog :CaseAutomation: Automated diff --git a/tests/foreman/ui/test_audit.py b/tests/foreman/ui/test_audit.py index ca506d21292..822f7bcb1bf 100644 --- a/tests/foreman/ui/test_audit.py +++ b/tests/foreman/ui/test_audit.py @@ -1,6 +1,6 @@ """Test class for Audit UI -:Requirement: Audit +:Requirement: AuditLog :CaseAutomation: Automated From f227da0a0d878756b2f3a856a6b3a6286c007f83 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 22 Dec 2023 10:04:57 -0500 Subject: [PATCH 406/586] [6.14.z] fix cli errata tests and update cli factory method (#13555) --- robottelo/host_helpers/cli_factory.py | 23 +++++++++++++---------- tests/foreman/cli/test_errata.py | 25 ++++++++++++------------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/robottelo/host_helpers/cli_factory.py b/robottelo/host_helpers/cli_factory.py index bcf7120eedf..45ea935ab88 100644 --- a/robottelo/host_helpers/cli_factory.py +++ b/robottelo/host_helpers/cli_factory.py @@ -689,7 +689,7 @@ def _setup_org_for_a_rh_repo(self, options=None): 1. Checks if organization and lifecycle environment were given, otherwise creates new ones. - 2. Clones and uploads manifest. + 2. If manifest does not exist, clone and upload it. 3. Enables RH repo and synchronizes it. 4. Checks if content view was given, otherwise creates a new one and - adds the RH repo @@ -715,15 +715,13 @@ def _setup_org_for_a_rh_repo(self, options=None): env_id = self.make_lifecycle_environment({'organization-id': org_id})['id'] else: env_id = options['lifecycle-environment-id'] - # Clone manifest and upload it - with clone() as manifest: - self._satellite.put(manifest.path, manifest.name) - try: - self._satellite.cli.Subscription.upload( - {'file': manifest.name, 'organization-id': org_id} - ) - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to upload manifest\n{err.msg}') + # If manifest does not exist, clone and upload it + if len(self._satellite.cli.Subscription.exists({'organization-id': org_id})) == 0: + with clone() as manifest: + try: + self._satellite.upload_manifest(org_id, manifest.content) + except CLIReturnCodeError as err: + raise CLIFactoryError(f'Failed to upload manifest\n{err.msg}') # Enable repo from Repository Set try: self._satellite.cli.RepositorySet.enable( @@ -818,6 +816,11 @@ def _setup_org_for_a_rh_repo(self, options=None): ), } ) + # Override RHST product to true ( turned off by default in 6.14 ) + rhel_repo = self._satellite.cli.Repository.info({'id': rhel_repo['id']}) + self._satellite.cli.ActivationKey.content_override( + {'id': activationkey_id, 'content-label': rhel_repo['content-label'], 'value': 'true'} + ) return { 'activationkey-id': activationkey_id, 'content-view-id': cv_id, diff --git a/tests/foreman/cli/test_errata.py b/tests/foreman/cli/test_errata.py index 0b4b3a37f5c..d707891481f 100644 --- a/tests/foreman/cli/test_errata.py +++ b/tests/foreman/cli/test_errata.py @@ -134,16 +134,14 @@ def products_with_repos(orgs, module_target_sat): @pytest.fixture(scope='module') -def rh_repo( - module_entitlement_manifest_org, module_lce, module_cv, module_ak_cv_lce, module_target_sat -): +def rh_repo(module_sca_manifest_org, module_lce, module_cv, module_ak_cv_lce, module_target_sat): """Add a subscription for the Satellite Tools repo to activation key.""" module_target_sat.cli_factory.setup_org_for_a_rh_repo( { 'product': PRDS['rhel'], 'repository-set': REPOSET['rhst7'], 'repository': REPOS['rhst7']['name'], - 'organization-id': module_entitlement_manifest_org.id, + 'organization-id': module_sca_manifest_org.id, 'content-view-id': module_cv.id, 'lifecycle-environment-id': module_lce.id, 'activationkey-id': module_ak_cv_lce.id, @@ -153,13 +151,13 @@ def rh_repo( @pytest.fixture(scope='module') def custom_repo( - module_entitlement_manifest_org, module_lce, module_cv, module_ak_cv_lce, module_target_sat + module_sca_manifest_org, module_lce, module_cv, module_ak_cv_lce, module_target_sat ): """Create custom repo and add a subscription to activation key.""" module_target_sat.cli_factory.setup_org_for_a_custom_repo( { 'url': REPO_WITH_ERRATA['url'], - 'organization-id': module_entitlement_manifest_org.id, + 'organization-id': module_sca_manifest_org.id, 'content-view-id': module_cv.id, 'lifecycle-environment-id': module_lce.id, 'activationkey-id': module_ak_cv_lce.id, @@ -377,7 +375,7 @@ def filter_sort_errata(sat, org, sort_by_date='issued', filter_by_org=None): # Build a sorted errata info list, which also contains the sort field. errata_internal_ids = [errata['id'] for errata in errata_list] sorted_errata_info = get_sorted_errata_info_by_id( - errata_internal_ids, sort_by=sort_by_date, sort_reversed=sort_reversed + sat, errata_internal_ids, sort_by=sort_by_date, sort_reversed=sort_reversed ) sort_field_values = [errata[sort_by_date] for errata in sorted_errata_info] @@ -1043,7 +1041,7 @@ def cleanup(): ids=('org_id', 'org_name', 'org_label', 'no_org_filter'), ) def test_positive_list_filter_by_org_sort_by_date( - module_entitlement_manifest_org, rh_repo, custom_repo, filter_by_org, sort_by_date + module_sca_manifest_org, rh_repo, custom_repo, filter_by_org, sort_by_date, module_target_sat ): """Filter by organization and sort by date. @@ -1060,7 +1058,8 @@ def test_positive_list_filter_by_org_sort_by_date( :expectedresults: Errata are filtered by org and sorted by date. """ filter_sort_errata( - module_entitlement_manifest_org, + sat=module_target_sat, + org=module_sca_manifest_org, sort_by_date=sort_by_date, filter_by_org=filter_by_org, ) @@ -1189,7 +1188,7 @@ def test_positive_list_filter_by_org(target_sat, products_with_repos, filter_by_ @pytest.mark.run_in_one_thread @pytest.mark.tier3 -def test_positive_list_filter_by_cve(module_entitlement_manifest_org, rh_repo, target_sat): +def test_positive_list_filter_by_cve(module_sca_manifest_org, rh_repo, target_sat): """Filter errata by CVE :id: 7791137c-95a7-4518-a56b-766a5680c5fb @@ -1204,7 +1203,7 @@ def test_positive_list_filter_by_cve(module_entitlement_manifest_org, rh_repo, t target_sat.cli.RepositorySet.enable( { 'name': REPOSET['rhva6'], - 'organization-id': module_entitlement_manifest_org.id, + 'organization-id': module_sca_manifest_org.id, 'product': PRDS['rhel'], 'releasever': '6Server', 'basearch': 'x86_64', @@ -1213,14 +1212,14 @@ def test_positive_list_filter_by_cve(module_entitlement_manifest_org, rh_repo, t target_sat.cli.Repository.synchronize( { 'name': REPOS['rhva6']['name'], - 'organization-id': module_entitlement_manifest_org.id, + 'organization-id': module_sca_manifest_org.id, 'product': PRDS['rhel'], } ) repository_info = target_sat.cli.Repository.info( { 'name': REPOS['rhva6']['name'], - 'organization-id': module_entitlement_manifest_org.id, + 'organization-id': module_sca_manifest_org.id, 'product': PRDS['rhel'], } ) From f64ce0d4c9ff687681d851ada96051d6f2790663 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 26 Dec 2023 03:56:41 -0500 Subject: [PATCH 407/586] [6.14.z] Mark Provisioning tests for upgrade (#13546) Mark Provisioning tests for upgrade (#13542) Signed-off-by: Shubham Ganar (cherry picked from commit 5ecedf15e4a81a24a106ea881bd35abdc9202c78) Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> --- tests/foreman/api/test_provisioning.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/foreman/api/test_provisioning.py b/tests/foreman/api/test_provisioning.py index 825f9fa188e..b2cb3388b3b 100644 --- a/tests/foreman/api/test_provisioning.py +++ b/tests/foreman/api/test_provisioning.py @@ -65,6 +65,7 @@ def assert_host_logs(channel, pattern): @pytest.mark.e2e +@pytest.mark.upgrade @pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) @pytest.mark.on_premises_provisioning @pytest.mark.rhel_ver_match('[^6]') @@ -191,6 +192,7 @@ def test_rhel_pxe_provisioning( @pytest.mark.e2e +@pytest.mark.upgrade @pytest.mark.parametrize('pxe_loader', ['ipxe'], indirect=True) @pytest.mark.on_premises_provisioning @pytest.mark.rhel_ver_match('[^6]') @@ -320,6 +322,7 @@ def test_rhel_ipxe_provisioning( @pytest.mark.skip_if_open("BZ:2242925") @pytest.mark.e2e +@pytest.mark.upgrade @pytest.mark.parametrize('pxe_loader', ['http_uefi'], indirect=True) @pytest.mark.on_premises_provisioning @pytest.mark.rhel_ver_match('[^6]') From 3c6e9d1afa9f68044dd614804270d146b865040c Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 27 Dec 2023 07:20:34 -0500 Subject: [PATCH 408/586] [6.14.z] extra check not recording property incase of video recording is false (#13564) extra check not recording property incase of video recording is false (#13475) (cherry picked from commit 61219a18502cbd082d5f9bf486707c68fa083967) Co-authored-by: Omkar Khatavkar --- robottelo/hosts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/robottelo/hosts.py b/robottelo/hosts.py index a975ebeb215..822fed5a568 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1866,8 +1866,9 @@ def get_caller(): video_url = settings.ui.grid_url.replace( ':4444', f'/videos/{ui_session.ui_session_id}.mp4' ) - self.record_property('video_url', video_url) - self.record_property('session_id', ui_session.ui_session_id) + if self.record_property is not None and settings.ui.record_video: + self.record_property('video_url', video_url) + self.record_property('session_id', ui_session.ui_session_id) @property def satellite(self): From f5a5ac4d25fb45ed428082c741db6a9697d633fc Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 28 Dec 2023 07:39:22 -0500 Subject: [PATCH 409/586] [6.14.z] Add Capsule Provisioning test (#13558) * Add Capsule Provisioning test (#13241) Signed-off-by: Shubham Ganar Signed-off-by: Gaurav Talreja (cherry picked from commit ff9a765adbf2ce5c0e9b2d69fc9f56d017ccd281) * Update s/request.param/request.param['rhel_version'] --------- Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> Co-authored-by: Gaurav Talreja --- 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..d158e596a88 --- /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['rhel_version'] + 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 From cc9bfdc5b3d3d03b3ee508804592d99b1711691a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 28 Dec 2023 09:00:22 -0500 Subject: [PATCH 410/586] [6.14.z] Add test for registration with rex pull-mode (#13569) --- pytest_fixtures/core/sat_cap_factory.py | 14 +++ robottelo/hosts.py | 12 ++- .../foreman/destructive/test_registration.py | 96 +++++++++++++++++++ 3 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 tests/foreman/destructive/test_registration.py diff --git a/pytest_fixtures/core/sat_cap_factory.py b/pytest_fixtures/core/sat_cap_factory.py index 436d8b6d443..d58ea9e8b44 100644 --- a/pytest_fixtures/core/sat_cap_factory.py +++ b/pytest_fixtures/core/sat_cap_factory.py @@ -126,6 +126,20 @@ def session_satellite_host(request, satellite_factory): yield sat +@pytest.fixture(scope='module') +def module_satellite_mqtt(module_target_sat): + """Configure satellite with MQTT broker enabled""" + module_target_sat.set_rex_script_mode_provider('pull-mqtt') + # lower the mqtt_resend_interval interval + module_target_sat.set_mqtt_resend_interval('30') + result = module_target_sat.execute('systemctl status mosquitto') + assert result.status == 0, 'MQTT broker is not running' + result = module_target_sat.execute('firewall-cmd --permanent --add-port="1883/tcp"') + assert result.status == 0, 'Failed to open mqtt port on capsule' + module_target_sat.execute('firewall-cmd --reload') + return module_target_sat + + @pytest.fixture def capsule_host(request, capsule_factory): """A fixture that provides a Capsule based on config settings""" diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 822fed5a568..425155a8879 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1665,11 +1665,13 @@ def update_download_policy(self, policy): def set_rex_script_mode_provider(self, mode='ssh'): """Set provider for remote execution script mode. One of: ssh(default), pull-mqtt, ssh-async""" - installer_opts = { - 'foreman-proxy-templates': 'true', - 'foreman-proxy-registration': 'true', - 'foreman-proxy-plugin-remote-execution-script-mode': mode, - } + + installer_opts = {'foreman-proxy-plugin-remote-execution-script-mode': mode} + + if self.__class__.__name__ == 'Capsule': + installer_opts['foreman-proxy-templates'] = 'true' + installer_opts['foreman-proxy-registration'] = 'true' + enable_mqtt_command = InstallerCommand( installer_opts=installer_opts, ) diff --git a/tests/foreman/destructive/test_registration.py b/tests/foreman/destructive/test_registration.py new file mode 100644 index 00000000000..0320f3b633e --- /dev/null +++ b/tests/foreman/destructive/test_registration.py @@ -0,0 +1,96 @@ +"""Tests for registration. + +:Requirement: Registration + +:CaseLevel: Acceptance + +:CaseComponent: Registration + +:CaseAutomation: Automated + +:CaseImportance: High + +:Team: Rocket + +:TestType: Functional + +:Upstream: No +""" +import pytest + +from robottelo.config import settings + + +@pytest.mark.tier3 +@pytest.mark.no_containers +@pytest.mark.rhel_ver_match('[^6]') +def test_host_registration_rex_pull_mode( + module_org, + module_satellite_mqtt, + module_location, + module_ak_with_cv, + module_capsule_configured_mqtt, + rhel_contenthost, +): + """Verify content host registration with Satellite/Capsule as MQTT broker + + :id: a082f599-fbf7-4779-aa18-5139e2bce779 + + :expectedresults: Host registered successfully with MQTT broker + + :parametrized: yes + """ + org = module_org + client_repo = settings.repos.SATCLIENT_REPO[f'rhel{rhel_contenthost.os_version.major}'] + # register host to satellite with pull provider rex + command = module_satellite_mqtt.api.RegistrationCommand( + organization=org, + location=module_location, + activation_keys=[module_ak_with_cv.name], + setup_remote_execution_pull=True, + insecure=True, + repo=client_repo, + ).create() + result = rhel_contenthost.execute(command) + assert result.status == 0, f'Failed to register host: {result.stderr}' + + # check mqtt client is running + result = rhel_contenthost.execute('systemctl status yggdrasild') + assert result.status == 0, f'Failed to start yggdrasil on client: {result.stderr}' + assert rhel_contenthost.execute('yggdrasil status').status == 0 + mqtt_url = f'mqtts://{module_satellite_mqtt.hostname}:1883' + assert rhel_contenthost.execute(f'cat /etc/yggdrasil/config.toml | grep {mqtt_url}').status == 0 + + # Update module_capsule_configured_mqtt to include module_org/module_location + nc = module_capsule_configured_mqtt.nailgun_smart_proxy + module_satellite_mqtt.api.SmartProxy(id=nc.id, organization=[org]).update(['organization']) + module_satellite_mqtt.api.SmartProxy(id=nc.id, location=[module_location]).update(['location']) + + # register host to capsule with pull provider rex + command = module_satellite_mqtt.api.RegistrationCommand( + smart_proxy=nc, + organization=org, + location=module_location, + activation_keys=[module_ak_with_cv.name], + setup_remote_execution_pull=True, + repo=client_repo, + insecure=True, + force=True, + ).create() + result = rhel_contenthost.execute(command) + assert result.status == 0, f'Failed to register host: {result.stderr}' + + # check mqtt client is running + result = rhel_contenthost.execute('systemctl status yggdrasild') + assert result.status == 0, f'Failed to start yggdrasil on client: {result.stderr}' + assert rhel_contenthost.execute('yggdrasil status').status == 0 + new_mqtt_url = f'mqtts://{module_capsule_configured_mqtt.hostname}:1883' + assert ( + rhel_contenthost.execute(f'cat /etc/yggdrasil/config.toml | grep {new_mqtt_url}').status + == 0 + ) + # After force register existing config.toml is saved as backup + assert ( + rhel_contenthost.execute(f'cat /etc/yggdrasil/config.toml.bak | grep {mqtt_url}').status + == 0 + ) From 45d63519047e6f288d70a296f5044ede6d0bb737 Mon Sep 17 00:00:00 2001 From: Gaurav Talreja Date: Tue, 2 Jan 2024 15:52:51 +0530 Subject: [PATCH 411/586] [6.14.z] Add test for show_unsupported_templates setting (#13575) Add test for show_unsupported_templates setting (#13560) Signed-off-by: Gaurav Talreja (cherry picked from commit 97d47c75db86e49d5ceff5d6cdd102fd046627f7) --- tests/foreman/ui/test_settings.py | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/foreman/ui/test_settings.py b/tests/foreman/ui/test_settings.py index e0a2ca0cf9c..43111a7aadc 100644 --- a/tests/foreman/ui/test_settings.py +++ b/tests/foreman/ui/test_settings.py @@ -549,3 +549,37 @@ def test_positive_entries_per_page(session, setting_update): total_pages_str = page_content["Pagination"]['_items'].split()[-2] total_pages = math.ceil(int(total_pages_str.split()[-1]) / property_value) assert str(total_pages) == page_content["Pagination"]['_total_pages'].split()[-1] + + +@pytest.mark.tier2 +def test_positive_show_unsupported_templates(request, target_sat, module_org, module_location): + """Verify setting show_unsupported_templates with new custom template + + :id: e0eaab69-4926-4c1e-b111-30c51ede273z + + :Steps: + 1. Goto Settings -> Provisioning tab -> Show unsupported provisioning templates + + :CaseImportance: Medium + + :expectedresults: Custom template aren't searchable when set to No, + and are searchable when set to Yes(default) + """ + pt = target_sat.api.ProvisioningTemplate( + name=gen_string('alpha'), + organization=[module_org], + location=[module_location], + template=gen_string('alpha'), + snippet=False, + ).create() + request.addfinalizer(pt.delete) + with target_sat.ui_session() as session: + session.organization.select(org_name=module_org.name) + session.location.select(loc_name=module_location.name) + default_value = target_sat.update_setting('show_unsupported_templates', 'No') + assert not session.provisioningtemplate.search(f'name={pt.name}') + + # Verify with show_unsupported_templates=Yes + target_sat.update_setting('show_unsupported_templates', default_value) + template = session.provisioningtemplate.search(f'name={pt.name}') + assert template[0]['Name'] == pt.name From 4a46c9c38d4da86b12c7f23e02089616751e3e34 Mon Sep 17 00:00:00 2001 From: vsedmik <46570670+vsedmik@users.noreply.github.com> Date: Tue, 2 Jan 2024 15:08:14 +0100 Subject: [PATCH 412/586] [6.14.z] Add ISS Network Sync class and basic scenario (#13588) * Add ISS Network Sync class and basic scenario * minor param renames --- robottelo/cli/org.py | 7 ++ tests/foreman/cli/test_satellitesync.py | 151 ++++++++++++++++++++++++ 2 files changed, 158 insertions(+) diff --git a/robottelo/cli/org.py b/robottelo/cli/org.py index 0ae1308013a..38103aff3a5 100644 --- a/robottelo/cli/org.py +++ b/robottelo/cli/org.py @@ -21,6 +21,7 @@ add-subnet Associate a resource add-user Associate a resource create Create an organization + configure-cdn Update the CDN configuration delete Delete an organization delete-parameter Delete parameter for an organization. info Show an organization @@ -166,3 +167,9 @@ def remove_user(cls, options=None): """Removes an user from an org""" cls.command_sub = 'remove-user' return cls.execute(cls._construct_command(options)) + + @classmethod + def configure_cdn(cls, options=None): + """Update the CDN configuration""" + cls.command_sub = 'configure-cdn' + return cls.execute(cls._construct_command(options)) diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index aa660748303..e802ae99603 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -2059,3 +2059,154 @@ def test_positive_install_package_from_imported_repos(self): :CaseLevel: System """ + + +@pytest.fixture(scope='module') +def module_downstream_sat(module_satellite_host): + """Provides Downstream Satellite.""" + module_satellite_host.cli.Settings.set( + {'name': 'subscription_connection_enabled', 'value': 'No'} + ) + return module_satellite_host + + +@pytest.fixture +def function_downstream_org(module_downstream_sat, function_sca_manifest): + """Provides organization with manifest on Downstream Satellite.""" + org = module_downstream_sat.api.Organization().create() + module_downstream_sat.upload_manifest(org.id, function_sca_manifest.content) + return org + + +def _set_downstream_org( + downstream_sat, + upstream_sat, + downstream_org, + upstream_org='Default_Organization', + username=settings.server.admin_username, + password=settings.server.admin_password, + lce_label=None, + cv_label=None, +): + """Configures Downstream organization to sync from particular Upstream organization. + + :param downstream_sat: Downstream Satellite instance. + :param upstream_sat: Upstream Satellite instance. + :param downstream_org: Downstream organization to be configured. + :param upstream_org: Upstream organization to sync CDN content from, + default: Default_Organization + :param username: Username for authentication, default: admin username from settings. + :param password: Password for authentication, default: admin password from settings. + :param lce_label: Upstream Lifecycle Environment, default: Library + :param cv_label: Upstream Content View Label, default: Default_Organization_View. + :return: True if succeeded. + """ + # Create Content Credentials with Upstream Satellite's katello-server-ca.crt. + crt_file = f'{upstream_sat.hostname}.crt' + downstream_sat.execute( + f'curl -o {crt_file} http://{upstream_sat.hostname}/pub/katello-server-ca.crt' + ) + cc = downstream_sat.cli.ContentCredential.create( + { + 'name': upstream_sat.hostname, + 'organization-id': downstream_org.id, + 'path': crt_file, + 'content-type': 'cert', + } + ) + # Set the CDN configuration to Network Sync. + res = downstream_sat.cli.Org.configure_cdn( + { + 'id': downstream_org.id, + 'type': 'network_sync', + 'url': f'https://{upstream_sat.hostname}/', + 'username': username, + 'password': password, + 'upstream-organization-label': upstream_org.label, + 'upstream-lifecycle-environment-label': lce_label, + 'upstream-content-view-label': cv_label, + 'ssl-ca-credential-id': cc['id'], + } + ) + return 'Updated CDN configuration' in res + + +class TestNetworkSync: + """Implements Network Sync scenarios.""" + + @pytest.mark.tier2 + @pytest.mark.parametrize( + 'function_synced_rhel_repo', + ['rhae2'], + indirect=True, + ) + def test_positive_network_sync_rh_repo( + self, + target_sat, + function_sca_manifest_org, + function_synced_rhel_repo, + module_downstream_sat, + function_downstream_org, + ): + """Sync a RH repo from Upstream to Downstream Satellite + + :id: fdb58c18-0a64-418b-990d-2233381fee8f + + :parametrized: yes + + :setup: + 1. Enabled and synced RH yum repository at Upstream Sat. + 2. Organization with manifest at Downstream Sat. + + :steps: + 1. Set the Downstream org to sync from Upstream org. + 2. Enable and sync the repository from Upstream to Downstream. + + :expectedresults: + 1. Repository can be enabled and synced. + + :CaseLevel: System + + :BZ: 2213128 + + :customerscenario: true + + """ + assert _set_downstream_org( + downstream_sat=module_downstream_sat, + upstream_sat=target_sat, + downstream_org=function_downstream_org, + upstream_org=function_sca_manifest_org, + ), 'Downstream org configuration failed' + + # Enable and sync the repository. + reposet = module_downstream_sat.cli.RepositorySet.list( + { + 'organization-id': function_downstream_org.id, + 'search': f'content_label={function_synced_rhel_repo["content-label"]}', + } + ) + assert ( + len(reposet) == 1 + ), f'Expected just one reposet for "{function_synced_rhel_repo["content-label"]}"' + res = module_downstream_sat.cli.RepositorySet.enable( + { + 'organization-id': function_downstream_org.id, + 'id': reposet[0]['id'], + 'basearch': DEFAULT_ARCHITECTURE, + } + ) + assert 'Repository enabled' in str(res), 'Repository enable failed' + + repos = module_downstream_sat.cli.Repository.list( + {'organization-id': function_downstream_org.id} + ) + assert len(repos) == 1, 'Expected 1 repo enabled' + repo = repos[0] + module_downstream_sat.cli.Repository.synchronize({'id': repo['id']}) + + repo = module_downstream_sat.cli.Repository.info({'id': repo['id']}) + assert 'Success' in repo['sync']['status'], 'Sync did not succeed' + assert ( + repo['content-counts'] == function_synced_rhel_repo['content-counts'] + ), 'Content counts do not match' From cde9ac261368f1c2218ea9ad5ee00f8331904956 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 4 Jan 2024 09:37:02 -0500 Subject: [PATCH 413/586] [6.14.z] Markers as test property in reports (#13593) Markers as test property in reports (#13043) (cherry picked from commit 8d9af7883fa241ac1a7a7083b0b468ca4668c3e3) Co-authored-by: Jitendra Yejare --- pytest_plugins/metadata_markers.py | 12 +++++++++++- tests/foreman/virtwho/ui/test_nutanix_sca.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pytest_plugins/metadata_markers.py b/pytest_plugins/metadata_markers.py index 70aee47c6c7..311628a8962 100644 --- a/pytest_plugins/metadata_markers.py +++ b/pytest_plugins/metadata_markers.py @@ -116,8 +116,18 @@ def pytest_collection_modifyitems(items, config): # add markers as user_properties so they are recorded in XML properties of the report # pytest-ibutsu will include user_properties dict in testresult metadata + markers_prop_data = [] + exclude_markers = ['parametrize', 'skipif', 'usefixtures', 'skip_if_not_set'] for marker in item.iter_markers(): - item.user_properties.append((marker.name, next(iter(marker.args), None))) + prop = marker.name + if prop in exclude_markers: + continue + if marker_val := next(iter(marker.args), None): + prop = '='.join([prop, str(marker_val)]) + markers_prop_data.append(prop) + item.user_properties.append(("markers", ", ".join(markers_prop_data))) + + # Version specific user properties item.user_properties.append(("BaseOS", rhel_version)) item.user_properties.append(("SatelliteVersion", sat_version)) item.user_properties.append(("SnapVersion", snap_version)) diff --git a/tests/foreman/virtwho/ui/test_nutanix_sca.py b/tests/foreman/virtwho/ui/test_nutanix_sca.py index eb2d7d889ad..89bae08771d 100644 --- a/tests/foreman/virtwho/ui/test_nutanix_sca.py +++ b/tests/foreman/virtwho/ui/test_nutanix_sca.py @@ -8,7 +8,7 @@ :CaseComponent: Virt-whoConfigurePlugin -:Team: Phoenix +:Team: Phoenix-subscriptions :TestType: Functional From 93fbe94fbe82fde788f4f4421a8bf455ea95c709 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 4 Jan 2024 13:04:56 -0500 Subject: [PATCH 414/586] [6.14.z] Test capsule without registration module (#13611) --- tests/foreman/api/test_registration.py | 44 ++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/foreman/api/test_registration.py b/tests/foreman/api/test_registration.py index 77fd856db11..50f5e0c0b09 100644 --- a/tests/foreman/api/test_registration.py +++ b/tests/foreman/api/test_registration.py @@ -227,3 +227,47 @@ def test_negative_global_registration_without_ak(module_target_sat): with pytest.raises(HTTPError) as context: module_target_sat.api.RegistrationCommand().create() assert 'Missing activation key!' in context.value.response.text + + +def test_negative_capsule_without_registration_enabled( + module_target_sat, + module_capsule_configured, + module_ak_with_cv, + module_entitlement_manifest_org, + module_location, +): + """Verify registration with Capsule, when registration isn't configured in installer + + :id: a2f23e42-648d-4428-a961-6e0b933c6dff + + :steps: + 1. Get a configured capsule + 2. The registration is set to False on capsule by default + 3. Try to register host with that capsule + + :expectedresults: Registration fails with HTTP error code 422 and an error message. + """ + org = module_entitlement_manifest_org + + nc = module_capsule_configured.nailgun_smart_proxy + module_target_sat.api.SmartProxy(id=nc.id, organization=[org]).update(['organization']) + module_target_sat.api.SmartProxy(id=nc.id, location=[module_location]).update(['location']) + + res = module_capsule_configured.install( + cmd_args={}, + cmd_kwargs={'foreman-proxy-registration': 'false', 'foreman-proxy-templates': 'true'}, + ) + assert res.status == 0 + error_message = '422 Client Error' + with pytest.raises(HTTPError, match=f'{error_message}') as context: + module_target_sat.api.RegistrationCommand( + smart_proxy=nc, + organization=org, + location=module_location, + activation_keys=[module_ak_with_cv.name], + insecure=True, + ).create() + assert ( + "Proxy lacks one of the following features: 'Registration', 'Templates'" + in context.value.response.text + ) From 24b28154a16980703ddf9d67c9c53d04e2fd29b4 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 5 Jan 2024 10:08:24 -0500 Subject: [PATCH 415/586] [6.14.z] Fixing the missing markers as independent properties (#13605) Fixing the missing markers as independent properties (#13599) (cherry picked from commit ae99d0d0fe2ceda3d35c6586788e1587ab3c5ff6) Co-authored-by: Jitendra Yejare --- pytest_plugins/metadata_markers.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pytest_plugins/metadata_markers.py b/pytest_plugins/metadata_markers.py index 311628a8962..57b12aa5c1f 100644 --- a/pytest_plugins/metadata_markers.py +++ b/pytest_plugins/metadata_markers.py @@ -119,12 +119,15 @@ def pytest_collection_modifyitems(items, config): markers_prop_data = [] exclude_markers = ['parametrize', 'skipif', 'usefixtures', 'skip_if_not_set'] for marker in item.iter_markers(): - prop = marker.name - if prop in exclude_markers: + proprty = marker.name + if proprty in exclude_markers: continue if marker_val := next(iter(marker.args), None): - prop = '='.join([prop, str(marker_val)]) - markers_prop_data.append(prop) + proprty = '='.join([proprty, str(marker_val)]) + markers_prop_data.append(proprty) + # Adding independent marker as a property + item.user_properties.append((marker.name, marker_val)) + # Adding all markers as a single property item.user_properties.append(("markers", ", ".join(markers_prop_data))) # Version specific user properties From 4017815151795090e118bac77ce7d1037595dd56 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 5 Jan 2024 14:17:26 -0500 Subject: [PATCH 416/586] [6.14.z] Bump flake8 from 6.1.0 to 7.0.0 (#13629) --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 24b8b4c6001..1dcaeaa560f 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -1,5 +1,5 @@ # For running tests and checking code quality using these modules. -flake8==6.1.0 +flake8==7.0.0 pytest-cov==4.1.0 redis==5.0.1 pre-commit==3.6.0 From bbc3820d37f4ad068fcf5bf30f8ec77257b6c1c7 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 5 Jan 2024 16:50:35 -0500 Subject: [PATCH 417/586] [6.14.z] Correct Casecomponent in upgrades/test_classparameter.py (#13624) Correct Casecomponent in upgrades/test_classparameter.py (#13621) (cherry picked from commit f267421c45779c4246b0eb2171fd069d443634db) Co-authored-by: Gaurav Talreja --- tests/upgrades/test_classparameter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/upgrades/test_classparameter.py b/tests/upgrades/test_classparameter.py index f113f7ab235..689374d7dba 100644 --- a/tests/upgrades/test_classparameter.py +++ b/tests/upgrades/test_classparameter.py @@ -6,7 +6,7 @@ :CaseLevel: Acceptance -:CaseComponent: Parameters +:CaseComponent: Puppet :Team: Rocket From ca9f2510c52da3d61095e3d3363e0ecedcdd9f0f Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 8 Jan 2024 03:12:07 -0500 Subject: [PATCH 418/586] [6.14.z] Add a test for parameter precedence on the host (#13636) --- tests/foreman/api/test_parameters.py | 76 ++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/foreman/api/test_parameters.py diff --git a/tests/foreman/api/test_parameters.py b/tests/foreman/api/test_parameters.py new file mode 100644 index 00000000000..9aa297a5949 --- /dev/null +++ b/tests/foreman/api/test_parameters.py @@ -0,0 +1,76 @@ +"""Tests for parameters + +:Requirement: Parameters + +:CaseAutomation: Automated + +:CaseLevel: Acceptance + +:CaseComponent: Parameters + +:Team: Rocket + +:TestType: Functional + +:CaseImportance: Critical + +:Upstream: No +""" +from fauxfactory import gen_string +import pytest + + +@pytest.mark.tier1 +@pytest.mark.e2e +@pytest.mark.upgrade +def test_positive_parameter_precedence_impact( + request, module_org, module_location, module_target_sat +): + """Check parameter precedences for Global, Hostgroup, and Host parameters + + :id: 8dd6c4e8-4ec9-4bee-8a04-f5788960979b + + :steps: + 1. Create Global Parameter + 2. Create host and verify global parameter is assigned + 3. Create Host Group with parameter + 4. Assign hostgroup to host created above and verify hostgroup parameter is assigned. + 5. Add parameter on the host directly, and verify that this should take precedence + over Host group and Global Parameter + + :expectedresults: Host parameter take precedence over hostgroup and global parameter, + and hostgroup take precedence over global parameter when there are no host parameters + """ + param_name = gen_string('alpha') + param_value = gen_string('alpha') + + cp = module_target_sat.api.CommonParameter(name=param_name, value=param_value).create() + host = module_target_sat.api.Host(organization=module_org, location=module_location).create() + result = [res for res in host.all_parameters if res['name'] == param_name] + assert result[0]['name'] == param_name + assert result[0]['associated_type'] == 'global' + + hg = module_target_sat.api.HostGroup( + organization=[module_org], + group_parameters_attributes=[{'name': param_name, 'value': param_value}], + ).create() + host.hostgroup = hg + host = host.update(['hostgroup']) + result = [res for res in host.all_parameters if res['name'] == param_name] + assert result[0]['name'] == param_name + assert result[0]['associated_type'] != 'global' + assert result[0]['associated_type'] == 'host group' + + @request.addfinalizer + def _finalize(): + host.delete() + hg.delete() + cp.delete() + + host.host_parameters_attributes = [{'name': param_name, 'value': param_value}] + host = host.update(['host_parameters_attributes']) + result = [res for res in host.all_parameters if res['name'] == param_name] + assert result[0]['name'] == param_name + assert result[0]['associated_type'] != 'global' + assert result[0]['associated_type'] != 'host group' + assert result[0]['associated_type'] == 'host' From 53e9c7f014841d39e791d28eea533076658a466a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 8 Jan 2024 05:30:12 -0500 Subject: [PATCH 419/586] [6.14.z] Fix test AK create with CV (#13643) --- tests/foreman/cli/test_activationkey.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/foreman/cli/test_activationkey.py b/tests/foreman/cli/test_activationkey.py index da61301e19f..15aca83eaf7 100644 --- a/tests/foreman/cli/test_activationkey.py +++ b/tests/foreman/cli/test_activationkey.py @@ -154,10 +154,11 @@ def test_positive_create_with_cv(name, module_org, get_default_env, module_targe new_cv = module_target_sat.cli_factory.make_content_view( {'name': name, 'organization-id': module_org.id} ) + module_target_sat.cli.ContentView.publish({'id': new_cv['id']}) new_ak_cv = module_target_sat.cli_factory.make_activation_key( { 'content-view': new_cv['name'], - 'environment': get_default_env['name'], + 'lifecycle-environment': get_default_env['name'], 'organization-id': module_org.id, } ) From e17b8f8cbed9e76db4b91d6f0d9fa013c67e7d7a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 8 Jan 2024 15:57:52 -0500 Subject: [PATCH 420/586] [6.14.z] Small fix for SyncPlan test (#13664) --- tests/foreman/cli/test_syncplan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/cli/test_syncplan.py b/tests/foreman/cli/test_syncplan.py index b6dc18e9bc6..483a7145c85 100644 --- a/tests/foreman/cli/test_syncplan.py +++ b/tests/foreman/cli/test_syncplan.py @@ -368,7 +368,7 @@ def test_positive_info_with_assigned_product(module_org, module_target_sat): module_target_sat.cli.Product.set_sync_plan( {'id': product['id'], 'sync-plan-id': sync_plan['id']} ) - updated_plan = module_target_sat.info({'id': sync_plan['id']}) + updated_plan = module_target_sat.cli.SyncPlan.info({'id': sync_plan['id']}) assert len(updated_plan['products']) == 2 assert {prod['name'] for prod in updated_plan['products']} == {prod1, prod2} From 4d309c22065d82e92fb425da6b2d11aa6de04362 Mon Sep 17 00:00:00 2001 From: Peter Ondrejka Date: Mon, 8 Jan 2024 18:46:36 +0100 Subject: [PATCH 421/586] fixing a forgotten make_job_invocaiton call (#13648) (cherry picked from commit d0b6ebba27553f503e5707b6bdc7e0cf0a49a8f0) --- tests/foreman/cli/test_remoteexecution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index b3f0cf0eb74..840bdd0153e 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -1203,7 +1203,7 @@ def test_positive_run_job_on_host_registered_to_pull_provider( module_target_sat, make_user_job['id'], rhel_contenthost.hostname ) # create a file as new user - invocation_command = module_target_sat.make_job_invocation( + invocation_command = module_target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', 'inputs': f"command=touch /home/{username}/{filename}", From 1bd5a43687ae9bbb8466f612e22c86f83931cd34 Mon Sep 17 00:00:00 2001 From: vsedmik <46570670+vsedmik@users.noreply.github.com> Date: Tue, 9 Jan 2024 11:11:53 +0100 Subject: [PATCH 422/586] [6.14.z] Add coverage for BZ#2112098 (#13671) * ISS test for custom CDN with content credentials * Rename fixture with better name --- tests/foreman/cli/test_satellitesync.py | 203 +++++++++++++++++++----- 1 file changed, 167 insertions(+), 36 deletions(-) diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index e802ae99603..bdb678e59b8 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -130,8 +130,8 @@ def function_synced_custom_repo(target_sat, function_org, function_product): @pytest.fixture -def function_synced_rhel_repo(request, target_sat, function_sca_manifest_org): - """Enable and synchronize rhel content with immediate policy""" +def function_synced_rh_repo(request, target_sat, function_sca_manifest_org): + """Enable and synchronize RH repo with immediate policy""" repo_dict = ( REPOS['kickstart'][request.param.replace('kickstart', '')[1:]] if 'kickstart' in request.param @@ -325,7 +325,7 @@ def test_positive_export_library_custom_repo( @pytest.mark.tier3 @pytest.mark.upgrade @pytest.mark.parametrize( - 'function_synced_rhel_repo', + 'function_synced_rh_repo', ['rhae2'], indirect=True, ) @@ -334,7 +334,7 @@ def test_positive_export_complete_library_rh_repo( target_sat, export_import_cleanup_function, function_sca_manifest_org, - function_synced_rhel_repo, + function_synced_rh_repo, ): """Export RedHat repo via complete library @@ -363,7 +363,7 @@ def test_positive_export_complete_library_rh_repo( { 'id': cv['id'], 'organization-id': function_sca_manifest_org.id, - 'repository-id': function_synced_rhel_repo['id'], + 'repository-id': function_synced_rh_repo['id'], } ) target_sat.cli.ContentView.publish({'id': cv['id']}) @@ -621,7 +621,7 @@ def test_positive_export_import_cv_end_to_end( @pytest.mark.upgrade @pytest.mark.tier3 @pytest.mark.parametrize( - 'function_synced_rhel_repo', + 'function_synced_rh_repo', ['rhae2'], indirect=True, ) @@ -633,7 +633,7 @@ def test_positive_export_import_default_org_view( function_sca_manifest_org, function_import_org_with_manifest, function_synced_custom_repo, - function_synced_rhel_repo, + function_synced_rh_repo, ): """Export Default Organization View version contents in directory and Import them. @@ -676,7 +676,7 @@ def test_positive_export_import_default_org_view( { 'id': cv['id'], 'organization-id': function_sca_manifest_org.id, - 'repository-id': function_synced_rhel_repo['id'], + 'repository-id': function_synced_rh_repo['id'], } ) target_sat.cli.ContentView.publish({'id': cv['id']}) @@ -897,7 +897,7 @@ def test_positive_export_import_promoted_cv( @pytest.mark.upgrade @pytest.mark.e2e @pytest.mark.parametrize( - 'function_synced_rhel_repo', + 'function_synced_rh_repo', ['kickstart-rhel7', 'kickstart-rhel8_bos', 'rhscl7'], indirect=True, ) @@ -908,7 +908,7 @@ def test_positive_export_import_redhat_cv( config_export_import_settings, function_sca_manifest_org, function_import_org_with_manifest, - function_synced_rhel_repo, + function_synced_rh_repo, ): """Export CV version with RedHat contents in directory and import them. @@ -943,7 +943,7 @@ def test_positive_export_import_redhat_cv( { 'id': cv['id'], 'organization-id': function_sca_manifest_org.id, - 'repository-id': function_synced_rhel_repo['id'], + 'repository-id': function_synced_rh_repo['id'], } ) target_sat.cli.ContentView.publish({'id': cv['id']}) @@ -980,15 +980,15 @@ def test_positive_export_import_redhat_cv( assert len(exported_packages) == len(imported_packages) exported_repo = target_sat.cli.Repository.info( { - 'name': function_synced_rhel_repo['name'], - 'product': function_synced_rhel_repo['product']['name'], + 'name': function_synced_rh_repo['name'], + 'product': function_synced_rh_repo['product']['name'], 'organization-id': function_sca_manifest_org.id, } ) imported_repo = target_sat.cli.Repository.info( { - 'name': function_synced_rhel_repo['name'], - 'product': function_synced_rhel_repo['product']['name'], + 'name': function_synced_rh_repo['name'], + 'product': function_synced_rh_repo['product']['name'], 'organization-id': function_import_org_with_manifest.id, } ) @@ -1321,7 +1321,7 @@ def test_postive_export_import_cv_with_file_content( @pytest.mark.tier2 @pytest.mark.parametrize( - 'function_synced_rhel_repo', + 'function_synced_rh_repo', ['rhae2'], indirect=True, ) @@ -1330,7 +1330,7 @@ def test_positive_export_rerun_failed_import( target_sat, config_export_import_settings, export_import_cleanup_function, - function_synced_rhel_repo, + function_synced_rh_repo, function_sca_manifest_org, function_import_org_with_manifest, ): @@ -1367,7 +1367,7 @@ def test_positive_export_rerun_failed_import( { 'id': cv['id'], 'organization-id': function_sca_manifest_org.id, - 'repository-id': function_synced_rhel_repo['id'], + 'repository-id': function_synced_rh_repo['id'], } ) target_sat.cli.ContentView.publish({'id': cv['id']}) @@ -1548,7 +1548,7 @@ def test_postive_export_import_repo_with_GPG( @pytest.mark.tier3 @pytest.mark.parametrize( - 'function_synced_rhel_repo', + 'function_synced_rh_repo', ['rhae2'], indirect=True, ) @@ -1558,7 +1558,7 @@ def test_negative_import_redhat_cv_without_manifest( export_import_cleanup_function, config_export_import_settings, function_sca_manifest_org, - function_synced_rhel_repo, + function_synced_rh_repo, ): """Redhat content can't be imported into satellite/organization without manifest @@ -1588,7 +1588,7 @@ def test_negative_import_redhat_cv_without_manifest( { 'id': cv['id'], 'organization-id': function_sca_manifest_org.id, - 'repository-id': function_synced_rhel_repo['id'], + 'repository-id': function_synced_rh_repo['id'], } ) target_sat.cli.ContentView.publish({'id': cv['id']}) @@ -1697,7 +1697,7 @@ def test_positive_import_content_for_disconnected_sat_with_existing_content( @pytest.mark.tier3 @pytest.mark.parametrize( - 'function_synced_rhel_repo', + 'function_synced_rh_repo', ['rhae2'], indirect=True, ) @@ -1707,7 +1707,7 @@ def test_positive_export_incremental_syncable_check_content( export_import_cleanup_function, config_export_import_settings, function_sca_manifest_org, - function_synced_rhel_repo, + function_synced_rh_repo, ): """Export complete and incremental CV version in syncable format and assert that all files referenced in the repomd.xml (including productid) are present in the exports. @@ -1743,7 +1743,7 @@ def test_positive_export_incremental_syncable_check_content( { 'id': cv['id'], 'organization-id': function_sca_manifest_org.id, - 'repository-id': function_synced_rhel_repo['id'], + 'repository-id': function_synced_rh_repo['id'], } ) target_sat.cli.ContentView.publish({'id': cv['id']}) @@ -1945,7 +1945,7 @@ def test_positive_export_import_incremental_yum_repo( @pytest.mark.tier3 @pytest.mark.parametrize( - 'function_synced_rhel_repo', + 'function_synced_rh_repo', ['rhae2'], indirect=True, ) @@ -1956,7 +1956,7 @@ def test_positive_export_import_mismatch_label( config_export_import_settings, function_sca_manifest_org, function_import_org_with_manifest, - function_synced_rhel_repo, + function_synced_rh_repo, ): """Export and import repo with mismatched label @@ -1985,7 +1985,7 @@ def test_positive_export_import_mismatch_label( assert target_sat.validate_pulp_filepath(function_sca_manifest_org, PULP_EXPORT_DIR) == '' # Export the repository and check the export directory export = target_sat.cli.ContentExport.completeRepository( - {'id': function_synced_rhel_repo['id']} + {'id': function_synced_rh_repo['id']} ) assert '1.0' in target_sat.validate_pulp_filepath( function_sca_manifest_org, PULP_EXPORT_DIR @@ -1999,8 +1999,8 @@ def test_positive_export_import_mismatch_label( ) import_repo = target_sat.cli.Repository.info( { - 'name': function_synced_rhel_repo['name'], - 'product': function_synced_rhel_repo['product']['name'], + 'name': function_synced_rh_repo['name'], + 'product': function_synced_rh_repo['product']['name'], 'organization-id': function_sca_manifest_org.id, } ) @@ -2008,7 +2008,7 @@ def test_positive_export_import_mismatch_label( # Export again and check the export directory export = target_sat.cli.ContentExport.completeRepository( - {'id': function_synced_rhel_repo['id']} + {'id': function_synced_rh_repo['id']} ) assert '2.0' in target_sat.validate_pulp_filepath( function_sca_manifest_org, PULP_EXPORT_DIR @@ -2017,7 +2017,7 @@ def test_positive_export_import_mismatch_label( # Change the repo label in metadata.json and run the import again import_path = target_sat.move_pulp_archive(function_sca_manifest_org, export['message']) target_sat.execute( - f'''sed -i 's/"label":"{function_synced_rhel_repo['label']}"/''' + f'''sed -i 's/"label":"{function_synced_rh_repo['label']}"/''' f'''"label":"{gen_string("alpha")}"/g' {import_path}/metadata.json''' ) target_sat.cli.ContentImport.repository( @@ -2034,6 +2034,137 @@ def test_positive_export_import_mismatch_label( ['success' in task['result'] for task in tasks] ), 'Not every import task succeeded' + @pytest.mark.tier3 + @pytest.mark.parametrize( + 'function_synced_rh_repo', + ['rhae2'], + indirect=True, + ) + def test_positive_custom_cdn_with_credential( + self, + request, + target_sat, + export_import_cleanup_function, + config_export_import_settings, + function_sca_manifest_org, + function_synced_rh_repo, + satellite_host, + function_sca_manifest, + ): + """Export and sync repository using custom cert for custom CDN. + + :id: de1f4b06-267a-4bad-9a90-665f2906ef5f + + :parametrized: yes + + :setup: + 1. Upstream Satellite with enabled and synced RH yum repository. + 2. Downstream Satellite to sync from Upstream Satellite. + + :steps: + On the Upstream Satellite: + 1. Export the repository in syncable format and move it + to /var/www/html/pub/repos to mimic custom CDN. + On the Downstream Satellite: + 2. Create new Organization, import manifest. + 3. Create Content Credentials with Upstream Satellite's katello-server-ca.crt. + 4. Set the CDN configuration to custom CDN and use the url and CC from above. + 5. Enable and sync the repository. + + :expectedresults: + 1. Repository can be enabled and synced from Upstream to Downstream Satellite. + + :CaseLevel: System + + :BZ: 2112098 + + :customerscenario: true + """ + meta_file = 'metadata.json' + crt_file = 'source.crt' + pub_dir = '/var/www/html/pub/repos' + + # Export the repository in syncable format and move it + # to /var/www/html/pub/repos to mimic custom CDN. + target_sat.cli.ContentExport.completeRepository( + {'id': function_synced_rh_repo['id'], 'format': 'syncable'} + ) + assert '1.0' in target_sat.validate_pulp_filepath( + function_sca_manifest_org, PULP_EXPORT_DIR + ) + exp_dir = target_sat.execute( + f'find {PULP_EXPORT_DIR}{function_sca_manifest_org.name}/ -name {meta_file}' + ).stdout.splitlines() + assert len(exp_dir) == 1 + exp_dir = exp_dir[0].replace(meta_file, '') + + assert target_sat.execute(f'mv {exp_dir} {pub_dir}').status == 0 + request.addfinalizer(lambda: target_sat.execute(f'rm -rf {pub_dir}')) + target_sat.execute(f'semanage fcontext -a -t httpd_sys_content_t "{pub_dir}(/.*)?"') + target_sat.execute(f'restorecon -R {pub_dir}') + + # Create new Organization, import manifest. + import_org = satellite_host.api.Organization().create() + satellite_host.upload_manifest(import_org.id, function_sca_manifest.content) + + # Create Content Credentials with Upstream Satellite's katello-server-ca.crt. + satellite_host.execute( + f'curl -o {crt_file} http://{target_sat.hostname}/pub/katello-server-ca.crt' + ) + cc = satellite_host.cli.ContentCredential.create( + { + 'name': gen_string('alpha'), + 'organization-id': import_org.id, + 'path': crt_file, + 'content-type': 'cert', + } + ) + assert cc, 'No content credential created' + + # Set the CDN configuration to custom CDN and use the url and CC from above. + res = satellite_host.cli.Org.configure_cdn( + { + 'id': import_org.id, + 'type': 'custom_cdn', + 'ssl-ca-credential-id': cc['id'], + 'url': f'https://{target_sat.hostname}/pub/repos/', + } + ) + assert 'Updated CDN configuration' in res + + # Enable and sync the repository. + reposet = satellite_host.cli.RepositorySet.list( + { + 'organization-id': import_org.id, + 'search': f'content_label={function_synced_rh_repo["content-label"]}', + } + ) + assert ( + len(reposet) == 1 + ), f'Expected just one reposet for "{function_synced_rh_repo["content-label"]}"' + res = satellite_host.cli.RepositorySet.enable( + { + 'organization-id': import_org.id, + 'id': reposet[0]['id'], + 'basearch': DEFAULT_ARCHITECTURE, + } + ) + assert 'Repository enabled' in str(res) + + repos = satellite_host.cli.Repository.list({'organization-id': import_org.id}) + assert len(repos) == 1, 'Expected 1 repo enabled' + repo = repos[0] + satellite_host.cli.Repository.synchronize({'id': repo['id']}) + + repo = satellite_host.cli.Repository.info({'id': repo['id']}) + assert ( + f'{target_sat.hostname}/pub/repos/' in repo['url'] + ), 'Enabled repo does not point to the upstream Satellite' + assert 'Success' in repo['sync']['status'], 'Sync did not succeed' + assert ( + repo['content-counts'] == function_synced_rh_repo['content-counts'] + ), 'Content counts do not match' + @pytest.mark.stubbed @pytest.mark.tier3 @pytest.mark.upgrade @@ -2136,7 +2267,7 @@ class TestNetworkSync: @pytest.mark.tier2 @pytest.mark.parametrize( - 'function_synced_rhel_repo', + 'function_synced_rh_repo', ['rhae2'], indirect=True, ) @@ -2144,7 +2275,7 @@ def test_positive_network_sync_rh_repo( self, target_sat, function_sca_manifest_org, - function_synced_rhel_repo, + function_synced_rh_repo, module_downstream_sat, function_downstream_org, ): @@ -2183,12 +2314,12 @@ def test_positive_network_sync_rh_repo( reposet = module_downstream_sat.cli.RepositorySet.list( { 'organization-id': function_downstream_org.id, - 'search': f'content_label={function_synced_rhel_repo["content-label"]}', + 'search': f'content_label={function_synced_rh_repo["content-label"]}', } ) assert ( len(reposet) == 1 - ), f'Expected just one reposet for "{function_synced_rhel_repo["content-label"]}"' + ), f'Expected just one reposet for "{function_synced_rh_repo["content-label"]}"' res = module_downstream_sat.cli.RepositorySet.enable( { 'organization-id': function_downstream_org.id, @@ -2208,5 +2339,5 @@ def test_positive_network_sync_rh_repo( repo = module_downstream_sat.cli.Repository.info({'id': repo['id']}) assert 'Success' in repo['sync']['status'], 'Sync did not succeed' assert ( - repo['content-counts'] == function_synced_rhel_repo['content-counts'] + repo['content-counts'] == function_synced_rh_repo['content-counts'] ), 'Content counts do not match' From 2e86545bae78de5b7100559b9edb1cf34027733f Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 9 Jan 2024 07:14:02 -0500 Subject: [PATCH 423/586] [6.14.z] Removing stubbed marker (#13685) --- tests/foreman/api/test_discoveryrule.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/foreman/api/test_discoveryrule.py b/tests/foreman/api/test_discoveryrule.py index 14ead0aabed..c8ad3dc062b 100644 --- a/tests/foreman/api/test_discoveryrule.py +++ b/tests/foreman/api/test_discoveryrule.py @@ -115,7 +115,6 @@ def test_negative_create_with_invalid_host_limit_and_priority(module_target_sat) module_target_sat.api.DiscoveryRule(priority=gen_string('alpha')).create() -@pytest.mark.stubbed @pytest.mark.tier3 def test_positive_update_and_provision_with_rule_priority( module_target_sat, module_discovery_hostgroup, discovery_location, discovery_org From d37b0088054b99d967db0111fee48a7a54cd5a09 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 10 Jan 2024 02:52:31 -0500 Subject: [PATCH 424/586] [6.14.z] Bump kentaro-m/auto-assign-action from 1.2.5 to 1.2.6 (#13706) --- .github/workflows/auto_assignment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto_assignment.yaml b/.github/workflows/auto_assignment.yaml index 72c62c984a9..5b914ed6424 100644 --- a/.github/workflows/auto_assignment.yaml +++ b/.github/workflows/auto_assignment.yaml @@ -15,6 +15,6 @@ jobs: if: "!contains(github.event.pull_request.labels.*.name, 'Auto_Cherry_Picked')" runs-on: ubuntu-latest steps: - - uses: kentaro-m/auto-assign-action@v1.2.5 + - uses: kentaro-m/auto-assign-action@v1.2.6 with: configuration-path: ".github/auto_assign.yml" From 0afe1289dcb9724bd7578cdd523c9a3f986f4fd1 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 10 Jan 2024 05:24:02 -0500 Subject: [PATCH 425/586] [6.14.z] Fix domain read in discovery auto provision test (#13711) Fix domain read in discovery auto provision test (#13708) Signed-off-by: Gaurav Talreja (cherry picked from commit 6d75a026be2e234c88f39328408e716a2d91f51c) Co-authored-by: Gaurav Talreja --- tests/foreman/ui/test_discoveredhost.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/ui/test_discoveredhost.py b/tests/foreman/ui/test_discoveredhost.py index 322571f5954..87b62e412e1 100644 --- a/tests/foreman/ui/test_discoveredhost.py +++ b/tests/foreman/ui/test_discoveredhost.py @@ -194,7 +194,7 @@ def test_positive_auto_provision_host_with_rule( discovered_host.build = True discovered_host_name = discovered_host.name - domain_name = provisioning_hostgroup.domain.name + domain_name = provisioning_hostgroup.domain.read().name host_name = f'{discovered_host_name}.{domain_name}' discovery_rule = sat.api.DiscoveryRule( From 64b97841a248b0b934db269a6f3ac57b8a061353 Mon Sep 17 00:00:00 2001 From: Jake Callahan Date: Wed, 10 Jan 2024 10:23:23 -0500 Subject: [PATCH 426/586] [6.14.z] Add Ruff rule group B (flake8-bugbear) (#13695) * Add Ruff rule group B (flake8-bugbear) This change includes the rule addition and mostly manual changes to existing code in order to bring it in line with the new rules and avoid as many ignores as is reasonable. I added a number of {{from err}} statements to the end of exceptions that currently include the error message. The error message being included in the raised string can be removed later, but keeping for consistency for now. In cases where I raised from None, the raised exception is clear enough that the additional context of the parent exception isn't helpful. cli/test\_ping.py has a zip that should probably be strict. * update type check fix type check from list to str Co-authored-by: Gaurav Talreja --------- Co-authored-by: Gaurav Talreja --- pyproject.toml | 1 + robottelo/cli/base.py | 10 +-- robottelo/cli/hammer.py | 2 +- robottelo/host_helpers/api_factory.py | 10 +-- robottelo/host_helpers/capsule_mixins.py | 4 +- robottelo/host_helpers/cli_factory.py | 70 ++++++++++++------- robottelo/host_helpers/repository_mixins.py | 2 +- robottelo/host_helpers/satellite_mixins.py | 16 ++--- robottelo/host_helpers/ui_factory.py | 4 +- robottelo/hosts.py | 25 ++++--- robottelo/utils/decorators/func_locker.py | 4 +- .../utils/decorators/func_shared/shared.py | 3 +- robottelo/utils/ohsnap.py | 4 +- robottelo/utils/virtwho.py | 24 +++---- tests/foreman/api/test_acs.py | 2 +- tests/foreman/api/test_ansible.py | 8 ++- tests/foreman/api/test_discoveredhost.py | 32 +++++---- tests/foreman/api/test_host.py | 2 +- tests/foreman/api/test_media.py | 4 +- tests/foreman/api/test_notifications.py | 4 +- tests/foreman/api/test_organization.py | 4 +- tests/foreman/api/test_partitiontable.py | 4 +- tests/foreman/api/test_provisioning.py | 4 +- tests/foreman/api/test_repository.py | 38 ++++++---- tests/foreman/api/test_role.py | 2 +- tests/foreman/api/test_webhook.py | 4 +- tests/foreman/cli/test_errata.py | 4 +- tests/foreman/cli/test_host.py | 4 +- tests/foreman/cli/test_model.py | 4 +- tests/foreman/cli/test_partitiontable.py | 1 + tests/foreman/cli/test_ping.py | 2 +- tests/foreman/cli/test_remoteexecution.py | 8 +-- tests/foreman/cli/test_report.py | 2 +- tests/foreman/cli/test_repository.py | 2 +- tests/foreman/cli/test_role.py | 4 +- .../destructive/test_capsulecontent.py | 2 +- .../destructive/test_discoveredhost.py | 26 +++---- .../destructive/test_ldap_authentication.py | 2 +- .../destructive/test_remoteexecution.py | 4 +- tests/foreman/longrun/test_oscap.py | 8 +-- tests/foreman/maintain/test_advanced.py | 4 +- tests/foreman/maintain/test_health.py | 4 +- .../foreman/ui/test_computeresource_vmware.py | 8 +-- tests/foreman/ui/test_contenthost.py | 10 +-- tests/foreman/ui/test_dashboard.py | 2 +- tests/foreman/ui/test_host.py | 2 +- tests/foreman/ui/test_hostcollection.py | 7 +- tests/foreman/ui/test_sync.py | 2 +- tests/robottelo/test_func_shared.py | 2 +- tests/robottelo/test_report.py | 10 +-- 50 files changed, 233 insertions(+), 178 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7988aa2a58d..61982c48c08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ target-version = "py311" fixable = ["ALL"] select = [ + "B", # bugbear # "C90", # mccabe "E", # pycodestyle "F", # flake8 diff --git a/robottelo/cli/base.py b/robottelo/cli/base.py index 2d3d7974696..2d3e6dbf850 100644 --- a/robottelo/cli/base.py +++ b/robottelo/cli/base.py @@ -169,15 +169,9 @@ def _get_username_password(cls, username=None, password=None): """ if username is None: - try: - username = getattr(cls, 'foreman_admin_username') - except AttributeError: - username = settings.server.admin_username + username = getattr(cls, 'foreman_admin_username', settings.server.admin_username) if password is None: - try: - password = getattr(cls, 'foreman_admin_password') - except AttributeError: - password = settings.server.admin_password + password = getattr(cls, 'foreman_admin_password', settings.server.admin_password) return (username, password) diff --git a/robottelo/cli/hammer.py b/robottelo/cli/hammer.py index f6ddb11b4f3..661042050e6 100644 --- a/robottelo/cli/hammer.py +++ b/robottelo/cli/hammer.py @@ -42,7 +42,7 @@ def parse_csv(output): # Generate the key names, spaces will be converted to dashes "-" keys = [_normalize(header) for header in next(reader)] # For each entry, create a dict mapping each key with each value - return [dict(zip(keys, values)) for values in reader if len(values) > 0] + return [dict(zip(keys, values, strict=True)) for values in reader if len(values) > 0] def parse_help(output): diff --git a/robottelo/host_helpers/api_factory.py b/robottelo/host_helpers/api_factory.py index 92f40652d34..0fff2579352 100644 --- a/robottelo/host_helpers/api_factory.py +++ b/robottelo/host_helpers/api_factory.py @@ -468,14 +468,14 @@ def create_role_permissions( if entity_permission.name != name: raise self._satellite.api.APIResponseError( 'the returned permission is different from the' - ' requested one "{} != {}"'.format(entity_permission.name, name) + f' requested one "{entity_permission.name} != {name}"' ) permissions_entities.append(entity_permission) else: if not permissions_name: raise ValueError( - 'resource type "{}" empty. You must select at' - ' least one permission'.format(resource_type) + f'resource type "{resource_type}" empty. You must select at' + ' least one permission' ) resource_type_permissions_entities = self._satellite.api.Permission().search( @@ -575,8 +575,8 @@ def satellite_setting(self, key_val: str): setting = self._satellite.api.Setting().search( query={'search': f'name={name.strip()}'} )[0] - except IndexError: - raise KeyError(f'The setting {name} in not available in satellite.') + except IndexError as err: + raise KeyError(f'The setting {name} in not available in satellite.') from err old_value = setting.value setting.value = value.strip() setting.update({'value'}) diff --git a/robottelo/host_helpers/capsule_mixins.py b/robottelo/host_helpers/capsule_mixins.py index 712bf98f533..0589b5f15a3 100644 --- a/robottelo/host_helpers/capsule_mixins.py +++ b/robottelo/host_helpers/capsule_mixins.py @@ -61,10 +61,12 @@ def wait_for_tasks( raise AssertionError(f"No task was found using query '{search_query}'") return tasks - def wait_for_sync(self, timeout=600, start_time=datetime.utcnow()): + def wait_for_sync(self, timeout=600, start_time=None): """Wait for capsule sync to finish and assert the sync task succeeded""" # Assert that a task to sync lifecycle environment to the capsule # is started (or finished already) + if start_time is None: + start_time = datetime.utcnow() logger.info(f"Waiting for capsule {self.hostname} sync to finish ...") sync_status = self.nailgun_capsule.content_get_sync() logger.info(f"Active tasks {sync_status['active_sync_tasks']}") diff --git a/robottelo/host_helpers/cli_factory.py b/robottelo/host_helpers/cli_factory.py index 45ea935ab88..575de2256ac 100644 --- a/robottelo/host_helpers/cli_factory.py +++ b/robottelo/host_helpers/cli_factory.py @@ -4,7 +4,7 @@ example: my_satellite.cli_factory.make_org() """ import datetime -from functools import lru_cache, partial +from functools import partial import inspect import os from os import chmod @@ -14,6 +14,7 @@ from time import sleep from box import Box +from cachetools import cachedmethod from fauxfactory import ( gen_alpha, gen_alphanumeric, @@ -59,9 +60,9 @@ def create_object(cli_object, options, values=None, credentials=None): 'Failed to create {} with data:\n{}\n{}'.format( cli_object.__name__, pprint.pformat(options, indent=2), err.msg ) - ) + ) from err # Sometimes we get a list with a dictionary and not a dictionary. - if type(result) is list and len(result) > 0: + if isinstance(result, list) and len(result) > 0: result = result[0] return Box(result) @@ -294,7 +295,7 @@ def _evaluate_functions(self, iterable): if not key.startswith('_') } - @lru_cache + @cachedmethod def _find_entity_class(self, entity_name): entity_name = entity_name.replace('_', '').lower() for name, class_obj in self._satellite.cli.__dict__.items(): @@ -394,8 +395,8 @@ def make_product_wait(self, options=None, wait_for=5): product = self._satellite.cli.Product.info( {'name': options.get('name'), 'organization-id': options.get('organization-id')} ) - except CLIReturnCodeError: - raise err + except CLIReturnCodeError as nested_err: + raise nested_err from err if not product: raise err return product @@ -503,7 +504,7 @@ def make_proxy(self, options=None): args['url'] = url return create_object(self._satellite.cli.Proxy, args, options) except CapsuleTunnelError as err: - raise CLIFactoryError(f'Failed to create ssh tunnel: {err}') + raise CLIFactoryError(f'Failed to create ssh tunnel: {err}') from None args['url'] = options['url'] return create_object(self._satellite.cli.Proxy, args, options) @@ -569,7 +570,7 @@ def activationkey_add_subscription_to_repo(self, options=None): except CLIReturnCodeError as err: raise CLIFactoryError( f'Failed to add subscription to activation key\n{err.msg}' - ) + ) from err def setup_org_for_a_custom_repo(self, options=None): """Sets up Org for the given custom repo by: @@ -608,7 +609,7 @@ def setup_org_for_a_custom_repo(self, options=None): try: self._satellite.cli.Repository.synchronize({'id': custom_repo['id']}) except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to synchronize repository\n{err.msg}') + raise CLIFactoryError(f'Failed to synchronize repository\n{err.msg}') from err # Create CV if needed and associate repo with it if options.get('content-view-id') is None: cv_id = self.make_content_view({'organization-id': org_id})['id'] @@ -619,12 +620,14 @@ def setup_org_for_a_custom_repo(self, options=None): {'id': cv_id, 'organization-id': org_id, 'repository-id': custom_repo['id']} ) except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to add repository to content view\n{err.msg}') + raise CLIFactoryError(f'Failed to add repository to content view\n{err.msg}') from err # Publish a new version of CV try: self._satellite.cli.ContentView.publish({'id': cv_id}) except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to publish new version of content view\n{err.msg}') + raise CLIFactoryError( + f'Failed to publish new version of content view\n{err.msg}' + ) from err # Get the version id cv_info = self._satellite.cli.ContentView.info({'id': cv_id}) lce_promoted = cv_info['lifecycle-environments'] @@ -640,7 +643,9 @@ def setup_org_for_a_custom_repo(self, options=None): } ) except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to promote version to next environment\n{err.msg}') + raise CLIFactoryError( + f'Failed to promote version to next environment\n{err.msg}' + ) from err # Create activation key if needed and associate content view with it if options.get('activationkey-id') is None: activationkey_id = self.make_activation_key( @@ -659,7 +664,9 @@ def setup_org_for_a_custom_repo(self, options=None): {'content-view-id': cv_id, 'id': activationkey_id, 'organization-id': org_id} ) except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to associate activation-key with CV\n{err.msg}') + raise CLIFactoryError( + f'Failed to associate activation-key with CV\n{err.msg}' + ) from err # Add custom_product subscription to activation-key, if SCA mode is disabled if self._satellite.is_sca_mode_enabled(org_id) is False: @@ -718,10 +725,13 @@ def _setup_org_for_a_rh_repo(self, options=None): # If manifest does not exist, clone and upload it if len(self._satellite.cli.Subscription.exists({'organization-id': org_id})) == 0: with clone() as manifest: - try: - self._satellite.upload_manifest(org_id, manifest.content) - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to upload manifest\n{err.msg}') + self._satellite.put(manifest.path, manifest.name) + try: + self._satellite.cli.Subscription.upload( + {'file': manifest.name, 'organization-id': org_id} + ) + except CLIReturnCodeError as err: + raise CLIFactoryError(f'Failed to upload manifest\n{err.msg}') from err # Enable repo from Repository Set try: self._satellite.cli.RepositorySet.enable( @@ -734,7 +744,7 @@ def _setup_org_for_a_rh_repo(self, options=None): } ) except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to enable repository set\n{err.msg}') + raise CLIFactoryError(f'Failed to enable repository set\n{err.msg}') from err # Fetch repository info try: rhel_repo = self._satellite.cli.Repository.info( @@ -745,7 +755,7 @@ def _setup_org_for_a_rh_repo(self, options=None): } ) except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to fetch repository info\n{err.msg}') + raise CLIFactoryError(f'Failed to fetch repository info\n{err.msg}') from err # Synchronize the RH repository try: self._satellite.cli.Repository.synchronize( @@ -756,7 +766,7 @@ def _setup_org_for_a_rh_repo(self, options=None): } ) except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to synchronize repository\n{err.msg}') + raise CLIFactoryError(f'Failed to synchronize repository\n{err.msg}') from err # Create CV if needed and associate repo with it if options.get('content-view-id') is None: cv_id = self.make_content_view({'organization-id': org_id})['id'] @@ -767,24 +777,28 @@ def _setup_org_for_a_rh_repo(self, options=None): {'id': cv_id, 'organization-id': org_id, 'repository-id': rhel_repo['id']} ) except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to add repository to content view\n{err.msg}') + raise CLIFactoryError(f'Failed to add repository to content view\n{err.msg}') from err # Publish a new version of CV try: self._satellite.cli.ContentView.publish({'id': cv_id}) except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to publish new version of content view\n{err.msg}') + raise CLIFactoryError( + f'Failed to publish new version of content view\n{err.msg}' + ) from err # Get the version id try: cvv = self._satellite.cli.ContentView.info({'id': cv_id})['versions'][-1] except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to fetch content view info\n{err.msg}') + raise CLIFactoryError(f'Failed to fetch content view info\n{err.msg}') from err # Promote version1 to next env try: self._satellite.cli.ContentView.version_promote( {'id': cvv['id'], 'organization-id': org_id, 'to-lifecycle-environment-id': env_id} ) except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to promote version to next environment\n{err.msg}') + raise CLIFactoryError( + f'Failed to promote version to next environment\n{err.msg}' + ) from err # Create activation key if needed and associate content view with it if options.get('activationkey-id') is None: activationkey_id = self.make_activation_key( @@ -803,7 +817,9 @@ def _setup_org_for_a_rh_repo(self, options=None): {'id': activationkey_id, 'organization-id': org_id, 'content-view-id': cv_id} ) except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to associate activation-key with CV\n{err.msg}') + raise CLIFactoryError( + f'Failed to associate activation-key with CV\n{err.msg}' + ) from err # Add default subscription to activation-key, if SCA mode is disabled if self._satellite.is_sca_mode_enabled(org_id) is False: @@ -874,7 +890,7 @@ def setup_org_for_a_rh_repo( } ) except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to upload manifest\n{err.msg}') + raise CLIFactoryError(f'Failed to upload manifest\n{err.msg}') from err # Add default subscription to activation-key, if SCA mode is disabled if self._satellite.is_sca_mode_enabled(result['organization-id']) is False: @@ -1084,7 +1100,7 @@ def setup_cdn_and_custom_repos_content( try: self._satellite.upload_manifest(org_id, interface='CLI') except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to upload manifest\n{err.msg}') + raise CLIFactoryError(f'Failed to upload manifest\n{err.msg}') from err custom_product, repos_info = self.setup_cdn_and_custom_repositories( org_id=org_id, repos=repos, download_policy=download_policy diff --git a/robottelo/host_helpers/repository_mixins.py b/robottelo/host_helpers/repository_mixins.py index 53b83c80b84..6becbe29325 100644 --- a/robottelo/host_helpers/repository_mixins.py +++ b/robottelo/host_helpers/repository_mixins.py @@ -794,7 +794,7 @@ def setup_virtual_machine( patch_os_release_distro = self.os_repo.distro rh_repo_ids = [] if enable_rh_repos: - rh_repo_ids = [getattr(repo, 'rh_repository_id') for repo in self.rh_repos] + rh_repo_ids = [repo.rh_repository_id for repo in self.rh_repos] repo_labels = [] if enable_custom_repos: repo_labels = [ diff --git a/robottelo/host_helpers/satellite_mixins.py b/robottelo/host_helpers/satellite_mixins.py index 751e9607ffc..6c47228c147 100644 --- a/robottelo/host_helpers/satellite_mixins.py +++ b/robottelo/host_helpers/satellite_mixins.py @@ -1,10 +1,10 @@ import contextlib -from functools import cache import io import os import random import re +from cachetools import cachedmethod import requests from robottelo.cli.proxy import CapsuleTunnelError @@ -188,7 +188,7 @@ def publish_content_view(self, org, repo_list): :returns: A dictionary containing the details of the published content view. """ - repo = repo_list if type(repo_list) is list else [repo_list] + repo = repo_list if isinstance(repo_list, list) else [repo_list] content_view = self.api.ContentView(organization=org, repository=repo).create() content_view.publish() content_view = content_view.read() @@ -235,9 +235,9 @@ def available_capsule_port(self): :rtype: int """ port_pool_range = settings.fake_capsules.port_range - if type(port_pool_range) is str: + if isinstance(port_pool_range, str): port_pool_range = tuple(port_pool_range.split('-')) - if type(port_pool_range) is tuple and len(port_pool_range) == 2: + if isinstance(port_pool_range, tuple) and len(port_pool_range) == 2: port_pool = range(int(port_pool_range[0]), int(port_pool_range[1])) else: raise TypeError( @@ -263,14 +263,14 @@ def available_capsule_port(self): except ValueError: raise CapsuleTunnelError( f'Failed parsing the port numbers from stdout: {ss_cmd.stdout.splitlines()[:-1]}' - ) + ) from None try: # take the list of available ports and return randomly selected one return random.choice([port for port in port_pool if port not in used_ports]) except IndexError: raise CapsuleTunnelError( 'Failed to create ssh tunnel: No more ports available for mapping' - ) + ) from None @contextlib.contextmanager def default_url_on_new_port(self, oldport, newport): @@ -308,7 +308,7 @@ def validate_pulp_filepath( self, org, dir_path, - file_names=['*.json', '*.tar.gz'], + file_names=('*.json', '*.tar.gz'), ): """Checks the existence of certain files in a pulp dir""" extension_query = ' -o '.join([f'-name "{file}"' for file in file_names]) @@ -358,6 +358,6 @@ def api_factory(self): self._api_factory = APIFactory(self) return self._api_factory - @cache + @cachedmethod def ui_factory(self, session): return UIFactory(self, session=session) diff --git a/robottelo/host_helpers/ui_factory.py b/robottelo/host_helpers/ui_factory.py index cb85436051e..df156ad6d6f 100644 --- a/robottelo/host_helpers/ui_factory.py +++ b/robottelo/host_helpers/ui_factory.py @@ -18,12 +18,14 @@ def __init__(self, satellite, session=None): def create_fake_host( self, host, - interface_id=gen_string('alpha'), + interface_id=None, global_parameters=None, host_parameters=None, extra_values=None, new_host_details=False, ): + if interface_id is None: + interface_id = gen_string('alpha') if extra_values is None: extra_values = {} os_name = f'{host.operatingsystem.name} {host.operatingsystem.major}' diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 425155a8879..37dab609a2e 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -16,6 +16,7 @@ from box import Box from broker import Broker from broker.hosts import Host +from cachetools import cachedmethod from dynaconf.vendor.box.exceptions import BoxKeyError from fauxfactory import gen_alpha, gen_string from manifester import Manifester @@ -413,11 +414,11 @@ def power_control(self, state=VmState.RUNNING, ensure=True): try: vm_operation = POWER_OPERATIONS.get(state) workflow_name = settings.broker.host_workflows.power_control - except (AttributeError, KeyError): + except (AttributeError, KeyError) as err: raise NotImplementedError( 'No workflow in broker.host_workflows for power control, ' 'or VM operation not supported' - ) + ) from err assert ( # TODO read the kwarg name from settings too? Broker() @@ -524,10 +525,12 @@ def subscription_manager_status(self): def subscription_manager_list(self): return self.execute('subscription-manager list') - def subscription_manager_get_pool(self, sub_list=[]): + def subscription_manager_get_pool(self, sub_list=None): """ Return pool ids for the corresponding subscriptions in the list """ + if sub_list is None: + sub_list = [] pool_ids = [] for sub in sub_list: result = self.execute( @@ -539,10 +542,12 @@ def subscription_manager_get_pool(self, sub_list=[]): pool_ids.append(result) return pool_ids - def subscription_manager_attach_pool(self, pool_list=[]): + def subscription_manager_attach_pool(self, pool_list=None): """ Attach pool ids to the host and return the result """ + if pool_list is None: + pool_list = [] result = [] for pool in pool_list: result.append(self.execute(f'subscription-manager attach --pool={pool}')) @@ -615,8 +620,8 @@ def install_katello_agent(self): # We're in a traditional VM, so goferd should be running after katello-agent install try: wait_for(lambda: self.execute('service goferd status').status == 0) - except TimedOutError: - raise ContentHostError('katello-agent is not running') + except TimedOutError as err: + raise ContentHostError('katello-agent is not running') from err def install_katello_host_tools(self): """Installs Katello host tools on the broker virtual machine @@ -1461,8 +1466,10 @@ def install_tracer(self): raise ContentHostError('There was an error installing katello-host-tools-tracer') self.execute('katello-tracer-upload') - def register_to_cdn(self, pool_ids=[settings.subscription.rhn_poolid]): + def register_to_cdn(self, pool_ids=None): """Subscribe satellite to CDN""" + if pool_ids is None: + pool_ids = [settings.subscription.rhn_poolid] self.remove_katello_ca() cmd_result = self.register_contenthost( org=None, @@ -2289,7 +2296,7 @@ def get_rhsso_client_id(self): break return client_id - @lru_cache + @cachedmethod def get_rhsso_user_details(self, username): """Getter method to receive the user id""" result = self.execute( @@ -2298,7 +2305,7 @@ def get_rhsso_user_details(self, username): result_json = json.loads(result.stdout) return result_json[0] - @lru_cache + @cachedmethod def get_rhsso_groups_details(self, group_name): """Getter method to receive the group id""" result = self.execute(f"{KEY_CLOAK_CLI} get groups -r {settings.rhsso.realm}") diff --git a/robottelo/utils/decorators/func_locker.py b/robottelo/utils/decorators/func_locker.py index 2d575d8f560..4d82c3add28 100644 --- a/robottelo/utils/decorators/func_locker.py +++ b/robottelo/utils/decorators/func_locker.py @@ -227,8 +227,8 @@ def lock_function( def main_wrapper(func): - setattr(func, '__class_name__', class_name) - setattr(func, '__function_locked__', True) + func.__class_name__ = class_name + func.__function_locked__ = True @functools.wraps(func) def function_wrapper(*args, **kwargs): diff --git a/robottelo/utils/decorators/func_shared/shared.py b/robottelo/utils/decorators/func_shared/shared.py index 62e529b8d8a..072ebc3fb47 100644 --- a/robottelo/utils/decorators/func_shared/shared.py +++ b/robottelo/utils/decorators/func_shared/shared.py @@ -392,8 +392,7 @@ def __call__(self): # if was not able to restore the original exception raise this one raise SharedFunctionException( - 'Error generated by process: {} Exception: {}' - ' error: {}'.format(pid, error_class_name, error) + f'Error generated by process: {pid} Exception: {error_class_name} error: {error}' ) if not call_function and self._inject: diff --git a/robottelo/utils/ohsnap.py b/robottelo/utils/ohsnap.py index 74494766114..89eb6a97e22 100644 --- a/robottelo/utils/ohsnap.py +++ b/robottelo/utils/ohsnap.py @@ -90,7 +90,9 @@ def dogfood_repository( try: repository = next(r for r in res.json() if r['label'] == repo) except StopIteration: - raise RepositoryDataNotFound(f'Repository "{repo}" is not provided by the given product') + raise RepositoryDataNotFound( + f'Repository "{repo}" is not provided by the given product' + ) from None repository['baseurl'] = repository['baseurl'].replace('$basearch', arch) # If repo check is enabled, check that the repository actually exists on the remote server dogfood_req = requests.get(repository['baseurl']) diff --git a/robottelo/utils/virtwho.py b/robottelo/utils/virtwho.py index 8c7a6c48658..aaae57d7c97 100644 --- a/robottelo/utils/virtwho.py +++ b/robottelo/utils/virtwho.py @@ -301,9 +301,9 @@ def get_hypervisor_ahv_mapping(hypervisor_type): # Always check the last json section to get the host_uuid for item in mapping: if 'entities' in item: - for item in item['entities']: - if 'host_uuid' in item: - system_uuid = item['host_uuid'] + for _item in item['entities']: + if 'host_uuid' in _item: + system_uuid = _item['host_uuid'] break message = f"Host UUID {system_uuid} found for VM: {guest_uuid}" for line in logs.split('\n'): @@ -384,8 +384,8 @@ def deploy_configure_by_command_check(command): virtwho_cleanup() try: ret, stdout = runcmd(command) - except Exception: - raise VirtWhoError(f"Failed to deploy configure by {command}") + except Exception as err: + raise VirtWhoError(f"Failed to deploy configure by {command}") from err else: if ret != 0 or 'Finished successfully' not in stdout: raise VirtWhoError(f"Failed to deploy configure by {command}") @@ -410,7 +410,7 @@ def update_configure_option(option, value, config_file): :param value: set the option to the value :param config_file: path of virt-who config file """ - cmd = 'sed -i "s|^{0}.*|{0}={1}|g" {2}'.format(option, value, config_file) + cmd = f'sed -i "s|^{option}.*|{option}={value}|g" {config_file}' ret, output = runcmd(cmd) if ret != 0: raise VirtWhoError(f"Failed to set option {option} value to {value}") @@ -422,7 +422,7 @@ def delete_configure_option(option, config_file): :param option: the option you want to delete :param config_file: path of virt-who config file """ - cmd = 'sed -i "/^{0}/d" {1}; sed -i "/^#{0}/d" {1}'.format(option, config_file) + cmd = f'sed -i "/^{option}/d" {config_file}; sed -i "/^#{option}/d" {config_file}' ret, output = runcmd(cmd) if ret != 0: raise VirtWhoError(f"Failed to delete option {option}") @@ -437,11 +437,11 @@ def add_configure_option(option, value, config_file): """ try: get_configure_option(option, config_file) - except Exception: + except Exception as err: cmd = f'echo -e "\n{option}={value}" >> {config_file}' - ret, output = runcmd(cmd) + ret, _ = runcmd(cmd) if ret != 0: - raise VirtWhoError(f"Failed to add option {option}={value}") + raise VirtWhoError(f"Failed to add option {option}={value}") from err else: raise VirtWhoError(f"option {option} is already exist in {config_file}") @@ -456,9 +456,9 @@ def hypervisor_json_create(hypervisors, guests): :param guests: how many guests will be created """ hypervisors_list = [] - for i in range(hypervisors): + for _ in range(hypervisors): guest_list = [] - for c in range(guests): + for _ in range(guests): guest_list.append( { "guestId": str(uuid.uuid4()), diff --git a/tests/foreman/api/test_acs.py b/tests/foreman/api/test_acs.py index bb4cfaf125e..bdca5243454 100644 --- a/tests/foreman/api/test_acs.py +++ b/tests/foreman/api/test_acs.py @@ -130,7 +130,7 @@ def test_positive_run_bulk_actions(module_target_sat, module_yum_repo): """ acs_ids = [] - for i in range(3): + for _ in range(3): acs = module_target_sat.api.AlternateContentSource( name=gen_string('alpha'), alternate_content_source_type='simplified', diff --git a/tests/foreman/api/test_ansible.py b/tests/foreman/api/test_ansible.py index a842ec5c3ca..1f143e7320d 100644 --- a/tests/foreman/api/test_ansible.py +++ b/tests/foreman/api/test_ansible.py @@ -245,10 +245,14 @@ def test_add_and_remove_ansible_role_hostgroup(target_sat): for role in ROLE_NAMES ] target_sat.api.HostGroup(id=hg.id).assign_ansible_roles(data={'ansible_role_ids': ROLES[:2]}) - for r1, r2 in zip(target_sat.api.HostGroup(id=hg.id).list_ansible_roles(), ROLE_NAMES[:2]): + for r1, r2 in zip( + target_sat.api.HostGroup(id=hg.id).list_ansible_roles(), ROLE_NAMES[:2], strict=True + ): assert r1['name'] == r2 target_sat.api.HostGroup(id=hg.id).add_ansible_role(data={'ansible_role_id': ROLES[2]}) - for r1, r2 in zip(target_sat.api.HostGroup(id=hg.id).list_ansible_roles(), ROLE_NAMES): + for r1, r2 in zip( + target_sat.api.HostGroup(id=hg.id).list_ansible_roles(), ROLE_NAMES, strict=True + ): assert r1['name'] == r2 for role in ROLES: diff --git a/tests/foreman/api/test_discoveredhost.py b/tests/foreman/api/test_discoveredhost.py index 7d260b3d5e8..948c68e0549 100644 --- a/tests/foreman/api/test_discoveredhost.py +++ b/tests/foreman/api/test_discoveredhost.py @@ -72,9 +72,9 @@ def _assert_discovered_host(host, channel=None, user_config=None): ]: try: dhcp_pxe = _wait_for_log(channel, pattern[0], timeout=10) - except TimedOutError: + except TimedOutError as err: # raise assertion error - raise AssertionError(f'Timed out waiting for {pattern[1]} from VM') + raise AssertionError(f'Timed out waiting for {pattern[1]} from VM') from err groups = re.search('DHCPACK on (\\d.+) to', dhcp_pxe.out) assert len(groups.groups()) == 1, 'Unable to parse bootloader ip address' pxe_ip = groups.groups()[0] @@ -87,9 +87,9 @@ def _assert_discovered_host(host, channel=None, user_config=None): ]: try: _wait_for_log(channel, pattern[0], timeout=20) - except TimedOutError: + except TimedOutError as err: # raise assertion error - raise AssertionError(f'Timed out waiting for VM (tftp) to fetch {pattern[1]}') + raise AssertionError(f'Timed out waiting for VM (tftp) to fetch {pattern[1]}') from err # assert that server receives DHCP discover from FDI for pattern in [ ( @@ -100,9 +100,9 @@ def _assert_discovered_host(host, channel=None, user_config=None): ]: try: dhcp_fdi = _wait_for_log(channel, pattern[0], timeout=30) - except TimedOutError: + except TimedOutError as err: # raise assertion error - raise AssertionError(f'Timed out waiting for {pattern[1]} from VM') + raise AssertionError(f'Timed out waiting for {pattern[1]} from VM') from err groups = re.search('DHCPACK on (\\d.+) to', dhcp_fdi.out) assert len(groups.groups()) == 1, 'Unable to parse FDI ip address' fdi_ip = groups.groups()[0] @@ -114,17 +114,17 @@ def _assert_discovered_host(host, channel=None, user_config=None): f'"/api/v2/discovered_hosts/facts" for {fdi_ip}', timeout=60, ) - except TimedOutError: + except TimedOutError as err: # raise assertion error - raise AssertionError('Timed out waiting for /facts POST request') + raise AssertionError('Timed out waiting for /facts POST request') from err groups = re.search('\\[I\\|app\\|([a-z0-9]+)\\]', facts_fdi.out) assert len(groups.groups()) == 1, 'Unable to parse POST request UUID' req_id = groups.groups()[0] try: _wait_for_log(channel, f'\\[I\\|app\\|{req_id}\\] Completed 201 Created') - except TimedOutError: + except TimedOutError as err: # raise assertion error - raise AssertionError('Timed out waiting for "/facts" 201 response') + raise AssertionError('Timed out waiting for "/facts" 201 response') from err default_config = entity_mixins.DEFAULT_SERVER_CONFIG try: wait_for( @@ -138,8 +138,10 @@ def _assert_discovered_host(host, channel=None, user_config=None): delay=2, logger=logger, ) - except TimedOutError: - raise AssertionError('Timed out waiting for discovered_host to appear on satellite') + except TimedOutError as err: + raise AssertionError( + 'Timed out waiting for discovered_host to appear on satellite' + ) from err discovered_host = host.api.DiscoveredHost(user_config or default_config).search( query={'search': f'name={host.guest_name}'} ) @@ -153,8 +155,8 @@ def assert_discovered_host_provisioned(channel, ksrepo): try: log = _wait_for_log(channel, pattern, timeout=300, delay=10) assert pattern in log - except TimedOutError: - raise AssertionError(f'Timed out waiting for {pattern} from VM') + except TimedOutError as err: + raise AssertionError(f'Timed out waiting for {pattern} from VM') from err @pytest.fixture @@ -434,7 +436,7 @@ def test_positive_reboot_all_pxe_hosts( host.power_control(ensure=False) mac = host._broker_args['provisioning_nic_mac_addr'] wait_for( - lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], + lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], # noqa: B023 timeout=240, delay=20, ) diff --git a/tests/foreman/api/test_host.py b/tests/foreman/api/test_host.py index 15411a4741b..d495d6c7c24 100644 --- a/tests/foreman/api/test_host.py +++ b/tests/foreman/api/test_host.py @@ -998,7 +998,7 @@ def test_positive_read_content_source_id( 'lifecycle_environment_id': module_lce.id, }, ).create() - content_facet_attributes = getattr(host, 'content_facet_attributes') + content_facet_attributes = host.content_facet_attributes assert content_facet_attributes is not None content_source_id = content_facet_attributes.get('content_source_id') assert content_source_id is not None diff --git a/tests/foreman/api/test_media.py b/tests/foreman/api/test_media.py index 8fb0ec5af66..f68ea37bd72 100644 --- a/tests/foreman/api/test_media.py +++ b/tests/foreman/api/test_media.py @@ -41,7 +41,9 @@ def class_media(self, module_org, class_target_sat): @pytest.mark.upgrade @pytest.mark.parametrize( ('name', 'new_name'), - **parametrized(list(zip(valid_data_list().values(), valid_data_list().values()))) + **parametrized( + list(zip(valid_data_list().values(), valid_data_list().values(), strict=True)) + ) ) def test_positive_crud_with_name(self, module_org, name, new_name, module_target_sat): """Create, update, delete media with valid name only diff --git a/tests/foreman/api/test_notifications.py b/tests/foreman/api/test_notifications.py index a8b4c24ebf8..cb0320b3b4b 100644 --- a/tests/foreman/api/test_notifications.py +++ b/tests/foreman/api/test_notifications.py @@ -109,11 +109,11 @@ def wait_for_long_running_task_mail(target_sat, clean_root_mailbox, long_running timeout=timeout, delay=5, ) - except TimedOutError: + except TimedOutError as err: raise AssertionError( f'No notification e-mail with long-running task ID {long_running_task["task"]["id"]} ' f'has arrived to {clean_root_mailbox} after {timeout} seconds.' - ) + ) from err return True diff --git a/tests/foreman/api/test_organization.py b/tests/foreman/api/test_organization.py index 868ec7368b1..c407691804e 100644 --- a/tests/foreman/api/test_organization.py +++ b/tests/foreman/api/test_organization.py @@ -230,7 +230,7 @@ def test_positive_update_name(self, module_org, name): :parametrized: yes """ - setattr(module_org, 'name', name) + module_org.name = name module_org = module_org.update(['name']) assert module_org.name == name @@ -247,7 +247,7 @@ def test_positive_update_description(self, module_org, desc): :parametrized: yes """ - setattr(module_org, 'description', desc) + module_org.description = desc module_org = module_org.update(['description']) assert module_org.description == desc diff --git a/tests/foreman/api/test_partitiontable.py b/tests/foreman/api/test_partitiontable.py index 6d78ba1135b..fd0027cbc40 100644 --- a/tests/foreman/api/test_partitiontable.py +++ b/tests/foreman/api/test_partitiontable.py @@ -66,6 +66,7 @@ def test_positive_create_with_one_character_name(self, target_sat, name): zip( generate_strings_list(length=gen_integer(4, 30)), generate_strings_list(length=gen_integer(4, 30)), + strict=True, ) ) ), @@ -95,7 +96,8 @@ def test_positive_crud_with_name(self, target_sat, name, new_name): @pytest.mark.tier1 @pytest.mark.parametrize( - ('layout', 'new_layout'), **parametrized(list(zip(valid_data_list(), valid_data_list()))) + ('layout', 'new_layout'), + **parametrized(list(zip(valid_data_list(), valid_data_list(), strict=True))), ) def test_positive_create_update_with_layout(self, target_sat, layout, new_layout): """Create new and update partition tables using different inputs as a diff --git a/tests/foreman/api/test_provisioning.py b/tests/foreman/api/test_provisioning.py index cee77929db8..5e8c1ea133b 100644 --- a/tests/foreman/api/test_provisioning.py +++ b/tests/foreman/api/test_provisioning.py @@ -60,8 +60,8 @@ def assert_host_logs(channel, pattern): try: log = _wait_for_log(channel, pattern, timeout=300, delay=10) assert pattern in log - except TimedOutError: - raise AssertionError(f'Timed out waiting for {pattern} from VM') + except TimedOutError as err: + raise AssertionError(f'Timed out waiting for {pattern} from VM') from err @pytest.mark.e2e diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index b93bcf4c736..465e2762e00 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -2609,6 +2609,17 @@ def test_positive_create_with_long_token( container_repo = container_repos[0] except IndexError: pytest.skip('No registries with "long_pass" set to true') + + to_clean = [] + + @request.addfinalizer + def clean_repos(): + for repo in to_clean: + try: + repo.delete(synchronous=False) + except Exception: + logger.exception(f'Exception cleaning up docker repo:\n{repo}') + for docker_repo_name in container_repo.repos_to_sync: repo_options = dict( content_type='docker', @@ -2626,13 +2637,7 @@ def test_positive_create_with_long_token( pytest.skip('The "long_pass" registry does not meet length requirement') repo = module_target_sat.api.Repository(**repo_options).create() - - @request.addfinalizer - def clean_repo(): - try: - repo.delete(synchronous=False) - except Exception: - logger.exception('Exception cleaning up docker repo:') + to_clean.append(repo) repo = repo.read() for field in 'name', 'docker_upstream_name', 'content_type', 'upstream_username': @@ -2659,6 +2664,17 @@ def test_positive_tag_whitelist( :expectedresults: multiple products and repos are created """ container_repo = getattr(settings.container_repo.registries, repo_key) + + to_clean = [] + + @request.addfinalizer + def clean_repos(): + for repo in to_clean: + try: + repo.delete(synchronous=False) + except Exception: + logger.exception(f'Exception cleaning up docker repo:\n{repo}') + for docker_repo_name in container_repo.repos_to_sync: repo_options = dict( content_type='docker', @@ -2673,13 +2689,7 @@ def test_positive_tag_whitelist( repo_options['product'] = module_product repo = module_target_sat.api.Repository(**repo_options).create() - - @request.addfinalizer - def clean_repo(): - try: - repo.delete(synchronous=False) - except Exception: - logger.exception('Exception cleaning up docker repo:') + to_clean.append(repo) for field in 'name', 'docker_upstream_name', 'content_type', 'upstream_username': assert getattr(repo, field) == repo_options[field] diff --git a/tests/foreman/api/test_role.py b/tests/foreman/api/test_role.py index da880c71f59..371e9652084 100644 --- a/tests/foreman/api/test_role.py +++ b/tests/foreman/api/test_role.py @@ -37,7 +37,7 @@ class TestRole: @pytest.mark.upgrade @pytest.mark.parametrize( ('name', 'new_name'), - **parametrized(list(zip(generate_strings_list(), generate_strings_list()))), + **parametrized(list(zip(generate_strings_list(), generate_strings_list(), strict=True))), ) def test_positive_crud(self, name, new_name, target_sat): """Create, update and delete role with name ``name_generator()``. diff --git a/tests/foreman/api/test_webhook.py b/tests/foreman/api/test_webhook.py index 77d8784ca5e..7b253e2b714 100644 --- a/tests/foreman/api/test_webhook.py +++ b/tests/foreman/api/test_webhook.py @@ -61,8 +61,8 @@ def assert_event_triggered(channel, event): try: log = _wait_for_log(channel, pattern) assert pattern in log - except TimedOutError: - raise AssertionError(f'Timed out waiting for {pattern} from VM') + except TimedOutError as err: + raise AssertionError(f'Timed out waiting for {pattern} from VM') from err class TestWebhook: diff --git a/tests/foreman/cli/test_errata.py b/tests/foreman/cli/test_errata.py index d707891481f..5fab1951910 100644 --- a/tests/foreman/cli/test_errata.py +++ b/tests/foreman/cli/test_errata.py @@ -114,7 +114,7 @@ def products_with_repos(orgs, module_target_sat): """Create and return a list of products. For each product, create and sync a single repo.""" products = [] # Create one product for each org, and a second product for the last org. - for org, params in zip(orgs + orgs[-1:], REPOS_WITH_ERRATA): + for org, params in zip(orgs + orgs[-1:], REPOS_WITH_ERRATA, strict=True): product = module_target_sat.api.Product(organization=org).create() # Replace the organization entity returned by create(), which contains only the id, # with the one we already have. @@ -341,7 +341,7 @@ def check_errata(errata_ids, by_org=False): :param errata_ids: a list containing a list of errata ids for each repo :type errata_ids: list[list] """ - for ids, repo_with_errata in zip(errata_ids, REPOS_WITH_ERRATA): + for ids, repo_with_errata in zip(errata_ids, REPOS_WITH_ERRATA, strict=True): assert len(ids) == repo_with_errata['org_errata_count' if by_org else 'errata_count'] assert repo_with_errata['errata_id'] in ids diff --git a/tests/foreman/cli/test_host.py b/tests/foreman/cli/test_host.py index 14d0e023c30..139df1026b6 100644 --- a/tests/foreman/cli/test_host.py +++ b/tests/foreman/cli/test_host.py @@ -1762,11 +1762,11 @@ def test_positive_erratum_applicability( timeout=300, delay=5, ) - except TimedOutError: + except TimedOutError as err: raise TimedOutError( f"Timed out waiting for erratum \"{setup_custom_repo['security_errata']}\"" " to disappear from the list" - ) + ) from err @pytest.mark.cli_katello_host_tools diff --git a/tests/foreman/cli/test_model.py b/tests/foreman/cli/test_model.py index 0dfc2cb3ba1..a627a3158b7 100644 --- a/tests/foreman/cli/test_model.py +++ b/tests/foreman/cli/test_model.py @@ -40,7 +40,9 @@ def class_model(self, target_sat): @pytest.mark.upgrade @pytest.mark.parametrize( ('name', 'new_name'), - **parametrized(list(zip(valid_data_list().values(), valid_data_list().values()))) + **parametrized( + list(zip(valid_data_list().values(), valid_data_list().values(), strict=True)) + ) ) def test_positive_crud_with_name(self, name, new_name, module_target_sat): """Successfully creates, updates and deletes a Model. diff --git a/tests/foreman/cli/test_partitiontable.py b/tests/foreman/cli/test_partitiontable.py index fe863cfd42e..d32bd11bf03 100644 --- a/tests/foreman/cli/test_partitiontable.py +++ b/tests/foreman/cli/test_partitiontable.py @@ -55,6 +55,7 @@ def test_positive_create_with_one_character_name(self, name, target_sat): zip( generate_strings_list(length=randint(4, 30)), generate_strings_list(length=randint(4, 30)), + strict=True, ) ) ) diff --git a/tests/foreman/cli/test_ping.py b/tests/foreman/cli/test_ping.py index 3a59698fb82..68f6024372b 100644 --- a/tests/foreman/cli/test_ping.py +++ b/tests/foreman/cli/test_ping.py @@ -52,7 +52,7 @@ def test_positive_ping(target_sat, switch_user): # iterate over the lines grouping every 3 lines # example [1, 2, 3, 4, 5, 6] will return [(1, 2, 3), (4, 5, 6)] # only the status line is relevant for this test - for _, status, _ in zip(*[iter(result.stdout)] * 3): + for _, status, _ in zip(*[iter(result.stdout)] * 3, strict=False): # should this be strict? status_count += 1 if status.split(':')[1].strip().lower() == 'ok': diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index 840bdd0153e..29810a3df19 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -64,7 +64,7 @@ def assert_job_invocation_result( result = sat.cli.JobInvocation.info({'id': invocation_command_id}) try: assert result[expected_result] == '1' - except AssertionError: + except AssertionError as err: raise AssertionError( 'host output: {}'.format( ' '.join( @@ -73,7 +73,7 @@ def assert_job_invocation_result( ) ) ) - ) + ) from err def assert_job_invocation_status(sat, invocation_command_id, client_hostname, status): @@ -82,7 +82,7 @@ def assert_job_invocation_status(sat, invocation_command_id, client_hostname, st result = sat.cli.JobInvocation.info({'id': invocation_command_id}) try: assert result['status'] == status - except AssertionError: + except AssertionError as err: raise AssertionError( 'host output: {}'.format( ' '.join( @@ -91,7 +91,7 @@ def assert_job_invocation_status(sat, invocation_command_id, client_hostname, st ) ) ) - ) + ) from err class TestRemoteExecution: diff --git a/tests/foreman/cli/test_report.py b/tests/foreman/cli/test_report.py index 94946b88e66..34bae335913 100644 --- a/tests/foreman/cli/test_report.py +++ b/tests/foreman/cli/test_report.py @@ -84,7 +84,7 @@ def test_positive_install_configure_host( :BZ: 2126891, 2026239 """ puppet_infra_host = [session_puppet_enabled_sat, session_puppet_enabled_capsule] - for client, puppet_proxy in zip(content_hosts, puppet_infra_host): + for client, puppet_proxy in zip(content_hosts, puppet_infra_host, strict=True): client.configure_puppet(proxy_hostname=puppet_proxy.hostname) report = session_puppet_enabled_sat.cli.ConfigReport.list( {'search': f'host~{client.hostname},origin=Puppet'} diff --git a/tests/foreman/cli/test_repository.py b/tests/foreman/cli/test_repository.py index 1ca398b9ff4..4eb7ac31c5d 100644 --- a/tests/foreman/cli/test_repository.py +++ b/tests/foreman/cli/test_repository.py @@ -270,7 +270,7 @@ def test_positive_create_with_auth_yum_repo(self, repo_options, repo): for key in 'url', 'content-type': assert repo.get(key) == repo_options[key] repo = entities.Repository(id=repo['id']).read() - assert getattr(repo, 'upstream_username') == repo_options['upstream-username'] + assert repo.upstream_username == repo_options['upstream-username'] @pytest.mark.tier1 @pytest.mark.upgrade diff --git a/tests/foreman/cli/test_role.py b/tests/foreman/cli/test_role.py index 11474b42f1e..b3cd83c6d1f 100644 --- a/tests/foreman/cli/test_role.py +++ b/tests/foreman/cli/test_role.py @@ -35,7 +35,9 @@ class TestRole: @pytest.mark.parametrize( ('name', 'new_name'), **parametrized( - list(zip(generate_strings_list(length=10), generate_strings_list(length=10))) + list( + zip(generate_strings_list(length=10), generate_strings_list(length=10), strict=True) + ) ), ) def test_positive_crud_with_name(self, name, new_name, module_target_sat): diff --git a/tests/foreman/destructive/test_capsulecontent.py b/tests/foreman/destructive/test_capsulecontent.py index f7a6618ad3e..9def4e5c46b 100644 --- a/tests/foreman/destructive/test_capsulecontent.py +++ b/tests/foreman/destructive/test_capsulecontent.py @@ -71,7 +71,7 @@ def test_positive_sync_without_deadlock( cv = target_sat.publish_content_view(function_entitlement_manifest_org, repo) - for i in range(4): + for _ in range(4): copy_id = target_sat.api.ContentView(id=cv.id).copy(data={'name': gen_alpha()})['id'] copy_cv = target_sat.api.ContentView(id=copy_id).read() copy_cv.publish() diff --git a/tests/foreman/destructive/test_discoveredhost.py b/tests/foreman/destructive/test_discoveredhost.py index 4466747f2ed..cff2fac1f8f 100644 --- a/tests/foreman/destructive/test_discoveredhost.py +++ b/tests/foreman/destructive/test_discoveredhost.py @@ -70,9 +70,9 @@ def _assert_discovered_host(host, channel=None, user_config=None, sat=None): ]: try: dhcp_pxe = _wait_for_log(channel, pattern[0], timeout=10) - except TimedOutError: + except TimedOutError as err: # raise assertion error - raise AssertionError(f'Timed out waiting for {pattern[1]} from VM') + raise AssertionError(f'Timed out waiting for {pattern[1]} from VM') from err groups = re.search('DHCPACK on (\\d.+) to', dhcp_pxe.out) assert len(groups.groups()) == 1, 'Unable to parse bootloader ip address' @@ -87,9 +87,9 @@ def _assert_discovered_host(host, channel=None, user_config=None, sat=None): ]: try: _wait_for_log(channel, pattern[0], timeout=20) - except TimedOutError: + except TimedOutError as err: # raise assertion error - raise AssertionError(f'Timed out waiting for VM (tftp) to fetch {pattern[1]}') + raise AssertionError(f'Timed out waiting for VM (tftp) to fetch {pattern[1]}') from err # assert that server receives DHCP discover from FDI for pattern in [ @@ -101,9 +101,9 @@ def _assert_discovered_host(host, channel=None, user_config=None, sat=None): ]: try: dhcp_fdi = _wait_for_log(channel, pattern[0], timeout=30) - except TimedOutError: + except TimedOutError as err: # raise assertion error - raise AssertionError(f'Timed out waiting for {pattern[1]} from VM') + raise AssertionError(f'Timed out waiting for {pattern[1]} from VM') from err groups = re.search('DHCPACK on (\\d.+) to', dhcp_fdi.out) assert len(groups.groups()) == 1, 'Unable to parse FDI ip address' fdi_ip = groups.groups()[0] @@ -116,18 +116,18 @@ def _assert_discovered_host(host, channel=None, user_config=None, sat=None): f'"/api/v2/discovered_hosts/facts" for {fdi_ip}', timeout=60, ) - except TimedOutError: + except TimedOutError as err: # raise assertion error - raise AssertionError('Timed out waiting for /facts POST request') + raise AssertionError('Timed out waiting for /facts POST request') from err groups = re.search('\\[I\\|app\\|([a-z0-9]+)\\]', facts_fdi.out) assert len(groups.groups()) == 1, 'Unable to parse POST request UUID' req_id = groups.groups()[0] try: _wait_for_log(channel, f'\\[I\\|app\\|{req_id}\\] Completed 201 Created') - except TimedOutError: + except TimedOutError as err: # raise assertion error - raise AssertionError('Timed out waiting for "/facts" 201 response') + raise AssertionError('Timed out waiting for "/facts" 201 response') from err default_config = entity_mixins.DEFAULT_SERVER_CONFIG @@ -143,8 +143,10 @@ def _assert_discovered_host(host, channel=None, user_config=None, sat=None): delay=2, logger=logger, ) - except TimedOutError: - raise AssertionError('Timed out waiting for discovered_host to appear on satellite') + except TimedOutError as err: + raise AssertionError( + 'Timed out waiting for discovered_host to appear on satellite' + ) from err discovered_host = sat.api.DiscoveredHost(user_config or default_config).search( query={'search': f'name={host.guest_name}'} ) diff --git a/tests/foreman/destructive/test_ldap_authentication.py b/tests/foreman/destructive/test_ldap_authentication.py index bc35a10bd83..9ce3cc07261 100644 --- a/tests/foreman/destructive/test_ldap_authentication.py +++ b/tests/foreman/destructive/test_ldap_authentication.py @@ -548,7 +548,7 @@ def test_user_permissions_rhsso_user_multiple_group( group_names = ['sat_users', 'sat_admins'] arguments = [{'roles': katello_role.name}, {'admin': 1}] external_auth_source = module_target_sat.cli.ExternalAuthSource.info({'name': "External"}) - for group_name, argument in zip(group_names, arguments): + for group_name, argument in zip(group_names, arguments, strict=True): # adding/creating rhsso groups default_sso_host.create_group(group_name=group_name) default_sso_host.update_rhsso_user(username, group_name=group_name) diff --git a/tests/foreman/destructive/test_remoteexecution.py b/tests/foreman/destructive/test_remoteexecution.py index 23cee12cff1..6bbb3853cdb 100644 --- a/tests/foreman/destructive/test_remoteexecution.py +++ b/tests/foreman/destructive/test_remoteexecution.py @@ -131,14 +131,14 @@ def test_positive_use_alternate_directory( result = target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) try: assert result['success'] == '1' - except AssertionError: + except AssertionError as err: output = ' '.join( target_sat.cli.JobInvocation.get_output( {'id': invocation_command['id'], 'host': client.hostname} ) ) result = f'host output: {output}' - raise AssertionError(result) + raise AssertionError(result) from err task = target_sat.cli.Task.list_tasks({'search': command})[0] search = target_sat.cli.Task.list_tasks({'search': f'id={task["id"]}'}) diff --git a/tests/foreman/longrun/test_oscap.py b/tests/foreman/longrun/test_oscap.py index b4ef1571975..31b772f63c8 100644 --- a/tests/foreman/longrun/test_oscap.py +++ b/tests/foreman/longrun/test_oscap.py @@ -215,12 +215,12 @@ def test_positive_oscap_run_via_ansible( try: result = target_sat.cli.JobInvocation.info({'id': job_id})['success'] assert result == '1' - except AssertionError: + except AssertionError as err: output = ' '.join( target_sat.cli.JobInvocation.get_output({'id': job_id, 'host': vm.hostname}) ) result = f'host output: {output}' - raise AssertionError(result) + raise AssertionError(result) from err result = vm.run('cat /etc/foreman_scap_client/config.yaml | grep profile') assert result.status == 0 # Runs the actual oscap scan on the vm/clients and @@ -320,12 +320,12 @@ def test_positive_oscap_run_via_ansible_bz_1814988( try: result = target_sat.cli.JobInvocation.info({'id': job_id})['success'] assert result == '1' - except AssertionError: + except AssertionError as err: output = ' '.join( target_sat.cli.JobInvocation.get_output({'id': job_id, 'host': vm.hostname}) ) result = f'host output: {output}' - raise AssertionError(result) + raise AssertionError(result) from err result = vm.run('cat /etc/foreman_scap_client/config.yaml | grep profile') assert result.status == 0 # Runs the actual oscap scan on the vm/clients and diff --git a/tests/foreman/maintain/test_advanced.py b/tests/foreman/maintain/test_advanced.py index 279ce48c4eb..5b105b4c0bb 100644 --- a/tests/foreman/maintain/test_advanced.py +++ b/tests/foreman/maintain/test_advanced.py @@ -27,8 +27,10 @@ def get_satellite_capsule_repos( - x_y_release=sat_x_y_release, product='satellite', os_major_ver=get_sat_rhel_version().major + x_y_release=sat_x_y_release, product='satellite', os_major_ver=None ): + if os_major_ver is None: + os_major_ver = get_sat_rhel_version().major if product == 'capsule': product = 'satellite-capsule' repos = [ diff --git a/tests/foreman/maintain/test_health.py b/tests/foreman/maintain/test_health.py index 3793dad8b01..2dcd93cc8d0 100644 --- a/tests/foreman/maintain/test_health.py +++ b/tests/foreman/maintain/test_health.py @@ -214,7 +214,7 @@ def test_negative_health_check_upstream_repository(sat_maintain, request): assert result.status == 0 assert 'System has upstream foreman_repo,puppet_repo repositories enabled' in result.stdout assert 'FAIL' in result.stdout - for name in upstream_url.keys(): + for name in upstream_url: result = sat_maintain.execute(f'cat /etc/yum.repos.d/{name}.repo') if name == 'fedorapeople_repo': assert 'enabled=1' in result.stdout @@ -223,7 +223,7 @@ def test_negative_health_check_upstream_repository(sat_maintain, request): @request.addfinalizer def _finalize(): - for name, url in upstream_url.items(): + for name in upstream_url: sat_maintain.execute(f'rm -fr /etc/yum.repos.d/{name}.repo') sat_maintain.execute('dnf clean all') diff --git a/tests/foreman/ui/test_computeresource_vmware.py b/tests/foreman/ui/test_computeresource_vmware.py index 34d0127e983..6db84bf5e59 100644 --- a/tests/foreman/ui/test_computeresource_vmware.py +++ b/tests/foreman/ui/test_computeresource_vmware.py @@ -295,8 +295,8 @@ def test_positive_resource_vm_power_management(session): timeout=30, delay=2, ) - except TimedOutError: - raise AssertionError('Timed out waiting for VM to toggle power state') + except TimedOutError as err: + raise AssertionError('Timed out waiting for VM to toggle power state') from err @pytest.mark.tier2 @@ -573,8 +573,8 @@ def test_positive_virt_card(session, target_sat, module_location, module_org): timeout=30, delay=2, ) - except TimedOutError: - raise AssertionError('Timed out waiting for VM to toggle power state') + except TimedOutError as err: + raise AssertionError('Timed out waiting for VM to toggle power state') from err virt_card = session.host_new.get_virtualization(host_name)['details'] assert virt_card['datacenter'] == settings.vmware.datacenter diff --git a/tests/foreman/ui/test_contenthost.py b/tests/foreman/ui/test_contenthost.py index 80a0eedf3a9..06ca2463850 100644 --- a/tests/foreman/ui/test_contenthost.py +++ b/tests/foreman/ui/test_contenthost.py @@ -1492,7 +1492,7 @@ def test_syspurpose_attributes_empty(session, default_location, vm_module_stream ] syspurpose_status = details['system_purpose_status'] assert syspurpose_status.lower() == 'not specified' - for spname, spdata in DEFAULT_SYSPURPOSE_ATTRIBUTES.items(): + for spname in DEFAULT_SYSPURPOSE_ATTRIBUTES: assert details[spname] == '' @@ -1530,7 +1530,7 @@ def test_set_syspurpose_attributes_cli(session, default_location, vm_module_stre with session: session.location.select(default_location.name) # Set sypurpose attributes - for spname, spdata in DEFAULT_SYSPURPOSE_ATTRIBUTES.items(): + for spdata in DEFAULT_SYSPURPOSE_ATTRIBUTES.values(): run_remote_command_on_content_host( f'syspurpose set-{spdata[0]} "{spdata[1]}"', vm_module_streams ) @@ -1575,11 +1575,11 @@ def test_unset_syspurpose_attributes_cli(session, default_location, vm_module_st :CaseImportance: High """ # Set sypurpose attributes... - for spname, spdata in DEFAULT_SYSPURPOSE_ATTRIBUTES.items(): + for spdata in DEFAULT_SYSPURPOSE_ATTRIBUTES.values(): run_remote_command_on_content_host( f'syspurpose set-{spdata[0]} "{spdata[1]}"', vm_module_streams ) - for spname, spdata in DEFAULT_SYSPURPOSE_ATTRIBUTES.items(): + for spdata in DEFAULT_SYSPURPOSE_ATTRIBUTES.values(): # ...and unset them. run_remote_command_on_content_host(f'syspurpose unset-{spdata[0]}', vm_module_streams) @@ -1588,7 +1588,7 @@ def test_unset_syspurpose_attributes_cli(session, default_location, vm_module_st details = session.contenthost.read(vm_module_streams.hostname, widget_names='details')[ 'details' ] - for spname, spdata in DEFAULT_SYSPURPOSE_ATTRIBUTES.items(): + for spname in DEFAULT_SYSPURPOSE_ATTRIBUTES: assert details[spname] == '' diff --git a/tests/foreman/ui/test_dashboard.py b/tests/foreman/ui/test_dashboard.py index 80b7ce16732..f7ab199ce48 100644 --- a/tests/foreman/ui/test_dashboard.py +++ b/tests/foreman/ui/test_dashboard.py @@ -86,7 +86,7 @@ def test_positive_host_configuration_status(session): else: assert dashboard_values['status_list'][criteria] == 0 - for criteria, search in zip(criteria_list, search_strings_list): + for criteria, search in zip(criteria_list, search_strings_list, strict=True): if criteria == 'Hosts with no reports': session.dashboard.action({'HostConfigurationStatus': {'status_list': criteria}}) values = session.host.read_all() diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index c4ae7a73773..93f2dedc83c 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -804,7 +804,7 @@ def test_positive_search_by_parameter_with_different_values( for host in hosts: assert session.host.search(host.name)[0]['Name'] == host.name # Check that search by parameter returns only one host in the list - for param_value, host in zip(param_values, hosts): + for param_value, host in zip(param_values, hosts, strict=True): values = session.host.search(f'params.{param_name} = {param_value}') assert len(values) == 1 assert values[0]['Name'] == host.name diff --git a/tests/foreman/ui/test_hostcollection.py b/tests/foreman/ui/test_hostcollection.py index 2c60125c293..e01be3e49a4 100644 --- a/tests/foreman/ui/test_hostcollection.py +++ b/tests/foreman/ui/test_hostcollection.py @@ -705,9 +705,10 @@ def test_negative_hosts_limit( session.hostcollection.associate_host(hc_name, hosts[0].name) with pytest.raises(AssertionError) as context: session.hostcollection.associate_host(hc_name, hosts[1].name) - assert "cannot have more than 1 host(s) associated with host collection '{}'".format( - hc_name - ) in str(context.value) + assert ( + f"cannot have more than 1 host(s) associated with host collection '{hc_name}'" + in str(context.value) + ) @pytest.mark.tier3 diff --git a/tests/foreman/ui/test_sync.py b/tests/foreman/ui/test_sync.py index 30ccce8c95f..041a36a011d 100644 --- a/tests/foreman/ui/test_sync.py +++ b/tests/foreman/ui/test_sync.py @@ -80,7 +80,7 @@ def test_positive_sync_rh_repos(session, target_sat, module_entitlement_manifest distros = ['rhel6', 'rhel7'] repo_collections = [ target_sat.cli_factory.RepositoryCollection(distro=distro, repositories=[repo]) - for distro, repo in zip(distros, repos) + for distro, repo in zip(distros, repos, strict=True) ] for repo_collection in repo_collections: repo_collection.setup(module_entitlement_manifest_org.id, synchronize=False) diff --git a/tests/robottelo/test_func_shared.py b/tests/robottelo/test_func_shared.py index 7cc635e1aa0..4e70550a08d 100644 --- a/tests/robottelo/test_func_shared.py +++ b/tests/robottelo/test_func_shared.py @@ -538,7 +538,7 @@ def test_function_kw_scope(self): """ prefixes = [f'pre_{i}' for i in range(10)] suffixes = [f'suf_{i}' for i in range(10)] - for prefix, suffix in zip(prefixes, suffixes): + for prefix, suffix in zip(prefixes, suffixes, strict=True): counter_value = gen_integer(min_value=2, max_value=10000) inc_string = basic_shared_counter_string( prefix=prefix, suffix=suffix, counter=counter_value diff --git a/tests/robottelo/test_report.py b/tests/robottelo/test_report.py index e5f2090b21a..acdc41c6ad4 100644 --- a/tests/robottelo/test_report.py +++ b/tests/robottelo/test_report.py @@ -44,11 +44,13 @@ def test_junit_timestamps(exec_test, property_level): prop = [prop] try: assert 'start_time' in [p['@name'] for p in prop] - except KeyError as e: - raise AssertionError(f'Missing property node: "start_time": {e}') + except KeyError as err: + raise AssertionError(f'Missing property node: "start_time": {err}') from err try: for p in prop: if p['@name'] == 'start_time': datetime.datetime.strptime(p['@value'], XUNIT_TIME_FORMAT) - except ValueError as e: - raise AssertionError(f'Unable to parse datetime for "start_time" property node: {e}') + except ValueError as err: + raise AssertionError( + f'Unable to parse datetime for "start_time" property node: {err}' + ) from err From 4353101f88273bf353cbbc0f836f05a868339b5d Mon Sep 17 00:00:00 2001 From: Jacob Callahan Date: Wed, 10 Jan 2024 10:23:38 -0500 Subject: [PATCH 427/586] Removing changes associated with Bugbear rule B019 This PR reverts the change from lru_cache to cachedmethod. The primary goal of the original change was to avoid memory leaks, due to lru_cache holding instances of a class alive longer than they should be. However, for our test framework, the workaround to avoid these memory leaks really aren't worth it. I also added an lru_cache to cli_factory.__getattr__, improving its recurring lookup time substantially. Finally, I corrected a type check that was erroneously converted in the initial pass. (cherry picked from commit 8f7fe941d1f474328d6862692b20f9ed5d2f1b14) --- pyproject.toml | 1 + robottelo/host_helpers/cli_factory.py | 6 +++--- robottelo/host_helpers/satellite_mixins.py | 4 ++-- robottelo/hosts.py | 5 ++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 61982c48c08..4f5c8af613f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ select = [ ] ignore = [ + "B019", # lru_cache can lead to memory leaks - acceptable tradeoff "E501", # line too long - handled by black "PT004", # pytest underscrore prefix for non-return fixtures "PT005", # pytest no underscrore prefix for return fixtures diff --git a/robottelo/host_helpers/cli_factory.py b/robottelo/host_helpers/cli_factory.py index 575de2256ac..290fc7a85b9 100644 --- a/robottelo/host_helpers/cli_factory.py +++ b/robottelo/host_helpers/cli_factory.py @@ -4,7 +4,7 @@ example: my_satellite.cli_factory.make_org() """ import datetime -from functools import partial +from functools import lru_cache, partial import inspect import os from os import chmod @@ -14,7 +14,6 @@ from time import sleep from box import Box -from cachetools import cachedmethod from fauxfactory import ( gen_alpha, gen_alphanumeric, @@ -250,6 +249,7 @@ def __init__(self, satellite): self._satellite = satellite self.__dict__.update(initiate_repo_helpers(self._satellite)) + @lru_cache def __getattr__(self, name): """We intercept the usual attribute behavior on this class to emulate make_entity methods The keys in the dictionary above correspond to potential make_ methods @@ -295,7 +295,7 @@ def _evaluate_functions(self, iterable): if not key.startswith('_') } - @cachedmethod + @lru_cache def _find_entity_class(self, entity_name): entity_name = entity_name.replace('_', '').lower() for name, class_obj in self._satellite.cli.__dict__.items(): diff --git a/robottelo/host_helpers/satellite_mixins.py b/robottelo/host_helpers/satellite_mixins.py index 6c47228c147..4b56326f8c1 100644 --- a/robottelo/host_helpers/satellite_mixins.py +++ b/robottelo/host_helpers/satellite_mixins.py @@ -1,10 +1,10 @@ import contextlib +from functools import lru_cache import io import os import random import re -from cachetools import cachedmethod import requests from robottelo.cli.proxy import CapsuleTunnelError @@ -358,6 +358,6 @@ def api_factory(self): self._api_factory = APIFactory(self) return self._api_factory - @cachedmethod + @lru_cache def ui_factory(self, session): return UIFactory(self, session=session) diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 37dab609a2e..a60d937a2c4 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -16,7 +16,6 @@ from box import Box from broker import Broker from broker.hosts import Host -from cachetools import cachedmethod from dynaconf.vendor.box.exceptions import BoxKeyError from fauxfactory import gen_alpha, gen_string from manifester import Manifester @@ -2296,7 +2295,7 @@ def get_rhsso_client_id(self): break return client_id - @cachedmethod + @lru_cache def get_rhsso_user_details(self, username): """Getter method to receive the user id""" result = self.execute( @@ -2305,7 +2304,7 @@ def get_rhsso_user_details(self, username): result_json = json.loads(result.stdout) return result_json[0] - @cachedmethod + @lru_cache def get_rhsso_groups_details(self, group_name): """Getter method to receive the group id""" result = self.execute(f"{KEY_CLOAK_CLI} get groups -r {settings.rhsso.realm}") From 190c46fbb13ac69eba09a4da790100c7927de9f8 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 10 Jan 2024 16:03:23 -0500 Subject: [PATCH 428/586] [6.14.z] fixing the incorrect recording video url (#13720) fixing the incorrect recording video url (#13714) fixing the recording video url (cherry picked from commit de84bd3e1d30074534ac42c7e66718ec73e19929) Co-authored-by: Omkar Khatavkar --- robottelo/hosts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robottelo/hosts.py b/robottelo/hosts.py index a60d937a2c4..4844ca0ba24 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1872,7 +1872,7 @@ def get_caller(): raise finally: video_url = settings.ui.grid_url.replace( - ':4444', f'/videos/{ui_session.ui_session_id}.mp4' + ':4444', f'/videos/{ui_session.ui_session_id}/video.mp4' ) if self.record_property is not None and settings.ui.record_video: self.record_property('video_url', video_url) From 46b806a946ca609a2418a4b170553429a1409031 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 11 Jan 2024 08:36:25 -0500 Subject: [PATCH 429/586] [6.14.z] Non tabular output denied for CSV conversion (#13745) Non tabular output denied for CSV conversion (cherry picked from commit 39be0d7f962dc5bb55946a5034d239709e799592) Co-authored-by: jyejare --- robottelo/cli/hammer.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/robottelo/cli/hammer.py b/robottelo/cli/hammer.py index 661042050e6..cf14cd2f0d7 100644 --- a/robottelo/cli/hammer.py +++ b/robottelo/cli/hammer.py @@ -34,10 +34,23 @@ def _normalize_obj(obj): return obj +def is_csv(output): + """Verifies if the output string is eligible for converting into CSV""" + sniffer = csv.Sniffer() + try: + sniffer.sniff(output) + return True + except csv.Error: + return False + + def parse_csv(output): """Parse CSV output from Hammer CLI and convert it to python dictionary.""" # ignore warning about puppet and ostree deprecation output.replace('Puppet and OSTree will no longer be supported in Katello 3.16\n', '') + # Validate if the output is eligible for CSV conversions else return as it is + if not is_csv(output): + return output reader = csv.reader(output.splitlines()) # Generate the key names, spaces will be converted to dashes "-" keys = [_normalize(header) for header in next(reader)] From 4f26023f6a390769153f57bc1edd2ca213d8a9c4 Mon Sep 17 00:00:00 2001 From: vsedmik <46570670+vsedmik@users.noreply.github.com> Date: Thu, 11 Jan 2024 17:07:38 +0100 Subject: [PATCH 430/586] [6.14.z] Fixes in cli_factory (#13757) Fixes in cli_factory - remove @lru_cache from CLIFactory.__getattr__ - flip output format of cli.Repository.synchronize to from 'csv' to 'base' (output is not in csv format here) --- robottelo/cli/repository.py | 2 +- robottelo/host_helpers/cli_factory.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/robottelo/cli/repository.py b/robottelo/cli/repository.py index 85296f25e51..94fa8baa180 100644 --- a/robottelo/cli/repository.py +++ b/robottelo/cli/repository.py @@ -60,7 +60,7 @@ def synchronize(cls, options, return_raw_response=None, timeout=3600000): cls.command_sub = 'synchronize' return cls.execute( cls._construct_command(options), - output_format='csv', + output_format='base', ignore_stderr=True, return_raw_response=return_raw_response, timeout=timeout, diff --git a/robottelo/host_helpers/cli_factory.py b/robottelo/host_helpers/cli_factory.py index 290fc7a85b9..0703740760b 100644 --- a/robottelo/host_helpers/cli_factory.py +++ b/robottelo/host_helpers/cli_factory.py @@ -249,7 +249,6 @@ def __init__(self, satellite): self._satellite = satellite self.__dict__.update(initiate_repo_helpers(self._satellite)) - @lru_cache def __getattr__(self, name): """We intercept the usual attribute behavior on this class to emulate make_entity methods The keys in the dictionary above correspond to potential make_ methods From a48c6719ffff2fef3cdf0a036226a930bf5c231d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 11 Jan 2024 12:54:19 -0500 Subject: [PATCH 431/586] [6.14.z] Add missing on_premises_provisioning marker for capsule provisioning test (#13761) Add missing on_premises_provisioning marker for capsule provisioning test (#13760) Add on premise marker for capsule provisioning test (cherry picked from commit 7e9fcc81dab139184893b09cd00a9048f7f48894) Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> --- tests/foreman/api/test_provisioning.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/foreman/api/test_provisioning.py b/tests/foreman/api/test_provisioning.py index 5e8c1ea133b..3e466d81286 100644 --- a/tests/foreman/api/test_provisioning.py +++ b/tests/foreman/api/test_provisioning.py @@ -587,6 +587,7 @@ def test_rhel_pxe_provisioning_fips_enabled( @pytest.mark.e2e @pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) +@pytest.mark.on_premises_provisioning @pytest.mark.rhel_ver_match('[^6]') def test_capsule_pxe_provisioning( request, From f92c947eb00bbb8b373ce96a75ffc88fa9740dd1 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 11 Jan 2024 17:34:43 -0500 Subject: [PATCH 432/586] [6.14.z] Add CU scenario for puma worker count (#13765) --- tests/foreman/destructive/test_installer.py | 39 +++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/foreman/destructive/test_installer.py b/tests/foreman/destructive/test_installer.py index b06df33d968..a654c4e6bf5 100644 --- a/tests/foreman/destructive/test_installer.py +++ b/tests/foreman/destructive/test_installer.py @@ -16,6 +16,8 @@ :Upstream: No """ +import random + from fauxfactory import gen_domain, gen_string import pytest @@ -147,3 +149,40 @@ def test_positive_installer_certs_regenerate(target_sat): ) assert result.status == 0 assert 'FAIL' not in target_sat.cli.Base.ping() + + +def test_positive_installer_puma_worker_count(target_sat): + """Installer should set the puma worker count and thread max without having to manually + restart the foreman service. + + :id: d0e7d958-dd3e-4962-bf5a-8d7ec36f3485 + + :steps: + 1. Check how many puma workers there are + 2. Select a new worker count that is less than the default + 2. Change answer's file to have new count for puma workers + 3. Run satellite-installer --foreman-foreman-service-puma-workers new_count --foreman-foreman-service-puma-threads-max new_count + + :expectedresults: aux should show there are only new_count puma workers after installer runs + + :BZ: 2025760 + + :customerscenario: true + """ + count = int(target_sat.execute('pgrep --full "puma: cluster worker" | wc -l').stdout) + worker_count = str(random.randint(1, count - 1)) + result = target_sat.install( + InstallerCommand( + foreman_foreman_service_puma_workers=worker_count, + foreman_foreman_service_puma_threads_max=worker_count, + ) + ) + assert result.status == 0 + result = target_sat.execute(f'grep "foreman_service_puma_workers" {SATELLITE_ANSWER_FILE}') + assert worker_count in result.stdout + result = target_sat.execute('ps aux | grep -v grep | grep -e USER -e puma') + for i in range(count): + if i < int(worker_count): + assert f'cluster worker {i}' in result.stdout + else: + assert f'cluster worker {i}' not in result.stdout From 7c8fd8b222fa7d9e7166b4e775d2bac9d35db156 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 12 Jan 2024 03:53:26 -0500 Subject: [PATCH 433/586] [6.14.z] Increasing the timeout for mac collection (#13749) Increasing the timeout for mac collection (#13741) (cherry picked from commit 9d5e84ceec5debdabaadc2244e060782c31bba01) Co-authored-by: Adarsh dubey --- tests/foreman/api/test_discoveredhost.py | 8 ++++---- tests/foreman/cli/test_discoveredhost.py | 4 ++-- tests/foreman/ui/test_discoveredhost.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/foreman/api/test_discoveredhost.py b/tests/foreman/api/test_discoveredhost.py index 948c68e0549..b3c9ca621b2 100644 --- a/tests/foreman/api/test_discoveredhost.py +++ b/tests/foreman/api/test_discoveredhost.py @@ -206,7 +206,7 @@ def test_positive_provision_pxe_host( mac = provisioning_host._broker_args['provisioning_nic_mac_addr'] wait_for( lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], - timeout=600, + timeout=1500, delay=40, ) discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] @@ -256,7 +256,7 @@ def test_positive_provision_pxe_less_host( mac = pxeless_discovery_host._broker_args['provisioning_nic_mac_addr'] wait_for( lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], - timeout=600, + timeout=1500, delay=40, ) discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] @@ -391,7 +391,7 @@ def test_positive_reboot_pxe_host( mac = provisioning_host._broker_args['provisioning_nic_mac_addr'] wait_for( lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], - timeout=240, + timeout=1500, delay=20, ) @@ -437,7 +437,7 @@ def test_positive_reboot_all_pxe_hosts( mac = host._broker_args['provisioning_nic_mac_addr'] wait_for( lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], # noqa: B023 - timeout=240, + timeout=1500, delay=20, ) discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] diff --git a/tests/foreman/cli/test_discoveredhost.py b/tests/foreman/cli/test_discoveredhost.py index 4ee7015ec03..e91db52a804 100644 --- a/tests/foreman/cli/test_discoveredhost.py +++ b/tests/foreman/cli/test_discoveredhost.py @@ -57,7 +57,7 @@ def test_rhel_pxe_discovery_provisioning( wait_for( lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], - timeout=600, + timeout=1500, delay=40, ) discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] @@ -120,7 +120,7 @@ def test_rhel_pxeless_discovery_provisioning( wait_for( lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], - timeout=600, + timeout=1500, delay=40, ) discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] diff --git a/tests/foreman/ui/test_discoveredhost.py b/tests/foreman/ui/test_discoveredhost.py index 87b62e412e1..82b29d897a8 100644 --- a/tests/foreman/ui/test_discoveredhost.py +++ b/tests/foreman/ui/test_discoveredhost.py @@ -86,7 +86,7 @@ def test_positive_provision_pxe_host( mac = provisioning_host._broker_args['provisioning_nic_mac_addr'] wait_for( lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], - timeout=240, + timeout=1500, delay=20, ) discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] @@ -184,7 +184,7 @@ def test_positive_auto_provision_host_with_rule( mac = pxeless_discovery_host._broker_args['provisioning_nic_mac_addr'] wait_for( lambda: sat.api.DiscoveredHost().search(query={'mac': mac}) != [], - timeout=240, + timeout=1500, delay=20, ) discovered_host = sat.api.DiscoveredHost().search(query={'mac': mac})[0] From 120b31cd0a15145ed01a2a13af27332fa50c9a39 Mon Sep 17 00:00:00 2001 From: Omkar Khatavkar Date: Fri, 12 Jan 2024 14:41:56 +0530 Subject: [PATCH 434/586] [6.14.z] - remove the unused testimony tokens & none used for reporting (#13736) Bump flake8 from 6.1.0 to 7.0.0 Bumps [flake8](https://github.com/pycqa/flake8) from 6.1.0 to 7.0.0. - [Commits](https://github.com/pycqa/flake8/compare/6.1.0...7.0.0) --- updated-dependencies: - dependency-name: flake8 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- scripts/polarion-test-case-upload.sh | 5 + testimony.yaml | 23 --- tests/foreman/api/test_acs.py | 7 - tests/foreman/api/test_activationkey.py | 18 +-- tests/foreman/api/test_ansible.py | 13 +- tests/foreman/api/test_architecture.py | 5 - tests/foreman/api/test_audit.py | 5 - tests/foreman/api/test_bookmarks.py | 31 ++-- tests/foreman/api/test_capsule.py | 10 -- tests/foreman/api/test_capsulecontent.py | 5 - tests/foreman/api/test_classparameters.py | 5 - tests/foreman/api/test_computeprofile.py | 15 -- .../api/test_computeresource_azurerm.py | 22 +-- tests/foreman/api/test_computeresource_gce.py | 17 +-- .../api/test_computeresource_libvirt.py | 23 --- tests/foreman/api/test_contentcredentials.py | 5 - tests/foreman/api/test_contentview.py | 83 ----------- tests/foreman/api/test_contentviewfilter.py | 55 ------- tests/foreman/api/test_contentviewversion.py | 61 ++------ tests/foreman/api/test_convert2rhel.py | 9 +- tests/foreman/api/test_discoveredhost.py | 25 ++-- tests/foreman/api/test_discoveryrule.py | 5 - tests/foreman/api/test_docker.py | 12 -- tests/foreman/api/test_environment.py | 9 -- tests/foreman/api/test_errata.py | 45 +----- tests/foreman/api/test_filter.py | 5 - tests/foreman/api/test_foremantask.py | 5 - tests/foreman/api/test_host.py | 65 -------- tests/foreman/api/test_hostcollection.py | 9 -- tests/foreman/api/test_hostgroup.py | 25 ---- tests/foreman/api/test_http_proxy.py | 7 - tests/foreman/api/test_ldapauthsource.py | 5 - .../foreman/api/test_lifecycleenvironment.py | 15 +- tests/foreman/api/test_location.py | 11 -- tests/foreman/api/test_media.py | 7 - tests/foreman/api/test_multiple_paths.py | 5 - tests/foreman/api/test_notifications.py | 5 - tests/foreman/api/test_operatingsystem.py | 8 - tests/foreman/api/test_organization.py | 10 -- .../foreman/api/test_oscap_tailoringfiles.py | 5 - tests/foreman/api/test_oscappolicy.py | 7 - tests/foreman/api/test_parameters.py | 6 - tests/foreman/api/test_partitiontable.py | 5 - tests/foreman/api/test_permission.py | 5 - tests/foreman/api/test_ping.py | 5 - tests/foreman/api/test_product.py | 15 -- tests/foreman/api/test_provisioning.py | 5 - tests/foreman/api/test_provisioning_puppet.py | 5 - .../foreman/api/test_provisioningtemplate.py | 12 +- tests/foreman/api/test_registration.py | 9 -- tests/foreman/api/test_remoteexecution.py | 5 - tests/foreman/api/test_reporttemplates.py | 5 - tests/foreman/api/test_repositories.py | 21 +-- tests/foreman/api/test_repository.py | 45 +----- tests/foreman/api/test_repository_set.py | 5 - tests/foreman/api/test_rhc.py | 7 +- tests/foreman/api/test_rhcloud_inventory.py | 13 +- tests/foreman/api/test_rhsm.py | 5 - tests/foreman/api/test_role.py | 36 ----- tests/foreman/api/test_settings.py | 13 +- tests/foreman/api/test_subnet.py | 21 --- tests/foreman/api/test_subscription.py | 5 - tests/foreman/api/test_syncplan.py | 41 ----- .../foreman/api/test_template_combination.py | 5 - tests/foreman/api/test_templatesync.py | 66 ++++---- tests/foreman/api/test_user.py | 18 --- tests/foreman/api/test_usergroup.py | 7 - tests/foreman/api/test_webhook.py | 5 - tests/foreman/cli/test_abrt.py | 20 +-- tests/foreman/cli/test_acs.py | 9 -- tests/foreman/cli/test_activationkey.py | 72 ++------- tests/foreman/cli/test_ansible.py | 9 +- tests/foreman/cli/test_architecture.py | 5 - tests/foreman/cli/test_auth.py | 26 +--- tests/foreman/cli/test_bootdisk.py | 5 - tests/foreman/cli/test_bootstrap_script.py | 7 +- tests/foreman/cli/test_capsule.py | 9 +- tests/foreman/cli/test_classparameters.py | 5 - .../cli/test_computeresource_azurerm.py | 18 +-- tests/foreman/cli/test_computeresource_ec2.py | 7 - .../cli/test_computeresource_libvirt.py | 31 ---- tests/foreman/cli/test_computeresource_osp.py | 5 - .../foreman/cli/test_computeresource_rhev.py | 8 - .../cli/test_computeresource_vmware.py | 5 - .../foreman/cli/test_container_management.py | 7 +- tests/foreman/cli/test_contentaccess.py | 7 - tests/foreman/cli/test_contentcredentials.py | 25 ---- tests/foreman/cli/test_contentview.py | 123 ++------------- tests/foreman/cli/test_contentviewfilter.py | 11 -- tests/foreman/cli/test_discoveredhost.py | 11 +- tests/foreman/cli/test_discoveryrule.py | 18 --- tests/foreman/cli/test_docker.py | 11 -- tests/foreman/cli/test_domain.py | 7 - tests/foreman/cli/test_environment.py | 7 - tests/foreman/cli/test_errata.py | 21 --- tests/foreman/cli/test_fact.py | 5 - tests/foreman/cli/test_filter.py | 5 - tests/foreman/cli/test_foremantask.py | 6 - tests/foreman/cli/test_globalparam.py | 5 - tests/foreman/cli/test_hammer.py | 5 - tests/foreman/cli/test_host.py | 81 +--------- tests/foreman/cli/test_hostcollection.py | 13 -- tests/foreman/cli/test_hostgroup.py | 20 +-- tests/foreman/cli/test_http_proxy.py | 13 +- tests/foreman/cli/test_installer.py | 5 - tests/foreman/cli/test_jobtemplate.py | 6 - tests/foreman/cli/test_ldapauthsource.py | 5 - tests/foreman/cli/test_leapp_client.py | 7 +- .../foreman/cli/test_lifecycleenvironment.py | 5 - tests/foreman/cli/test_location.py | 6 - tests/foreman/cli/test_logging.py | 7 - tests/foreman/cli/test_medium.py | 6 - tests/foreman/cli/test_model.py | 5 - tests/foreman/cli/test_operatingsystem.py | 8 - tests/foreman/cli/test_organization.py | 30 ---- tests/foreman/cli/test_oscap.py | 5 - .../foreman/cli/test_oscap_tailoringfiles.py | 5 - tests/foreman/cli/test_ostreebranch.py | 7 - tests/foreman/cli/test_partitiontable.py | 7 - tests/foreman/cli/test_ping.py | 5 - tests/foreman/cli/test_product.py | 8 +- tests/foreman/cli/test_provisioning.py | 5 - .../foreman/cli/test_provisioningtemplate.py | 9 -- tests/foreman/cli/test_puppetclass.py | 5 - tests/foreman/cli/test_realm.py | 5 - tests/foreman/cli/test_registration.py | 7 - tests/foreman/cli/test_remoteexecution.py | 93 +++++++++--- tests/foreman/cli/test_report.py | 5 - tests/foreman/cli/test_reporttemplates.py | 8 - tests/foreman/cli/test_repositories.py | 5 - tests/foreman/cli/test_repository.py | 94 ++++-------- tests/foreman/cli/test_repository_set.py | 5 - tests/foreman/cli/test_rhcloud_inventory.py | 33 +--- tests/foreman/cli/test_role.py | 6 - tests/foreman/cli/test_satellitesync.py | 29 ---- tests/foreman/cli/test_settings.py | 11 -- tests/foreman/cli/test_sso.py | 5 - tests/foreman/cli/test_subnet.py | 5 - tests/foreman/cli/test_subscription.py | 7 - tests/foreman/cli/test_syncplan.py | 23 --- tests/foreman/cli/test_templatesync.py | 13 +- tests/foreman/cli/test_user.py | 59 +++++--- tests/foreman/cli/test_usergroup.py | 13 -- .../cli/test_vm_install_products_package.py | 5 - tests/foreman/cli/test_webhook.py | 5 - tests/foreman/destructive/conftest.py | 1 - tests/foreman/destructive/test_ansible.py | 9 +- tests/foreman/destructive/test_auth.py | 6 - tests/foreman/destructive/test_capsule.py | 7 - .../destructive/test_capsule_loadbalancer.py | 14 +- .../destructive/test_capsulecontent.py | 6 - tests/foreman/destructive/test_clone.py | 5 - tests/foreman/destructive/test_contenthost.py | 7 - tests/foreman/destructive/test_contentview.py | 5 - .../destructive/test_discoveredhost.py | 7 +- .../foreman/destructive/test_foreman_rake.py | 7 +- .../destructive/test_foreman_service.py | 7 +- tests/foreman/destructive/test_host.py | 7 - tests/foreman/destructive/test_infoblox.py | 7 +- tests/foreman/destructive/test_installer.py | 7 - .../foreman/destructive/test_katello_agent.py | 6 - .../destructive/test_katello_certs_check.py | 6 - .../destructive/test_ldap_authentication.py | 16 -- .../destructive/test_ldapauthsource.py | 5 - .../destructive/test_leapp_satellite.py | 5 - tests/foreman/destructive/test_packages.py | 5 - tests/foreman/destructive/test_ping.py | 5 - .../foreman/destructive/test_puppetplugin.py | 5 - tests/foreman/destructive/test_realm.py | 5 - .../foreman/destructive/test_registration.py | 6 - .../destructive/test_remoteexecution.py | 5 - tests/foreman/destructive/test_rename.py | 6 - tests/foreman/destructive/test_repository.py | 5 - tests/foreman/endtoend/test_api_endtoend.py | 5 - tests/foreman/endtoend/test_cli_endtoend.py | 5 - tests/foreman/installer/test_infoblox.py | 13 +- tests/foreman/installer/test_installer.py | 18 --- tests/foreman/longrun/test_inc_updates.py | 7 - tests/foreman/longrun/test_oscap.py | 27 +--- tests/foreman/maintain/test_advanced.py | 5 - tests/foreman/maintain/test_backup_restore.py | 6 +- tests/foreman/maintain/test_health.py | 5 - .../foreman/maintain/test_maintenance_mode.py | 5 - tests/foreman/maintain/test_offload_DB.py | 9 +- tests/foreman/maintain/test_packages.py | 5 - tests/foreman/maintain/test_service.py | 5 - tests/foreman/maintain/test_upgrade.py | 5 - tests/foreman/sanity/test_bvt.py | 7 - tests/foreman/sys/test_dynflow.py | 5 - tests/foreman/sys/test_fam.py | 7 - tests/foreman/sys/test_katello_certs_check.py | 6 - tests/foreman/sys/test_pulp3_filesystem.py | 5 - tests/foreman/ui/test_acs.py | 5 - tests/foreman/ui/test_activationkey.py | 77 +--------- tests/foreman/ui/test_ansible.py | 31 +--- tests/foreman/ui/test_architecture.py | 7 - tests/foreman/ui/test_audit.py | 15 -- tests/foreman/ui/test_bookmarks.py | 23 +-- tests/foreman/ui/test_branding.py | 9 +- tests/foreman/ui/test_computeprofiles.py | 7 - tests/foreman/ui/test_computeresource.py | 19 --- .../ui/test_computeresource_azurerm.py | 11 -- tests/foreman/ui/test_computeresource_ec2.py | 11 +- tests/foreman/ui/test_computeresource_gce.py | 13 +- .../ui/test_computeresource_libvirt.py | 9 -- .../foreman/ui/test_computeresource_vmware.py | 19 --- tests/foreman/ui/test_config_group.py | 7 - tests/foreman/ui/test_containerimagetag.py | 7 - tests/foreman/ui/test_contentcredentials.py | 33 ---- tests/foreman/ui/test_contenthost.py | 59 -------- tests/foreman/ui/test_contentview.py | 141 +----------------- tests/foreman/ui/test_dashboard.py | 26 +--- tests/foreman/ui/test_discoveredhost.py | 5 - tests/foreman/ui/test_discoveryrule.py | 11 +- tests/foreman/ui/test_domain.py | 15 -- tests/foreman/ui/test_errata.py | 57 ++----- tests/foreman/ui/test_hardwaremodel.py | 8 - tests/foreman/ui/test_host.py | 125 +++++----------- tests/foreman/ui/test_hostcollection.py | 39 +---- tests/foreman/ui/test_hostgroup.py | 11 +- tests/foreman/ui/test_http_proxy.py | 18 +-- tests/foreman/ui/test_jobinvocation.py | 19 +-- tests/foreman/ui/test_jobtemplate.py | 7 - tests/foreman/ui/test_ldap_authentication.py | 25 ++-- tests/foreman/ui/test_lifecycleenvironment.py | 19 +-- tests/foreman/ui/test_location.py | 13 -- tests/foreman/ui/test_media.py | 7 - tests/foreman/ui/test_modulestreams.py | 7 - tests/foreman/ui/test_operatingsystem.py | 9 -- tests/foreman/ui/test_organization.py | 23 +-- tests/foreman/ui/test_oscapcontent.py | 13 +- tests/foreman/ui/test_oscappolicy.py | 11 +- tests/foreman/ui/test_oscaptailoringfile.py | 7 - tests/foreman/ui/test_package.py | 15 -- tests/foreman/ui/test_partitiontable.py | 19 --- tests/foreman/ui/test_product.py | 13 +- tests/foreman/ui/test_provisioningtemplate.py | 17 +-- tests/foreman/ui/test_puppetclass.py | 7 - tests/foreman/ui/test_puppetenvironment.py | 9 -- tests/foreman/ui/test_registration.py | 10 -- tests/foreman/ui/test_remoteexecution.py | 65 ++------ tests/foreman/ui/test_reporttemplates.py | 12 +- tests/foreman/ui/test_repositories.py | 5 - tests/foreman/ui/test_repository.py | 73 ++------- tests/foreman/ui/test_rhc.py | 9 +- tests/foreman/ui/test_rhcloud_insights.py | 15 +- tests/foreman/ui/test_rhcloud_inventory.py | 11 +- tests/foreman/ui/test_role.py | 7 - tests/foreman/ui/test_settings.py | 29 +--- tests/foreman/ui/test_smartclassparameter.py | 15 -- tests/foreman/ui/test_subnet.py | 7 - tests/foreman/ui/test_subscription.py | 15 -- tests/foreman/ui/test_sync.py | 15 +- tests/foreman/ui/test_syncplan.py | 13 -- tests/foreman/ui/test_templatesync.py | 11 +- tests/foreman/ui/test_user.py | 27 ---- tests/foreman/ui/test_usergroup.py | 9 -- tests/foreman/ui/test_webhook.py | 5 - tests/foreman/virtwho/api/test_esx.py | 23 --- tests/foreman/virtwho/api/test_esx_sca.py | 23 --- tests/foreman/virtwho/api/test_hyperv.py | 9 -- tests/foreman/virtwho/api/test_hyperv_sca.py | 9 -- tests/foreman/virtwho/api/test_kubevirt.py | 9 -- .../foreman/virtwho/api/test_kubevirt_sca.py | 9 -- tests/foreman/virtwho/api/test_libvirt.py | 9 -- tests/foreman/virtwho/api/test_libvirt_sca.py | 9 -- tests/foreman/virtwho/api/test_nutanix.py | 14 -- tests/foreman/virtwho/api/test_nutanix_sca.py | 13 -- tests/foreman/virtwho/cli/test_esx.py | 27 ---- tests/foreman/virtwho/cli/test_esx_sca.py | 29 ---- tests/foreman/virtwho/cli/test_hyperv.py | 9 -- tests/foreman/virtwho/cli/test_hyperv_sca.py | 9 -- tests/foreman/virtwho/cli/test_kubevirt.py | 9 -- .../foreman/virtwho/cli/test_kubevirt_sca.py | 9 -- tests/foreman/virtwho/cli/test_libvirt.py | 9 -- tests/foreman/virtwho/cli/test_libvirt_sca.py | 9 -- tests/foreman/virtwho/cli/test_nutanix.py | 15 +- tests/foreman/virtwho/cli/test_nutanix_sca.py | 13 -- tests/foreman/virtwho/conftest.py | 2 - tests/foreman/virtwho/ui/test_esx.py | 31 +--- tests/foreman/virtwho/ui/test_esx_sca.py | 29 ---- tests/foreman/virtwho/ui/test_hyperv.py | 9 -- tests/foreman/virtwho/ui/test_hyperv_sca.py | 9 -- tests/foreman/virtwho/ui/test_kubevirt.py | 9 -- tests/foreman/virtwho/ui/test_kubevirt_sca.py | 9 -- tests/foreman/virtwho/ui/test_libvirt.py | 9 -- tests/foreman/virtwho/ui/test_libvirt_sca.py | 9 -- tests/foreman/virtwho/ui/test_nutanix.py | 14 -- tests/foreman/virtwho/ui/test_nutanix_sca.py | 15 -- tests/upgrades/test_activation_key.py | 5 - tests/upgrades/test_bookmarks.py | 13 +- tests/upgrades/test_capsule.py | 5 - tests/upgrades/test_classparameter.py | 5 - tests/upgrades/test_client.py | 5 - tests/upgrades/test_contentview.py | 5 - tests/upgrades/test_discovery.py | 5 - tests/upgrades/test_errata.py | 5 - tests/upgrades/test_host.py | 9 +- tests/upgrades/test_hostcontent.py | 5 - tests/upgrades/test_hostgroup.py | 5 - tests/upgrades/test_performance_tuning.py | 5 - tests/upgrades/test_provisioningtemplate.py | 5 - tests/upgrades/test_puppet.py | 5 - tests/upgrades/test_remoteexecution.py | 5 - tests/upgrades/test_repository.py | 5 - tests/upgrades/test_role.py | 5 - tests/upgrades/test_satellite_maintain.py | 5 - tests/upgrades/test_satellitesync.py | 5 - tests/upgrades/test_subnet.py | 5 - tests/upgrades/test_subscription.py | 5 - tests/upgrades/test_syncplan.py | 5 - tests/upgrades/test_user.py | 5 - tests/upgrades/test_usergroup.py | 5 - tests/upgrades/test_virtwho.py | 5 - 314 files changed, 503 insertions(+), 4108 deletions(-) diff --git a/scripts/polarion-test-case-upload.sh b/scripts/polarion-test-case-upload.sh index 888bacb04f0..0c24ba35c8d 100755 --- a/scripts/polarion-test-case-upload.sh +++ b/scripts/polarion-test-case-upload.sh @@ -69,6 +69,11 @@ DEFAULT_APPROVERS_VALUE = '${POLARION_USERNAME}:approved' DEFAULT_STATUS_VALUE = 'approved' DEFAULT_SUBTYPE2_VALUE = '-' TESTCASE_CUSTOM_FIELDS = default_config.TESTCASE_CUSTOM_FIELDS + ('customerscenario',) + ('team',) + ('markers',) +# converting TESTCASE_CUSTOM_FIELDS to list for removing tokens since the tokens are defined as defaults/mandatory in betelgeuse +TESTCASE_CUSTOM_FIELDS = list(TESTCASE_CUSTOM_FIELDS) +REMOVE_TOKEN_LIST = ['caselevel', 'upstream', 'testtype'] +TESTCASE_CUSTOM_FIELDS = tuple([token for token in TESTCASE_CUSTOM_FIELDS if token not in REMOVE_TOKEN_LIST]) + REQUIREMENT_CUSTOM_FIELDS = default_config.REQUIREMENT_CUSTOM_FIELDS + ('team',) TRANSFORM_CUSTOMERSCENARIO_VALUE = default_config._transform_to_lower DEFAULT_CUSTOMERSCENARIO_VALUE = 'false' diff --git a/testimony.yaml b/testimony.yaml index 46d0b289f43..1681b97fe0c 100644 --- a/testimony.yaml +++ b/testimony.yaml @@ -122,15 +122,6 @@ CaseImportance: - Low required: true type: choice -CaseLevel: - casesensitive: true - choices: - - Component - - Integration - - System - - Acceptance - required: true - type: choice CasePosNeg: casesensitive: false choices: @@ -158,20 +149,6 @@ SubComponent: type: choice Subtype1: {} Teardown: {} -TestType: - casesensitive: false - choices: - - Functional - - Structural - required: true - type: choice -Upstream: - casesensitive: false - choices: - - 'Yes' - - 'No' - required: true - type: choice Parametrized: casesensitive: false choices: diff --git a/tests/foreman/api/test_acs.py b/tests/foreman/api/test_acs.py index bdca5243454..7821db07fc5 100644 --- a/tests/foreman/api/test_acs.py +++ b/tests/foreman/api/test_acs.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: AlternateContentSources :Team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -50,7 +45,6 @@ def test_positive_CRUD_all_types( 3. ACS can be updated and read with new name. 4. ACS can be refreshed. 5. ACS can be deleted. - """ if 'rhui' in request.node.name and 'file' in request.node.name: pytest.skip('unsupported parametrize combination') @@ -127,7 +121,6 @@ def test_positive_run_bulk_actions(module_target_sat, module_yum_repo): :expectedresults: 1. All ACSes can be refreshed via bulk action. 2. Only the proper ACSes are deleted on bulk destroy. - """ acs_ids = [] for _ in range(3): diff --git a/tests/foreman/api/test_activationkey.py b/tests/foreman/api/test_activationkey.py index d909c4b87ab..56513c03d85 100644 --- a/tests/foreman/api/test_activationkey.py +++ b/tests/foreman/api/test_activationkey.py @@ -4,17 +4,13 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ActivationKeys :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No + """ import http @@ -278,8 +274,6 @@ def test_positive_get_releases_status_code(target_sat): :expectedresults: HTTP 200 is returned with an ``application/json`` content-type - - :CaseLevel: Integration """ act_key = target_sat.api.ActivationKey().create() path = act_key.path('releases') @@ -296,8 +290,6 @@ def test_positive_get_releases_content(target_sat): :id: 2fec3d71-33e9-40e5-b934-90b03afc26a1 :expectedresults: A list of results is returned. - - :CaseLevel: Integration """ act_key = target_sat.api.ActivationKey().create() response = client.get(act_key.path('releases'), auth=get_credentials(), verify=False).json() @@ -319,8 +311,6 @@ def test_positive_add_host_collections(module_org, module_target_sat): collections and reading that activation key, the correct host collections are listed. - :CaseLevel: Integration - :CaseImportance: Critical """ # An activation key has no host collections by default. @@ -358,8 +348,6 @@ def test_positive_remove_host_collection(module_org, module_target_sat): 3. Disassociating host collection from the activation key actually removes it from the list - :CaseLevel: Integration - :CaseImportance: Critical """ # An activation key has no host collections by default. @@ -451,8 +439,6 @@ def test_positive_fetch_product_content( :expectedresults: Both Red Hat and custom product subscriptions are assigned as Activation Key's product content - :CaseLevel: Integration - :CaseImportance: Critical """ module_target_sat.upload_manifest(module_org.id, session_entitlement_manifest.content) @@ -506,8 +492,6 @@ def test_positive_add_future_subscription(): :CaseAutomation: NotAutomated - :CaseLevel: Integration - :CaseImportance: Critical """ diff --git a/tests/foreman/api/test_ansible.py b/tests/foreman/api/test_ansible.py index 1f143e7320d..730e469b0e0 100644 --- a/tests/foreman/api/test_ansible.py +++ b/tests/foreman/api/test_ansible.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Ansible :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -33,7 +28,7 @@ def test_fetch_and_sync_ansible_playbooks(target_sat): :customerscenario: true - :Steps: + :steps: 1. Install ansible collection with playbooks. 2. Try to fetch the playbooks via api. @@ -79,7 +74,7 @@ def test_positive_ansible_job_on_host( :id: c8dcdc54-cb98-4b24-bff9-049a6cc36acb - :Steps: + :steps: 1. Register a content host with satellite 2. Import a role into satellite 3. Assign that role to a host @@ -220,7 +215,7 @@ def test_add_and_remove_ansible_role_hostgroup(target_sat): :id: 7672cf86-fa31-11ed-855a-0fd307d2d66b - :Steps: + :steps: 1. Create a hostgroup 2. Sync few ansible roles 3. Assign a few ansible roles with the host group @@ -264,7 +259,7 @@ def test_add_and_remove_ansible_role_hostgroup(target_sat): @pytest.fixture def filtered_user(target_sat, module_org, module_location): """ - :Steps: + :steps: 1. Create a role with a host view filtered 2. Create a user with that role 3. Setup a host diff --git a/tests/foreman/api/test_architecture.py b/tests/foreman/api/test_architecture.py index f0b7c428b0f..362abbfae32 100644 --- a/tests/foreman/api/test_architecture.py +++ b/tests/foreman/api/test_architecture.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_choice import pytest diff --git a/tests/foreman/api/test_audit.py b/tests/foreman/api/test_audit.py index 85bcec9bd70..a90eda83884 100644 --- a/tests/foreman/api/test_audit.py +++ b/tests/foreman/api/test_audit.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: AuditLog :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/foreman/api/test_bookmarks.py b/tests/foreman/api/test_bookmarks.py index 694d0a6654a..b51ac194cb3 100644 --- a/tests/foreman/api/test_bookmarks.py +++ b/tests/foreman/api/test_bookmarks.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Search :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random @@ -38,7 +33,7 @@ def test_positive_create_with_name(controller, target_sat): :parametrized: yes - :Steps: + :steps: 1. Create a bookmark with a random name and valid controller. 2. List the bookmarks. @@ -64,7 +59,7 @@ def test_positive_create_with_query(controller, target_sat): :parametrized: yes - :Steps: + :steps: 1. Create a bookmark with a random query and valid controller. 2. List the bookmarks. @@ -91,7 +86,7 @@ def test_positive_create_public(controller, public, target_sat): :parametrized: yes - :Steps: + :steps: 1. Create a bookmark with a valid controller and public attribute True or False. 2. List the bookmarks. @@ -116,7 +111,7 @@ def test_negative_create_with_invalid_name(controller, target_sat): :parametrized: yes - :Steps: + :steps: 1. Attempt to create a bookmark with an invalid name. 2. List the bookmarks. @@ -144,7 +139,7 @@ def test_negative_create_empty_query(controller, target_sat): :parametrized: yes - :Steps: + :steps: 1. Attempt to create a bookmark with a random name, valid controller, and empty query. 2. List the bookmarks. @@ -172,7 +167,7 @@ def test_negative_create_same_name(controller, target_sat): :parametrized: yes - :Steps: + :steps: 1. Create a bookmark with a random name and valid controller. 2. Attempt to create a second bookmark, using the same name as the previous bookmark. @@ -202,7 +197,7 @@ def test_negative_create_null_public(controller, target_sat): :parametrized: yes - :Steps: + :steps: 1. Attempt to create a bookmark with a random name and valid controller, with public attribute set to None. @@ -233,7 +228,7 @@ def test_positive_update_name(controller, target_sat): :parametrized: yes - :Steps: + :steps: 1. Create a bookmark with a valid controller. 2. Update the bookmark with a random name. @@ -260,7 +255,7 @@ def test_negative_update_same_name(controller, target_sat): :parametrized: yes - :Steps: + :steps: 1. Create a bookmark with a random name and valid controller. 2. Create a second bookmark for the same controller. @@ -291,7 +286,7 @@ def test_negative_update_invalid_name(controller, target_sat): :parametrized: yes - :Steps: + :steps: 1. Create a bookmark with valid controller. 2. Attempt to update the bookmark with an invalid name. @@ -320,7 +315,7 @@ def test_positive_update_query(controller, target_sat): :parametrized: yes - :Steps: + :steps: 1. Create a bookmark with a valid controller. 2. Update the bookmark's query with a random value. @@ -347,7 +342,7 @@ def test_negative_update_empty_query(controller, target_sat): :parametrized: yes - :Steps: + :steps: 1. Create a bookmark for a valid controller. 2. Attempt to update the query to an empty value. @@ -376,7 +371,7 @@ def test_positive_update_public(controller, public, target_sat): :parametrized: yes - :Steps: + :steps: 1. Create a bookmark for a valid controller. 2. Update the bookmark's public attribute. diff --git a/tests/foreman/api/test_capsule.py b/tests/foreman/api/test_capsule.py index b8304979e70..3d499ccf16e 100644 --- a/tests/foreman/api/test_capsule.py +++ b/tests/foreman/api/test_capsule.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Capsule :Team: Platform -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ from fauxfactory import gen_string, gen_url import pytest @@ -43,7 +38,6 @@ def test_positive_update_capsule(target_sat, module_capsule_configured): :bz: 2077824 :customerscenario: true - """ new_name = f'{gen_string("alpha")}-{module_capsule_configured.name}' capsule = target_sat.api.SmartProxy().search( @@ -89,7 +83,6 @@ def test_negative_create_with_url(target_sat): :id: e48a6260-97e0-4234-a69c-77bbbcde85d6 :expectedresults: Proxy is not created - """ # Create a random proxy with pytest.raises(HTTPError) as context: @@ -125,7 +118,6 @@ def test_positive_update_url(request, target_sat): :id: 0305fd54-4e0c-4dd9-a537-d342c3dc867e :expectedresults: Capsule has the url updated - """ # Create fake capsule with name name = gen_string('alpha') @@ -159,8 +151,6 @@ def test_positive_import_puppet_classes( :CaseComponent: Puppet - :CaseLevel: Integration - :BZ: 1398695, 2142555 :customerscenario: true diff --git a/tests/foreman/api/test_capsulecontent.py b/tests/foreman/api/test_capsulecontent.py index e68872a0f15..ee16d1daa1e 100644 --- a/tests/foreman/api/test_capsulecontent.py +++ b/tests/foreman/api/test_capsulecontent.py @@ -5,17 +5,12 @@ :CaseAutomation: Automated -:CaseLevel: System - :CaseComponent: Capsule-Content :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from datetime import datetime import re diff --git a/tests/foreman/api/test_classparameters.py b/tests/foreman/api/test_classparameters.py index 446bdf14182..931a11edec2 100644 --- a/tests/foreman/api/test_classparameters.py +++ b/tests/foreman/api/test_classparameters.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Puppet :CaseImportance: Medium :Team: Rocket -:TestType: Functional - -:Upstream: No """ import json from random import choice diff --git a/tests/foreman/api/test_computeprofile.py b/tests/foreman/api/test_computeprofile.py index dffe47620b4..6d374bc8b34 100644 --- a/tests/foreman/api/test_computeprofile.py +++ b/tests/foreman/api/test_computeprofile.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ComputeResources :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest from requests.exceptions import HTTPError @@ -37,8 +32,6 @@ def test_positive_create_with_name(name, target_sat): :CaseImportance: Critical - :CaseLevel: Component - :parametrized: yes """ profile = target_sat.api.ComputeProfile(name=name).create() @@ -56,8 +49,6 @@ def test_negative_create(name, target_sat): :CaseImportance: Critical - :CaseLevel: Component - :parametrized: yes """ with pytest.raises(HTTPError): @@ -75,8 +66,6 @@ def test_positive_update_name(new_name, target_sat): :CaseImportance: Critical - :CaseLevel: Component - :parametrized: yes """ profile = target_sat.api.ComputeProfile().create() @@ -96,8 +85,6 @@ def test_negative_update_name(new_name, target_sat): :CaseImportance: Critical - :CaseLevel: Component - :parametrized: yes """ profile = target_sat.api.ComputeProfile().create() @@ -118,8 +105,6 @@ def test_positive_delete(new_name, target_sat): :CaseImportance: Critical - :CaseLevel: Component - :parametrized: yes """ profile = target_sat.api.ComputeProfile(name=new_name).create() diff --git a/tests/foreman/api/test_computeresource_azurerm.py b/tests/foreman/api/test_computeresource_azurerm.py index 74d6a733093..66f25f74b2b 100644 --- a/tests/foreman/api/test_computeresource_azurerm.py +++ b/tests/foreman/api/test_computeresource_azurerm.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ComputeResources-Azure :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -53,7 +48,6 @@ def test_positive_crud_azurerm_cr( :CaseImportance: Critical - :CaseLevel: Component """ # Create CR cr_name = gen_string('alpha') @@ -108,7 +102,6 @@ def test_positive_create_cloud_init_image( :expectedresults: Cloud init image should be added in AzureRM CR along with username - :CaseLevel: Integration """ assert module_azurerm_cloudimg.architecture.id == sat_azure_default_architecture.id @@ -128,7 +121,6 @@ def test_positive_check_available_networks(self, azurermclient, module_azurerm_c :expectedresults: All the networks from AzureRM CR should be available. - :CaseLevel: Integration """ cr_nws = module_azurerm_cr.available_networks() portal_nws = azurermclient.list_network() @@ -143,8 +135,6 @@ def test_gov_cloud_regions_from_azure_compute_resources(self): :CaseImportance: Medium - :CaseLevel: Acceptance - :CaseAutomation: ManualOnly :steps: @@ -267,9 +257,7 @@ def test_positive_azurerm_host_provisioned(self, class_host_ft, azureclient_host :id: ff27905f-fa3c-43ac-b969-9525b32f75f5 - :CaseLevel: Component - - ::CaseImportance: Critical + :CaseImportance: Critical :steps: 1. Create a AzureRM Compute Resource and provision host. @@ -300,8 +288,6 @@ def test_positive_azurerm_host_power_on_off(self, class_host_ft, azureclient_hos :id: 9ced29d7-d866-4d0c-ac27-78753b5b5a94 - :CaseLevel: System - :steps: 1. Create a AzureRM Compute Resource. 2. Provision a Host on Azure Cloud using above CR. @@ -421,9 +407,7 @@ def test_positive_azurerm_ud_host_provisioned(self, class_host_ud, azureclient_h :id: df496d7c-3443-4afe-b807-5bbfc90e866e - :CaseLevel: Component - - ::CaseImportance: Critical + :CaseImportance: Critical :steps: 1. Create a AzureRM Compute Resource and provision host. @@ -574,8 +558,6 @@ def test_positive_azurerm_custom_image_host_provisioned( :id: b5be5128-ad49-4dbd-a660-3e38ce012327 - :CaseLevel: System - :CaseImportance: Critical :steps: diff --git a/tests/foreman/api/test_computeresource_gce.py b/tests/foreman/api/test_computeresource_gce.py index 69bcdd7cbdf..2d088f4f2a2 100644 --- a/tests/foreman/api/test_computeresource_gce.py +++ b/tests/foreman/api/test_computeresource_gce.py @@ -7,17 +7,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ComputeResources-GCE :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random @@ -43,7 +38,6 @@ def test_positive_crud_gce_cr(self, sat_gce, sat_gce_org, sat_gce_loc, gce_cert) :CaseImportance: Critical - :CaseLevel: Component """ cr_name = gen_string('alpha') # Testing Create @@ -83,7 +77,6 @@ def test_positive_check_available_images(self, module_gce_compute, googleclient) :expectedresults: RHEL images from GCP are available to select in GCE CR - :CaseLevel: Integration """ satgce_images = module_gce_compute.available_images() googleclient_images = googleclient.list_templates( @@ -105,7 +98,6 @@ def test_positive_check_available_networks(self, module_gce_compute, googleclien :expectedresults: All the networks from Google CR should be available to select in GCE CR - :CaseLevel: Integration """ gceavailable_networks = module_gce_compute.available_networks() satgce_networks = [net['name'] for net in gceavailable_networks['results']] @@ -122,7 +114,6 @@ def test_positive_check_available_flavors(self, module_gce_compute): :expectedresults: All the flavors from Google CR should be available to select in GCE Host - :CaseLevel: Integration """ satgce_flavors = int(module_gce_compute.available_flavors()['total']) assert satgce_flavors > 1 @@ -143,7 +134,6 @@ def test_positive_create_finish_template_image( :expectedresults: Finish template image should be added in GCE CR along with username - :CaseLevel: Integration """ assert module_gce_finishimg.compute_resource.id == module_gce_compute.id assert module_gce_finishimg.uuid == gce_latest_rhel_uuid @@ -162,7 +152,6 @@ def test_positive_create_cloud_init_image( :expectedresults: Cloud init image should be added in GCE CR along with username - :CaseLevel: Integration """ assert module_gce_cloudimg.compute_resource.id == module_gce_compute.id assert module_gce_cloudimg.uuid == gce_custom_cloudinit_uuid @@ -243,9 +232,7 @@ def test_positive_gce_host_provisioned(self, class_host, google_host): :id: 889975f2-56ca-4584-95a7-21c513969630 - :CaseLevel: Component - - ::CaseImportance: Critical + :CaseImportance: Critical :steps: 1. Create a GCE Compute Resource @@ -269,8 +256,6 @@ def test_positive_gce_host_power_on_off(self, class_host, google_host): :id: b622c6fc-c45e-431d-8de3-9d0237873998 - :CaseLevel: System - :steps: 1. Create a GCE Compute Resource 2. Create a Hostgroup with all the Global and Foreman entities but diff --git a/tests/foreman/api/test_computeresource_libvirt.py b/tests/foreman/api/test_computeresource_libvirt.py index a02d741e899..5189af9d445 100644 --- a/tests/foreman/api/test_computeresource_libvirt.py +++ b/tests/foreman/api/test_computeresource_libvirt.py @@ -8,17 +8,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ComputeResources-libvirt :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -46,8 +41,6 @@ def test_positive_crud_libvirt_cr(module_target_sat, module_org, module_location :expectedresults: Compute resources are created with expected names :CaseImportance: Critical - - :CaseLevel: Component """ name = gen_string('alphanumeric') description = gen_string('alphanumeric') @@ -111,8 +104,6 @@ def test_positive_create_with_name_description( :CaseImportance: Critical - :CaseLevel: Component - :parametrized: yes """ compresource = module_target_sat.api.LibvirtComputeResource( @@ -137,8 +128,6 @@ def test_positive_create_with_orgs_and_locs(request, module_target_sat): locations assigned :CaseImportance: High - - :CaseLevel: Integration """ orgs = [module_target_sat.api.Organization().create() for _ in range(2)] locs = [module_target_sat.api.Location(organization=[org]).create() for org in orgs] @@ -161,8 +150,6 @@ def test_negative_create_with_invalid_name(name, module_target_sat, module_org, :CaseImportance: High - :CaseLevel: Component - :parametrized: yes """ with pytest.raises(HTTPError): @@ -183,8 +170,6 @@ def test_negative_create_with_same_name(request, module_target_sat, module_org, :expectedresults: Compute resources is not created :CaseImportance: High - - :CaseLevel: Component """ name = gen_string('alphanumeric') cr = module_target_sat.api.LibvirtComputeResource( @@ -212,8 +197,6 @@ def test_negative_create_with_url(module_target_sat, module_org, module_location :CaseImportance: High - :CaseLevel: Component - :parametrized: yes """ with pytest.raises(HTTPError): @@ -235,8 +218,6 @@ def test_negative_update_invalid_name( :CaseImportance: High - :CaseLevel: Component - :parametrized: yes """ name = gen_string('alphanumeric') @@ -259,8 +240,6 @@ def test_negative_update_same_name(request, module_target_sat, module_org, modul :expectedresults: Compute resources is not updated :CaseImportance: High - - :CaseLevel: Component """ name = gen_string('alphanumeric') compresource = module_target_sat.api.LibvirtComputeResource( @@ -291,8 +270,6 @@ def test_negative_update_url(url, request, module_target_sat, module_org, module :CaseImportance: High - :CaseLevel: Component - :parametrized: yes """ compresource = module_target_sat.api.LibvirtComputeResource( diff --git a/tests/foreman/api/test_contentcredentials.py b/tests/foreman/api/test_contentcredentials.py index aaad0322976..3f088ae6253 100644 --- a/tests/foreman/api/test_contentcredentials.py +++ b/tests/foreman/api/test_contentcredentials.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ContentCredentials :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from copy import copy diff --git a/tests/foreman/api/test_contentview.py b/tests/foreman/api/test_contentview.py index f2015bd11cf..0ef7ac02105 100644 --- a/tests/foreman/api/test_contentview.py +++ b/tests/foreman/api/test_contentview.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ContentViews :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random @@ -118,8 +113,6 @@ def test_positive_subscribe_host( :expectedresults: It is possible to create a host and set its 'content_view_id' facet attribute - :CaseLevel: Integration - :CaseAutomation: Automated :CaseImportance: High @@ -153,8 +146,6 @@ def test_positive_clone_within_same_env(self, class_published_cloned_cv, module_ :expectedresults: Cloned content view can be published and promoted to the same environment as the original content view - :CaseLevel: Integration - :CaseImportance: High """ class_published_cloned_cv.read().version[0].promote(data={'environment_ids': module_lce.id}) @@ -173,8 +164,6 @@ def test_positive_clone_with_diff_env( :expectedresults: Cloned content view can be published and promoted to a different environment as the original content view - :CaseLevel: Integration - :CaseImportance: Medium """ le_clone = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() @@ -188,8 +177,6 @@ def test_positive_add_custom_content(self, module_product, module_org, module_ta :expectedresults: Custom content assigned and present in content view - :CaseLevel: Integration - :CaseImportance: Critical """ yum_repo = module_target_sat.api.Repository(product=module_product).create() @@ -214,8 +201,6 @@ def test_positive_add_custom_module_streams( :expectedresults: Custom content (module streams) assigned and present in content view - :CaseLevel: Integration - :CaseImportance: High """ yum_repo = module_target_sat.api.Repository( @@ -241,8 +226,6 @@ def test_negative_add_dupe_repos( :expectedresults: User cannot add repos multiple times to the view - :CaseLevel: Integration - :CaseImportance: Low """ yum_repo = module_target_sat.api.Repository(product=module_product).create() @@ -265,8 +248,6 @@ def test_positive_add_sha512_rpm(self, content_view, module_org, module_target_s :expectedresults: Custom sha512 assigned and present in content view - :CaseLevel: Integration - :CaseComponent: Pulp :team: Rocket @@ -428,8 +409,6 @@ def test_positive_publish_with_content_multiple(self, content_view, module_org): content view can be published several times, and each content view version has at least one package. - :CaseLevel: Integration - :CaseImportance: High """ content_view.repository = [self.yum_repo] @@ -456,8 +435,6 @@ def test_positive_publish_composite_multiple_content_once(self, module_org, modu :expectedresults: Composite content view is published and corresponding version is assigned to it. - :CaseLevel: Integration - :CaseImportance: Critical """ composite_cv = module_target_sat.api.ContentView( @@ -483,8 +460,6 @@ def test_positive_publish_composite_multiple_content_multiple( :expectedresults: Composite content view is published several times and corresponding versions are assigned to it. - :CaseLevel: Integration - :CaseImportance: High """ composite_cv = module_target_sat.api.ContentView( @@ -508,8 +483,6 @@ def test_positive_promote_with_yum_multiple(self, content_view, module_org, modu version is in ``REPEAT + 1`` lifecycle environments and it has at least one package. - :CaseLevel: Integration - :CaseImportance: High """ content_view.repository = [self.yum_repo] @@ -542,8 +515,6 @@ def test_positive_add_to_composite(self, content_view, module_org, module_target :expectedresults: Content view can be created and assigned to composite one through content view versions mechanism - :CaseLevel: Integration - :CaseImportance: Critical """ content_view.repository = [self.yum_repo] @@ -574,8 +545,6 @@ def test_negative_add_components_to_composite( :expectedresults: User cannot add components to the view - :CaseLevel: Integration - :CaseImportance: Low """ content_view.repository = [self.yum_repo] @@ -605,8 +574,6 @@ def test_positive_promote_composite_multiple_content_once( :expectedresults: Composite content view version points to ``Library + 1`` lifecycle environments after the promotions. - :CaseLevel: Integration - :CaseImportance: High """ composite_cv = module_target_sat.api.ContentView( @@ -634,8 +601,6 @@ def test_positive_promote_composite_multiple_content_multiple( :expectedresults: Composite content view version points to ``Library + random`` lifecycle environments after the promotions. - :CaseLevel: Integration - :CaseImportance: High """ composite_cv = module_target_sat.api.ContentView( @@ -663,8 +628,6 @@ def test_positive_promote_out_of_sequence(self, content_view, module_org): :expectedresults: Content view promoted out of sequence properly - :CaseLevel: Integration - :CaseImportance: Medium """ for _ in range(REPEAT): @@ -699,8 +662,6 @@ def test_positive_publish_multiple_repos(self, content_view, module_org, module_ :expectedresults: Content view publish should not raise an exception. - :CaseLevel: Integration - :CaseComponent: Pulp :team: Rocket @@ -793,8 +754,6 @@ def test_ccv_audit_scenarios(self, module_org, target_sat): :expectedresults: When appropriate, a ccv and it's cvs needs_publish flags get set or unset - :CaseLevel: Integration - :CaseImportance: High """ composite_cv = target_sat.api.ContentView(composite=True).create() @@ -854,8 +813,6 @@ def test_check_needs_publish_flag(self, target_sat): :expectedresults: The publish_only_if_needed flag is working as intended, and is defaulted to false - :CaseLevel: Integration - :CaseImportance: High """ cv = target_sat.api.ContentView().create() @@ -989,8 +946,6 @@ def test_positive_add_rh(self): :expectedresults: RH Content assigned and present in a view - :CaseLevel: Integration - :CaseImportance: High """ assert len(self.yumcv.repository) == 1 @@ -1005,8 +960,6 @@ def test_positive_add_rh_custom_spin(self, target_sat): :expectedresults: Filtered RH content is available and can be seen in a view - :CaseLevel: Integration - :CaseImportance: High """ # content_view ← cv_filter @@ -1033,8 +986,6 @@ def test_positive_update_rh_custom_spin(self, target_sat): :expectedresults: edited content view save is successful and info is updated - :CaseLevel: Integration - :CaseImportance: High """ cvf = target_sat.api.ErratumContentViewFilter( @@ -1059,8 +1010,6 @@ def test_positive_publish_rh(self, module_org, content_view): :expectedresults: Content view can be published - :CaseLevel: Integration - :CaseImportance: Critical """ content_view.repository = [self.repo] @@ -1077,8 +1026,6 @@ def test_positive_publish_rh_custom_spin(self, module_org, content_view, module_ :expectedresults: Content view can be published - :CaseLevel: Integration - :CaseImportance: High """ content_view.repository = [self.repo] @@ -1097,8 +1044,6 @@ def test_positive_promote_rh(self, module_org, content_view, module_lce): :expectedresults: Content view can be promoted - :CaseLevel: Integration - :CaseImportance: Critical """ content_view.repository = [self.repo] @@ -1122,8 +1067,6 @@ def test_positive_promote_rh_custom_spin(self, content_view, module_lce, module_ :expectedresults: Content view can be promoted - :CaseLevel: Integration - :CaseImportance: High """ content_view.repository = [self.repo] @@ -1158,8 +1101,6 @@ def test_cv_audit_scenarios(self, module_product, target_sat): :expectedresults: All of the above steps should results in the CV needing to be be published - :CaseLevel: Integration - :CaseImportance: High """ # needs_publish is set to true when created @@ -1243,8 +1184,6 @@ def test_positive_admin_user_actions( :expectedresults: The user can Read, Modify, Delete, Publish, Promote the content views - :CaseLevel: Integration - :CaseImportance: Critical """ user_login = gen_string('alpha') @@ -1303,8 +1242,6 @@ def test_positive_readonly_user_actions(target_sat, function_role, content_view, :expectedresults: User with read-only role for content view can view the repository in the content view - :CaseLevel: Integration - :CaseImportance: Critical """ user_login = gen_string('alpha') @@ -1369,8 +1306,6 @@ def test_negative_readonly_user_actions( :BZ: 1922134 - :CaseLevel: Integration - :CaseImportance: Critical """ user_login = gen_string('alpha') @@ -1446,8 +1381,6 @@ def test_negative_non_readonly_user_actions(target_sat, content_view, function_r :expectedresults: the user can perform different operations against content view, but not read it - :CaseLevel: Integration - :CaseImportance: Critical """ user_login = gen_string('alpha') @@ -1525,8 +1458,6 @@ def test_positive_add_custom_ostree_content(self, content_view): :expectedresults: Custom ostree content assigned and present in content view - :CaseLevel: Integration - :CaseImportance: High """ assert len(content_view.repository) == 0 @@ -1544,8 +1475,6 @@ def test_positive_publish_custom_ostree(self, content_view): :expectedresults: Content-view with Custom ostree published successfully - :CaseLevel: Integration - :CaseImportance: High """ content_view.repository = [self.ostree_repo] @@ -1562,8 +1491,6 @@ def test_positive_promote_custom_ostree(self, content_view, module_lce): :expectedresults: Content-view with custom ostree contents promoted successfully - :CaseLevel: Integration - :CaseImportance: High """ content_view.repository = [self.ostree_repo] @@ -1584,8 +1511,6 @@ def test_positive_publish_promote_with_custom_ostree_and_other(self, content_vie :expectedresults: Content-view with custom ostree and other contents promoted successfully - :CaseLevel: Integration - :CaseImportance: High """ content_view.repository = [self.ostree_repo, self.yum_repo, self.docker_repo] @@ -1628,8 +1553,6 @@ def test_positive_add_rh_ostree_content(self, content_view): :expectedresults: RH atomic ostree content assigned and present in content view - :CaseLevel: Integration - :CaseImportance: High """ assert len(content_view.repository) == 0 @@ -1647,8 +1570,6 @@ def test_positive_publish_RH_ostree(self, content_view): :expectedresults: Content-view with RH ostree contents published successfully - :CaseLevel: Integration - :CaseImportance: High """ content_view.repository = [self.repo] @@ -1665,8 +1586,6 @@ def test_positive_promote_RH_ostree(self, content_view, module_lce): :expectedresults: Content-view with RH ostree contents promoted successfully - :CaseLevel: Integration - :CaseImportance: High """ content_view.repository = [self.repo] @@ -1689,8 +1608,6 @@ def test_positive_publish_promote_with_RH_ostree_and_other( :expectedresults: Content-view with RH ostree and other contents promoted successfully - :CaseLevel: Integration - :CaseImportance: High """ repo_id = module_target_sat.api_factory.enable_rhrepo_and_fetchid( diff --git a/tests/foreman/api/test_contentviewfilter.py b/tests/foreman/api/test_contentviewfilter.py index 8e4ac95c75d..6360bf5da16 100644 --- a/tests/foreman/api/test_contentviewfilter.py +++ b/tests/foreman/api/test_contentviewfilter.py @@ -8,17 +8,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ContentViews :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import http from random import randint @@ -79,8 +74,6 @@ def test_negative_get_with_no_args(self, target_sat): :expectedresults: An HTTP 200 response is received if a GET request is issued with no arguments specified. - :CaseLevel: Integration - :CaseImportance: Low """ response = client.get( @@ -99,8 +92,6 @@ def test_negative_get_with_bad_args(self, target_sat): :expectedresults: An HTTP 200 response is received if a GET request is issued with bad arguments specified. - :CaseLevel: Integration - :CaseImportance: Low """ response = client.get( @@ -123,7 +114,6 @@ def test_positive_create_erratum_with_name(self, name, content_view, target_sat) :expectedresults: Content view filter created successfully and has correct name and type - :CaseLevel: Integration """ cvf = target_sat.api.ErratumContentViewFilter(content_view=content_view, name=name).create() assert cvf.name == name @@ -141,8 +131,6 @@ def test_positive_create_pkg_group_with_name(self, name, content_view, target_sa :expectedresults: Content view filter created successfully and has correct name and type - :CaseLevel: Integration - :CaseImportance: Medium """ cvf = target_sat.api.PackageGroupContentViewFilter( @@ -164,8 +152,6 @@ def test_positive_create_rpm_with_name(self, name, content_view, target_sat): :expectedresults: Content view filter created successfully and has correct name and type - :CaseLevel: Integration - :CaseImportance: Medium """ cvf = target_sat.api.RPMContentViewFilter(content_view=content_view, name=name).create() @@ -184,7 +170,6 @@ def test_positive_create_with_inclusion(self, inclusion, content_view, target_sa :expectedresults: Content view filter created successfully and has correct inclusion value - :CaseLevel: Integration """ cvf = target_sat.api.RPMContentViewFilter( content_view=content_view, inclusion=inclusion @@ -203,8 +188,6 @@ def test_positive_create_with_description(self, description, content_view, targe :expectedresults: Content view filter created successfully and has correct description - :CaseLevel: Integration - :CaseImportance: Low """ cvf = target_sat.api.RPMContentViewFilter( @@ -222,7 +205,6 @@ def test_positive_create_with_repo(self, content_view, sync_repo, target_sat): :expectedresults: Content view filter created successfully and has repository assigned - :CaseLevel: Integration """ cvf = target_sat.api.RPMContentViewFilter( content_view=content_view, @@ -246,8 +228,6 @@ def test_positive_create_with_original_packages( :expectedresults: Content view filter created successfully and has 'original packages' value - :CaseLevel: Integration - :CaseImportance: Medium """ cvf = target_sat.api.RPMContentViewFilter( @@ -271,7 +251,6 @@ def test_positive_create_with_docker_repos( :expectedresults: Content view filter created successfully and has both repositories assigned (yum and docker) - :CaseLevel: Integration """ docker_repository = module_target_sat.api.Repository( content_type='docker', @@ -305,7 +284,6 @@ def test_positive_create_with_module_streams( :expectedresults: Content view filter created successfully for both Include and Exclude Type - :CaseLevel: Integration """ content_view.repository += [sync_repo_module_stream] content_view.update(['repository']) @@ -331,8 +309,6 @@ def test_negative_create_with_invalid_name(self, name, content_view, target_sat) :expectedresults: Content view filter was not created - :CaseLevel: Integration - :CaseImportance: Critical """ with pytest.raises(HTTPError): @@ -346,8 +322,6 @@ def test_negative_create_with_same_name(self, content_view, target_sat): :expectedresults: Second content view filter was not created - :CaseLevel: Integration - :CaseImportance: Low """ kwargs = {'content_view': content_view, 'name': gen_string('alpha')} @@ -364,8 +338,6 @@ def test_negative_create_without_cv(self, target_sat): :expectedresults: Content view filter is not created - :CaseLevel: Integration - :CaseImportance: Low """ with pytest.raises(HTTPError): @@ -380,8 +352,6 @@ def test_negative_create_with_invalid_repo_id(self, content_view, target_sat): :expectedresults: Content view filter is not created - :CaseLevel: Integration - :CaseImportance: Low """ with pytest.raises(HTTPError): @@ -398,8 +368,6 @@ def test_positive_delete_by_id(self, content_view, target_sat): :expectedresults: Content view filter was deleted - :CaseLevel: Integration - :CaseImportance: Critical """ cvf = target_sat.api.RPMContentViewFilter(content_view=content_view).create() @@ -419,7 +387,6 @@ def test_positive_update_name(self, name, content_view, target_sat): :expectedresults: Content view filter updated successfully and name was changed - :CaseLevel: Integration """ cvf = target_sat.api.RPMContentViewFilter(content_view=content_view).create() cvf.name = name @@ -437,8 +404,6 @@ def test_positive_update_description(self, description, content_view, target_sat :expectedresults: Content view filter updated successfully and description was changed - :CaseLevel: Integration - :CaseImportance: Low """ cvf = target_sat.api.RPMContentViewFilter(content_view=content_view).create() @@ -458,7 +423,6 @@ def test_positive_update_inclusion(self, inclusion, content_view, target_sat): :expectedresults: Content view filter updated successfully and inclusion value was changed - :CaseLevel: Integration """ cvf = target_sat.api.RPMContentViewFilter(content_view=content_view).create() cvf.inclusion = inclusion @@ -474,7 +438,6 @@ def test_positive_update_repo(self, module_product, sync_repo, content_view, tar :expectedresults: Content view filter updated successfully and has new repository assigned - :CaseLevel: Integration """ cvf = target_sat.api.RPMContentViewFilter( content_view=content_view, @@ -499,8 +462,6 @@ def test_positive_update_repos(self, module_product, sync_repo, content_view, ta :expectedresults: Content view filter updated successfully and has new repositories assigned - :CaseLevel: Integration - :CaseImportance: Low """ cvf = target_sat.api.RPMContentViewFilter( @@ -533,7 +494,6 @@ def test_positive_update_original_packages( :expectedresults: Content view filter updated successfully and 'original packages' value was changed - :CaseLevel: Integration """ cvf = target_sat.api.RPMContentViewFilter( content_view=content_view, @@ -556,7 +516,6 @@ def test_positive_update_repo_with_docker( :expectedresults: Content view filter was updated successfully and has both repositories assigned (yum and docker) - :CaseLevel: Integration """ cvf = target_sat.api.RPMContentViewFilter( content_view=content_view, @@ -588,8 +547,6 @@ def test_negative_update_name(self, name, content_view, target_sat): :expectedresults: Content view filter was not updated - :CaseLevel: Integration - :CaseImportance: Low """ cvf = target_sat.api.RPMContentViewFilter(content_view=content_view).create() @@ -605,8 +562,6 @@ def test_negative_update_same_name(self, content_view, target_sat): :expectedresults: Content view filter was not updated - :CaseLevel: Integration - :CaseImportance: Low """ name = gen_string('alpha', 8) @@ -625,7 +580,6 @@ def test_negative_update_cv_by_id(self, content_view, target_sat): :expectedresults: Content view filter was not updated - :CaseLevel: Integration """ cvf = target_sat.api.RPMContentViewFilter(content_view=content_view).create() cvf.content_view.id = gen_integer(10000, 99999) @@ -641,7 +595,6 @@ def test_negative_update_repo_by_id(self, sync_repo, content_view, target_sat): :expectedresults: Content view filter was not updated - :CaseLevel: Integration """ cvf = target_sat.api.RPMContentViewFilter( content_view=content_view, @@ -660,8 +613,6 @@ def test_negative_update_repo(self, module_product, sync_repo, content_view, tar :expectedresults: Content view filter was not updated - :CaseLevel: Integration - :CaseImportance: Low """ cvf = target_sat.api.RPMContentViewFilter( @@ -697,8 +648,6 @@ def test_positive_check_filter_applied_flag( :expectedresults: 1. Filters applied flag is set correctly per usage. - :CaseLevel: Integration - :CaseImportance: Medium """ @@ -784,7 +733,6 @@ def test_positive_promote_module_stream_filter( :expectedresults: Content View should get published and promoted successfully with correct Module Stream count. - :CaseLevel: Integration """ # Exclude module stream filter content_view = content_view_module_stream @@ -840,7 +788,6 @@ def test_positive_include_exclude_module_stream_filter( :expectedresults: Module Stream count changes automatically after including or excluding modular errata - :CaseLevel: Integration """ content_view = content_view_module_stream cv_filter = target_sat.api.ErratumContentViewFilter( @@ -890,7 +837,6 @@ def test_positive_multi_level_filters(self, content_view_module_stream, target_s :expectedresults: Verify module stream and errata count should correct - :CaseLevel: Integration """ content_view = content_view_module_stream # apply include errata filter @@ -940,7 +886,6 @@ def test_positive_dependency_solving_module_stream_filter( :expectedresults: Verify dependant/non dependant module streams are getting fetched. - :CaseLevel: Integration """ content_view = content_view_module_stream content_view.solve_dependencies = True diff --git a/tests/foreman/api/test_contentviewversion.py b/tests/foreman/api/test_contentviewversion.py index 5953edeed42..5f3414456e3 100644 --- a/tests/foreman/api/test_contentviewversion.py +++ b/tests/foreman/api/test_contentviewversion.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ContentViews :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -52,8 +47,6 @@ def test_positive_create(module_cv): :expectedresults: Content View Version is created. - :CaseLevel: Integration - :CaseImportance: Critical """ # Fetch content view for latest information @@ -76,8 +69,6 @@ def test_negative_create(module_org, module_target_sat): :expectedresults: Content View Version is not created - :CaseLevel: Integration - :CaseImportance: Critical """ # The default content view cannot be published @@ -99,8 +90,6 @@ def test_positive_promote_valid_environment(module_lce_cv, module_org, module_ta :expectedresults: Promotion succeeds. - :CaseLevel: Integration - :CaseImportance: Critical """ # Create a new content view... @@ -131,8 +120,6 @@ def test_positive_promote_out_of_sequence_environment(module_org, module_lce_cv, :id: e88405de-843d-4279-9d81-cedaab7c23cf :expectedresults: The promotion succeeds. - - :CaseLevel: Integration """ # Create a new content view... cv = module_target_sat.api.ContentView(organization=module_org).create() @@ -159,8 +146,6 @@ def test_negative_promote_valid_environment(module_lce_cv): :expectedresults: The promotion fails. - :CaseLevel: Integration - :CaseImportance: Low """ lce1, _, default_cvv = module_lce_cv @@ -176,8 +161,6 @@ def test_negative_promote_out_of_sequence_environment(module_lce_cv, module_org, :id: 621d1bb6-92c6-4209-8369-6ea14a4c8a01 :expectedresults: The promotion fails. - - :CaseLevel: Integration """ # Create a new content view... cv = module_target_sat.api.ContentView(organization=module_org).create() @@ -209,8 +192,6 @@ def test_positive_delete(module_org, module_product, module_target_sat): :expectedresults: Content version deleted successfully - :CaseLevel: Integration - :CaseImportance: Critical """ key_content = DataFile.ZOO_CUSTOM_GPG_KEY.read_text() @@ -253,8 +234,6 @@ def test_positive_delete_non_default(module_org, module_target_sat): :expectedresults: Content view version deleted successfully - :CaseLevel: Integration - :CaseImportance: Critical """ content_view = module_target_sat.api.ContentView(organization=module_org).create() @@ -289,8 +268,6 @@ def test_positive_delete_composite_version(module_org, module_target_sat): :expectedresults: Content version deleted successfully - :CaseLevel: Integration - :BZ: 1276479 """ # Create product with repository and publish it @@ -330,8 +307,6 @@ def test_negative_delete(module_org, module_target_sat): :expectedresults: Content view version is not deleted - :CaseLevel: Integration - :CaseImportance: Critical """ content_view = module_target_sat.api.ContentView(organization=module_org).create() @@ -352,7 +327,7 @@ def test_positive_remove_renamed_cv_version_from_default_env(module_org, module_ :id: 7d5961d0-6a9a-4610-979e-cbc4ddbc50ca - :Steps: + :steps: 1. Create a content view 2. Add a yum repo to the content view @@ -362,8 +337,6 @@ def test_positive_remove_renamed_cv_version_from_default_env(module_org, module_ :expectedresults: content view version is removed from Library environment - - :CaseLevel: Integration """ new_name = gen_string('alpha') # create yum product and repo @@ -405,7 +378,7 @@ def test_positive_remove_qe_promoted_cv_version_from_default_env(module_org, mod :id: c7795762-93bd-419c-ac49-d10dc26b842b - :Steps: + :steps: 1. Create a content view 2. Add docker repo(s) to it @@ -416,8 +389,6 @@ def test_positive_remove_qe_promoted_cv_version_from_default_env(module_org, mod :expectedresults: Content view version exist only in DEV, QE and not in Library - - :CaseLevel: Integration """ lce_dev = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() lce_qe = module_target_sat.api.LifecycleEnvironment( @@ -465,7 +436,7 @@ def test_positive_remove_prod_promoted_cv_version_from_default_env(module_org, m :id: 24911876-7c2a-4a12-a3aa-98051dfda29d - :Steps: + :steps: 1. Create a content view 2. Add yum repositories and docker repositories to CV @@ -476,8 +447,6 @@ def test_positive_remove_prod_promoted_cv_version_from_default_env(module_org, m :expectedresults: Content view version exist only in DEV, QE, PROD and not in Library - - :CaseLevel: Integration """ lce_dev = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() lce_qe = module_target_sat.api.LifecycleEnvironment( @@ -534,7 +503,7 @@ def test_positive_remove_cv_version_from_env(module_org, module_target_sat): :id: 17cf18bf-09d5-4641-b0e0-c50e628fa6c8 - :Steps: + :steps: 1. Create a content view 2. Add a yum repo and a docker repo to the content view @@ -548,8 +517,6 @@ def test_positive_remove_cv_version_from_env(module_org, module_target_sat): :expectedresults: Content view version exist in Library, DEV, QE, STAGE, PROD - - :CaseLevel: Integration """ lce_dev = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() lce_qe = module_target_sat.api.LifecycleEnvironment( @@ -616,7 +583,7 @@ def test_positive_remove_cv_version_from_multi_env(module_org, module_target_sat :id: 18b86a68-8e6a-43ea-b95e-188fba125a26 - :Steps: + :steps: 1. Create a content view 2. Add a yum repo and a docker repo to the content view @@ -627,8 +594,6 @@ def test_positive_remove_cv_version_from_multi_env(module_org, module_target_sat :expectedresults: Content view version exists only in Library, DEV - :CaseLevel: Integration - :CaseImportance: Low """ lce_dev = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() @@ -693,7 +658,7 @@ def test_positive_delete_cv_promoted_to_multi_env(module_org, module_target_sat) :id: c164bd97-e710-4a5a-9c9f-657e6bed804b - :Steps: + :steps: 1. Create a content view 2. Add a yum repo and a docker repo to the content view @@ -705,8 +670,6 @@ def test_positive_delete_cv_promoted_to_multi_env(module_org, module_target_sat) :expectedresults: The content view doesn't exist - :CaseLevel: Integration - :CaseImportance: Critical """ lce_dev = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() @@ -770,7 +733,7 @@ def test_positive_remove_cv_version_from_env_with_host_registered(): :id: a5b9ba8b-80e6-4435-bc0a-041b3fda227c - :Steps: + :steps: 1. Create a content view cv1 2. Add a yum repo to the content view @@ -795,8 +758,6 @@ def test_positive_remove_cv_version_from_env_with_host_registered(): 5. At content-host some package from cv1 is installable :CaseAutomation: NotAutomated - - :CaseLevel: System """ @@ -810,7 +771,7 @@ def test_positive_delete_cv_multi_env_promoted_with_host_registered(): :id: 10699af9-617e-4930-9c80-2827a0ba52eb - :Steps: + :steps: 1. Create two content views, cv1 and cv2 2. Add a yum repo to both content views @@ -837,8 +798,6 @@ def test_positive_delete_cv_multi_env_promoted_with_host_registered(): 6. At content-host some package from cv2 is installable :CaseAutomation: NotAutomated - - :CaseLevel: System """ @@ -850,7 +809,7 @@ def test_positive_remove_cv_version_from_multi_env_capsule_scenario(): :id: 1e8a8e64-eec8-49e0-b121-919c53f416d2 - :Steps: + :steps: 1. Create a content view 2. module_lce_cv satellite to use a capsule and to sync all lifecycle @@ -874,6 +833,4 @@ def test_positive_remove_cv_version_from_multi_env_capsule_scenario(): Library and DEV and exists only in QE and PROD :CaseAutomation: NotAutomated - - :CaseLevel: System """ diff --git a/tests/foreman/api/test_convert2rhel.py b/tests/foreman/api/test_convert2rhel.py index ce2e78d85b2..781f571ed02 100644 --- a/tests/foreman/api/test_convert2rhel.py +++ b/tests/foreman/api/test_convert2rhel.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Integration - :CaseComponent: Registration :Team: Rocket -:TestType: Functional - -:Upstream: No """ import pytest import requests @@ -255,7 +250,7 @@ def test_convert2rhel_oracle(module_target_sat, oracle, activation_key_rhel, ver :id: 7fd393f0-551a-4de0-acdd-7f026b485f79 - :Steps: + :steps: 0. Have host registered to Satellite 1. Check for operating system 2. Convert host to RHEL @@ -307,7 +302,7 @@ def test_convert2rhel_centos(module_target_sat, centos, activation_key_rhel, ver :id: 6f698440-7d85-4deb-8dd9-363ea9003b92 - :Steps: + :steps: 0. Have host registered to Satellite 1. Check for operating system 2. Convert host to RHEL diff --git a/tests/foreman/api/test_discoveredhost.py b/tests/foreman/api/test_discoveredhost.py index b3c9ca621b2..1bf31452aeb 100644 --- a/tests/foreman/api/test_discoveredhost.py +++ b/tests/foreman/api/test_discoveredhost.py @@ -8,11 +8,6 @@ :CaseAutomation: Automated -:CaseLevel: System - -:TestType: Functional - -:Upstream: No """ import re @@ -192,7 +187,7 @@ def test_positive_provision_pxe_host( :Setup: Provisioning and discovery should be configured - :Steps: + :steps: 1. Boot up the host to discover 2. Provision the host @@ -245,7 +240,7 @@ def test_positive_provision_pxe_less_host( :Setup: Provisioning should be configured and a host should be discovered - :Steps: PUT /api/v2/discovered_hosts/:id + :steps: PUT /api/v2/discovered_hosts/:id :expectedresults: Host should be provisioned successfully @@ -287,7 +282,7 @@ def test_positive_auto_provision_pxe_host( :Setup: Provisioning should be configured and a host should be discovered - :Steps: POST /api/v2/discovered_hosts/:id/auto_provision + :steps: POST /api/v2/discovered_hosts/:id/auto_provision :expectedresults: Selected Host should be auto-provisioned successfully @@ -316,7 +311,7 @@ def test_positive_auto_provision_all( :Setup: Provisioning should be configured and more than one host should be discovered - :Steps: POST /api/v2/discovered_hosts/auto_provision_all + :steps: POST /api/v2/discovered_hosts/auto_provision_all :expectedresults: All discovered hosts should be auto-provisioned successfully @@ -351,7 +346,7 @@ def test_positive_refresh_facts_pxe_host(self, module_target_sat): be discovered 2. Add a NIC on discovered host - :Steps: PUT /api/v2/discovered_hosts/:id/refresh_facts + :steps: PUT /api/v2/discovered_hosts/:id/refresh_facts :expectedresults: Added Fact should be displayed on refreshing the facts @@ -380,7 +375,7 @@ def test_positive_reboot_pxe_host( :Setup: Provisioning should be configured and a host should be discovered via PXE boot. - :Steps: PUT /api/v2/discovered_hosts/:id/reboot + :steps: PUT /api/v2/discovered_hosts/:id/reboot :expectedresults: Selected host should be rebooted successfully @@ -425,7 +420,7 @@ def test_positive_reboot_all_pxe_hosts( :Setup: Provisioning should be configured and hosts should be discovered via PXE boot. - :Steps: PUT /api/v2/discovered_hosts/reboot_all + :steps: PUT /api/v2/discovered_hosts/reboot_all :expectedresults: All discovered hosst should be rebooted successfully @@ -495,15 +490,13 @@ def test_positive_upload_facts(self, target_sat): :BZ: 1349364, 1392919 - :Steps: + :steps: 1. POST /api/v2/discovered_hosts/facts 2. Read the created discovered host :expectedresults: Host should be created successfully - :CaseLevel: Integration - :BZ: 1731112 """ name = gen_choice(list(valid_data_list().values())) @@ -521,7 +514,7 @@ def test_positive_delete_pxe_host(self, target_sat): :Setup: Provisioning should be configured and a host should be discovered - :Steps: DELETE /api/v2/discovered_hosts/:id + :steps: DELETE /api/v2/discovered_hosts/:id :expectedresults: Discovered Host should be deleted successfully """ diff --git a/tests/foreman/api/test_discoveryrule.py b/tests/foreman/api/test_discoveryrule.py index c8ad3dc062b..5e0666c9b2d 100644 --- a/tests/foreman/api/test_discoveryrule.py +++ b/tests/foreman/api/test_discoveryrule.py @@ -8,13 +8,8 @@ :Team: Rocket -:TestType: Functional - -:CaseLevel: Acceptance - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_choice, gen_integer, gen_string import pytest diff --git a/tests/foreman/api/test_docker.py b/tests/foreman/api/test_docker.py index 68a2fe6e378..b11cc4bc126 100644 --- a/tests/foreman/api/test_docker.py +++ b/tests/foreman/api/test_docker.py @@ -4,13 +4,8 @@ :CaseAutomation: Automated -:CaseLevel: Component - -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from random import choice, randint, shuffle @@ -173,7 +168,6 @@ def test_positive_create_repos_using_same_product(self, module_product, module_t :expectedresults: Multiple docker repositories are created with a Docker upstream repository and they all belong to the same product. - :CaseLevel: Integration """ for _ in range(randint(2, 5)): repo = _create_repository(module_target_sat, module_product) @@ -189,7 +183,6 @@ def test_positive_create_repos_using_multiple_products(self, module_org, module_ Docker upstream repository and they all belong to their respective products. - :CaseLevel: Integration """ for _ in range(randint(2, 5)): product = module_target_sat.api.Product(organization=module_org).create() @@ -326,8 +319,6 @@ class TestDockerContentView: :CaseComponent: ContentViews :team: Phoenix-content - - :CaseLevel: Integration """ @pytest.mark.tier2 @@ -983,8 +974,6 @@ class TestDockerActivationKey: :CaseComponent: ActivationKeys :team: Phoenix-subscriptions - - :CaseLevel: Integration """ @pytest.mark.tier2 @@ -1019,7 +1008,6 @@ def test_positive_remove_docker_repo_cv( :expectedresults: Docker-based content view can be added and then removed from the activation key. - :CaseLevel: Integration """ content_view = content_view_publish_promote ak = module_target_sat.api.ActivationKey( diff --git a/tests/foreman/api/test_environment.py b/tests/foreman/api/test_environment.py index cb965244211..02f7a3928e3 100644 --- a/tests/foreman/api/test_environment.py +++ b/tests/foreman/api/test_environment.py @@ -8,17 +8,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Puppet :Team: Rocket -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -205,8 +200,6 @@ def test_positive_update_loc(module_puppet_environment): field. :BZ: 1262029 - - :CaseLevel: Integration """ names = {'location', 'location_ids', 'locations'} attributes = set(module_puppet_environment.update_json([]).keys()) @@ -223,8 +216,6 @@ def test_positive_update_org(module_puppet_environment): ``organization`` field. :BZ: 1262029 - - :CaseLevel: Integration """ names = {'organization', 'organization_ids', 'organizations'} attributes = set(module_puppet_environment.update_json([]).keys()) diff --git a/tests/foreman/api/test_errata.py b/tests/foreman/api/test_errata.py index 601f5f3832d..df24f7c66d2 100644 --- a/tests/foreman/api/test_errata.py +++ b/tests/foreman/api/test_errata.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: System - :CaseComponent: ErrataManagement :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ # For ease of use hc refers to host-collection throughout this document from time import sleep @@ -167,12 +162,10 @@ def test_positive_install_in_hc(module_org, activation_key, custom_repo, target_ :Setup: Errata synced on satellite server. - :Steps: PUT /api/v2/hosts/bulk/update_content + :steps: PUT /api/v2/hosts/bulk/update_content :expectedresults: errata is installed in the host-collection. - :CaseLevel: System - :BZ: 1983043 """ for client in content_hosts: @@ -233,8 +226,6 @@ def test_positive_install_multiple_in_host( :CaseImportance: Medium :parametrized: yes - - :CaseLevel: System """ rhel_contenthost.install_katello_ca(target_sat) rhel_contenthost.register_contenthost(module_org.label, activation_key.name) @@ -275,12 +266,10 @@ def test_positive_list(module_org, custom_repo, target_sat): :Setup: Errata synced on satellite server. - :Steps: Create two repositories each synced and containing errata + :steps: Create two repositories each synced and containing errata :expectedresults: Check that the errata belonging to one repo is not showing in the other. - - :CaseLevel: System """ repo1 = target_sat.api.Repository(id=custom_repo['repository-id']).read() repo2 = target_sat.api.Repository( @@ -309,11 +298,9 @@ def test_positive_list_updated(module_org, custom_repo, target_sat): :Setup: Errata synced on satellite server. - :Steps: GET /katello/api/errata + :steps: GET /katello/api/errata :expectedresults: Errata is filtered by Org and sorted by Updated date. - - :CaseLevel: System """ repo = target_sat.api.Repository(id=custom_repo['repository-id']).read() assert repo.sync()['result'] == 'success' @@ -332,11 +319,9 @@ def test_positive_sorted_issue_date_and_filter_by_cve(module_org, custom_repo, t :Setup: Errata synced on satellite server. - :Steps: GET /katello/api/errata + :steps: GET /katello/api/errata :expectedresults: Errata is sorted by issued date and filtered by CVE. - - :CaseLevel: System """ # Errata is sorted by issued date. erratum_list = target_sat.api.Errata(repository=custom_repo['repository-id']).search( @@ -427,12 +412,10 @@ def test_positive_get_count_for_host(setup_content_rhel6, rhel6_contenthost, tar 1. Errata synced on satellite server. 2. Some Content hosts present. - :Steps: GET /api/v2/hosts + :steps: GET /api/v2/hosts :expectedresults: The available errata count is retrieved. - :CaseLevel: System - :parametrized: yes :CaseImportance: Medium @@ -469,12 +452,10 @@ def test_positive_get_applicable_for_host(setup_content_rhel6, rhel6_contenthost 1. Errata synced on satellite server. 2. Some Content hosts present. - :Steps: GET /api/v2/hosts/:id/errata + :steps: GET /api/v2/hosts/:id/errata :expectedresults: The available errata is retrieved. - :CaseLevel: System - :parametrized: yes :CaseImportance: Medium @@ -517,12 +498,10 @@ def test_positive_get_diff_for_cv_envs(target_sat): 1. Errata synced on satellite server. 2. Multiple environments present. - :Steps: GET /katello/api/compare + :steps: GET /katello/api/compare :expectedresults: Difference in errata between a set of environments for a content view is retrieved. - - :CaseLevel: System """ org = target_sat.api.Organization().create() env = target_sat.api.LifecycleEnvironment(organization=org).create() @@ -575,7 +554,7 @@ def test_positive_incremental_update_required( :Setup: 1. Errata synced on satellite server - :Steps: + :steps: 1. Create VM as Content Host, registering to CV with custom errata 2. Install package in VM so it needs one erratum 3. Check if incremental_updates required: @@ -593,8 +572,6 @@ def test_positive_incremental_update_required( :parametrized: yes - :CaseLevel: System - :BZ: 2013093 """ rhel7_contenthost.install_katello_ca(target_sat) @@ -704,8 +681,6 @@ def test_errata_installation_with_swidtags( :parametrized: yes :CaseImportance: Critical - - :CaseLevel: System """ module_name = 'kangaroo' version = '20180704111719' @@ -862,8 +837,6 @@ def test_apply_modular_errata_using_default_content_view( :CaseAutomation: Automated :parametrized: yes - - :CaseLevel: System """ module_name = 'duck' stream = '0' @@ -912,8 +885,6 @@ def test_positive_sync_repos_with_large_errata(target_sat): :BZ: 1463811 - :CaseLevel: Integration - :expectedresults: both repositories were successfully synchronized """ org = target_sat.api.Organization().create() diff --git a/tests/foreman/api/test_filter.py b/tests/foreman/api/test_filter.py index bf04a011276..efcf26e7b49 100644 --- a/tests/foreman/api/test_filter.py +++ b/tests/foreman/api/test_filter.py @@ -8,17 +8,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: UsersRoles :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/foreman/api/test_foremantask.py b/tests/foreman/api/test_foremantask.py index 6d8bee0b42d..c736a0e7161 100644 --- a/tests/foreman/api/test_foremantask.py +++ b/tests/foreman/api/test_foremantask.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: TasksPlugin :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest from requests.exceptions import HTTPError diff --git a/tests/foreman/api/test_host.py b/tests/foreman/api/test_host.py index d495d6c7c24..94730d56925 100644 --- a/tests/foreman/api/test_host.py +++ b/tests/foreman/api/test_host.py @@ -8,17 +8,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import http @@ -93,8 +88,6 @@ def test_positive_search_by_org_id(target_sat): organization id was done :BZ: 1447958 - - :CaseLevel: Integration """ host = target_sat.api.Host().create() # adding org id as GET parameter for correspondence with BZ @@ -220,8 +213,6 @@ def test_positive_create_and_update_with_hostgroup( :id: 8f9601f9-afd8-4a88-8f28-a5cbc996e805 :expectedresults: A host is created and updated with expected hostgroup assigned - - :CaseLevel: Integration """ module_published_cv.version[0].promote(data={'environment_ids': module_lce.id, 'force': False}) hostgroup = module_target_sat.api.HostGroup( @@ -261,8 +252,6 @@ def test_positive_create_inherit_lce_cv( :expectedresults: Host's lifecycle environment and content view match the ones specified in hostgroup - :CaseLevel: Integration - :BZ: 1391656 """ hostgroup = module_target_sat.api.HostGroup( @@ -413,8 +402,6 @@ def test_positive_create_and_update_with_subnet( :id: 9aa97aff-8439-4027-89ee-01c643fbf7d1 :expectedresults: A host is created and updated with expected subnet assigned - - :CaseLevel: Integration """ host = module_target_sat.api.Host( location=module_location, organization=module_org, subnet=module_default_subnet @@ -438,8 +425,6 @@ def test_positive_create_and_update_with_compresource( :expectedresults: A host is created and updated with expected compute resource assigned - - :CaseLevel: Integration """ host = module_target_sat.api.Host( compute_resource=module_cr_libvirt, location=module_location, organization=module_org @@ -460,8 +445,6 @@ def test_positive_create_and_update_with_model(module_model, module_target_sat): :id: 7a912a19-71e4-4843-87fd-bab98c156f4a :expectedresults: A host is created and updated with expected model assigned - - :CaseLevel: Integration """ host = module_target_sat.api.Host(model=module_model).create() assert host.model.read().name == module_model.name @@ -480,8 +463,6 @@ def test_positive_create_and_update_with_user( :id: 72e20f8f-17dc-4e38-8ac1-d08df8758f56 :expectedresults: A host is created and updated with expected user assigned - - :CaseLevel: Integration """ host = module_target_sat.api.Host( owner=module_user, owner_type='User', organization=module_org, location=module_location @@ -504,8 +485,6 @@ def test_positive_create_and_update_with_usergroup( :id: 706e860c-8c05-4ddc-be20-0ecd9f0da813 :expectedresults: A host is created and updated with expected user group assigned - - :CaseLevel: Integration """ user = module_target_sat.api.User( location=[module_location], organization=[module_org], role=[function_role] @@ -619,8 +598,6 @@ def test_positive_create_and_update_with_compute_profile(module_compute_profile, :expectedresults: A host is created and updated with expected compute profile assigned - - :CaseLevel: Integration """ host = module_target_sat.api.Host(compute_profile=module_compute_profile).create() assert host.compute_profile.read().name == module_compute_profile.name @@ -639,8 +616,6 @@ def test_positive_create_and_update_with_content_view( :id: 10f69c7a-088e-474c-b869-1ad12deda2ad :expectedresults: A host is created and updated with expected content view - - :CaseLevel: Integration """ host = module_target_sat.api.Host( organization=module_org, @@ -710,8 +685,6 @@ def test_positive_end_to_end_with_image( :expectedresults: A host is created with expected image, image is removed and host is updated with expected image - - :CaseLevel: Integration """ host = module_target_sat.api.Host( organization=module_org, @@ -780,8 +753,6 @@ def test_positive_create_and_update_domain( :id: 8ca9f67c-4c11-40f9-b434-4f200bad000f :expectedresults: A host is created and updated with expected domain - - :CaseLevel: Integration """ host = module_target_sat.api.Host( organization=module_org, location=module_location, domain=module_domain @@ -805,8 +776,6 @@ def test_positive_create_and_update_env( :id: 87a08dbf-fd4c-4b6c-bf73-98ab70756fc6 :expectedresults: A host is created and updated with expected environment - - :CaseLevel: Integration """ host = session_puppet_enabled_sat.api.Host( organization=module_puppet_org, @@ -830,8 +799,6 @@ def test_positive_create_and_update_arch(module_architecture, module_target_sat) :id: 5f190b14-e6db-46e1-8cd1-e94e048e6a77 :expectedresults: A host is created and updated with expected architecture - - :CaseLevel: Integration """ host = module_target_sat.api.Host(architecture=module_architecture).create() assert host.architecture.read().name == module_architecture.name @@ -849,8 +816,6 @@ def test_positive_create_and_update_os(module_os, module_target_sat): :id: 46edced1-8909-4066-b196-b8e22512341f :expectedresults: A host is created updated with expected operating system - - :CaseLevel: Integration """ host = module_target_sat.api.Host(operatingsystem=module_os).create() assert host.operatingsystem.read().name == module_os.name @@ -873,8 +838,6 @@ def test_positive_create_and_update_medium(module_org, module_location, module_t :id: d81cb65c-48b3-4ce3-971e-51b9dd123697 :expectedresults: A host is created and updated with expected medium - - :CaseLevel: Integration """ medium = module_target_sat.api.Media( organization=[module_org], location=[module_location] @@ -938,8 +901,6 @@ def test_negative_update_arch(module_architecture, module_target_sat): :id: 07b9c0e7-f02b-4aff-99ae-5c203255aba1 :expectedresults: A host is not updated - - :CaseLevel: Integration """ host = module_target_sat.api.Host().create() host.architecture = module_architecture @@ -956,8 +917,6 @@ def test_negative_update_os(target_sat): :id: 40e79f73-6356-4d61-9806-7ade2f4f8829 :expectedresults: A host is not updated - - :CaseLevel: Integration """ host = target_sat.api.Host().create() new_os = target_sat.api.OperatingSystem( @@ -984,8 +943,6 @@ def test_positive_read_content_source_id( response :BZ: 1339613, 1488130 - - :CaseLevel: System """ proxy = target_sat.api.SmartProxy().search(query={'url': f'{target_sat.url}:9090'})[0].read() module_published_cv.version[0].promote(data={'environment_ids': module_lce.id, 'force': False}) @@ -1020,8 +977,6 @@ def test_positive_update_content_source_id( response :BZ: 1339613, 1488130 - - :CaseLevel: System """ proxy = target_sat.api.SmartProxy().search(query={'url': f'{target_sat.url}:9090'})[0] module_published_cv.version[0].promote(data={'environment_ids': module_lce.id, 'force': False}) @@ -1065,8 +1020,6 @@ def test_positive_read_enc_information( :expectedresults: host ENC information read successfully :BZ: 1362372 - - :CaseLevel: Integration """ lce = ( session_puppet_enabled_sat.api.LifecycleEnvironment() @@ -1127,8 +1080,6 @@ def test_positive_add_future_subscription(): 2. Add the subscription to the content host :expectedresults: The future-dated subscription was added to the host - - :CaseLevel: Integration """ @@ -1148,8 +1099,6 @@ def test_positive_add_future_subscription_with_ak(): 3. Register a new content host with the activation key :expectedresults: The host was registered and future subscription added - - :CaseLevel: Integration """ @@ -1168,8 +1117,6 @@ def test_negative_auto_attach_future_subscription(): 3. Run auto-attach on the content host :expectedresults: Only the current subscription was added to the host - - :CaseLevel: Integration """ @@ -1189,8 +1136,6 @@ def test_positive_create_baremetal_with_bios(): :expectedresults: Host is created :CaseAutomation: NotAutomated - - :CaseLevel: System """ @@ -1210,8 +1155,6 @@ def test_positive_create_baremetal_with_uefi(): :expectedresults: Host is created :CaseAutomation: NotAutomated - - :CaseLevel: System """ @@ -1242,8 +1185,6 @@ def test_positive_verify_files_with_pxegrub_uefi(): And record in /var/lib/dhcpd/dhcpd.leases points to the bootloader :CaseAutomation: NotAutomated - - :CaseLevel: System """ @@ -1278,8 +1219,6 @@ def test_positive_verify_files_with_pxegrub_uefi_secureboot(): :CaseComponent: TFTP :Team: Rocket - - :CaseLevel: Integration """ @@ -1314,8 +1253,6 @@ def test_positive_verify_files_with_pxegrub2_uefi(): :CaseComponent: TFTP :Team: Rocket - - :CaseLevel: Integration """ @@ -1347,8 +1284,6 @@ def test_positive_verify_files_with_pxegrub2_uefi_secureboot(): :CaseAutomation: NotAutomated - :CaseLevel: Integration - :CaseComponent: TFTP :Team: Rocket diff --git a/tests/foreman/api/test_hostcollection.py b/tests/foreman/api/test_hostcollection.py index 8935779101c..aea239ac1ad 100644 --- a/tests/foreman/api/test_hostcollection.py +++ b/tests/foreman/api/test_hostcollection.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: HostCollections :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from random import choice, randint @@ -205,8 +200,6 @@ def test_positive_add_host(module_org, fake_hosts, module_target_sat): :expectedresults: Host was added to the host collection. - :CaseLevel: Integration - :BZ:1325989 """ host_collection = module_target_sat.api.HostCollection(organization=module_org).create() @@ -224,8 +217,6 @@ def test_positive_add_hosts(module_org, fake_hosts, module_target_sat): :expectedresults: Hosts were added to the host collection. - :CaseLevel: Integration - :BZ: 1325989 """ host_collection = module_target_sat.api.HostCollection(organization=module_org).create() diff --git a/tests/foreman/api/test_hostgroup.py b/tests/foreman/api/test_hostgroup.py index 784b404e5dd..e0e6fd9ed91 100644 --- a/tests/foreman/api/test_hostgroup.py +++ b/tests/foreman/api/test_hostgroup.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: HostGroup :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from random import randint @@ -62,7 +57,6 @@ def test_inherit_puppetclass(self, session_puppet_enabled_sat): :BZ: 1107708, 1222118, 1487586 - :CaseLevel: System """ # Creating entities like organization, content view and lifecycle_env # with not utf-8 names for easier interaction with puppet environment @@ -169,7 +163,6 @@ def test_rebuild_config(self, module_org, module_location, hostgroup, module_tar :CaseImportance: Medium - :CaseLevel: System """ lce = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() content_view = module_target_sat.api.ContentView(organization=module_org).create() @@ -248,8 +241,6 @@ def test_positive_create_with_properties( :expectedresults: A hostgroup is created with expected properties, updated and deleted - :CaseLevel: Integration - :CaseImportance: High """ env = session_puppet_enabled_sat.api.Environment( @@ -402,7 +393,6 @@ def test_positive_create_with_realm(self, module_org, module_location, target_sa :expectedresults: A hostgroup is created with expected realm assigned - :CaseLevel: Integration """ realm = target_sat.api.Realm( location=[module_location], @@ -427,7 +417,6 @@ def test_positive_create_with_locs(self, module_org, module_target_sat): :expectedresults: A hostgroup is created with expected multiple locations assigned - :CaseLevel: Integration """ locs = [ module_target_sat.api.Location(organization=[module_org]).create() @@ -449,7 +438,6 @@ def test_positive_create_with_orgs(self, target_sat): :expectedresults: A hostgroup is created with expected multiple organizations assigned - :CaseLevel: Integration """ orgs = [target_sat.api.Organization().create() for _ in range(randint(3, 5))] hostgroup = target_sat.api.HostGroup(organization=orgs).create() @@ -482,7 +470,6 @@ def test_positive_update_puppet_ca_proxy(self, puppet_hostgroup, session_puppet_ :CaseImportance: Medium - :CaseLevel: Integration """ new_proxy = session_puppet_enabled_sat.api.SmartProxy().search( query={'search': f'url = {session_puppet_enabled_sat.url}:9090'} @@ -502,7 +489,6 @@ def test_positive_update_realm(self, module_org, module_location, target_sat): :expectedresults: A hostgroup is updated with expected realm - :CaseLevel: Integration """ realm = target_sat.api.Realm( location=[module_location], @@ -535,7 +521,6 @@ def test_positive_update_puppet_proxy(self, puppet_hostgroup, session_puppet_ena :expectedresults: A hostgroup is updated with expected puppet proxy - :CaseLevel: Integration """ new_proxy = session_puppet_enabled_sat.api.SmartProxy().search( query={'search': f'url = {session_puppet_enabled_sat.url}:9090'} @@ -554,7 +539,6 @@ def test_positive_update_content_source(self, hostgroup, target_sat): :expectedresults: A hostgroup is updated with expected puppet proxy - :CaseLevel: Integration """ new_content_source = target_sat.api.SmartProxy().search( query={'search': f'url = {target_sat.url}:9090'} @@ -573,7 +557,6 @@ def test_positive_update_locs(self, module_org, hostgroup, module_target_sat): :expectedresults: A hostgroup is updated with expected locations - :CaseLevel: Integration """ new_locs = [ module_target_sat.api.Location(organization=[module_org]).create() @@ -593,7 +576,6 @@ def test_positive_update_orgs(self, hostgroup, target_sat): :expectedresults: A hostgroup is updated with expected organizations - :CaseLevel: Integration """ new_orgs = [target_sat.api.Organization().create() for _ in range(randint(3, 5))] hostgroup.organization = new_orgs @@ -649,8 +631,6 @@ def test_positive_create_with_group_parameters(self, module_org, module_target_s :customerscenario: true - :CaseLevel: Integration - :BZ: 1710853 """ group_params = {'name': gen_string('alpha'), 'value': gen_string('alpha')} @@ -681,7 +661,6 @@ def test_positive_get_content_source(self, hostgroup, module_target_sat): :expectedresults: The response contains both values for the ``content_source`` field. - :CaseLevel: Integration """ names = module_target_sat.api_factory.one_to_one_names('content_source') hostgroup_attrs = set(hostgroup.read_json().keys()) @@ -700,7 +679,6 @@ def test_positive_get_cv(self, hostgroup, module_target_sat): :expectedresults: The response contains both values for the ``content_view`` field. - :CaseLevel: Integration """ names = module_target_sat.api_factory.one_to_one_names('content_view') hostgroup_attrs = set(hostgroup.read_json().keys()) @@ -719,7 +697,6 @@ def test_positive_get_lce(self, hostgroup, module_target_sat): :expectedresults: The response contains both values for the ``lifecycle_environment`` field. - :CaseLevel: Integration """ names = module_target_sat.api_factory.one_to_one_names('lifecycle_environment') hostgroup_attrs = set(hostgroup.read_json().keys()) @@ -740,7 +717,6 @@ def test_positive_read_puppet_proxy_name(self, session_puppet_enabled_sat): :BZ: 1371900 - :CaseLevel: Integration """ proxy = session_puppet_enabled_sat.api.SmartProxy().search( query={'search': f'url = {session_puppet_enabled_sat.url}:9090'} @@ -762,7 +738,6 @@ def test_positive_read_puppet_ca_proxy_name(self, session_puppet_enabled_sat): :BZ: 1371900 - :CaseLevel: Integration """ proxy = session_puppet_enabled_sat.api.SmartProxy().search( query={'search': f'url = {session_puppet_enabled_sat.url}:9090'} diff --git a/tests/foreman/api/test_http_proxy.py b/tests/foreman/api/test_http_proxy.py index 1e6da4dfcf6..0f8f1cab5c5 100644 --- a/tests/foreman/api/test_http_proxy.py +++ b/tests/foreman/api/test_http_proxy.py @@ -2,19 +2,14 @@ :Requirement: HttpProxy -:CaseLevel: Acceptance - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: High :CaseAutomation: Automated -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -157,8 +152,6 @@ def test_positive_auto_attach_with_http_proxy( :BZ: 2046337 :parametrized: yes - - :CaseLevel: System """ org = function_entitlement_manifest_org lce = module_target_sat.api.LifecycleEnvironment(organization=org).create() diff --git a/tests/foreman/api/test_ldapauthsource.py b/tests/foreman/api/test_ldapauthsource.py index 2139e7c947f..fbcc7039193 100644 --- a/tests/foreman/api/test_ldapauthsource.py +++ b/tests/foreman/api/test_ldapauthsource.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: LDAP :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest from requests.exceptions import HTTPError diff --git a/tests/foreman/api/test_lifecycleenvironment.py b/tests/foreman/api/test_lifecycleenvironment.py index dfc3ed1ba27..b68017efb87 100644 --- a/tests/foreman/api/test_lifecycleenvironment.py +++ b/tests/foreman/api/test_lifecycleenvironment.py @@ -8,17 +8,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: LifecycleEnvironments :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -138,8 +133,6 @@ def test_positive_update_description(module_lce, new_desc, module_target_sat): :expectedresults: Lifecycle environment is created and updated properly - :CaseLevel: Integration - :CaseImportance: Low :parametrized: yes @@ -197,7 +190,7 @@ def test_positive_search_in_org(name, target_sat): :id: 110e4777-c374-4365-b676-b1db4552fe51 - :Steps: + :steps: 1. Create an organization. 2. Create a lifecycle environment belonging to the organization. @@ -206,8 +199,6 @@ def test_positive_search_in_org(name, target_sat): :expectedresults: Only "Library" and the lifecycle environment just created are in the search results. - :CaseLevel: Integration - :parametrized: yes """ new_org = target_sat.api.Organization().create() @@ -232,11 +223,9 @@ def test_positive_create_environment_after_host_register(): 2. Create a new content host. 3. Register the content host to the Library environment. - :Steps: Create a new environment. + :steps: Create a new environment. :expectedresults: The environment is created without any errors. - :CaseLevel: Integration - :CaseAutomation: NotAutomated """ diff --git a/tests/foreman/api/test_location.py b/tests/foreman/api/test_location.py index ad226389c57..2e54a4862fa 100644 --- a/tests/foreman/api/test_location.py +++ b/tests/foreman/api/test_location.py @@ -7,17 +7,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: OrganizationsandLocations :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from random import randint @@ -40,7 +35,6 @@ def valid_loc_data_list(): Note: The maximum allowed length of location name is 246 only. This is an intended behavior (Also note that 255 is the standard across other entities.) - """ return dict( alpha=gen_string('alpha', randint(1, 246)), @@ -127,7 +121,6 @@ def test_positive_create_and_update_with_org(self, make_orgs, target_sat): :expectedresults: Location created successfully and has correct organization assigned to it with expected title - :CaseLevel: Integration """ location = target_sat.api.Location(organization=[make_orgs['org']]).create() assert location.organization[0].id == make_orgs['org'].id @@ -208,7 +201,6 @@ def test_positive_update_entities(self, make_entities, target_sat): :expectedresults: Location updated successfully and has correct domain assigned - :CaseLevel: Integration """ location = target_sat.api.Location().create() @@ -247,8 +239,6 @@ def test_positive_create_update_and_remove_capsule(self, make_proxies, target_sa :BZ: 1398695 - :CaseLevel: Integration - :CaseImportance: High """ proxy_id_1 = make_proxies['proxy1']['id'] @@ -276,7 +266,6 @@ def test_negative_update_domain(self, target_sat): :expectedresults: Location is not updated - :CaseLevel: Integration """ location = target_sat.api.Location(domain=[target_sat.api.Domain().create()]).create() domain = target_sat.api.Domain().create() diff --git a/tests/foreman/api/test_media.py b/tests/foreman/api/test_media.py index f68ea37bd72..79c5cb7f20e 100644 --- a/tests/foreman/api/test_media.py +++ b/tests/foreman/api/test_media.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random @@ -90,7 +85,6 @@ def test_positive_create_with_location(self, module_org, module_location, module :expectedresults: Media entity is created and has proper location - :CaseLevel: Integration """ media = module_target_sat.api.Media( organization=[module_org], location=[module_location] @@ -105,7 +99,6 @@ def test_positive_create_with_os(self, module_org, module_target_sat): :expectedresults: Media entity is created and assigned to expected OS - :CaseLevel: Integration """ os = module_target_sat.api.OperatingSystem().create() media = module_target_sat.api.Media( diff --git a/tests/foreman/api/test_multiple_paths.py b/tests/foreman/api/test_multiple_paths.py index 4cacae5051e..30531f77025 100644 --- a/tests/foreman/api/test_multiple_paths.py +++ b/tests/foreman/api/test_multiple_paths.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: API :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import http diff --git a/tests/foreman/api/test_notifications.py b/tests/foreman/api/test_notifications.py index cb0320b3b4b..7954ae9bd0d 100644 --- a/tests/foreman/api/test_notifications.py +++ b/tests/foreman/api/test_notifications.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Notifications :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from mailbox import mbox from re import findall diff --git a/tests/foreman/api/test_operatingsystem.py b/tests/foreman/api/test_operatingsystem.py index 4cdb27a04a2..9e25a92e167 100644 --- a/tests/foreman/api/test_operatingsystem.py +++ b/tests/foreman/api/test_operatingsystem.py @@ -4,17 +4,12 @@ :CaseAutomation: NotAutomated -:CaseLevel: System - :CaseComponent: Provisioning :Team: Rocket -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ from http.client import NOT_FOUND import random @@ -213,7 +208,6 @@ def test_positive_create_with_archs(self, target_sat): :expectedresults: The operating system is created and points at the expected architectures. - :CaseLevel: Integration """ amount = range(random.randint(3, 5)) archs = [target_sat.api.Architecture().create() for _ in amount] @@ -231,7 +225,6 @@ def test_positive_create_with_ptables(self, target_sat): :expectedresults: The operating system is created and points at the expected partition tables. - :CaseLevel: Integration """ amount = range(random.randint(3, 5)) ptables = [target_sat.api.PartitionTable().create() for _ in amount] @@ -365,7 +358,6 @@ def test_positive_update_medias(self, module_org, module_target_sat): :expectedresults: The operating system is updated and points at the expected medias. - :CaseLevel: Integration """ initial_media = module_target_sat.api.Media(organization=[module_org]).create() os = module_target_sat.api.OperatingSystem(medium=[initial_media]).create() diff --git a/tests/foreman/api/test_organization.py b/tests/foreman/api/test_organization.py index c407691804e..89988935f98 100644 --- a/tests/foreman/api/test_organization.py +++ b/tests/foreman/api/test_organization.py @@ -7,17 +7,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: OrganizationsandLocations :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import http import json @@ -259,7 +254,6 @@ def test_positive_update_user(self, module_org, target_sat): :expectedresults: User is associated with organization. - :CaseLevel: Integration """ user = target_sat.api.User().create() module_org.user = [user] @@ -275,7 +269,6 @@ def test_positive_update_subnet(self, module_org, target_sat): :expectedresults: Subnet is associated with organization. - :CaseLevel: Integration """ subnet = target_sat.api.Subnet().create() module_org.subnet = [subnet] @@ -293,8 +286,6 @@ def test_positive_add_and_remove_hostgroup(self, target_sat): :expectedresults: Hostgroup is added to organization and then removed - :CaseLevel: Integration - :CaseImportance: Medium """ org = target_sat.api.Organization().create() @@ -317,7 +308,6 @@ def test_positive_add_and_remove_smart_proxy(self, target_sat): :BZ: 1395229 - :CaseLevel: Integration """ # Every Satellite has a built-in smart proxy, so let's find it smart_proxy = target_sat.api.SmartProxy().search( diff --git a/tests/foreman/api/test_oscap_tailoringfiles.py b/tests/foreman/api/test_oscap_tailoringfiles.py index fb6b78750f0..af6b5f47a65 100644 --- a/tests/foreman/api/test_oscap_tailoringfiles.py +++ b/tests/foreman/api/test_oscap_tailoringfiles.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: SCAPPlugin :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/foreman/api/test_oscappolicy.py b/tests/foreman/api/test_oscappolicy.py index c9500c465d1..ba318c801cb 100644 --- a/tests/foreman/api/test_oscappolicy.py +++ b/tests/foreman/api/test_oscappolicy.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: SCAPPlugin :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -34,8 +29,6 @@ def test_positive_crud_scap_policy( :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: Critical """ name = gen_string('alpha') diff --git a/tests/foreman/api/test_parameters.py b/tests/foreman/api/test_parameters.py index 9aa297a5949..1e415d15dc8 100644 --- a/tests/foreman/api/test_parameters.py +++ b/tests/foreman/api/test_parameters.py @@ -4,17 +4,11 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Parameters :Team: Rocket -:TestType: Functional - :CaseImportance: Critical - -:Upstream: No """ from fauxfactory import gen_string import pytest diff --git a/tests/foreman/api/test_partitiontable.py b/tests/foreman/api/test_partitiontable.py index fd0027cbc40..9b8e2a29d2c 100644 --- a/tests/foreman/api/test_partitiontable.py +++ b/tests/foreman/api/test_partitiontable.py @@ -8,17 +8,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random diff --git a/tests/foreman/api/test_permission.py b/tests/foreman/api/test_permission.py index 30e065d77c3..a4267d71f92 100644 --- a/tests/foreman/api/test_permission.py +++ b/tests/foreman/api/test_permission.py @@ -8,17 +8,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: UsersRoles :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from itertools import chain import json diff --git a/tests/foreman/api/test_ping.py b/tests/foreman/api/test_ping.py index 14e4401efa8..e5e72933b3c 100644 --- a/tests/foreman/api/test_ping.py +++ b/tests/foreman/api/test_ping.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: API :Team: Endeavour -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import pytest diff --git a/tests/foreman/api/test_product.py b/tests/foreman/api/test_product.py index dfa9f59f705..7915ddd2de7 100644 --- a/tests/foreman/api/test_product.py +++ b/tests/foreman/api/test_product.py @@ -7,17 +7,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -96,8 +91,6 @@ def test_positive_create_with_gpg(module_org, module_target_sat): :id: 57331c1f-15dd-4c9f-b8fc-3010847b2975 :expectedresults: A product is created with the specified GPG key. - - :CaseLevel: Integration """ gpg_key = module_target_sat.api.GPGKey( content=DataFile.VALID_GPG_KEY_FILE.read_text(), @@ -225,8 +218,6 @@ def test_positive_update_gpg(module_org, module_target_sat): :id: 3b08f155-a0d6-4987-b281-dc02e8d5a03e :expectedresults: The updated product points to a new GPG key. - - :CaseLevel: Integration """ # Create a product and make it point to a GPG key. gpg_key_1 = module_target_sat.api.GPGKey( @@ -254,8 +245,6 @@ def test_positive_update_organization(module_org, module_target_sat): :expectedresults: The updated product points to a new organization - :CaseLevel: Integration - :BZ: 1310422 """ product = module_target_sat.api.Product(organization=module_org).create() @@ -350,8 +339,6 @@ def test_positive_sync_several_repos(module_org, module_target_sat): :expectedresults: All repositories within a product are successfully synced. - :CaseLevel: Integration - :BZ: 1389543 """ product = module_target_sat.api.Product(organization=module_org).create() @@ -380,8 +367,6 @@ def test_positive_filter_product_list(module_entitlement_manifest_org, module_ta :expectedresults: Able to list the products based on defined filter. - :CaseLevel: Integration - :BZ: 1667129 """ org = module_entitlement_manifest_org diff --git a/tests/foreman/api/test_provisioning.py b/tests/foreman/api/test_provisioning.py index 3e466d81286..0364eef8f22 100644 --- a/tests/foreman/api/test_provisioning.py +++ b/tests/foreman/api/test_provisioning.py @@ -4,17 +4,12 @@ :CaseAutomation: NotAutomated -:CaseLevel: System - :CaseComponent: Provisioning :Team: Rocket -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import re diff --git a/tests/foreman/api/test_provisioning_puppet.py b/tests/foreman/api/test_provisioning_puppet.py index a32d542abb3..447a462f05f 100644 --- a/tests/foreman/api/test_provisioning_puppet.py +++ b/tests/foreman/api/test_provisioning_puppet.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Puppet :Team: Rocket -:TestType: Functional - :CaseImportance: Medium -:Upstream: No """ from fauxfactory import gen_string from packaging.version import Version diff --git a/tests/foreman/api/test_provisioningtemplate.py b/tests/foreman/api/test_provisioningtemplate.py index 42bc807245c..8735ae4fbe7 100644 --- a/tests/foreman/api/test_provisioningtemplate.py +++ b/tests/foreman/api/test_provisioningtemplate.py @@ -11,13 +11,8 @@ :Team: Rocket -:TestType: Functional - -:CaseLevel: Integration - :CaseImportance: High -:Upstream: No """ from random import choice @@ -129,10 +124,7 @@ def tftpboot(module_org, module_target_sat): class TestProvisioningTemplate: - """Tests for provisioning templates - - :CaseLevel: Acceptance - """ + """Tests for provisioning templates""" @pytest.mark.tier1 @pytest.mark.e2e @@ -225,8 +217,6 @@ def test_positive_build_pxe_default(self, tftpboot, module_target_sat): :expectedresults: The response is a JSON payload, all templates are deployed to TFTP/HTTP and are rendered correctly - :CaseLevel: Integration - :CaseImportance: Critical :BZ: 1202564 diff --git a/tests/foreman/api/test_registration.py b/tests/foreman/api/test_registration.py index 50f5e0c0b09..9a3b98e3550 100644 --- a/tests/foreman/api/test_registration.py +++ b/tests/foreman/api/test_registration.py @@ -2,8 +2,6 @@ :Requirement: Registration -:CaseLevel: Acceptance - :CaseComponent: Registration :CaseAutomation: Automated @@ -12,9 +10,6 @@ :Team: Rocket -:TestType: Functional - -:Upstream: No """ import uuid @@ -110,8 +105,6 @@ def test_positive_allow_reregistration_when_dmi_uuid_changed( :customerscenario: true :BZ: 1747177,2229112 - - :CaseLevel: Integration """ uuid_1 = str(uuid.uuid1()) uuid_2 = str(uuid.uuid4()) @@ -148,8 +141,6 @@ def test_positive_update_packages_registration( :id: 3d0a3252-ab81-4acf-bca6-253b746f26bb :expectedresults: Package update is successful on host post registration. - - :CaseLevel: Component """ org = module_sca_manifest_org command = module_target_sat.api.RegistrationCommand( diff --git a/tests/foreman/api/test_remoteexecution.py b/tests/foreman/api/test_remoteexecution.py index 4cf5174238c..e8333ced360 100644 --- a/tests/foreman/api/test_remoteexecution.py +++ b/tests/foreman/api/test_remoteexecution.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: RemoteExecution :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/foreman/api/test_reporttemplates.py b/tests/foreman/api/test_reporttemplates.py index 7d6bf4d2e1d..3c3bf97658e 100644 --- a/tests/foreman/api/test_reporttemplates.py +++ b/tests/foreman/api/test_reporttemplates.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Reporting :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from broker import Broker from fauxfactory import gen_string diff --git a/tests/foreman/api/test_repositories.py b/tests/foreman/api/test_repositories.py index a8728ab46e1..9d576b55f38 100644 --- a/tests/foreman/api/test_repositories.py +++ b/tests/foreman/api/test_repositories.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ from manifester import Manifester from nailgun.entity_mixins import call_entity_method_with_timeout @@ -35,7 +30,7 @@ def test_negative_disable_repository_with_cv(module_entitlement_manifest_org, ta :id: e521a7a4-2502-4fe2-b297-a13fc99e679b - :Steps: + :steps: 1. Enable and sync a RH Repo 2. Create a Content View with Repository attached 3. Publish Content View @@ -77,7 +72,7 @@ def test_positive_update_repository_metadata(module_org, target_sat): :id: 6fe7bb3f-1640-4904-a223-b4764534afe8 - :Steps: + :steps: 1. Create a Product and Yum Repository 2. Sync the Repository and returns its content_counts for rpm 3. Update the url to a different Repo and re-sync the Repository @@ -127,7 +122,7 @@ def test_positive_epel_repositories_with_mirroring_policy( :id: 5c4e0ba4-4486-4eaf-b6ad-62831b7353a4 - :Steps: + :steps: 1. Create a Epel repository with mirroring_policy set 2. Sync the Repository and return its content_counts for rpm 3. Assert content was synced and mirroring policy type is correct @@ -151,8 +146,6 @@ def test_positive_sync_kickstart_repo(module_entitlement_manifest_org, target_sa :expectedresults: No encoding gzip errors present in /var/log/messages. - :CaseLevel: Integration - :customerscenario: true :steps: @@ -199,7 +192,7 @@ def test_negative_upload_expired_manifest(module_org, target_sat): :id: d6e652d8-5f46-4d15-9191-d842466d45d0 - :Steps: + :steps: 1. Upload a manifest 2. Delete the Subscription Allocation on RHSM 3. Attempt to refresh the manifest @@ -224,7 +217,7 @@ def test_positive_multiple_orgs_with_same_repo(target_sat): :id: 39cff8ea-969d-4b8f-9fb4-33b1ba768ff2 - :Steps: + :steps: 1. Create multiple organizations 2. Sync the same repository to each organization 3. Assert that each repository from each organization contain the same content counts @@ -249,7 +242,7 @@ def test_positive_sync_mulitple_large_repos(module_target_sat, module_entitlemen :id: b51c4a3d-d532-4342-be61-e868f7c3a723 - :Steps: + :steps: 1. Enabled multiple large Repositories Red Hat Enterprise Linux 8 for x86_64 - AppStream RPMs 8 Red Hat Enterprise Linux 8 for x86_64 - BaseOS RPMs 8 @@ -297,7 +290,7 @@ def test_positive_available_repositories_endpoint(module_sca_manifest_org, targe :id: f4c9d4a0-9a82-4f06-b772-b1f7e3f45e7d - :Steps: + :steps: 1. Enable a Red Hat Repository 2. Attempt to hit the enpoint: GET /katello/api/repository_sets/:id/available_repositories diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index 465e2762e00..764e14bbfd7 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import re from string import punctuation @@ -317,7 +312,6 @@ def test_positive_create_with_gpg(self, module_org, module_product, module_targe :expectedresults: A repository is created with the given GPG key ID. - :CaseLevel: Integration """ gpg_key = module_target_sat.api.GPGKey( organization=module_org, @@ -336,7 +330,6 @@ def test_positive_create_same_name_different_orgs(self, repo, target_sat): :expectedresults: The two repositories are successfully created and have given name. - :CaseLevel: Integration """ org_2 = target_sat.api.Organization().create() product_2 = target_sat.api.Product(organization=org_2).create() @@ -678,7 +671,6 @@ def test_positive_update_gpg(self, module_org, module_product, module_target_sat :expectedresults: The updated repository points to a new GPG key. - :CaseLevel: Integration """ # Create a repo and make it point to a GPG key. gpg_key_1 = module_target_sat.api.GPGKey( @@ -705,7 +697,6 @@ def test_positive_update_contents(self, repo): :expectedresults: The repository's contents include one RPM. - :CaseLevel: Integration """ # Upload RPM content. repo.upload_content(files={'content': DataFile.RPM_TO_UPLOAD.read_bytes()}) @@ -867,7 +858,6 @@ def test_positive_synchronize(self, repo): :expectedresults: The repo has at least one RPM. - :CaseLevel: Integration """ repo.sync() assert repo.read().content_counts['rpm'] >= 1 @@ -901,7 +891,6 @@ def test_positive_synchronize_auth_yum_repo(self, repo): :expectedresults: Repository is created and synced - :CaseLevel: Integration """ # Verify that repo is not yet synced assert repo.content_counts['rpm'] == 0 @@ -940,7 +929,6 @@ def test_negative_synchronize_auth_yum_repo(self, repo): :expectedresults: Repository is created but synchronization fails - :CaseLevel: Integration """ with pytest.raises(TaskFailedError): repo.sync() @@ -967,7 +955,6 @@ def test_positive_resynchronize_rpm_repo(self, repo, target_sat): :BZ: 1459845, 1318004 - :CaseLevel: Integration """ # Synchronize it repo.sync() @@ -1023,7 +1010,6 @@ def test_positive_delete_rpm(self, repo): :expectedresults: The repository deleted successfully. - :CaseLevel: Integration """ repo.sync() # Check that there is at least one package @@ -1058,8 +1044,6 @@ def test_positive_access_protected_repository(self, module_org, repo, target_sat :BZ: 1242310 - :CaseLevel: Integration - :CaseImportance: High """ repo.sync() @@ -1100,8 +1084,6 @@ def test_positive_access_unprotected_repository(self, module_org, repo, target_s :expectedresults: The repository data file is successfully accessed. - :CaseLevel: Integration - :CaseImportance: Medium """ repo.sync() @@ -1208,7 +1190,6 @@ def test_positive_mirroring_policy(self, target_sat): :expectedresults: 1. The resync restores the original content properly. - :CaseLevel: System """ repo_url = settings.repos.yum_0.url packages_count = constants.FAKE_0_YUM_REPO_PACKAGES_COUNT @@ -1350,8 +1331,6 @@ def test_positive_sync_repos_with_lots_files(self, target_sat): :BZ: 1404345 - :CaseLevel: Integration - :expectedresults: repository was successfully synchronized """ org = target_sat.api.Organization().create() @@ -1369,7 +1348,6 @@ def test_positive_sync_rh(self, module_entitlement_manifest_org, target_sat): :expectedresults: Synced repo should fetch the data successfully. - :CaseLevel: Integration """ repo_id = target_sat.api_factory.enable_rhrepo_and_fetchid( basearch='x86_64', @@ -1401,8 +1379,6 @@ def test_positive_sync_yum_with_string_based_version(self, repo): :expectedresults: Synced repo should fetch the data successfully and parse versions as string. - :CaseLevel: Integration - :customerscenario: true :BZ: 1741011 @@ -1423,7 +1399,6 @@ def test_positive_sync_rh_app_stream(self): :expectedresults: Synced repo should fetch the data successfully and it should contain the module streams. - :CaseLevel: Integration """ pass @@ -1826,7 +1801,6 @@ def test_positive_synchronize_private_registry(self, repo): :BZ: 1475121 - :CaseLevel: Integration """ repo.sync() assert repo.read().content_counts['docker_manifest'] >= 1 @@ -1864,7 +1838,6 @@ def test_negative_synchronize_private_registry_wrong_password(self, repo_options :BZ: 1475121, 1580510 - :CaseLevel: Integration """ msg = "401, message=\'Unauthorized\'" with pytest.raises(TaskFailedError, match=msg): @@ -1903,7 +1876,6 @@ def test_negative_synchronize_private_registry_wrong_repo(self, repo_options, re :BZ: 1475121, 1580510 - :CaseLevel: Integration """ msg = "404, message=\'Not Found\'" with pytest.raises(TaskFailedError, match=msg): @@ -1943,7 +1915,6 @@ def test_negative_synchronize_private_registry_no_passwd( :BZ: 1475121, 1580510 - :CaseLevel: Integration """ with pytest.raises( HTTPError, @@ -2211,8 +2182,6 @@ def test_negative_synchronize_docker_repo_with_invalid_tags(self, repo_options, # # :expectedresults: Synced repo should fetch the data successfully. # -# :CaseLevel: Integration -# # :customerscenario: true # # :BZ: 1625783 @@ -2314,8 +2283,6 @@ class TestSRPMRepositoryIgnoreContent: In particular sync of duplicate SRPMs would fail when using the flag ``ignorable_content``. - :CaseLevel: Integration - :CaseComponent: Pulp :customerscenario: true @@ -2425,7 +2392,7 @@ def test_positive_upload_file_to_file_repo(self, repo, target_sat): :id: fdb46481-f0f4-45aa-b075-2a8f6725e51b - :Steps: + :steps: 1. Create a File Repository 2. Upload an arbitrary file to it @@ -2454,7 +2421,7 @@ def test_positive_file_permissions(self): 1. Create a File Repository 2. Upload an arbitrary file to it - :Steps: Retrieve file permissions from File Repository + :steps: Retrieve file permissions from File Repository :expectedresults: uploaded file permissions are kept after upload @@ -2480,7 +2447,7 @@ def test_positive_remove_file(self, repo, target_sat): 1. Create a File Repository 2. Upload an arbitrary file to it - :Steps: Remove a file from File Repository + :steps: Remove a file from File Repository :expectedresults: file is not listed under File Repository after removal @@ -2510,7 +2477,7 @@ def test_positive_remote_directory_sync(self): 1. Create a directory to be synced with a pulp manifest on its root 2. Make the directory available through http - :Steps: + :steps: 1. Create a File Repository with url pointing to http url created on setup 2. Initialize synchronization @@ -2533,7 +2500,7 @@ def test_positive_local_directory_sync(self): 1. Create a directory to be synced with a pulp manifest on its root locally (on the Satellite/Foreman host) - :Steps: + :steps: 1. Create a File Repository with url pointing to local url created on setup 2. Initialize synchronization @@ -2559,7 +2526,7 @@ def test_positive_symlinks_sync(self): locally (on the Satellite/Foreman host) 2. Make sure it contains synlinks - :Steps: + :steps: 1. Create a File Repository with url pointing to local url created on setup 2. Initialize synchronization diff --git a/tests/foreman/api/test_repository_set.py b/tests/foreman/api/test_repository_set.py index e4766585824..8f8c56ea04e 100644 --- a/tests/foreman/api/test_repository_set.py +++ b/tests/foreman/api/test_repository_set.py @@ -7,17 +7,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/foreman/api/test_rhc.py b/tests/foreman/api/test_rhc.py index 992d1017c77..521b1cb15da 100644 --- a/tests/foreman/api/test_rhc.py +++ b/tests/foreman/api/test_rhc.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: RHCloud-CloudConnector :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -44,7 +39,7 @@ def test_positive_configure_cloud_connector(target_sat, default_org, fixture_ena :id: 1338dc6a-12e0-4378-9a51-a33f4679ba30 - :Steps: + :steps: 1. Enable RH Cloud Connector 2. Check if the task is completed successfully diff --git a/tests/foreman/api/test_rhcloud_inventory.py b/tests/foreman/api/test_rhcloud_inventory.py index 67bd321c073..b673e639abd 100644 --- a/tests/foreman/api/test_rhcloud_inventory.py +++ b/tests/foreman/api/test_rhcloud_inventory.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: System - :CaseComponent: RHCloud-Inventory :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_alphanumeric, gen_string import pytest @@ -133,7 +128,7 @@ def test_rhcloud_inventory_api_hosts_synchronization( :id: 7be22e1c-906b-4ae5-93dd-5f79f395601c - :Steps: + :steps: 1. Prepare machine and upload its data to Insights. 2. Sync inventory status using RH Cloud plugin api. @@ -171,7 +166,7 @@ def test_rhcloud_inventory_auto_upload_setting(): :customerscenario: true - :Steps: + :steps: 1. Register a content host with satellite. 2. Enable "Automatic inventory upload" setting. 3. Verify that satellite automatically generate and upload @@ -201,7 +196,7 @@ def test_inventory_upload_with_http_proxy(): :customerscenario: true - :Steps: + :steps: 1. Create a http proxy which is using port 80. 2. Update general and content proxy in Satellite settings. 3. Register a content host with satellite. @@ -232,7 +227,7 @@ def test_include_parameter_tags_setting( :id: 3136a1e3-f844-416b-8334-75b27fd9e3a1 - :Steps: + :steps: 1. Enable include_parameter_tags setting. 2. Register a content host with satellite. 3. Create a host parameter with long text value. diff --git a/tests/foreman/api/test_rhsm.py b/tests/foreman/api/test_rhsm.py index 2194ecdb147..096163a8719 100644 --- a/tests/foreman/api/test_rhsm.py +++ b/tests/foreman/api/test_rhsm.py @@ -8,17 +8,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: SubscriptionManagement :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import http diff --git a/tests/foreman/api/test_role.py b/tests/foreman/api/test_role.py index 371e9652084..fe6807734e0 100644 --- a/tests/foreman/api/test_role.py +++ b/tests/foreman/api/test_role.py @@ -8,17 +8,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: UsersRoles :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from nailgun.config import ServerConfig import pytest @@ -576,7 +571,6 @@ def test_negative_access_entities_from_org_admin( :expectedresults: User should not be able to access any resources and permissions in taxonomies selected in Org Admin role - :CaseLevel: System """ user = self.create_org_admin_user( target_sat, role_taxos=role_taxonomies, user_taxos=filter_taxonomies @@ -609,7 +603,6 @@ def test_negative_access_entities_from_user( :expectedresults: User should not be able to access any resources and permissions in its own taxonomies - :CaseLevel: System """ user = self.create_org_admin_user( target_sat, role_taxos=role_taxonomies, user_taxos=filter_taxonomies @@ -636,7 +629,6 @@ def test_positive_override_cloned_role_filter(self, role_taxonomies, target_sat) :expectedresults: Filter in cloned role should be overridden - :CaseLevel: Integration """ role_name = gen_string('alpha') role = target_sat.api.Role(name=role_name).create() @@ -679,7 +671,6 @@ def test_positive_emptiness_of_filter_taxonomies_on_role_clone( None in cloned role 2. Override flag is set to True in cloned role filter - :CaseLevel: Integration """ role = target_sat.api.Role( name=gen_string('alpha'), @@ -722,7 +713,6 @@ def test_positive_clone_role_having_overridden_filter_with_taxonomies( :expectedresults: Unlimited and Override flags should be set to True on filter for filter that is overridden in parent role - :CaseLevel: Integration """ role = target_sat.api.Role( name=gen_string('alpha'), @@ -770,7 +760,6 @@ def test_positive_clone_role_having_non_overridden_filter_with_taxonomies( :expectedresults: Both unlimited and override flag should be set to False on filter for filter that is not overridden in parent role - :CaseLevel: Integration """ role = target_sat.api.Role( name=gen_string('alpha'), @@ -811,7 +800,6 @@ def test_positive_clone_role_having_unlimited_filter_with_taxonomies( :expectedresults: Both unlimited and override flags should be set to False on filter for filter that is unlimited in parent role - :CaseLevel: Integration """ role = target_sat.api.Role( name=gen_string('alpha'), @@ -852,7 +840,6 @@ def test_positive_clone_role_having_overridden_filter_without_taxonomies( :expectedresults: Both unlimited and Override flags should be set to True on filter for filter that is overridden in parent role - :CaseLevel: Integration """ role = target_sat.api.Role( name=gen_string('alpha'), @@ -895,8 +882,6 @@ def test_positive_clone_role_without_taxonomies_non_overided_filter( 1. Unlimited flag should be set to True 2. Override flag should be set to False - :CaseLevel: Integration - :BZ: 1488908 """ role = target_sat.api.Role( @@ -935,8 +920,6 @@ def test_positive_clone_role_without_taxonomies_unlimited_filter( 1. Unlimited flag should be set to True 2. Override flag should be set to False - :CaseLevel: Integration - :BZ: 1488908 """ role = target_sat.api.Role( @@ -974,7 +957,6 @@ def test_positive_user_group_users_access_as_org_admin(self, role_taxonomies, ta :expectedresults: Both the user should have access to the resources of organization A and Location A - :CaseLevel: System """ org_admin = self.create_org_admin_role( target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] @@ -1059,7 +1041,6 @@ def test_positive_user_group_users_access_contradict_as_org_admins(self): 2. User assigned to Organization A and Location A should have access to the resources of organization A and Location A - :CaseLevel: System """ @pytest.mark.tier2 @@ -1082,7 +1063,6 @@ def test_negative_assign_org_admin_to_user_group( :expectedresults: Both the user shouldn't have access to the resources of organization A,B and Location A,B - :CaseLevel: System """ org_admin = self.create_org_admin_role( target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] @@ -1124,7 +1104,6 @@ def test_negative_assign_taxonomies_by_org_admin( :expectedresults: Org Admin should not be able to assign the organizations to any of its resources - :CaseLevel: Integration """ org_admin = self.create_org_admin_role( target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] @@ -1206,7 +1185,6 @@ def test_positive_taxonomies_control_to_superadmin_with_org_admin( :expectedresults: Super admin should be able to access the target_sat.api in taxonomies assigned to Org Admin - :CaseLevel: Integration """ user = self.create_org_admin_user( target_sat, role_taxos=role_taxonomies, user_taxos=role_taxonomies @@ -1251,7 +1229,6 @@ def test_positive_taxonomies_control_to_superadmin_without_org_admin( :expectedresults: Super admin should be able to access the target_sat.api in taxonomies assigned to Org Admin after deleting Org Admin - :CaseLevel: Integration """ user = self.create_org_admin_user( target_sat, role_taxos=role_taxonomies, user_taxos=role_taxonomies @@ -1367,7 +1344,6 @@ def test_negative_admin_permissions_to_org_admin(self, role_taxonomies, target_s :expectedresults: Org Admin should not have access of Admin user - :CaseLevel: Integration """ org_admin = self.create_org_admin_role( target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] @@ -1414,7 +1390,6 @@ def test_positive_create_user_by_org_admin(self, role_taxonomies, target_sat): :customerscenario: true - :CaseLevel: Integration """ org_admin = self.create_org_admin_role( target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] @@ -1468,7 +1443,6 @@ def test_positive_access_users_inside_org_admin_taxonomies(self, role_taxonomies :expectedresults: Org Admin should be able to access users inside its taxonomies - :CaseLevel: Integration """ user = self.create_org_admin_user( target_sat, role_taxos=role_taxonomies, user_taxos=role_taxonomies @@ -1499,7 +1473,6 @@ def test_positive_create_nested_location(self, role_taxonomies, target_sat): :expectedresults: after adding the needed permissions, user should be able to create nested locations - :CaseLevel: Integration """ user_login = gen_string('alpha') user_pass = gen_string('alphanumeric') @@ -1544,7 +1517,6 @@ def test_negative_access_users_outside_org_admin_taxonomies( :expectedresults: Org Admin should not be able to access users outside its taxonomies - :CaseLevel: Integration """ user = self.create_org_admin_user( target_sat, role_taxos=role_taxonomies, user_taxos=role_taxonomies @@ -1669,8 +1641,6 @@ def test_negative_access_entities_from_ldap_org_admin( :expectedresults: LDAP User should not be able to access resources and permissions in taxonomies selected in Org Admin role - :CaseLevel: System - :CaseAutomation: Automated """ org_admin = self.create_org_admin_role( @@ -1716,8 +1686,6 @@ def test_negative_access_entities_from_ldap_user( :expectedresults: LDAP User should not be able to access any resources and permissions in its own taxonomies - :CaseLevel: System - :CaseAutomation: Automated """ org_admin = self.create_org_admin_role( @@ -1762,8 +1730,6 @@ def test_positive_assign_org_admin_to_ldap_user_group( resources in taxonomies if the taxonomies of Org Admin role are same - :CaseLevel: System - :CaseAutomation: Automated """ group_name = gen_string("alpha") @@ -1825,8 +1791,6 @@ def test_negative_assign_org_admin_to_ldap_user_group( resources in taxonomies if the taxonomies of Org Admin role is not same - :CaseLevel: System - :CaseAutomation: Automated """ group_name = gen_string("alpha") diff --git a/tests/foreman/api/test_settings.py b/tests/foreman/api/test_settings.py index b4fe49c23c1..4cbb5609bd0 100644 --- a/tests/foreman/api/test_settings.py +++ b/tests/foreman/api/test_settings.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Settings :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random @@ -50,7 +45,6 @@ def test_positive_update_login_page_footer_text(setting_update): :parametrized: yes :expectedresults: Parameter is updated successfully - """ login_text_value = random.choice(list(valid_data_list().values())) setting_update.value = login_text_value @@ -69,7 +63,6 @@ def test_positive_update_login_page_footer_text_without_value(setting_update): :parametrized: yes :expectedresults: login_text has empty value after update - """ setting_update.value = "" setting_update = setting_update.update({'value'}) @@ -88,7 +81,6 @@ def test_positive_update_login_page_footer_text_with_long_string(setting_update) :parametrized: yes :expectedresults: Parameter is updated - """ login_text_value = random.choice(list(generate_strings_list(1000))) setting_update.value = login_text_value @@ -126,7 +118,6 @@ def test_positive_update_hostname_prefix_without_value(setting_update): :BZ: 1911228 :expectedresults: Error should be raised on setting empty value for discovery_prefix setting - """ setting_update.value = "" with pytest.raises(HTTPError): @@ -192,7 +183,7 @@ def test_positive_custom_repo_download_policy(setting_update, download_policy, t :id: d5150cce-ba85-4ea0-a8d1-6a54d0d29571 - :Steps: + :steps: 1. Create a product, Organization 2. Update the Default Custom Repository download policy in the setting. 3. Create a custom repo under the created organization. @@ -205,8 +196,6 @@ def test_positive_custom_repo_download_policy(setting_update, download_policy, t repository. :CaseImportance: Medium - - :CaseLevel: Acceptance """ org = target_sat.api.Organization().create() prod = target_sat.api.Product(organization=org).create() diff --git a/tests/foreman/api/test_subnet.py b/tests/foreman/api/test_subnet.py index 9ac9fca0979..b4882116f8e 100644 --- a/tests/foreman/api/test_subnet.py +++ b/tests/foreman/api/test_subnet.py @@ -9,17 +9,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Networking :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import re @@ -206,8 +201,6 @@ def test_positive_inherit_subnet_parmeters_in_host(): 2. The parameters from subnet should be displayed in host enc output - :CaseLevel: System - :CaseImportance: Medium :BZ: 1470014 @@ -234,8 +227,6 @@ def test_positive_subnet_parameters_override_from_host(): 2. The new value should be assigned to parameter 3. The parameter and value should be accessible as host parameters - :CaseLevel: Integration - :CaseImportance: Medium :BZ: 1470014 @@ -258,8 +249,6 @@ def test_positive_subnet_parameters_override_impact_on_subnet(target_sat): :expectedresults: The override value of subnet parameter from host should not change actual value in subnet parameter - :CaseLevel: System - :CaseImportance: Medium """ @@ -368,8 +357,6 @@ def test_positive_update_subnet_parameter_host_impact(): 2. The inherited subnet parameter in host enc should have updated name and value - :CaseLevel: Integration - :BZ: 1470014 """ @@ -415,8 +402,6 @@ def test_positive_delete_subnet_parameter_host_impact(): 1. The parameter should be deleted from host 2. The parameter should be deleted from host enc - :CaseLevel: Integration - :BZ: 1470014 """ @@ -444,8 +429,6 @@ def test_positive_delete_subnet_overridden_parameter_host_impact(): host parameter now 2. The parameter should not be deleted from host enc as well - :CaseLevel: Integration - :BZ: 1470014 """ @@ -512,8 +495,6 @@ def test_positive_subnet_parameter_priority(): 2. Host enc should display the parameter with value inherited from higher priority component(HostGroup in this case) - :CaseLevel: System - :CaseImportance: Low :BZ: 1470014 @@ -543,8 +524,6 @@ def test_negative_component_overrides_subnet_parameter(): 2. Host enc should not display the parameter with value inherited from lower priority component(domain in this case) - :CaseLevel: System - :CaseImportance: Low :BZ: 1470014 diff --git a/tests/foreman/api/test_subscription.py b/tests/foreman/api/test_subscription.py index bccb968f479..316ef4e7448 100644 --- a/tests/foreman/api/test_subscription.py +++ b/tests/foreman/api/test_subscription.py @@ -8,17 +8,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: SubscriptionManagement :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string from nailgun.config import ServerConfig diff --git a/tests/foreman/api/test_syncplan.py b/tests/foreman/api/test_syncplan.py index dec71b6b9c3..3ecbfac8714 100644 --- a/tests/foreman/api/test_syncplan.py +++ b/tests/foreman/api/test_syncplan.py @@ -8,17 +8,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: SyncPlans :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from datetime import datetime, timedelta from time import sleep @@ -485,8 +480,6 @@ def test_positive_add_product(module_org, target_sat): :expectedresults: A sync plan can be created and one product can be added to it. - :CaseLevel: Integration - :CaseImportance: Critical """ sync_plan = target_sat.api.SyncPlan(enabled=False, organization=module_org).create() @@ -506,8 +499,6 @@ def test_positive_add_products(module_org, target_sat): :expectedresults: A sync plan can be created and two products can be added to it. - - :CaseLevel: Integration """ sync_plan = target_sat.api.SyncPlan(enabled=False, organization=module_org).create() products = [target_sat.api.Product(organization=module_org).create() for _ in range(2)] @@ -528,8 +519,6 @@ def test_positive_remove_product(module_org, target_sat): :expectedresults: A sync plan can be created and one product can be removed from it. - :CaseLevel: Integration - :BZ: 1199150 """ sync_plan = target_sat.api.SyncPlan(enabled=False, organization=module_org).create() @@ -553,8 +542,6 @@ def test_positive_remove_products(module_org, target_sat): :expectedresults: A sync plan can be created and both products can be removed from it. - - :CaseLevel: Integration """ sync_plan = target_sat.api.SyncPlan(enabled=False, organization=module_org).create() products = [target_sat.api.Product(organization=module_org).create() for _ in range(2)] @@ -574,8 +561,6 @@ def test_positive_repeatedly_add_remove(module_org, request, target_sat): :expectedresults: A task is returned which can be used to monitor the additions and removals. - :CaseLevel: Integration - :BZ: 1199150 """ sync_plan = target_sat.api.SyncPlan(organization=module_org).create() @@ -598,8 +583,6 @@ def test_positive_add_remove_products_custom_cron(module_org, request, target_sa :expectedresults: A sync plan can be created and both products can be removed from it. - - :CaseLevel: Integration """ cron_expression = gen_choice(valid_cron_expressions()) @@ -625,8 +608,6 @@ def test_negative_synchronize_custom_product_past_sync_date(module_org, request, :expectedresults: Product was not synchronized :BZ: 1279539 - - :CaseLevel: System """ product = target_sat.api.Product(organization=module_org).create() repo = target_sat.api.Repository(product=product).create() @@ -657,8 +638,6 @@ def test_positive_synchronize_custom_product_past_sync_date(module_org, request, :expectedresults: Product is synchronized successfully. :BZ: 1279539 - - :CaseLevel: System """ interval = 60 * 60 # 'hourly' sync interval in seconds delay = 2 * 60 @@ -703,8 +682,6 @@ def test_positive_synchronize_custom_product_future_sync_date(module_org, reques :expectedresults: Product is synchronized successfully. - :CaseLevel: System - :BZ: 1655595, 1695733 """ delay = 2 * 60 # delay for sync date in seconds @@ -754,8 +731,6 @@ def test_positive_synchronize_custom_products_future_sync_date(module_org, reque :expectedresults: Products are synchronized successfully. - :CaseLevel: System - :BZ: 1695733 """ # Test with multiple products and multiple repos needs more delay. @@ -821,8 +796,6 @@ def test_positive_synchronize_rh_product_past_sync_date( :customerscenario: true :BZ: 1279539, 1879537 - - :CaseLevel: System """ interval = 60 * 60 # 'hourly' sync interval in seconds delay = 2 * 60 @@ -886,8 +859,6 @@ def test_positive_synchronize_rh_product_future_sync_date( :id: 6697a00f-2181-4c2b-88eb-2333268d780b :expectedresults: Product is synchronized successfully. - - :CaseLevel: System """ delay = 2 * 60 # delay for sync date in seconds org = function_entitlement_manifest_org @@ -944,8 +915,6 @@ def test_positive_synchronize_custom_product_daily_recurrence(module_org, reques :id: d60e33a0-f75c-498e-9e6f-0a2025295a9d :expectedresults: Product is synchronized successfully. - - :CaseLevel: System """ delay = 2 * 60 product = target_sat.api.Product(organization=module_org).create() @@ -989,8 +958,6 @@ def test_positive_synchronize_custom_product_weekly_recurrence(module_org, reque :expectedresults: Product is synchronized successfully. :BZ: 1396647 - - :CaseLevel: System """ delay = 2 * 60 product = target_sat.api.Product(organization=module_org).create() @@ -1031,8 +998,6 @@ def test_positive_delete_one_product(module_org, target_sat): :expectedresults: A sync plan is created with one product and sync plan can be deleted. - - :CaseLevel: Integration """ sync_plan = target_sat.api.SyncPlan(organization=module_org).create() product = target_sat.api.Product(organization=module_org).create() @@ -1051,8 +1016,6 @@ def test_positive_delete_products(module_org, target_sat): :expectedresults: A sync plan is created with one product and sync plan can be deleted. - - :CaseLevel: Integration """ sync_plan = target_sat.api.SyncPlan(organization=module_org).create() products = [target_sat.api.Product(organization=module_org).create() for _ in range(2)] @@ -1072,8 +1035,6 @@ def test_positive_delete_synced_product(module_org, module_target_sat): :expectedresults: A sync plan is created with one synced product and sync plan can be deleted. - - :CaseLevel: Integration """ sync_plan = module_target_sat.api.SyncPlan(organization=module_org).create() product = module_target_sat.api.Product(organization=module_org).create() @@ -1095,8 +1056,6 @@ def test_positive_delete_synced_product_custom_cron(module_org, module_target_sa :expectedresults: A sync plan is created with one synced product and sync plan can be deleted. - - :CaseLevel: Integration """ sync_plan = module_target_sat.api.SyncPlan( organization=module_org, diff --git a/tests/foreman/api/test_template_combination.py b/tests/foreman/api/test_template_combination.py index 3eb5d59c125..a10125e0a54 100644 --- a/tests/foreman/api/test_template_combination.py +++ b/tests/foreman/api/test_template_combination.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ProvisioningTemplates :Team: Rocket -:TestType: Functional - -:Upstream: No """ import pytest from requests.exceptions import HTTPError diff --git a/tests/foreman/api/test_templatesync.py b/tests/foreman/api/test_templatesync.py index 8e38540d395..d91c8ad378d 100644 --- a/tests/foreman/api/test_templatesync.py +++ b/tests/foreman/api/test_templatesync.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Integration - :CaseComponent: TemplatesPlugin :Team: Endeavour -:TestType: Functional - -:Upstream: No """ import base64 import json @@ -35,10 +30,7 @@ class TestTemplateSyncTestCase: - """Implements TemplateSync tests from API - - :CaseLevel: Acceptance - """ + """Implements TemplateSync tests from API""" @pytest.fixture(scope='module', autouse=True) def setUpClass(self, module_target_sat): @@ -74,7 +66,7 @@ def test_positive_import_filtered_templates_from_git( :id: 628a95d6-7a4e-4e56-ad7b-d9fecd34f765 - :Steps: + :steps: 1. Using nailgun or direct API call import only the templates matching with regex e.g: `^atomic.*` refer to: `/apidoc/v2/template/import.html` @@ -150,7 +142,7 @@ def test_import_filtered_templates_from_git_with_negate(self, module_org, module :id: a6857454-249b-4a2e-9b53-b5d7b4eb34e3 - :Steps: + :steps: 1. Using nailgun or direct API call import the templates NOT matching with regex e.g: `^freebsd.*` refer to: `/apidoc/v2/template/import.html` using the @@ -194,7 +186,7 @@ def test_import_filtered_templates_from_git_with_negate(self, module_org, module def test_import_template_with_puppet(self, parametrized_puppet_sat): """Importing puppet templates with enabled/disabled puppet module - :Steps: + :steps: 1. Have enabled(disabled) puppet module 2. Import template containing puppet 3. Check if template was imported @@ -243,7 +235,7 @@ def test_positive_import_and_associate( :id: 04a14a56-bd71-412b-b2da-4b8c3991c401 - :Steps: + :steps: 1. Create new taxonomies, lets say org X and loc Y. 2. From X and Y taxonomies scope, Import template1 as associate 'never', where the template contains the metadata anything other than X and Y taxonomies. @@ -373,7 +365,7 @@ def test_positive_import_from_subdirectory(self, module_org, module_target_sat): :id: 8ea11a1a-165e-4834-9387-7accb4c94e77 - :Steps: + :steps: 1. Using nailgun or direct API call import templates specifying a git subdirectory e.g: `-d {'dirname': 'test_sub_dir'}` in POST body @@ -411,7 +403,7 @@ def test_positive_export_filtered_templates_to_localdir( :id: b7c98b75-4dd1-4b6a-b424-35b0f48c25db - :Steps: + :steps: 1. Using nailgun or direct API call export only the templates matching with regex e.g: `robottelo` refer to: `/apidoc/v2/template/export.html` @@ -447,7 +439,7 @@ def test_positive_export_filtered_templates_negate( :id: 2f8ad8f3-f02b-4b2d-85af-423a228976f3 - :Steps: + :steps: 1. Using nailgun or direct API call export templates matching that does not matches regex e.g: `robottelo` using `negate` option. @@ -482,7 +474,7 @@ def test_positive_export_and_import_with_metadata( :id: ba8a34ce-c2c6-4889-8729-59714c0a4b19 - :Steps: + :steps: 1. Create a template in local directory and specify Org/Loc. 2. Use import to pull this specific template (using filter). 3. Using nailgun or direct API call @@ -560,7 +552,7 @@ def test_positive_import_json_output_verbose(self, module_org, verbose, module_t :id: 74b0a701-341f-4062-9769-e5cb1a1c4792 - :Steps: + :steps: 1. Using nailgun or direct API call Impot a template with verbose `True` and `False` option @@ -612,7 +604,7 @@ def test_positive_import_json_output_changed_key_true( :id: 4b866144-822c-4786-9188-53bc7e2dd44a - :Steps: + :steps: 1. Using nailgun or direct API call Create a template and import it from a source 2. Update the template data in source location @@ -648,7 +640,7 @@ def test_positive_import_json_output_changed_key_false( :id: 64456c0c-c2c6-4a1c-a16e-54ca4a8b66d3 - :Steps: + :steps: 1. Using nailgun or direct API call Create a template and import it from a source 2. Dont update the template data in source location @@ -681,7 +673,7 @@ def test_positive_import_json_output_name_key( :id: a5639368-3d23-4a37-974a-889e2ec0916e - :Steps: + :steps: 1. Using nailgun or direct API call Create a template with some name and import it from a source @@ -713,7 +705,7 @@ def test_positive_import_json_output_imported_key( :id: 5bc11163-e8f3-4744-8a76-5c16e6e46e86 - :Steps: + :steps: 1. Using nailgun or direct API call Create a template and import it from a source @@ -740,7 +732,7 @@ def test_positive_import_json_output_file_key( :id: da0b094c-6dc8-4526-b115-8e08bfb05fbb - :Steps: + :steps: 1. Using nailgun or direct API call Create a template with some name and import it from a source @@ -767,7 +759,7 @@ def test_positive_import_json_output_corrupted_metadata( :id: 6bd5bc6b-a7a2-4529-9df6-47a670cd86d8 - :Steps: + :steps: 1. Create a template with wrong syntax in metadata 2. Using nailgun or direct API call Import above template @@ -801,7 +793,7 @@ def test_positive_import_json_output_filtered_skip_message( :id: db68b5de-7647-4568-b79c-2aec3292328a - :Steps: + :steps: 1. Using nailgun or direct API call Create template with name not matching filter @@ -837,7 +829,7 @@ def test_positive_import_json_output_no_name_error( :id: 259a8a3a-8749-442d-a2bc-51e9af89ce8c - :Steps: + :steps: 1. Create a template without name in metadata 2. Using nailgun or direct API call Import above template @@ -871,7 +863,7 @@ def test_positive_import_json_output_no_model_error( :id: d3f1ffe4-58d7-45a8-b278-74e081dc5062 - :Steps: + :steps: 1. Create a template without model keyword in metadata 2. Using nailgun or direct API call Import above template @@ -905,7 +897,7 @@ def test_positive_import_json_output_blank_model_error( :id: 5007b12d-1cf6-49e6-8e54-a189d1a209de - :Steps: + :steps: 1. Create a template with blank model name in metadata 2. Using nailgun or direct API call Import above template @@ -938,7 +930,7 @@ def test_positive_export_json_output( :id: 141b893d-72a3-47c2-bb03-004c757bcfc9 - :Steps: + :steps: 1. Using nailgun or direct API call Export all the templates @@ -990,7 +982,7 @@ def test_positive_import_log_to_production(self, module_org, target_sat): :id: 19ed0e6a-ee77-4e28-86c9-49db1adec479 - :Steps: + :steps: 1. Using nailgun or direct API call Import template from a source @@ -999,8 +991,6 @@ def test_positive_import_log_to_production(self, module_org, target_sat): :Requirement: Take Templates out of tech preview - :CaseLevel: System - :CaseImportance: Low """ target_sat.api.Template().imports( @@ -1028,7 +1018,7 @@ def test_positive_export_log_to_production( :id: 8ae370b1-84e8-436e-a7d7-99cd0b8f45b1 - :Steps: + :steps: 1. Using nailgun or direct API call Export template to destination @@ -1037,8 +1027,6 @@ def test_positive_export_log_to_production( :Requirement: Take Templates out of tech preview - :CaseLevel: System - :CaseImportance: Low """ target_sat.api.Template().imports( @@ -1085,7 +1073,7 @@ def test_positive_export_all_templates_to_repo( :id: 0bf6fe77-01a3-4843-86d6-22db5b8adf3b - :Steps: + :steps: 1. Using nailgun export all templates to repository (ensure filters are empty) :expectedresults: @@ -1126,7 +1114,7 @@ def test_positive_import_all_templates_from_repo(self, module_org, module_target :id: 95ac9543-d989-44f4-b4d9-18f20a0b58b9 - :Steps: + :steps: 1. Using nailgun import all templates from repository (ensure filters are empty) :expectedresults: @@ -1158,7 +1146,7 @@ def test_negative_import_locked_template(self, module_org, module_target_sat): :id: 88e21cad-448e-45e0-add2-94493a1319c5 - :Steps: + :steps: 1. Using nailgun try to import a locked template :expectedresults: @@ -1207,7 +1195,7 @@ def test_positive_import_locked_template(self, module_org, module_target_sat): :id: 936c91cc-1947-45b0-8bf0-79ba4be87b97 - :Steps: + :steps: 1. Using nailgun try to import a locked template with force parameter :expectedresults: diff --git a/tests/foreman/api/test_user.py b/tests/foreman/api/test_user.py index de78386805e..75dd19e6f55 100644 --- a/tests/foreman/api/test_user.py +++ b/tests/foreman/api/test_user.py @@ -8,17 +8,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: UsersRoles :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import json import re @@ -640,7 +635,6 @@ def test_positive_ssh_key_in_host_enc(self, class_target_sat): :expectedresults: SSH key should be added to host ENC output - :CaseLevel: Integration """ org = class_target_sat.api.Organization().create() loc = class_target_sat.api.Location(organization=[org]).create() @@ -709,7 +703,6 @@ def test_positive_create_in_ldap_mode(self, username, create_ldap, target_sat): :expectedresults: User is created without specifying the password - :CaseLevel: Integration """ user = target_sat.api.User( login=username, auth_source=create_ldap['authsource'], password='' @@ -728,7 +721,6 @@ def test_positive_ad_basic_no_roles(self, create_ldap, target_sat): :expectedresults: Log in to foreman successfully but cannot access target_sat.api. - :CaseLevel: System """ sc = ServerConfig( auth=(create_ldap['ldap_user_name'], create_ldap['ldap_user_passwd']), @@ -756,7 +748,6 @@ def test_positive_access_entities_from_ldap_org_admin(self, create_ldap, module_ :expectedresults: LDAP User should be able to access all the resources and permissions in taxonomies selected in Org Admin role - :CaseLevel: System """ # Workaround issue where, in an upgrade template, there is already # some auth source present with this user. That auth source instance @@ -861,7 +852,6 @@ def test_positive_ipa_basic_no_roles(self, create_ldap, target_sat): :expectedresults: Log in to foreman successfully but cannot access target_sat.api. - :CaseLevel: System """ sc = ServerConfig( auth=(create_ldap['username'], create_ldap['ldap_user_passwd']), @@ -889,7 +879,6 @@ def test_positive_access_entities_from_ipa_org_admin(self, create_ldap, target_s :expectedresults: FreeIPA User should be able to access all the resources and permissions in taxonomies selected in Org Admin role - :CaseLevel: System """ role_name = gen_string('alpha') default_org_admin = target_sat.api.Role().search( @@ -949,8 +938,6 @@ def test_personal_access_token_admin(self): 1. Should show output of the api endpoint 2. When revoked, authentication error - :CaseLevel: System - :CaseImportance: High """ @@ -973,10 +960,7 @@ def test_positive_personal_access_token_user_with_role(self): 2. When an incorrect role and end point is used, missing permission should be displayed. - :CaseLevel: System - :CaseImportance: High - """ @pytest.mark.tier2 @@ -993,8 +977,6 @@ def test_expired_personal_access_token(self): :expectedresults: Authentication error - :CaseLevel: System - :CaseImportance: Medium """ diff --git a/tests/foreman/api/test_usergroup.py b/tests/foreman/api/test_usergroup.py index a01ee213a38..95d1547934f 100644 --- a/tests/foreman/api/test_usergroup.py +++ b/tests/foreman/api/test_usergroup.py @@ -7,17 +7,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: UsersRoles :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from random import randint @@ -155,7 +150,6 @@ def test_positive_create_with_usergroups(self, target_sat): :expectedresults: User group is created successfully and contains all expected user groups - :CaseLevel: Integration """ sub_user_groups = [target_sat.api.UserGroup().create() for _ in range(randint(3, 5))] user_group = target_sat.api.UserGroup(usergroup=sub_user_groups).create() @@ -234,7 +228,6 @@ def test_positive_update_with_existing_user(self, target_sat): :expectedresults: User group is updated successfully. - :CaseLevel: Integration """ users = [target_sat.api.User().create() for _ in range(2)] user_group = target_sat.api.UserGroup(user=[users[0]]).create() diff --git a/tests/foreman/api/test_webhook.py b/tests/foreman/api/test_webhook.py index 7b253e2b714..82512e8eb10 100644 --- a/tests/foreman/api/test_webhook.py +++ b/tests/foreman/api/test_webhook.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: HooksandWebhooks :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import re diff --git a/tests/foreman/cli/test_abrt.py b/tests/foreman/cli/test_abrt.py index ecba0b067b7..0543679b733 100644 --- a/tests/foreman/cli/test_abrt.py +++ b/tests/foreman/cli/test_abrt.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Infrastructure :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -30,7 +25,7 @@ def test_positive_create_report(self): :Setup: abrt - :Steps: start a sleep process in background, kill it send the report + :steps: start a sleep process in background, kill it send the report using smart-proxy-abrt-send :expectedresults: A abrt report with ccpp.* extension created under @@ -39,7 +34,6 @@ def test_positive_create_report(self): :CaseAutomation: NotAutomated :CaseImportance: Critical - """ @@ -51,7 +45,7 @@ def test_positive_create_reports(self): :Setup: abrt - :Steps: + :steps: 1. Create multiple reports of abrt 2. Keep track of counts @@ -59,7 +53,6 @@ def test_positive_create_reports(self): :expectedresults: Count is updated in proper manner :CaseAutomation: NotAutomated - """ @@ -71,12 +64,11 @@ def test_positive_update_timer(self): :Setup: abrt - :Steps: edit the timer for /etc/cron.d/rubygem-smart_proxy_abrt + :steps: edit the timer for /etc/cron.d/rubygem-smart_proxy_abrt :expectedresults: the timer file is edited :CaseAutomation: NotAutomated - """ @@ -88,12 +80,11 @@ def test_positive_identify_hostname(self): :Setup: abrt - :Steps: UI => Settings => Abrt tab => edit hostnames + :steps: UI => Settings => Abrt tab => edit hostnames :expectedresults: Assertion of hostnames is possible :CaseAutomation: NotAutomated - """ @@ -105,10 +96,9 @@ def test_positive_search_report(self): :Setup: abrt - :Steps: access /var/tmp/abrt/ccpp-* files + :steps: access /var/tmp/abrt/ccpp-* files :expectedresults: Assertion of parameters :CaseAutomation: NotAutomated - """ diff --git a/tests/foreman/cli/test_acs.py b/tests/foreman/cli/test_acs.py index 37750aed9bf..f7ad33cf204 100644 --- a/tests/foreman/cli/test_acs.py +++ b/tests/foreman/cli/test_acs.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: AlternateContentSources :Team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_alphanumeric import pytest @@ -58,7 +53,6 @@ def test_positive_CRUD_all_types( 3. ACS can be updated and read with new name. 4. ACS can be refreshed. 5. ACS can be deleted. - """ if 'rhui' in request.node.name and 'file' in request.node.name: pytest.skip('unsupported parametrize combination') @@ -141,7 +135,6 @@ def test_negative_check_name_validation(module_target_sat, acs_type): :expectedresults: 1. Should fail with validation error and proper message. - """ with pytest.raises(CLIReturnCodeError) as context: module_target_sat.cli.ACS.create({'alternate-content-source-type': acs_type}) @@ -167,7 +160,6 @@ def test_negative_check_custom_rhui_validations(module_target_sat, acs_type, mod :expectedresults: 1. Should fail as base-url and verify-ssl are required. 2. Should fail as product-ids is forbidden. - """ # Create with required missing with pytest.raises(CLIReturnCodeError) as context: @@ -217,7 +209,6 @@ def test_negative_check_simplified_validations( :expectedresults: 1. Should fail and list all the forbidden parameters must be blank. - """ # Create with forbidden present params = { diff --git a/tests/foreman/cli/test_activationkey.py b/tests/foreman/cli/test_activationkey.py index 15aca83eaf7..7a0d5f57dd4 100644 --- a/tests/foreman/cli/test_activationkey.py +++ b/tests/foreman/cli/test_activationkey.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ActivationKeys :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from random import choice import re @@ -147,8 +142,6 @@ def test_positive_create_with_cv(name, module_org, get_default_env, module_targe :expectedresults: Activation key is created and has proper content view assigned - :CaseLevel: Integration - :parametrized: yes """ new_cv = module_target_sat.cli_factory.make_content_view( @@ -206,8 +199,6 @@ def test_positive_create_content_and_check_enabled(module_org, module_target_sat successfully :BZ: 1361993 - - :CaseLevel: Integration """ result = module_target_sat.cli_factory.setup_org_for_a_custom_repo( {'url': settings.repos.yum_0.url, 'organization-id': module_org.id} @@ -367,8 +358,6 @@ def test_positive_delete_with_cv(module_org, module_target_sat): :id: bba323fa-0362-4a9b-97af-560d446cbb6c :expectedresults: Activation key is deleted - - :CaseLevel: Integration """ new_cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) new_ak = module_target_sat.cli_factory.make_activation_key( @@ -387,8 +376,6 @@ def test_positive_delete_with_lce(module_org, get_default_env, module_target_sat :id: e1830e52-5b1a-4ac4-8d0a-df6efb218a8b :expectedresults: Activation key is deleted - - :CaseLevel: Integration """ new_ak = module_target_sat.cli_factory.make_activation_key( {'organization-id': module_org.id, 'lifecycle-environment': get_default_env['name']} @@ -477,8 +464,6 @@ def test_positive_update_lce(module_org, get_default_env, module_target_sat): :id: 55aaee60-b8c8-49f0-995a-6c526b9b653b :expectedresults: Activation key is updated - - :CaseLevel: Integration """ ak_env = module_target_sat.cli_factory.make_activation_key( {'organization-id': module_org.id, 'lifecycle-environment-id': get_default_env['id']} @@ -511,8 +496,6 @@ def test_positive_update_cv(module_org, module_target_sat): :id: aa94997d-fc9b-4532-aeeb-9f27b9834914 :expectedresults: Activation key is updated - - :CaseLevel: Integration """ cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) ak_cv = module_target_sat.cli_factory.make_activation_key( @@ -616,7 +599,7 @@ def test_positive_usage_limit(module_org, target_sat): :id: 00ded856-e939-4140-ac84-91b6a8643623 - :Steps: + :steps: 1. Create Activation key 2. Update Usage Limit to a finite number @@ -628,8 +611,6 @@ def test_positive_usage_limit(module_org, target_sat): shown :CaseImportance: Critical - - :CaseLevel: System """ env = target_sat.cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) new_cv = target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) @@ -671,8 +652,6 @@ def test_positive_update_host_collection(module_org, host_col_name, module_targe :expectedresults: Host collections are successfully associated to Activation key - :CaseLevel: Integration - :parametrized: yes """ activation_key = module_target_sat.cli_factory.make_activation_key( @@ -731,8 +710,6 @@ def test_positive_add_redhat_product(function_entitlement_manifest_org, target_s :expectedresults: RH products are successfully associated to Activation key - - :CaseLevel: System """ org = function_entitlement_manifest_org @@ -763,8 +740,6 @@ def test_positive_add_custom_product(module_org, module_target_sat): :expectedresults: Custom products are successfully associated to Activation key - :CaseLevel: System - :BZ: 1426386 """ result = module_target_sat.cli_factory.setup_org_for_a_custom_repo( @@ -788,7 +763,7 @@ def test_positive_add_redhat_and_custom_products( :id: 74c77426-18f5-4abb-bca9-a2135f7fcc1f - :Steps: + :steps: 1. Create Activation key 2. Associate RH product(s) to Activation Key @@ -797,8 +772,6 @@ def test_positive_add_redhat_and_custom_products( :expectedresults: RH/Custom product is successfully associated to Activation key - :CaseLevel: System - :BZ: 1426386 """ org = function_entitlement_manifest_org @@ -836,7 +809,7 @@ def test_positive_delete_manifest(function_entitlement_manifest_org, target_sat) :id: 8256ac6d-3f60-4668-897d-2e88d29532d3 - :Steps: + :steps: 1. Upload manifest 2. Create activation key - attach some subscriptions 3. Delete manifest @@ -877,8 +850,6 @@ def test_positive_delete_subscription(function_entitlement_manifest_org, module_ :expectedresults: Deleting subscription removes it from the Activation key - - :CaseLevel: Integration """ org = function_entitlement_manifest_org new_ak = module_target_sat.cli_factory.make_activation_key({'organization-id': org.id}) @@ -916,8 +887,6 @@ def test_positive_update_aks_to_chost(module_org, rhel7_contenthost, target_sat) host :parametrized: yes - - :CaseLevel: System """ env = target_sat.cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) new_cv = target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) @@ -965,8 +934,6 @@ def test_positive_update_aks_to_chost_in_one_command(module_org): :expectedresults: Multiple Activation keys are attached to a Content host - - :CaseLevel: System """ @@ -1048,7 +1015,7 @@ def test_positive_remove_host_collection_by_id(module_org, module_target_sat): :id: 20f8ecca-1756-4900-b966-f0144b6bd0aa - :Steps: + :steps: 1. Create Activation key 2. Create host collection @@ -1061,8 +1028,6 @@ def test_positive_remove_host_collection_by_id(module_org, module_target_sat): :CaseImportance: Medium - :CaseLevel: Integration - :BZ: 1336716 """ activation_key = module_target_sat.cli_factory.make_activation_key( @@ -1099,7 +1064,7 @@ def test_positive_remove_host_collection_by_name(module_org, host_col, module_ta :id: 1a559a82-db5f-48b0-beeb-2fa02aed7ef9 - :Steps: + :steps: 1. Create Activation key 2. Create host collection @@ -1110,8 +1075,6 @@ def test_positive_remove_host_collection_by_name(module_org, host_col, module_ta :expectedresults: Host collection successfully removed from activation key - :CaseLevel: Integration - :BZ: 1336716 :parametrized: yes @@ -1151,7 +1114,7 @@ def test_create_ak_with_syspurpose_set(module_entitlement_manifest_org, module_t :id: ac8931e5-7089-494a-adac-cee2a8ab57ee - :Steps: + :steps: 1. Create Activation key with system purpose values set 2. Read Activation key values and assert system purpose values are set 3. Clear AK system purpose values @@ -1202,7 +1165,7 @@ def test_update_ak_with_syspurpose_values(module_entitlement_manifest_org, modul :id: db943c05-70f1-4385-9537-fe23368a9dfd - :Steps: + :steps: 1. Create Activation key with no system purpose values set 2. Assert system purpose values are not set @@ -1270,7 +1233,7 @@ def test_positive_add_subscription_by_id(module_entitlement_manifest_org, module :id: b884be1c-b35d-440a-9a9d-c854c83e10a7 - :Steps: + :steps: 1. Create Activation key 2. Upload manifest and add subscription @@ -1280,8 +1243,6 @@ def test_positive_add_subscription_by_id(module_entitlement_manifest_org, module :BZ: 1463685 - :CaseLevel: Integration - :BZ: 1463685 """ org_id = module_entitlement_manifest_org.id @@ -1346,7 +1307,6 @@ def test_negative_copy_with_same_name(module_org, module_target_sat): :id: f867c468-4155-495c-a1e5-c04d9868a2e0 :expectedresults: Activation key is not successfully copied - """ parent_ak = module_target_sat.cli_factory.make_activation_key( {'organization-id': module_org.id} @@ -1372,15 +1332,13 @@ def test_positive_copy_subscription(module_entitlement_manifest_org, module_targ :id: f4ee8096-4120-4d06-8c9a-57ac1eaa8f68 - :Steps: + :steps: 1. Create parent key and add content 2. Copy Activation key by passing id of parent 3. Verify content was successfully copied :expectedresults: Activation key is successfully copied - - :CaseLevel: Integration """ # Begin test setup org = module_entitlement_manifest_org @@ -1410,7 +1368,7 @@ def test_positive_update_autoattach_toggle(module_org, module_target_sat): :id: de3b5fb7-7963-420a-b4c9-c66e78a111dc - :Steps: + :steps: 1. Get the key's current auto attach value. 2. Update the key with the value's inverse. @@ -1455,7 +1413,7 @@ def test_negative_update_autoattach(module_org, module_target_sat): :id: 54b6f808-ff54-4e69-a54d-e1f99a4652f9 - :Steps: + :steps: 1. Attempt to update a key with incorrect auto-attach value 2. Verify that an appropriate error message was returned @@ -1484,7 +1442,7 @@ def test_positive_content_override(module_org, module_target_sat): :id: a4912cc0-3bf7-4e90-bb51-ec88b2fad227 - :Steps: + :steps: 1. Create activation key and add content 2. Get the first product's label @@ -1492,8 +1450,6 @@ def test_positive_content_override(module_org, module_target_sat): 4. Verify that the command succeeded :expectedresults: Activation key content override was successful - - :CaseLevel: System """ result = module_target_sat.cli_factory.setup_org_for_a_custom_repo( {'url': settings.repos.yum_0.url, 'organization-id': module_org.id} @@ -1574,8 +1530,6 @@ def test_positive_view_subscriptions_by_non_admin_user( subscription :BZ: 1406076 - - :CaseLevel: System """ org = module_entitlement_manifest_org user_name = gen_alphanumeric() @@ -1715,7 +1669,7 @@ def test_positive_ak_with_custom_product_on_rhel6(module_org, rhel6_contenthost, :customerscenario: true - :Steps: + :steps: 1. Create a custom repo 2. Create ak and add custom repo to ak 3. Add subscriptions to the ak diff --git a/tests/foreman/cli/test_ansible.py b/tests/foreman/cli/test_ansible.py index a684cdbd440..b628c9d8fa2 100644 --- a/tests/foreman/cli/test_ansible.py +++ b/tests/foreman/cli/test_ansible.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Ansible :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -31,7 +26,7 @@ def test_positive_ansible_e2e(target_sat, module_org, rhel_contenthost): :id: 0c52bc63-a41a-4f48-a980-fe49b4ecdbdc - :Steps: + :steps: 1. Register a content host with satellite 2. Import a role into satellite 3. Assign that role to a host @@ -132,7 +127,7 @@ def test_add_and_remove_ansible_role_hostgroup(target_sat): :customerscenario: true - :Steps: + :steps: 1. Create a hostgroup 2. Sync few ansible roles 3. Assign a few ansible roles with the host group diff --git a/tests/foreman/cli/test_architecture.py b/tests/foreman/cli/test_architecture.py index 4f0fe73061b..7578fe577b6 100644 --- a/tests/foreman/cli/test_architecture.py +++ b/tests/foreman/cli/test_architecture.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_choice import pytest diff --git a/tests/foreman/cli/test_auth.py b/tests/foreman/cli/test_auth.py index c57ca0ab277..59aafee0be5 100644 --- a/tests/foreman/cli/test_auth.py +++ b/tests/foreman/cli/test_auth.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Authentication :Team: Endeavour -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ from time import sleep @@ -70,7 +65,7 @@ def test_positive_create_session(admin_user, target_sat): :id: fcee7f5f-1040-41a9-bf17-6d0c24a93e22 - :Steps: + :steps: 1. Set use_sessions, set short expiration time 2. Authenticate, assert credentials are not demanded @@ -109,7 +104,7 @@ def test_positive_disable_session(admin_user, target_sat): :id: 38ee0d85-c2fe-4cac-a992-c5dbcec11031 - :Steps: + :steps: 1. Set use_sessions 2. Authenticate, assert credentials are not demanded @@ -117,7 +112,6 @@ def test_positive_disable_session(admin_user, target_sat): 3. Disable use_sessions :expectedresults: The session is terminated - """ result = configure_sessions(target_sat) assert result == 0, 'Failed to configure hammer sessions' @@ -141,7 +135,7 @@ def test_positive_log_out_from_session(admin_user, target_sat): :id: 0ba05f2d-7b83-4b0c-a04c-80e62b7c4cf2 - :Steps: + :steps: 1. Set use_sessions 2. Authenticate, assert credentials are not demanded @@ -149,7 +143,6 @@ def test_positive_log_out_from_session(admin_user, target_sat): 3. Run `hammer auth logout` :expectedresults: The session is terminated - """ result = configure_sessions(target_sat) assert result == 0, 'Failed to configure hammer sessions' @@ -171,7 +164,7 @@ def test_positive_change_session(admin_user, non_admin_user, target_sat): :id: b6ea6f3c-fcbd-4e7b-97bd-f3e0e6b9da8f - :Steps: + :steps: 1. Set use_sessions 2. Authenticate, assert credentials are not demanded @@ -181,7 +174,6 @@ def test_positive_change_session(admin_user, non_admin_user, target_sat): :CaseImportance: High :expectedresults: The session is altered - """ result = configure_sessions(target_sat) assert result == 0, 'Failed to configure hammer sessions' @@ -202,7 +194,7 @@ def test_positive_session_survives_unauthenticated_call(admin_user, target_sat): :id: 8bc304a0-70ea-489c-9c3f-ea8343c5284c - :Steps: + :steps: 1. Set use_sessions 2. Authenticate, assert credentials are not demanded @@ -212,7 +204,6 @@ def test_positive_session_survives_unauthenticated_call(admin_user, target_sat): :CaseImportance: Medium :expectedresults: The session is unchanged - """ result = configure_sessions(target_sat) assert result == 0, 'Failed to configure hammer sessions' @@ -236,7 +227,7 @@ def test_positive_session_survives_failed_login(admin_user, non_admin_user, targ :BZ: 1465552 - :Steps: + :steps: 1. Set use_sessions 2. Authenticate, assert credentials are not demanded @@ -244,7 +235,6 @@ def test_positive_session_survives_failed_login(admin_user, non_admin_user, targ 3. Run login with invalid credentials :expectedresults: The session is unchanged - """ result = configure_sessions(target_sat) assert result == 0, 'Failed to configure hammer sessions' @@ -275,7 +265,7 @@ def test_positive_session_preceeds_saved_credentials(admin_user, target_sat): :CaseImportance: High - :Steps: + :steps: 1. Set use_sessions, set username and password, set short expiration time @@ -285,7 +275,6 @@ def test_positive_session_preceeds_saved_credentials(admin_user, target_sat): :expectedresults: Session expires after specified time and saved credentials are not applied - """ try: idle_timeout = target_sat.cli.Settings.list({'search': 'name=idle_timeout'})[0]['value'] @@ -331,7 +320,6 @@ def test_negative_no_permissions(admin_user, non_admin_user, target_sat): :expectedresults: Command is not executed :CaseImportance: High - """ result = configure_sessions(target_sat) assert result == 0, 'Failed to configure hammer sessions' diff --git a/tests/foreman/cli/test_bootdisk.py b/tests/foreman/cli/test_bootdisk.py index 7e8ad4fca9c..e6515e1af9f 100644 --- a/tests/foreman/cli/test_bootdisk.py +++ b/tests/foreman/cli/test_bootdisk.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: System - :CaseComponent: BootdiskPlugin :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_mac, gen_string import pytest diff --git a/tests/foreman/cli/test_bootstrap_script.py b/tests/foreman/cli/test_bootstrap_script.py index e3b910a0a87..f4b71c24bd1 100644 --- a/tests/foreman/cli/test_bootstrap_script.py +++ b/tests/foreman/cli/test_bootstrap_script.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Bootstrap :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -35,7 +30,7 @@ def test_positive_register( :id: e34561fd-e0d6-4587-84eb-f86bd131aab1 - :Steps: + :steps: 1. Ensure system is not registered 2. Register a system diff --git a/tests/foreman/cli/test_capsule.py b/tests/foreman/cli/test_capsule.py index 94d8fdb8f0c..e41759fbc2b 100644 --- a/tests/foreman/cli/test_capsule.py +++ b/tests/foreman/cli/test_capsule.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Capsule :Team: Platform -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import pytest @@ -29,8 +24,6 @@ def test_positive_import_puppet_classes(session_puppet_enabled_sat): :id: 42e3a9c0-62e1-4049-9667-f3c0cdfe0b04 :expectedresults: Puppet classes are imported from proxy - - :CaseLevel: Component """ with session_puppet_enabled_sat as puppet_sat: port = puppet_sat.available_capsule_port @@ -50,7 +43,7 @@ def test_positive_capsule_content(): :Setup: Capsule with some content synced - :Steps: + :steps: 1. Register a host to the capsule 2. Sync content from capsule to the host diff --git a/tests/foreman/cli/test_classparameters.py b/tests/foreman/cli/test_classparameters.py index aa0d6e3fb23..433552737da 100644 --- a/tests/foreman/cli/test_classparameters.py +++ b/tests/foreman/cli/test_classparameters.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Puppet :CaseImportance: Medium :Team: Rocket -:TestType: Functional - -:Upstream: No """ import pytest diff --git a/tests/foreman/cli/test_computeresource_azurerm.py b/tests/foreman/cli/test_computeresource_azurerm.py index e2814110372..1621b48d12f 100644 --- a/tests/foreman/cli/test_computeresource_azurerm.py +++ b/tests/foreman/cli/test_computeresource_azurerm.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ComputeResources-Azure :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -73,7 +68,6 @@ def test_positive_crud_azurerm_cr( :CaseImportance: Critical - :CaseLevel: Component """ # Create CR cr_name = gen_string('alpha') @@ -156,7 +150,6 @@ def test_positive_image_crud( :CaseImportance: Critical - :CaseLevel: Integration """ # Create @@ -230,8 +223,6 @@ def test_positive_check_available_networks(self, sat_azure, azurermclient, modul :expectedresults: All the networks from AzureRM CR should be available. - :CaseLevel: Integration - :BZ: 1850934 """ @@ -255,7 +246,6 @@ def test_positive_create_compute_profile_values( :expectedresults: Compute-profile values should be create with AzureRm CR - :CaseLevel: Integration """ username = gen_string('alpha') password = gen_string('alpha') @@ -398,9 +388,7 @@ def test_positive_azurerm_host_provisioned( :id: 9e8242e5-3ef3-4884-a200-7ba79b8ef49f - :CaseLevel: Component - - ::CaseImportance: Critical + :CaseImportance: Critical :steps: 1. Create a AzureRM Compute Resource and provision host. @@ -528,9 +516,7 @@ def test_positive_azurerm_host_provisioned( :id: c99d2679-1742-4ef3-9288-2961d18a30e7 - :CaseLevel: Component - - ::CaseImportance: Critical + :CaseImportance: Critical :steps: 1. Create a AzureRM Compute Resource and provision host. diff --git a/tests/foreman/cli/test_computeresource_ec2.py b/tests/foreman/cli/test_computeresource_ec2.py index 61efdd42839..e4e1e669e6b 100644 --- a/tests/foreman/cli/test_computeresource_ec2.py +++ b/tests/foreman/cli/test_computeresource_ec2.py @@ -1,17 +1,12 @@ """ :Requirement: Computeresource EC2 -:CaseLevel: Acceptance - :CaseComponent: ComputeResources-EC2 :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -53,8 +48,6 @@ def test_positive_create_ec2_with_custom_region(aws, module_target_sat): :CaseAutomation: Automated :CaseImportance: Critical - - :CaseLevel: Component """ cr_name = gen_string(str_type='alpha') cr_description = gen_string(str_type='alpha') diff --git a/tests/foreman/cli/test_computeresource_libvirt.py b/tests/foreman/cli/test_computeresource_libvirt.py index 91baee553d9..e2109872f99 100644 --- a/tests/foreman/cli/test_computeresource_libvirt.py +++ b/tests/foreman/cli/test_computeresource_libvirt.py @@ -21,17 +21,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ComputeResources-libvirt :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random @@ -120,8 +115,6 @@ def test_positive_create_with_name(libvirt_url, module_target_sat): :expectedresults: Compute resource is created :CaseImportance: Critical - - :CaseLevel: Component """ module_target_sat.cli.ComputeResource.create( { @@ -141,8 +134,6 @@ def test_positive_info(libvirt_url, module_target_sat): :expectedresults: Compute resource Info is displayed :CaseImportance: Critical - - :CaseLevel: Component """ name = gen_string('utf8') compute_resource = module_target_sat.cli_factory.compute_resource( @@ -165,8 +156,6 @@ def test_positive_list(libvirt_url, module_target_sat): :expectedresults: Compute resource List is displayed :CaseImportance: Critical - - :CaseLevel: Component """ comp_res = module_target_sat.cli_factory.compute_resource( {'provider': FOREMAN_PROVIDERS['libvirt'], 'url': libvirt_url} @@ -190,8 +179,6 @@ def test_positive_delete_by_name(libvirt_url, module_target_sat): :expectedresults: Compute resource deleted :CaseImportance: Critical - - :CaseLevel: Component """ comp_res = module_target_sat.cli_factory.compute_resource( {'provider': FOREMAN_PROVIDERS['libvirt'], 'url': libvirt_url} @@ -215,8 +202,6 @@ def test_positive_create_with_libvirt(libvirt_url, options, target_sat): :CaseImportance: Critical - :CaseLevel: Component - :parametrized: yes """ target_sat.cli.ComputeResource.create( @@ -238,8 +223,6 @@ def test_positive_create_with_loc(libvirt_url, module_target_sat): :expectedresults: Compute resource is created and has location assigned :CaseImportance: High - - :CaseLevel: Integration """ location = module_target_sat.cli_factory.make_location() comp_resource = module_target_sat.cli_factory.compute_resource( @@ -263,8 +246,6 @@ def test_positive_create_with_locs(libvirt_url, module_target_sat): locations assigned :CaseImportance: High - - :CaseLevel: Integration """ locations_amount = random.randint(3, 5) locations = [module_target_sat.cli_factory.make_location() for _ in range(locations_amount)] @@ -294,8 +275,6 @@ def test_negative_create_with_name_url(libvirt_url, options, target_sat): :CaseImportance: High - :CaseLevel: Component - :parametrized: yes """ with pytest.raises(CLIReturnCodeError): @@ -317,8 +296,6 @@ def test_negative_create_with_same_name(libvirt_url, module_target_sat): :expectedresults: Compute resource not created :CaseImportance: High - - :CaseLevel: Component """ comp_res = module_target_sat.cli_factory.compute_resource( {'provider': FOREMAN_PROVIDERS['libvirt'], 'url': libvirt_url} @@ -347,8 +324,6 @@ def test_positive_update_name(libvirt_url, options, module_target_sat): :CaseImportance: Critical - :CaseLevel: Component - :parametrized: yes """ comp_res = module_target_sat.cli_factory.compute_resource( @@ -379,8 +354,6 @@ def test_negative_update(libvirt_url, options, module_target_sat): :CaseImportance: High - :CaseLevel: Component - :parametrized: yes """ comp_res = module_target_sat.cli_factory.compute_resource( @@ -411,8 +384,6 @@ def test_positive_create_with_console_password_and_name( :CaseImportance: High - :CaseLevel: Component - :parametrized: yes """ module_target_sat.cli.ComputeResource.create( @@ -438,8 +409,6 @@ def test_positive_update_console_password(libvirt_url, set_console_password, mod :CaseImportance: High - :CaseLevel: Component - :parametrized: yes """ cr_name = gen_string('utf8') diff --git a/tests/foreman/cli/test_computeresource_osp.py b/tests/foreman/cli/test_computeresource_osp.py index 1d76fe561d4..d88daaf1ad8 100644 --- a/tests/foreman/cli/test_computeresource_osp.py +++ b/tests/foreman/cli/test_computeresource_osp.py @@ -3,17 +3,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ComputeResources-OpenStack :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from box import Box from fauxfactory import gen_string diff --git a/tests/foreman/cli/test_computeresource_rhev.py b/tests/foreman/cli/test_computeresource_rhev.py index 5036bd2fd7c..808f1bbfd68 100644 --- a/tests/foreman/cli/test_computeresource_rhev.py +++ b/tests/foreman/cli/test_computeresource_rhev.py @@ -3,17 +3,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ComputeResources-RHEV :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -344,7 +339,6 @@ def test_negative_add_image_rhev_with_invalid_name(rhev, module_os, module_targe name parameter, compute-resource image create. :expectedresults: The image should not be added to the CR - """ if rhev.image_uuid is None: pytest.skip('Missing configuration for rhev.image_uuid') @@ -538,8 +532,6 @@ def test_positive_provision_rhev_without_host_group(rhev): :expectedresults: The host should be provisioned successfully :CaseAutomation: NotAutomated - - :CaseLevel: Integration """ diff --git a/tests/foreman/cli/test_computeresource_vmware.py b/tests/foreman/cli/test_computeresource_vmware.py index 3606b0a077d..e106fa9f49f 100644 --- a/tests/foreman/cli/test_computeresource_vmware.py +++ b/tests/foreman/cli/test_computeresource_vmware.py @@ -1,19 +1,14 @@ """ :Requirement: Computeresource Vmware -:CaseLevel: Acceptance - :CaseComponent: ComputeResources-VMWare :Team: Rocket -:TestType: Functional - :CaseImportance: High :CaseAutomation: Automated -:Upstream: No """ from fauxfactory import gen_string import pytest diff --git a/tests/foreman/cli/test_container_management.py b/tests/foreman/cli/test_container_management.py index 9229260475a..17d0b0b4bd0 100644 --- a/tests/foreman/cli/test_container_management.py +++ b/tests/foreman/cli/test_container_management.py @@ -4,13 +4,10 @@ :CaseAutomation: Automated -:TestType: Functional - :Team: Phoenix-content :CaseComponent: ContainerManagement-Content -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -52,8 +49,6 @@ class TestDockerClient: """Tests specific to using ``Docker`` as a client to pull Docker images from a Satellite 6 instance. - :CaseLevel: System - :CaseImportance: Medium """ @@ -64,7 +59,7 @@ def test_positive_pull_image(self, module_org, container_contenthost, target_sat :id: 023f0538-2aad-4f87-b8a8-6ccced648366 - :Steps: + :steps: 1. Publish and promote content view with Docker content 2. Register Docker-enabled client against Satellite 6. diff --git a/tests/foreman/cli/test_contentaccess.py b/tests/foreman/cli/test_contentaccess.py index 526ff086d33..0c59e5c70e6 100644 --- a/tests/foreman/cli/test_contentaccess.py +++ b/tests/foreman/cli/test_contentaccess.py @@ -2,17 +2,12 @@ :Requirement: Content Access -:CaseLevel: Acceptance - :CaseComponent: Hosts-Content :CaseAutomation: Automated :team: Phoenix-subscriptions -:TestType: Functional - -:Upstream: No """ import time @@ -237,8 +232,6 @@ def test_negative_unregister_and_pull_content(vm): :expectedresults: Host can no longer retrieve content from satellite - :CaseLevel: System - :parametrized: yes :CaseImportance: Critical diff --git a/tests/foreman/cli/test_contentcredentials.py b/tests/foreman/cli/test_contentcredentials.py index 8541ce0f718..208812cb857 100644 --- a/tests/foreman/cli/test_contentcredentials.py +++ b/tests/foreman/cli/test_contentcredentials.py @@ -6,17 +6,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ContentCredentials :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from tempfile import mkstemp @@ -381,8 +376,6 @@ def test_positive_add_empty_product(target_sat, module_org): :id: 61c700db-43ab-4b8c-8527-f4cfc085afaa :expectedresults: gpg key is associated with product - - :CaseLevel: Integration """ gpg_key = target_sat.cli_factory.make_content_credential({'organization-id': module_org.id}) product = target_sat.cli_factory.make_product( @@ -400,8 +393,6 @@ def test_positive_add_product_with_repo(target_sat, module_org): :expectedresults: gpg key is associated with product as well as with the repository - - :CaseLevel: Integration """ product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) repo = target_sat.cli_factory.make_repository( @@ -427,8 +418,6 @@ def test_positive_add_product_with_repos(target_sat, module_org): :expectedresults: gpg key is associated with product as well as with the repositories - - :CaseLevel: Integration """ product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) repos = [ @@ -456,8 +445,6 @@ def test_positive_add_repo_from_product_with_repo(target_sat, module_org): :expectedresults: gpg key is associated with the repository but not with the product - - :CaseLevel: Integration """ product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) repo = target_sat.cli_factory.make_repository( @@ -484,8 +471,6 @@ def test_positive_add_repo_from_product_with_repos(target_sat, module_org): :id: e3019a61-ec32-4044-9087-e420b8db4e09 :expectedresults: gpg key is associated with the repository - - :CaseLevel: Integration """ product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) repos = [ @@ -517,8 +502,6 @@ def test_positive_update_key_for_empty_product(target_sat, module_org): :expectedresults: gpg key is associated with product before/after update - - :CaseLevel: Integration """ # Create a product and a gpg key product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) @@ -555,8 +538,6 @@ def test_positive_update_key_for_product_with_repo(target_sat, module_org): :expectedresults: gpg key is associated with product before/after update as well as with the repository - - :CaseLevel: Integration """ # Create a product and a gpg key product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) @@ -602,8 +583,6 @@ def test_positive_update_key_for_product_with_repos(target_sat, module_org): :expectedresults: gpg key is associated with product before/after update as well as with the repositories - - :CaseLevel: Integration """ # Create a product and a gpg key product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) @@ -652,8 +631,6 @@ def test_positive_update_key_for_repo_from_product_with_repo(target_sat, module_ :expectedresults: gpg key is associated with the repository before/after update, but not with the product - - :CaseLevel: Integration """ # Create a product and a gpg key product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) @@ -693,8 +670,6 @@ def test_positive_update_key_for_repo_from_product_with_repos(target_sat, module :expectedresults: gpg key is associated with a single repository before/after update and not associated with product or other repositories - - :CaseLevel: Integration """ # Create a product and a gpg key product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) diff --git a/tests/foreman/cli/test_contentview.py b/tests/foreman/cli/test_contentview.py index 703787189e9..6aeea931ff7 100644 --- a/tests/foreman/cli/test_contentview.py +++ b/tests/foreman/cli/test_contentview.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ContentViews :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random @@ -205,8 +200,6 @@ def test_positive_update_filter(self, repo_setup, module_target_sat): :expectedresults: Edited content view save is successful and info is updated - :CaseLevel: Integration - :CaseImportance: High """ # Create CV @@ -289,7 +282,6 @@ def test_positive_delete_version_by_name(self, module_org, module_target_sat): :CaseImportance: High - :CaseLevel: Integration """ content_view = module_target_sat.cli_factory.make_content_view( {'organization-id': module_org.id} @@ -372,7 +364,6 @@ def test_negative_delete_version_by_id(self, module_org, module_target_sat): :CaseImportance: Critical - :CaseLevel: Integration """ content_view = module_target_sat.cli_factory.make_content_view( {'organization-id': module_org.id} @@ -417,7 +408,6 @@ def test_positive_remove_lce_by_id_and_reassign_ak(self, module_org, module_targ :CaseImportance: Medium - :CaseLevel: Integration """ env = [ module_target_sat.cli_factory.make_lifecycle_environment( @@ -484,7 +474,6 @@ def test_positive_remove_lce_by_id_and_reassign_chost(self, module_org, module_t :CaseImportance: Low - :CaseLevel: Integration """ env = [ module_target_sat.cli_factory.make_lifecycle_environment( @@ -668,8 +657,6 @@ def test_positive_create_composite(self, module_org, module_target_sat): :expectedresults: Composite content views are created - :CaseLevel: Integration - :CaseImportance: High """ # Create REPO @@ -714,8 +701,6 @@ def test_positive_create_composite_by_name(self, module_org, module_target_sat): :BZ: 1416857 - :CaseLevel: Integration - :CaseImportance: High """ new_product = module_target_sat.cli_factory.make_product({'organization-id': module_org.id}) @@ -765,8 +750,6 @@ def test_positive_remove_version_by_id_from_composite( :expectedresults: Composite content view info output does not contain any values - :CaseLevel: Integration - :CaseImportance: High """ # Create new repository @@ -820,8 +803,6 @@ def test_positive_remove_component_by_name(self, module_org, module_product, mod :BZ: 1416857 - :CaseLevel: Integration - :CaseImportance: High """ # Create new repository @@ -874,8 +855,6 @@ def test_positive_create_composite_with_component_ids(self, module_org, module_t :BZ: 1487265 - :CaseLevel: Integration - :CaseImportance: High """ # Create first CV @@ -911,8 +890,6 @@ def test_negative_create_composite_with_component_ids(self, module_org, module_t :BZ: 1487265 - :CaseLevel: Integration - :CaseImportance: Low """ # Create CV @@ -943,8 +920,6 @@ def test_positive_update_composite_with_component_ids(module_org, module_target_ :expectedresults: Composite content view component ids are similar to the nested content view versions ids - :CaseLevel: Integration - :CaseImportance: Low """ # Create a CV to add to the composite one @@ -980,8 +955,6 @@ def test_positive_add_rh_repo_by_id( :expectedresults: RH Content can be seen in the content view - :CaseLevel: Integration - :CaseImportance: Critical """ # Create CV @@ -1019,8 +992,6 @@ def test_positive_add_rh_repo_by_id_and_create_filter( :CaseImportance: Low - :CaseLevel: Integration - :BZ: 1359665 """ # Create CV @@ -1064,7 +1035,6 @@ def test_positive_add_module_stream_filter_rule(self, module_org, target_sat): :CaseImportance: Low - :CaseLevel: Integration """ filter_name = gen_string('alpha') repo_name = gen_string('alpha') @@ -1112,7 +1082,6 @@ def test_positive_add_custom_repo_by_id(self, module_org, module_product, module :CaseImportance: High - :CaseLevel: Integration """ new_repo = module_target_sat.cli_factory.make_repository( {'content-type': 'yum', 'product-id': module_product.id} @@ -1172,7 +1141,6 @@ def test_negative_add_component_in_non_composite_cv( :CaseImportance: Low - :CaseLevel: Integration """ # Create REPO new_repo = module_target_sat.cli_factory.make_repository( @@ -1209,7 +1177,6 @@ def test_negative_add_same_yum_repo_twice(self, module_org, module_product, modu :CaseImportance: Low - :CaseLevel: Integration """ new_repo = module_target_sat.cli_factory.make_repository( {'content-type': 'yum', 'product-id': module_product.id} @@ -1247,7 +1214,6 @@ def test_positive_promote_rh_content( :CaseImportance: Critical - :CaseLevel: Integration """ # Create CV new_cv = module_target_sat.cli_factory.make_content_view( @@ -1287,7 +1253,6 @@ def test_positive_promote_rh_and_custom_content( :CaseImportance: Low - :CaseLevel: Integration """ # Create custom repo new_repo = module_target_sat.cli_factory.make_repository( @@ -1348,7 +1313,6 @@ def test_positive_promote_custom_content(self, module_org, module_product, modul :CaseImportance: High - :CaseLevel: Integration """ # Create REPO new_repo = module_target_sat.cli_factory.make_repository( @@ -1395,7 +1359,6 @@ def test_positive_promote_ccv(self, module_org, module_product, module_target_sa :CaseImportance: High - :CaseLevel: Integration """ # Create REPO new_repo = module_target_sat.cli_factory.make_repository( @@ -1452,7 +1415,6 @@ def test_negative_promote_default_cv(self, module_org, module_target_sat): :CaseImportance: Low - :CaseLevel: Integration """ environment = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id} @@ -1482,7 +1444,6 @@ def test_negative_promote_with_invalid_lce(self, module_org, module_product, mod :CaseImportance: Low - :CaseLevel: Integration """ # Create REPO new_repo = module_target_sat.cli_factory.make_repository( @@ -1527,7 +1488,6 @@ def test_positive_publish_rh_content( :CaseImportance: Critical - :CaseLevel: Integration """ # Create CV new_cv = module_target_sat.cli_factory.make_content_view( @@ -1561,7 +1521,6 @@ def test_positive_publish_rh_and_custom_content( :CaseImportance: High - :CaseLevel: Integration """ # Create custom repo new_repo = module_target_sat.cli_factory.make_repository( @@ -1615,7 +1574,6 @@ def test_positive_publish_custom_content(self, module_org, module_product, modul :CaseImportance: Critical - :CaseLevel: Integration """ new_repo = module_target_sat.cli_factory.make_repository( {'content-type': 'yum', 'product-id': module_product.id} @@ -1651,7 +1609,6 @@ def test_positive_publish_custom_major_minor_cv_version(self, module_target_sat) 1. CV version with custom major and minor versions is created - :CaseLevel: System """ org = module_target_sat.cli_factory.make_org() major = random.randint(1, 1000) @@ -1687,7 +1644,6 @@ def test_positive_publish_custom_content_module_stream( :CaseImportance: Medium - :CaseLevel: Integration """ software_repo = module_target_sat.cli_factory.make_repository( { @@ -1756,7 +1712,6 @@ def test_positive_republish_after_content_removed( :customerscenario: true - :CaseLevel: Integration """ # Create new Yum repository yum_repo = module_target_sat.cli_factory.make_repository( @@ -1824,8 +1779,6 @@ def test_positive_republish_after_rh_content_removed( :BZ: 1323751 - :CaseLevel: Integration - :customerscenario: true :CaseImportance: Medium @@ -1871,7 +1824,6 @@ def test_positive_publish_ccv(self, module_org, module_product, module_target_sa :CaseImportance: Critical - :CaseLevel: Integration """ repository = module_target_sat.cli_factory.make_repository( {'content-type': 'yum', 'product-id': module_product.id} @@ -1932,8 +1884,6 @@ def test_positive_update_version_once(self, module_org, module_product, module_t environment. - :CaseLevel: Integration - :CaseImportance: Critical """ # Create REPO @@ -2011,7 +1961,6 @@ def test_positive_update_version_multiple(self, module_org, module_product, modu :CaseImportance: Low - :CaseLevel: Integration """ # Create REPO new_repo = module_target_sat.cli_factory.make_repository( @@ -2098,8 +2047,6 @@ def test_positive_auto_update_composite_to_latest_cv_version( :BZ: 1177766 - :CaseLevel: Integration - :CaseImportance: High """ content_view = module_target_sat.cli_factory.make_content_view( @@ -2155,7 +2102,6 @@ def test_positive_subscribe_chost_by_id(self, module_org, module_target_sat): :CaseImportance: High - :CaseLevel: System """ env = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id} @@ -2195,8 +2141,6 @@ def test_positive_subscribe_chost_by_id_using_rh_content( :expectedresults: Content Host can be subscribed to content view with Red Hat repository - :CaseLevel: System - :CaseImportance: Medium """ env = module_target_sat.cli_factory.make_lifecycle_environment( @@ -2247,8 +2191,6 @@ def test_positive_subscribe_chost_by_id_using_rh_content_and_filters( :expectedresults: Content Host can be subscribed to filtered content view with Red Hat repository - :CaseLevel: System - :BZ: 1359665 :CaseImportance: Low @@ -2320,8 +2262,6 @@ def test_positive_subscribe_chost_by_id_using_custom_content( :expectedresults: Content Host can be subscribed to content view with custom repository - :CaseLevel: System - :CaseImportance: High """ new_product = module_target_sat.cli_factory.make_product({'organization-id': module_org.id}) @@ -2375,7 +2315,6 @@ def test_positive_subscribe_chost_by_id_using_ccv(self, module_org, module_targe :CaseImportance: High - :CaseLevel: System """ env = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id} @@ -2448,7 +2387,6 @@ def test_positive_sub_host_with_restricted_user_perm_at_custom_loc( :parametrized: yes - :CaseLevel: System """ # Note: this test has been stubbed waitin for bug 1511481 resolution # prepare the user and the required permissions data @@ -2605,7 +2543,6 @@ def test_positive_sub_host_with_restricted_user_perm_at_default_loc( :parametrized: yes - :CaseLevel: System """ # prepare the user and the required permissions data user_name = gen_alphanumeric() @@ -2777,8 +2714,6 @@ def test_positive_clone_within_same_env(self, module_org, module_target_sat): :expectedresults: Cloned content view can be published and promoted to the same environment as the original content view - :CaseLevel: Integration - :CaseImportance: High """ cloned_cv_name = gen_string('alpha') @@ -2818,7 +2753,6 @@ def test_positive_clone_with_diff_env(self, module_org, module_target_sat): :CaseImportance: Low - :CaseLevel: Integration """ cloned_cv_name = gen_string('alpha') lc_env = module_target_sat.cli_factory.make_lifecycle_environment( @@ -2893,7 +2827,7 @@ def test_positive_remove_renamed_cv_version_from_default_env( :id: aa9bbfda-72e8-45ec-b26d-fdf2691980cf - :Steps: + :steps: 1. Create a content view 2. Add a yum repo to the content view @@ -2904,8 +2838,6 @@ def test_positive_remove_renamed_cv_version_from_default_env( :expectedresults: content view version is removed from Library environment - :CaseLevel: Integration - :CaseImportance: Low """ new_name = gen_string('alpha') @@ -2969,7 +2901,7 @@ def test_positive_remove_promoted_cv_version_from_default_env( :id: 6643837a-560a-47de-aa4d-90778914dcfa - :Steps: + :steps: 1. Create a content view 2. Add a yum repo to the content view @@ -2982,8 +2914,6 @@ def test_positive_remove_promoted_cv_version_from_default_env( 1. Content view version exist only in DEV and not in Library 2. The yum repo exists in content view version - :CaseLevel: Integration - :CaseImportance: High """ lce_dev = module_target_sat.cli_factory.make_lifecycle_environment( @@ -3061,7 +2991,7 @@ def test_positive_remove_qe_promoted_cv_version_from_default_env( :id: e286697f-4113-40a3-b8e8-9ca50647e6d5 - :Steps: + :steps: 1. Create a content view 2. Add docker repo(s) to it @@ -3073,8 +3003,6 @@ def test_positive_remove_qe_promoted_cv_version_from_default_env( :expectedresults: Content view version exist only in DEV, QE and not in Library - :CaseLevel: Integration - :CaseImportance: High """ lce_dev = module_target_sat.cli_factory.make_lifecycle_environment( @@ -3150,7 +3078,7 @@ def test_positive_remove_prod_promoted_cv_version_from_default_env( :id: ffe3d64e-c3d2-4889-9454-ccc6b10f4db7 - :Steps: + :steps: 1. Create a content view 2. Add yum repositories and docker repositories to CV @@ -3164,7 +3092,6 @@ def test_positive_remove_prod_promoted_cv_version_from_default_env( :CaseImportance: High - :CaseLevel: Integration """ lce_dev = module_target_sat.cli_factory.make_lifecycle_environment( {'organization-id': module_org.id} @@ -3257,7 +3184,7 @@ def test_positive_remove_cv_version_from_env(self, module_org, module_target_sat :id: 577757ac-b184-4ece-9310-182dd5ceb718 - :Steps: + :steps: 1. Create a content view 2. Add a yum repo and a docker repo to the content view @@ -3272,8 +3199,6 @@ def test_positive_remove_cv_version_from_env(self, module_org, module_target_sat :expectedresults: Content view version exist in Library, DEV, QE, STAGE, PROD - :CaseLevel: Integration - :CaseImportance: High """ lce_dev = module_target_sat.cli_factory.make_lifecycle_environment( @@ -3385,7 +3310,7 @@ def test_positive_remove_cv_version_from_multi_env(self, module_org, module_targ :id: 997cfd7d-9029-47e2-a41e-84f4370b5ce5 - :Steps: + :steps: 1. Create a content view 2. Add a yum repo and a docker to the content view @@ -3396,8 +3321,6 @@ def test_positive_remove_cv_version_from_multi_env(self, module_org, module_targ :expectedresults: Content view version exists only in Library, DEV - :CaseLevel: Integration - :CaseImportance: High """ lce_dev = module_target_sat.cli_factory.make_lifecycle_environment( @@ -3491,7 +3414,7 @@ def test_positive_delete_cv_promoted_to_multi_env(self, module_org, module_targe :id: 93dd7518-5901-4a71-a4c3-0f1215238b26 - :Steps: + :steps: 1. Create a content view 2. Add a yum repo and a docker to the content view @@ -3503,8 +3426,6 @@ def test_positive_delete_cv_promoted_to_multi_env(self, module_org, module_targe :expectedresults: The content view doesn't exists - :CaseLevel: Integration - :CaseImportance: High """ lce_dev = module_target_sat.cli_factory.make_lifecycle_environment( @@ -3603,7 +3524,7 @@ def test_positive_remove_cv_version_from_env_with_host_registered(self): :id: 001a2b76-a87b-4c11-8837-f5fe3c04a075 - :Steps: + :steps: 1. Create a content view cv1 2. Add a yum repo to the content view @@ -3630,7 +3551,6 @@ def test_positive_remove_cv_version_from_env_with_host_registered(self): :CaseAutomation: NotAutomated - :CaseLevel: System """ @pytest.mark.stubbed @@ -3642,7 +3562,7 @@ def test_positive_delete_cv_multi_env_promoted_with_host_registered(self): :id: 82442d23-45b5-4d39-b867-c5d46bbcbbf9 - :Steps: + :steps: 1. Create two content view cv1 and cv2 2. Add a yum repo to both content views @@ -3670,7 +3590,6 @@ def test_positive_delete_cv_multi_env_promoted_with_host_registered(self): :CaseAutomation: NotAutomated - :CaseLevel: System """ @pytest.mark.run_in_one_thread @@ -3687,7 +3606,7 @@ def test_positive_remove_cv_version_from_multi_env_capsule_scenario( :id: 3725fef6-73a4-4dcb-a306-70e6ba826a3d - :Steps: + :steps: 1. Create a content view 2. Setup satellite to use a capsule and to sync all lifecycle @@ -3712,8 +3631,6 @@ def test_positive_remove_cv_version_from_multi_env_capsule_scenario( :CaseAutomation: Automated - :CaseLevel: System - :CaseImportance: High """ # Note: This test case requires complete external capsule @@ -3911,8 +3828,6 @@ def test_negative_user_with_read_only_cv_permission(self, module_org, module_tar :BZ: 1922134 - :CaseLevel: Integration - :CaseImportance: Critical """ cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) @@ -3988,8 +3903,6 @@ def test_positive_user_with_all_cv_permissions(self, module_org, module_target_s :BZ: 1464414 - :CaseLevel: Integration - :CaseImportance: Critical """ cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) @@ -4062,7 +3975,6 @@ def test_positive_inc_update_no_lce(self, module_org, module_product, module_tar :CaseImportance: Medium - :CaseLevel: Integration """ repo = module_target_sat.cli_factory.make_repository( { @@ -4156,15 +4068,13 @@ def test_positive_arbitrary_file_repo_addition( 2. Upload an arbitrary file to it 3. Create a Content View (CV) - :Steps: + :steps: 1. Add the FR to the CV :expectedresults: Check FR is added to CV :CaseAutomation: Automated - :CaseLevel: Integration - :CaseImportance: High :BZ: 1610309, 1908465 @@ -4201,15 +4111,13 @@ def test_positive_arbitrary_file_repo_removal( 3. Create a Content View (CV) 4. Add the FR to the CV - :Steps: + :steps: 1. Remove the FR from the CV :expectedresults: Check FR is removed from CV :CaseAutomation: Automated - :CaseLevel: Integration - :BZ: 1908465 """ cv = module_target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) @@ -4241,14 +4149,13 @@ def test_positive_arbitrary_file_sync_over_capsule(self): 5. Create a Capsule 6. Connect the Capsule with Satellite/Foreman host - :Steps: + :steps: 1. Start synchronization :expectedresults: Check CV with FR is synced over Capsule :CaseAutomation: NotAutomated - :CaseLevel: System """ @pytest.mark.tier3 @@ -4267,7 +4174,7 @@ def test_positive_arbitrary_file_repo_promotion( 4. Add the FR to the CV 5. Create an Environment - :Steps: + :steps: 1. Promote the CV to the Environment :expectedresults: Check arbitrary files from FR is available on @@ -4275,8 +4182,6 @@ def test_positive_arbitrary_file_repo_promotion( :CaseAutomation: Automated - :CaseLevel: Integration - :CaseImportance: High """ diff --git a/tests/foreman/cli/test_contentviewfilter.py b/tests/foreman/cli/test_contentviewfilter.py index dac2b0d0a6a..65ffeb940a5 100644 --- a/tests/foreman/cli/test_contentviewfilter.py +++ b/tests/foreman/cli/test_contentviewfilter.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ContentViews :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random @@ -592,8 +587,6 @@ def test_positive_update_name(self, new_name, module_org, content_view, module_t :expectedresults: Content view filter updated successfully and has proper and expected name - :CaseLevel: Integration - :CaseImportance: Critical """ cvf_name = gen_string('utf8') @@ -630,8 +623,6 @@ def test_positive_update_repo_with_same_type( :expectedresults: Content view filter updated successfully and has new repository affected - :CaseLevel: Integration - :CaseImportance: Critical """ cvf_name = gen_string('utf8') @@ -686,7 +677,6 @@ def test_positive_update_repo_with_different_type( :expectedresults: Content view filter updated successfully and has new repository affected - :CaseLevel: Integration """ cvf_name = gen_string('utf8') module_target_sat.cli.ContentView.filter.create( @@ -739,7 +729,6 @@ def test_positive_update_inclusion(self, module_org, content_view, module_target :expectedresults: Content view filter updated successfully and has correct and expected value for inclusion parameter - :CaseLevel: Integration """ cvf_name = gen_string('utf8') module_target_sat.cli.ContentView.filter.create( diff --git a/tests/foreman/cli/test_discoveredhost.py b/tests/foreman/cli/test_discoveredhost.py index e91db52a804..5fbeca4ef42 100644 --- a/tests/foreman/cli/test_discoveredhost.py +++ b/tests/foreman/cli/test_discoveredhost.py @@ -8,11 +8,6 @@ :Team: Rocket -:TestType: Functional - -:CaseLevel: System - -:Upstream: No """ import pytest from wait_for import wait_for @@ -40,7 +35,7 @@ def test_rhel_pxe_discovery_provisioning( :Setup: Satellite with Provisioning and Discovery features configured - :Steps: + :steps: 1. Boot up the host to discover 2. Provision the host @@ -166,7 +161,7 @@ def test_positive_provision_pxeless_bios_syslinux(): :Setup: 1. Craft the FDI with remaster the image to have ssh enabled - :Steps: + :steps: 1. Create a BIOS VM and set it to boot from the FDI 2. Run assertion steps #1-2 3. Provision the discovered host using PXELinux loader @@ -417,7 +412,7 @@ def test_positive_list_facts(): :Setup: 1. Provisioning is configured and Host is already discovered - :Steps: Validate specified builtin and custom facts + :steps: Validate specified builtin and custom facts :expectedresults: All checked facts should be displayed correctly diff --git a/tests/foreman/cli/test_discoveryrule.py b/tests/foreman/cli/test_discoveryrule.py index 4daea9571e4..2d9059f196b 100644 --- a/tests/foreman/cli/test_discoveryrule.py +++ b/tests/foreman/cli/test_discoveryrule.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: DiscoveryPlugin :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from functools import partial import random @@ -140,7 +135,6 @@ def test_positive_create_with_hostname(self, discoveryrule_factory): :expectedresults: Rule should be successfully created and has expected hostname value - :CaseLevel: Component """ host_name = 'myhost' rule = discoveryrule_factory(options={'hostname': host_name}) @@ -239,7 +233,6 @@ def test_positive_create_with_hosts_limit(self, discoveryrule_factory): :expectedresults: Rule should be successfully created and has expected hosts limit value - :CaseLevel: Component """ hosts_limit = '5' rule = discoveryrule_factory(options={'hosts-limit': hosts_limit}) @@ -276,7 +269,6 @@ def test_positive_create_disabled_rule(self, discoveryrule_factory): :expectedresults: Disabled rule should be successfully created - :CaseLevel: Component """ rule = discoveryrule_factory(options={'enabled': 'false'}) assert rule.enabled == 'false' @@ -292,8 +284,6 @@ def test_negative_create_with_invalid_name(self, name, discoveryrule_factory): :CaseImportance: Medium - :CaseLevel: Component - :parametrized: yes """ with pytest.raises(CLIFactoryError): @@ -310,8 +300,6 @@ def test_negative_create_with_invalid_hostname(self, name, discoveryrule_factory :CaseImportance: Medium - :CaseLevel: Component - :BZ: 1378427 :parametrized: yes @@ -356,8 +344,6 @@ def test_positive_update_discovery_params(self, discoveryrule_factory, class_org :expectedresults: Rule params are updated - :CaseLevel: Component - :CaseImportance: Medium """ rule = discoveryrule_factory(options={'hosts-limit': '5'}) @@ -410,8 +396,6 @@ def test_negative_update_discovery_params(self, name, discoveryrule_factory, tar :expectedresults: Rule params are not updated - :CaseLevel: Component - :CaseImportance: Medium :parametrized: yes @@ -507,7 +491,6 @@ def test_positive_crud_with_non_admin_user( :expectedresults: Rule should be created and deleted successfully. - :CaseLevel: Integration """ rule_name = gen_string('alpha') new_name = gen_string('alpha') @@ -564,7 +547,6 @@ def test_negative_delete_rule_with_non_admin_user( :expectedresults: User should validation error and rule should not be deleted successfully. - :CaseLevel: Integration """ rule = target_sat.cli_factory.make_discoveryrule( { diff --git a/tests/foreman/cli/test_docker.py b/tests/foreman/cli/test_docker.py index e745f44af8c..a4dd51c17c2 100644 --- a/tests/foreman/cli/test_docker.py +++ b/tests/foreman/cli/test_docker.py @@ -4,13 +4,8 @@ :CaseAutomation: Automated -:CaseLevel: Component - -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from random import choice, randint @@ -169,7 +164,6 @@ def test_positive_create_repos_using_same_product( :expectedresults: Multiple docker repositories are created with a Docker upstream repository and they all belong to the same product. - :CaseLevel: Integration """ repo_names = set() for _ in range(randint(2, 5)): @@ -191,7 +185,6 @@ def test_positive_create_repos_using_multiple_products(self, module_org, module_ Docker upstream repository and they all belong to their respective products. - :CaseLevel: Integration """ for _ in range(randint(2, 5)): product = module_target_sat.cli_factory.make_product_wait( @@ -404,8 +397,6 @@ class TestDockerContentView: :CaseComponent: ContentViews :team: Phoenix-content - - :CaseLevel: Integration """ @pytest.mark.tier2 @@ -1145,8 +1136,6 @@ class TestDockerActivationKey: :CaseComponent: ActivationKeys :team: Phoenix-subscriptions - - :CaseLevel: Integration """ @pytest.mark.tier2 diff --git a/tests/foreman/cli/test_domain.py b/tests/foreman/cli/test_domain.py index ad48827e8f1..8b1c157c657 100644 --- a/tests/foreman/cli/test_domain.py +++ b/tests/foreman/cli/test_domain.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -196,8 +191,6 @@ def test_negative_create_with_invalid_dns_id(module_target_sat): :BZ: 1398392 - :CaseLevel: Integration - :CaseImportance: Medium """ with pytest.raises(CLIFactoryError) as context: diff --git a/tests/foreman/cli/test_environment.py b/tests/foreman/cli/test_environment.py index 10481c743c6..90e5b79e72a 100644 --- a/tests/foreman/cli/test_environment.py +++ b/tests/foreman/cli/test_environment.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Puppet :Team: Rocket -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ from random import choice @@ -49,8 +44,6 @@ def test_negative_list_with_parameters( :expectedresults: Server returns empty result as there is no environment associated with location - :CaseLevel: Integration - :BZ: 1337947 """ session_puppet_enabled_sat.cli.Environment.create( diff --git a/tests/foreman/cli/test_errata.py b/tests/foreman/cli/test_errata.py index 5fab1951910..6cdb4a13cdf 100644 --- a/tests/foreman/cli/test_errata.py +++ b/tests/foreman/cli/test_errata.py @@ -4,17 +4,11 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ErrataManagement :team: Phoenix-content -:TestType: Functional - :CaseImportance: High - -:Upstream: No """ from datetime import date, datetime, timedelta from operator import itemgetter @@ -264,7 +258,6 @@ def start_and_wait_errata_recalculate(sat, host): :param sat: Satellite instance to check for task(s) :param host: ContentHost instance to schedule errata recalculate - """ # Find any in-progress task for this host search = "label = Actions::Katello::Applicability::Hosts::BulkGenerate and result = pending" @@ -454,8 +447,6 @@ def test_positive_install_by_host_collection_and_org( :expectedresults: Erratum is installed. - :CaseLevel: System - :BZ: 1457977, 1983043 """ errata_id = REPO_WITH_ERRATA['errata'][0]['id'] @@ -505,8 +496,6 @@ def test_negative_install_by_hc_id_without_errata_info( :expectedresults: Error message thrown. :CaseImportance: Low - - :CaseLevel: System """ with pytest.raises(CLIReturnCodeError, match="Error: Option '--errata' is required"): target_sat.cli.HostCollection.erratum_install( @@ -534,8 +523,6 @@ def test_negative_install_by_hc_name_without_errata_info( :expectedresults: Error message thrown. :CaseImportance: Low - - :CaseLevel: System """ with pytest.raises(CLIReturnCodeError, match="Error: Option '--errata' is required"): target_sat.cli.HostCollection.erratum_install( @@ -566,8 +553,6 @@ def test_negative_install_without_hc_info( :BZ: 1928281 :CaseImportance: Low - - :CaseLevel: System """ module_target_sat.cli_factory.make_host_collection( {'organization-id': module_entitlement_manifest_org.id} @@ -597,8 +582,6 @@ def test_negative_install_by_hc_id_without_org_info( :expectedresults: Error message thrown. :CaseImportance: Low - - :CaseLevel: System """ with pytest.raises(CLIReturnCodeError, match='Error: Could not find organization'): module_target_sat.cli.HostCollection.erratum_install( @@ -623,7 +606,6 @@ def test_negative_install_by_hc_name_without_org_info( :CaseImportance: Low - :CaseLevel: System """ with pytest.raises(CLIReturnCodeError, match='Error: Could not find organization'): module_target_sat.cli.HostCollection.erratum_install( @@ -1142,8 +1124,6 @@ def test_negative_list_filter_by_product_name(products_with_repos, module_target :expectedresults: Error must be returned. :CaseImportance: Low - - :CaseLevel: System """ with pytest.raises(CLIReturnCodeError): module_target_sat.cli.Erratum.list( @@ -1198,7 +1178,6 @@ def test_positive_list_filter_by_cve(module_sca_manifest_org, rh_repo, target_sa :Steps: erratum list --cve :expectedresults: Errata is filtered by CVE. - """ target_sat.cli.RepositorySet.enable( { diff --git a/tests/foreman/cli/test_fact.py b/tests/foreman/cli/test_fact.py index 1b1dd658250..ddaaa477944 100644 --- a/tests/foreman/cli/test_fact.py +++ b/tests/foreman/cli/test_fact.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Fact :Team: Rocket -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ from fauxfactory import gen_string import pytest diff --git a/tests/foreman/cli/test_filter.py b/tests/foreman/cli/test_filter.py index 434b91e3980..9c12b239089 100644 --- a/tests/foreman/cli/test_filter.py +++ b/tests/foreman/cli/test_filter.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: UsersRoles :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/foreman/cli/test_foremantask.py b/tests/foreman/cli/test_foremantask.py index 7dcd90ac669..bdce6e63a97 100644 --- a/tests/foreman/cli/test_foremantask.py +++ b/tests/foreman/cli/test_foremantask.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: TasksPlugin :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -48,5 +43,4 @@ def test_positive_tasks_backup(): :CaseImportance: High :CaseAutomation: NotAutomated - """ diff --git a/tests/foreman/cli/test_globalparam.py b/tests/foreman/cli/test_globalparam.py index 2a5d49cf966..41bb8ca769b 100644 --- a/tests/foreman/cli/test_globalparam.py +++ b/tests/foreman/cli/test_globalparam.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Parameters :Team: Rocket -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ from functools import partial diff --git a/tests/foreman/cli/test_hammer.py b/tests/foreman/cli/test_hammer.py index 8a424bb62ee..c795352c5ed 100644 --- a/tests/foreman/cli/test_hammer.py +++ b/tests/foreman/cli/test_hammer.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Hammer :Team: Endeavour -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import io import json diff --git a/tests/foreman/cli/test_host.py b/tests/foreman/cli/test_host.py index 139df1026b6..0390f79bd3a 100644 --- a/tests/foreman/cli/test_host.py +++ b/tests/foreman/cli/test_host.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from random import choice import re @@ -576,8 +571,6 @@ def test_positive_katello_and_openscap_loaded(target_sat): and foreman_openscap are available in help message (note: help is generated dynamically based on apipie cache) - :CaseLevel: System - :customerscenario: true :CaseImportance: Medium @@ -604,8 +597,6 @@ def test_positive_list_and_unregister( Unlike content host, host has not disappeared from list of hosts after unregistering. :parametrized: yes - - :CaseLevel: System """ rhel7_contenthost.register(module_org, None, module_ak_with_cv.name, target_sat) assert rhel7_contenthost.subscribed @@ -633,8 +624,6 @@ def test_positive_list_by_last_checkin( :BZ: 1285992 :parametrized: yes - - :CaseLevel: System """ rhel7_contenthost.install_katello_ca(target_sat) rhel7_contenthost.register_contenthost( @@ -661,8 +650,6 @@ def test_positive_list_infrastructure_hosts( :expectedresults: Infrastructure hosts are listed :parametrized: yes - - :CaseLevel: System """ rhel7_contenthost.install_katello_ca(target_sat) rhel7_contenthost.register_contenthost( @@ -698,8 +685,6 @@ def test_positive_create_inherit_lce_cv( :expectedresults: Host's lifecycle environment and content view match the ones specified in hostgroup - :CaseLevel: Integration - :BZ: 1391656 """ hostgroup = target_sat.api.HostGroup( @@ -727,8 +712,6 @@ def test_positive_create_inherit_nested_hostgroup(target_sat): :expectedresults: Host created successfully using host group title - :CaseLevel: System - :customerscenario: true :BZ: 1436162 @@ -789,8 +772,6 @@ def test_positive_list_with_nested_hostgroup(target_sat): nested host groups names in its hostgroup parameter :BZ: 1427554, 1955421 - - :CaseLevel: System """ options = target_sat.api.Host() options.create_missing() @@ -866,9 +847,7 @@ def test_negative_create_with_incompatible_pxe_loader(): 2. Files not deployed on TFTP 3. Host not created - :CaseAutomation: NotAutomated - - :CaseLevel: System + :CaseAutomation: NotAutomated """ @@ -983,8 +962,6 @@ def test_negative_update_arch(function_host, module_architecture, target_sat): :id: a86524da-8caf-472b-9a3d-17a4385c3a18 :expectedresults: A host is not updated - - :CaseLevel: Integration """ with pytest.raises(CLIReturnCodeError): target_sat.cli.Host.update( @@ -1003,8 +980,6 @@ def test_negative_update_os(target_sat, function_host, module_architecture): :id: ff13d2af-e54a-4daf-a24d-7ec930b4fbbe :expectedresults: A host is not updated - - :CaseLevel: Integration """ p_table = function_host['operating-system']['partition-table'] p_table = target_sat.api.PartitionTable().search(query={'search': f'name="{p_table}"'})[0] @@ -1034,7 +1009,7 @@ def test_hammer_host_info_output(target_sat, module_user): :id: 03468516-0ebb-11eb-8ad8-0c7a158cbff4 - :Steps: + :steps: 1. Update the host with any owner 2. Get host info by running `hammer host info` 3. Create new user and update his location and organization based on the hosts @@ -1282,8 +1257,6 @@ def test_positive_set_multi_line_and_with_spaces_parameter_value(function_host, from yaml format :BZ: 1315282 - - :CaseLevel: Integration """ param_name = gen_string('alpha').lower() # long string that should be escaped and affected by line break with @@ -1351,9 +1324,7 @@ def test_positive_provision_baremetal_with_bios_syslinux(): 6. GRUB config changes the boot order (boot local first) 7. Hosts boots straight to RHEL after reboot (step #4) - :CaseAutomation: NotAutomated - - :CaseLevel: System + :CaseAutomation: NotAutomated """ @@ -1389,9 +1360,7 @@ def test_positive_provision_baremetal_with_uefi_syslinux(): 6. GRUB config changes the boot order (boot local first) 7. Hosts boots straight to RHEL after reboot (step #4) - :CaseAutomation: NotAutomated - - :CaseLevel: System + :CaseAutomation: NotAutomated """ @@ -1430,9 +1399,7 @@ def test_positive_provision_baremetal_with_uefi_grub(): 7. Hosts boots straight to RHEL after reboot (step #4) - :CaseAutomation: NotAutomated - - :CaseLevel: System + :CaseAutomation: NotAutomated """ @@ -1473,9 +1440,7 @@ def test_positive_provision_baremetal_with_uefi_grub2(): 7. Hosts boots straight to RHEL after reboot (step #4) - :CaseAutomation: NotAutomated - - :CaseLevel: System + :CaseAutomation: NotAutomated """ @@ -1508,9 +1473,7 @@ def test_positive_provision_baremetal_with_uefi_secureboot(): :expectedresults: Host is provisioned - :CaseAutomation: NotAutomated - - :CaseLevel: System + :CaseAutomation: NotAutomated """ @@ -1614,8 +1577,6 @@ def test_positive_report_package_installed_removed( :BZ: 1463809 :parametrized: yes - - :CaseLevel: System """ client = katello_host_tools_host host_info = target_sat.cli.Host.info({'name': client.hostname}) @@ -1660,8 +1621,6 @@ def test_positive_package_applicability(katello_host_tools_host, setup_custom_re :BZ: 1463809 :parametrized: yes - - :CaseLevel: System """ client = katello_host_tools_host host_info = target_sat.cli.Host.info({'name': client.hostname}) @@ -1724,8 +1683,6 @@ def test_positive_erratum_applicability( :BZ: 1463809,1740790 :parametrized: yes - - :CaseLevel: System """ client = katello_host_tools_host host_info = target_sat.cli.Host.info({'name': client.hostname}) @@ -1779,8 +1736,6 @@ def test_positive_apply_security_erratum(katello_host_tools_host, setup_custom_r :expectedresults: erratum is recognized by the `yum update --security` command on client - :CaseLevel: System - :customerscenario: true :BZ: 1420671 @@ -1819,8 +1774,6 @@ def test_positive_install_package_via_rex( :expectedresults: Package was installed - :CaseLevel: System - :parametrized: yes """ client = katello_host_tools_host @@ -1883,8 +1836,6 @@ def test_positive_register( :expectedresults: host successfully registered :parametrized: yes - - :CaseLevel: System """ hosts = target_sat.cli.Host.list( { @@ -1947,8 +1898,6 @@ def test_positive_attach( enabled, and repository package installed :parametrized: yes - - :CaseLevel: System """ # create an activation key without subscriptions # register the client host @@ -2004,8 +1953,6 @@ def test_positive_attach_with_lce( repository enabled, and repository package installed :parametrized: yes - - :CaseLevel: System """ host_subscription_client.register_contenthost( module_org.name, @@ -2043,8 +1990,6 @@ def test_negative_without_attach( :expectedresults: repository list is empty :parametrized: yes - - :CaseLevel: System """ target_sat.cli.Host.subscription_register( { @@ -2082,8 +2027,6 @@ def test_negative_without_attach_with_lce( :expectedresults: repository not enabled on host :parametrized: yes - - :CaseLevel: System """ content_view = target_sat.api.ContentView(organization=function_org).create() ak = target_sat.api.ActivationKey( @@ -2146,8 +2089,6 @@ def test_positive_remove( :expectedresults: subscription successfully removed from host :parametrized: yes - - :CaseLevel: System """ target_sat.cli.Host.subscription_register( { @@ -2221,8 +2162,6 @@ def test_positive_auto_attach( repository enabled, and repository package installed :parametrized: yes - - :CaseLevel: System """ target_sat.cli.Host.subscription_register( { @@ -2257,8 +2196,6 @@ def test_positive_unregister_host_subscription( :expectedresults: host subscription is unregistered :parametrized: yes - - :CaseLevel: System """ # register the host client host_subscription_client.register_contenthost( @@ -2315,8 +2252,6 @@ def test_syspurpose_end_to_end( :CaseImportance: Critical :parametrized: yes - - :CaseLevel: System """ # Create an activation key with test values purpose_addons = "test-addon1, test-addon2" @@ -2576,8 +2511,6 @@ def test_positive_list_scparams( :expectedresults: Overridden sc-param from puppet class are listed - - :CaseLevel: Integration """ update_smart_proxy(session_puppet_enabled_sat, module_puppet_loc, session_puppet_enabled_proxy) # Create hostgroup with associated puppet class diff --git a/tests/foreman/cli/test_hostcollection.py b/tests/foreman/cli/test_hostcollection.py index 91bf5c4f563..ce3426030b5 100644 --- a/tests/foreman/cli/test_hostcollection.py +++ b/tests/foreman/cli/test_hostcollection.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: HostCollections :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from broker import Broker from fauxfactory import gen_string @@ -224,8 +219,6 @@ def test_positive_list_by_org_id(module_org, module_target_sat): :id: afbe077a-0de1-432c-a0c4-082129aab92e :expectedresults: Only host-collection within specific org is listed - - :CaseLevel: Integration """ # Create two host collections within different organizations module_target_sat.cli_factory.make_host_collection({'organization-id': module_org.id}) @@ -253,8 +246,6 @@ def test_positive_host_collection_host_pagination(module_org, module_target_sat) :expectedresults: Number of host per page follows per_page configuration restriction - - :CaseLevel: Integration """ host_collection = module_target_sat.cli_factory.make_host_collection( {'organization-id': module_org.id} @@ -287,8 +278,6 @@ def test_positive_copy_by_id(module_org, module_target_sat): :expectedresults: Host collection is cloned successfully :BZ: 1328925 - - :CaseLevel: Integration """ host_collection = module_target_sat.cli_factory.make_host_collection( {'name': gen_string('alpha', 15), 'organization-id': module_org.id} @@ -311,8 +300,6 @@ def test_positive_register_host_ak_with_host_collection(module_org, module_ak_wi :expectedresults: Host successfully registered and listed in host collection :BZ: 1385814 - - :CaseLevel: System """ host_info = _make_fake_host_helper(module_org, target_sat) diff --git a/tests/foreman/cli/test_hostgroup.py b/tests/foreman/cli/test_hostgroup.py index 3f5ebbf0ed0..febe94038f8 100644 --- a/tests/foreman/cli/test_hostgroup.py +++ b/tests/foreman/cli/test_hostgroup.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: HostGroup :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_integer from nailgun import entities @@ -116,8 +111,6 @@ def test_positive_create_with_multiple_entities_and_delete( :BZ: 1395254, 1313056 - :CaseLevel: Integration - :CaseImportance: Critical """ with session_puppet_enabled_sat: @@ -215,8 +208,6 @@ def test_negative_create_with_content_source(module_org, module_target_sat): :BZ: 1260697 :expectedresults: Hostgroup was not created - - :CaseLevel: Integration """ with pytest.raises(CLIFactoryError): module_target_sat.cli_factory.hostgroup( @@ -248,8 +239,6 @@ def test_positive_update_hostgroup_with_puppet( :expectedresults: Hostgroup was successfully updated with new content source, name and puppet classes - - :CaseLevel: Integration """ with session_puppet_enabled_sat as puppet_sat: hostgroup = puppet_sat.cli_factory.hostgroup( @@ -303,8 +292,6 @@ def test_positive_update_hostgroup( :expectedresults: Hostgroup was successfully updated with new content source and name - - :CaseLevel: Integration """ hostgroup = module_target_sat.cli_factory.hostgroup( { @@ -338,8 +325,6 @@ def test_negative_update_content_source(hostgroup, content_source, module_target :expectedresults: Host group was not updated. Content source remains the same as it was before update - - :CaseLevel: Integration """ with pytest.raises(CLIReturnCodeError): module_target_sat.cli.HostGroup.update( @@ -371,8 +356,6 @@ def test_negative_delete_by_id(module_target_sat): :id: 047c9f1a-4dd6-4fdc-b7ed-37cc725c68d3 :expectedresults: HostGroup is not deleted - - :CaseLevel: Integration """ entity_id = invalid_id_list()[0] with pytest.raises(CLIReturnCodeError): @@ -391,7 +374,6 @@ def test_positive_created_nested_hostgroup(module_org, module_target_sat): :customerscenario: true :CaseImportance: Low - """ parent_hg = module_target_sat.cli_factory.hostgroup({'organization-ids': module_org.id}) nested = module_target_sat.cli_factory.hostgroup( @@ -413,7 +395,7 @@ def test_positive_nested_hostgroup_info(): :customerscenario: true - :Steps: + :steps: 1. Create parent hostgroup and nested hostgroup, with puppet environment, classes, and parameters on each. diff --git a/tests/foreman/cli/test_http_proxy.py b/tests/foreman/cli/test_http_proxy.py index 8fb4d6af0a4..c6bea60857d 100644 --- a/tests/foreman/cli/test_http_proxy.py +++ b/tests/foreman/cli/test_http_proxy.py @@ -2,19 +2,14 @@ :Requirement: HttpProxy -:CaseLevel: Acceptance - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: High :CaseAutomation: Automated -:Upstream: No """ from fauxfactory import gen_integer, gen_string, gen_url import pytest @@ -99,7 +94,7 @@ def test_insights_client_registration_with_http_proxy(): :customerscenario: true - :Steps: + :steps: 1. Create HTTP Proxy. 2. Set created proxy as "Default HTTP Proxy" in settings. 3. Edit /etc/resolv.conf and comment out all entries so that @@ -130,7 +125,7 @@ def test_positive_set_content_default_http_proxy(block_fake_repo_access, target_ :id: c12868eb-98f1-4763-a168-281ac44d9ff5 - :Steps: + :steps: 1. Create a product with repo. 2. Create an un-authenticated proxy. 3. Set the proxy to be the global default proxy. @@ -139,7 +134,6 @@ def test_positive_set_content_default_http_proxy(block_fake_repo_access, target_ :expectedresults: Repo is synced :CaseImportance: High - """ org = target_sat.api.Organization().create() proxy_name = gen_string('alpha', 15) @@ -184,7 +178,7 @@ def test_positive_environment_variable_unset_set(): :customerscenario: true - :Steps: + :steps: 1. Export any environment variable from [http_proxy, https_proxy, ssl_cert_file, HTTP_PROXY, HTTPS_PROXY, SSL_CERT_FILE] 2. satellite-installer @@ -195,7 +189,6 @@ def test_positive_environment_variable_unset_set(): :CaseImportance: High :CaseAutomation: NotAutomated - """ diff --git a/tests/foreman/cli/test_installer.py b/tests/foreman/cli/test_installer.py index 7980f40a385..4a42eaa1315 100644 --- a/tests/foreman/cli/test_installer.py +++ b/tests/foreman/cli/test_installer.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Installer :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/foreman/cli/test_jobtemplate.py b/tests/foreman/cli/test_jobtemplate.py index 7d52d3486b8..66553d2c54d 100644 --- a/tests/foreman/cli/test_jobtemplate.py +++ b/tests/foreman/cli/test_jobtemplate.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: RemoteExecution :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -158,7 +153,6 @@ def test_positive_view_dump(module_org, module_target_sat): :id: 25fcfcaa-fc4c-425e-919e-330e36195c4a :expectedresults: Verify no errors are thrown - """ template_name = gen_string('alpha', 7) module_target_sat.cli_factory.job_template( diff --git a/tests/foreman/cli/test_ldapauthsource.py b/tests/foreman/cli/test_ldapauthsource.py index 8f363f5606c..528a47420a5 100644 --- a/tests/foreman/cli/test_ldapauthsource.py +++ b/tests/foreman/cli/test_ldapauthsource.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: LDAP :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string from nailgun import entities diff --git a/tests/foreman/cli/test_leapp_client.py b/tests/foreman/cli/test_leapp_client.py index 550a1b662a4..6ab248b0f92 100644 --- a/tests/foreman/cli/test_leapp_client.py +++ b/tests/foreman/cli/test_leapp_client.py @@ -2,19 +2,14 @@ :Requirement: leapp -:CaseLevel: Integration - :CaseComponent: Leappintegration :Team: Rocket -:TestType: Functional - :CaseImportance: High :CaseAutomation: Automated -:Upstream: No """ from broker import Broker import pytest @@ -228,7 +223,7 @@ def test_leapp_upgrade_rhel( :id: 8eccc689-3bea-4182-84f3-c121e95d54c3 - :Steps: + :steps: 1. Import a subscription manifest and enable, sync source & target repositories 2. Create LCE, Create CV, add repositories to it, publish and promote CV, Create AK, etc. 3. Register content host with AK diff --git a/tests/foreman/cli/test_lifecycleenvironment.py b/tests/foreman/cli/test_lifecycleenvironment.py index 93001a70fe8..75bf12c681b 100644 --- a/tests/foreman/cli/test_lifecycleenvironment.py +++ b/tests/foreman/cli/test_lifecycleenvironment.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: LifecycleEnvironments :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from math import ceil diff --git a/tests/foreman/cli/test_location.py b/tests/foreman/cli/test_location.py index 4779bf9373d..9f835c1c581 100644 --- a/tests/foreman/cli/test_location.py +++ b/tests/foreman/cli/test_location.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: OrganizationsandLocations :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -282,7 +277,6 @@ def test_positive_add_and_remove_capsule(self, request, target_sat): :BZ: 1398695 - :CaseLevel: Integration """ location = _location(request, target_sat) proxy = _proxy(request, target_sat) diff --git a/tests/foreman/cli/test_logging.py b/tests/foreman/cli/test_logging.py index 61ead1bee0e..8d315b3e59c 100644 --- a/tests/foreman/cli/test_logging.py +++ b/tests/foreman/cli/test_logging.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Logging :Team: Rocket -:TestType: Functional - :CaseImportance: Medium -:Upstream: No """ import re @@ -223,8 +218,6 @@ def test_positive_logging_from_pulp3(module_org, target_sat): :id: 8d5718e6-3442-47d6-b541-0aa78d007e8b - :CaseLevel: Component - :CaseImportance: High """ source_log = '/var/log/foreman/production.log' diff --git a/tests/foreman/cli/test_medium.py b/tests/foreman/cli/test_medium.py index 5ded3225e84..26017b415ab 100644 --- a/tests/foreman/cli/test_medium.py +++ b/tests/foreman/cli/test_medium.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_alphanumeric import pytest @@ -93,7 +88,6 @@ def test_positive_remove_os(self, module_target_sat): :expectedresults: Operating system removed - :CaseLevel: Integration """ medium = module_target_sat.cli_factory.make_medium() os = module_target_sat.cli_factory.make_os() diff --git a/tests/foreman/cli/test_model.py b/tests/foreman/cli/test_model.py index a627a3158b7..8fc51be9f62 100644 --- a/tests/foreman/cli/test_model.py +++ b/tests/foreman/cli/test_model.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest diff --git a/tests/foreman/cli/test_operatingsystem.py b/tests/foreman/cli/test_operatingsystem.py index 7b5e0bd60a5..48ed8af8099 100644 --- a/tests/foreman/cli/test_operatingsystem.py +++ b/tests/foreman/cli/test_operatingsystem.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Provisioning :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_alphanumeric, gen_string import pytest @@ -223,7 +218,6 @@ def test_positive_add_arch(self, target_sat): :expectedresults: Architecture is added to Operating System - :CaseLevel: Integration """ architecture = target_sat.cli_factory.make_architecture() os = target_sat.cli_factory.make_os() @@ -243,7 +237,6 @@ def test_positive_add_template(self, target_sat): :expectedresults: Provisioning template is added to Operating System - :CaseLevel: Integration """ template = target_sat.cli_factory.make_template() os = target_sat.cli_factory.make_os() @@ -265,7 +258,6 @@ def test_positive_add_ptable(self, target_sat): :expectedresults: Partition table is added to Operating System - :CaseLevel: Integration """ # Create a partition table. ptable_name = target_sat.cli_factory.make_partition_table()['name'] diff --git a/tests/foreman/cli/test_organization.py b/tests/foreman/cli/test_organization.py index 9b2b3893c8d..c7d84466390 100644 --- a/tests/foreman/cli/test_organization.py +++ b/tests/foreman/cli/test_organization.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: OrganizationsandLocations :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -172,8 +167,6 @@ def test_positive_add_and_remove_subnets(module_org, module_target_sat): :expectedresults: Subnets are handled as expected :BZ: 1395229 - - :CaseLevel: Integration """ subnets = [module_target_sat.cli_factory.make_subnet() for _ in range(0, 2)] module_target_sat.cli.Org.add_subnet({'name': module_org.name, 'subnet': subnets[0]['name']}) @@ -203,8 +196,6 @@ def test_positive_add_and_remove_users(module_org, module_target_sat): 4. create and delete admin user by id :BZ: 1395229 - - :CaseLevel: Integration """ user = module_target_sat.cli_factory.user() admin_user = module_target_sat.cli_factory.user({'admin': '1'}) @@ -250,8 +241,6 @@ def test_positive_add_and_remove_hostgroups(module_org, module_target_sat): :steps: 1. add and remove hostgroup by name 2. add and remove hostgroup by id - - :CaseLevel: Integration """ hostgroups = [module_target_sat.cli_factory.hostgroup() for _ in range(0, 2)] @@ -291,8 +280,6 @@ def test_positive_add_and_remove_compute_resources(module_org, module_target_sat :steps: 1. Add and remove compute resource by id 2. Add and remove compute resource by name - - :CaseLevel: Integration """ compute_resources = [ module_target_sat.cli_factory.compute_resource( @@ -339,8 +326,6 @@ def test_positive_add_and_remove_media(module_org, module_target_sat): :steps: 1. add and remove medium by id 2. add and remove medium by name - - :CaseLevel: Integration """ media = [module_target_sat.cli_factory.make_medium() for _ in range(0, 2)] module_target_sat.cli.Org.add_medium({'id': module_org.id, 'medium-id': media[0]['id']}) @@ -370,8 +355,6 @@ def test_positive_add_and_remove_templates(module_org, module_target_sat): :steps: 1. Add and remove template by id 2. Add and remove template by name - - :CaseLevel: Integration """ # create and remove templates by name name = list(valid_data_list().values())[0] @@ -428,8 +411,6 @@ def test_positive_add_and_remove_domains(module_org, module_target_sat): :steps: 1. Add and remove domain by name 2. Add and remove domain by id - - :CaseLevel: Integration """ domains = [module_target_sat.cli_factory.make_domain() for _ in range(0, 2)] module_target_sat.cli.Org.add_domain({'domain-id': domains[0]['id'], 'name': module_org.name}) @@ -456,8 +437,6 @@ def test_positive_add_and_remove_lce(module_org, module_target_sat): :steps: 1. create and add lce to org 2. remove lce from org - - :CaseLevel: Integration """ # Create a lifecycle environment. lc_env_name = module_target_sat.cli_factory.make_lifecycle_environment( @@ -488,8 +467,6 @@ def test_positive_add_and_remove_capsules(proxy, module_org, module_target_sat): :steps: 1. add and remove capsule by ip 2. add and remove capsule by name - - :CaseLevel: Integration """ module_target_sat.cli.Org.add_smart_proxy({'id': module_org.id, 'smart-proxy-id': proxy['id']}) org_info = module_target_sat.cli.Org.info({'name': module_org.name}) @@ -525,8 +502,6 @@ def test_positive_add_and_remove_locations(module_org, module_target_sat): :steps: 1. add and remove locations by name 2. add and remove locations by id - - :CaseLevel: Integration """ locations = [module_target_sat.cli_factory.make_location() for _ in range(0, 2)] module_target_sat.cli.Org.add_location( @@ -600,7 +575,6 @@ def test_negative_create_with_invalid_name(name, module_target_sat): :parametrized: yes :expectedresults: organization is not created - """ with pytest.raises(CLIFactoryError): module_target_sat.cli_factory.make_org( @@ -667,7 +641,6 @@ def test_negative_update_name(new_name, module_org, module_target_sat): :parametrized: yes :expectedresults: organization name is not updated - """ with pytest.raises(CLIReturnCodeError): module_target_sat.cli.Org.update({'id': module_org.id, 'new-name': new_name}) @@ -683,8 +656,6 @@ def test_positive_create_user_with_timezone(module_org, module_target_sat): :BZ: 1733269 - :CaseLevel: Integration - :CaseImportance: Medium :steps: @@ -693,7 +664,6 @@ def test_positive_create_user_with_timezone(module_org, module_target_sat): 3. Remove user from organization and validate :expectedresults: User created and removed successfully with valid timezone - """ users_timezones = [ 'Pacific Time (US & Canada)', diff --git a/tests/foreman/cli/test_oscap.py b/tests/foreman/cli/test_oscap.py index f2430235a57..ecbc50f06de 100644 --- a/tests/foreman/cli/test_oscap.py +++ b/tests/foreman/cli/test_oscap.py @@ -2,19 +2,14 @@ :Requirement: Oscap -:CaseLevel: Acceptance - :CaseComponent: SCAPPlugin :Team: Endeavour -:TestType: Functional - :CaseImportance: High :CaseAutomation: Automated -:Upstream: No """ from fauxfactory import gen_string from nailgun import entities diff --git a/tests/foreman/cli/test_oscap_tailoringfiles.py b/tests/foreman/cli/test_oscap_tailoringfiles.py index 3f36bff3288..e95cfb2891a 100644 --- a/tests/foreman/cli/test_oscap_tailoringfiles.py +++ b/tests/foreman/cli/test_oscap_tailoringfiles.py @@ -2,19 +2,14 @@ :Requirement: tailoringfiles -:CaseLevel: Acceptance - :CaseComponent: SCAPPlugin :Team: Endeavour -:TestType: Functional - :CaseImportance: High :CaseAutomation: Automated -:Upstream: No """ from fauxfactory import gen_string import pytest diff --git a/tests/foreman/cli/test_ostreebranch.py b/tests/foreman/cli/test_ostreebranch.py index ccb35ea0bae..d768b3f2d70 100644 --- a/tests/foreman/cli/test_ostreebranch.py +++ b/tests/foreman/cli/test_ostreebranch.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random @@ -94,7 +89,6 @@ def test_positive_list_by_repo_id( :id: 8cf1a973-031c-4c02-af14-0faba22ab60b :expectedresults: Ostree Branch List is displayed - """ branch = module_target_sat.cli.OstreeBranch.with_user(*ostree_user_credentials) @@ -139,7 +133,6 @@ def test_positive_list_by_cv_id(ostree_repo_with_user, ostree_user_credentials, :id: 3654f107-44ee-4af2-a9e4-f9fd8c68491e :expectedresults: Ostree Branch List is displayed - """ result = module_target_sat.cli.OstreeBranch.with_user(*ostree_user_credentials).list( {'content-view-id': ostree_repo_with_user['cv']['id']} diff --git a/tests/foreman/cli/test_partitiontable.py b/tests/foreman/cli/test_partitiontable.py index d32bd11bf03..35e52cbbe65 100644 --- a/tests/foreman/cli/test_partitiontable.py +++ b/tests/foreman/cli/test_partitiontable.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from random import randint @@ -135,7 +130,6 @@ def test_positive_add_remove_os_by_id(self, module_target_sat): :expectedresults: Operating system is added to partition table - :CaseLevel: Integration """ ptable = module_target_sat.cli_factory.make_partition_table() os = module_target_sat.cli_factory.make_os() @@ -160,7 +154,6 @@ def test_positive_add_remove_os_by_name(self, module_target_sat): :expectedresults: Operating system is added to partition table - :CaseLevel: Integration """ ptable = module_target_sat.cli_factory.make_partition_table() os = module_target_sat.cli_factory.make_os() diff --git a/tests/foreman/cli/test_ping.py b/tests/foreman/cli/test_ping.py index 68f6024372b..0a288143233 100644 --- a/tests/foreman/cli/test_ping.py +++ b/tests/foreman/cli/test_ping.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Hammer :Team: Endeavour -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import pytest diff --git a/tests/foreman/cli/test_product.py b/tests/foreman/cli/test_product.py index 5a61c2dfb31..3d413209db3 100644 --- a/tests/foreman/cli/test_product.py +++ b/tests/foreman/cli/test_product.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_alphanumeric, gen_integer, gen_string, gen_url import pytest @@ -172,7 +167,6 @@ def test_product_list_with_default_settings(module_org, target_sat): :customerscenario: true :expectedresults: product/reporsitory list should work as expected. - """ org_id = str(module_org.id) default_product_name = gen_string('alpha') @@ -227,7 +221,7 @@ def test_positive_product_sync_state(module_org, module_target_sat): :customerscenario: true - :Steps: + :steps: 1. Sync a custom repository that fails. 2. Run `hammer product info --product-id `. 3. Successfully sync another repository under the same product. diff --git a/tests/foreman/cli/test_provisioning.py b/tests/foreman/cli/test_provisioning.py index 8e67bdcbb48..1c7799edc53 100644 --- a/tests/foreman/cli/test_provisioning.py +++ b/tests/foreman/cli/test_provisioning.py @@ -4,17 +4,12 @@ :CaseAutomation: NotAutomated -:CaseLevel: System - :CaseComponent: Provisioning :Team: Rocket -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import pytest diff --git a/tests/foreman/cli/test_provisioningtemplate.py b/tests/foreman/cli/test_provisioningtemplate.py index 663aa64a1e2..59aabc2d2c4 100644 --- a/tests/foreman/cli/test_provisioningtemplate.py +++ b/tests/foreman/cli/test_provisioningtemplate.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ProvisioningTemplates :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random from random import randint @@ -167,8 +162,6 @@ def test_positive_add_remove_os_by_id(module_target_sat, module_os_with_minor): :id: d9f481b3-9757-4208-b451-baf4792d4d70 :expectedresults: Operating system is added/removed from the template - - :CaseLevel: Integration """ os = module_os_with_minor os_string = f'{os.name} {os.major}.{os.minor}' @@ -212,8 +205,6 @@ def test_positive_clone(module_target_sat): :id: 27d69c1e-0d83-4b99-8a3c-4f1bdec3d261 :expectedresults: The template is cloned successfully - - :CaseLevel: Integration """ cloned_template_name = gen_string('alpha') template = module_target_sat.cli_factory.make_template() diff --git a/tests/foreman/cli/test_puppetclass.py b/tests/foreman/cli/test_puppetclass.py index 9d849ff0ff1..3a8b50856bb 100644 --- a/tests/foreman/cli/test_puppetclass.py +++ b/tests/foreman/cli/test_puppetclass.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Puppet :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/foreman/cli/test_realm.py b/tests/foreman/cli/test_realm.py index e488e150cc6..c05dc1291f8 100644 --- a/tests/foreman/cli/test_realm.py +++ b/tests/foreman/cli/test_realm.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Authentication :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random diff --git a/tests/foreman/cli/test_registration.py b/tests/foreman/cli/test_registration.py index 633fbf3be9f..f4260cb1181 100644 --- a/tests/foreman/cli/test_registration.py +++ b/tests/foreman/cli/test_registration.py @@ -2,8 +2,6 @@ :Requirement: Registration -:CaseLevel: Acceptance - :CaseComponent: Registration :CaseAutomation: Automated @@ -12,9 +10,6 @@ :Team: Rocket -:TestType: Functional - -:Upstream: No """ import pytest @@ -161,8 +156,6 @@ def test_negative_register_twice(module_ak_with_cv, module_org, rhel_contenthost :expectedresults: host cannot be registered twice :parametrized: yes - - :CaseLevel: System """ rhel_contenthost.register(module_org, None, module_ak_with_cv.name, target_sat) assert rhel_contenthost.subscribed diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index 29810a3df19..b775c42768c 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: RemoteExecution :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from calendar import monthrange from datetime import datetime, timedelta @@ -491,7 +486,7 @@ def test_positive_run_effective_user_job(self, rex_contenthost, target_sat): :id: a5fa20d8-c2bd-4bbf-a6dc-bf307b59dd8c - :Steps: + :steps: 0. Create a VM and register to SAT and prepare for REX (ssh key) @@ -505,8 +500,6 @@ def test_positive_run_effective_user_job(self, rex_contenthost, target_sat): :CaseAutomation: Automated - :CaseLevel: System - :parametrized: yes """ client = rex_contenthost @@ -546,7 +539,7 @@ def test_positive_run_reccuring_job(self, rex_contenthost, target_sat): :id: 49b0d31d-58f9-47f1-aa5d-561a1dcb0d66 - :Steps: + :steps: 0. Create a VM and register to SAT and prepare for REX (ssh key) @@ -562,8 +555,6 @@ def test_positive_run_reccuring_job(self, rex_contenthost, target_sat): :bz: 2129432 - :CaseLevel: System - :parametrized: yes """ client = rex_contenthost @@ -598,7 +589,7 @@ def test_positive_run_concurrent_jobs(self, registered_hosts, target_sat): :id: ad0f108c-03f2-49c7-8732-b1056570567b - :Steps: + :steps: 0. Create 2 hosts, disable foreman_tasks_proxy_batch_trigger @@ -610,8 +601,6 @@ def test_positive_run_concurrent_jobs(self, registered_hosts, target_sat): :customerscenario: true - :CaseLevel: System - :BZ: 1817320 :parametrized: yes @@ -644,6 +633,76 @@ def test_positive_run_concurrent_jobs(self, registered_hosts, target_sat): target_sat.cli.GlobalParameter().delete({'name': param_name}) assert len(target_sat.cli.GlobalParameter().list({'search': param_name})) == 0 + @pytest.mark.tier3 + @pytest.mark.no_containers + def test_positive_run_serial(self, registered_hosts, target_sat): + """Tests subtasks in a job run one by one when concurrency level set to 1 + + :id: 5ce39447-82d0-42df-81be-16ed3d67a2a4 + + :Setup: + 0. Create 2 hosts + + :steps: + + 0. Run a bash command job with concurrency level 1 + + :expectedresults: First subtask should run immediately, second one after the first one finishes + + :CaseAutomation: Automated + + :parametrized: yes + """ + hosts = registered_hosts + output_msgs = [] + template_file = f"/root/{gen_string('alpha')}.template" + target_sat.execute( + f"echo 'rm /root/test-<%= @host %>; echo $(date +%s) >> /root/test-<%= @host %>; sleep 120; echo $(date +%s) >> /root/test-<%= @host %>' > {template_file}" + ) + template = target_sat.cli.JobTemplate.create( + { + 'name': gen_string('alpha'), + 'file': template_file, + 'job-category': 'Commands', + 'provider-type': 'script', + } + ) + invocation = target_sat.cli_factory.job_invocation( + { + 'job-template': template['name'], + 'search-query': f'name ~ {hosts[0].hostname} or name ~ {hosts[1].hostname}', + 'concurrency-level': 1, + } + ) + for vm in hosts: + output_msgs.append( + 'host output from {}: {}'.format( + vm.hostname, + ' '.join( + target_sat.cli.JobInvocation.get_output( + {'id': invocation['id'], 'host': vm.hostname} + ) + ), + ) + ) + result = target_sat.cli.JobInvocation.info({'id': invocation['id']}) + assert result['success'] == '2', output_msgs + # assert for time diffs + file1 = hosts[0].execute('cat /root/test-$(hostname)').stdout + file2 = hosts[1].execute('cat /root/test-$(hostname)').stdout + file1_start, file1_end = map(int, file1.rstrip().split('\n')) + file2_start, file2_end = map(int, file2.rstrip().split('\n')) + if file1_start > file2_start: + file1_start, file1_end, file2_start, file2_end = ( + file2_start, + file2_end, + file1_start, + file1_end, + ) + assert file1_end - file1_start >= 120 + assert file2_end - file2_start >= 120 + assert file2_start >= file1_end # the jobs did NOT run concurrently + @pytest.mark.tier3 @pytest.mark.upgrade @pytest.mark.e2e @@ -660,7 +719,7 @@ def test_positive_run_packages_and_services_job( :id: 47ed82fb-77ca-43d6-a52e-f62bae5d3a42 - :Steps: + :steps: 0. Create a VM and register to SAT and prepare for REX (ssh key) @@ -676,8 +735,6 @@ def test_positive_run_packages_and_services_job( :CaseAutomation: Automated - :CaseLevel: System - :bz: 1872688, 1811166 :CaseImportance: Critical @@ -741,7 +798,7 @@ def test_positive_install_ansible_collection( ): """Test whether Ansible collection can be installed via REX - :Steps: + :steps: 1. Upload a manifest. 2. Enable and sync Ansible repository. 3. Register content host to Satellite. diff --git a/tests/foreman/cli/test_report.py b/tests/foreman/cli/test_report.py index 34bae335913..b46c3881a15 100644 --- a/tests/foreman/cli/test_report.py +++ b/tests/foreman/cli/test_report.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Puppet :Team: Rocket -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import random diff --git a/tests/foreman/cli/test_reporttemplates.py b/tests/foreman/cli/test_reporttemplates.py index d0d94693f95..ca93b8a7318 100644 --- a/tests/foreman/cli/test_reporttemplates.py +++ b/tests/foreman/cli/test_reporttemplates.py @@ -3,17 +3,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Reporting :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from broker import Broker from fauxfactory import gen_alpha @@ -120,7 +115,6 @@ def test_positive_report_help(module_target_sat): :expectedresults: report-templates command is included in help, report-templates command details are displayed, report-templates create command details are displayed - """ command_output = module_target_sat.cli.Base().execute('--help') assert 'report-template' in command_output @@ -298,7 +292,6 @@ def test_positive_report_add_userinput(module_target_sat): 1. hammer template-input create ... :expectedresults: User input is assigned to the report template - """ name = gen_alpha() report_template = module_target_sat.cli_factory.report_template({'name': name}) @@ -441,7 +434,6 @@ def test_positive_applied_errata(): :expectedresults: 1. A report is generated with all applied errata listed 2,3. A report is generated asynchronously - """ diff --git a/tests/foreman/cli/test_repositories.py b/tests/foreman/cli/test_repositories.py index 222f6938860..cf6e10db5c0 100644 --- a/tests/foreman/cli/test_repositories.py +++ b/tests/foreman/cli/test_repositories.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import pytest from requests.exceptions import HTTPError diff --git a/tests/foreman/cli/test_repository.py b/tests/foreman/cli/test_repository.py index 4eb7ac31c5d..b43da6e4e81 100644 --- a/tests/foreman/cli/test_repository.py +++ b/tests/foreman/cli/test_repository.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from random import choice from string import punctuation @@ -730,8 +725,6 @@ def test_positive_synchronize_yum_repo(self, repo_options, repo, target_sat): :expectedresults: Repository is created and synced - :CaseLevel: Integration - :CaseImportance: Critical """ # Repo is not yet synced @@ -757,8 +750,6 @@ def test_positive_synchronize_file_repo(self, repo_options, repo, target_sat): :expectedresults: Repository is created and synced - :CaseLevel: Integration - :CaseImportance: Critical """ # Assertion that repo is not yet synced @@ -799,7 +790,6 @@ def test_positive_synchronize_auth_yum_repo(self, repo, target_sat): :BZ: 1328092 - :CaseLevel: Integration """ # Assertion that repo is not yet synced assert repo['sync']['status'] == 'Not Synced' @@ -840,7 +830,6 @@ def test_negative_synchronize_auth_yum_repo(self, repo, target_sat): :BZ: 1405503, 1453118 - :CaseLevel: Integration """ # Try to synchronize it repo_sync = target_sat.cli.Repository.synchronize({'id': repo['id'], 'async': True}) @@ -1122,7 +1111,6 @@ def test_positive_resynchronize_rpm_repo(self, repo, target_sat): :BZ: 1459845, 1459874, 1318004 - :CaseLevel: Integration """ target_sat.cli.Repository.synchronize({'id': repo['id']}) repo = target_sat.cli.Repository.info({'id': repo['id']}) @@ -1179,7 +1167,7 @@ def test_mirror_on_sync_removes_rpm(self, module_org, repo, repo_options_2, modu 3. Delete one package from repo 1. 4. Sync the second repo (repo 2) from the first repo (repo 1). - :Steps: + :steps: 1. Check that the package deleted from repo 1 was removed from repo 2. :expectedresults: A package removed from repo 1 is removed from repo 2 when synced. @@ -1251,8 +1239,6 @@ def test_positive_synchronize_rpm_repo_ignore_SRPM( :BZ: 1591358 - :CaseLevel: Integration - """ target_sat.cli.Repository.synchronize({'id': repo['id']}) repo = target_sat.cli.Repository.info({'id': repo['id']}) @@ -1681,7 +1667,7 @@ def test_negative_restricted_user_cv_add_repository(self, module_org, repo, modu :BZ: 1436209,1410916 - :Steps: + :steps: 1. Setup a restricted user with permissions that filter the products with names like Test_* or "rhel7*" 2. Create a content view @@ -1697,7 +1683,6 @@ def test_negative_restricted_user_cv_add_repository(self, module_org, repo, modu view, assert that the restricted user still cannot view the product repository. - :CaseLevel: Integration """ required_permissions = { 'Katello::Product': ( @@ -1980,7 +1965,7 @@ def test_positive_create_get_update_delete_module_streams( :Setup: 1. valid yum repo with Module Streams. - :Steps: + :steps: 1. Create Yum Repository with url contain module-streams 2. Initialize synchronization 3. Another Repository with same Url @@ -2049,7 +2034,7 @@ def test_module_stream_list_validation( :Setup: 1. valid yum repo with Module Streams. - :Steps: + :steps: 1. Create Yum Repositories with url contain module-streams and Products 2. Initialize synchronization 3. Verify the module-stream list with various inputs options @@ -2087,7 +2072,7 @@ def test_module_stream_info_validation(self, repo, module_target_sat): :Setup: 1. valid yum repo with Module Streams. - :Steps: + :steps: 1. Create Yum Repositories with url contain module-streams 2. Initialize synchronization 3. Verify the module-stream info with various inputs options @@ -2121,7 +2106,7 @@ def test_negative_update_red_hat_repo(self, module_manifest_org, module_target_s :BZ: 1756951, 2002653 - :Steps: + :steps: 1. Import manifest and enable a Red Hat repository. 2. Attempt to update the Red Hat repository: # hammer repository update --id --url http://example.com/repo @@ -2359,9 +2344,8 @@ def test_positive_sync_third_party_repo(self, repo_options, module_target_sat): # :parametrized: yes # # :expectedresults: Ostree repository is created and synced -# -# :CaseLevel: Integration -# + + # :BZ: 1625783 # """ # # Synchronize it @@ -2546,8 +2530,6 @@ def test_positive_sync_ansible_collection(self, repo, module_target_sat): :expectedresults: All content synced successfully - :CaseLevel: Integration - :CaseImportance: High :parametrized: yes @@ -2580,8 +2562,6 @@ def test_positive_export_ansible_collection(self, repo, module_org, target_sat): :expectedresults: All content exported and imported successfully - :CaseLevel: Integration - :CaseImportance: High """ @@ -2640,8 +2620,6 @@ def test_positive_sync_ansible_collection_from_satellite(self, repo, target_sat) :expectedresults: All content synced successfully - :CaseLevel: Integration - :CaseImportance: High """ @@ -2813,11 +2791,9 @@ def test_positive_git_local_create(self): :id: 89211cd5-82b8-4391-b729-a7502e57f824 - :CaseLevel: Integration - :Setup: Assure local GIT puppet has been created and found by pulp - :Steps: Create link to local puppet mirror via cli + :steps: Create link to local puppet mirror via cli :expectedresults: Content source containing local GIT puppet mirror content is created @@ -2832,11 +2808,9 @@ def test_positive_git_local_update(self): :id: 341f40f2-3501-4754-9acf-7cda1a61f7db - :CaseLevel: Integration - :Setup: Assure local GIT puppet has been created and found by pulp - :Steps: Modify details for existing puppet repo (name, etc.) via cli + :steps: Modify details for existing puppet repo (name, etc.) via cli :expectedresults: Content source containing local GIT puppet mirror content is modified @@ -2852,11 +2826,9 @@ def test_positive_git_local_delete(self): :id: a243f5bb-5186-41b3-8e8a-07d5cc784ccd - :CaseLevel: Integration - :Setup: Assure local GIT puppet has been created and found by pulp - :Steps: Delete link to local puppet mirror via cli + :steps: Delete link to local puppet mirror via cli :expectedresults: Content source containing local GIT puppet mirror content no longer exists/is available. @@ -2871,11 +2843,9 @@ def test_positive_git_remote_create(self): :id: 8582529f-3112-4b49-8d8f-f2bbf7dceca7 - :CaseLevel: Integration - :Setup: Assure remote GIT puppet has been created and found by pulp - :Steps: Create link to local puppet mirror via cli + :steps: Create link to local puppet mirror via cli :expectedresults: Content source containing remote GIT puppet mirror content is created @@ -2890,11 +2860,9 @@ def test_positive_git_remote_update(self): :id: 582c50b3-3b90-4244-b694-97642b1b13a9 - :CaseLevel: Integration - :Setup: Assure remote GIT puppet has been created and found by pulp - :Steps: modify details for existing puppet repo (name, etc.) via cli + :steps: modify details for existing puppet repo (name, etc.) via cli :expectedresults: Content source containing remote GIT puppet mirror content is modified @@ -2910,11 +2878,9 @@ def test_positive_git_remote_delete(self): :id: 0a23f969-b202-4c6c-b12e-f651a0b7d049 - :CaseLevel: Integration - :Setup: Assure remote GIT puppet has been created and found by pulp - :Steps: Delete link to remote puppet mirror via cli + :steps: Delete link to remote puppet mirror via cli :expectedresults: Content source containing remote GIT puppet mirror content no longer exists/is available. @@ -2929,11 +2895,9 @@ def test_positive_git_sync(self): :id: a46c16bd-0986-48db-8e62-aeb3907ba4d2 - :CaseLevel: Integration - :Setup: git mirror (local or remote) exists as a content source - :Steps: Attempt to sync content from mirror via cli + :steps: Attempt to sync content from mirror via cli :expectedresults: Content is pulled down without error @@ -2950,11 +2914,9 @@ def test_positive_git_sync_schedule(self): :id: 0d58d180-9836-4524-b608-66b67f9cab12 - :CaseLevel: Integration - :Setup: git mirror (local or remote) exists as a content source - :Steps: Attempt to create a scheduled sync content from mirror, via cli + :steps: Attempt to create a scheduled sync content from mirror, via cli :expectedresults: Content is pulled down without error on expected schedule @@ -2969,11 +2931,9 @@ def test_positive_git_view_content(self): :id: 02f06092-dd6c-49fa-be9f-831e52476e41 - :CaseLevel: Integration - :Setup: git mirror (local or remote) exists as a content source - :Steps: Attempt to list contents of repo via cli + :steps: Attempt to list contents of repo via cli :expectedresults: Spot-checked items (filenames, dates, perhaps checksums?) are correct. @@ -2998,7 +2958,7 @@ def test_positive_upload_file_to_file_repo(self, repo_options, repo, target_sat) :parametrized: yes - :Steps: + :steps: 1. Create a File Repository 2. Upload an arbitrary file to it @@ -3039,7 +2999,7 @@ def test_positive_file_permissions(self): 1. Create a File Repository 2. Upload an arbitrary file to it - :Steps: Retrieve file permissions from File Repository + :steps: Retrieve file permissions from File Repository :expectedresults: uploaded file permissions are kept after upload @@ -3066,7 +3026,7 @@ def test_positive_remove_file(self, repo, target_sat): 1. Create a File Repository 2. Upload an arbitrary file to it - :Steps: Remove a file from File Repository + :steps: Remove a file from File Repository :expectedresults: file is not listed under File Repository after removal @@ -3122,7 +3082,7 @@ def test_positive_remote_directory_sync(self, repo, module_target_sat): 1. Create a directory to be synced with a pulp manifest on its root 2. Make the directory available through http - :Steps: + :steps: 1. Create a File Repository with url pointing to http url created on setup 2. Initialize synchronization @@ -3152,7 +3112,7 @@ def test_positive_file_repo_local_directory_sync(self, repo, target_sat): 1. Create a directory to be synced with a pulp manifest on its root locally (on the Satellite/Foreman host) - :Steps: + :steps: 1. Create a File Repository with url pointing to local url created on setup 2. Initialize synchronization @@ -3190,7 +3150,7 @@ def test_positive_symlinks_sync(self, repo, target_sat): locally (on the Satellite/Foreman host) 2. Make sure it contains symlinks - :Steps: + :steps: 1. Create a File Repository with url pointing to local url created on setup 2. Initialize synchronization @@ -3232,7 +3192,7 @@ def test_file_repo_contains_only_newer_file(self, repo_options, repo, target_sat 4. Add some text keyword to the file locally. 5. Upload new version of file. - :Steps: + :steps: 1. Check that the repo contains only the new version of the file :expectedresults: The file is not duplicated and only the latest version of the file @@ -3295,7 +3255,7 @@ def test_copy_package_group_between_repos(): 2. Create another product and create a yum repo (repo 2) 3. Select the package group from repo 1 and sync it to repo 2 - :Steps: + :steps: Assert the list of package in repo 2 matches the group list from repo 1 :CaseAutomation: NotAutomated @@ -3320,7 +3280,7 @@ def test_include_and_exclude_content_units(): 4. Select a package and exclude its dependencies 5. Copy packages from repo 1 to repo 2 - :Steps: + :steps: Assert the list of packages in repo 2 matches the packages selected in repo 1, including only those dependencies expected. @@ -3347,7 +3307,7 @@ def test_copy_erratum_and_RPMs_within_a_date_range(): 5. Copy filtered list of items from repo 1 to repo 2 6. Repeat using errata in place of RPMs - :Steps: + :steps: Assert the list of packages or errata in repo 2 matches those selected and filtered in repo 1, including those dependencies expected. diff --git a/tests/foreman/cli/test_repository_set.py b/tests/foreman/cli/test_repository_set.py index 317f19e33d5..092034c6dc8 100644 --- a/tests/foreman/cli/test_repository_set.py +++ b/tests/foreman/cli/test_repository_set.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/foreman/cli/test_rhcloud_inventory.py b/tests/foreman/cli/test_rhcloud_inventory.py index cdbf63c2d2f..e127f6e7382 100644 --- a/tests/foreman/cli/test_rhcloud_inventory.py +++ b/tests/foreman/cli/test_rhcloud_inventory.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: System - :CaseComponent: RHCloud-Inventory :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from datetime import datetime import time @@ -41,7 +36,7 @@ def test_positive_inventory_generate_upload_cli( :customerscenario: true - :Steps: + :steps: 0. Create a VM and register to insights within org having manifest. 1. Generate and upload report for all organizations @@ -64,8 +59,6 @@ def test_positive_inventory_generate_upload_cli( :BZ: 1957129, 1895953, 1956190 :CaseAutomation: Automated - - :CaseLevel: System """ org = rhcloud_manifest_org cmd = f'organization_id={org.id} foreman-rake rh_cloud_inventory:report:generate_upload' @@ -112,7 +105,7 @@ def test_positive_inventory_recommendation_sync( :id: 361af91d-1246-4308-9cc8-66beada7d651 - :Steps: + :steps: 0. Create a VM and register to insights within org having manifest. 1. Sync insights recommendation using following foreman-rake command. @@ -123,8 +116,6 @@ def test_positive_inventory_recommendation_sync( :BZ: 1957186 :CaseAutomation: Automated - - :CaseLevel: System """ org = rhcloud_manifest_org cmd = f'organization_id={org.id} foreman-rake rh_cloud_insights:sync' @@ -156,7 +147,7 @@ def test_positive_sync_inventory_status( :id: 915ffbfd-c2e6-4296-9d69-f3f9a0e79b32 - :Steps: + :steps: 0. Create a VM and register to insights within org having manifest. 1. Sync inventory status for specific organization. @@ -168,8 +159,6 @@ def test_positive_sync_inventory_status( :BZ: 1957186 :CaseAutomation: Automated - - :CaseLevel: System """ org = rhcloud_manifest_org cmd = f'organization_id={org.id} foreman-rake rh_cloud_inventory:sync' @@ -203,7 +192,7 @@ def test_max_org_size_variable(): :id: 7dd964c3-fde8-4335-ab13-02329119d7f6 - :Steps: + :steps: 1. Register few content hosts with satellite. 2. Change value of max_org_size for testing purpose(See BZ#1962694#c2). @@ -218,8 +207,6 @@ def test_max_org_size_variable(): :BZ: 1962694 :CaseAutomation: ManualOnly - - :CaseLevel: System """ @@ -229,7 +216,7 @@ def test_satellite_inventory_slice_variable(): :id: ffbef1c7-08f3-444b-9255-2251d5594fcb - :Steps: + :steps: 1. Register few content hosts with satellite. 2. Set SATELLITE_INVENTORY_SLICE_SIZE=1 dynflow environment variable. @@ -244,8 +231,6 @@ def test_satellite_inventory_slice_variable(): :BZ: 1945661 :CaseAutomation: ManualOnly - - :CaseLevel: System """ @@ -255,7 +240,7 @@ def test_rhcloud_external_links(): :id: bc7f6354-ed3e-4ac5-939d-90bfe4177043 - :Steps: + :steps: 1. Go to Configure > Inventory upload 2. Go to Configure > Insights @@ -267,8 +252,6 @@ def test_rhcloud_external_links(): :BZ: 1975093 :CaseAutomation: ManualOnly - - :CaseLevel: System """ @@ -278,7 +261,7 @@ def test_positive_generate_all_reports_job(target_sat): :id: a9e4bfdb-6d7c-4f8c-ae57-a81442926dd8 - :Steps: + :steps: 1. Disable the Automatic Inventory upload setting. 2. Execute Foreman GenerateAllReportsJob via foreman-rake. @@ -289,8 +272,6 @@ def test_positive_generate_all_reports_job(target_sat): :customerscenario: true :CaseAutomation: Automated - - :CaseLevel: System """ try: target_sat.update_setting('allow_auto_inventory_upload', False) diff --git a/tests/foreman/cli/test_role.py b/tests/foreman/cli/test_role.py index b3cd83c6d1f..5eb4e8569f8 100644 --- a/tests/foreman/cli/test_role.py +++ b/tests/foreman/cli/test_role.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: UsersRoles :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from math import ceil from random import choice @@ -283,7 +278,6 @@ def test_system_admin_role_end_to_end(self, target_sat): 4. System Admin role should be able to create Organization admins 5. User with sys admin role should be able to edit filters on roles - :CaseLevel: System """ org = target_sat.cli_factory.make_org() location = target_sat.cli_factory.make_location() diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index bdb678e59b8..dcbc2848756 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: InterSatelliteSync :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import os from time import sleep @@ -239,8 +234,6 @@ def test_positive_export_version_custom_repo( 1. Complete export succeeds, exported files are present on satellite machine. 2. Incremental export succeeds, exported files are present on satellite machine. - :CaseLevel: System - :BZ: 1944733 :customerscenario: true @@ -298,7 +291,6 @@ def test_positive_export_library_custom_repo( 1. Complete export succeeds, exported files are present on satellite machine. 2. Incremental export succeeds, exported files are present on satellite machine. - :CaseLevel: System """ # Create cv and publish cv_name = gen_string('alpha') @@ -352,7 +344,6 @@ def test_positive_export_complete_library_rh_repo( :expectedresults: 1. Repository was successfully exported, exported files are present on satellite machine - :CaseLevel: System """ # Create cv and publish cv_name = gen_string('alpha') @@ -562,8 +553,6 @@ def test_positive_export_import_cv_end_to_end( 1. CV version custom contents has been exported to directory. 2. All The exported custom contents has been imported in org/satellite. - :CaseLevel: System - :BZ: 1832858 :customerscenario: true @@ -654,8 +643,6 @@ def test_positive_export_import_default_org_view( 1. Default Organization View version custom contents has been exported. 2. All the exported custom contents has been imported in org/satellite. - :CaseLevel: System - :BZ: 1671319 :customerscenario: true @@ -753,7 +740,6 @@ def test_positive_export_import_filtered_cvv( 1. Filtered CV version custom contents has been exported to directory 2. Filtered exported custom contents has been imported in org/satellite - :CaseLevel: System """ exporting_cv_name = importing_cvv = gen_string('alpha') exporting_cv, exporting_cvv = _create_cv( @@ -846,7 +832,6 @@ def test_positive_export_import_promoted_cv( 2. Promoted CV version contents has been imported successfully. 3. The imported CV should only be published and not promoted. - :CaseLevel: System """ import_cv_name = class_export_entities['exporting_cv_name'] export_cv_id = class_export_entities['exporting_cv']['id'] @@ -932,7 +917,6 @@ def test_positive_export_import_redhat_cv( :customerscenario: true - :CaseLevel: System """ # Create cv and publish cv_name = gen_string('alpha') @@ -1728,8 +1712,6 @@ def test_positive_export_incremental_syncable_check_content( 1. Complete and incremental export succeed. 2. All files referenced in the repomd.xml files are present in the exports. - :CaseLevel: System - :BZ: 2212523 :customerscenario: true @@ -1814,7 +1796,6 @@ def test_positive_export_import_cv_incremental(self): :CaseAutomation: NotAutomated - :CaseLevel: System """ @pytest.mark.stubbed @@ -1836,7 +1817,6 @@ def test_positive_reimport_repo(self): :CaseAutomation: NotAutomated - :CaseLevel: System """ @pytest.mark.stubbed @@ -1854,7 +1834,6 @@ def test_negative_export_repo_from_future_datetime(self): :CaseAutomation: NotAutomated - :CaseLevel: System """ @pytest.mark.tier3 @@ -1886,7 +1865,6 @@ def test_positive_export_import_incremental_yum_repo( in the importing organization and content counts match. 2. Incremental export and import succeeds, content counts match the updated counts. - :CaseLevel: System """ export_cc = target_sat.cli.Repository.info({'id': function_synced_custom_repo.id})[ 'content-counts' @@ -1973,8 +1951,6 @@ def test_positive_export_import_mismatch_label( :expectedresults: 1. All exports and imports succeed. - :CaseLevel: System - :CaseImportance: Medium :BZ: 2092039 @@ -2074,8 +2050,6 @@ def test_positive_custom_cdn_with_credential( :expectedresults: 1. Repository can be enabled and synced from Upstream to Downstream Satellite. - :CaseLevel: System - :BZ: 2112098 :customerscenario: true @@ -2188,7 +2162,6 @@ def test_positive_install_package_from_imported_repos(self): :CaseAutomation: NotAutomated - :CaseLevel: System """ @@ -2296,8 +2269,6 @@ def test_positive_network_sync_rh_repo( :expectedresults: 1. Repository can be enabled and synced. - :CaseLevel: System - :BZ: 2213128 :customerscenario: true diff --git a/tests/foreman/cli/test_settings.py b/tests/foreman/cli/test_settings.py index 4923e8ec5f8..fdd005e02e8 100644 --- a/tests/foreman/cli/test_settings.py +++ b/tests/foreman/cli/test_settings.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Settings :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random from time import sleep @@ -60,7 +55,6 @@ def test_positive_update_hostname_prefix_without_value(setting_update, module_ta :BZ: 1470083 :expectedresults: Error should be raised on setting empty value for discovery_prefix setting - """ with pytest.raises(CLIReturnCodeError): module_target_sat.cli.Settings.set({'name': "discovery_prefix", 'value': ""}) @@ -76,7 +70,6 @@ def test_positive_update_hostname_default_prefix(setting_update, module_target_s :parametrized: yes :expectedresults: Default set prefix should be updated with new value - """ hostname_prefix_value = gen_string('alpha') module_target_sat.cli.Settings.set({'name': "discovery_prefix", 'value': hostname_prefix_value}) @@ -128,7 +121,6 @@ def test_positive_update_login_page_footer_text(setting_update, module_target_sa :parametrized: yes :expectedresults: Parameter is updated successfully - """ login_text_value = random.choice(list(valid_data_list().values())) module_target_sat.cli.Settings.set({'name': "login_text", 'value': login_text_value}) @@ -151,7 +143,6 @@ def test_positive_update_login_page_footer_text_without_value(setting_update, mo :parametrized: yes :expectedresults: Message on login screen should be removed - """ module_target_sat.cli.Settings.set({'name': "login_text", 'value': ""}) login_text = module_target_sat.cli.Settings.list({'search': 'name=login_text'})[0] @@ -465,8 +456,6 @@ def test_positive_failed_login_attempts_limit(setting_update, target_sat): :CaseImportance: Critical - :CaseLevel: System - :parametrized: yes :expectedresults: failed_login_attempts_limit works as expected diff --git a/tests/foreman/cli/test_sso.py b/tests/foreman/cli/test_sso.py index d70a56a139f..d8b772a936f 100644 --- a/tests/foreman/cli/test_sso.py +++ b/tests/foreman/cli/test_sso.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: LDAP :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/foreman/cli/test_subnet.py b/tests/foreman/cli/test_subnet.py index a08fe818dfd..12272ed57c2 100644 --- a/tests/foreman/cli/test_subnet.py +++ b/tests/foreman/cli/test_subnet.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Networking :Team: Rocket -:TestType: Functional - :CaseImportance: Medium -:Upstream: No """ import random import re diff --git a/tests/foreman/cli/test_subscription.py b/tests/foreman/cli/test_subscription.py index f2491479e40..c6d053a4659 100644 --- a/tests/foreman/cli/test_subscription.py +++ b/tests/foreman/cli/test_subscription.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: SubscriptionManagement :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string from nailgun import entities @@ -92,8 +87,6 @@ def test_positive_enable_manifest_reposet(function_entitlement_manifest_org, mod :expectedresults: you are able to enable and synchronize repository contained in a manifest - :CaseLevel: Integration - :CaseImportance: Critical """ module_target_sat.cli.Subscription.list( diff --git a/tests/foreman/cli/test_syncplan.py b/tests/foreman/cli/test_syncplan.py index 483a7145c85..86f33b68ffb 100644 --- a/tests/foreman/cli/test_syncplan.py +++ b/tests/foreman/cli/test_syncplan.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: SyncPlans :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from datetime import datetime, timedelta from time import sleep @@ -349,8 +344,6 @@ def test_positive_info_with_assigned_product(module_org, module_target_sat): :BZ: 1390545 :CaseImportance: Critical - - :CaseLevel: Integration """ prod1 = gen_string('alpha') prod2 = gen_string('alpha') @@ -384,8 +377,6 @@ def test_negative_synchronize_custom_product_past_sync_date(module_org, request, :expectedresults: Repository was not synchronized :BZ: 1279539 - - :CaseLevel: System """ new_sync_plan = target_sat.cli_factory.sync_plan( { @@ -415,8 +406,6 @@ def test_positive_synchronize_custom_product_past_sync_date(module_org, request, :expectedresults: Product is synchronized successfully. :BZ: 1279539 - - :CaseLevel: System """ interval = 60 * 60 # 'hourly' sync interval in seconds delay = 2 * 60 @@ -467,8 +456,6 @@ def test_positive_synchronize_custom_product_future_sync_date(module_org, reques :expectedresults: Product is synchronized successfully. - :CaseLevel: System - :BZ: 1655595 """ cron_multiple = 5 # sync event is on every multiple of this value, starting from 00 mins @@ -526,8 +513,6 @@ def test_positive_synchronize_custom_products_future_sync_date(module_org, reque :expectedresults: Products are synchronized successfully. - :CaseLevel: System - :BZ: 1655595 """ cron_multiple = 5 # sync event is on every multiple of this value, starting from 00 mins @@ -606,8 +591,6 @@ def test_positive_synchronize_rh_product_past_sync_date( :expectedresults: Product is synchronized successfully. :BZ: 1279539 - - :CaseLevel: System """ interval = 60 * 60 # 'hourly' sync interval in seconds delay = 2 * 60 @@ -673,8 +656,6 @@ def test_positive_synchronize_rh_product_future_sync_date( :expectedresults: Product is synchronized successfully. - :CaseLevel: System - :BZ: 1655595 """ cron_multiple = 5 # sync event is on every multiple of this value, starting from 00 mins @@ -746,8 +727,6 @@ def test_positive_synchronize_custom_product_daily_recurrence(module_org, reques :id: 8d882e8b-b5c1-4449-81c6-0efd31ad75a7 :expectedresults: Product is synchronized successfully. - - :CaseLevel: System """ delay = 2 * 60 product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) @@ -797,8 +776,6 @@ def test_positive_synchronize_custom_product_weekly_recurrence(module_org, reque :expectedresults: Product is synchronized successfully. :BZ: 1396647 - - :CaseLevel: System """ delay = 2 * 60 product = target_sat.cli_factory.make_product({'organization-id': module_org.id}) diff --git a/tests/foreman/cli/test_templatesync.py b/tests/foreman/cli/test_templatesync.py index 2facc9139de..3f8472d829e 100644 --- a/tests/foreman/cli/test_templatesync.py +++ b/tests/foreman/cli/test_templatesync.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Integration - :CaseComponent: TemplatesPlugin :Team: Endeavour -:TestType: Functional - -:Upstream: No """ import base64 @@ -66,7 +61,7 @@ def test_positive_import_force_locked_template( :id: b80fbfc4-bcab-4a5d-b6c1-0e22906cd8ab - :Steps: + :steps: 1. Import some of the locked template specifying the `force` parameter `false`. 2. After ensuring the template is not updated, Import same locked template @@ -134,7 +129,7 @@ def test_positive_update_templates_in_git( :id: 5b0be026-2983-4570-bc63-d9aba36fca65 - :Steps: + :steps: 1. Repository contains file with same name as exported template. 2. Export "Atomic Kickstart default" templates to git repo. @@ -201,7 +196,7 @@ def test_positive_export_filtered_templates_to_git( :id: fd583f85-f170-4b93-b9b1-36d72f31c31f - :Steps: + :steps: 1. Export only the templates matching with regex e.g: `^atomic.*` to git repo. :expectedresults: @@ -242,7 +237,7 @@ def test_positive_export_filtered_templates_to_temp_dir(self, module_org, target :bz: 1778177 - :Steps: Export the templates matching with regex e.g: `ansible` to /tmp directory. + :steps: Export the templates matching with regex e.g: `ansible` to /tmp directory. :expectedresults: The templates are exported /tmp directory diff --git a/tests/foreman/cli/test_user.py b/tests/foreman/cli/test_user.py index 9c9ebd67822..c549b9e667a 100644 --- a/tests/foreman/cli/test_user.py +++ b/tests/foreman/cli/test_user.py @@ -10,17 +10,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: UsersRoles :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import datetime import random @@ -205,7 +200,6 @@ def test_positive_create_with_orgs_and_update(self, module_target_sat): :expectedresults: User is created with orgs, orgs are updated - :CaseLevel: Integration """ orgs_amount = 2 orgs = [module_target_sat.cli_factory.make_org() for _ in range(orgs_amount)] @@ -247,7 +241,6 @@ def test_positive_last_login_for_new_user(self, module_target_sat): :BZ: 1763816 - :CaseLevel: Integration """ login = gen_string('alpha') password = gen_string('alpha') @@ -280,7 +273,7 @@ def test_positive_update_all_locales(self, module_target_sat): :id: f0993495-5117-461d-a116-44867b820139 - :Steps: Update current User with all different Language options + :steps: Update current User with all different Language options :expectedresults: Current User is updated @@ -309,7 +302,6 @@ def test_positive_add_and_delete_roles(self, module_roles, module_target_sat): :expectedresults: Roles are added to user and deleted successfully - :CaseLevel: Integration """ user = module_target_sat.cli_factory.user() original_role_names = set(user['roles']) @@ -415,8 +407,6 @@ def test_personal_access_token_admin_user(self, target_sat): 1. Should show output of the api endpoint 2. When revoked, authentication error - :CaseLevel: System - :CaseImportance: High """ user = target_sat.cli_factory.user({'admin': '1'}) @@ -453,8 +443,6 @@ def test_positive_personal_access_token_user_with_role(self, target_sat): 2. When an incorrect end point is used, missing permission should be displayed. - :CaseLevel: System - :CaseImportance: High """ user = target_sat.cli_factory.user() @@ -487,8 +475,6 @@ def test_expired_personal_access_token(self, target_sat): :expectedresults: Authentication error - :CaseLevel: System - :CaseImportance: Medium """ user = target_sat.cli_factory.user() @@ -527,8 +513,6 @@ def test_custom_personal_access_token_role(self, target_sat): :expectedresults: Non admin user is able to view only the assigned entity - :CaseLevel: System - :CaseImportance: High :BZ: 1974685, 1996048 @@ -562,3 +546,44 @@ def test_custom_personal_access_token_role(self, target_sat): f'curl -k -u {user["login"]}:{token_value} {target_sat.url}/api/v2/users' ) assert f'Unable to authenticate user {user["login"]}' in command_output.stdout + + @pytest.mark.tier2 + def test_negative_personal_access_token_invalid_date(self, target_sat): + """Personal access token with invalid expire date. + + :id: 8c7c91c5-f6d9-4709-857c-6a875db41b88 + + :steps: + 1. Set the expired time to a nonsensical datetime + 2. Set the expired time to a past datetime + + :expectedresults: token is not created with invalid or past expire time + + :CaseImportance: Medium + + :BZ: 2231814 + """ + user = target_sat.cli_factory.user() + target_sat.cli.User.add_role({'login': user['login'], 'role': 'Viewer'}) + token_name = gen_alphanumeric() + # check for invalid datetime + invalid_datetimes = ['00-14-00 09:30:55', '2028-08-22 28:30:55', '0000-00-22 15:90:55'] + for datetime_expire in invalid_datetimes: + with pytest.raises(CLIReturnCodeError): + target_sat.cli.User.access_token( + action='create', + options={ + 'name': token_name, + 'user-id': user['id'], + 'expires-at': datetime_expire, + }, + ) + # check for past datetime + datetime_now = datetime.datetime.utcnow() + datetime_expire = datetime_now - datetime.timedelta(seconds=20) + datetime_expire = datetime_expire.strftime("%Y-%m-%d %H:%M:%S") + with pytest.raises(CLIReturnCodeError): + target_sat.cli.User.access_token( + action='create', + options={'name': token_name, 'user-id': user['id'], 'expires-at': datetime_expire}, + ) diff --git a/tests/foreman/cli/test_usergroup.py b/tests/foreman/cli/test_usergroup.py index 81e412de415..6ad115d96f6 100644 --- a/tests/foreman/cli/test_usergroup.py +++ b/tests/foreman/cli/test_usergroup.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: UsersRoles :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random @@ -118,8 +113,6 @@ def test_positive_add_and_remove_elements(module_target_sat): :expectedresults: Elements are added to user group and then removed successfully. - - :CaseLevel: Integration """ role = module_target_sat.cli_factory.make_role() user_group = module_target_sat.cli_factory.usergroup() @@ -165,8 +158,6 @@ def test_positive_remove_user_assigned_to_usergroup(module_target_sat): :customerscenario: true - :CaseLevel: Integration - :BZ: 1667704 """ user = module_target_sat.cli_factory.user() @@ -190,8 +181,6 @@ def test_positive_automate_bz1426957(ldap_auth_source, function_user_group, targ :customerscenario: true - :CaseLevel: Integration - :BZ: 1426957, 1667704 """ ext_user_group = target_sat.cli_factory.usergroup_external( @@ -230,8 +219,6 @@ def test_negative_automate_bz1437578(ldap_auth_source, function_user_group, modu :expectedresults: Error message as Domain Users is a special group in AD. - :CaseLevel: Integration - :BZ: 1437578 """ with pytest.raises(CLIReturnCodeError): diff --git a/tests/foreman/cli/test_vm_install_products_package.py b/tests/foreman/cli/test_vm_install_products_package.py index 53777235341..f03fc19e997 100644 --- a/tests/foreman/cli/test_vm_install_products_package.py +++ b/tests/foreman/cli/test_vm_install_products_package.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from broker import Broker import pytest diff --git a/tests/foreman/cli/test_webhook.py b/tests/foreman/cli/test_webhook.py index 516df792b63..2849b6aa57d 100644 --- a/tests/foreman/cli/test_webhook.py +++ b/tests/foreman/cli/test_webhook.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: HooksandWebhooks :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from functools import partial from random import choice diff --git a/tests/foreman/destructive/conftest.py b/tests/foreman/destructive/conftest.py index 190bc2cabe0..c80a2b6d288 100644 --- a/tests/foreman/destructive/conftest.py +++ b/tests/foreman/destructive/conftest.py @@ -20,7 +20,6 @@ def test_foo(session): with session: # your ui test steps here session.architecture.create({'name': 'bar'}) - """ with module_target_sat.ui_session(test_name, ui_user.login, ui_user.password) as session: yield session diff --git a/tests/foreman/destructive/test_ansible.py b/tests/foreman/destructive/test_ansible.py index 34e7985ec8c..0ff8fc67a1d 100644 --- a/tests/foreman/destructive/test_ansible.py +++ b/tests/foreman/destructive/test_ansible.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Ansible :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -30,7 +25,7 @@ def test_positive_persistent_ansible_cfg_change(target_sat): :customerscenario: true - :Steps: + :steps: 1. Update value in ansible.cfg. 2. Verify value is updated in the file. 3. Run "satellite-installer". @@ -53,7 +48,7 @@ def test_positive_import_all_roles(target_sat): :id: 53fe3857-a08f-493d-93c7-3fed331ed391 - :Steps: + :steps: 1. Navigate to the Configure > Roles page. 2. Click the `Import from [hostname]` button. diff --git a/tests/foreman/destructive/test_auth.py b/tests/foreman/destructive/test_auth.py index 7ab8f29e2a7..d97c592d05b 100644 --- a/tests/foreman/destructive/test_auth.py +++ b/tests/foreman/destructive/test_auth.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Authentication :Team: Endeavour -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -38,7 +33,6 @@ def test_positive_password_reset(target_sat): :expectedresults: verify the 'foreman-rake permissions:reset' command for the admin user :CaseImportance: High - """ result = target_sat.execute('foreman-rake permissions:reset') assert result.status == 0 diff --git a/tests/foreman/destructive/test_capsule.py b/tests/foreman/destructive/test_capsule.py index 0051e062be5..40935c114b9 100644 --- a/tests/foreman/destructive/test_capsule.py +++ b/tests/foreman/destructive/test_capsule.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Capsule :Team: Platform -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -33,8 +28,6 @@ def test_positive_capsule_certs_generate_with_special_char(target_sat): :expectedresults: capsule-certs-generate works - :CaseLevel: Component - :BZ: 1908841 :customerscenario: true diff --git a/tests/foreman/destructive/test_capsule_loadbalancer.py b/tests/foreman/destructive/test_capsule_loadbalancer.py index 77adccd1fcf..a392e0907d5 100644 --- a/tests/foreman/destructive/test_capsule_loadbalancer.py +++ b/tests/foreman/destructive/test_capsule_loadbalancer.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Integration - :CaseComponent: Capsule :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest from wrapanapi import VmState @@ -183,7 +178,7 @@ def test_loadbalancer_install_package( :id: bd3c2e50-18e2-4be7-8a7f-c32472e17c61 - :Steps: + :steps: 1. run `subscription-manager register --org=Your_Organization \ --activationkey=Your_Activation_Key \` 2. Try package installation @@ -195,8 +190,7 @@ def test_loadbalancer_install_package( :expectedresults: The client should be get the package irrespective of the capsule registration. - :CaseLevel: Integration - """ + """ # Register content host result = rhel7_contenthost.register( org=module_org, @@ -255,7 +249,7 @@ def test_client_register_through_lb( :id: c7e47d61-167b-4fc2-8d1a-d9a64350fdc4 - :Steps: + :steps: 1. Setup capsules, host and loadbalancer. 2. Generate curl command for host registration. 3. Register host through loadbalancer using global registration @@ -263,8 +257,6 @@ def test_client_register_through_lb( :expectedresults: Global Registration should have option to register through loadbalancer and host should get registered successfully. - :CaseLevel: Integration - :BZ: 1963266 :customerscenario: true diff --git a/tests/foreman/destructive/test_capsulecontent.py b/tests/foreman/destructive/test_capsulecontent.py index 9def4e5c46b..12f3455c08d 100644 --- a/tests/foreman/destructive/test_capsulecontent.py +++ b/tests/foreman/destructive/test_capsulecontent.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: System - :CaseComponent: Capsule-Content :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from box import Box from fauxfactory import gen_alpha @@ -52,7 +47,6 @@ def test_positive_sync_without_deadlock( :customerscenario: true :BZ: 2062526 - """ # Note: As of now BZ#2122872 prevents us to use the originally intended RHEL7 repo because # of a memory leak causing Satellite OOM crash in this scenario. Therefore, for now we use diff --git a/tests/foreman/destructive/test_clone.py b/tests/foreman/destructive/test_clone.py index 77a7147859a..f5f75dcd1a1 100644 --- a/tests/foreman/destructive/test_clone.py +++ b/tests/foreman/destructive/test_clone.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: SatelliteClone :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/foreman/destructive/test_contenthost.py b/tests/foreman/destructive/test_contenthost.py index 3d07a2b493d..d300ef9ab0e 100644 --- a/tests/foreman/destructive/test_contenthost.py +++ b/tests/foreman/destructive/test_contenthost.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Hosts-Content :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -33,8 +28,6 @@ def test_content_access_after_stopped_foreman(target_sat, rhel7_contenthost): :expectedresults: Package should get installed even after foreman service is stopped - :CaseLevel: System - :CaseImportance: Medium :CaseComponent: Infrastructure diff --git a/tests/foreman/destructive/test_contentview.py b/tests/foreman/destructive/test_contentview.py index 76f91ac0f42..03af433a939 100644 --- a/tests/foreman/destructive/test_contentview.py +++ b/tests/foreman/destructive/test_contentview.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ContentViews :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from nailgun.entity_mixins import TaskFailedError import pytest diff --git a/tests/foreman/destructive/test_discoveredhost.py b/tests/foreman/destructive/test_discoveredhost.py index cff2fac1f8f..bcf73a14ed7 100644 --- a/tests/foreman/destructive/test_discoveredhost.py +++ b/tests/foreman/destructive/test_discoveredhost.py @@ -8,11 +8,6 @@ :CaseAutomation: Automated -:CaseLevel: System - -:TestType: Functional - -:Upstream: No """ from copy import copy import re @@ -212,7 +207,7 @@ def test_positive_provision_pxe_host_dhcp_change(): :Setup: Provisioning should be configured and a host should be discovered - :Steps: + :steps: 1. Set some dhcp range in dhcpd.conf in satellite. 2. Create subnet entity in satellite with a range different from whats defined in `dhcpd.conf`. diff --git a/tests/foreman/destructive/test_foreman_rake.py b/tests/foreman/destructive/test_foreman_rake.py index f655fc3787b..5162fa94be2 100644 --- a/tests/foreman/destructive/test_foreman_rake.py +++ b/tests/foreman/destructive/test_foreman_rake.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: System - -:TestType: Functional - :CaseImportance: Medium :CaseComponent: TasksPlugin :Team: Endeavour -:Upstream: No """ import pytest @@ -28,7 +23,7 @@ def test_positive_katello_reimport(target_sat): :id: b4119265-1bf0-4b0b-8b96-43f68af39708 - :Steps: Have satellite up and run 'foreman-rake katello:reimport' + :steps: Have satellite up and run 'foreman-rake katello:reimport' :expectedresults: Successfully reimport without errors diff --git a/tests/foreman/destructive/test_foreman_service.py b/tests/foreman/destructive/test_foreman_service.py index 7f481df3114..6d0d24de110 100644 --- a/tests/foreman/destructive/test_foreman_service.py +++ b/tests/foreman/destructive/test_foreman_service.py @@ -4,13 +4,8 @@ :CaseAutomation: Automated -:CaseLevel: System - -:TestType: Functional - :CaseImportance: Medium -:Upstream: No """ import pytest @@ -29,7 +24,7 @@ def test_positive_foreman_service_auto_restart(foreman_service_teardown): :id: 766560b8-30bb-11eb-8dae-d46d6dd3b5b2 - :Steps: + :steps: 1. Stop the Foreman Service 2. Make any API call to Satellite diff --git a/tests/foreman/destructive/test_host.py b/tests/foreman/destructive/test_host.py index 65d7ebe184d..bab00e5acab 100644 --- a/tests/foreman/destructive/test_host.py +++ b/tests/foreman/destructive/test_host.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from airgun.exceptions import NoSuchElementException import pytest @@ -41,8 +36,6 @@ def test_positive_cockpit(self, cockpit_host, class_cockpit_sat, class_org): :BZ: 1876220 - :CaseLevel: System - :steps: 1. kill the cockpit service. 2. go to web console and verify if getting 503 error. diff --git a/tests/foreman/destructive/test_infoblox.py b/tests/foreman/destructive/test_infoblox.py index 8aa92f22a83..52e26830b14 100644 --- a/tests/foreman/destructive/test_infoblox.py +++ b/tests/foreman/destructive/test_infoblox.py @@ -2,17 +2,12 @@ :Requirement: Infoblox, Installer -:CaseLevel: System - :CaseComponent: DHCPDNS :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_mac, gen_string import pytest @@ -100,7 +95,7 @@ def test_plugin_installation(target_sat, command_args, command_opts, rpm_command :id: c75aa5f3-870a-4f4a-9d7a-0a871b47fd6f - :Steps: Run installer with mininum options required to install plugins + :steps: Run installer with mininum options required to install plugins :expectedresults: Plugins install successfully diff --git a/tests/foreman/destructive/test_installer.py b/tests/foreman/destructive/test_installer.py index a654c4e6bf5..c130e0fcd20 100644 --- a/tests/foreman/destructive/test_installer.py +++ b/tests/foreman/destructive/test_installer.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Installer :Team: Platform -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import random @@ -57,8 +52,6 @@ def test_installer_sat_pub_directory_accessibility(target_sat): :CaseImportance: High - :CaseLevel: System - :BZ: 1960801 :customerscenario: true diff --git a/tests/foreman/destructive/test_katello_agent.py b/tests/foreman/destructive/test_katello_agent.py index e64a0e3ec34..86943017ee7 100644 --- a/tests/foreman/destructive/test_katello_agent.py +++ b/tests/foreman/destructive/test_katello_agent.py @@ -40,7 +40,6 @@ def test_positive_apply_errata(katello_agent_client): :parametrized: yes - :CaseLevel: System """ sat = katello_agent_client['sat'] client = katello_agent_client['client'] @@ -68,7 +67,6 @@ def test_positive_install_and_remove_package(katello_agent_client): :parametrized: yes - :CaseLevel: System """ sat = katello_agent_client['sat'] client = katello_agent_client['client'] @@ -93,7 +91,6 @@ def test_positive_upgrade_package(katello_agent_client): :parametrized: yes - :CaseLevel: System """ sat = katello_agent_client['sat'] client = katello_agent_client['client'] @@ -115,7 +112,6 @@ def test_positive_upgrade_packages_all(katello_agent_client): :parametrized: yes - :CaseLevel: System """ sat = katello_agent_client['sat'] client = katello_agent_client['client'] @@ -135,7 +131,6 @@ def test_positive_install_and_remove_package_group(katello_agent_client): :parametrized: yes - :CaseLevel: System """ sat = katello_agent_client['sat'] client = katello_agent_client['client'] @@ -156,7 +151,6 @@ def test_positive_upgrade_warning(sat_with_katello_agent): :expectedresults: Upgrade check fails and warning with proper message is displayed. - :CaseLevel: System """ sat = sat_with_katello_agent ver = sat.version.split('.') diff --git a/tests/foreman/destructive/test_katello_certs_check.py b/tests/foreman/destructive/test_katello_certs_check.py index 0d0f9fd5a99..4b297f0f979 100644 --- a/tests/foreman/destructive/test_katello_certs_check.py +++ b/tests/foreman/destructive/test_katello_certs_check.py @@ -4,18 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: System - :CaseComponent: Certificates :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No - """ import re diff --git a/tests/foreman/destructive/test_ldap_authentication.py b/tests/foreman/destructive/test_ldap_authentication.py index 9ce3cc07261..ecedf803e00 100644 --- a/tests/foreman/destructive/test_ldap_authentication.py +++ b/tests/foreman/destructive/test_ldap_authentication.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Integration - :CaseComponent: LDAP :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import os from time import sleep @@ -224,7 +219,6 @@ def test_single_sign_on_ldap_ipa_server( :expectedresults: After single sign on user should redirected from /extlogin to /hosts page :BZ: 1941997 - """ result = target_sat.execute(f'echo {settings.ipa.password} | kinit {settings.ipa.user}') assert result.status == 0 @@ -249,7 +243,6 @@ def test_single_sign_on_ldap_ad_server( using curl. It should navigate to hosts page. (verify using url only) :BZ: 1941997 - """ # create the kerberos ticket for authentication result = target_sat.execute(f'echo {settings.ldap.password} | kinit {settings.ldap.username}') @@ -460,7 +453,6 @@ def test_user_permissions_rhsso_user_after_group_delete( :expectedresults: external rhsso user's permissions should get revoked after external rhsso group deletion. - """ default_sso_host.get_rhsso_client_id() username = settings.rhsso.rhsso_user @@ -618,7 +610,6 @@ def test_permissions_external_ldap_mapped_rhsso_group( :expectedresults: The external ldap mapped rhsso user should contain the permissions based on the user group level - """ ad_data = ad_data() login_details = { @@ -663,7 +654,6 @@ def test_negative_negotiate_login_without_ticket( :expectedresults: 1. Proper messages are returned in all cases. 2. Login and hosts listing fails without Kerberos ticket. - """ result = parametrized_enrolled_sat.cli.Auth.status() assert NO_KERB_MSG in str(result) @@ -702,7 +692,6 @@ def test_positive_negotiate_login_with_ticket( 2. Negotiate login works with the ticket. 3. External user is created and permissions enforcing works. 4. Proper messages are returned in all cases. - """ auth_type = request.node.callspec.params['parametrized_enrolled_sat'] user = ( @@ -766,7 +755,6 @@ def test_positive_negotiate_CRUD( 3. Listing and CRUD operations via hammer succeed. :BZ: 2122617 - """ auth_type = request.node.callspec.params['parametrized_enrolled_sat'] user = ( @@ -843,7 +831,6 @@ def test_positive_negotiate_logout( 1. Session is closed on log out properly on logout. 2. Hammer command fails after log out. 3. Proper messages are returned in all cases. - """ auth_type = request.node.callspec.params['parametrized_enrolled_sat'] user = ( @@ -906,7 +893,6 @@ def test_positive_autonegotiate( :expectedresults: 1. Kerberos ticket can be acquired. 2. Automatic login occurs on first hammer command, user is created - """ auth_type = request.node.callspec.params['parametrized_enrolled_sat'] user = ( @@ -966,7 +952,6 @@ def test_positive_negotiate_manual_with_autonegotiation_disabled( 1. Kerberos ticket can be acquired. 2. Manual login successful, user is created. 3. Session is kept for following Hammer commands. - """ with parametrized_enrolled_sat.omit_credentials(): auth_type = request.node.callspec.params['parametrized_enrolled_sat'] @@ -1047,7 +1032,6 @@ def test_negative_autonegotiate_with_autonegotiation_disabled( 1. Kerberos ticket can be acquired. 2. Autonegotiation doesn't occur 3. Action is denied and user not created because the user isn't authenticated. - """ with parametrized_enrolled_sat.omit_credentials(): auth_type = request.node.callspec.params['parametrized_enrolled_sat'] diff --git a/tests/foreman/destructive/test_ldapauthsource.py b/tests/foreman/destructive/test_ldapauthsource.py index 03e09384db8..020aad9f716 100644 --- a/tests/foreman/destructive/test_ldapauthsource.py +++ b/tests/foreman/destructive/test_ldapauthsource.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: LDAP :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from time import sleep diff --git a/tests/foreman/destructive/test_leapp_satellite.py b/tests/foreman/destructive/test_leapp_satellite.py index f32d8bdfd13..9022b4fec86 100644 --- a/tests/foreman/destructive/test_leapp_satellite.py +++ b/tests/foreman/destructive/test_leapp_satellite.py @@ -2,17 +2,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Upgrades :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from broker import Broker import pytest diff --git a/tests/foreman/destructive/test_packages.py b/tests/foreman/destructive/test_packages.py index 05c44bf1501..382f0639e2d 100644 --- a/tests/foreman/destructive/test_packages.py +++ b/tests/foreman/destructive/test_packages.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ForemanMaintain :Team: Platform -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import re diff --git a/tests/foreman/destructive/test_ping.py b/tests/foreman/destructive/test_ping.py index e683a84a2b6..96ae171c8f7 100644 --- a/tests/foreman/destructive/test_ping.py +++ b/tests/foreman/destructive/test_ping.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Hammer :Team: Endeavour -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import pytest diff --git a/tests/foreman/destructive/test_puppetplugin.py b/tests/foreman/destructive/test_puppetplugin.py index 3e79850a263..9bd118ac4e7 100644 --- a/tests/foreman/destructive/test_puppetplugin.py +++ b/tests/foreman/destructive/test_puppetplugin.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Puppet :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/foreman/destructive/test_realm.py b/tests/foreman/destructive/test_realm.py index 6d429e483c0..a4baa15e7bf 100644 --- a/tests/foreman/destructive/test_realm.py +++ b/tests/foreman/destructive/test_realm.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Authentication :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random diff --git a/tests/foreman/destructive/test_registration.py b/tests/foreman/destructive/test_registration.py index 0320f3b633e..357e779c5d7 100644 --- a/tests/foreman/destructive/test_registration.py +++ b/tests/foreman/destructive/test_registration.py @@ -2,8 +2,6 @@ :Requirement: Registration -:CaseLevel: Acceptance - :CaseComponent: Registration :CaseAutomation: Automated @@ -11,10 +9,6 @@ :CaseImportance: High :Team: Rocket - -:TestType: Functional - -:Upstream: No """ import pytest diff --git a/tests/foreman/destructive/test_remoteexecution.py b/tests/foreman/destructive/test_remoteexecution.py index 6bbb3853cdb..c9c6bdb8b22 100644 --- a/tests/foreman/destructive/test_remoteexecution.py +++ b/tests/foreman/destructive/test_remoteexecution.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: RemoteExecution :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string from nailgun import client diff --git a/tests/foreman/destructive/test_rename.py b/tests/foreman/destructive/test_rename.py index 493f3042c2d..d5af1d6cfee 100644 --- a/tests/foreman/destructive/test_rename.py +++ b/tests/foreman/destructive/test_rename.py @@ -4,18 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: System - :CaseComponent: satellite-change-hostname :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No - """ from fauxfactory import gen_string import pytest diff --git a/tests/foreman/destructive/test_repository.py b/tests/foreman/destructive/test_repository.py index 2ff48fb13b2..026134f2017 100644 --- a/tests/foreman/destructive/test_repository.py +++ b/tests/foreman/destructive/test_repository.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from nailgun.entity_mixins import TaskFailedError import pytest diff --git a/tests/foreman/endtoend/test_api_endtoend.py b/tests/foreman/endtoend/test_api_endtoend.py index 0b32f10c460..dd1cb4fbd56 100644 --- a/tests/foreman/endtoend/test_api_endtoend.py +++ b/tests/foreman/endtoend/test_api_endtoend.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: API :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from collections import defaultdict import http diff --git a/tests/foreman/endtoend/test_cli_endtoend.py b/tests/foreman/endtoend/test_cli_endtoend.py index 2a63dd53924..30de18bd960 100644 --- a/tests/foreman/endtoend/test_cli_endtoend.py +++ b/tests/foreman/endtoend/test_cli_endtoend.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hammer :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_alphanumeric, gen_ipaddr import pytest diff --git a/tests/foreman/installer/test_infoblox.py b/tests/foreman/installer/test_infoblox.py index e8b77fcc67d..5f839d4e850 100644 --- a/tests/foreman/installer/test_infoblox.py +++ b/tests/foreman/installer/test_infoblox.py @@ -2,19 +2,14 @@ :Requirement: Infoblox, Installer -:CaseLevel: System - :CaseAutomation: Automated :CaseComponent: DHCPDNS :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -27,13 +22,11 @@ def test_dhcp_ip_range(): :id: ba957e82-79bb-11e6-94c5-68f72889dc7f - :Steps: Provision a host with infoblox as dhcp provider + :steps: Provision a host with infoblox as dhcp provider :expectedresults: Check host ip is on infoblox range configured by option --foreman-proxy-plugin-dhcp-infoblox-use-ranges=true - :CaseLevel: System - :CaseAutomation: NotAutomated """ @@ -46,14 +39,12 @@ def test_dns_records(): :id: 007ad06e-79bc-11e6-885f-68f72889dc7f - :Steps: + :steps: 1. Provision a host with infoblox as dns provider 2. Update a DNS record on infoblox :expectedresults: Check host dns is updated accordingly to infoblox - :CaseLevel: System - :CaseAutomation: NotAutomated """ diff --git a/tests/foreman/installer/test_installer.py b/tests/foreman/installer/test_installer.py index 7a6f3f04a55..5b3f81ed9a0 100644 --- a/tests/foreman/installer/test_installer.py +++ b/tests/foreman/installer/test_installer.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Installer :Team: Platform -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import pytest import requests @@ -1557,8 +1552,6 @@ def test_positive_selinux_foreman_module(target_sat): 1. Check "foreman-selinux" package availability on satellite. 2. Check SELinux foreman module on satellite. - :CaseLevel: System - :expectedresults: Foreman RPM and SELinux module are both present on the satellite """ rpm_result = target_sat.execute('rpm -q foreman-selinux') @@ -1603,8 +1596,6 @@ def test_positive_check_installer_hammer_ping(target_sat): :customerscenario: true :expectedresults: All services are active (running) - - :CaseLevel: System """ # check status reported by hammer ping command result = target_sat.execute('hammer ping') @@ -1672,8 +1663,6 @@ def test_satellite_installation_on_ipv6(): 4: Satellite service restart should work. 5: After system reboot all the services comes to up state. - :CaseLevel: System - :CaseAutomation: NotAutomated """ @@ -1695,8 +1684,6 @@ def test_capsule_installation_on_ipv6(): 3. Satellite service restart should work. 4. After system reboot all the services come to up state. - :CaseLevel: System - :CaseAutomation: NotAutomated """ @@ -1719,8 +1706,6 @@ def test_installer_check_on_ipv6(): 1. Tuning parameter set successfully for medium size. 2. custom-hiera.yaml related changes should be successfully applied. - :CaseLevel: System - :CaseAutomation: NotAutomated """ @@ -1746,8 +1731,6 @@ def test_installer_cap_pub_directory_accessibility(capsule_configured): :CaseImportance: High - :CaseLevel: System - :BZ: 1860519 :customerscenario: true @@ -1802,6 +1785,5 @@ def test_satellite_installation(installer_satellite): 4. satellite-maintain health check runs successfully :CaseImportance: Critical - """ common_sat_install_assertions(installer_satellite) diff --git a/tests/foreman/longrun/test_inc_updates.py b/tests/foreman/longrun/test_inc_updates.py index 12c32a61c7c..652f1a20e56 100644 --- a/tests/foreman/longrun/test_inc_updates.py +++ b/tests/foreman/longrun/test_inc_updates.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts-Content :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from datetime import datetime, timedelta @@ -206,8 +201,6 @@ def test_positive_noapply_api( Content view has a newer version :parametrized: yes - - :CaseLevel: System """ # Promote CV to new LCE versions = sorted(module_cv.read().version, key=lambda ver: ver.id) diff --git a/tests/foreman/longrun/test_oscap.py b/tests/foreman/longrun/test_oscap.py index 31b772f63c8..fa3654f8a48 100644 --- a/tests/foreman/longrun/test_oscap.py +++ b/tests/foreman/longrun/test_oscap.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: SCAPPlugin :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from broker import Broker from fauxfactory import gen_string @@ -344,7 +339,7 @@ def test_positive_has_arf_report_summary_page(): :id: 25be7898-50c5-4825-adc7-978c7b4e3488 - :Steps: + :steps: 1. Make sure the oscap report with it's corresponding hostname is visible in the UI. 2. Click on the host name to access the oscap report. @@ -352,8 +347,6 @@ def test_positive_has_arf_report_summary_page(): :expectedresults: Oscap ARF reports should have summary page. :CaseAutomation: NotAutomated - - :CaseLevel: System """ @@ -364,7 +357,7 @@ def test_positive_view_full_report_button(): :id: 5a41916d-66db-4d2f-8261-b83f833189b9 - :Steps: + :steps: 1. Make sure the oscap report with it's corresponding hostname is visible in the UI. 2. Click on the host name to access the oscap report. @@ -373,8 +366,6 @@ def test_positive_view_full_report_button(): actual HTML report. :CaseAutomation: NotAutomated - - :CaseLevel: System """ @@ -386,7 +377,7 @@ def test_positive_download_xml_button(): :id: 07a5f495-a702-4ca4-b5a4-579a133f9181 - :Steps: + :steps: 1. Make sure the oscap report with it's corresponding hostname is visible in the UI. 2. Click on the host name to access the oscap report. @@ -395,8 +386,6 @@ def test_positive_download_xml_button(): the xml report. :CaseAutomation: NotAutomated - - :CaseLevel: System """ @@ -408,15 +397,13 @@ def test_positive_select_oscap_proxy(): :id: d56576c8-6fab-4af6-91c1-6a56d9cca94b - :Steps: Choose the Oscap Proxy/capsule appropriately for the host or + :steps: Choose the Oscap Proxy/capsule appropriately for the host or host-groups. :expectedresults: Should have an Oscap-Proxy select box while filling hosts and host-groups form. :CaseAutomation: NotAutomated - - :CaseLevel: System """ @@ -427,7 +414,7 @@ def test_positive_delete_multiple_arf_reports(): :id: c1a8ce02-f42f-4c48-893d-8f31432b5520 - :Steps: + :steps: 1. Run Oscap scans are run for multiple Hosts. 2. Make sure the oscap reports with it's corresponding hostnames are visible in the UI. @@ -437,8 +424,6 @@ def test_positive_delete_multiple_arf_reports(): :expectedresults: Multiple Oscap ARF reports can be deleted. :CaseAutomation: NotAutomated - - :CaseLevel: System """ @@ -452,8 +437,6 @@ def test_positive_reporting_emails_of_oscap_reports(): :expectedresults: Whether email reporting of oscap reports is possible. :CaseAutomation: NotAutomated - - :CaseLevel: System """ diff --git a/tests/foreman/maintain/test_advanced.py b/tests/foreman/maintain/test_advanced.py index 5b105b4c0bb..2771994d568 100644 --- a/tests/foreman/maintain/test_advanced.py +++ b/tests/foreman/maintain/test_advanced.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ForemanMaintain :Team: Platform -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import pytest import yaml diff --git a/tests/foreman/maintain/test_backup_restore.py b/tests/foreman/maintain/test_backup_restore.py index 6f6817730b1..a845e71a166 100644 --- a/tests/foreman/maintain/test_backup_restore.py +++ b/tests/foreman/maintain/test_backup_restore.py @@ -4,17 +4,13 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ForemanMaintain :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No + """ import re diff --git a/tests/foreman/maintain/test_health.py b/tests/foreman/maintain/test_health.py index 2dcd93cc8d0..200928fe943 100644 --- a/tests/foreman/maintain/test_health.py +++ b/tests/foreman/maintain/test_health.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ForemanMaintain :Team: Platform -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import time diff --git a/tests/foreman/maintain/test_maintenance_mode.py b/tests/foreman/maintain/test_maintenance_mode.py index f72ac4468d0..3cc340f774b 100644 --- a/tests/foreman/maintain/test_maintenance_mode.py +++ b/tests/foreman/maintain/test_maintenance_mode.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ForemanMaintain :Team: Platform -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import pytest import yaml diff --git a/tests/foreman/maintain/test_offload_DB.py b/tests/foreman/maintain/test_offload_DB.py index e9913918f96..ca0f64f87d8 100644 --- a/tests/foreman/maintain/test_offload_DB.py +++ b/tests/foreman/maintain/test_offload_DB.py @@ -4,17 +4,12 @@ :CaseAutomation: ManualOnly -:CaseLevel: Integration - :CaseComponent: ForemanMaintain :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -28,12 +23,10 @@ def test_offload_internal_db_to_external_db_host(): :id: d07235c8-4584-469a-a87d-ace4dadb0a1f - :Steps: Run satellite-installer with foreman, candlepin and pulpcore options + :steps: Run satellite-installer with foreman, candlepin and pulpcore options referring to external DB host :expectedresults: Installed successful, all services running - :CaseLevel: Integration - :CaseComponent: Installer """ diff --git a/tests/foreman/maintain/test_packages.py b/tests/foreman/maintain/test_packages.py index ba288549ab0..9931d144352 100644 --- a/tests/foreman/maintain/test_packages.py +++ b/tests/foreman/maintain/test_packages.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ForemanMaintain :Team: Platform -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import pytest diff --git a/tests/foreman/maintain/test_service.py b/tests/foreman/maintain/test_service.py index 5e9f24c6510..dc0b6366e8b 100644 --- a/tests/foreman/maintain/test_service.py +++ b/tests/foreman/maintain/test_service.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ForemanMaintain :Team: Platform -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ from fauxfactory import gen_string import pytest diff --git a/tests/foreman/maintain/test_upgrade.py b/tests/foreman/maintain/test_upgrade.py index a087bda028f..cb855208e44 100644 --- a/tests/foreman/maintain/test_upgrade.py +++ b/tests/foreman/maintain/test_upgrade.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ForemanMaintain :Team: Platform -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import pytest diff --git a/tests/foreman/sanity/test_bvt.py b/tests/foreman/sanity/test_bvt.py index 7fe5dc77f02..7da1ccc3d8e 100644 --- a/tests/foreman/sanity/test_bvt.py +++ b/tests/foreman/sanity/test_bvt.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: BVT :Team: JPL -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import re @@ -76,8 +71,6 @@ def test_all_interfaces_are_accessible(target_sat): :id: 0a212120-8e49-4489-a1a4-4272004e16dc :expectedresults: All three satellite interfaces are accessible - - """ errors = {} # API Interface diff --git a/tests/foreman/sys/test_dynflow.py b/tests/foreman/sys/test_dynflow.py index 5a0acaf0e54..f08f07d4f8d 100644 --- a/tests/foreman/sys/test_dynflow.py +++ b/tests/foreman/sys/test_dynflow.py @@ -2,19 +2,14 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Dynflow :Team: Endeavour :Requirement: Dynflow -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/foreman/sys/test_fam.py b/tests/foreman/sys/test_fam.py index aeab1f2cc4c..ebc9d155769 100644 --- a/tests/foreman/sys/test_fam.py +++ b/tests/foreman/sys/test_fam.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: System - -:TestType: Functional - :CaseImportance: High :CaseComponent: AnsibleCollection :Team: Platform -:Upstream: No """ import pytest @@ -46,7 +41,6 @@ def test_positive_ansible_modules_installation(target_sat): :expectedresults: ansible-collection-redhat-satellite package is available and supported modules are contained - """ # list installed modules result = target_sat.execute(f'ls {FAM_MODULE_PATH} | grep .py$ | sed "s/.[^.]*$//"') @@ -72,7 +66,6 @@ def test_positive_import_run_roles(sync_roles, target_sat): :id: d3379fd3-b847-43ce-a51f-c02170e7b267 :expectedresults: fam roles import and run successfully - """ roles = sync_roles.get('roles') target_sat.cli.Host.ansible_roles_assign({'ansible-roles': roles, 'name': target_sat.hostname}) diff --git a/tests/foreman/sys/test_katello_certs_check.py b/tests/foreman/sys/test_katello_certs_check.py index 3ce28168e46..5bfeed3eee5 100644 --- a/tests/foreman/sys/test_katello_certs_check.py +++ b/tests/foreman/sys/test_katello_certs_check.py @@ -4,18 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: System - :CaseComponent: Certificates :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No - """ import re diff --git a/tests/foreman/sys/test_pulp3_filesystem.py b/tests/foreman/sys/test_pulp3_filesystem.py index 1e868cd691f..af19af65f92 100644 --- a/tests/foreman/sys/test_pulp3_filesystem.py +++ b/tests/foreman/sys/test_pulp3_filesystem.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: System - :CaseComponent: Pulp :team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from datetime import datetime import json diff --git a/tests/foreman/ui/test_acs.py b/tests/foreman/ui/test_acs.py index 9705bc0cd8f..a657d44719f 100644 --- a/tests/foreman/ui/test_acs.py +++ b/tests/foreman/ui/test_acs.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: AlternateContentSources :Team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/foreman/ui/test_activationkey.py b/tests/foreman/ui/test_activationkey.py index 19e163c6cb8..79e9dbfa655 100644 --- a/tests/foreman/ui/test_activationkey.py +++ b/tests/foreman/ui/test_activationkey.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ActivationKeys :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random @@ -40,8 +35,6 @@ def test_positive_end_to_end_crud(session, module_org): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ name = gen_string('alpha') @@ -91,8 +84,6 @@ def test_positive_end_to_end_register( :expectedresults: Content host was registered successfully using activation key, association is reflected on webUI - :CaseLevel: System - :parametrized: yes :CaseImportance: High @@ -126,8 +117,6 @@ def test_positive_create_with_cv(session, module_org, cv_name, target_sat): :parametrized: yes :expectedresults: Activation key is created - - :CaseLevel: Integration """ name = gen_string('alpha') env_name = gen_string('alpha') @@ -155,8 +144,6 @@ def test_positive_search_scoped(session, module_org, target_sat): :BZ: 1259374 - :CaseLevel: Integration - :CaseImportance: High """ name = gen_string('alpha') @@ -190,8 +177,6 @@ def test_positive_create_with_host_collection(session, module_org): :id: 0e4ad2b4-47a7-4087-828f-2b0535a97b69 :expectedresults: Activation key is created - - :CaseLevel: Integration """ name = gen_string('alpha') hc = entities.HostCollection(organization=module_org).create() @@ -211,8 +196,6 @@ def test_positive_create_with_envs(session, module_org, target_sat): :id: f75e994a-6da1-40a3-9685-f8387388b3f0 :expectedresults: Activation key is created - - :CaseLevel: Integration """ name = gen_string('alpha') cv_name = gen_string('alpha') @@ -241,8 +224,6 @@ def test_positive_add_host_collection_non_admin(module_org, test_name, target_sa listed :BZ: 1473212 - - :CaseLevel: Integration """ ak_name = gen_string('alpha') hc = entities.HostCollection(organization=module_org).create() @@ -277,8 +258,6 @@ def test_positive_remove_host_collection_non_admin(module_org, test_name, target :expectedresults: Activation key is created, removed host collection is not listed - - :CaseLevel: Integration """ ak_name = gen_string('alpha') hc = entities.HostCollection(organization=module_org).create() @@ -314,8 +293,6 @@ def test_positive_delete_with_env(session, module_org, target_sat): :id: b6019881-3d6e-4b75-89f5-1b62aff3b1ca :expectedresults: Activation key is deleted - - :CaseLevel: Integration """ name = gen_string('alpha') cv_name = gen_string('alpha') @@ -338,8 +315,6 @@ def test_positive_delete_with_cv(session, module_org, target_sat): :id: 7e40e1ed-8314-406b-9451-05f64806a6e6 :expectedresults: Activation key is deleted - - :CaseLevel: Integration """ name = gen_string('alpha') cv_name = gen_string('alpha') @@ -364,8 +339,6 @@ def test_positive_update_env(session, module_org, target_sat): :id: 895cda6a-bb1e-4b94-a858-95f0be78a17b :expectedresults: Activation key is updated - - :CaseLevel: Integration """ name = gen_string('alpha') cv_name = gen_string('alpha') @@ -395,14 +368,12 @@ def test_positive_update_cv(session, module_org, cv2_name, target_sat): :parametrized: yes - :Steps: + :steps: 1. Create Activation key 2. Update the Content view with another Content view which has custom products :expectedresults: Activation key is updated - - :CaseLevel: Integration """ name = gen_string('alpha') env1_name = gen_string('alpha') @@ -434,15 +405,13 @@ def test_positive_update_rh_product(function_entitlement_manifest_org, session, :id: 9b0ac209-45de-4cc4-97e8-e191f3f37239 - :Steps: + :steps: 1. Create an activation key 2. Update the content view with another content view which has RH products :expectedresults: Activation key is updated - - :CaseLevel: Integration """ name = gen_string('alpha') env1_name = gen_string('alpha') @@ -491,8 +460,6 @@ def test_positive_add_rh_product(function_entitlement_manifest_org, session, tar :id: d805341b-6d2f-4e16-8cb4-902de00b9a6c :expectedresults: RH products are successfully associated to Activation key - - :CaseLevel: Integration """ name = gen_string('alpha') cv_name = gen_string('alpha') @@ -528,8 +495,6 @@ def test_positive_add_custom_product(session, module_org, target_sat): :expectedresults: Custom products are successfully associated to Activation key - - :CaseLevel: Integration """ name = gen_string('alpha') cv_name = gen_string('alpha') @@ -561,15 +526,13 @@ def test_positive_add_rh_and_custom_products( :id: 3d8876fa-1412-47ca-a7a4-bce2e8baf3bc - :Steps: + :steps: 1. Create Activation key 2. Associate RH product(s) to Activation Key 3. Associate custom product(s) to Activation Key :expectedresults: RH/Custom product is successfully associated to Activation key - - :CaseLevel: Integration """ name = gen_string('alpha') rh_repo = { @@ -626,8 +589,6 @@ def test_positive_fetch_product_content(target_sat, function_entitlement_manifes assigned as Activation Key's product content :BZ: 1426386, 1432285 - - :CaseLevel: Integration """ org = function_entitlement_manifest_org rh_repo_id = target_sat.api_factory.enable_rhrepo_and_fetchid( @@ -675,8 +636,6 @@ def test_positive_access_non_admin_user(session, test_name): admin user :BZ: 1463813 - - :CaseLevel: Integration """ ak_name = gen_string('alpha') non_searchable_ak_name = gen_string('alpha') @@ -787,8 +746,6 @@ def test_positive_add_docker_repo_cv(session, module_org): :expectedresults: Content view with docker repo can be added to activation key - - :CaseLevel: Integration """ lce = entities.LifecycleEnvironment(organization=module_org).create() repo = entities.Repository( @@ -823,8 +780,6 @@ def test_positive_add_docker_repo_ccv(session, module_org): :expectedresults: Docker-based content view can be added to activation key - - :CaseLevel: Integration """ lce = entities.LifecycleEnvironment(organization=module_org).create() repo = entities.Repository( @@ -861,7 +816,7 @@ def test_positive_add_host(session, module_org, rhel6_contenthost, target_sat): :id: 886e9ea5-d917-40e0-a3b1-41254c4bf5bf - :Steps: + :steps: 1. Create Activation key 2. Create different hosts 3. Associate the hosts to Activation key @@ -869,8 +824,6 @@ def test_positive_add_host(session, module_org, rhel6_contenthost, target_sat): :expectedresults: Hosts are successfully associated to Activation key :parametrized: yes - - :CaseLevel: System """ ak = entities.ActivationKey( environment=entities.LifecycleEnvironment( @@ -897,7 +850,7 @@ def test_positive_delete_with_system(session, rhel6_contenthost, target_sat): :id: 86cd070e-cf46-4bb1-b555-e7cb42e4dc9f - :Steps: + :steps: 1. Create an Activation key 2. Register systems to it 3. Delete the Activation key @@ -905,8 +858,6 @@ def test_positive_delete_with_system(session, rhel6_contenthost, target_sat): :expectedresults: Activation key is deleted :parametrized: yes - - :CaseLevel: System """ name = gen_string('alpha') cv_name = gen_string('alpha') @@ -939,7 +890,7 @@ def test_negative_usage_limit(session, module_org, target_sat): :id: 9fe2d661-66f8-46a4-ae3f-0a9329494bdd - :Steps: + :steps: 1. Create Activation key 2. Update Usage Limit to a finite number 3. Register Systems to match the Usage Limit @@ -947,8 +898,6 @@ def test_negative_usage_limit(session, module_org, target_sat): Limit :expectedresults: System Registration fails. Appropriate error shown - - :CaseLevel: System """ name = gen_string('alpha') hosts_limit = '1' @@ -982,8 +931,6 @@ def test_positive_add_multiple_aks_to_system(session, module_org, rhel6_contenth :expectedresults: Multiple Activation keys are attached to a system :parametrized: yes - - :CaseLevel: System """ key_1_name = gen_string('alpha') key_2_name = gen_string('alpha') @@ -1046,8 +993,6 @@ def test_positive_host_associations(session, target_sat): :customerscenario: true :BZ: 1344033, 1372826, 1394388 - - :CaseLevel: System """ org = entities.Organization().create() org_entities = target_sat.cli_factory.setup_org_for_a_custom_repo( @@ -1110,8 +1055,6 @@ def test_positive_service_level_subscription_with_custom_product( :BZ: 1394357 :parametrized: yes - - :CaseLevel: System """ org = function_entitlement_manifest_org entities_ids = target_sat.cli_factory.setup_org_for_a_custom_repo( @@ -1155,15 +1098,13 @@ def test_positive_delete_manifest(session, function_entitlement_manifest_org): :id: 512d8e41-b937-451e-a9c6-840457d3d7d4 - :Steps: + :steps: 1. Create Activation key 2. Associate a manifest to the Activation Key 3. Delete the manifest :expectedresults: Deleting a manifest removes it from the Activation key - - :CaseLevel: Integration """ org = function_entitlement_manifest_org # Create activation key @@ -1202,7 +1143,7 @@ def test_positive_ak_with_custom_product_on_rhel6(session, rhel6_contenthost, ta :customerscenario: true - :Steps: + :steps: 1. Create a custom repo 2. Create ak and add custom repo to ak 3. Add subscriptions to the ak @@ -1211,8 +1152,6 @@ def test_positive_ak_with_custom_product_on_rhel6(session, rhel6_contenthost, ta :expectedresults: Host is registered successfully :bz: 2038388 - - :CaseLevel: Integration """ org = target_sat.api.Organization().create() entities_ids = target_sat.cli_factory.setup_org_for_a_custom_repo( diff --git a/tests/foreman/ui/test_ansible.py b/tests/foreman/ui/test_ansible.py index 378552b5dc5..a7a2ed28c06 100644 --- a/tests/foreman/ui/test_ansible.py +++ b/tests/foreman/ui/test_ansible.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Ansible :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -29,7 +24,7 @@ def test_positive_create_and_delete_variable(target_sat): :id: 7006d7c7-788a-4447-a564-d6b03ec06aaf - :Steps: + :steps: 1. Import Ansible roles if none have been imported yet. 2. Create an Ansible variable with only a name and an assigned Ansible role. @@ -61,7 +56,7 @@ def test_positive_create_variable_with_overrides(target_sat): :id: 90acea37-4c2f-42e5-92a6-0c88148f4fb6 - :Steps: + :steps: 1. Import Ansible roles if none have been imported yet. 2. Create an Anible variable, populating all fields on the creation form. @@ -167,7 +162,7 @@ def test_positive_ansible_custom_role(target_sat, session, module_org, rhel_cont :customerscenario: true - :Steps: + :steps: 1. Register a content host with satellite 2. Create a custom role and import into satellite 3. Assign that role to a host @@ -249,9 +244,7 @@ def test_positive_host_role_information(target_sat, function_host): :id: 7da913ef-3b43-4bfa-9a45-d895431c8b56 - :CaseLevel: System - - :Steps: + :steps: 1. Register a RHEL host to Satellite. 2. Import all roles available by default. 3. Assign one role to the RHEL host. @@ -259,7 +252,6 @@ def test_positive_host_role_information(target_sat, function_host): 5. Select the 'Ansible' tab, then the 'Inventory' sub-tab. :expectedresults: Roles assigned directly to the Host are visible on the subtab. - """ SELECTED_ROLE = 'RedHatInsights.insights-client' @@ -289,9 +281,7 @@ def test_positive_role_variable_information(self): :id: 4ab2813a-6b83-4907-b104-0473465814f5 - :CaseLevel: System - - :Steps: + :steps: 1. Register a RHEL host to Satellite. 2. Import all roles available by default. 3. Create a host group and assign one of the Ansible roles to the host group. @@ -303,7 +293,6 @@ def test_positive_role_variable_information(self): 9. Select the 'Ansible' tab, then the 'Variables' sub-tab. :expectedresults: The variables information for the given Host is visible. - """ @@ -314,9 +303,7 @@ def test_positive_assign_role_in_new_ui(self): :id: 044f38b4-cff2-4ddc-b93c-7e9f2826d00d - :CaseLevel: System - - :Steps: + :steps: 1. Register a RHEL host to Satellite. 2. Import all roles available by default. 3. Navigate to the new UI for the given Host. @@ -325,7 +312,6 @@ def test_positive_assign_role_in_new_ui(self): 6. Using the popup, assign a role to the Host. :expectedresults: The Role is successfully assigned to the Host, and shows up on the UI - """ @@ -336,9 +322,7 @@ def test_positive_remove_role_in_new_ui(self): :id: d6de5130-45f6-4349-b490-fbde2aed082c - :CaseLevel: System - - :Steps: + :steps: 1. Register a RHEL host to Satellite. 2. Import all roles available by default. 3. Assign a role to the host. @@ -349,5 +333,4 @@ def test_positive_remove_role_in_new_ui(self): :expectedresults: The Role is successfully removed from the Host, and no longer shows up on the UI - """ diff --git a/tests/foreman/ui/test_architecture.py b/tests/foreman/ui/test_architecture.py index bafaa89f78f..045d7f06cc6 100644 --- a/tests/foreman/ui/test_architecture.py +++ b/tests/foreman/ui/test_architecture.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - :CaseImportance: Low -:Upstream: No """ from fauxfactory import gen_string from nailgun import entities @@ -30,8 +25,6 @@ def test_positive_end_to_end(session): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ name = gen_string('alpha') diff --git a/tests/foreman/ui/test_audit.py b/tests/foreman/ui/test_audit.py index 822f7bcb1bf..5f6b87780d5 100644 --- a/tests/foreman/ui/test_audit.py +++ b/tests/foreman/ui/test_audit.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Medium - :CaseComponent: AuditLog :Team: Endeavour -:TestType: Functional - -:Upstream: No """ from fauxfactory import gen_string from nailgun import entities @@ -40,8 +35,6 @@ def test_positive_create_event(session, module_org, module_location): :CaseAutomation: Automated - :CaseLevel: Integration - :CaseImportance: Medium :BZ: 1730360 @@ -87,8 +80,6 @@ def test_positive_audit_comment(session, module_org): :CaseAutomation: Automated - :CaseLevel: Component - :CaseImportance: Low """ name = gen_string('alpha') @@ -122,8 +113,6 @@ def test_positive_update_event(session, module_org): :CaseAutomation: Automated - :CaseLevel: Integration - :CaseImportance: Medium :bz: 2222890 @@ -156,8 +145,6 @@ def test_positive_delete_event(session, module_org): :CaseAutomation: Automated - :CaseLevel: Component - :CaseImportance: Medium """ architecture = entities.Architecture().create() @@ -183,8 +170,6 @@ def test_positive_add_event(session, module_org): :CaseAutomation: Automated - :CaseLevel: Integration - :CaseImportance: Medium """ cv = entities.ContentView(organization=module_org).create() diff --git a/tests/foreman/ui/test_bookmarks.py b/tests/foreman/ui/test_bookmarks.py index 180a0bf205e..94e10d4f911 100644 --- a/tests/foreman/ui/test_bookmarks.py +++ b/tests/foreman/ui/test_bookmarks.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Search :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from airgun.exceptions import NoSuchElementException from airgun.session import Session @@ -71,8 +66,6 @@ def test_positive_end_to_end(session, ui_entity): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ name = gen_string('alpha') @@ -105,7 +98,7 @@ def test_positive_create_bookmark_public(session, ui_entity, default_viewer_role :Setup: Create a non-admin user with 'viewer' role - :Steps: + :steps: 1. Navigate to the entity page 2. Choose "bookmark this search" from the search drop-down menu @@ -120,8 +113,6 @@ def test_positive_create_bookmark_public(session, ui_entity, default_viewer_role :expectedresults: No errors, public bookmarks is displayed for all users, non-public bookmark is displayed for creator but not for different user - - :CaseLevel: Integration """ public_name = gen_string('alphanumeric') nonpublic_name = gen_string('alphanumeric') @@ -151,7 +142,7 @@ def test_positive_update_bookmark_public( public and one private 2. Create a non-admin user with 'viewer' role - :Steps: + :steps: 1. Login to Satellite server (establish a UI session) as the pre-created user @@ -177,8 +168,6 @@ def test_positive_update_bookmark_public( :expectedresults: New public bookmark is listed, and the private one is hidden - :CaseLevel: Integration - :BZ: 2141187 :customerscenario: true @@ -222,15 +211,13 @@ def test_negative_delete_bookmark(ui_entity, default_viewer_role, test_name): 2. Create a non-admin user without destroy_bookmark role (e.g. viewer) - :Steps: + :steps: 1. Login to Satellite server (establish a UI session) as a non-admin user 2. List the bookmarks (Navigate to Administer -> Bookmarks) :expectedresults: The delete buttons are not displayed - - :CaseLevel: Integration """ bookmark = entities.Bookmark(controller=ui_entity['controller'], public=True).create() with Session( @@ -252,15 +239,13 @@ def test_negative_create_with_duplicate_name(session, ui_entity): 1. Create a bookmark of a random name with random query. - :Steps: + :steps: 1. Create new bookmark with duplicate name. :expectedresults: Bookmark can't be created, submit button is disabled :BZ: 1920566, 1992652 - - :CaseLevel: Integration """ query = gen_string('alphanumeric') bookmark = entities.Bookmark(controller=ui_entity['controller'], public=True).create() diff --git a/tests/foreman/ui/test_branding.py b/tests/foreman/ui/test_branding.py index 3067614e02a..a7e08659d8b 100644 --- a/tests/foreman/ui/test_branding.py +++ b/tests/foreman/ui/test_branding.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Branding :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from airgun.session import Session import pytest @@ -27,14 +22,12 @@ def test_verify_satellite_login_screen_info(target_sat): :id: f48110ad-29b4-49b1-972a-a70075a05732 - :Steps: Get the info from the login screen + :steps: Get the info from the login screen :expectedresults: 1. Correct Satellite version 2. If 'Beta' is present, should fail until a GA snap is received - :CaseLevel: System - :BZ: 1315849, 1367495, 1372436, 1502098, 1540710, 1582476, 1724738, 1959135, 2076979, 1687250, 1686540, 1742872, 1805642, 2105949 """ diff --git a/tests/foreman/ui/test_computeprofiles.py b/tests/foreman/ui/test_computeprofiles.py index 5aea38df2cf..31075e7c1fb 100644 --- a/tests/foreman/ui/test_computeprofiles.py +++ b/tests/foreman/ui/test_computeprofiles.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ComputeResources :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string from nailgun import entities @@ -30,8 +25,6 @@ def test_positive_end_to_end(session, module_location, module_org): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ name = gen_string('alpha') diff --git a/tests/foreman/ui/test_computeresource.py b/tests/foreman/ui/test_computeresource.py index 000f95951f8..7bd9f2552ce 100644 --- a/tests/foreman/ui/test_computeresource.py +++ b/tests/foreman/ui/test_computeresource.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Integration - :CaseComponent: ComputeResources :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from nailgun import entities import pytest @@ -62,8 +57,6 @@ def test_positive_end_to_end(session, rhev_data, module_org, module_location): :expectedresults: All expected CRUD actions finished successfully. - :CaseLevel: Integration - :CaseImportance: Critical """ name = gen_string('alpha') @@ -105,8 +98,6 @@ def test_positive_add_resource(session, rhev_data): :expectedresults: resource created successfully - :CaseLevel: Integration - :CaseImportance: Critical """ # Our RHEV testing uses custom cert which we specify manually. @@ -138,8 +129,6 @@ def test_positive_edit_resource_description(session, rhev_data): :parametrized: yes :expectedresults: resource updated successfully and has new description - - :CaseLevel: Integration """ name = gen_string('alpha') description = gen_string('alpha') @@ -173,8 +162,6 @@ def test_positive_list_resource_vms(session, rhev_data): :parametrized: yes :expectedresults: VMs listed for provided compute resource - - :CaseLevel: Integration """ name = gen_string('alpha') with session: @@ -205,8 +192,6 @@ def test_positive_resource_vm_power_management(session, rhev_data): :expectedresults: virtual machine is powered on or powered off depending on its initial state - - :CaseLevel: Integration """ name = gen_string('alpha') with session: @@ -249,8 +234,6 @@ def test_positive_VM_import(session, module_org, module_location, rhev_data): :expectedresults: VM is shown as Host in Foreman - :CaseLevel: Integration - :CaseImportance: Medium :BZ: 1636067 @@ -381,8 +364,6 @@ def test_positive_image_end_to_end(session, rhev_data, module_location, target_s :expectedresults: All expected CRUD actions finished successfully. - :CaseLevel: Integration - :CaseImportance: High """ cr_name = gen_string('alpha') diff --git a/tests/foreman/ui/test_computeresource_azurerm.py b/tests/foreman/ui/test_computeresource_azurerm.py index aab0bdf6c53..36228513ffa 100644 --- a/tests/foreman/ui/test_computeresource_azurerm.py +++ b/tests/foreman/ui/test_computeresource_azurerm.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ComputeResources-Azure :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -91,7 +86,6 @@ def test_positive_end_to_end_azurerm_ft_host_provision( sat_azure_loc, module_azure_hg, ): - """Provision Host with hostgroup and Compute-profile using finish template on AzureRm compute resource @@ -101,8 +95,6 @@ def test_positive_end_to_end_azurerm_ft_host_provision( 1. Host is provisioned. 2. Host is deleted Successfully. - :CaseLevel: System - :BZ: 1850934 """ hostname = f'test-{gen_string("alpha")}' @@ -175,7 +167,6 @@ def test_positive_azurerm_host_provision_ud( sat_azure_loc, module_azure_hg, ): - """Provision a Host with hostgroup and Compute-profile using cloud-init image on AzureRm compute resource @@ -185,8 +176,6 @@ def test_positive_azurerm_host_provision_ud( :CaseImportance: Critical - :CaseLevel: System - :BZ: 1850934 """ diff --git a/tests/foreman/ui/test_computeresource_ec2.py b/tests/foreman/ui/test_computeresource_ec2.py index 94c50f69547..8f575bd4a80 100644 --- a/tests/foreman/ui/test_computeresource_ec2.py +++ b/tests/foreman/ui/test_computeresource_ec2.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ComputeResources-EC2 :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string from nailgun import entities @@ -55,7 +50,7 @@ def test_positive_default_end_to_end_with_custom_profile( :id: 33f80a8f-2ecf-4f15-b0c3-aab5fe0ac8d3 - :Steps: + :steps: 1. Create an EC2 compute resource with default properties and taxonomies. 2. Update the compute resource name and add new taxonomies. @@ -65,8 +60,6 @@ def test_positive_default_end_to_end_with_custom_profile( :expectedresults: The EC2 compute resource is created, updated, compute profile associated and deleted. - :CaseLevel: Integration - :BZ: 1451626, 2032530 :CaseImportance: High @@ -163,8 +156,6 @@ def test_positive_create_ec2_with_custom_region(session, module_ec2_settings): :BZ: 1456942 - :CaseLevel: Integration - :CaseImportance: Critical """ cr_name = gen_string('alpha') diff --git a/tests/foreman/ui/test_computeresource_gce.py b/tests/foreman/ui/test_computeresource_gce.py index c4618bfb608..73f4fb11daf 100644 --- a/tests/foreman/ui/test_computeresource_gce.py +++ b/tests/foreman/ui/test_computeresource_gce.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ComputeResources-GCE :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import json import random @@ -43,7 +38,7 @@ def test_positive_default_end_to_end_with_custom_profile( :id: 59ffd83e-a984-4c22-b91b-cad055b4fbd7 - :Steps: + :steps: 1. Create an GCE compute resource with default properties. 2. Update the compute resource name and add new taxonomies. @@ -53,8 +48,6 @@ def test_positive_default_end_to_end_with_custom_profile( :expectedresults: The GCE compute resource is created, updated, compute profile associated and deleted. - :CaseLevel: Integration - :CaseImportance: Critical """ cr_name = gen_string('alpha') @@ -167,8 +160,6 @@ def test_positive_gce_provision_end_to_end( :id: 8d1877bb-fbc2-4969-a13e-e95e4df4f4cd :expectedresults: Host is provisioned successfully - - :CaseLevel: System """ name = f'test{gen_string("alpha", 4).lower()}' hostname = f'{name}.{gce_domain.name}' @@ -253,8 +244,6 @@ def test_positive_gce_cloudinit_provision_end_to_end( :id: 6ee63ec6-2e8e-4ed6-ae48-e68b078233c6 :expectedresults: Host is provisioned successfully - - :CaseLevel: System """ name = f'test{gen_string("alpha", 4).lower()}' hostname = f'{name}.{gce_domain.name}' diff --git a/tests/foreman/ui/test_computeresource_libvirt.py b/tests/foreman/ui/test_computeresource_libvirt.py index 2d05fa45bf3..6938edd2fbd 100644 --- a/tests/foreman/ui/test_computeresource_libvirt.py +++ b/tests/foreman/ui/test_computeresource_libvirt.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ComputeResources-libvirt :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from random import choice @@ -44,8 +39,6 @@ def test_positive_end_to_end(session, module_target_sat, module_org, module_loca :expectedresults: All expected CRUD actions finished successfully. - :CaseLevel: Integration - :CaseImportance: High :BZ: 1662164 @@ -144,8 +137,6 @@ def test_positive_provision_end_to_end( :expectedresults: Host is provisioned successfully - :CaseLevel: System - :customerscenario: true :BZ: 1243223, 2236693 diff --git a/tests/foreman/ui/test_computeresource_vmware.py b/tests/foreman/ui/test_computeresource_vmware.py index 6db84bf5e59..e85a406e141 100644 --- a/tests/foreman/ui/test_computeresource_vmware.py +++ b/tests/foreman/ui/test_computeresource_vmware.py @@ -8,13 +8,8 @@ :Team: Rocket -:CaseLevel: Acceptance - -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from math import floor, log10 from random import choice @@ -92,8 +87,6 @@ def test_positive_end_to_end(session, module_org, module_location): :id: 47fc9e77-5b22-46b4-a76c-3217434fde2f :expectedresults: All expected CRUD actions finished successfully. - - :CaseLevel: Integration """ cr_name = gen_string('alpha') new_cr_name = gen_string('alpha') @@ -172,8 +165,6 @@ def test_positive_retrieve_virtual_machine_list(session): 2. Go to "Virtual Machines" tab. :expectedresults: The Virtual machines should be displayed - - :CaseLevel: Integration """ cr_name = gen_string('alpha') vm_name = settings.vmware.vm_name @@ -202,8 +193,6 @@ def test_positive_image_end_to_end(session, target_sat): :id: 6b7949ef-c684-40aa-b181-11f8d4cd39c6 :expectedresults: All expected CRUD actions finished successfully. - - :CaseLevel: Integration """ cr_name = gen_string('alpha') image_name = gen_string('alpha') @@ -263,8 +252,6 @@ def test_positive_resource_vm_power_management(session): :id: faeabe45-5112-43a6-bde9-f869dfb26cf5 :expectedresults: virtual machine is powered on or powered off depending on its initial state - - :CaseLevel: Integration """ cr_name = gen_string('alpha') vm_name = settings.vmware.vm_name @@ -321,8 +308,6 @@ def test_positive_select_vmware_custom_profile_guest_os_rhel7(session): :expectedresults: Guest OS RHEL7 is selected successfully. :BZ: 1315277 - - :CaseLevel: Integration """ cr_name = gen_string('alpha') guest_os_name = 'Red Hat Enterprise Linux 7 (64-bit)' @@ -363,8 +348,6 @@ def test_positive_access_vmware_with_custom_profile(session): :expectedresults: The Compute Resource created and associated to compute profile (3-Large) with provided values. - - :CaseLevel: Integration """ cr_name = gen_string('alpha') data_store_summary_string = _get_vmware_datastore_summary_string() @@ -487,8 +470,6 @@ def test_positive_virt_card(session, target_sat, module_location, module_org): :expectedresults: Virtualization card appears in the new Host UI for the VM - :CaseLevel: Integration - :CaseImportance: Medium """ # create entities for hostgroup diff --git a/tests/foreman/ui/test_config_group.py b/tests/foreman/ui/test_config_group.py index ada7fc5b8c4..b38384d2977 100644 --- a/tests/foreman/ui/test_config_group.py +++ b/tests/foreman/ui/test_config_group.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Puppet :Team: Rocket -:TestType: Functional - :CaseImportance: Low -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -34,8 +29,6 @@ def test_positive_end_to_end(session_puppet_enabled_sat, module_puppet_class): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ name = gen_string('alpha') diff --git a/tests/foreman/ui/test_containerimagetag.py b/tests/foreman/ui/test_containerimagetag.py index a1e68bc7e61..86fb97ebab1 100644 --- a/tests/foreman/ui/test_containerimagetag.py +++ b/tests/foreman/ui/test_containerimagetag.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ContainerManagement-Content :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from nailgun import entities import pytest @@ -60,8 +55,6 @@ def test_positive_search(session, module_org, module_product, module_repository) :expectedresults: The docker image tag can be searched and found, details are read - :CaseLevel: Integration - :BZ: 2009069, 2242515 """ with session: diff --git a/tests/foreman/ui/test_contentcredentials.py b/tests/foreman/ui/test_contentcredentials.py index d53d84618f6..4fa3f519986 100644 --- a/tests/foreman/ui/test_contentcredentials.py +++ b/tests/foreman/ui/test_contentcredentials.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ContentCredentials :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -44,8 +39,6 @@ def test_positive_end_to_end(session, target_sat, module_org, gpg_content): :id: d1a8cc1b-a072-465b-887d-5bca0acd21c3 :expectedresults: All expected CRUD actions finished successfully - - :CaseLevel: Integration """ name = gen_string('alpha') new_name = gen_string('alpha') @@ -123,8 +116,6 @@ def test_positive_add_empty_product(session, target_sat, module_org, gpg_content :id: e18ae9f5-43d9-4049-92ca-1eafaca05096 :expectedresults: gpg key is associated with product - - :CaseLevel: Integration """ prod_name = gen_string('alpha') gpg_key = target_sat.api.GPGKey(content=gpg_content, organization=module_org).create() @@ -146,8 +137,6 @@ def test_positive_add_product_with_repo(session, target_sat, module_org, gpg_con :expectedresults: gpg key is associated with product as well as with the repository - - :CaseLevel: Integration """ name = gen_string('alpha') gpg_key = target_sat.api.GPGKey( @@ -181,8 +170,6 @@ def test_positive_add_product_with_repos(session, target_sat, module_org, gpg_co :id: 0edffad7-0ab4-4bef-b16b-f6c8de55b0dc :expectedresults: gpg key is properly associated with repositories - - :CaseLevel: Integration """ name = gen_string('alpha') gpg_key = target_sat.api.GPGKey( @@ -211,8 +198,6 @@ def test_positive_add_repo_from_product_with_repo(session, target_sat, module_or :expectedresults: gpg key is associated with the repository but not with the product - - :CaseLevel: Integration """ name = gen_string('alpha') gpg_key = target_sat.api.GPGKey( @@ -244,8 +229,6 @@ def test_positive_add_repo_from_product_with_repos(session, target_sat, module_o :expectedresults: gpg key is associated with one of the repositories but not with the product - - :CaseLevel: Integration """ name = gen_string('alpha') gpg_key = target_sat.api.GPGKey( @@ -280,8 +263,6 @@ def test_positive_add_product_using_repo_discovery(session, gpg_path): the repositories :BZ: 1210180, 1461804, 1595792 - - :CaseLevel: Integration """ name = gen_string('alpha') product_name = gen_string('alpha') @@ -327,8 +308,6 @@ def test_positive_add_product_and_search(session, target_sat, module_org, gpg_co gpg key 'Product' tab :BZ: 1411800 - - :CaseLevel: Integration """ name = gen_string('alpha') gpg_key = target_sat.api.GPGKey( @@ -364,8 +343,6 @@ def test_positive_update_key_for_product_using_repo_discovery(session, gpg_path) repository before/after update :BZ: 1210180, 1461804 - - :CaseLevel: Integration """ name = gen_string('alpha') new_name = gen_string('alpha') @@ -416,8 +393,6 @@ def test_positive_update_key_for_empty_product(session, target_sat, module_org, :expectedresults: gpg key is associated with product before/after update - - :CaseLevel: Integration """ name = gen_string('alpha') new_name = gen_string('alpha') @@ -448,8 +423,6 @@ def test_positive_update_key_for_product_with_repo(session, target_sat, module_o :expectedresults: gpg key is associated with product as well as with repository after update - - :CaseLevel: Integration """ name = gen_string('alpha') new_name = gen_string('alpha') @@ -482,8 +455,6 @@ def test_positive_update_key_for_product_with_repos(session, target_sat, module_ :expectedresults: gpg key is associated with product as well as with repositories after update - - :CaseLevel: Integration """ name = gen_string('alpha') new_name = gen_string('alpha') @@ -518,8 +489,6 @@ def test_positive_update_key_for_repo_from_product_with_repo( :expectedresults: gpg key is associated with repository after update but not with product. - - :CaseLevel: Integration """ name = gen_string('alpha') new_name = gen_string('alpha') @@ -557,8 +526,6 @@ def test_positive_update_key_for_repo_from_product_with_repos( :expectedresults: gpg key is associated with single repository after update but not with product - - :CaseLevel: Integration """ name = gen_string('alpha') new_name = gen_string('alpha') diff --git a/tests/foreman/ui/test_contenthost.py b/tests/foreman/ui/test_contenthost.py index 06ca2463850..b77d9fe4ef7 100644 --- a/tests/foreman/ui/test_contenthost.py +++ b/tests/foreman/ui/test_contenthost.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Hosts-Content :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from datetime import datetime, timedelta import re @@ -131,8 +126,6 @@ def test_positive_end_to_end(session, default_location, module_repos_collection_ :expectedresults: content host details are the same as expected, package and errata installation are successful - :CaseLevel: System - :parametrized: yes :CaseImportance: Critical @@ -245,8 +238,6 @@ def test_positive_end_to_end_bulk_update(session, default_location, vm, target_s :BZ: 1712069, 1838800 :parametrized: yes - - :CaseLevel: System """ hc_name = gen_string('alpha') description = gen_string('alpha') @@ -329,8 +320,6 @@ def test_positive_search_by_subscription_status(session, default_location, vm): :BZ: 1406855, 1498827, 1495271 :parametrized: yes - - :CaseLevel: System """ with session: session.location.select(default_location.name) @@ -380,8 +369,6 @@ def test_positive_toggle_subscription_status(session, default_location, vm): :BZ: 1836868 - :CaseLevel: System - :parametrized: yes :CaseImportance: Medium @@ -438,8 +425,6 @@ def test_negative_install_package(session, default_location, vm): :expectedresults: Task finished with warning :parametrized: yes - - :CaseLevel: System """ with session: session.location.select(default_location.name) @@ -475,8 +460,6 @@ def test_positive_remove_package(session, default_location, vm): :expectedresults: Package was successfully removed :parametrized: yes - - :CaseLevel: System """ vm.download_install_rpm(settings.repos.yum_6.url, FAKE_0_CUSTOM_PACKAGE) with session: @@ -514,8 +497,6 @@ def test_positive_upgrade_package(session, default_location, vm): :expectedresults: Package was successfully upgraded :parametrized: yes - - :CaseLevel: System """ vm.run(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}') with session: @@ -554,8 +535,6 @@ def test_positive_install_package_group(session, default_location, vm): :expectedresults: Package group was successfully installed :parametrized: yes - - :CaseLevel: System """ with session: session.location.select(default_location.name) @@ -595,8 +574,6 @@ def test_positive_remove_package_group(session, default_location, vm): :expectedresults: Package group was successfully removed :parametrized: yes - - :CaseLevel: System """ with session: session.location.select(default_location.name) @@ -640,8 +617,6 @@ def test_positive_search_errata_non_admin( listed :parametrized: yes - - :CaseLevel: System """ vm.run(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}') with Session( @@ -695,8 +670,6 @@ def test_positive_ensure_errata_applicability_with_host_reregistered(session, de :BZ: 1463818 :parametrized: yes - - :CaseLevel: System """ vm.run(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}') result = vm.run(f'rpm -q {FAKE_1_CUSTOM_PACKAGE}') @@ -755,8 +728,6 @@ def test_positive_host_re_registration_with_host_rename( :BZ: 1762793 :parametrized: yes - - :CaseLevel: System """ vm.run(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}') result = vm.run(f'rpm -q {FAKE_1_CUSTOM_PACKAGE}') @@ -824,8 +795,6 @@ def test_positive_check_ignore_facts_os_setting(session, default_location, vm, m :BZ: 1155704 :parametrized: yes - - :CaseLevel: System """ major = str(gen_integer(15, 99)) minor = str(gen_integer(1, 9)) @@ -894,8 +863,6 @@ def test_positive_virt_who_hypervisor_subscription_status( :BZ: 1336924, 1860928 :parametrized: yes - - :CaseLevel: System """ org = entities.Organization().create() lce = entities.LifecycleEnvironment(organization=org).create() @@ -979,8 +946,6 @@ def test_module_stream_actions_on_content_host(session, default_location, vm_mod :expectedresults: Remote execution for module actions should succeed. :parametrized: yes - - :CaseLevel: System """ stream_version = '5.21' run_remote_command_on_content_host('dnf -y upload-profile', vm_module_streams) @@ -1101,8 +1066,6 @@ def test_module_streams_customize_action(session, default_location, vm_module_st :expectedresults: Remote execution for module actions should be succeed. - :CaseLevel: System - :parametrized: yes :CaseImportance: Medium @@ -1169,8 +1132,6 @@ def test_install_modular_errata(session, default_location, vm_module_streams): :expectedresults: Modular Errata should get installed on content host. :parametrized: yes - - :CaseLevel: System """ stream_version = '0' module_name = 'kangaroo' @@ -1250,8 +1211,6 @@ def test_module_status_update_from_content_host_to_satellite( :expectedresults: module stream status should get updated in Satellite :parametrized: yes - - :CaseLevel: System """ module_name = 'walrus' stream_version = '0.71' @@ -1317,8 +1276,6 @@ def test_module_status_update_without_force_upload_package_profile( :expectedresults: module stream status should get updated in Satellite - :CaseLevel: System - :parametrized: yes :CaseImportance: Medium @@ -1401,8 +1358,6 @@ def test_module_stream_update_from_satellite(session, default_location, vm_modul :expectedresults: module stream should get updated. :parametrized: yes - - :CaseLevel: System """ module_name = 'duck' stream_version = '0' @@ -1479,8 +1434,6 @@ def test_syspurpose_attributes_empty(session, default_location, vm_module_stream :expectedresults: Syspurpose attrs are empty, and syspurpose status is set as 'Not specified' - :CaseLevel: System - :parametrized: yes :CaseImportance: High @@ -1521,8 +1474,6 @@ def test_set_syspurpose_attributes_cli(session, default_location, vm_module_stre :expectedresults: Syspurpose attributes set for the content host - :CaseLevel: System - :parametrized: yes :CaseImportance: High @@ -1568,8 +1519,6 @@ def test_unset_syspurpose_attributes_cli(session, default_location, vm_module_st :expectedresults: Syspurpose attributes are empty - :CaseLevel: System - :parametrized: yes :CaseImportance: High @@ -1619,8 +1568,6 @@ def test_syspurpose_matched(session, default_location, vm_module_streams): :expectedresults: Syspurpose status is Matched - :CaseLevel: System - :parametrized: yes :CaseImportance: High @@ -1662,8 +1609,6 @@ def test_syspurpose_bulk_action(session, default_location, vm): :expectedresults: Syspurpose parameters are set and reflected on the host - :CaseLevel: System - :CaseImportance: High """ syspurpose_attributes = { @@ -1708,8 +1653,6 @@ def test_syspurpose_mismatched(session, default_location, vm_module_streams): :expectedresults: Syspurpose status is 'Mismatched' - :CaseLevel: System - :parametrized: yes :CaseImportance: High @@ -1795,8 +1738,6 @@ def test_search_for_virt_who_hypervisors(session, default_location): :customerscenario: true - :CaseLevel: System - :CaseImportance: Medium """ org = entities.Organization().create() diff --git a/tests/foreman/ui/test_contentview.py b/tests/foreman/ui/test_contentview.py index 89e02be56fa..356c17e3fe0 100644 --- a/tests/foreman/ui/test_contentview.py +++ b/tests/foreman/ui/test_contentview.py @@ -7,17 +7,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: ContentViews :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import datetime from random import randint @@ -78,8 +73,6 @@ def test_positive_add_custom_content(session): :expectedresults: Custom content can be seen in a view - :CaseLevel: Integration - :CaseImportance: Critical """ org = entities.Organization().create() @@ -113,8 +106,6 @@ def test_positive_end_to_end(session, module_org, target_sat): :expectedresults: content view is created, updated with repo publish and promoted to next selected env - :CaseLevel: Integration - :CaseImportance: High """ repo_name = gen_string('alpha') @@ -150,8 +141,6 @@ def test_positive_publish_version_changes_in_source_env(session, module_org): :expectedresults: Content view version is updated in source environment. - :CaseLevel: Integration - :CaseImportance: High """ lce = entities.LifecycleEnvironment(organization=module_org).create() @@ -203,8 +192,6 @@ def test_positive_repo_count_for_composite_cv(session, module_org, target_sat): :BZ: 1431778 - :CaseLevel: Integration - :CaseImportance: High """ lce = entities.LifecycleEnvironment(organization=module_org).create() @@ -252,8 +239,6 @@ def test_positive_create_composite( :expectedresults: Composite content views are created - :CaseLevel: System - :CaseImportance: High """ org = module_entitlement_manifest_org @@ -306,8 +291,6 @@ def test_positive_add_rh_content(session, function_entitlement_manifest_org, tar :expectedresults: RH Content can be seen in a view - :CaseLevel: Integration - :CaseImportance: Critical """ cv_name = gen_string('alpha') @@ -339,8 +322,6 @@ def test_positive_add_docker_repo(session, module_org, module_prod): :expectedresults: The repo is added to a non-composite content view - :CaseLevel: Integration - :CaseImportance: High """ content_view = entities.ContentView(composite=False, organization=module_org).create() @@ -363,8 +344,6 @@ def test_positive_add_docker_repos(session, module_org, module_prod): :expectedresults: The repos are added to a non-composite content view. - :CaseLevel: Integration - :CaseImportance: Low """ content_view = entities.ContentView(composite=False, organization=module_org).create() @@ -392,8 +371,6 @@ def test_positive_add_synced_docker_repo(session, module_org, module_prod): :expectedresults: Synchronized docker repository was successfully added to content view. - :CaseLevel: Integration - :CaseImportance: High """ content_view = entities.ContentView(composite=False, organization=module_org).create() @@ -418,8 +395,6 @@ def test_positive_add_docker_repo_to_ccv(session, module_org, module_prod): :expectedresults: The repository is added to a content view which is then added to a composite content view. - :CaseLevel: Integration - :CaseImportance: High """ content_view = entities.ContentView(composite=False, organization=module_org).create() @@ -446,8 +421,6 @@ def test_positive_add_docker_repos_to_ccv(session, module_org, module_prod): :expectedresults: The repository is added to a random number of content views which are then added to a composite content view. - :CaseLevel: Integration - :CaseImportance: Low """ cvs = [] @@ -481,8 +454,6 @@ def test_positive_publish_with_docker_repo(session, module_org, module_prod): :expectedresults: The repo is added to a content view which is then successfully published. - :CaseLevel: Integration - :CaseImportance: High """ content_view = entities.ContentView(composite=False, organization=module_org).create() @@ -507,8 +478,6 @@ def test_positive_publish_with_docker_repo_composite(session, module_org, module which is then published only once and then added to a composite content view which is also published only once. - :CaseLevel: Integration - :CaseImportance: High """ repo = entities.Repository( @@ -536,8 +505,6 @@ def test_positive_publish_multiple_with_docker_repo(session, module_org, module_ :expectedresults: Content view with docker repo is successfully published multiple times. - :CaseLevel: Integration - :CaseImportance: Low """ repo = entities.Repository( @@ -561,8 +528,6 @@ def test_positive_publish_multiple_with_docker_repo_composite(session, module_or :expectedresults: Composite content view with docker repo is successfully published multiple times. - :CaseLevel: Integration - :CaseImportance: Low """ repo = entities.Repository( @@ -590,8 +555,6 @@ def test_positive_promote_with_docker_repo(session, module_org, module_prod): :expectedresults: Docker repository is promoted to content view found in the specific lifecycle-environment. - :CaseLevel: Integration - :CaseImportance: High """ lce = entities.LifecycleEnvironment(organization=module_org).create() @@ -618,8 +581,6 @@ def test_positive_promote_multiple_with_docker_repo(session, module_org, module_ :expectedresults: Docker repository is promoted to content view found in the specific lifecycle-environments. - :CaseLevel: Integration - :CaseImportance: Low """ repo = entities.Repository( @@ -647,8 +608,6 @@ def test_positive_promote_with_docker_repo_composite(session, module_org, module :expectedresults: Docker repository is promoted to content view found in the specific lifecycle-environment. - :CaseLevel: Integration - :CaseImportance: High """ lce = entities.LifecycleEnvironment(organization=module_org).create() @@ -681,8 +640,6 @@ def test_positive_promote_multiple_with_docker_repo_composite(session, module_or :expectedresults: Docker repository is promoted to content view found in the specific lifecycle-environments. - :CaseLevel: Integration - :CaseImportance: Low """ repo = entities.Repository( @@ -713,8 +670,6 @@ def test_negative_add_components_to_non_composite(session): :expectedresults: User cannot add components to the view - :CaseLevel: Integration - :CaseImportance: Low """ cv1_name = gen_string('alpha') @@ -743,8 +698,6 @@ def test_positive_add_unpublished_cv_to_composite(session): :expectedresults: Non-composite content view is added to composite one - :CaseLevel: Integration - :CaseImportance: Low :BZ: 1367123 @@ -787,8 +740,6 @@ def test_positive_add_non_composite_cv_to_composite(session): composite content view. 3. Composite content view is successfully published - :CaseLevel: Integration - :BZ: 1367123 :CaseImportance: High @@ -834,8 +785,6 @@ def test_positive_check_composite_cv_addition_list_versions(session): :expectedresults: second non-composite content view version should be listed as default one to be added to composite view - :CaseLevel: Integration - :BZ: 1411074 :CaseImportance: Low @@ -873,8 +822,6 @@ def test_negative_add_dupe_repos(session, module_org, target_sat): :expectedresults: User cannot add repos multiple times to the view - :CaseLevel: Integration - :CaseImportance: Low """ cv_name = gen_string('alpha') @@ -901,8 +848,6 @@ def test_positive_publish_with_custom_content(session, module_org, target_sat): :expectedresults: Content view can be published - :CaseLevel: Integration - :CaseImportance: Critical """ repo_name = gen_string('alpha') @@ -930,8 +875,6 @@ def test_positive_publish_with_rh_content(session, function_entitlement_manifest :expectedresults: Content view can be published - :CaseLevel: Integration - :CaseImportance: Critical """ cv_name = gen_string('alpha') @@ -970,8 +913,6 @@ def test_positive_publish_composite_with_custom_content( :expectedresults: Composite content view can be published - :CaseLevel: Integration - :CaseImportance: High """ cv1_name = gen_string('alpha') @@ -1062,8 +1003,6 @@ def test_positive_publish_version_changes_in_target_env(session, module_org, tar :expectedresults: Content view version is updated in target environment. - :CaseLevel: Integration - :CaseImportance: High """ cv_name = gen_string('alpha') @@ -1114,8 +1053,6 @@ def test_positive_promote_with_custom_content(session, module_org, target_sat): :expectedresults: Content view can be promoted - :CaseLevel: Integration - :BZ: 1361793 :CaseImportance: Critical @@ -1159,8 +1096,6 @@ def test_positive_promote_with_rh_content(session, function_entitlement_manifest :expectedresults: Content view can be promoted - :CaseLevel: System - :CaseImportance: Critical """ cv_name = gen_string('alpha') @@ -1202,8 +1137,6 @@ def test_positive_promote_composite_with_custom_content( :expectedresults: Composite content view can be promoted - :CaseLevel: Integration - :CaseImportance: High """ cv1_name = gen_string('alpha') @@ -1334,8 +1267,6 @@ def test_negative_add_same_package_filter_twice(session, module_org, target_sat) :expectedresults: Same package filter can not be added again - :CaseLevel: Integration - :CaseImportance: High """ cv_name = gen_string('alpha') @@ -1381,8 +1312,6 @@ def test_positive_remove_cv_version_from_default_env(session, module_org, target :expectedresults: content view version is removed from Library environment - :CaseLevel: Integration - :CaseImportance: Critical """ cv_name = gen_string('alpha') @@ -1424,8 +1353,6 @@ def test_positive_remove_promoted_cv_version_from_default_env(session, module_or 1. Content view version exist only in DEV and not in Library 2. The yum repos exists in content view version - :CaseLevel: Integration - :CaseImportance: High """ repo = target_sat.cli_factory.RepositoryCollection( @@ -1470,8 +1397,6 @@ def test_positive_remove_qe_promoted_cv_version_from_default_env(session, module :expectedresults: Content view version exist only in DEV, QE and not in Library - :CaseLevel: Integration - :CaseImportance: Low """ dev_lce = entities.LifecycleEnvironment(organization=module_org).create() @@ -1541,8 +1466,6 @@ def test_positive_remove_cv_version_from_env(session, module_org, repos_collecti :expectedresults: Content view version exist in Library, DEV, QE - :CaseLevel: Integration - :CaseImportance: High """ dev_lce = entities.LifecycleEnvironment(organization=module_org).create() @@ -1599,8 +1522,6 @@ def test_positive_delete_cv_promoted_to_multi_env(session, module_org, target_sa :expectedresults: The content view doesn't exists. - :CaseLevel: Integration - :CaseImportance:High """ repo = target_sat.cli_factory.RepositoryCollection( @@ -1634,8 +1555,6 @@ def test_positive_delete_composite_version(session, module_org, target_sat): :expectedresults: Deletion was performed successfully - :CaseLevel: Integration - :BZ: 1276479 :CaseImportance: High @@ -1672,8 +1591,6 @@ def test_positive_delete_non_default_version(session, target_sat): :expectedresults: Deletion was performed successfully - :CaseLevel: Integration - :CaseImportance: Critical """ repo_name = gen_string('alpha') @@ -1706,8 +1623,6 @@ def test_positive_delete_version_with_ak(session): :expectedresults: Delete operation was performed successfully - :CaseLevel: Integration - :CaseImportance: High """ org = entities.Organization().create() @@ -1747,8 +1662,6 @@ def test_positive_clone_within_same_env(session, module_org, target_sat): :BZ: 1461017 - :CaseLevel: Integration - :CaseImportance: High """ repo_name = gen_string('alpha') @@ -1780,8 +1693,6 @@ def test_positive_clone_within_diff_env(session, module_org, target_sat): :BZ: 1461017 - :CaseLevel: Integration - :CaseImportance: High """ repo_name = gen_string('alpha') @@ -1820,8 +1731,6 @@ def test_positive_remove_filter(session, module_org): :expectedresults: content views filter removed successfully - :CaseLevel: Integration - :CaseImportance: Low """ filter_name = gen_string('alpha') @@ -1849,8 +1758,6 @@ def test_positive_add_package_filter(session, module_org, target_sat): :expectedresults: content views filter created and selected packages can be added for inclusion - :CaseLevel: Integration - :CaseImportance: High """ packages = ( @@ -1893,8 +1800,6 @@ def test_positive_add_package_inclusion_filter_and_publish(session, module_org, :expectedresults: Package is included in content view version - :CaseLevel: Integration - :CaseImportance: High """ filter_name = gen_string('alpha') @@ -1939,8 +1844,6 @@ def test_positive_add_package_exclusion_filter_and_publish(session, module_org, :expectedresults: Package is excluded from content view version - :CaseLevel: Integration - :CaseImportance: High """ filter_name = gen_string('alpha') @@ -1986,8 +1889,6 @@ def test_positive_remove_package_from_exclusion_filter(session, module_org, targ :expectedresults: Package was successfully removed from content view filter and is present in next published content view version - :CaseLevel: Integration - :CaseImportance: High """ filter_name = gen_string('alpha') @@ -2035,8 +1936,6 @@ def test_positive_update_inclusive_filter_package_version(session, module_org, t :expectedresults: Version was updated, next content view version contains package with updated version - :CaseLevel: Integration - :CaseImportance: High """ filter_name = gen_string('alpha') @@ -2099,8 +1998,6 @@ def test_positive_update_exclusive_filter_package_version(session, module_org, t :expectedresults: Version was updated, next content view version contains package with updated version - :CaseLevel: Integration - :CaseImportance: High """ filter_name = gen_string('alpha') @@ -2215,8 +2112,6 @@ def test_positive_edit_rh_custom_spin(session, target_sat): :expectedresults: edited content view save is successful and info is updated - :CaseLevel: System - :CaseImportance: High """ filter_name = gen_string('alpha') @@ -2276,8 +2171,6 @@ def test_positive_promote_with_rh_custom_spin(session, target_sat): :expectedresults: Content view can be promoted - :CaseLevel: Integration - :CaseImportance: Critical """ filter_name = gen_string('alpha') @@ -2368,8 +2261,6 @@ def test_positive_add_errata_filter(session, module_org, target_sat): :expectedresults: content views filter created and selected errata-id can be added for inclusion/exclusion - :CaseLevel: Integration - :CaseImportance: High """ filter_name = gen_string('alpha') @@ -2407,8 +2298,6 @@ def test_positive_add_module_stream_filter(session, module_org, target_sat): :expectedresults: content views filter created and selected module stream can be added for inclusion/exclusion - :CaseLevel: Integration - :CaseImportance: High """ filter_name = gen_string('alpha') @@ -2448,8 +2337,6 @@ def test_positive_add_package_group_filter(session, module_org, target_sat): :expectedresults: content views filter created and selected package groups can be added for inclusion/exclusion - :CaseLevel: Integration - :CaseImportance: Low """ filter_name = gen_string('alpha') @@ -2484,8 +2371,6 @@ def test_positive_update_filter_affected_repos(session, module_org, target_sat): version publishing only updated repos are affected by content view filter - :CaseLevel: Integration - :CaseImportance: High """ filter_name = gen_string('alpha') @@ -2550,8 +2435,6 @@ def test_positive_search_composite(session): :BZ: 1259374 - :CaseLevel: Integration - :CaseImportance: Low """ composite_name = gen_string('alpha') @@ -2584,8 +2467,6 @@ def test_positive_publish_with_repo_with_disabled_http(session, module_org, targ :BZ: 1355752 - :CaseLevel: Integration - :CaseImportance: Low """ repo_name = gen_string('alpha') @@ -2631,8 +2512,6 @@ def test_positive_subscribe_system_with_custom_content( :expectedresults: Systems can be subscribed to content view(s) - :CaseLevel: Integration - :parametrized: yes :CaseImportance: High @@ -2667,8 +2546,6 @@ def test_positive_delete_with_kickstart_repo_and_host_group( :BZ: 1417072 - :CaseLevel: Integration - :CaseImportance: High """ hg_name = gen_string('alpha') @@ -2744,8 +2621,6 @@ def test_positive_rh_mixed_content_end_to_end( :expectedresults: CV should be published and promoted with RH OSTree and all other contents. Then version is removed successfully. - :CaseLevel: System - :customerscenario: true :CaseImportance: High @@ -2800,7 +2675,7 @@ def test_positive_errata_inc_update_list_package(session, target_sat): :CaseImportance: High - :CaseLevel: Integration + """ org = entities.Organization().create() product = entities.Product(organization=org).create() @@ -2888,7 +2763,6 @@ def test_positive_composite_child_inc_update(session, rhel7_contenthost, target_ :parametrized: yes - :CaseLevel: Integration """ org = entities.Organization().create() lce = entities.LifecycleEnvironment(organization=org).create() @@ -2982,8 +2856,6 @@ def test_positive_module_stream_end_to_end(session, module_org, target_sat): :expectedresults: Content view works properly with module_streams and count shown should be correct - :CaseLevel: Integration - :CaseImportance: Medium """ repo_name = gen_string('alpha') @@ -3025,8 +2897,6 @@ def test_positive_search_module_streams_in_content_view(session, module_org, tar :expectedresults: Searching for module streams should work inside content view version - :CaseLevel: Integration - :CaseImportance: Low """ repo_name = gen_string('alpha') @@ -3069,8 +2939,6 @@ def test_positive_non_admin_user_actions(session, module_org, test_name, target_ :BZ: 1461017 - :CaseLevel: Integration - :CaseImportance: Critical """ # note: the user to be created should not have permissions to access @@ -3160,8 +3028,6 @@ def test_positive_readonly_user_actions(module_org, test_name, target_sat): :expectedresults: User with read-only role for content view can view the repository in the content view - :CaseLevel: Integration - :CaseImportance: Critical """ user_login = gen_string('alpha') @@ -3214,8 +3080,6 @@ def test_negative_read_only_user_actions(session, module_org, test_name, target_ :BZ: 1922134 - :CaseLevel: Integration - :CaseImportance: Critical """ # create a content view read only user with lifecycle environment @@ -3318,8 +3182,6 @@ def test_negative_non_readonly_user_actions(module_org, test_name, target_sat): :expectedresults: the user cannot access content views web resources - :CaseLevel: Integration - :CaseImportance: High """ user_login = gen_string('alpha') @@ -3740,7 +3602,6 @@ def test_positive_no_duplicate_key_violate_unique_constraint_using_filters( :CaseImportance: Medium - :CaseLevel: Integration """ cv = gen_string('alpha') filter_name = gen_string('alpha') diff --git a/tests/foreman/ui/test_dashboard.py b/tests/foreman/ui/test_dashboard.py index f7ab199ce48..ca93bd08650 100644 --- a/tests/foreman/ui/test_dashboard.py +++ b/tests/foreman/ui/test_dashboard.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Dashboard :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from airgun.session import Session from nailgun import entities @@ -35,7 +30,7 @@ def test_positive_host_configuration_status(session): :customerscenario: true - :Steps: + :steps: 1. Navigate to Monitor -> Dashboard 2. Review the Host Configuration Status @@ -45,8 +40,6 @@ def test_positive_host_configuration_status(session): :expectedresults: Each link shows the right info :BZ: 1631219 - - :CaseLevel: Integration """ org = entities.Organization().create() loc = entities.Location().create() @@ -106,15 +99,13 @@ def test_positive_host_configuration_chart(session): :id: b03314aa-4394-44e5-86da-c341c783003d - :Steps: + :steps: 1. Navigate to Monitor -> Dashboard 2. Review the Host Configuration Chart widget 3. Check that chart contains correct percentage value :expectedresults: Chart showing correct data - - :CaseLevel: Integration """ org = entities.Organization().create() loc = entities.Location().create() @@ -135,7 +126,7 @@ def test_positive_task_status(session): :id: fb667d6a-7255-4341-9f79-2f03d19e8e0f - :Steps: + :steps: 1. Navigate to Monitor -> Dashboard 2. Review the Latest Warning/Error Tasks widget @@ -148,8 +139,6 @@ def test_positive_task_status(session): from Tasks dashboard :BZ: 1718889 - - :CaseLevel: Integration """ url = 'www.non_existent_repo_url.org' org = entities.Organization().create() @@ -215,7 +204,7 @@ def test_positive_user_access_with_host_filter( :id: 24b4b371-cba0-4bc8-bc6a-294c62e0586d - :Steps: + :steps: 1. Specify proper filter with permission for your role 2. Create new user and assign role to it @@ -229,8 +218,6 @@ def test_positive_user_access_with_host_filter( :BZ: 1417114 :parametrized: yes - - :CaseLevel: System """ user_login = gen_string('alpha') user_password = gen_string('alphanumeric') @@ -280,12 +267,11 @@ def test_positive_user_access_with_host_filter( @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') def test_positive_sync_overview_widget(session, module_org, module_product): - """Check if the Sync Overview widget is working in the Dashboard UI :id: 553fbe33-0f6f-46fb-8d80-5d1d9ed483cf - :Steps: + :steps: 1. Sync some repositories 2. Navigate to Monitor -> Dashboard 3. Review the Sync Overview widget @@ -293,8 +279,6 @@ def test_positive_sync_overview_widget(session, module_org, module_product): :expectedresults: Correct data should appear in the widget :BZ: 1995424 - - :CaseLevel: Integration """ repo = entities.Repository(url=settings.repos.yum_1.url, product=module_product).create() with session: diff --git a/tests/foreman/ui/test_discoveredhost.py b/tests/foreman/ui/test_discoveredhost.py index 82b29d897a8..292ad94f7a4 100644 --- a/tests/foreman/ui/test_discoveredhost.py +++ b/tests/foreman/ui/test_discoveredhost.py @@ -8,11 +8,6 @@ :Team: Rocket -:TestType: Functional - -:CaseLevel: System - -:Upstream: No """ from fauxfactory import gen_string import pytest diff --git a/tests/foreman/ui/test_discoveryrule.py b/tests/foreman/ui/test_discoveryrule.py index d61d5067f85..e2b0fcef95e 100644 --- a/tests/foreman/ui/test_discoveryrule.py +++ b/tests/foreman/ui/test_discoveryrule.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: System - :CaseComponent: DiscoveryPlugin :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from airgun.session import Session from fauxfactory import gen_integer, gen_ipaddr, gen_string @@ -77,8 +72,6 @@ def test_positive_crud_with_non_admin_user( :id: 6a03983b-363d-4646-b277-34af5f5abc55 :expectedresults: All crud operations should work with non_admin user. - - :CaseLevel: Integration """ rule_name = gen_string('alpha') search = gen_string('alpha') @@ -138,8 +131,6 @@ def test_negative_delete_rule_with_non_admin_user( :expectedresults: User should validation error and rule should not be deleted successfully. - - :CaseLevel: Integration """ hg_name = gen_string('alpha') rule_name = gen_string('alpha') @@ -172,7 +163,7 @@ def test_positive_list_host_based_on_rule_search_query( :id: f7473fa2-7349-42d3-9cdb-f74b55d2f440 - :Steps: + :steps: 1. discovered host with cpu_count = 2 2. Define a rule 'rule1' with search query cpu_count = 2 diff --git a/tests/foreman/ui/test_domain.py b/tests/foreman/ui/test_domain.py index c082bef7d83..09e4864f07f 100644 --- a/tests/foreman/ui/test_domain.py +++ b/tests/foreman/ui/test_domain.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string from nailgun import entities @@ -40,8 +35,6 @@ def test_positive_set_parameter(session, valid_domain_name, param_value): :parametrized: yes :expectedresults: Domain parameter is created. - - :CaseLevel: Integration """ new_param = {'name': gen_string('alpha', 255), 'value': param_value} with session: @@ -62,8 +55,6 @@ def test_negative_set_parameter(session, valid_domain_name): :expectedresults: Domain parameter is not updated. Error is raised - :CaseLevel: Integration - :CaseImportance: Medium """ update_values = { @@ -85,8 +76,6 @@ def test_negative_set_parameter_same(session, valid_domain_name): :expectedresults: Domain parameter with same values is not created. - :CaseLevel: Integration - :CaseImportance: Medium """ param_name = gen_string('alpha') @@ -108,8 +97,6 @@ def test_positive_remove_parameter(session, valid_domain_name): :expectedresults: Domain parameter is removed - :CaseLevel: Integration - :CaseImportance: Medium """ param_name = gen_string('alpha') @@ -133,8 +120,6 @@ def test_positive_end_to_end(session, module_org, module_location, valid_domain_ :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ dns_domain_name = valid_domain_name diff --git a/tests/foreman/ui/test_errata.py b/tests/foreman/ui/test_errata.py index 3bec22806b8..f604e177bc2 100644 --- a/tests/foreman/ui/test_errata.py +++ b/tests/foreman/ui/test_errata.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ErrataManagement :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from airgun.session import Session from broker import Broker @@ -200,8 +195,6 @@ def test_end_to_end( :BZ: 2029192 :customerscenario: true - - :CaseLevel: System """ ERRATA_DETAILS = { 'advisory': 'RHSA-2012:0055', @@ -272,7 +265,7 @@ def test_content_host_errata_page_pagination(session, function_org_with_paramete :id: 6363eda7-a162-4a4a-b70f-75decbd8202e - :Steps: + :steps: 1. Install more than 20 packages that need errata 2. View Content Host's Errata page 3. Assert total_pages > 1 @@ -294,8 +287,6 @@ def test_content_host_errata_page_pagination(session, function_org_with_paramete :customerscenario: true :BZ: 1662254, 1846670 - - :CaseLevel: System """ org = function_org_with_parameter @@ -359,15 +350,13 @@ def test_positive_list(session, function_org_with_parameter, lce, target_sat): :Setup: Errata synced on satellite server. - :Steps: Create two Orgs each having a product synced which contains errata. + :steps: Create two Orgs each having a product synced which contains errata. :expectedresults: Check that the errata belonging to one Org is not showing in the other. :BZ: 1659941, 1837767 :customerscenario: true - - :CaseLevel: Integration """ org = function_org_with_parameter rc = target_sat.cli_factory.RepositoryCollection( @@ -413,12 +402,10 @@ def test_positive_list_permission( 2. Make sure that they both have errata. 3. Create a user with view access on one product and not on the other. - :Steps: Go to Content -> Errata. + :steps: Go to Content -> Errata. :expectedresults: Check that the new user is able to see errata for one product only. - - :CaseLevel: Integration """ module_org = module_org_with_parameter role = entities.Role().create() @@ -470,15 +457,13 @@ def test_positive_apply_for_all_hosts( :customerscenario: true - :Steps: + :steps: 1. Go to Content -> Errata. Select an erratum -> Content Hosts tab. 2. Select all Content Hosts and apply the erratum. :expectedresults: Check that the erratum is applied in all the content hosts. - - :CaseLevel: System """ with Broker( nick=module_repos_collection_with_setup.distro, host_class=ContentHost, _count=2 @@ -524,14 +509,12 @@ def test_positive_view_cve(session, module_repos_collection_with_setup): :Setup: Errata synced on satellite server. - :Steps: Go to Content -> Errata. Select an Errata. + :steps: Go to Content -> Errata. Select an Errata. :expectedresults: 1. Check if the CVE information is shown in Errata Details page. 2. Check if 'N/A' is displayed if CVE information is not present. - - :CaseLevel: Integration """ with session: errata_values = session.errata.read(RHVA_ERRATA_ID) @@ -570,12 +553,10 @@ def test_positive_filter_by_environment( :Setup: Errata synced on satellite server. - :Steps: Go to Content -> Errata. Select an Errata -> Content Hosts tab + :steps: Go to Content -> Errata. Select an Errata -> Content Hosts tab -> Filter content hosts by Environment. :expectedresults: Content hosts can be filtered by Environment. - - :CaseLevel: System """ module_org = module_org_with_parameter with Broker( @@ -647,14 +628,12 @@ def test_positive_content_host_previous_env( 1. Make sure multiple environments are present. 2. Content host's previous environments have additional errata. - :Steps: Go to Content Hosts -> Select content host -> Errata Tab -> + :steps: Go to Content Hosts -> Select content host -> Errata Tab -> Select Previous environments. :expectedresults: The errata from previous environments are displayed. :parametrized: yes - - :CaseLevel: System """ module_org = module_org_with_parameter hostname = vm.hostname @@ -706,13 +685,11 @@ def test_positive_content_host_library(session, module_org_with_parameter, vm): 1. Make sure multiple environments are present. 2. Content host's Library environment has additional errata. - :Steps: Go to Content Hosts -> Select content host -> Errata Tab -> Select 'Library'. + :steps: Go to Content Hosts -> Select content host -> Errata Tab -> Select 'Library'. :expectedresults: The errata from Library are displayed. :parametrized: yes - - :CaseLevel: System """ hostname = vm.hostname assert _install_client_package(vm, FAKE_1_CUSTOM_PACKAGE, errata_applicability=True) @@ -746,14 +723,12 @@ def test_positive_content_host_search_type(session, erratatype_vm): :customerscenario: true - :Steps: Search for errata on content host by type (e.g. 'type = security') + :steps: Search for errata on content host by type (e.g. 'type = security') Step 1 Search for "type = security", assert expected amount and IDs found Step 2 Search for "type = bugfix", assert expected amount and IDs found Step 3 Search for "type = enhancement", assert expected amount and IDs found :BZ: 1653293 - - :CaseLevel: Integration """ pkgs = ' '.join(FAKE_9_YUM_OUTDATED_PACKAGES) @@ -821,15 +796,13 @@ def test_positive_show_count_on_content_host_page( 1. Errata synced on satellite server. 2. Some content hosts are present. - :Steps: Go to Hosts -> Content Hosts. + :steps: Go to Hosts -> Content Hosts. :expectedresults: The available errata count is displayed. :BZ: 1484044, 1775427 :customerscenario: true - - :CaseLevel: System """ vm = erratatype_vm hostname = vm.hostname @@ -879,13 +852,11 @@ def test_positive_show_count_on_content_host_details_page( 1. Errata synced on satellite server. 2. Some content hosts are present. - :Steps: Go to Hosts -> Content Hosts -> Select Content Host -> Details page. + :steps: Go to Hosts -> Content Hosts -> Select Content Host -> Details page. :expectedresults: The errata section should be displayed with Security, Bug fix, Enhancement. :BZ: 1484044 - - :CaseLevel: System """ vm = erratatype_vm hostname = vm.hostname @@ -925,7 +896,7 @@ def test_positive_filtered_errata_status_installable_param( :id: ed94cf34-b8b9-4411-8edc-5e210ea6af4f - :Steps: + :steps: 1. Prepare setup: Create Lifecycle Environment, Content View, Activation Key and all necessary repos @@ -941,8 +912,6 @@ def test_positive_filtered_errata_status_installable_param( :BZ: 1368254, 2013093 :CaseImportance: Medium - - :CaseLevel: System """ org = function_entitlement_manifest_org lce = entities.LifecycleEnvironment(organization=org).create() @@ -1043,7 +1012,7 @@ def test_content_host_errata_search_commands( :customerscenario: true - :Steps: + :steps: 1. host list --search "errata_status = security_needed" 2. host list --search "errata_status = errata_needed" 3. host list --search "applicable_errata = RHSA-2012:0055" diff --git a/tests/foreman/ui/test_hardwaremodel.py b/tests/foreman/ui/test_hardwaremodel.py index 4ec42851ab7..38b42de6aac 100644 --- a/tests/foreman/ui/test_hardwaremodel.py +++ b/tests/foreman/ui/test_hardwaremodel.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - -:Upstream: No """ from fauxfactory import gen_string from nailgun import entities @@ -23,15 +18,12 @@ @pytest.mark.tier2 @pytest.mark.upgrade def test_positive_end_to_end(session, host_ui_options): - """Perform end to end testing for hardware model component :id: 93663cc9-7c8f-4f43-8050-444be1313bed :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: Medium :BZ:1758260 diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index 93f2dedc83c..8823accdb70 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import copy import csv @@ -123,8 +118,6 @@ def test_positive_end_to_end(session, module_global_params, target_sat, host_ui_ and deleted. :BZ: 1419161 - - :CaseLevel: System """ api_values, host_name = host_ui_options global_params = [ @@ -185,8 +178,6 @@ def test_positive_read_from_details_page(session, module_host_template): :id: ffba5d40-918c-440e-afbb-6b910db3a8fb :expectedresults: Host is created and has expected content - - :CaseLevel: System """ template = module_host_template @@ -219,8 +210,6 @@ def test_positive_read_from_edit_page(session, host_ui_options): :id: 758fcab3-b363-4bfc-8f5d-173098a7e72d :expectedresults: Host is created and has expected content - - :CaseLevel: System """ api_values, host_name = host_ui_options with session: @@ -268,8 +257,6 @@ def test_positive_assign_taxonomies( :expectedresults: Host Assign Organization and Location actions are working as expected. - - :CaseLevel: Integration """ host = target_sat.api.Host(organization=module_org, location=smart_proxy_location).create() with session: @@ -331,8 +318,6 @@ def test_positive_assign_compliance_policy(session, scap_policy, target_sat, fun expected. :BZ: 1862135 - - :CaseLevel: Integration """ org = function_host.organization.read() loc = function_host.location.read() @@ -384,8 +369,6 @@ def test_positive_export(session, target_sat, function_org, function_location): :id: ffc512ad-982e-4b60-970a-41e940ebc74c :expectedresults: csv file contains same values as on web UI - - :CaseLevel: System """ hosts = [ target_sat.api.Host(organization=function_org, location=function_location).create() @@ -455,9 +438,6 @@ def test_negative_delete_primary_interface(session, host_ui_options): :BZ: 1417119 :expectedresults: Interface was not deleted - - - :CaseLevel: System """ values, host_name = host_ui_options interface_id = values['interfaces.interface.device_identifier'] @@ -484,8 +464,6 @@ def test_positive_view_hosts_with_non_admin_user( :expectedresults: user with only view_hosts, edit_hosts and view_organization permissions is able to read content hosts and hosts - - :CaseLevel: Component """ user_password = gen_string('alpha') role = target_sat.api.Role(organization=[module_org]).create() @@ -523,8 +501,6 @@ def test_positive_remove_parameter_non_admin_user( :expectedresults: user with sufficient permissions may remove host parameter - - :CaseLevel: System """ user_password = gen_string('alpha') parameter = {'name': gen_string('alpha'), 'value': gen_string('alpha')} @@ -579,8 +555,6 @@ def test_negative_remove_parameter_non_admin_user( :expectedresults: user with insufficient permissions is unable to remove host parameter, 'Remove' link is not visible for him - - :CaseLevel: System """ user_password = gen_string('alpha') @@ -635,8 +609,6 @@ def test_positive_check_permissions_affect_create_procedure( entities for create host procedure that he has access to :BZ: 1293716 - - :CaseLevel: System """ # Create two lifecycle environments lc_env = target_sat.api.LifecycleEnvironment(organization=function_org).create() @@ -751,8 +723,6 @@ def test_positive_search_by_parameter(session, module_org, smart_proxy_location, :expectedresults: Only one specific host is returned by search :BZ: 1725686 - - :CaseLevel: Integration """ param_name = gen_string('alpha') param_value = gen_string('alpha') @@ -786,8 +756,6 @@ def test_positive_search_by_parameter_with_different_values( :expectedresults: Only one specific host is returned by search :BZ: 1725686 - - :CaseLevel: Integration """ param_name = gen_string('alpha') param_values = [gen_string('alpha'), gen_string('alphanumeric')] @@ -822,8 +790,6 @@ def test_positive_search_by_parameter_with_prefix( :expectedresults: All assigned hosts to organization are returned by search - - :CaseLevel: Integration """ param_name = gen_string('alpha') param_value = gen_string('alpha') @@ -861,8 +827,6 @@ def test_positive_search_by_parameter_with_operator( search :BZ: 1463806 - - :CaseLevel: Integration """ param_name = gen_string('alpha') param_value = gen_string('alpha') @@ -903,8 +867,6 @@ def test_positive_search_with_org_and_loc_context( :BZ: 1405496 :customerscenario: true - - :CaseLevel: Integration """ host = target_sat.api.Host(organization=function_org, location=function_location).create() with session: @@ -928,8 +890,6 @@ def test_positive_search_by_org(session, smart_proxy_location, target_sat): result is returned :BZ: 1447958 - - :CaseLevel: Integration """ host = target_sat.api.Host(location=smart_proxy_location).create() org = host.organization.read() @@ -950,8 +910,6 @@ def test_positive_validate_inherited_cv_lce_ansiblerole(session, target_sat, mod :expectedresults: Host's lifecycle environment, content view and ansible role match the ones specified in hostgroup. - :CaseLevel: Integration - :customerscenario: true :BZ: 1391656, 2094912 @@ -1024,8 +982,6 @@ def test_positive_global_registration_form( :customerscenario: true :expectedresults: The curl command contains all required parameters - - :CaseLevel: Integration """ # rex and insights parameters are only specified in curl when differing from # inerited parameters @@ -1102,8 +1058,6 @@ def test_positive_global_registration_end_to_end( client work out of the box :parametrized: yes - - :CaseLevel: Integration """ # make sure global parameters for rex and insights are set to true insights_cp = ( @@ -1237,8 +1191,6 @@ def test_global_registration_form_populate( e.g. activation key, operating system, life-cycle environment, host parameters for remote-execution, insights setup. - :CaseLevel: Integration - :steps: 1. create and sync repository 2. create the content view and activation-key @@ -1304,8 +1256,6 @@ def test_global_registration_with_capsule_host( :expectedresults: Host is successfully registered with capsule host, remote execution and insights client work out of the box - :CaseLevel: Integration - :steps: 1. create and sync repository 2. create the content view and activation-key @@ -1398,8 +1348,6 @@ def test_global_registration_with_gpg_repo_and_default_package( :expectedresults: Host is successfully registered, gpg repo is enabled and default package is installed. - :CaseLevel: Integration - :steps: 1. create and sync repository 2. create the content view and activation-key @@ -1466,8 +1414,6 @@ def test_global_registration_upgrade_subscription_manager( :expectedresults: Host is successfully registered, repo is enabled on advanced tab and subscription-manager is updated. - :CaseLevel: Integration - :steps: 1. Create activation-key 2. Open the global registration form, add repo and activation key @@ -1519,8 +1465,6 @@ def test_global_re_registration_host_with_force_ignore_error_options( :expectedresults: Verify the force and ignore checkbox options - :CaseLevel: Integration - :steps: 1. create and sync repository 2. create the content view and activation-key @@ -1564,8 +1508,6 @@ def test_global_registration_token_restriction( :expectedresults: global registration token should be restricted for any api calls other than the registration - :CaseLevel: Integration - :steps: 1. open the global registration form and generate the curl token 2. use that curl token to execute other api calls e.g. GET /hosts, /users @@ -1605,8 +1547,6 @@ def test_positive_bulk_delete_host(session, smart_proxy_location, target_sat, fu :expectedresults: All selected hosts should be deleted successfully :BZ: 1368026 - - :CaseLevel: System """ host_template = target_sat.api.Host(organization=function_org, location=smart_proxy_location) host_template.create_missing() @@ -1642,8 +1582,6 @@ def test_positive_read_details_page_from_new_ui(session, host_ui_options): :id: ef0c5942-9049-11ec-8029-98fa9b6ecd5a :expectedresults: Host is created and has expected content - - :CaseLevel: System """ with session: api_values, host_name = host_ui_options @@ -1676,8 +1614,6 @@ def test_rex_new_ui(session, target_sat, rex_contenthost): :expectedresults: Remote execution succeeded and the job is visible on Recent jobs card on Overview tab - - :CaseLevel: System """ hostname = rex_contenthost.hostname job_args = { @@ -1714,8 +1650,6 @@ def test_positive_manage_table_columns(session, current_sat_org, current_sat_loc :expectedresults: Check if the custom columns were set properly, i.e., are displayed or not displayed in the table. - :CaseLevel: System - :BZ: 1813274 :customerscenario: true @@ -1764,8 +1698,6 @@ def test_positive_host_details_read_templates( :BZ: 2128038 :customerscenario: true - - :CaseLevel: System """ host = target_sat.api.Host().search(query={'search': f'name={target_sat.hostname}'})[0] api_templates = [template['name'] for template in host.list_provisioning_templates()] @@ -1816,7 +1748,6 @@ def test_positive_update_delete_package( 9. Delete the package :expectedresults: The package is updated and deleted - """ client = rhel_contenthost client.add_rex_key(target_sat) @@ -1936,7 +1867,6 @@ def test_positive_apply_erratum( 5. Select errata and apply via rex. :expectedresults: The erratum is applied - """ # install package client = rhel_contenthost @@ -2019,7 +1949,6 @@ def test_positive_crud_module_streams( 5. Reset the Module stream :expectedresults: Module streams can be enabled, installed, removed and reset using the new UI. - """ module_name = 'duck' client = rhel_contenthost @@ -2111,8 +2040,6 @@ def test_positive_inherit_puppet_env_from_host_group_when_action( :expectedresults: Expected puppet environment is inherited to the host :BZ: 1414914 - - :CaseLevel: System """ host = session_puppet_enabled_sat.api.Host( organization=module_puppet_org, location=module_puppet_loc @@ -2165,8 +2092,6 @@ def test_positive_create_with_puppet_class( :id: d883f169-1105-435c-8422-a7160055734a :expectedresults: Host is created and contains correct puppet class - - :CaseLevel: System """ host_template = session_puppet_enabled_sat.api.Host( @@ -2226,8 +2151,6 @@ def test_positive_inherit_puppet_env_from_host_group_when_create( :expectedresults: Expected puppet environment is inherited to the form :BZ: 1414914 - - :CaseLevel: Integration """ hg_name = gen_string('alpha') @@ -2279,8 +2202,6 @@ def test_positive_set_multi_line_and_with_spaces_parameter_value( 2. host parameter value is the same when restored from yaml format :BZ: 1315282 - - :CaseLevel: System """ host_template = session_puppet_enabled_sat.api.Host( organization=module_puppet_org, location=module_puppet_loc @@ -2339,9 +2260,7 @@ def test_positive_tracer_enable_reload(tracer_install_host, target_sat): :Team: Phoenix-subscriptions - :CaseLevel: System - - :Steps: + :steps: 1. Register a RHEL host to Satellite. 2. Prepare katello-tracer to be installed 3. Navigate to the Traces tab in New Host UI @@ -2349,7 +2268,6 @@ def test_positive_tracer_enable_reload(tracer_install_host, target_sat): :expectedresults: The Tracer tab message updates accordingly during the process, and displays the state the correct Title - """ host = ( target_sat.api.Host().search(query={'search': tracer_install_host.hostname})[0].read_json() @@ -2387,8 +2305,6 @@ def test_positive_host_registration_with_non_admin_user( :id: 35458bbc-4556-41b9-ba26-ae0b15179731 :expectedresults: User with register hosts permission able to register hosts. - - :CaseLevel: Component """ user_password = gen_string('alpha') org = module_sca_manifest_org @@ -2426,3 +2342,42 @@ def test_positive_host_registration_with_non_admin_user( # Verify server.hostname and server.port from subscription-manager config assert target_sat.hostname == rhel8_contenthost.subscription_config['server']['hostname'] assert constants.CLIENT_PORT == rhel8_contenthost.subscription_config['server']['port'] + + +@pytest.mark.tier2 +def test_all_hosts_delete(session, target_sat, function_org, function_location, new_host_ui): + """Create a host and delete it through All Hosts UI + + :id: 42b4560c-bb57-4c58-928e-e5fd5046b93f + + :expectedresults: Successful deletion of a host through the table dropdown + + :CaseComponent:Hosts-Content + + :Team: Phoenix-subscriptions + """ + host = target_sat.api.Host(organization=function_org, location=function_location).create() + with target_sat.ui_session() as session: + session.organization.select(function_org.name) + session.location.select(function_location.name) + assert session.all_hosts.delete(host.name) + + +@pytest.mark.tier2 +def test_all_hosts_bulk_delete(session, target_sat, function_org, function_location, new_host_ui): + """Create several hosts, and delete them via Bulk Actions in All Hosts UI + + :id: af1b4a66-dd83-47c3-904b-e8627119cc53 + + :expectedresults: Successful deletion of multiple hosts at once through Bulk Action + + :CaseComponent:Hosts-Content + + :Team: Phoenix-subscriptions + """ + for _ in range(10): + target_sat.api.Host(organization=function_org, location=function_location).create() + with target_sat.ui_session() as session: + session.organization.select(function_org.name) + session.location.select(function_location.name) + assert session.all_hosts.bulk_delete_all() diff --git a/tests/foreman/ui/test_hostcollection.py b/tests/foreman/ui/test_hostcollection.py index e01be3e49a4..c2acfd7a26a 100644 --- a/tests/foreman/ui/test_hostcollection.py +++ b/tests/foreman/ui/test_hostcollection.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: HostCollections :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import time @@ -223,8 +218,6 @@ def test_positive_end_to_end( :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ hc_name = gen_string('alpha') @@ -275,8 +268,6 @@ def test_negative_install_via_remote_execution( :expectedresults: The package is not installed, and the job invocation status contains some expected values: hosts information, jos status. - - :CaseLevel: Integration """ hosts = [] for _ in range(2): @@ -315,8 +306,6 @@ def test_negative_install_via_custom_remote_execution( :expectedresults: The package is not installed, and the job invocation status contains some expected values: hosts information, jos status. - - :CaseLevel: Integration """ hosts = [] for _ in range(2): @@ -350,8 +339,6 @@ def test_positive_add_host(session, module_target_sat): :id: 80824c9f-15a1-4f76-b7ac-7d9ca9f6ed9e :expectedresults: Host is added to Host Collection successfully - - :CaseLevel: System """ hc_name = gen_string('alpha') org = module_target_sat.api.Organization().create() @@ -386,8 +373,6 @@ def test_positive_install_package( :expectedresults: Package was successfully installed on all the hosts in host collection - - :CaseLevel: System """ with session: session.organization.select(org_name=module_org_with_parameter.name) @@ -412,8 +397,6 @@ def test_positive_remove_package( :expectedresults: Package was successfully removed from all the hosts in host collection - - :CaseLevel: System """ _install_package_with_assertion(vm_content_hosts, constants.FAKE_0_CUSTOM_PACKAGE) with session: @@ -440,8 +423,6 @@ def test_positive_upgrade_package( :expectedresults: Package was successfully upgraded on all the hosts in host collection - - :CaseLevel: System """ _install_package_with_assertion(vm_content_hosts, constants.FAKE_1_CUSTOM_PACKAGE) with session: @@ -467,8 +448,6 @@ def test_positive_install_package_group( :expectedresults: Package group was successfully installed on all the hosts in host collection - - :CaseLevel: System """ with session: session.organization.select(org_name=module_org_with_parameter.name) @@ -494,8 +473,6 @@ def test_positive_remove_package_group( :expectedresults: Package group was successfully removed on all the hosts in host collection - - :CaseLevel: System """ for client in vm_content_hosts: result = client.run(f'yum groups install -y {constants.FAKE_0_CUSTOM_PACKAGE_GROUP_NAME}') @@ -527,8 +504,6 @@ def test_positive_install_errata( :expectedresults: Errata was successfully installed in all the hosts in host collection - - :CaseLevel: System """ _install_package_with_assertion(vm_content_hosts, constants.FAKE_1_CUSTOM_PACKAGE) with session: @@ -597,8 +572,6 @@ def test_positive_change_assigned_content( names :BZ: 1315280 - - :CaseLevel: System """ new_lce_name = gen_string('alpha') new_cv_name = gen_string('alpha') @@ -666,7 +639,7 @@ def test_negative_hosts_limit( :id: 57b70977-2110-47d9-be3b-461ad15c70c7 - :Steps: + :steps: 1. Create Host Collection entity that can contain only one Host (using Host Limit field) 2. Create Host and add it to Host Collection. Check that it was @@ -676,8 +649,6 @@ def test_negative_hosts_limit( :expectedresults: Second host is not added to Host Collection and appropriate error is shown - - :CaseLevel: System """ hc_name = gen_string('alpha') org = module_target_sat.api.Organization().create() @@ -732,7 +703,7 @@ def test_positive_install_module_stream( :id: e5d882e0-3520-4cb6-8629-ef4c18692868 - :Steps: + :steps: 1. Run dnf upload profile to sync module streams from hosts to Satellite 2. Navigate to host_collection 3. Install the module stream duck @@ -741,8 +712,6 @@ def test_positive_install_module_stream( :expectedresults: Module-Stream should get installed on all the hosts in host collection - - :CaseLevel: System """ _run_remote_command_on_content_hosts('dnf -y upload-profile', vm_content_hosts_module_stream) with session: @@ -782,7 +751,7 @@ def test_positive_install_modular_errata( :id: 8d6fb447-af86-4084-a147-7910f0cecdef - :Steps: + :steps: 1. Generate modular errata by installing older version of module stream 2. Run dnf upload-profile 3. Install the modular errata by 'remote execution' @@ -790,8 +759,6 @@ def test_positive_install_modular_errata( :expectedresults: Modular Errata should get installed on all hosts in host collection. - - :CaseLevel: System """ stream = "0" version = "20180704111719" diff --git a/tests/foreman/ui/test_hostgroup.py b/tests/foreman/ui/test_hostgroup.py index c8a1d23a897..718b5091fca 100644 --- a/tests/foreman/ui/test_hostgroup.py +++ b/tests/foreman/ui/test_hostgroup.py @@ -6,15 +6,10 @@ :CaseComponent: HostGroup -:CaseLevel: Integration - :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string from nailgun import entities @@ -179,7 +174,7 @@ def test_positive_create_new_host(): :id: 49704437-5ca1-46cb-b74e-de58396add37 - :Steps: + :steps: 1. Create hostgroup with the Content Source field populated. 2. Create host from Hosts > Create Host, selecting the hostgroup in the Host Group field. @@ -202,7 +197,7 @@ def test_positive_nested_host_groups( :id: 547f8e72-df65-48eb-aeb1-6b5fd3cbf4e5 - :Steps: + :steps: 1. Create the parent host-group. 2. Create, Update and Delete the nested host-group. @@ -276,7 +271,7 @@ def test_positive_clone_host_groups( :id: 9f02dcc5-98aa-48bd-8114-edd3a0be65c1 - :Steps: + :steps: 1. Create the host-group. 2. Clone the host-group created in step 1 3. Update and Delete the cloned host-group. diff --git a/tests/foreman/ui/test_http_proxy.py b/tests/foreman/ui/test_http_proxy.py index abe02f12002..cc995e84da9 100644 --- a/tests/foreman/ui/test_http_proxy.py +++ b/tests/foreman/ui/test_http_proxy.py @@ -2,19 +2,14 @@ :Requirement: HttpProxy -:CaseLevel: Acceptance - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: High :CaseAutomation: Automated -:Upstream: No """ from fauxfactory import gen_integer, gen_string, gen_url import pytest @@ -32,8 +27,6 @@ def test_positive_create_update_delete(module_org, module_location, target_sat): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ http_proxy_name = gen_string('alpha', 15) @@ -196,7 +189,7 @@ def test_set_default_http_proxy(module_org, module_location, setting_update, tar :id: e93733e1-5c05-4b7f-89e4-253b9ce55a5a - :Steps: + :steps: 1. Navigate to Infrastructure > Http Proxies 2. Create a Http Proxy 3. GoTo to Administer > Settings > content tab @@ -210,8 +203,6 @@ def test_set_default_http_proxy(module_org, module_location, setting_update, tar :expectedresults: Setting "Default HTTP Proxy" to "no global default" result in success.''' :CaseImportance: Medium - - :CaseLevel: Acceptance """ property_name = setting_update.name @@ -242,12 +233,11 @@ def test_set_default_http_proxy(module_org, module_location, setting_update, tar def test_check_http_proxy_value_repository_details( function_org, function_location, function_product, setting_update, target_sat ): - """Deleted Global Http Proxy is reflected in repository details page". :id: 3f64255a-ef6c-4acb-b99b-e5579133b564 - :Steps: + :steps: 1. Create Http Proxy (Go to Infrastructure > Http Proxies > New Http Proxy) 2. GoTo to Administer > Settings > content tab 3. Update the "Default HTTP Proxy" with created above. @@ -264,8 +254,6 @@ def test_check_http_proxy_value_repository_details( 2. "HTTP Proxy" field in repository details page should be set to Global Default (None). :CaseImportance: Medium - - :CaseLevel: Acceptance """ property_name = setting_update.name @@ -310,7 +298,7 @@ def test_http_proxy_containing_special_characters(): :customerscenario: true - :Steps: + :steps: 1. Navigate to Infrastructure > Http Proxies 2. Create HTTP Proxy with special characters in password. 3. Go To to Administer > Settings > content tab diff --git a/tests/foreman/ui/test_jobinvocation.py b/tests/foreman/ui/test_jobinvocation.py index bb773841ef6..c98e512f527 100644 --- a/tests/foreman/ui/test_jobinvocation.py +++ b/tests/foreman/ui/test_jobinvocation.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: RemoteExecution :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from inflection import camelize import pytest @@ -42,7 +37,7 @@ def test_positive_run_default_job_template_by_ip( :Setup: Use pre-defined job template. - :Steps: + :steps: 1. Set remote_execution_connect_by_ip on host to true 2. Navigate to an individual host and click Run Job @@ -52,8 +47,6 @@ def test_positive_run_default_job_template_by_ip( :expectedresults: Verify the job was successfully ran against the host :parametrized: yes - - :CaseLevel: System """ hostname = module_rhel_client_by_ip.hostname with session: @@ -83,7 +76,7 @@ def test_positive_run_custom_job_template_by_ip( :Setup: Create a working job template. - :Steps: + :steps: 1. Set remote_execution_connect_by_ip on host to true 2. Navigate to an individual host and click Run Job @@ -93,8 +86,6 @@ def test_positive_run_custom_job_template_by_ip( :expectedresults: Verify the job was successfully ran against the host :parametrized: yes - - :CaseLevel: System """ hostname = module_rhel_client_by_ip.hostname job_template_name = gen_string('alpha') @@ -139,7 +130,7 @@ def test_positive_schedule_recurring_host_job(self): :Team: Rocket - :Steps: + :steps: 1. Register a RHEL host to Satellite. 2. Import all roles available by default. 3. Assign a role to host. @@ -151,7 +142,6 @@ def test_positive_schedule_recurring_host_job(self): :expectedresults: The scheduled Job appears in the Job Invocation list at the appointed time - """ @@ -166,7 +156,7 @@ def test_positive_schedule_recurring_hostgroup_job(self): :Team: Rocket - :Steps: + :steps: 1. Register a RHEL host to Satellite. 2. Import all roles available by default. 3. Assign a role to host. @@ -178,5 +168,4 @@ def test_positive_schedule_recurring_hostgroup_job(self): :expectedresults: The scheduled Job appears in the Job Invocation list at the appointed time - """ diff --git a/tests/foreman/ui/test_jobtemplate.py b/tests/foreman/ui/test_jobtemplate.py index 798400aa976..e2be74da640 100644 --- a/tests/foreman/ui/test_jobtemplate.py +++ b/tests/foreman/ui/test_jobtemplate.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: RemoteExecution :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -29,8 +24,6 @@ def test_positive_end_to_end(session, module_org, module_location, target_sat): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ template_name = gen_string('alpha') diff --git a/tests/foreman/ui/test_ldap_authentication.py b/tests/foreman/ui/test_ldap_authentication.py index b72d1bbcd8f..e882f1c489d 100644 --- a/tests/foreman/ui/test_ldap_authentication.py +++ b/tests/foreman/ui/test_ldap_authentication.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Integration - :CaseComponent: LDAP :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import os @@ -144,8 +139,6 @@ def test_positive_end_to_end(session, ldap_auth_source, ldap_tear_down): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High :parametrized: yes @@ -247,7 +240,7 @@ def test_positive_add_katello_role( :id: aa5e3bf4-cb42-43a4-93ea-a2eea54b847a - :Steps: + :steps: 1. Create an UserGroup. 2. Assign some foreman roles to UserGroup. 3. Create and associate an External UserGroup. @@ -503,7 +496,7 @@ def test_positive_add_admin_role_with_org_loc( :setup: LDAP Auth Source should be created with Org and Location Associated. - :Steps: + :steps: 1. Create an UserGroup. 2. Assign admin role to UserGroup. 3. Create and associate an External UserGroup. @@ -563,7 +556,7 @@ def test_positive_add_foreman_role_with_org_loc( :setup: LDAP Auth Source should be created with Org and Location Associated. - :Steps: + :steps: 1. Create an UserGroup. 2. Assign some foreman roles to UserGroup. @@ -629,7 +622,7 @@ def test_positive_add_katello_role_with_org( :setup: LDAP Auth Source should be created with Organization associated. - :Steps: + :steps: 1. Create an UserGroup. 2. Assign some katello roles to UserGroup. 3. Create and associate an External UserGroup. @@ -955,7 +948,7 @@ def test_onthefly_functionality(session, ldap_auth_source, ldap_tear_down): :id: 6998de30-ef77-11ea-a0ce-0c7a158cbff4 - :Steps: + :steps: 1. Create an auth source with onthefly disabled 2. Try login with a user from auth source @@ -1043,7 +1036,7 @@ def test_verify_attribute_of_users_are_updated(session, ldap_auth_source, ldap_t :customerscenario: true - :Steps: + :steps: 1. Create authsource with onthefly disabled 2. Create a user manually and select the authsource created 3. Attributes of the user (like names and email) should be synced. @@ -1220,7 +1213,7 @@ def test_positive_group_sync_open_ldap_authsource( :BZ: 1883209 - :Steps: + :steps: 1. Create an UserGroup. 2. Assign some foreman roles to UserGroup. 3. Create and associate an External OpenLDAP UserGroup. @@ -1267,7 +1260,7 @@ def test_verify_group_permissions( :id: 7e2ef59c-0c68-11eb-b6f3-0c7a158cbff4 - :Steps: + :steps: 1. Create two usergroups and link it with external group having a user in common 2. Give those usergroup different permissions @@ -1315,7 +1308,7 @@ def test_verify_ldap_filters_ipa( :id: 0052b272-08b1-11eb-80c6-0c7a158cbff4 - :Steps: + :steps: 1. Create authsource with onthefly enabled and ldap filter 2. Verify login from users according to the filter diff --git a/tests/foreman/ui/test_lifecycleenvironment.py b/tests/foreman/ui/test_lifecycleenvironment.py index 5ff44751f6e..e99d9f9c895 100644 --- a/tests/foreman/ui/test_lifecycleenvironment.py +++ b/tests/foreman/ui/test_lifecycleenvironment.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: LifecycleEnvironments :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from airgun.session import Session from navmazing import NavigationTriesExceeded @@ -42,8 +37,6 @@ def test_positive_end_to_end(session): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ lce_name = gen_string('alpha') @@ -79,8 +72,6 @@ def test_positive_create_chain(session): :id: ed3d2c88-ef0a-4a1a-9f11-5bdb2119fc18 :expectedresults: Environment is created - - :CaseLevel: Integration """ lce_path_name = gen_string('alpha') lce_name = gen_string('alpha') @@ -120,8 +111,6 @@ def test_positive_search_lce_content_view_packages_by_full_name(session, module_ :expectedresults: only the searched packages where found :BZ: 1432155 - - :CaseLevel: System """ packages = [ {'name': FAKE_0_CUSTOM_PACKAGE_NAME, 'full_names': [FAKE_0_CUSTOM_PACKAGE]}, @@ -173,8 +162,6 @@ def test_positive_search_lce_content_view_packages_by_name(session, module_org, :expectedresults: only the searched packages where found :BZ: 1432155 - - :CaseLevel: System """ packages = [ {'name': FAKE_0_CUSTOM_PACKAGE_NAME, 'packages_count': 1}, @@ -218,8 +205,6 @@ def test_positive_search_lce_content_view_module_streams_by_name(session, module 5. Search by module stream names :expectedresults: only the searched module streams where found - - :CaseLevel: System """ module_streams = [ {'name': FAKE_1_CUSTOM_PACKAGE_NAME, 'streams_count': 2}, @@ -254,7 +239,7 @@ def test_positive_custom_user_view_lce(session, test_name, target_sat): :BZ: 1420511 - :Steps: + :steps: As an admin user: @@ -281,8 +266,6 @@ def test_positive_custom_user_view_lce(session, test_name, target_sat): :expectedresults: The additional lifecycle environment is viewable and accessible by the custom user. - - :CaseLevel: Integration """ role_name = gen_string('alpha') lce_name = gen_string('alpha') diff --git a/tests/foreman/ui/test_location.py b/tests/foreman/ui/test_location.py index 1142dbe878c..98d2d2a8749 100644 --- a/tests/foreman/ui/test_location.py +++ b/tests/foreman/ui/test_location.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: OrganizationsandLocations :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_ipaddr, gen_string from nailgun import entities @@ -34,8 +29,6 @@ def test_positive_end_to_end(session): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: Critical """ loc_parent = entities.Location().create() @@ -125,8 +118,6 @@ def test_positive_update_with_all_users(session): was enabled and then disabled afterwards :BZ: 1321543, 1479736, 1479736 - - :CaseLevel: Integration """ user = entities.User().create() loc = entities.Location().create() @@ -161,8 +152,6 @@ def test_positive_add_org_hostgroup_template(session): :expectedresults: organization, hostgroup, provisioning template are added to location - - :CaseLevel: Integration """ org = entities.Organization().create() loc = entities.Location().create() @@ -203,8 +192,6 @@ def test_positive_update_compresource(session): :id: 1d24414a-666d-490d-89b9-cd0704684cdd :expectedresults: compute resource is added and removed from the location - - :CaseLevel: Integration """ url = LIBVIRT_RESOURCE_URL % settings.libvirt.libvirt_hostname resource = entities.LibvirtComputeResource(url=url).create() diff --git a/tests/foreman/ui/test_media.py b/tests/foreman/ui/test_media.py index db78fe4b414..75a39f80edb 100644 --- a/tests/foreman/ui/test_media.py +++ b/tests/foreman/ui/test_media.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - :CaseImportance: Low -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -32,8 +27,6 @@ def test_positive_end_to_end(session, module_org, module_location): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ name = gen_string('alpha') diff --git a/tests/foreman/ui/test_modulestreams.py b/tests/foreman/ui/test_modulestreams.py index 23461437f48..f8a67ed15c6 100644 --- a/tests/foreman/ui/test_modulestreams.py +++ b/tests/foreman/ui/test_modulestreams.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string from nailgun import entities @@ -56,8 +51,6 @@ def test_positive_module_stream_details_search_in_repo(session, module_org, modu :expectedresults: Content search functionality works as intended and expected module_streams are present inside of repository - :CaseLevel: Integration - :BZ: 1948758 """ with session: diff --git a/tests/foreman/ui/test_operatingsystem.py b/tests/foreman/ui/test_operatingsystem.py index 3a172369115..1324b556230 100644 --- a/tests/foreman/ui/test_operatingsystem.py +++ b/tests/foreman/ui/test_operatingsystem.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Provisioning :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -31,8 +26,6 @@ def test_positive_end_to_end(session, module_org, module_location, target_sat): :expectedresults: All scenarios flows work properly - :CaseLevel: Integration - :CaseImportance: Critical """ name = gen_string('alpha') @@ -155,8 +148,6 @@ def test_positive_verify_os_name(session, target_sat): :BZ: 1778503 - :CaseLevel: Component - :CaseImportance: Low """ name = gen_string('alpha') diff --git a/tests/foreman/ui/test_organization.py b/tests/foreman/ui/test_organization.py index 460e2ab38a7..a3d6d50c47d 100644 --- a/tests/foreman/ui/test_organization.py +++ b/tests/foreman/ui/test_organization.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: OrganizationsandLocations :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string from nailgun import entities @@ -59,8 +54,6 @@ def test_positive_end_to_end(session): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: Critical """ name = gen_string('alpha') @@ -208,8 +201,6 @@ def test_positive_create_with_all_users(session): :expectedresults: Organization and user entities assigned to each other :BZ: 1321543 - - :CaseLevel: Integration """ user = entities.User().create() org = entities.Organization().create() @@ -233,8 +224,6 @@ def test_positive_update_compresource(session): :id: a49349b9-4637-4ef6-b65b-bd3eccb5a12a :expectedresults: Compute resource is added and then removed. - - :CaseLevel: Integration """ url = f'{LIBVIRT_RESOURCE_URL}{settings.libvirt.libvirt_hostname}' resource = entities.LibvirtComputeResource(url=url).create() @@ -265,8 +254,6 @@ def test_positive_delete_with_manifest_lces(session, target_sat, function_entitl :expectedresults: Organization is deleted successfully. - :CaseLevel: Integration - :CaseImportance: Critical """ org = function_entitlement_manifest_org @@ -294,8 +281,6 @@ def test_positive_download_debug_cert_after_refresh( :expectedresults: Scenario passed successfully - :CaseLevel: Integration - :CaseImportance: High """ org = function_entitlement_manifest_org @@ -317,14 +302,12 @@ def test_positive_errata_view_organization_switch( :id: faad9cf3-f8d5-49a6-87d1-431837b67675 - :Steps: Create an Organization having a product synced which contains errata. + :steps: Create an Organization having a product synced which contains errata. :expectedresults: Verify that the errata belonging to one Organization is not showing in the Default organization. :CaseImportance: High - - :CaseLevel: Integration """ rc = module_target_sat.cli_factory.RepositoryCollection( repositories=[module_target_sat.cli_factory.YumRepository(settings.repos.yum_3.url)] @@ -346,7 +329,7 @@ def test_positive_product_view_organization_switch(session, module_org, module_p :id: 50cc459a-3a23-433a-99b9-9f3b929e6d64 - :Steps: + :steps: 1. Create an Organization having a product and verify that product is present in the Organization. 2. Switch the Organization to default and verify that product is not visible in it. @@ -354,8 +337,6 @@ def test_positive_product_view_organization_switch(session, module_org, module_p :expectedresults: Verify that the Product belonging to one Organization is not visible in another organization. - :CaseLevel: Integration - :CaseImportance: High """ with session: diff --git a/tests/foreman/ui/test_oscapcontent.py b/tests/foreman/ui/test_oscapcontent.py index 21e6367de0f..5669fb9e854 100644 --- a/tests/foreman/ui/test_oscapcontent.py +++ b/tests/foreman/ui/test_oscapcontent.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: SCAPPlugin :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import os @@ -45,7 +40,7 @@ def test_positive_end_to_end( :id: 9870555d-0b60-41ab-a481-81d4d3f78fec - :Steps: + :steps: 1. Create an openscap content. 2. Read values from created entity. @@ -53,8 +48,6 @@ def test_positive_end_to_end( 4. Delete openscap content :expectedresults: All expected CRUD actions finished successfully - - :CaseLevel: Integration """ title = gen_string('alpha') new_title = gen_string('alpha') @@ -93,7 +86,7 @@ def test_negative_create_with_same_name(session, oscap_content_path, default_org :id: f5c6491d-b83c-4ca2-afdf-4bb93e6dd92b - :Steps: + :steps: 1. Create an openscap content. 2. Provide all the appropriate parameters. @@ -128,7 +121,7 @@ def test_external_disa_scap_content(session, default_org, default_location): :id: 5f29254e-7c15-45e1-a2ec-4da1d3d8d74d - :Steps: + :steps: 1. Create an openscap content with external DISA SCAP content. 2. Assert that openscap content has been created. diff --git a/tests/foreman/ui/test_oscappolicy.py b/tests/foreman/ui/test_oscappolicy.py index 37196d8e2cf..712e6f37481 100644 --- a/tests/foreman/ui/test_oscappolicy.py +++ b/tests/foreman/ui/test_oscappolicy.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: SCAPPlugin :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from nailgun import entities import pytest @@ -47,7 +42,7 @@ def test_positive_check_dashboard( :customerscenario: true - :Steps: + :steps: 1. Create new host group 2. Create new host using host group from step 1 @@ -58,8 +53,6 @@ def test_positive_check_dashboard( data :BZ: 1424936 - - :CaseLevel: Integration """ name = gen_string('alpha') oscap_content_title = gen_string('alpha') @@ -124,8 +117,6 @@ def test_positive_end_to_end( :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: Critical """ name = '{} {}'.format(gen_string('alpha'), gen_string('alpha')) diff --git a/tests/foreman/ui/test_oscaptailoringfile.py b/tests/foreman/ui/test_oscaptailoringfile.py index 011cb9bff00..0f6bdcdfc63 100644 --- a/tests/foreman/ui/test_oscaptailoringfile.py +++ b/tests/foreman/ui/test_oscaptailoringfile.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: SCAPPlugin :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from nailgun import entities import pytest @@ -30,8 +25,6 @@ def test_positive_end_to_end(session, tailoring_file_path, default_org, default_ :id: 9aebccb8-6837-4583-8a8a-8883480ab688 :expectedresults: All expected CRUD actions finished successfully - - :CaseLevel: Integration """ name = gen_string('alpha') new_name = gen_string('alpha') diff --git a/tests/foreman/ui/test_package.py b/tests/foreman/ui/test_package.py index 257e571dc76..643608811d1 100644 --- a/tests/foreman/ui/test_package.py +++ b/tests/foreman/ui/test_package.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string from nailgun import entities @@ -82,8 +77,6 @@ def test_positive_search_in_repo(session, module_org, module_yum_repo): :expectedresults: Content search functionality works as intended and expected packages are present inside of repository - - :CaseLevel: Integration """ with session: session.organization.select(org_name=module_org.name) @@ -108,8 +101,6 @@ def test_positive_search_in_multiple_repos(session, module_org, module_yum_repo, expected packages are present inside of repositories :BZ: 1514457 - - :CaseLevel: Integration """ with session: session.organization.select(org_name=module_org.name) @@ -139,8 +130,6 @@ def test_positive_check_package_details(session, module_org, module_yum_repo): :expectedresults: Package is present inside of repository and has all expected values in details section - :CaseLevel: Integration - :customerscenario: true """ with session: @@ -181,8 +170,6 @@ def test_positive_check_custom_package_details(session, module_org, module_yum_r :expectedresults: Package is present inside of repository and it possible to view its details - :CaseLevel: Integration - :customerscenario: true :BZ: 1387766, 1394390 @@ -210,8 +197,6 @@ def test_positive_rh_repo_search_and_check_file_list(session, module_org, module :expectedresults: Content search functionality works as intended and package contains expected list of files - - :CaseLevel: System """ with session: session.organization.select(org_name=module_org.name) diff --git a/tests/foreman/ui/test_partitiontable.py b/tests/foreman/ui/test_partitiontable.py index e36d448698d..127dc1ec67f 100644 --- a/tests/foreman/ui/test_partitiontable.py +++ b/tests/foreman/ui/test_partitiontable.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -37,8 +32,6 @@ def test_positive_create_default_for_organization(session): :expectedresults: New partition table is created and is present in the list of selected partition tables for any new organization - :CaseLevel: Integration - :CaseImportance: Medium """ name = gen_string('alpha') @@ -66,8 +59,6 @@ def test_positive_create_custom_organization(session): :expectedresults: New partition table is created and is not present in the list of selected partition tables for any new organization - :CaseLevel: Integration - :CaseImportance: Medium """ name = gen_string('alpha') @@ -95,8 +86,6 @@ def test_positive_create_default_for_location(session): :expectedresults: New partition table is created and is present in the list of selected partition tables for any new location - :CaseLevel: Integration - :CaseImportance: Medium """ name = gen_string('alpha') @@ -124,8 +113,6 @@ def test_positive_create_custom_location(session): :expectedresults: New partition table is created and is not present in the list of selected partition tables for any new location - :CaseLevel: Integration - :CaseImportance: Medium """ name = gen_string('alpha') @@ -152,8 +139,6 @@ def test_positive_delete_with_lock_and_unlock(session): :expectedresults: New partition table is created and not deleted when locked and only deleted after unlock - :CaseLevel: Integration - :CaseImportance: Medium """ name = gen_string('alpha') @@ -182,8 +167,6 @@ def test_positive_clone(session): :expectedresults: New partition table is created and cloned successfully - :CaseLevel: Integration - :CaseImportance: Medium """ name = gen_string('alpha') @@ -219,8 +202,6 @@ def test_positive_end_to_end(session, module_org, module_location, template_data :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ name = gen_string('alpha') diff --git a/tests/foreman/ui/test_product.py b/tests/foreman/ui/test_product.py index a70623620f1..71a468ccb8e 100644 --- a/tests/foreman/ui/test_product.py +++ b/tests/foreman/ui/test_product.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from datetime import timedelta @@ -46,8 +41,6 @@ def test_positive_end_to_end(session, module_org): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: Critical """ product_name = gen_string('alpha') @@ -113,8 +106,6 @@ def test_positive_create_in_different_orgs(session, product_name): :expectedresults: Product is created successfully in both organizations. - - :CaseLevel: Integration """ orgs = [entities.Organization().create() for _ in range(2)] with session: @@ -134,8 +125,6 @@ def test_positive_product_create_with_create_sync_plan(session, module_org): :expectedresults: Ensure sync get created and assigned to Product. - :CaseLevel: Integration - :CaseImportance: Medium """ product_name = gen_string('alpha') @@ -180,7 +169,7 @@ def test_positive_bulk_action_advanced_sync(session, module_org): :customerscenario: true - :Steps: + :steps: 1. Enable or create a repository and sync it. 2. Navigate to Content > Product > click on the product. 3. Click Select Action > Advanced Sync. diff --git a/tests/foreman/ui/test_provisioningtemplate.py b/tests/foreman/ui/test_provisioningtemplate.py index 1b957fb4549..bc88d687f88 100644 --- a/tests/foreman/ui/test_provisioningtemplate.py +++ b/tests/foreman/ui/test_provisioningtemplate.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ProvisioningTemplates :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -50,13 +45,11 @@ def test_positive_clone(module_org, module_location, target_sat, clone_setup): :id: 912f1619-4bb0-4e0f-88ce-88b5726fdbe0 - :Steps: + :steps: 1. Go to Provisioning template UI 2. Choose a template and attempt to clone it :expectedresults: The template is cloned - - :CaseLevel: Integration """ clone_name = gen_string('alpha') with target_sat.ui_session() as session: @@ -81,13 +74,11 @@ def test_positive_clone_locked(target_sat): :id: 2df8550a-fe7d-405f-ab48-2896554cda12 - :Steps: + :steps: 1. Go to Provisioning template UI 2. Choose a locked provisioning template and attempt to clone it :expectedresults: The template is cloned - - :CaseLevel: Integration """ clone_name = gen_string('alpha') with target_sat.ui_session() as session: @@ -112,8 +103,6 @@ def test_positive_end_to_end(module_org, module_location, template_data, target_ :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ name = gen_string('alpha') @@ -190,7 +179,7 @@ def test_positive_verify_supported_templates_rhlogo(target_sat, module_org, modu :id: 2df8550a-fe7d-405f-ab48-2896554cda14 - :Steps: + :steps: 1. Go to Provisioning template UI 2. Choose a any provisioning template and check if its supported or not diff --git a/tests/foreman/ui/test_puppetclass.py b/tests/foreman/ui/test_puppetclass.py index 5c5fdb98d78..b44ad73e7a7 100644 --- a/tests/foreman/ui/test_puppetclass.py +++ b/tests/foreman/ui/test_puppetclass.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Puppet :Team: Rocket -:TestType: Functional - :CaseImportance: Low -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -29,8 +24,6 @@ def test_positive_end_to_end(session_puppet_enabled_sat, module_puppet_org, modu :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ name = gen_string('alpha') diff --git a/tests/foreman/ui/test_puppetenvironment.py b/tests/foreman/ui/test_puppetenvironment.py index 6d11797d1fb..57502a51191 100644 --- a/tests/foreman/ui/test_puppetenvironment.py +++ b/tests/foreman/ui/test_puppetenvironment.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Puppet :Team: Rocket -:TestType: Functional - :CaseImportance: Low -:Upstream: No """ import pytest @@ -31,8 +26,6 @@ def test_positive_end_to_end(session_puppet_enabled_sat, module_puppet_org, modu :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ name = gen_string('alpha') @@ -74,8 +67,6 @@ def test_positive_availability_for_host_and_hostgroup_in_multiple_orgs( :BZ: 543178 - :CaseLevel: Integration - :CaseImportance: High """ env_name = gen_string('alpha') diff --git a/tests/foreman/ui/test_registration.py b/tests/foreman/ui/test_registration.py index 9a8eedf359b..38f48b8c34b 100644 --- a/tests/foreman/ui/test_registration.py +++ b/tests/foreman/ui/test_registration.py @@ -2,8 +2,6 @@ :Requirement: Registration -:CaseLevel: Acceptance - :CaseComponent: Registration :CaseAutomation: Automated @@ -11,10 +9,6 @@ :CaseImportance: Critical :Team: Rocket - -:TestType: Functional - -:Upstream: No """ from airgun.exceptions import DisabledWidgetError import pytest @@ -35,8 +29,6 @@ def test_positive_verify_default_values_for_global_registration( :expectedresults: Default fields in the form should be auto-populated e.g. organization, location, rex, insights setup, etc - :CaseLevel: Component - :steps: 1. Check for the default values in the global registration template """ @@ -76,8 +68,6 @@ def test_positive_org_loc_change_for_registration( :expectedresults: organization and location is updated correctly on the global registration page as well as in the command. - :CaseLevel: Component - :CaseImportance: Medium """ new_org = target_sat.api.Organization().create() diff --git a/tests/foreman/ui/test_remoteexecution.py b/tests/foreman/ui/test_remoteexecution.py index 59fd554c146..5d9f3900f9a 100644 --- a/tests/foreman/ui/test_remoteexecution.py +++ b/tests/foreman/ui/test_remoteexecution.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: RemoteExecution :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import datetime import time @@ -35,7 +30,7 @@ def test_positive_run_default_job_template_by_ip(session, rex_contenthost, modul :Setup: Use pre-defined job template. - :Steps: + :steps: 1. Navigate to an individual host and click Run Job 2. Select the job and appropriate template @@ -48,8 +43,6 @@ def test_positive_run_default_job_template_by_ip(session, rex_contenthost, modul :bz: 1898656 :customerscenario: true - - :CaseLevel: Integration """ hostname = rex_contenthost.hostname with session: @@ -90,7 +83,7 @@ def test_positive_run_custom_job_template_by_ip(session, module_org, rex_content :Setup: Create a working job template. - :Steps: + :steps: 1. Set remote_execution_connect_by_ip on host to true 2. Navigate to an individual host and click Run Job @@ -100,8 +93,6 @@ def test_positive_run_custom_job_template_by_ip(session, module_org, rex_content :expectedresults: Verify the job was successfully ran against the host :parametrized: yes - - :CaseLevel: System """ hostname = rex_contenthost.hostname @@ -147,7 +138,7 @@ def test_positive_run_job_template_multiple_hosts_by_ip( :Setup: Create a working job template. - :Steps: + :steps: 1. Set remote_execution_connect_by_ip on hosts to true 2. Navigate to the hosts page and select at least two hosts @@ -156,8 +147,6 @@ def test_positive_run_job_template_multiple_hosts_by_ip( 5. Run the job :expectedresults: Verify the job was successfully ran against the hosts - - :CaseLevel: System """ host_names = [] for vm in registered_hosts: @@ -196,7 +185,7 @@ def test_positive_run_scheduled_job_template_by_ip(session, module_org, rex_cont :Setup: Use pre-defined job template. - :Steps: + :steps: 1. Set remote_execution_connect_by_ip on host to true 2. Navigate to an individual host and click Run Job @@ -211,8 +200,6 @@ def test_positive_run_scheduled_job_template_by_ip(session, module_org, rex_cont 2. Verify the job was successfully ran after the designated time :parametrized: yes - - :CaseLevel: System """ job_time = 10 * 60 hostname = rex_contenthost.hostname @@ -280,7 +267,7 @@ def test_positive_ansible_job_check_mode(session): :id: 7aeb7253-e555-4e28-977f-71f16d3c32e2 - :Steps: + :steps: 1. Set the value of the ansible_roles_check_mode parameter to true on a host 2. Associate one or more Ansible roles with the host @@ -289,8 +276,6 @@ def test_positive_ansible_job_check_mode(session): :expectedresults: Verify that the roles were run in check mode (i.e. no changes were made on the host) - :CaseLevel: System - :CaseAutomation: NotAutomated :CaseComponent: Ansible @@ -306,7 +291,7 @@ def test_positive_ansible_config_report_failed_tasks_errors(session): :id: 1a91e534-143f-4f35-953a-7ad8b7d2ddf3 - :Steps: + :steps: 1. Import Ansible roles 2. Assign Ansible roles to a host @@ -314,8 +299,6 @@ def test_positive_ansible_config_report_failed_tasks_errors(session): :expectedresults: Verify that any task failures are listed as errors in the config report - :CaseLevel: System - :CaseAutomation: NotAutomated :CaseComponent: Ansible @@ -331,7 +314,7 @@ def test_positive_ansible_config_report_changes_notice(session): :id: 8c90f179-8b70-4932-a477-75dc3566c437 - :Steps: + :steps: 1. Import Ansible Roles 2. Assign Ansible roles to a host @@ -340,8 +323,6 @@ def test_positive_ansible_config_report_changes_notice(session): :expectedresults: Verify that any tasks that make changes on the host are listed as notice in the config report - :CaseLevel: System - :CaseAutomation: NotAutomated :CaseComponent: Ansible @@ -357,14 +338,12 @@ def test_positive_ansible_variables_imported_with_roles(session): :id: 107c53e8-5a8a-4291-bbde-fbd66a0bb85e - :Steps: + :steps: 1. Import Ansible roles :expectedresults: Verify that any variables in the role were also imported to Satellite - :CaseLevel: System - :CaseAutomation: NotAutomated :CaseComponent: Ansible @@ -380,14 +359,12 @@ def test_positive_roles_import_in_background(session): :id: 4f1c7b76-9c67-42b2-9a73-980ca1f05abc - :Steps: + :steps: 1. Import Ansible roles :expectedresults: Verify that the UI is accessible while roles are importing - :CaseLevel: System - :CaseAutomation: NotAutomated :CaseComponent: Ansible @@ -403,15 +380,13 @@ def test_positive_ansible_roles_ignore_list(session): :id: 6fa1d8f0-b583-4a07-88eb-c9ae7fcd0219 - :Steps: + :steps: 1. Add roles to the ignore list in Administer > Settings > Ansible 2. Navigate to Configure > Roles :expectedresults: Verify that any roles on the ignore list are not available for import - :CaseLevel: System - :CaseAutomation: NotAutomated :CaseComponent: Ansible @@ -428,7 +403,7 @@ def test_positive_ansible_variables_installed_with_collection(session): :id: 7ff88022-fe9b-482f-a6bb-3922036a1e1c - :Steps: + :steps: 1. Install an Ansible collection 2. Navigate to Configure > Variables @@ -436,8 +411,6 @@ def test_positive_ansible_variables_installed_with_collection(session): :expectedresults: Verify that any variables associated with the collection are present on Configure > Variables - :CaseLevel: System - :CaseAutomation: NotAutomated :CaseComponent: Ansible @@ -453,7 +426,7 @@ def test_positive_install_ansible_collection_via_job_invocation(session): :id: d4096aef-f6fc-41b6-ae56-d19b1f49cd42 - :Steps: + :steps: 1. Enable a host for remote execution 2. Navigate to Hosts > Schedule Remote Job @@ -464,8 +437,6 @@ def test_positive_install_ansible_collection_via_job_invocation(session): :expectedresults: The Ansible collection is successfully installed on the host - :CaseLevel: System - :CaseAutomation: NotAutomated :CaseComponent: Ansible @@ -481,7 +452,7 @@ def test_positive_set_ansible_role_order_per_host(session): :id: 24fbcd60-7cd1-46ff-86ac-16d6b436202c - :Steps: + :steps: 1. Enable a host for remote execution 2. Navigate to Hosts > All Hosts > $hostname > Edit > Ansible Roles @@ -491,8 +462,6 @@ def test_positive_set_ansible_role_order_per_host(session): :expectedresults: The roles are run in the specified order - :CaseLevel: System - :CaseAutomation: NotAutomated :CaseComponent: Ansible @@ -508,7 +477,7 @@ def test_positive_set_ansible_role_order_per_hostgroup(session): :id: 9eb5bc8e-081a-45b9-8751-f4220c944da6 - :Steps: + :steps: 1. Enable a host for remote execution 2. Create a host group @@ -520,8 +489,6 @@ def test_positive_set_ansible_role_order_per_hostgroup(session): :expectedresults: The roles are run in the specified order - :CaseLevel: System - :CaseAutomation: NotAutomated :CaseComponent: Ansible @@ -537,7 +504,7 @@ def test_positive_matcher_field_highlight(session): :id: 67b45cfe-31bb-41a8-b88e-27917c68f33e - :Steps: + :steps: 1. Navigate to Configure > Variables > $variablename 2. Select the "Override" checkbox in the "Default Behavior" section @@ -548,8 +515,6 @@ def test_positive_matcher_field_highlight(session): :expectedresults: The background of each field turns yellow when a change is made - :CaseLevel: System - :CaseAutomation: NotAutomated :CaseComponent: Ansible diff --git a/tests/foreman/ui/test_reporttemplates.py b/tests/foreman/ui/test_reporttemplates.py index 1de38478844..f8fbfff52d9 100644 --- a/tests/foreman/ui/test_reporttemplates.py +++ b/tests/foreman/ui/test_reporttemplates.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Integration - :CaseComponent: Reporting :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import csv import json @@ -145,8 +140,6 @@ def test_positive_end_to_end(session, module_org, module_location): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: Critical """ name = gen_string('alpha') @@ -245,8 +238,6 @@ def test_positive_generate_registered_hosts_report(target_sat, module_org, modul :expectedresults: The Host - Registered Content Hosts report is generated (with host filter) and it contains created host with correct data - :CaseLevel: Integration - :CaseImportance: High """ # generate Host Status report @@ -300,8 +291,6 @@ def test_positive_generate_subscriptions_report_json( :expectedresults: The Subscriptions report is generated in JSON - :CaseLevel: Integration - :CaseImportance: Medium """ # generate Subscriptions report @@ -496,6 +485,7 @@ def test_negative_nonauthor_of_report_cant_download_it(session): 4. Wait for dynflow 5. As a different user, try to download the generated report :expectedresults: Report can't be downloaded. Error. + :CaseImportance: High """ diff --git a/tests/foreman/ui/test_repositories.py b/tests/foreman/ui/test_repositories.py index 7ad4ac8efc5..55f69a165d2 100644 --- a/tests/foreman/ui/test_repositories.py +++ b/tests/foreman/ui/test_repositories.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Integration - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import pytest diff --git a/tests/foreman/ui/test_repository.py b/tests/foreman/ui/test_repository.py index 8395d096967..08d44cc052e 100644 --- a/tests/foreman/ui/test_repository.py +++ b/tests/foreman/ui/test_repository.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from datetime import datetime, timedelta from random import randint, shuffle @@ -65,8 +60,6 @@ def test_positive_create_in_different_orgs(session, module_org): :expectedresults: Repository is created successfully for both organizations - - :CaseLevel: Integration """ repo_name = gen_string('alpha') org2 = entities.Organization().create() @@ -100,8 +93,6 @@ def test_positive_create_as_non_admin_user(module_org, test_name, target_sat): :expectedresults: Repository successfully created :BZ: 1426393 - - :CaseLevel: Integration """ user_login = gen_string('alpha') user_password = gen_string('alphanumeric') @@ -152,8 +143,6 @@ def test_positive_create_yum_repo_same_url_different_orgs(session, module_prod): :id: f4cb00ed-6faf-4c79-9f66-76cd333299cb :expectedresults: Repositories are created and have equal number of packages. - - :CaseLevel: Integration """ # Create first repository repo = entities.Repository(product=module_prod, url=settings.repos.yum_0.url).create() @@ -191,8 +180,6 @@ def test_positive_create_as_non_admin_user_with_cv_published(module_org, test_na :expectedresults: New repository successfully created by non admin user :BZ: 1447829 - - :CaseLevel: Integration """ user_login = gen_string('alpha') user_password = gen_string('alphanumeric') @@ -262,8 +249,6 @@ def test_positive_discover_repo_via_existing_product(session, module_org): :id: 9181950c-a756-456f-a46a-059e7a2add3c :expectedresults: Repository is discovered and created - - :CaseLevel: Integration """ repo_name = 'fakerepo01' product = entities.Product(organization=module_org).create() @@ -290,8 +275,6 @@ def test_positive_discover_repo_via_new_product(session, module_org): :id: dc5281f8-1a8a-4a17-b746-728f344a1504 :expectedresults: Repository is discovered and created - - :CaseLevel: Integration """ product_name = gen_string('alpha') repo_name = 'fakerepo01' @@ -319,11 +302,9 @@ def test_positive_discover_module_stream_repo_via_existing_product(session, modu :id: e7b9e2c4-7ecd-4cde-8f74-961fbac8919c - :CaseLevel: Integration - :BZ: 1676642 - :Steps: + :steps: 1. Create a product. 2. From Content > Products, click on the Repo Discovery button. 3. Enter a url containing a yum repository with module streams, e.g., @@ -361,8 +342,6 @@ def test_positive_sync_custom_repo_yum(session, module_org): :id: afa218f4-e97a-4240-a82a-e69538d837a1 :expectedresults: Sync procedure for specific yum repository is successful - - :CaseLevel: Integration """ product = entities.Product(organization=module_org).create() repo = entities.Repository(url=settings.repos.yum_1.url, product=product).create() @@ -385,8 +364,6 @@ def test_positive_sync_custom_repo_docker(session, module_org): :expectedresults: Sync procedure for specific docker repository is successful - - :CaseLevel: Integration """ product = entities.Product(organization=module_org).create() repo = entities.Repository( @@ -411,8 +388,6 @@ def test_positive_resync_custom_repo_after_invalid_update(session, module_org): procedure for specific yum repository is successful :BZ: 1487173, 1262313 - - :CaseLevel: Integration """ product = entities.Product(organization=module_org).create() repo = entities.Repository(url=settings.repos.yum_1.url, product=product).create() @@ -441,8 +416,6 @@ def test_positive_resynchronize_rpm_repo(session, module_prod): :expectedresults: Repository has updated non-zero package count - :CaseLevel: Integration - :BZ: 1318004 """ repo = entities.Repository( @@ -476,8 +449,6 @@ def test_positive_end_to_end_custom_yum_crud(session, module_org, module_prod): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ repo_name = gen_string('alpha') @@ -542,8 +513,6 @@ def test_positive_end_to_end_custom_module_streams_crud(session, module_org, mod :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ repo_name = gen_string('alpha') @@ -588,8 +557,6 @@ def test_positive_upstream_with_credentials(session, module_prod): 2. The custom repository upstream credentials are updated. 3. The credentials are cleared. - :CaseLevel: Integration - :CaseImportance: High :BZ: 1433481, 1743271 @@ -648,8 +615,7 @@ def test_positive_upstream_with_credentials(session, module_prod): # # :expectedresults: All expected CRUD actions finished successfully # -# :CaseLevel: Integration -# +# # # :CaseImportance: High # # :BZ: 1467722 @@ -688,10 +654,7 @@ def test_positive_sync_ansible_collection_gallaxy_repo(session, module_prod): :expectedresults: All content synced successfully - :CaseLevel: Integration - :CaseImportance: High - """ repo_name = f'gallaxy-{gen_string("alpha")}' requirements = ''' @@ -728,8 +691,6 @@ def test_positive_no_errors_on_repo_scan(target_sat, function_sca_manifest_org): :customerscenario: True :BZ: 1994212 - - :CaseLevel: Integration """ sat_rpm_extras = target_sat.cli_factory.RHELServerExtras(cdn=True) with target_sat.ui_session() as session: @@ -749,8 +710,6 @@ def test_positive_reposet_disable(session, target_sat, function_entitlement_mani :id: de596c56-1327-49e8-86d5-a1ab907f26aa :expectedresults: RH repo was disabled - - :CaseLevel: Integration """ org = function_entitlement_manifest_org sat_tools_repo = target_sat.cli_factory.SatelliteToolsRepository(distro='rhel7', cdn=True) @@ -797,8 +756,6 @@ def test_positive_reposet_disable_after_manifest_deleted( :expectedresults: RH repo was disabled :BZ: 1344391 - - :CaseLevel: Integration """ org = function_entitlement_manifest_org sub = entities.Subscription(organization=org) @@ -849,8 +806,6 @@ def test_positive_delete_random_docker_repo(session, module_org): :expectedresults: Random repository can be deleted from random product without altering the other products. - - :CaseLevel: Integration """ entities_list = [] products = [entities.Product(organization=module_org).create() for _ in range(randint(2, 5))] @@ -879,8 +834,6 @@ def test_positive_delete_rhel_repo(session, module_entitlement_manifest_org, tar :expectedresults: Repository can be successfully deleted - :CaseLevel: Integration - :BZ: 1152672 """ @@ -929,8 +882,6 @@ def test_positive_recommended_repos(session, module_entitlement_manifest_org): 1. Shows repositories as per On/Off 'Recommended Repositories'. 2. Check last Satellite version Capsule/Tools repos do not exist. - :CaseLevel: Integration - :BZ: 1776108 """ with session: @@ -963,7 +914,7 @@ def test_positive_upload_resigned_rpm(): :customerscenario: true - :Steps: + :steps: 1. Buld or prepare an unsigned rpm. 2. Create a gpg key. 3. Use the gpg key to sign the rpm with sha1. @@ -989,7 +940,7 @@ def test_positive_remove_srpm_change_checksum(): :BZ: 1850914 - :Steps: + :steps: 1. Sync a repository that contains rpms and srpms and uses sha1 repodata. 2. Re-sync the repository after an srpm has been removed and its repodata regenerated using sha256. @@ -1011,7 +962,7 @@ def test_positive_repo_discovery_change_ssl(): :BZ: 1789848 - :Steps: + :steps: 1. Navigate to Content > Products > click on 'Repo Discovery'. 2. Set the repository type to 'Yum Repositories'. 3. Enter an upstream URL to discover and click on 'Discover'. @@ -1035,7 +986,7 @@ def test_positive_remove_credentials(session, function_product, function_org, fu :customerscenario: true - :Steps: + :steps: 1. Create a custom repository, with a repository type of 'yum' and an upstream username and password. 3. Remove the saved credentials by clicking the delete icon next to the 'Upstream @@ -1079,7 +1030,7 @@ def test_sync_status_persists_after_task_delete(session, module_prod, module_org :customerscenario: true - :Steps: + :steps: 1. Sync a custom Repo. 2. Navigate to Content > Sync Status. Assert status is Synced. 3. Use foreman-rake console to delete the Sync task. @@ -1136,7 +1087,7 @@ def test_positive_sync_status_repo_display(): :customerscenario: true - :Steps: + :steps: 1. Import manifest and enable RHEL 8 repositories. 2. Navigate to Content > Sync Status. @@ -1157,7 +1108,7 @@ def test_positive_search_enabled_kickstart_repos(): :BZ: 1724807, 1829817 - :Steps: + :steps: 1. Import a manifest 2. Navigate to Content > Red Hat Repositories, and enable some kickstart repositories. 3. In the search bar on the right side, select 'Enabled/Both'. @@ -1180,7 +1131,7 @@ def test_positive_rpm_metadata_display(): :BZ: 1904369 - :Steps: + :steps: 1. Enable and sync a repository, e.g., 'Red Hat Satellite Tools 6.9 for RHEL 7 Server RPMs x86_64'. 2. Navigate to Content > Packages > click on a package in the repository (e.g., @@ -1210,7 +1161,7 @@ def test_positive_select_org_in_any_context(): :BZ: 1860957 - :Steps: + :steps: 1. Set "Any organization" and "Any location" on top 2. Click on Content -> "Sync Status" 3. "Select an Organization" page will come up. @@ -1236,7 +1187,7 @@ def test_positive_sync_repo_and_verify_checksum(session, module_org): :BZ: 1951626 - :Steps: + :steps: 1. Enable and sync repository 2. Go to Products -> Select Action -> Verify Content Checksum diff --git a/tests/foreman/ui/test_rhc.py b/tests/foreman/ui/test_rhc.py index 36d3d64875a..d9aa8e1a111 100644 --- a/tests/foreman/ui/test_rhc.py +++ b/tests/foreman/ui/test_rhc.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: RHCloud-CloudConnector :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from datetime import datetime, timedelta @@ -120,7 +115,7 @@ def test_positive_configure_cloud_connector( :id: 67e45cfe-31bb-51a8-b88f-27918c68f32e - :Steps: + :steps: 1. Navigate to Configure > Inventory Upload 2. Click Configure Cloud Connector @@ -128,8 +123,6 @@ def test_positive_configure_cloud_connector( :expectedresults: The Cloud Connector has been installed and the service is running - :CaseLevel: Integration - :CaseImportance: Critical :BZ: 1818076 diff --git a/tests/foreman/ui/test_rhcloud_insights.py b/tests/foreman/ui/test_rhcloud_insights.py index 153e2a11876..9dcd309055c 100644 --- a/tests/foreman/ui/test_rhcloud_insights.py +++ b/tests/foreman/ui/test_rhcloud_insights.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: System - :CaseComponent: RHCloud-Inventory :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from datetime import datetime @@ -46,7 +41,7 @@ def test_rhcloud_insights_e2e( :id: d952e83c-3faf-4299-a048-2eb6ccb8c9c2 - :Steps: + :steps: 1. Prepare misconfigured machine and upload its data to Insights. 2. In Satellite UI, go to Configure -> Insights -> Sync recommendations. 3. Run remediation for "OpenSSH config permissions" recommendation against host. @@ -141,7 +136,7 @@ def test_insights_reporting_status(): :id: 75629a08-b585-472b-a295-ce497075e519 - :Steps: + :steps: 1. Register a satellite content host with insights. 2. Change 48 hours of wait time to 4 minutes in insights_client_report_status.rb file. See foreman_rh_cloud PR#596. @@ -167,7 +162,7 @@ def test_recommendation_sync_for_satellite(): :id: ee3feba3-c255-42f1-8293-b04d540dcca5 - :Steps: + :steps: 1. Register Satellite with insights.(satellite-installer --register-with-insights) 2. Add RH cloud token in settings. 3. Go to Configure > Insights > Click on Sync recommendations button. @@ -194,7 +189,7 @@ def test_host_sorting_based_on_recommendation_count(): :id: b1725ec1-60db-422e-809d-f81d99ae156e - :Steps: + :steps: 1. Register few satellite content host with insights. 2. Sync Insights recommendations. 3. Go to Hosts > All Host @@ -227,7 +222,7 @@ def test_host_details_page( :customerscenario: true - :Steps: + :steps: 1. Prepare misconfigured machine and upload its data to Insights. 2. Sync insights recommendations. 3. Sync RH Cloud inventory status. diff --git a/tests/foreman/ui/test_rhcloud_inventory.py b/tests/foreman/ui/test_rhcloud_inventory.py index 743ef8925db..e242366bf0a 100644 --- a/tests/foreman/ui/test_rhcloud_inventory.py +++ b/tests/foreman/ui/test_rhcloud_inventory.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: System - :CaseComponent: RHCloud-Inventory :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from datetime import datetime, timedelta @@ -150,7 +145,7 @@ def test_rh_cloud_inventory_settings( :customerscenario: true - :Steps: + :steps: 1. Prepare machine and upload its data to Insights. 2. Go to Configure > Inventory upload > enable “Obfuscate host names” setting. @@ -294,7 +289,7 @@ def test_failed_inventory_upload(): :id: 230d3fc3-2810-4385-b07b-30f9bf632488 - :Steps: + :steps: 1. Register a satellite content host with insights. 2. Change 'DEST' from /var/lib/foreman/red_hat_inventory/uploads/uploader.sh to an invalid url. @@ -318,7 +313,7 @@ def test_rhcloud_inventory_without_manifest(session, module_org, target_sat): :id: 1d90bb24-2380-4653-8ed6-a084fce66d1e - :Steps: + :steps: 1. Don't import manifest to satellite. 3. Go to Configure > Inventory upload > Click on restart button. diff --git a/tests/foreman/ui/test_role.py b/tests/foreman/ui/test_role.py index 86b93fd7baa..dd7c9c9ddf3 100644 --- a/tests/foreman/ui/test_role.py +++ b/tests/foreman/ui/test_role.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: UsersRoles :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random @@ -37,8 +32,6 @@ def test_positive_end_to_end(session, module_org, module_location): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :customerscenario: true :BZ: 1353788 diff --git a/tests/foreman/ui/test_settings.py b/tests/foreman/ui/test_settings.py index 43111a7aadc..aa120bed5d3 100644 --- a/tests/foreman/ui/test_settings.py +++ b/tests/foreman/ui/test_settings.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: Settings :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import math @@ -64,8 +59,6 @@ def test_positive_update_restrict_composite_view(session, setting_update, repo_s :expectedresults: Parameter is updated successfully :CaseImportance: Critical - - :CaseLevel: Acceptance """ property_name = setting_update.name composite_cv = entities.ContentView(composite=True, organization=repo_setup['org']).create() @@ -110,7 +103,6 @@ def test_positive_httpd_proxy_url_update(session, setting_update): :BZ: 1677282 :CaseImportance: Medium - """ property_name = setting_update.name with session: @@ -222,8 +214,6 @@ def test_positive_update_login_page_footer_text(session, setting_update): :customerscenario: true :BZ: 2157869 - - :CaseLevel: Acceptance """ property_name = setting_update.name default_value = setting_update.default @@ -267,8 +257,6 @@ def test_negative_settings_access_to_non_admin(module_target_sat): :expectedresults: Administer -> Settings tab should not be available to non admin users :CaseImportance: Medium - - :CaseLevel: Acceptance """ login = gen_string('alpha') password = gen_string('alpha') @@ -314,8 +302,6 @@ def test_positive_update_email_delivery_method_smtp(): :CaseImportance: Critical - :CaseLevel: Acceptance - :CaseAutomation: NotAutomated """ @@ -349,8 +335,6 @@ def test_negative_update_email_delivery_method_smtp(): :CaseImportance: Critical - :CaseLevel: Acceptance - :CaseAutomation: NotAutomated """ @@ -380,8 +364,6 @@ def test_positive_update_email_delivery_method_sendmail(session, target_sat): :BZ: 2080324 :CaseImportance: Critical - - :CaseLevel: Acceptance """ property_name = "Email" mail_config_default_param = { @@ -444,8 +426,6 @@ def test_negative_update_email_delivery_method_sendmail(): :CaseImportance: Critical - :CaseLevel: Acceptance - :CaseAutomation: NotAutomated """ @@ -476,8 +456,6 @@ def test_positive_email_yaml_config_precedence(): :CaseImportance: Critical - :CaseLevel: Acceptance - :CaseAutomation: NotAutomated """ @@ -489,7 +467,7 @@ def test_negative_update_hostname_with_empty_fact(session, setting_update): :id: e0eaab69-4926-4c1e-b111-30c51ede273e - :Steps: + :steps: 1. Goto settings ->Discovered tab -> Hostname_facts 2. Set empty hostname_facts (without any value) @@ -502,7 +480,6 @@ def test_negative_update_hostname_with_empty_fact(session, setting_update): :expectedresults: Error should be raised on setting empty value for hostname_facts setting - """ new_hostname = "" property_name = setting_update.name @@ -520,7 +497,7 @@ def test_positive_entries_per_page(session, setting_update): :id: 009026b6-7550-40aa-9f78-5eb7f7e3800f - :Steps: + :steps: 1. Navigate to Administer > Settings > General tab 2. Update the entries per page value 3. GoTo Monitor > Tasks Table > Pagination @@ -537,8 +514,6 @@ def test_positive_entries_per_page(session, setting_update): :BZ: 1746221 :CaseImportance: Medium - - :CaseLevel: Acceptance """ property_name = setting_update.name property_value = 19 diff --git a/tests/foreman/ui/test_smartclassparameter.py b/tests/foreman/ui/test_smartclassparameter.py index 98c3ba430c3..adc5169f2ae 100644 --- a/tests/foreman/ui/test_smartclassparameter.py +++ b/tests/foreman/ui/test_smartclassparameter.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Puppet :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from random import choice, uniform @@ -80,8 +75,6 @@ def test_positive_end_to_end(session_puppet_enabled_sat, module_puppet_classes, :expectedresults: All expected basic actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ sc_param = sc_params_list.pop() @@ -201,8 +194,6 @@ def test_positive_create_matcher_attribute_priority( :expectedresults: The YAML output has the value only for fqdn matcher. - :CaseLevel: Integration - :BZ: 1241249 :CaseImportance: Critical @@ -380,8 +371,6 @@ def test_positive_update_matcher_from_attribute( 1. The host/hostgroup is saved with changes. 2. Matcher value in parameter is updated from fqdn/hostgroup. - :CaseLevel: Integration - :CaseImportance: Critical """ sc_param = sc_params_list.pop() @@ -448,8 +437,6 @@ def test_positive_impact_parameter_delete_attribute( 1. The matcher for deleted attribute removed from parameter. 2. On recreating attribute, the matcher should not reappear in parameter. - - :CaseLevel: Integration """ sc_param = sc_params_list.pop() matcher_value = gen_string('alpha') @@ -522,8 +509,6 @@ def test_positive_hidden_value_in_attribute( 4. And the value shown hidden. 5. Parameter is successfully unhidden. - :CaseLevel: Integration - :CaseImportance: Critical """ sc_param = sc_params_list.pop() diff --git a/tests/foreman/ui/test_subnet.py b/tests/foreman/ui/test_subnet.py index f188820d4ba..45886c3ec86 100644 --- a/tests/foreman/ui/test_subnet.py +++ b/tests/foreman/ui/test_subnet.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Networking :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_ipaddr import pytest @@ -40,8 +35,6 @@ def test_positive_end_to_end(session, module_target_sat, module_dom): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ name = gen_string('alpha') diff --git a/tests/foreman/ui/test_subscription.py b/tests/foreman/ui/test_subscription.py index c1fdd1f3c5c..84c7072d0a2 100644 --- a/tests/foreman/ui/test_subscription.py +++ b/tests/foreman/ui/test_subscription.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: SubscriptionManagement :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from tempfile import mkstemp import time @@ -150,8 +145,6 @@ def test_positive_access_with_non_admin_user_without_manifest(test_name, target_ :BZ: 1417082 - :CaseLevel: Integration - :CaseImportance: Critical """ org = entities.Organization().create() @@ -197,8 +190,6 @@ def test_positive_access_with_non_admin_user_with_manifest( :customerscenario: true - :CaseLevel: Integration - :CaseImportance: Critical """ org = function_entitlement_manifest_org @@ -237,8 +228,6 @@ def test_positive_access_manifest_as_another_admin_user( :customerscenario: true - :CaseLevel: Integration - :CaseImportance: High """ org = entities.Organization().create() @@ -299,8 +288,6 @@ def test_positive_view_vdc_subscription_products( :BZ: 1366327 :parametrized: yes - - :CaseLevel: System """ org = function_entitlement_manifest_org lce = entities.LifecycleEnvironment(organization=org).create() @@ -360,8 +347,6 @@ def test_positive_view_vdc_guest_subscription_products( :BZ: 1395788, 1506636, 1487317 :parametrized: yes - - :CaseLevel: System """ org = function_entitlement_manifest_org lce = entities.LifecycleEnvironment(organization=org).create() diff --git a/tests/foreman/ui/test_sync.py b/tests/foreman/ui/test_sync.py index 041a36a011d..c5ea76e0fa4 100644 --- a/tests/foreman/ui/test_sync.py +++ b/tests/foreman/ui/test_sync.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Repositories :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string from nailgun import entities @@ -70,8 +65,6 @@ def test_positive_sync_rh_repos(session, target_sat, module_entitlement_manifest :id: e30f6509-0b65-4bcc-a522-b4f3089d3911 :expectedresults: Sync procedure for RedHat Repos is successful - - :CaseLevel: Integration """ repos = ( target_sat.cli_factory.SatelliteCapsuleRepository(cdn=True), @@ -113,8 +106,6 @@ def test_positive_sync_custom_ostree_repo(session, module_custom_product): :customerscenario: true - :CaseLevel: Integration - :BZ: 1625783 """ repo = entities.Repository( @@ -139,7 +130,7 @@ def test_positive_sync_rh_ostree_repo(session, target_sat, module_entitlement_ma :id: 4d28fff0-5fda-4eee-aa0c-c5af02c31de5 - :Steps: + :steps: 1. Import a valid manifest 2. Enable the OStree repo and sync it @@ -147,8 +138,6 @@ def test_positive_sync_rh_ostree_repo(session, target_sat, module_entitlement_ma :expectedresults: ostree repo should be synced successfully from CDN - :CaseLevel: Integration - :BZ: 1625783 """ target_sat.api_factory.enable_rhrepo_and_fetchid( @@ -175,8 +164,6 @@ def test_positive_sync_docker_via_sync_status(session, module_org): :expectedresults: Sync procedure for specific docker repository is successful - - :CaseLevel: Integration """ product = entities.Product(organization=module_org).create() repo_name = gen_string('alphanumeric') diff --git a/tests/foreman/ui/test_syncplan.py b/tests/foreman/ui/test_syncplan.py index 10e1221cb06..ae4737fc9e1 100644 --- a/tests/foreman/ui/test_syncplan.py +++ b/tests/foreman/ui/test_syncplan.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: SyncPlans :team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from datetime import datetime, timedelta import time @@ -61,8 +56,6 @@ def test_positive_end_to_end(session, module_org, target_sat): :customerscenario: true - :CaseLevel: Integration - :BZ: 1693795 """ plan_name = gen_string('alpha') @@ -115,8 +108,6 @@ def test_positive_end_to_end_custom_cron(session): :id: 48c88529-6318-47b0-97bc-eb46aae0294a :expectedresults: All CRUD actions for component finished successfully - - :CaseLevel: Integration """ plan_name = gen_string('alpha') description = gen_string('alpha') @@ -198,8 +189,6 @@ def test_positive_synchronize_custom_product_custom_cron_real_time(session, modu :id: c551ef9a-6e5a-435a-b24d-e86de203a2bb :expectedresults: Product is synchronized successfully. - - :CaseLevel: System """ plan_name = gen_string('alpha') product = entities.Product(organization=module_org).create() @@ -266,8 +255,6 @@ def test_positive_synchronize_custom_product_custom_cron_past_sync_date( :id: 4d9ed0bf-a63c-44de-846d-7cf302273bcc :expectedresults: Product is synchronized successfully. - - :CaseLevel: System """ plan_name = gen_string('alpha') product = entities.Product(organization=module_org).create() diff --git a/tests/foreman/ui/test_templatesync.py b/tests/foreman/ui/test_templatesync.py index cfbea828a95..7f0cc0e70ac 100644 --- a/tests/foreman/ui/test_templatesync.py +++ b/tests/foreman/ui/test_templatesync.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Integration - :CaseComponent: TemplatesPlugin :Team: Endeavour -:TestType: Functional - -:Upstream: No """ from fauxfactory import gen_string from nailgun import entities @@ -45,7 +40,7 @@ def test_positive_import_templates(session, templates_org, templates_loc): :bz: 1778181, 1778139 - :Steps: + :steps: 1. Navigate to Host -> Sync Templates, and choose Import. 2. Select fields: @@ -102,7 +97,7 @@ def test_positive_export_templates(session, create_import_export_local_dir, targ :bz: 1778139 - :Steps: + :steps: 1. Navigate to Host -> Sync Templates, and choose Export. 2. Select fields: @@ -163,7 +158,7 @@ def test_positive_export_filtered_templates_to_git(session, git_repository, git_ :id: e4de338a-9ab9-492e-ac42-6cc2ebcd1792 - :Steps: + :steps: 1. Export only the templates matching with regex e.g: `^atomic.*` to git repo. :expectedresults: diff --git a/tests/foreman/ui/test_user.py b/tests/foreman/ui/test_user.py index 892024ffc8d..21147e88adc 100644 --- a/tests/foreman/ui/test_user.py +++ b/tests/foreman/ui/test_user.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: UsersRoles :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import random @@ -36,8 +31,6 @@ def test_positive_end_to_end(session, target_sat, test_name, module_org, module_ :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ name = gen_string('alpha') @@ -111,8 +104,6 @@ def test_positive_create_with_multiple_roles(session, target_sat): :expectedresults: User is created successfully and has proper roles assigned - - :CaseLevel: Integration """ name = gen_string('alpha') role1 = gen_string('alpha') @@ -142,8 +133,6 @@ def test_positive_create_with_all_roles(session): :id: 814593ca-1566-45ea-9eff-e880183b1ee3 :expectedresults: User is created successfully - - :CaseLevel: Integration """ name = gen_string('alpha') password = gen_string('alpha') @@ -169,8 +158,6 @@ def test_positive_create_with_multiple_orgs(session, target_sat): :id: d74c0284-3995-4a4a-8746-00858282bf5d :expectedresults: User is created successfully - - :CaseLevel: Integration """ name = gen_string('alpha') org_name1 = gen_string('alpha') @@ -205,8 +192,6 @@ def test_positive_update_with_multiple_roles(session, target_sat): :id: 127fb368-09fd-4f10-8319-566a1bcb5cd2 :expectedresults: User is updated successfully - - :CaseLevel: Integration """ name = gen_string('alpha') role_names = [target_sat.api.Role().create().name for _ in range(3)] @@ -233,8 +218,6 @@ def test_positive_update_with_all_roles(session): :id: cd7a9cfb-a700-45f2-a11d-bba6be3c810d :expectedresults: User is updated successfully - - :CaseLevel: Integration """ name = gen_string('alpha') password = gen_string('alpha') @@ -260,8 +243,6 @@ def test_positive_update_orgs(session, target_sat): :id: a207188d-1ad1-4ff1-9906-bae1d91104fd :expectedresults: User is updated - - :CaseLevel: Integration """ name = gen_string('alpha') password = gen_string('alpha') @@ -295,8 +276,6 @@ def test_positive_create_product_with_limited_user_permission( :customerscenario: true - :CaseLevel: Component - :CaseImportance: High :BZ: 1771937 @@ -344,8 +323,6 @@ def test_personal_access_token_admin(): 1. Should show output of the api endpoint 2. When revoked, authentication error - :CaseLevel: System - :CaseImportance: High """ @@ -369,8 +346,6 @@ def test_positive_personal_access_token_user_with_role(): 2. When an incorrect role and end point is used, missing permission should be displayed. - :CaseLevel: System - :CaseImportance: High """ @@ -389,7 +364,5 @@ def test_expired_personal_access_token(): :expectedresults: Authentication error - :CaseLevel: System - :CaseImportance: Medium """ diff --git a/tests/foreman/ui/test_usergroup.py b/tests/foreman/ui/test_usergroup.py index f5e2eaa9a96..092126ae868 100644 --- a/tests/foreman/ui/test_usergroup.py +++ b/tests/foreman/ui/test_usergroup.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: UsersRoles :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string, gen_utf8 from nailgun import entities @@ -29,8 +24,6 @@ def test_positive_delete_with_user(session, module_org, module_location): :id: 2bda3db5-f54f-412f-831f-8e005631f271 :expectedresults: Usergroup is deleted but added user is not - - :CaseLevel: Integration """ user_name = gen_string('alpha') group_name = gen_utf8(smp=False) @@ -60,8 +53,6 @@ def test_positive_end_to_end(session, module_org, module_location): :expectedresults: All expected CRUD actions finished successfully - :CaseLevel: Integration - :CaseImportance: High """ name = gen_string('alpha') diff --git a/tests/foreman/ui/test_webhook.py b/tests/foreman/ui/test_webhook.py index f3df30aef78..c0e3b470c20 100644 --- a/tests/foreman/ui/test_webhook.py +++ b/tests/foreman/ui/test_webhook.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: HooksandWebhooks :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string, gen_url import pytest diff --git a/tests/foreman/virtwho/api/test_esx.py b/tests/foreman/virtwho/api/test_esx.py index 1eda85fde0b..cdea45e72b4 100644 --- a/tests/foreman/virtwho/api/test_esx.py +++ b/tests/foreman/virtwho/api/test_esx.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -43,8 +38,6 @@ def test_positive_deploy_configure_by_id_script( :expectedresults: Config can be created and deployed - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_api.status == 'unknown' @@ -94,8 +87,6 @@ def test_positive_debug_option( :expectedresults: debug option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ options = {'true': '1', 'false': '0', '1': '1', '0': '0'} @@ -120,8 +111,6 @@ def test_positive_interval_option( :expectedresults: interval option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ options = { @@ -155,8 +144,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ # esx and rhevm support hwuuid option @@ -183,8 +170,6 @@ def test_positive_filter_option( :expectedresults: filter and filter_hosts can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ whitelist = {'filtering_mode': '1', 'whitelist': '.*redhat.com'} @@ -235,8 +220,6 @@ def test_positive_proxy_option( :expectedresults: http_proxy/https_proxy and no_proxy option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium :BZ: 1902199 @@ -282,8 +265,6 @@ def test_positive_configure_organization_list( :expectedresults: Config can be searched in org list - :CaseLevel: Integration - :CaseImportance: Medium """ command = get_configure_command(virtwho_config_api.id, default_org.name) @@ -303,8 +284,6 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( :expectedresults: Config can be created and deployed without any error - :CaseLevel: Integration - :CaseImportance: High :BZ: 1870816,1959136 @@ -358,8 +337,6 @@ def test_positive_remove_env_option( the option "env=" should be removed from etc/virt-who.d/virt-who.conf /var/log/messages should not display warning message - :CaseLevel: Integration - :customerscenario: true :CaseImportance: Medium diff --git a/tests/foreman/virtwho/api/test_esx_sca.py b/tests/foreman/virtwho/api/test_esx_sca.py index 3ac780254f0..290e67242f5 100644 --- a/tests/foreman/virtwho/api/test_esx_sca.py +++ b/tests/foreman/virtwho/api/test_esx_sca.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :Team: Phoenix -:TestType: Functional - -:Upstream: No """ import pytest @@ -41,8 +36,6 @@ def test_positive_deploy_configure_by_id_script( :expectedresults: Config can be created and deployed - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_api.status == 'unknown' @@ -65,8 +58,6 @@ def test_positive_debug_option( :expectedresults: debug option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ options = {'0': '0', '1': '1', 'false': '0', 'true': '1'} @@ -91,8 +82,6 @@ def test_positive_interval_option( :expectedresults: interval option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ options = { @@ -126,8 +115,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ for value in ['uuid', 'hostname']: @@ -156,8 +143,6 @@ def test_positive_filter_option( 1. filter and filter_hosts can be updated. 2. create virt-who config with filter and filter_hosts options work well. - :CaseLevel: Integration - :CaseImportance: Medium """ regex = '.*redhat.com' @@ -253,8 +238,6 @@ def test_positive_proxy_option(self, module_sca_manifest_org, form_data_api, tar 1. http_proxy and no_proxy option can be updated. 2. create virt-who config with http_proxy and no_proxy options work well. - :CaseLevel: Integration - :CaseImportance: Medium :BZ: 1902199 @@ -330,8 +313,6 @@ def test_positive_configure_organization_list( :expectedresults: Config can be searched in org list - :CaseLevel: Integration - :CaseImportance: Medium """ command = get_configure_command(virtwho_config_api.id, module_sca_manifest_org.name) @@ -351,8 +332,6 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( :expectedresults: Config can be created and deployed without any error - :CaseLevel: Integration - :CaseImportance: High :BZ: 1870816,1959136 @@ -406,8 +385,6 @@ def test_positive_remove_env_option( 1. the option "env=" should be removed from etc/virt-who.d/virt-who.conf 2. /var/log/messages should not display warning message - :CaseLevel: Integration - :customerscenario: true :CaseImportance: Medium diff --git a/tests/foreman/virtwho/api/test_hyperv.py b/tests/foreman/virtwho/api/test_hyperv.py index e76e12f2669..6d93f1ddb9a 100644 --- a/tests/foreman/virtwho/api/test_hyperv.py +++ b/tests/foreman/virtwho/api/test_hyperv.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -39,8 +34,6 @@ def test_positive_deploy_configure_by_id_script( :expectedresults: Config can be created and deployed - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_api.status == 'unknown' @@ -90,8 +83,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ values = ['uuid', 'hostname'] diff --git a/tests/foreman/virtwho/api/test_hyperv_sca.py b/tests/foreman/virtwho/api/test_hyperv_sca.py index 68dd7c0e4f1..715ac92cd12 100644 --- a/tests/foreman/virtwho/api/test_hyperv_sca.py +++ b/tests/foreman/virtwho/api/test_hyperv_sca.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -38,8 +33,6 @@ def test_positive_deploy_configure_by_id_script( :expectedresults: Config can be created and deployed - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_api.status == 'unknown' @@ -62,8 +55,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ for value in ['uuid', 'hostname']: diff --git a/tests/foreman/virtwho/api/test_kubevirt.py b/tests/foreman/virtwho/api/test_kubevirt.py index b1bfe96a34b..ef1e7ac578d 100644 --- a/tests/foreman/virtwho/api/test_kubevirt.py +++ b/tests/foreman/virtwho/api/test_kubevirt.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -40,8 +35,6 @@ def test_positive_deploy_configure_by_id_script( :expectedresults: Config can be created and deployed - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_api.status == 'unknown' @@ -91,8 +84,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ values = ['uuid', 'hostname'] diff --git a/tests/foreman/virtwho/api/test_kubevirt_sca.py b/tests/foreman/virtwho/api/test_kubevirt_sca.py index 364a637be5c..51628052cd9 100644 --- a/tests/foreman/virtwho/api/test_kubevirt_sca.py +++ b/tests/foreman/virtwho/api/test_kubevirt_sca.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - -:Upstream: No """ import pytest @@ -36,8 +31,6 @@ def test_positive_deploy_configure_by_id_script( :expectedresults: Config can be created and deployed - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_api.status == 'unknown' @@ -60,8 +53,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ for value in ['uuid', 'hostname']: diff --git a/tests/foreman/virtwho/api/test_libvirt.py b/tests/foreman/virtwho/api/test_libvirt.py index 2d05ebd5e49..b551e0e81dc 100644 --- a/tests/foreman/virtwho/api/test_libvirt.py +++ b/tests/foreman/virtwho/api/test_libvirt.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -39,8 +34,6 @@ def test_positive_deploy_configure_by_id_script( :expectedresults: Config can be created and deployed - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_api.status == 'unknown' @@ -90,8 +83,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ values = ['uuid', 'hostname'] diff --git a/tests/foreman/virtwho/api/test_libvirt_sca.py b/tests/foreman/virtwho/api/test_libvirt_sca.py index d805b4da5ef..f88edd8d478 100644 --- a/tests/foreman/virtwho/api/test_libvirt_sca.py +++ b/tests/foreman/virtwho/api/test_libvirt_sca.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - -:Upstream: No """ import pytest @@ -36,8 +31,6 @@ def test_positive_deploy_configure_by_id_script( :expectedresults: Config can be created and deployed - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_api.status == 'unknown' @@ -58,8 +51,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ for value in ['uuid', 'hostname']: diff --git a/tests/foreman/virtwho/api/test_nutanix.py b/tests/foreman/virtwho/api/test_nutanix.py index c065f164a5a..e40433b1c57 100644 --- a/tests/foreman/virtwho/api/test_nutanix.py +++ b/tests/foreman/virtwho/api/test_nutanix.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -43,8 +38,6 @@ def test_positive_deploy_configure_by_id_script( :expectedresults: Config can be created and deployed - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_api.status == 'unknown' @@ -98,8 +91,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ values = ['uuid', 'hostname'] @@ -126,8 +117,6 @@ def test_positive_prism_central_deploy_configure_by_id_script( Config can be created and deployed The prism_central has been set in /etc/virt-who.d/vir-who.conf file - :CaseLevel: Integration - :CaseImportance: High """ form_data_api['prism_flavor'] = "central" @@ -198,8 +187,6 @@ def test_positive_prism_central_prism_central_option( :expectedresults: prism_flavor option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ value = 'central' @@ -228,7 +215,6 @@ def test_positive_ahv_internal_debug_option( 5. message Host UUID {system_uuid} found for VM: {guest_uuid} exist in rhsm.log 6. ahv_internal_debug bas been set to true in virt-who-config-X.conf 7. warning message does not exist in log file /var/log/rhsm/rhsm.log - :CaseLevel: Integration :CaseImportance: Medium diff --git a/tests/foreman/virtwho/api/test_nutanix_sca.py b/tests/foreman/virtwho/api/test_nutanix_sca.py index c075db7f795..14b6d2a04fd 100644 --- a/tests/foreman/virtwho/api/test_nutanix_sca.py +++ b/tests/foreman/virtwho/api/test_nutanix_sca.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :Team: Phoenix -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -39,8 +34,6 @@ def test_positive_deploy_configure_by_id_script( :expectedresults: Config can be created and deployed - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_api.status == 'unknown' @@ -63,8 +56,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ for value in ['uuid', 'hostname']: @@ -90,8 +81,6 @@ def test_positive_prism_central_deploy_configure_by_id_script( Config can be created and deployed The prism_central has been set in /etc/virt-who.d/vir-who.conf file - :CaseLevel: Integration - :CaseImportance: High """ form_data_api['prism_flavor'] = "central" @@ -135,8 +124,6 @@ def test_positive_prism_central_prism_central_option( :expectedresults: prism_flavor option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ value = 'central' diff --git a/tests/foreman/virtwho/cli/test_esx.py b/tests/foreman/virtwho/cli/test_esx.py index b8e110af4f4..c661d31cd67 100644 --- a/tests/foreman/virtwho/cli/test_esx.py +++ b/tests/foreman/virtwho/cli/test_esx.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import re @@ -48,8 +43,6 @@ def test_positive_deploy_configure_by_id_script( :expectedresults: Config can be created and deployed - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_cli['status'] == 'No Report Yet' @@ -92,8 +85,6 @@ def test_positive_debug_option(self, default_org, form_data_cli, target_sat): :expectedresults: debug option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ virtwho_config = target_sat.cli.VirtWhoConfig.create(form_data_cli)['general-information'] @@ -123,8 +114,6 @@ def test_positive_interval_option( :expectedresults: interval option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ options = { @@ -155,8 +144,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ # esx and rhevm support hwuuid option @@ -184,8 +171,6 @@ def test_positive_filter_option( :expectedresults: filter and filter_hosts can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ regex = '.*redhat.com' @@ -237,8 +222,6 @@ def test_positive_proxy_option( :expectedresults: http_proxy and no_proxy option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium :BZ: 1902199 @@ -281,8 +264,6 @@ def test_positive_rhsm_option(self, default_org, form_data_cli, virtwho_config_c rhsm_hostname, rhsm_prefix are ecpected rhsm_username is not a login account - :CaseLevel: Integration - :CaseImportance: Medium """ config_file = get_configure_file(virtwho_config_cli['id']) @@ -304,8 +285,6 @@ def test_positive_post_hypervisors(self, function_org, target_sat): :expectedresults: hypervisor/guest json can be posted and the task is success status - :CaseLevel: Integration - :customerscenario: true :CaseImportance: Medium @@ -335,8 +314,6 @@ def test_positive_foreman_packages_protection( virt-who packages can be installed the virt-who plugin can be deployed successfully - :CaseLevel: Integration - :customerscenario: true :CaseImportance: Medium @@ -366,8 +343,6 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( :expectedresults: Config can be created and deployed without any error - :CaseLevel: Integration - :CaseImportance: High :BZ: 1870816,1959136 @@ -416,8 +391,6 @@ def test_positive_remove_env_option( the option "env=" should be removed from etc/virt-who.d/virt-who.conf /var/log/messages should not display warning message - :CaseLevel: Integration - :CaseImportance: Medium :BZ: 1834897 diff --git a/tests/foreman/virtwho/cli/test_esx_sca.py b/tests/foreman/virtwho/cli/test_esx_sca.py index 1a54ae9d569..b09cabef912 100644 --- a/tests/foreman/virtwho/cli/test_esx_sca.py +++ b/tests/foreman/virtwho/cli/test_esx_sca.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :Team: Phoenix -:TestType: Functional - -:Upstream: No """ import re @@ -49,8 +44,6 @@ def test_positive_deploy_configure_by_id_script( 1. Config can be created and deployed 2. Config can be created, fetch and deploy - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_cli['status'] == 'No Report Yet' @@ -69,8 +62,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ for value in ['uuid', 'hostname']: @@ -96,8 +87,6 @@ def test_positive_debug_option( :expectedresults: debug option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ assert virtwho_config_cli['name'] == form_data_cli['name'] @@ -120,8 +109,6 @@ def test_positive_name_option( :expectedresults: name option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ assert virtwho_config_cli['name'] == form_data_cli['name'] @@ -145,8 +132,6 @@ def test_positive_interval_option( :expectedresults: interval option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ options = { @@ -187,8 +172,6 @@ def test_positive_filter_option( 1. filter and filter_hosts can be updated. 2. create virt-who config with filter and filter_hosts options work well. - :CaseLevel: Integration - :CaseImportance: Medium """ regex = '.*redhat.com' @@ -273,8 +256,6 @@ def test_positive_proxy_option( 1. http_proxy and no_proxy option can be updated. 2. create virt-who config with http_proxy and no_proxy options work well. - :CaseLevel: Integration - :CaseImportance: Medium :BZ: 1902199 @@ -355,8 +336,6 @@ def test_positive_rhsm_option( 1. rhsm_hostname, rhsm_prefix are expected 2. rhsm_username is not a login account - :CaseLevel: Integration - :CaseImportance: Medium """ config_file = get_configure_file(virtwho_config_cli['id']) @@ -378,8 +357,6 @@ def test_positive_post_hypervisors(self, function_org, target_sat): :expectedresults: hypervisor/guest json can be posted and the task is success status - :CaseLevel: Integration - :customerscenario: true :CaseImportance: Medium @@ -409,8 +386,6 @@ def test_positive_foreman_packages_protection( 1. virt-who packages can be installed 2. the virt-who plugin can be deployed successfully - :CaseLevel: Integration - :customerscenario: true :CaseImportance: Medium @@ -437,8 +412,6 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( :expectedresults: Config can be created and deployed without any error - :CaseLevel: Integration - :CaseImportance: High :BZ: 1870816,1959136 @@ -493,8 +466,6 @@ def test_positive_remove_env_option( 1. the option "env=" should be removed from etc/virt-who.d/virt-who.conf 2. /var/log/messages should not display warning message - :CaseLevel: Integration - :CaseImportance: Medium :BZ: 1834897 diff --git a/tests/foreman/virtwho/cli/test_hyperv.py b/tests/foreman/virtwho/cli/test_hyperv.py index ffc421506f0..657ee3e04f6 100644 --- a/tests/foreman/virtwho/cli/test_hyperv.py +++ b/tests/foreman/virtwho/cli/test_hyperv.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -41,8 +36,6 @@ def test_positive_deploy_configure_by_id_script( 1. Config can be created and deployed 2. Config can be created, fetch and deploy - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_cli['status'] == 'No Report Yet' @@ -81,8 +74,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ values = ['uuid', 'hostname'] diff --git a/tests/foreman/virtwho/cli/test_hyperv_sca.py b/tests/foreman/virtwho/cli/test_hyperv_sca.py index 4b2725c5432..7c59213485d 100644 --- a/tests/foreman/virtwho/cli/test_hyperv_sca.py +++ b/tests/foreman/virtwho/cli/test_hyperv_sca.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -40,8 +35,6 @@ def test_positive_deploy_configure_by_id_script( 1. Config can be created and deployed 2. Config can be created, fetch and deploy - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_cli['status'] == 'No Report Yet' @@ -61,8 +54,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ for value in ['uuid', 'hostname']: diff --git a/tests/foreman/virtwho/cli/test_kubevirt.py b/tests/foreman/virtwho/cli/test_kubevirt.py index 4b8a5c57f06..f29a22e4a16 100644 --- a/tests/foreman/virtwho/cli/test_kubevirt.py +++ b/tests/foreman/virtwho/cli/test_kubevirt.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -41,8 +36,6 @@ def test_positive_deploy_configure_by_id_script( 1. Config can be created and deployed 2. Config can be created, fetch and deploy - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_cli['status'] == 'No Report Yet' @@ -81,8 +74,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ values = ['uuid', 'hostname'] diff --git a/tests/foreman/virtwho/cli/test_kubevirt_sca.py b/tests/foreman/virtwho/cli/test_kubevirt_sca.py index 2d7fd4fca97..d682b5f9159 100644 --- a/tests/foreman/virtwho/cli/test_kubevirt_sca.py +++ b/tests/foreman/virtwho/cli/test_kubevirt_sca.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - -:Upstream: No """ import pytest @@ -38,8 +33,6 @@ def test_positive_deploy_configure_by_id_script( 1. Config can be created and deployed 2. Config can be created, fetch and deploy - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_cli['status'] == 'No Report Yet' @@ -57,8 +50,6 @@ def test_positive_hypervisor_id_option( :id: b60f449d-6698-4a3a-be07-7440c2d9ba20 :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ for value in ['uuid', 'hostname']: diff --git a/tests/foreman/virtwho/cli/test_libvirt.py b/tests/foreman/virtwho/cli/test_libvirt.py index 5c9cfda2dbd..70ac4056fa3 100644 --- a/tests/foreman/virtwho/cli/test_libvirt.py +++ b/tests/foreman/virtwho/cli/test_libvirt.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -41,8 +36,6 @@ def test_positive_deploy_configure_by_id_script( 1. Config can be created and deployed 2. Config can be created, fetch and deploy - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_cli['status'] == 'No Report Yet' @@ -81,8 +74,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ values = ['uuid', 'hostname'] diff --git a/tests/foreman/virtwho/cli/test_libvirt_sca.py b/tests/foreman/virtwho/cli/test_libvirt_sca.py index 3532bb7983c..2bfeda30801 100644 --- a/tests/foreman/virtwho/cli/test_libvirt_sca.py +++ b/tests/foreman/virtwho/cli/test_libvirt_sca.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - -:Upstream: No """ import pytest @@ -38,8 +33,6 @@ def test_positive_deploy_configure_by_id_script( 1. Config can be created and deployed 2. Config can be created, fetch and deploy - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_cli['status'] == 'No Report Yet' @@ -58,8 +51,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ for value in ['uuid', 'hostname']: diff --git a/tests/foreman/virtwho/cli/test_nutanix.py b/tests/foreman/virtwho/cli/test_nutanix.py index 052101f792f..b5478912ab7 100644 --- a/tests/foreman/virtwho/cli/test_nutanix.py +++ b/tests/foreman/virtwho/cli/test_nutanix.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -44,8 +39,6 @@ def test_positive_deploy_configure_by_id_script( 1. Config can be created and deployed 2. Config can be created, fetch and deploy - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_cli['status'] == 'No Report Yet' @@ -84,8 +77,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ values = ['uuid', 'hostname'] @@ -115,8 +106,6 @@ def test_positive_prism_central_deploy_configure_by_id_script( Config can be created and deployed The prism_central has been set in /etc/virt-who.d/vir-who.conf file - :CaseLevel: Integration - :CaseImportance: High """ form_data_cli['prism-flavor'] = "central" @@ -171,8 +160,6 @@ def test_positive_prism_central_prism_central_option( :expectedresults: prism_central option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ value = 'central' @@ -207,11 +194,11 @@ def test_positive_ahv_internal_debug_option( 5. message Host UUID {system_uuid} found for VM: {guest_uuid} exist in rhsm.log 6. ahv_internal_debug bas been set to true in virt-who-config-X.conf 7. warning message does not exist in log file /var/log/rhsm/rhsm.log - :CaseLevel: Integration :CaseImportance: Medium :BZ: 2141719 + :customerscenario: true """ command = get_configure_command(virtwho_config_cli['id'], default_org.name) diff --git a/tests/foreman/virtwho/cli/test_nutanix_sca.py b/tests/foreman/virtwho/cli/test_nutanix_sca.py index f41daa8d75c..360aac0f676 100644 --- a/tests/foreman/virtwho/cli/test_nutanix_sca.py +++ b/tests/foreman/virtwho/cli/test_nutanix_sca.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :Team: Phoenix -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -41,8 +36,6 @@ def test_positive_deploy_configure_by_id_script( 1. Config can be created and deployed 2. Config can be created, fetch and deploy - :CaseLevel: Integration - :CaseImportance: High """ assert virtwho_config_cli['status'] == 'No Report Yet' @@ -61,8 +54,6 @@ def test_positive_hypervisor_id_option( :expectedresults: hypervisor_id option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ for value in ['uuid', 'hostname']: @@ -92,8 +83,6 @@ def test_positive_prism_central_deploy_configure_by_id_script( 2. The prism_central has been set in /etc/virt-who.d/vir-who.conf file 3. Config can be created, fetch and deploy - :CaseLevel: Integration - :CaseImportance: High """ form_data_cli['prism-flavor'] = "central" @@ -135,8 +124,6 @@ def test_positive_prism_element_prism_central_option( :expectedresults: prism_central option can be updated. - :CaseLevel: Integration - :CaseImportance: Medium """ value = 'central' diff --git a/tests/foreman/virtwho/conftest.py b/tests/foreman/virtwho/conftest.py index 8e422b22940..133fa0026a0 100644 --- a/tests/foreman/virtwho/conftest.py +++ b/tests/foreman/virtwho/conftest.py @@ -49,7 +49,6 @@ def test_foo(session): with session: # your ui test steps here session.architecture.create({'name': 'bar'}) - """ return Session(test_name, module_user.login, module_user.password) @@ -97,6 +96,5 @@ def test_foo(session): with session: # your ui test steps here session.architecture.create({'name': 'bar'}) - """ return Session(test_name, module_user_sca.login, module_user_sca.password) diff --git a/tests/foreman/virtwho/ui/test_esx.py b/tests/foreman/virtwho/ui/test_esx.py index 7c669a62022..5baba61e97d 100644 --- a/tests/foreman/virtwho/ui/test_esx.py +++ b/tests/foreman/virtwho/ui/test_esx.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from datetime import datetime @@ -59,8 +54,6 @@ def test_positive_deploy_configure_by_id_script( 4. Virtual sku can be generated and attached 5. Config can be deleted - :CaseLevel: Integration - :CaseImportance: High """ hypervisor_name, guest_name = deploy_type_ui @@ -93,8 +86,6 @@ def test_positive_debug_option(self, default_org, virtwho_config_ui, org_session 1. if debug is checked, VIRTWHO_DEBUG=1 in /etc/sysconfig/virt-who 2. if debug is unchecked, VIRTWHO_DEBUG=0 in /etc/sysconfig/virt-who - :CaseLevel: Integration - :CaseImportance: Medium """ name = form_data_ui['name'] @@ -124,8 +115,6 @@ def test_positive_interval_option( VIRTWHO_INTERVAL can be changed in /etc/sysconfig/virt-who if the dropdown option is selected to Every 2/4/8/12/24 hours, Every 2/3 days. - :CaseLevel: Integration - :CaseImportance: Medium """ name = form_data_ui['name'] @@ -162,8 +151,6 @@ def test_positive_hypervisor_id_option( hypervisor_id can be changed in virt-who-config-{}.conf if the dropdown option is selected to uuid/hwuuid/hostname. - :CaseLevel: Integration - :CaseImportance: Medium """ name = form_data_ui['name'] @@ -193,8 +180,6 @@ def test_positive_filtering_option( 1. if filtering is selected to Whitelist, 'Filter hosts' can be set. 2. if filtering is selected to Blacklist, 'Exclude hosts' can be set. - :CaseLevel: Integration - :CaseImportance: Medium :BZ: 1735670 @@ -239,8 +224,6 @@ def test_positive_proxy_option(self, default_org, virtwho_config_ui, org_session :expectedresults: http_proxy/https_proxy and NO_PROXY will be setting in /etc/sysconfig/virt-who. - :CaseLevel: Integration - :CaseImportance: Medium """ https_proxy, https_proxy_name, https_proxy_id = create_http_proxy(org=default_org) @@ -279,8 +262,6 @@ def test_positive_virtwho_roles(self, org_session): :expectedresults: 'Virt-who Manager', 'Virt-who Reporter', 'Virt-who Viewer' existing - :CaseLevel: Integration - :CaseImportance: Low """ roles = { @@ -311,7 +292,7 @@ def test_positive_virtwho_configs_widget(self, default_org, org_session, form_da :id: 5d61ce00-a640-4823-89d4-7b1d02b50ea6 - :Steps: + :steps: 1. Create a Virt-who Configuration 2. Navigate Monitor -> Dashboard @@ -319,8 +300,6 @@ def test_positive_virtwho_configs_widget(self, default_org, org_session, form_da :expectedresults: The widget is updated with all details. - :CaseLevel: Integration - :CaseImportance: Low """ org_name = gen_string('alpha') @@ -572,8 +551,6 @@ def test_positive_overview_label_name(self, default_org, form_data_ui, org_sessi :BZ: 1649928 - :CaseLevel: Integration - :customerscenario: true :CaseImportance: Medium @@ -638,8 +615,6 @@ def test_positive_last_checkin_status( :BZ: 1652323 - :CaseLevel: Integration - :customerscenario: true :CaseImportance: Medium @@ -674,8 +649,6 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( :expectedresults: Config can be created and deployed without any error - :CaseLevel: Integration - :CaseImportance: High :BZ: 1870816,1959136 @@ -730,8 +703,6 @@ def test_positive_remove_env_option( the option "env=" should be removed from etc/virt-who.d/virt-who.conf /var/log/messages should not display warning message - :CaseLevel: Integration - :CaseImportance: Medium :BZ: 1834897 diff --git a/tests/foreman/virtwho/ui/test_esx_sca.py b/tests/foreman/virtwho/ui/test_esx_sca.py index 068b8c9c84c..c0f09e740eb 100644 --- a/tests/foreman/virtwho/ui/test_esx_sca.py +++ b/tests/foreman/virtwho/ui/test_esx_sca.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - -:Upstream: No """ from datetime import datetime @@ -57,8 +52,6 @@ def test_positive_deploy_configure_by_id_script( 4. Virtual sku can be generated and attached 5. Config can be deleted - :CaseLevel: Integration - :CaseImportance: High """ assert org_session.virtwho_configure.search(form_data_ui['name'])[0]['Status'] == 'ok' @@ -75,8 +68,6 @@ def test_positive_debug_option( 1. if debug is checked, VIRTWHO_DEBUG=1 in /etc/sysconfig/virt-who 2. if debug is unchecked, VIRTWHO_DEBUG=0 in /etc/sysconfig/virt-who - :CaseLevel: Integration - :CaseImportance: Medium """ name = form_data_ui['name'] @@ -106,8 +97,6 @@ def test_positive_interval_option( VIRTWHO_INTERVAL can be changed in /etc/sysconfig/virt-who if the dropdown option is selected to Every 2/4/8/12/24 hours, Every 2/3 days. - :CaseLevel: Integration - :CaseImportance: Medium """ name = form_data_ui['name'] @@ -144,8 +133,6 @@ def test_positive_hypervisor_id_option( hypervisor_id can be changed in virt-who-config-{}.conf if the dropdown option is selected to uuid/hwuuid/hostname. - :CaseLevel: Integration - :CaseImportance: Medium """ name = form_data_ui['name'] @@ -181,7 +168,6 @@ def test_positive_filtering_option( 'Filter hosts' can be set. 4. Create virtwho config if filtering is selected to Blacklist, 'Exclude hosts' can be set. - :CaseLevel: Integration :CaseImportance: Medium @@ -267,8 +253,6 @@ def test_positive_last_checkin_status( :BZ: 1652323 - :CaseLevel: Integration - :customerscenario: true :CaseImportance: Medium @@ -305,8 +289,6 @@ def test_positive_remove_env_option( 1. the option "env=" should be removed from etc/virt-who.d/virt-who.conf 2. /var/log/messages should not display warning message - :CaseLevel: Integration - :CaseImportance: Medium :BZ: 1834897 @@ -344,8 +326,6 @@ def test_positive_virtwho_roles(self, org_session): :expectedresults: 'Virt-who Manager', 'Virt-who Reporter', 'Virt-who Viewer' existing - :CaseLevel: Integration - :CaseImportance: Low """ roles = { @@ -385,8 +365,6 @@ def test_positive_delete_configure(self, module_sca_manifest_org, org_session, f 1. Verify the virt-who server can no longer connect to the Satellite. - :CaseLevel: Integration - :CaseImportance: Low """ name = gen_string('alpha') @@ -417,8 +395,6 @@ def test_positive_virtwho_reporter_role( to upload the report, it can be used if you configure virt-who manually and want to use user that has locked down account. - :CaseLevel: Integration - :CaseImportance: Low """ username = gen_string('alpha') @@ -477,8 +453,6 @@ def test_positive_virtwho_viewer_role( including their configuration scripts, which means viewers could still deploy the virt-who instances for existing virt-who configurations. - :CaseLevel: Integration - :CaseImportance: Low """ username = gen_string('alpha') @@ -541,7 +515,6 @@ def test_positive_virtwho_manager_role( :expectedresults: Virt-who Manager Role granting all permissions to manage virt-who configurations, user needs this role to create, delete or update configurations. - :CaseLevel: Integration :CaseImportance: Low """ @@ -605,8 +578,6 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( :expectedresults: Config can be created and deployed without any error - :CaseLevel: Integration - :CaseImportance: High :BZ: 1870816,1959136 diff --git a/tests/foreman/virtwho/ui/test_hyperv.py b/tests/foreman/virtwho/ui/test_hyperv.py index a35878e2fc2..8348540326f 100644 --- a/tests/foreman/virtwho/ui/test_hyperv.py +++ b/tests/foreman/virtwho/ui/test_hyperv.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -45,8 +40,6 @@ def test_positive_deploy_configure_by_id_script( 4. Virtual sku can be generated and attached 5. Config can be deleted - :CaseLevel: Integration - :CaseImportance: High """ hypervisor_name, guest_name = deploy_type_ui @@ -81,8 +74,6 @@ def test_positive_hypervisor_id_option( hypervisor_id can be changed in virt-who-config-{}.conf if the dropdown option is selected to uuid/hwuuid/hostname. - :CaseLevel: Integration - :CaseImportance: Medium """ name = form_data_ui['name'] diff --git a/tests/foreman/virtwho/ui/test_hyperv_sca.py b/tests/foreman/virtwho/ui/test_hyperv_sca.py index 3e7b0f01c5e..a5b560c99a1 100644 --- a/tests/foreman/virtwho/ui/test_hyperv_sca.py +++ b/tests/foreman/virtwho/ui/test_hyperv_sca.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - -:Upstream: No """ import pytest @@ -42,8 +37,6 @@ def test_positive_deploy_configure_by_id_script( 4. Virtual sku can be generated and attached 5. Config can be deleted - :CaseLevel: Integration - :CaseImportance: High """ assert org_session.virtwho_configure.search(form_data_ui['name'])[0]['Status'] == 'ok' @@ -60,8 +53,6 @@ def test_positive_hypervisor_id_option( 1. hypervisor_id can be changed in virt-who-config-{}.conf if the dropdown option is selected to uuid/hwuuid/hostname. - :CaseLevel: Integration - :CaseImportance: Medium """ name = form_data_ui['name'] diff --git a/tests/foreman/virtwho/ui/test_kubevirt.py b/tests/foreman/virtwho/ui/test_kubevirt.py index 19d5bce7f63..e12c4774fc8 100644 --- a/tests/foreman/virtwho/ui/test_kubevirt.py +++ b/tests/foreman/virtwho/ui/test_kubevirt.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -45,8 +40,6 @@ def test_positive_deploy_configure_by_id_script( 4. Virtual sku can be generated and attached 5. Config can be deleted - :CaseLevel: Integration - :CaseImportance: High """ hypervisor_name, guest_name = deploy_type_ui @@ -81,8 +74,6 @@ def test_positive_hypervisor_id_option( hypervisor_id can be changed in virt-who-config-{}.conf if the dropdown option is selected to uuid/hwuuid/hostname. - :CaseLevel: Integration - :CaseImportance: Medium """ name = form_data_ui['name'] diff --git a/tests/foreman/virtwho/ui/test_kubevirt_sca.py b/tests/foreman/virtwho/ui/test_kubevirt_sca.py index 6ad0bdf1f96..a1554929866 100644 --- a/tests/foreman/virtwho/ui/test_kubevirt_sca.py +++ b/tests/foreman/virtwho/ui/test_kubevirt_sca.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - -:Upstream: No """ import pytest @@ -42,8 +37,6 @@ def test_positive_deploy_configure_by_id_script( 4. Virtual sku can be generated and attached 5. Config can be deleted - :CaseLevel: Integration - :CaseImportance: High """ assert org_session.virtwho_configure.search(form_data_ui['name'])[0]['Status'] == 'ok' @@ -60,8 +53,6 @@ def test_positive_hypervisor_id_option( 1. hypervisor_id can be changed in virt-who-config-{}.conf if the dropdown option is selected to uuid/hwuuid/hostname. - :CaseLevel: Integration - :CaseImportance: Medium """ name = form_data_ui['name'] diff --git a/tests/foreman/virtwho/ui/test_libvirt.py b/tests/foreman/virtwho/ui/test_libvirt.py index 86b3d3e6532..a66bf1737bb 100644 --- a/tests/foreman/virtwho/ui/test_libvirt.py +++ b/tests/foreman/virtwho/ui/test_libvirt.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -45,8 +40,6 @@ def test_positive_deploy_configure_by_id_script( 4. Virtual sku can be generated and attached 5. Config can be deleted - :CaseLevel: Integration - :CaseImportance: High """ hypervisor_name, guest_name = deploy_type_ui @@ -81,8 +74,6 @@ def test_positive_hypervisor_id_option( hypervisor_id can be changed in virt-who-config-{}.conf if the dropdown option is selected to uuid/hwuuid/hostname. - :CaseLevel: Integration - :CaseImportance: Medium """ name = form_data_ui['name'] diff --git a/tests/foreman/virtwho/ui/test_libvirt_sca.py b/tests/foreman/virtwho/ui/test_libvirt_sca.py index b6d33669744..ffef902441e 100644 --- a/tests/foreman/virtwho/ui/test_libvirt_sca.py +++ b/tests/foreman/virtwho/ui/test_libvirt_sca.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - -:Upstream: No """ import pytest @@ -42,8 +37,6 @@ def test_positive_deploy_configure_by_id_script( 4. Virtual sku can be generated and attached 5. Config can be deleted - :CaseLevel: Integration - :CaseImportance: High """ assert org_session.virtwho_configure.search(form_data_ui['name'])[0]['Status'] == 'ok' @@ -60,8 +53,6 @@ def test_positive_hypervisor_id_option( 1. hypervisor_id can be changed in virt-who-config-{}.conf if the dropdown option is selected to uuid/hwuuid/hostname. - :CaseLevel: Integration - :CaseImportance: Medium """ name = form_data_ui['name'] diff --git a/tests/foreman/virtwho/ui/test_nutanix.py b/tests/foreman/virtwho/ui/test_nutanix.py index 652f3354559..7ec05f191df 100644 --- a/tests/foreman/virtwho/ui/test_nutanix.py +++ b/tests/foreman/virtwho/ui/test_nutanix.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -49,8 +44,6 @@ def test_positive_deploy_configure_by_id_script( 4. Virtual sku can be generated and attached 5. Config can be deleted - :CaseLevel: Integration - :CaseImportance: High """ hypervisor_name, guest_name = deploy_type_ui @@ -85,8 +78,6 @@ def test_positive_hypervisor_id_option( hypervisor_id can be changed in virt-who-config-{}.conf if the dropdown option is selected to uuid/hwuuid/hostname. - :CaseLevel: Integration - :CaseImportance: Medium """ name = form_data_ui['name'] @@ -121,8 +112,6 @@ def test_positive_prism_central_deploy_configure_by_id_script( 5. Virtual sku can be generated and attached 6. Config can be deleted - :CaseLevel: Integration - :CaseImportance: High """ name = gen_string('alpha') @@ -180,8 +169,6 @@ def test_positive_prism_central_prism_flavor_option( prism_flavor can be changed in virt-who-config-{}.conf if the dropdown option is selected to prism central. - :CaseLevel: Integration - :CaseImportance: Medium """ name = form_data_ui['name'] @@ -216,7 +203,6 @@ def test_positive_ahv_internal_debug_option( 5. message Host UUID {system_uuid} found for VM: {guest_uuid} exist in rhsm.log 6. ahv_internal_debug bas been set to true in virt-who-config-X.conf 7. warning message does not exist in log file /var/log/rhsm/rhsm.log - :CaseLevel: Integration :CaseImportance: Medium diff --git a/tests/foreman/virtwho/ui/test_nutanix_sca.py b/tests/foreman/virtwho/ui/test_nutanix_sca.py index 89bae08771d..bb2483195d1 100644 --- a/tests/foreman/virtwho/ui/test_nutanix_sca.py +++ b/tests/foreman/virtwho/ui/test_nutanix_sca.py @@ -4,15 +4,10 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :Team: Phoenix-subscriptions -:TestType: Functional - -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -45,8 +40,6 @@ def test_positive_deploy_configure_by_id_script( 3. Report is sent to satellite 4. Config can be deleted - :CaseLevel: Integration - :CaseImportance: High """ assert org_session.virtwho_configure.search(form_data_ui['name'])[0]['Status'] == 'ok' @@ -63,8 +56,6 @@ def test_positive_hypervisor_id_option( 1. hypervisor_id can be changed in virt-who-config-{}.conf if the dropdown option is selected to uuid/hwuuid/hostname. - :CaseLevel: Integration - :CaseImportance: Medium """ name = form_data_ui['name'] @@ -101,8 +92,6 @@ def test_positive_prism_central_deploy_configure_by_id_script( 5. Virtual sku can be generated and attached 6. Config can be deleted - :CaseLevel: Integration - :CaseImportance: High """ name = gen_string('alpha') @@ -147,8 +136,6 @@ def test_positive_prism_central_prism_flavor_option( 1. prism_flavor can be changed in virt-who-config-{}.conf if the dropdown option is selected to prism central. - :CaseLevel: Integration - :CaseImportance: Medium """ name = form_data_ui['name'] @@ -184,8 +171,6 @@ def test_positive_ahv_internal_debug_option( 6. ahv_internal_debug bas been set to true in virt-who-config-X.conf 7. warning message does not exist in log file /var/log/rhsm/rhsm.log - :CaseLevel: Integration - :CaseImportance: Medium :BZ: 2141719 diff --git a/tests/upgrades/test_activation_key.py b/tests/upgrades/test_activation_key.py index e58c06fee13..bac55d78c35 100644 --- a/tests/upgrades/test_activation_key.py +++ b/tests/upgrades/test_activation_key.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ActivationKeys :Team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest from requests.exceptions import HTTPError diff --git a/tests/upgrades/test_bookmarks.py b/tests/upgrades/test_bookmarks.py index a3f330e7569..b0d04cfa985 100644 --- a/tests/upgrades/test_bookmarks.py +++ b/tests/upgrades/test_bookmarks.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Search :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest @@ -34,7 +29,7 @@ def test_pre_create_public_disable_bookmark(self, request, target_sat): :id: preupgrade-13904b14-6340-4b85-a56f-98080cf50a92 - :Steps: + :steps: 1. Create public disabled bookmarks before the upgrade for all system entities using available bookmark data. @@ -71,7 +66,7 @@ def test_post_create_public_disable_bookmark(self, dependent_scenario_name, targ :id: postupgrade-13904b14-6340-4b85-a56f-98080cf50a92 - :Steps: + :steps: 1. Check the bookmark status after post-upgrade. 2. Remove the bookmark. @@ -105,7 +100,7 @@ def test_pre_create_public_enable_bookmark(self, request, target_sat): :id: preupgrade-93c419db-66b4-4c9a-a82a-a6a68703881f - :Steps: + :steps: 1. Create public enable bookmarks before the upgrade for all system entities using available bookmark data. 2. Check the bookmark attribute(controller, name, query public) status @@ -140,7 +135,7 @@ def test_post_create_public_enable_bookmark(self, dependent_scenario_name, targe :id: postupgrade-93c419db-66b4-4c9a-a82a-a6a68703881f - :Steps: + :steps: 1. Check the bookmark status after post-upgrade. 2. Remove the bookmark. diff --git a/tests/upgrades/test_capsule.py b/tests/upgrades/test_capsule.py index e4f2d8a0720..a520a316c18 100644 --- a/tests/upgrades/test_capsule.py +++ b/tests/upgrades/test_capsule.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Capsule :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import os diff --git a/tests/upgrades/test_classparameter.py b/tests/upgrades/test_classparameter.py index 689374d7dba..ba63148b102 100644 --- a/tests/upgrades/test_classparameter.py +++ b/tests/upgrades/test_classparameter.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Puppet :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import json diff --git a/tests/upgrades/test_client.py b/tests/upgrades/test_client.py index 8877550a29f..8ca57aa4586 100644 --- a/tests/upgrades/test_client.py +++ b/tests/upgrades/test_client.py @@ -7,17 +7,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts-Content :Team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/upgrades/test_contentview.py b/tests/upgrades/test_contentview.py index 2f50ee5db3d..9b7fa90bb56 100644 --- a/tests/upgrades/test_contentview.py +++ b/tests/upgrades/test_contentview.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ContentViews :Team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_alpha import pytest diff --git a/tests/upgrades/test_discovery.py b/tests/upgrades/test_discovery.py index 157b04cc4b8..c85e02e01f4 100644 --- a/tests/upgrades/test_discovery.py +++ b/tests/upgrades/test_discovery.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: DiscoveryImage :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import re diff --git a/tests/upgrades/test_errata.py b/tests/upgrades/test_errata.py index caa4cf21fc5..ed2407d67c6 100644 --- a/tests/upgrades/test_errata.py +++ b/tests/upgrades/test_errata.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ErrataManagement :Team: Phoenix-content -:TestType: Functional - :CaseImportance: Critical -:Upstream: No """ import pytest from wait_for import wait_for diff --git a/tests/upgrades/test_host.py b/tests/upgrades/test_host.py index a53a36814ff..642fef7f2b5 100644 --- a/tests/upgrades/test_host.py +++ b/tests/upgrades/test_host.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest @@ -114,9 +109,7 @@ def test_pre_create_gce_cr_and_host( :id: 889975f2-56ca-4584-95a7-21c513969630 - :CaseLevel: Component - - ::CaseImportance: Critical + :CaseImportance: Critical :steps: 1. Create a GCE Compute Resource diff --git a/tests/upgrades/test_hostcontent.py b/tests/upgrades/test_hostcontent.py index 172039c1930..0a6a60e2a9e 100644 --- a/tests/upgrades/test_hostcontent.py +++ b/tests/upgrades/test_hostcontent.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Hosts-Content :Team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/upgrades/test_hostgroup.py b/tests/upgrades/test_hostgroup.py index c09de90e186..a6d67a106db 100644 --- a/tests/upgrades/test_hostgroup.py +++ b/tests/upgrades/test_hostgroup.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: HostGroup :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest diff --git a/tests/upgrades/test_performance_tuning.py b/tests/upgrades/test_performance_tuning.py index 16033bbd521..94e40a33ba7 100644 --- a/tests/upgrades/test_performance_tuning.py +++ b/tests/upgrades/test_performance_tuning.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Installer :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import filecmp diff --git a/tests/upgrades/test_provisioningtemplate.py b/tests/upgrades/test_provisioningtemplate.py index b5bc11af363..9d681a56ead 100644 --- a/tests/upgrades/test_provisioningtemplate.py +++ b/tests/upgrades/test_provisioningtemplate.py @@ -8,13 +8,8 @@ :Team: Rocket -:TestType: Functional - -:CaseLevel: Integration - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest diff --git a/tests/upgrades/test_puppet.py b/tests/upgrades/test_puppet.py index 96af4c82de6..5cf7b4e9ae6 100644 --- a/tests/upgrades/test_puppet.py +++ b/tests/upgrades/test_puppet.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Puppet :Team: Rocket -:TestType: Functional - :CaseImportance: Medium -:Upstream: No """ import pytest diff --git a/tests/upgrades/test_remoteexecution.py b/tests/upgrades/test_remoteexecution.py index 78af475c8da..62d469e5bc9 100644 --- a/tests/upgrades/test_remoteexecution.py +++ b/tests/upgrades/test_remoteexecution.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: RemoteExecution :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/upgrades/test_repository.py b/tests/upgrades/test_repository.py index 998db385ad4..31233d820c6 100644 --- a/tests/upgrades/test_repository.py +++ b/tests/upgrades/test_repository.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Repositories :Team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/upgrades/test_role.py b/tests/upgrades/test_role.py index 2202ade883d..c2ee4b25d20 100644 --- a/tests/upgrades/test_role.py +++ b/tests/upgrades/test_role.py @@ -4,17 +4,12 @@ :CaseAutomation: NotAutomated -:CaseLevel: Acceptance - :CaseComponent: UsersRoles :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/upgrades/test_satellite_maintain.py b/tests/upgrades/test_satellite_maintain.py index 81af10fdf1b..5091f8508fd 100644 --- a/tests/upgrades/test_satellite_maintain.py +++ b/tests/upgrades/test_satellite_maintain.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: ForemanMaintain :Team: Platform -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import re diff --git a/tests/upgrades/test_satellitesync.py b/tests/upgrades/test_satellitesync.py index dcbc7866f86..ad9356d2f97 100644 --- a/tests/upgrades/test_satellitesync.py +++ b/tests/upgrades/test_satellitesync.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Component - :CaseComponent: InterSatelliteSync :Team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/upgrades/test_subnet.py b/tests/upgrades/test_subnet.py index 63813c06de2..0fbc2f38277 100644 --- a/tests/upgrades/test_subnet.py +++ b/tests/upgrades/test_subnet.py @@ -4,17 +4,12 @@ :CaseAutomation: NotAutomated -:CaseLevel: Acceptance - :CaseComponent: Networking :Team: Rocket -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/upgrades/test_subscription.py b/tests/upgrades/test_subscription.py index f72b5b13948..4028cfc410c 100644 --- a/tests/upgrades/test_subscription.py +++ b/tests/upgrades/test_subscription.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: SubscriptionManagement :Team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from manifester import Manifester import pytest diff --git a/tests/upgrades/test_syncplan.py b/tests/upgrades/test_syncplan.py index ba14f9de831..5939c8e16dc 100644 --- a/tests/upgrades/test_syncplan.py +++ b/tests/upgrades/test_syncplan.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: SyncPlans :Team: Phoenix-content -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_choice import pytest diff --git a/tests/upgrades/test_user.py b/tests/upgrades/test_user.py index cd3a8e55297..b21da9b5ba1 100644 --- a/tests/upgrades/test_user.py +++ b/tests/upgrades/test_user.py @@ -4,17 +4,12 @@ :CaseAutomation: NotAutomated -:CaseLevel: Acceptance - :CaseComponent: UsersRoles :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ import pytest diff --git a/tests/upgrades/test_usergroup.py b/tests/upgrades/test_usergroup.py index aaa337d316f..0832602dbb3 100644 --- a/tests/upgrades/test_usergroup.py +++ b/tests/upgrades/test_usergroup.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: UsersRoles :Team: Endeavour -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest diff --git a/tests/upgrades/test_virtwho.py b/tests/upgrades/test_virtwho.py index 99606f2f062..ca785ab0149 100644 --- a/tests/upgrades/test_virtwho.py +++ b/tests/upgrades/test_virtwho.py @@ -4,17 +4,12 @@ :CaseAutomation: Automated -:CaseLevel: Acceptance - :CaseComponent: Virt-whoConfigurePlugin :Team: Phoenix-subscriptions -:TestType: Functional - :CaseImportance: High -:Upstream: No """ from fauxfactory import gen_string import pytest From 5f76248fd6cc346079853a98f31fe738a519ccdd Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 12 Jan 2024 04:15:37 -0500 Subject: [PATCH 435/586] [6.14.z] virtwho config hammer deploy option name organization-title and location-id support (#13701) virtwho config hammer deploy option name organization-title and location-id support (#13632) * virtwho config hammer option name organization-title and location-id support * use fstring and optimize duplicated code * remove print * Update pytest_fixtures/component/virtwho_config.py Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> * Update pytest_fixtures/component/virtwho_config.py Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> * Update pytest_fixtures/component/virtwho_config.py Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> * Update pytest_fixtures/component/virtwho_config.py Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> --------- Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> (cherry picked from commit 19f59625d710700b6c6489b6b11b6c3285aee873) Co-authored-by: yanpliu --- pytest_fixtures/component/virtwho_config.py | 21 +++++++++++++++------ robottelo/utils/virtwho.py | 14 ++++++++++++++ tests/foreman/virtwho/cli/test_esx_sca.py | 8 ++++++-- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/pytest_fixtures/component/virtwho_config.py b/pytest_fixtures/component/virtwho_config.py index fc7a262e802..b8970232136 100644 --- a/pytest_fixtures/component/virtwho_config.py +++ b/pytest_fixtures/component/virtwho_config.py @@ -6,6 +6,7 @@ deploy_configure_by_command, deploy_configure_by_script, get_configure_command, + get_configure_command_option, get_guest_info, ) @@ -271,21 +272,29 @@ def deploy_type_cli( form_data_cli, virtwho_config_cli, target_sat, + default_location, ): deploy_type = request.param.lower() assert virtwho_config_cli['status'] == 'No Report Yet' - if "id" in deploy_type: - command = get_configure_command(virtwho_config_cli['id'], org_module.name) - hypervisor_name, guest_name = deploy_configure_by_command( - command, form_data_cli['hypervisor-type'], debug=True, org=org_module.label - ) - elif "script" in deploy_type: + if 'script' in deploy_type: script = target_sat.cli.VirtWhoConfig.fetch( {'id': virtwho_config_cli['id']}, output_format='base' ) hypervisor_name, guest_name = deploy_configure_by_script( script, form_data_cli['hypervisor-type'], debug=True, org=org_module.label ) + elif deploy_type == 'organization-title': + virtwho_config_cli['organization-title'] = org_module.title + elif deploy_type == 'location-id': + virtwho_config_cli['location-id'] = default_location.id + if deploy_type in ['id', 'name', 'organization-title', 'location-id']: + if 'id' in deploy_type: + command = get_configure_command(virtwho_config_cli['id'], org_module.name) + else: + command = get_configure_command_option(deploy_type, virtwho_config_cli, org_module.name) + hypervisor_name, guest_name = deploy_configure_by_command( + command, form_data_cli['hypervisor-type'], debug=True, org=org_module.label + ) return hypervisor_name, guest_name diff --git a/robottelo/utils/virtwho.py b/robottelo/utils/virtwho.py index aaae57d7c97..2f176d55341 100644 --- a/robottelo/utils/virtwho.py +++ b/robottelo/utils/virtwho.py @@ -526,3 +526,17 @@ def create_http_proxy(org, name=None, url=None, http_type='https'): organization=[org.id], ).create() return http_proxy.url, http_proxy.name, http_proxy.id + + +def get_configure_command_option(deploy_type, args, org=DEFAULT_ORG): + """Return the deploy command line based on option. + :param str option: the unique id of the configure file you have created. + :param str org: the satellite organization name. + """ + username, password = Base._get_username_password() + if deploy_type == 'location-id': + return f"hammer -u {username} -p {password} virt-who-config deploy --id {args['id']} --location-id '{args['location-id']}' " + elif deploy_type == 'organization-title': + return f"hammer -u {username} -p {password} virt-who-config deploy --id {args['id']} --organization-title '{args['organization-title']}' " + elif deploy_type == 'name': + return f"hammer -u {username} -p {password} virt-who-config deploy --name {args['name']} --organization '{org}' " diff --git a/tests/foreman/virtwho/cli/test_esx_sca.py b/tests/foreman/virtwho/cli/test_esx_sca.py index b09cabef912..7c5a4f5bc20 100644 --- a/tests/foreman/virtwho/cli/test_esx_sca.py +++ b/tests/foreman/virtwho/cli/test_esx_sca.py @@ -32,8 +32,12 @@ class TestVirtWhoConfigforEsx: @pytest.mark.tier2 @pytest.mark.upgrade - @pytest.mark.parametrize('deploy_type_cli', ['id', 'script'], indirect=True) - def test_positive_deploy_configure_by_id_script( + @pytest.mark.parametrize( + 'deploy_type_cli', + ['id', 'script', 'name', 'location-id', 'organization-title'], + indirect=True, + ) + def test_positive_deploy_configure_by_id_script_name_locationid_organizationtitle( self, module_sca_manifest_org, target_sat, virtwho_config_cli, deploy_type_cli ): """Verify "hammer virt-who-config deploy & fetch" From 4ae8d7d40c9fff175a457071d6ca689c5c5d8bed Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 12 Jan 2024 04:22:07 -0500 Subject: [PATCH 436/586] [6.14.z] Bump jinja2 from 3.1.2 to 3.1.3 (#13733) Bump jinja2 from 3.1.2 to 3.1.3 (#13729) (cherry picked from commit 73e1475eefe95ca6c8b53cf0271ffdaddb538783) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e2e2cb0a926..f6007645014 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ cryptography==41.0.7 deepdiff==6.7.1 dynaconf[vault]==3.2.4 fauxfactory==3.1.0 -jinja2==3.1.2 +jinja2==3.1.3 manifester==0.0.14 navmazing==1.2.2 productmd==1.38 From 2bbcc3abafb1d96cb3b843a17c4960b4dee9143a Mon Sep 17 00:00:00 2001 From: Omkar Khatavkar Date: Fri, 12 Jan 2024 15:46:16 +0530 Subject: [PATCH 437/586] [6.14.z] - Removing the direct Airgun imports from robottelo (#13684) Removing the direct Airgun imports from robottelo --- pytest_fixtures/core/ui.py | 29 +++ tests/foreman/ui/test_activationkey.py | 140 +++++++------- tests/foreman/ui/test_bookmarks.py | 28 +-- tests/foreman/ui/test_branding.py | 3 +- tests/foreman/ui/test_contenthost.py | 48 ++--- tests/foreman/ui/test_dashboard.py | 42 ++-- tests/foreman/ui/test_discoveryrule.py | 9 +- tests/foreman/ui/test_errata.py | 52 ++--- tests/foreman/ui/test_host.py | 15 +- tests/foreman/ui/test_ldap_authentication.py | 183 +++++++++++------- tests/foreman/ui/test_lifecycleenvironment.py | 3 +- tests/foreman/ui/test_repository.py | 113 ++++++----- tests/foreman/ui/test_role.py | 60 +++--- tests/foreman/ui/test_settings.py | 22 ++- tests/foreman/ui/test_subscription.py | 52 ++--- tests/foreman/ui/test_user.py | 7 +- tests/foreman/virtwho/conftest.py | 9 +- tests/foreman/virtwho/ui/test_esx.py | 17 +- 18 files changed, 466 insertions(+), 366 deletions(-) diff --git a/pytest_fixtures/core/ui.py b/pytest_fixtures/core/ui.py index f87f4cec0ad..298632f59f6 100644 --- a/pytest_fixtures/core/ui.py +++ b/pytest_fixtures/core/ui.py @@ -2,6 +2,7 @@ import pytest from requests.exceptions import HTTPError +from robottelo.hosts import Satellite from robottelo.logging import logger @@ -70,3 +71,31 @@ def test_foo(autosession): """ with target_sat.ui_session(test_name, ui_user.login, ui_user.password) as started_session: yield started_session + + +@pytest.fixture(autouse=True) +def ui_session_record_property(request, record_property): + """ + Autouse fixture to set the record_property attribute for Satellite instances in the test. + + This fixture iterates over all fixtures in the current test node + (excluding the current fixture) and sets the record_property attribute + for instances of the Satellite class. + + Args: + request: The pytest request object. + record_property: The value to set for the record_property attribute. + """ + test_directories = [ + 'tests/foreman/destructive', + 'tests/foreman/ui', + 'tests/foreman/sanity', + 'tests/foreman/virtwho', + ] + test_file_path = request.node.fspath.strpath + if any(directory in test_file_path for directory in test_directories): + for fixture in request.node.fixturenames: + if request.fixturename != fixture: + if isinstance(request.getfixturevalue(fixture), Satellite): + sat = request.getfixturevalue(fixture) + sat.record_property = record_property diff --git a/tests/foreman/ui/test_activationkey.py b/tests/foreman/ui/test_activationkey.py index 79e9dbfa655..5dc53f39c1e 100644 --- a/tests/foreman/ui/test_activationkey.py +++ b/tests/foreman/ui/test_activationkey.py @@ -13,10 +13,8 @@ """ import random -from airgun.session import Session from broker import Broker from fauxfactory import gen_string -from nailgun import entities import pytest from robottelo import constants @@ -28,7 +26,7 @@ @pytest.mark.e2e @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_end_to_end_crud(session, module_org): +def test_positive_end_to_end_crud(session, module_org, module_target_sat): """Perform end to end testing for activation key component :id: b6b98c45-e41e-4c7a-9be4-997273b7e24d @@ -39,7 +37,7 @@ def test_positive_end_to_end_crud(session, module_org): """ name = gen_string('alpha') new_name = gen_string('alpha') - cv = entities.ContentView(organization=module_org).create() + cv = module_target_sat.api.ContentView(organization=module_org).create() cv.publish() with session: # Create activation key with content view and LCE assigned @@ -89,7 +87,7 @@ def test_positive_end_to_end_register( :CaseImportance: High """ org = function_entitlement_manifest_org - lce = entities.LifecycleEnvironment(organization=org).create() + lce = target_sat.api.LifecycleEnvironment(organization=org).create() repos_collection.setup_content(org.id, lce.id, upload_manifest=False) ak_name = repos_collection.setup_content_data['activation_key']['name'] @@ -171,7 +169,7 @@ def test_positive_search_scoped(session, module_org, target_sat): @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_create_with_host_collection(session, module_org): +def test_positive_create_with_host_collection(session, module_org, module_target_sat): """Create Activation key with Host Collection :id: 0e4ad2b4-47a7-4087-828f-2b0535a97b69 @@ -179,7 +177,7 @@ def test_positive_create_with_host_collection(session, module_org): :expectedresults: Activation key is created """ name = gen_string('alpha') - hc = entities.HostCollection(organization=module_org).create() + hc = module_target_sat.api.HostCollection(organization=module_org).create() with session: session.activationkey.create({'name': name, 'lce': {constants.ENVIRONMENT: True}}) assert session.activationkey.search(name)[0]['Name'] == name @@ -226,21 +224,21 @@ def test_positive_add_host_collection_non_admin(module_org, test_name, target_sa :BZ: 1473212 """ ak_name = gen_string('alpha') - hc = entities.HostCollection(organization=module_org).create() + hc = target_sat.api.HostCollection(organization=module_org).create() # Create non-admin user with specified permissions - roles = [entities.Role().create()] + roles = [target_sat.api.Role().create()] user_permissions = { 'Katello::ActivationKey': constants.PERMISSIONS['Katello::ActivationKey'], 'Katello::HostCollection': constants.PERMISSIONS['Katello::HostCollection'], } - viewer_role = entities.Role().search(query={'search': 'name="Viewer"'})[0] + viewer_role = target_sat.api.Role().search(query={'search': 'name="Viewer"'})[0] roles.append(viewer_role) target_sat.api_factory.create_role_permissions(roles[0], user_permissions) password = gen_string('alphanumeric') - user = entities.User( + user = target_sat.api.User( admin=False, role=roles, password=password, organization=[module_org] ).create() - with Session(test_name, user=user.login, password=password) as session: + with target_sat.ui_session(test_name, user=user.login, password=password) as session: session.activationkey.create({'name': ak_name, 'lce': {constants.ENVIRONMENT: True}}) assert session.activationkey.search(ak_name)[0]['Name'] == ak_name session.activationkey.add_host_collection(ak_name, hc.name) @@ -260,21 +258,21 @@ def test_positive_remove_host_collection_non_admin(module_org, test_name, target listed """ ak_name = gen_string('alpha') - hc = entities.HostCollection(organization=module_org).create() + hc = target_sat.api.HostCollection(organization=module_org).create() # Create non-admin user with specified permissions - roles = [entities.Role().create()] + roles = [target_sat.api.Role().create()] user_permissions = { 'Katello::ActivationKey': constants.PERMISSIONS['Katello::ActivationKey'], 'Katello::HostCollection': constants.PERMISSIONS['Katello::HostCollection'], } - viewer_role = entities.Role().search(query={'search': 'name="Viewer"'})[0] + viewer_role = target_sat.api.Role().search(query={'search': 'name="Viewer"'})[0] roles.append(viewer_role) target_sat.api_factory.create_role_permissions(roles[0], user_permissions) password = gen_string('alphanumeric') - user = entities.User( + user = target_sat.api.User( admin=False, role=roles, password=password, organization=[module_org] ).create() - with Session(test_name, user=user.login, password=password) as session: + with target_sat.ui_session(test_name, user=user.login, password=password) as session: session.activationkey.create({'name': ak_name, 'lce': {constants.ENVIRONMENT: True}}) assert session.activationkey.search(ak_name)[0]['Name'] == ak_name session.activationkey.add_host_collection(ak_name, hc.name) @@ -545,8 +543,8 @@ def test_positive_add_rh_and_custom_products( custom_product_name = gen_string('alpha') repo_name = gen_string('alpha') org = function_entitlement_manifest_org - product = entities.Product(name=custom_product_name, organization=org).create() - repo = entities.Repository(name=repo_name, product=product).create() + product = target_sat.api.Product(name=custom_product_name, organization=org).create() + repo = target_sat.api.Repository(name=repo_name, product=product).create() rhel_repo_id = target_sat.api_factory.enable_rhrepo_and_fetchid( basearch=rh_repo['basearch'], org_id=org.id, @@ -556,7 +554,7 @@ def test_positive_add_rh_and_custom_products( releasever=rh_repo['releasever'], ) for repo_id in [rhel_repo_id, repo.id]: - entities.Repository(id=repo_id).sync() + target_sat.api.Repository(id=repo_id).sync() with session: session.organization.select(org.name) session.activationkey.create( @@ -599,19 +597,21 @@ def test_positive_fetch_product_content(target_sat, function_entitlement_manifes reposet=constants.REPOSET['rhst7'], releasever=None, ) - rh_repo = entities.Repository(id=rh_repo_id).read() + rh_repo = target_sat.api.Repository(id=rh_repo_id).read() rh_repo.sync() - custom_product = entities.Product(organization=org).create() - custom_repo = entities.Repository( + custom_product = target_sat.api.Product(organization=org).create() + custom_repo = target_sat.api.Repository( name=gen_string('alphanumeric').upper(), # first letter is always # uppercase on product content page, workarounding it for # successful checks product=custom_product, ).create() custom_repo.sync() - cv = entities.ContentView(organization=org, repository=[rh_repo_id, custom_repo.id]).create() + cv = target_sat.api.ContentView( + organization=org, repository=[rh_repo_id, custom_repo.id] + ).create() cv.publish() - ak = entities.ActivationKey(content_view=cv, organization=org).create() + ak = target_sat.api.ActivationKey(content_view=cv, organization=org).create() with session: session.organization.select(org.name) for subscription in (constants.DEFAULT_SUBSCRIPTION_NAME, custom_product.name): @@ -624,7 +624,7 @@ def test_positive_fetch_product_content(target_sat, function_entitlement_manifes @pytest.mark.e2e @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_access_non_admin_user(session, test_name): +def test_positive_access_non_admin_user(session, test_name, target_sat): """Access activation key that has specific name and assigned environment by user that has filter configured for that specific activation key @@ -639,24 +639,26 @@ def test_positive_access_non_admin_user(session, test_name): """ ak_name = gen_string('alpha') non_searchable_ak_name = gen_string('alpha') - org = entities.Organization().create() + org = target_sat.api.Organization().create() envs_list = ['STAGING', 'DEV', 'IT', 'UAT', 'PROD'] for name in envs_list: - entities.LifecycleEnvironment(name=name, organization=org).create() + target_sat.api.LifecycleEnvironment(name=name, organization=org).create() env_name = random.choice(envs_list) - cv = entities.ContentView(organization=org).create() + cv = target_sat.api.ContentView(organization=org).create() cv.publish() content_view_version = cv.read().version[0] content_view_version.promote( - data={'environment_ids': [entities.LifecycleEnvironment(name=env_name).search()[0].id]} + data={ + 'environment_ids': [target_sat.api.LifecycleEnvironment(name=env_name).search()[0].id] + } ) # Create new role - role = entities.Role().create() + role = target_sat.api.Role().create() # Create filter with predefined activation keys search criteria envs_condition = ' or '.join(['environment = ' + s for s in envs_list]) - entities.Filter( + target_sat.api.Filter( organization=[org], - permission=entities.Permission().search( + permission=target_sat.api.Permission().search( filters={'name': 'view_activation_keys'}, query={'search': 'resource_type="Katello::ActivationKey"'}, ), @@ -665,20 +667,24 @@ def test_positive_access_non_admin_user(session, test_name): ).create() # Add permissions for Organization and Location - entities.Filter( - permission=entities.Permission().search(query={'search': 'resource_type="Organization"'}), + target_sat.api.Filter( + permission=target_sat.api.Permission().search( + query={'search': 'resource_type="Organization"'} + ), role=role, ).create() - entities.Filter( - permission=entities.Permission().search(query={'search': 'resource_type="Location"'}), + target_sat.api.Filter( + permission=target_sat.api.Permission().search(query={'search': 'resource_type="Location"'}), role=role, ).create() # Create new user with a configured role - default_loc = entities.Location().search(query={'search': f'name="{constants.DEFAULT_LOC}"'})[0] + default_loc = target_sat.api.Location().search( + query={'search': f'name="{constants.DEFAULT_LOC}"'} + )[0] user_login = gen_string('alpha') user_password = gen_string('alpha') - entities.User( + target_sat.api.User( role=[role], admin=False, login=user_login, @@ -699,7 +705,7 @@ def test_positive_access_non_admin_user(session, test_name): env_name ][env_name] - with Session(test_name, user=user_login, password=user_password) as session: + with target_sat.ui_session(test_name, user=user_login, password=user_password) as session: session.organization.select(org.name) session.location.select(constants.DEFAULT_LOC) assert session.activationkey.search(ak_name)[0]['Name'] == ak_name @@ -710,7 +716,7 @@ def test_positive_access_non_admin_user(session, test_name): @pytest.mark.tier2 -def test_positive_remove_user(session, module_org, test_name): +def test_positive_remove_user(session, module_org, test_name, module_target_sat): """Delete any user who has previously created an activation key and check that activation key still exists @@ -723,9 +729,11 @@ def test_positive_remove_user(session, module_org, test_name): ak_name = gen_string('alpha') # Create user password = gen_string('alpha') - user = entities.User(admin=True, default_organization=module_org, password=password).create() + user = module_target_sat.api.User( + admin=True, default_organization=module_org, password=password + ).create() # Create Activation Key using new user credentials - with Session(test_name, user.login, password) as non_admin_session: + with module_target_sat.ui_session(test_name, user.login, password) as non_admin_session: non_admin_session.activationkey.create( {'name': ak_name, 'lce': {constants.ENVIRONMENT: True}} ) @@ -737,7 +745,7 @@ def test_positive_remove_user(session, module_org, test_name): @pytest.mark.tier2 -def test_positive_add_docker_repo_cv(session, module_org): +def test_positive_add_docker_repo_cv(session, module_org, module_target_sat): """Add docker repository to a non-composite content view and publish it. Then create an activation key and associate it with the Docker content view. @@ -747,13 +755,13 @@ def test_positive_add_docker_repo_cv(session, module_org): :expectedresults: Content view with docker repo can be added to activation key """ - lce = entities.LifecycleEnvironment(organization=module_org).create() - repo = entities.Repository( + lce = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() + repo = module_target_sat.api.Repository( content_type=constants.REPO_TYPE['docker'], - product=entities.Product(organization=module_org).create(), + product=module_target_sat.api.Product(organization=module_org).create(), url=constants.CONTAINER_REGISTRY_HUB, ).create() - content_view = entities.ContentView( + content_view = module_target_sat.api.ContentView( composite=False, organization=module_org, repository=[repo] ).create() content_view.publish() @@ -770,7 +778,7 @@ def test_positive_add_docker_repo_cv(session, module_org): @pytest.mark.tier2 -def test_positive_add_docker_repo_ccv(session, module_org): +def test_positive_add_docker_repo_ccv(session, module_org, module_target_sat): """Add docker repository to a non-composite content view and publish it. Then add this content view to a composite content view and publish it. Create an activation key and associate it with the composite Docker content @@ -781,19 +789,19 @@ def test_positive_add_docker_repo_ccv(session, module_org): :expectedresults: Docker-based content view can be added to activation key """ - lce = entities.LifecycleEnvironment(organization=module_org).create() - repo = entities.Repository( + lce = module_target_sat.api.LifecycleEnvironment(organization=module_org).create() + repo = module_target_sat.api.Repository( content_type=constants.REPO_TYPE['docker'], - product=entities.Product(organization=module_org).create(), + product=module_target_sat.api.Product(organization=module_org).create(), url=constants.CONTAINER_REGISTRY_HUB, ).create() - content_view = entities.ContentView( + content_view = module_target_sat.api.ContentView( composite=False, organization=module_org, repository=[repo] ).create() content_view.publish() cvv = content_view.read().version[0].read() cvv.promote(data={'environment_ids': lce.id, 'force': False}) - composite_cv = entities.ContentView( + composite_cv = module_target_sat.api.ContentView( component=[cvv], composite=True, organization=module_org ).create() composite_cv.publish() @@ -825,8 +833,8 @@ def test_positive_add_host(session, module_org, rhel6_contenthost, target_sat): :parametrized: yes """ - ak = entities.ActivationKey( - environment=entities.LifecycleEnvironment( + ak = target_sat.api.ActivationKey( + environment=target_sat.api.LifecycleEnvironment( name=constants.ENVIRONMENT, organization=module_org ).search()[0], organization=module_org, @@ -863,7 +871,7 @@ def test_positive_delete_with_system(session, rhel6_contenthost, target_sat): cv_name = gen_string('alpha') env_name = gen_string('alpha') product_name = gen_string('alpha') - org = entities.Organization().create() + org = target_sat.api.Organization().create() # Helper function to create and promote CV to next environment repo_id = target_sat.api_factory.create_sync_custom_repo( product_name=product_name, org_id=org.id @@ -994,12 +1002,12 @@ def test_positive_host_associations(session, target_sat): :BZ: 1344033, 1372826, 1394388 """ - org = entities.Organization().create() + org = target_sat.api.Organization().create() org_entities = target_sat.cli_factory.setup_org_for_a_custom_repo( {'url': settings.repos.yum_1.url, 'organization-id': org.id} ) - ak1 = entities.ActivationKey(id=org_entities['activationkey-id']).read() - ak2 = entities.ActivationKey( + ak1 = target_sat.api.ActivationKey(id=org_entities['activationkey-id']).read() + ak2 = target_sat.api.ActivationKey( content_view=org_entities['content-view-id'], environment=org_entities['lifecycle-environment-id'], organization=org.id, @@ -1060,10 +1068,10 @@ def test_positive_service_level_subscription_with_custom_product( entities_ids = target_sat.cli_factory.setup_org_for_a_custom_repo( {'url': settings.repos.yum_1.url, 'organization-id': org.id} ) - product = entities.Product(id=entities_ids['product-id']).read() - activation_key = entities.ActivationKey(id=entities_ids['activationkey-id']).read() + product = target_sat.api.Product(id=entities_ids['product-id']).read() + activation_key = target_sat.api.ActivationKey(id=entities_ids['activationkey-id']).read() # add the default RH subscription - subscription = entities.Subscription(organization=org).search( + subscription = target_sat.api.Subscription(organization=org).search( query={'search': f'name="{constants.DEFAULT_SUBSCRIPTION_NAME}"'} )[0] activation_key.add_subscriptions(data={'quantity': 1, 'subscription_id': subscription.id}) @@ -1093,7 +1101,7 @@ def test_positive_service_level_subscription_with_custom_product( @pytest.mark.run_in_one_thread @pytest.mark.tier2 -def test_positive_delete_manifest(session, function_entitlement_manifest_org): +def test_positive_delete_manifest(session, function_entitlement_manifest_org, target_sat): """Check if deleting a manifest removes it from Activation key :id: 512d8e41-b937-451e-a9c6-840457d3d7d4 @@ -1108,9 +1116,9 @@ def test_positive_delete_manifest(session, function_entitlement_manifest_org): """ org = function_entitlement_manifest_org # Create activation key - activation_key = entities.ActivationKey(organization=org).create() + activation_key = target_sat.api.ActivationKey(organization=org).create() # Associate a manifest to the activation key - subscription = entities.Subscription(organization=org).search( + subscription = target_sat.api.Subscription(organization=org).search( query={'search': f'name="{constants.DEFAULT_SUBSCRIPTION_NAME}"'} )[0] activation_key.add_subscriptions(data={'quantity': 1, 'subscription_id': subscription.id}) diff --git a/tests/foreman/ui/test_bookmarks.py b/tests/foreman/ui/test_bookmarks.py index 94e10d4f911..f2622ae84a8 100644 --- a/tests/foreman/ui/test_bookmarks.py +++ b/tests/foreman/ui/test_bookmarks.py @@ -12,9 +12,7 @@ """ from airgun.exceptions import NoSuchElementException -from airgun.session import Session from fauxfactory import gen_string -from nailgun import entities import pytest from robottelo.config import user_nailgun_config @@ -91,7 +89,9 @@ def test_positive_end_to_end(session, ui_entity): @pytest.mark.tier2 -def test_positive_create_bookmark_public(session, ui_entity, default_viewer_role, test_name): +def test_positive_create_bookmark_public( + session, ui_entity, default_viewer_role, test_name, module_target_sat +): """Create and check visibility of the (non)public bookmarks :id: 93139529-7690-429b-83fe-3dcbac4f91dc @@ -123,7 +123,9 @@ def test_positive_create_bookmark_public(session, ui_entity, default_viewer_role {'name': name, 'query': gen_string('alphanumeric'), 'public': name == public_name} ) assert any(d['Name'] == name for d in session.bookmark.search(name)) - with Session(test_name, default_viewer_role.login, default_viewer_role.password) as session: + with module_target_sat.ui_session( + test_name, default_viewer_role.login, default_viewer_role.password + ) as session: assert any(d['Name'] == public_name for d in session.bookmark.search(public_name)) assert not session.bookmark.search(nonpublic_name) @@ -182,7 +184,7 @@ def test_positive_update_bookmark_public( controller=ui_entity['controller'], public=name == public_name, ).create() - with Session( + with target_sat.ui_session( test_name, default_viewer_role.login, default_viewer_role.password ) as non_admin_session: assert any(d['Name'] == public_name for d in non_admin_session.bookmark.search(public_name)) @@ -190,7 +192,7 @@ def test_positive_update_bookmark_public( with session: session.bookmark.update(public_name, {'public': False}) session.bookmark.update(nonpublic_name, {'public': True}) - with Session( + with target_sat.ui_session( test_name, default_viewer_role.login, default_viewer_role.password ) as non_admin_session: assert any( @@ -200,7 +202,7 @@ def test_positive_update_bookmark_public( @pytest.mark.tier2 -def test_negative_delete_bookmark(ui_entity, default_viewer_role, test_name): +def test_negative_delete_bookmark(ui_entity, default_viewer_role, test_name, module_target_sat): """Simple removal of a bookmark query without permissions :id: 1a94bf2b-bcc6-4663-b70d-e13244a0783b @@ -219,8 +221,10 @@ def test_negative_delete_bookmark(ui_entity, default_viewer_role, test_name): :expectedresults: The delete buttons are not displayed """ - bookmark = entities.Bookmark(controller=ui_entity['controller'], public=True).create() - with Session( + bookmark = module_target_sat.api.Bookmark( + controller=ui_entity['controller'], public=True + ).create() + with module_target_sat.ui_session( test_name, default_viewer_role.login, default_viewer_role.password ) as non_admin_session: assert non_admin_session.bookmark.search(bookmark.name)[0]['Name'] == bookmark.name @@ -230,7 +234,7 @@ def test_negative_delete_bookmark(ui_entity, default_viewer_role, test_name): @pytest.mark.tier2 -def test_negative_create_with_duplicate_name(session, ui_entity): +def test_negative_create_with_duplicate_name(session, ui_entity, module_target_sat): """Create bookmark with duplicate name :id: 18168c9c-bdd1-4839-a506-cf9b06c4ab44 @@ -248,7 +252,9 @@ def test_negative_create_with_duplicate_name(session, ui_entity): :BZ: 1920566, 1992652 """ query = gen_string('alphanumeric') - bookmark = entities.Bookmark(controller=ui_entity['controller'], public=True).create() + bookmark = module_target_sat.api.Bookmark( + controller=ui_entity['controller'], public=True + ).create() with session: existing_bookmark = session.bookmark.search(bookmark.name)[0] assert existing_bookmark['Name'] == bookmark.name diff --git a/tests/foreman/ui/test_branding.py b/tests/foreman/ui/test_branding.py index a7e08659d8b..8fe30b1c2ef 100644 --- a/tests/foreman/ui/test_branding.py +++ b/tests/foreman/ui/test_branding.py @@ -11,7 +11,6 @@ :CaseImportance: High """ -from airgun.session import Session import pytest @@ -31,7 +30,7 @@ def test_verify_satellite_login_screen_info(target_sat): :BZ: 1315849, 1367495, 1372436, 1502098, 1540710, 1582476, 1724738, 1959135, 2076979, 1687250, 1686540, 1742872, 1805642, 2105949 """ - with Session(login=False) as session: + with target_sat.ui_session(login=False) as session: version = session.login.read_sat_version() assert f'Version {target_sat.version}' == version['login_text'] assert 'Beta' not in version['login_text'], '"Beta" should not be there' diff --git a/tests/foreman/ui/test_contenthost.py b/tests/foreman/ui/test_contenthost.py index b77d9fe4ef7..f0c33476198 100644 --- a/tests/foreman/ui/test_contenthost.py +++ b/tests/foreman/ui/test_contenthost.py @@ -15,9 +15,7 @@ import re from urllib.parse import urlparse -from airgun.session import Session from fauxfactory import gen_integer, gen_string -from nailgun import entities import pytest from robottelo.config import setting_is_set, settings @@ -44,8 +42,10 @@ @pytest.fixture(scope='module', autouse=True) -def host_ui_default(): - settings_object = entities.Setting().search(query={'search': 'name=host_details_ui'})[0] +def host_ui_default(module_target_sat): + settings_object = module_target_sat.api.Setting().search( + query={'search': 'name=host_details_ui'} + )[0] settings_object.value = 'No' settings_object.update({'value'}) yield @@ -54,10 +54,10 @@ def host_ui_default(): @pytest.fixture(scope='module') -def module_org(): - org = entities.Organization(simple_content_access=False).create() +def module_org(module_target_sat): + org = module_target_sat.api.Organization(simple_content_access=False).create() # adding remote_execution_connect_by_ip=Yes at org level - entities.Parameter( + module_target_sat.api.Parameter( name='remote_execution_connect_by_ip', value='Yes', organization=org.id, @@ -84,9 +84,9 @@ def vm_module_streams(module_repos_collection_with_manifest, rhel8_contenthost, return rhel8_contenthost -def set_ignore_facts_for_os(value=False): +def set_ignore_facts_for_os(module_target_sat, value=False): """Helper to set 'ignore_facts_for_operatingsystem' setting""" - ignore_setting = entities.Setting().search( + ignore_setting = module_target_sat.api.Setting().search( query={'search': 'name="ignore_facts_for_operatingsystem"'} )[0] ignore_setting.value = str(value) @@ -603,7 +603,7 @@ def test_positive_remove_package_group(session, default_location, vm): indirect=True, ) def test_positive_search_errata_non_admin( - session, default_location, vm, test_name, default_viewer_role + default_location, vm, test_name, default_viewer_role, module_target_sat ): """Search for host's errata by non-admin user with enough permissions @@ -619,7 +619,7 @@ def test_positive_search_errata_non_admin( :parametrized: yes """ vm.run(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}') - with Session( + with module_target_sat.ui_session( test_name, user=default_viewer_role.login, password=default_viewer_role.password ) as session: session.location.select(default_location.name) @@ -766,7 +766,9 @@ def test_positive_host_re_registration_with_host_rename( ], indirect=True, ) -def test_positive_check_ignore_facts_os_setting(session, default_location, vm, module_org, request): +def test_positive_check_ignore_facts_os_setting( + session, default_location, vm, module_org, request, module_target_sat +): """Verify that 'Ignore facts for operating system' setting works properly @@ -799,9 +801,9 @@ def test_positive_check_ignore_facts_os_setting(session, default_location, vm, m major = str(gen_integer(15, 99)) minor = str(gen_integer(1, 9)) expected_os = f'RedHat {major}.{minor}' - set_ignore_facts_for_os(False) + set_ignore_facts_for_os(module_target_sat, False) host = ( - entities.Host() + module_target_sat.api.Host() .search(query={'search': f'name={vm.hostname} and organization_id={module_org.id}'})[0] .read() ) @@ -810,7 +812,7 @@ def test_positive_check_ignore_facts_os_setting(session, default_location, vm, m # Get host current operating system value os = session.contenthost.read(vm.hostname, widget_names='details')['details']['os'] # Change necessary setting to true - set_ignore_facts_for_os(True) + set_ignore_facts_for_os(module_target_sat, True) # Add cleanup function to roll back setting to default value request.addfinalizer(set_ignore_facts_for_os) # Read all facts for corresponding host @@ -827,7 +829,7 @@ def test_positive_check_ignore_facts_os_setting(session, default_location, vm, m # Check that host OS was not changed due setting was set to true assert os == updated_os # Put it to false and re-run the process - set_ignore_facts_for_os(False) + set_ignore_facts_for_os(module_target_sat, False) host.upload_facts(data={'name': vm.hostname, 'facts': facts}) session.contenthost.search('') updated_os = session.contenthost.read(vm.hostname, widget_names='details')['details']['os'] @@ -864,8 +866,8 @@ def test_positive_virt_who_hypervisor_subscription_status( :parametrized: yes """ - org = entities.Organization().create() - lce = entities.LifecycleEnvironment(organization=org).create() + org = target_sat.api.Organization().create() + lce = target_sat.api.LifecycleEnvironment(organization=org).create() # TODO move this to either hack around virt-who service or use an env-* compute resource provisioning_server = settings.libvirt.libvirt_hostname # Create a new virt-who config @@ -936,7 +938,9 @@ def test_positive_virt_who_hypervisor_subscription_status( ], indirect=True, ) -def test_module_stream_actions_on_content_host(session, default_location, vm_module_streams): +def test_module_stream_actions_on_content_host( + session, default_location, vm_module_streams, module_target_sat +): """Check remote execution for module streams actions e.g. install, remove, disable works on content host. Verify that correct stream module stream get installed/removed. @@ -949,7 +953,7 @@ def test_module_stream_actions_on_content_host(session, default_location, vm_mod """ stream_version = '5.21' run_remote_command_on_content_host('dnf -y upload-profile', vm_module_streams) - entities.Parameter( + module_target_sat.api.Parameter( name='remote_execution_connect_by_ip', value='Yes', parameter_type='boolean', @@ -1726,7 +1730,7 @@ def test_pagination_multiple_hosts_multiple_pages(session, module_host_template, @pytest.mark.tier3 -def test_search_for_virt_who_hypervisors(session, default_location): +def test_search_for_virt_who_hypervisors(session, default_location, module_target_sat): """ Search the virt_who hypervisors with hypervisor=True or hypervisor=False. @@ -1740,7 +1744,7 @@ def test_search_for_virt_who_hypervisors(session, default_location): :CaseImportance: Medium """ - org = entities.Organization().create() + org = module_target_sat.api.Organization().create() with session: session.organization.select(org.name) session.location.select(default_location.name) diff --git a/tests/foreman/ui/test_dashboard.py b/tests/foreman/ui/test_dashboard.py index ca93bd08650..9be16d4e6a9 100644 --- a/tests/foreman/ui/test_dashboard.py +++ b/tests/foreman/ui/test_dashboard.py @@ -11,8 +11,6 @@ :CaseImportance: High """ -from airgun.session import Session -from nailgun import entities from nailgun.entity_mixins import TaskFailedError import pytest @@ -23,7 +21,7 @@ @pytest.mark.tier2 -def test_positive_host_configuration_status(session): +def test_positive_host_configuration_status(session, target_sat): """Check if the Host Configuration Status Widget links are working :id: ffb0a6a1-2b65-4578-83c7-61492122d865 @@ -41,9 +39,9 @@ def test_positive_host_configuration_status(session): :BZ: 1631219 """ - org = entities.Organization().create() - loc = entities.Location().create() - host = entities.Host(organization=org, location=loc).create() + org = target_sat.api.Organization().create() + loc = target_sat.api.Location().create() + host = target_sat.api.Host(organization=org, location=loc).create() criteria_list = [ 'Hosts that had performed modifications without error', 'Hosts in error state', @@ -94,7 +92,7 @@ def test_positive_host_configuration_status(session): @pytest.mark.tier2 -def test_positive_host_configuration_chart(session): +def test_positive_host_configuration_chart(session, target_sat): """Check if the Host Configuration Chart is working in the Dashboard UI :id: b03314aa-4394-44e5-86da-c341c783003d @@ -107,9 +105,9 @@ def test_positive_host_configuration_chart(session): :expectedresults: Chart showing correct data """ - org = entities.Organization().create() - loc = entities.Location().create() - entities.Host(organization=org, location=loc).create() + org = target_sat.api.Organization().create() + loc = target_sat.api.Location().create() + target_sat.api.Host(organization=org, location=loc).create() with session: session.organization.select(org_name=org.name) session.location.select(loc_name=loc.name) @@ -120,7 +118,7 @@ def test_positive_host_configuration_chart(session): @pytest.mark.upgrade @pytest.mark.run_in_one_thread @pytest.mark.tier2 -def test_positive_task_status(session): +def test_positive_task_status(session, target_sat): """Check if the Task Status is working in the Dashboard UI and filter from Tasks index page is working correctly @@ -141,9 +139,11 @@ def test_positive_task_status(session): :BZ: 1718889 """ url = 'www.non_existent_repo_url.org' - org = entities.Organization().create() - product = entities.Product(organization=org).create() - repo = entities.Repository(url=f'http://{url}', product=product, content_type='yum').create() + org = target_sat.api.Organization().create() + product = target_sat.api.Product(organization=org).create() + repo = target_sat.api.Repository( + url=f'http://{url}', product=product, content_type='yum' + ).create() with pytest.raises(TaskFailedError): repo.sync() with session: @@ -222,9 +222,9 @@ def test_positive_user_access_with_host_filter( user_login = gen_string('alpha') user_password = gen_string('alphanumeric') org = function_entitlement_manifest_org - lce = entities.LifecycleEnvironment(organization=org).create() + lce = target_sat.api.LifecycleEnvironment(organization=org).create() # create a role with necessary permissions - role = entities.Role().create() + role = target_sat.api.Role().create() user_permissions = { 'Organization': ['view_organizations'], 'Location': ['view_locations'], @@ -233,7 +233,7 @@ def test_positive_user_access_with_host_filter( } target_sat.api_factory.create_role_permissions(role, user_permissions) # create a user and assign the above created role - entities.User( + target_sat.api.User( default_organization=org, organization=[org], default_location=module_location, @@ -242,7 +242,7 @@ def test_positive_user_access_with_host_filter( login=user_login, password=user_password, ).create() - with Session(test_name, user=user_login, password=user_password) as session: + with target_sat.ui_session(test_name, user=user_login, password=user_password) as session: assert session.dashboard.read('HostConfigurationStatus')['total_count'] == 0 assert len(session.dashboard.read('LatestErrata')['erratas']) == 0 rhel_contenthost.add_rex_key(target_sat) @@ -266,7 +266,7 @@ def test_positive_user_access_with_host_filter( @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_sync_overview_widget(session, module_org, module_product): +def test_positive_sync_overview_widget(session, module_product, module_target_sat): """Check if the Sync Overview widget is working in the Dashboard UI :id: 553fbe33-0f6f-46fb-8d80-5d1d9ed483cf @@ -280,7 +280,9 @@ def test_positive_sync_overview_widget(session, module_org, module_product): :BZ: 1995424 """ - repo = entities.Repository(url=settings.repos.yum_1.url, product=module_product).create() + repo = module_target_sat.api.Repository( + url=settings.repos.yum_1.url, product=module_product + ).create() with session: session.repository.synchronize(module_product.name, repo.name) sync_params = session.dashboard.read('SyncOverview')['syncs'] diff --git a/tests/foreman/ui/test_discoveryrule.py b/tests/foreman/ui/test_discoveryrule.py index e2b0fcef95e..0ca5af87129 100644 --- a/tests/foreman/ui/test_discoveryrule.py +++ b/tests/foreman/ui/test_discoveryrule.py @@ -11,7 +11,6 @@ :CaseImportance: High """ -from airgun.session import Session from fauxfactory import gen_integer, gen_ipaddr, gen_string import pytest @@ -82,7 +81,9 @@ def test_positive_crud_with_non_admin_user( new_priority = str(gen_integer(101, 200)) hg = module_target_sat.api.HostGroup(organization=[module_org]).create() new_hg_name = module_target_sat.api.HostGroup(organization=[module_org]).create() - with Session(user=manager_user.login, password=manager_user.password) as session: + with module_target_sat.ui_session( + user=manager_user.login, password=manager_user.password + ) as session: session.location.select(loc_name=module_location.name) session.discoveryrule.create( { @@ -145,7 +146,9 @@ def test_negative_delete_rule_with_non_admin_user( organization=[module_org], location=[module_location], ).create() - with Session(user=reader_user.login, password=reader_user.password) as session: + with module_target_sat.ui_session( + user=reader_user.login, password=reader_user.password + ) as session: with pytest.raises(ValueError): # noqa: PT011 - TODO Adarsh determine better exception session.discoveryrule.delete(dr.name) dr_val = session.discoveryrule.read_all() diff --git a/tests/foreman/ui/test_errata.py b/tests/foreman/ui/test_errata.py index f604e177bc2..cbbd91d71b1 100644 --- a/tests/foreman/ui/test_errata.py +++ b/tests/foreman/ui/test_errata.py @@ -11,11 +11,9 @@ :CaseImportance: High """ -from airgun.session import Session from broker import Broker from fauxfactory import gen_string from manifester import Manifester -from nailgun import entities import pytest from robottelo.config import settings @@ -50,9 +48,9 @@ pytestmark = [pytest.mark.run_in_one_thread] -def _generate_errata_applicability(hostname): +def _generate_errata_applicability(hostname, module_target_sat): """Force host to generate errata applicability""" - host = entities.Host().search(query={'search': f'name={hostname}'})[0].read() + host = module_target_sat.api.Host().search(query={'search': f'name={hostname}'})[0].read() host.errata_applicability(synchronous=False) @@ -140,9 +138,9 @@ def erratatype_vm(module_repos_collection_with_setup, target_sat): @pytest.fixture -def errata_status_installable(): +def errata_status_installable(module_target_sat): """Fixture to allow restoring errata_status_installable setting after usage""" - errata_status_installable = entities.Setting().search( + errata_status_installable = module_target_sat.api.Setting().search( query={'search': 'name="errata_status_installable"'} )[0] original_value = errata_status_installable.value @@ -390,7 +388,7 @@ def test_positive_list(session, function_org_with_parameter, lce, target_sat): indirect=True, ) def test_positive_list_permission( - test_name, module_org_with_parameter, module_repos_collection_with_setup + test_name, module_org_with_parameter, module_repos_collection_with_setup, module_target_sat ): """Show errata only if the User has permissions to view them @@ -408,23 +406,25 @@ def test_positive_list_permission( product only. """ module_org = module_org_with_parameter - role = entities.Role().create() - entities.Filter( + role = module_target_sat.api.Role().create() + module_target_sat.api.Filter( organization=[module_org], - permission=entities.Permission().search( + permission=module_target_sat.api.Permission().search( query={'search': 'resource_type="Katello::Product"'} ), role=role, search='name = "{}"'.format(PRDS['rhel']), ).create() user_password = gen_string('alphanumeric') - user = entities.User( + user = module_target_sat.api.User( default_organization=module_org, organization=[module_org], role=[role], password=user_password, ).create() - with Session(test_name, user=user.login, password=user_password) as session: + with module_target_sat.ui_session( + test_name, user=user.login, password=user_password + ) as session: assert ( session.errata.search(RHVA_ERRATA_ID, applicable=False)[0]['Errata ID'] == RHVA_ERRATA_ID @@ -568,14 +568,16 @@ def test_positive_filter_by_environment( ) assert _install_client_package(client, FAKE_1_CUSTOM_PACKAGE, errata_applicability=True) # Promote the latest content view version to a new lifecycle environment - content_view = entities.ContentView( + content_view = target_sat.api.ContentView( id=module_repos_collection_with_setup.setup_content_data['content_view']['id'] ).read() content_view_version = content_view.version[-1].read() lce = content_view_version.environment[-1].read() - new_lce = entities.LifecycleEnvironment(organization=module_org, prior=lce).create() + new_lce = target_sat.api.LifecycleEnvironment(organization=module_org, prior=lce).create() content_view_version.promote(data={'environment_ids': new_lce.id}) - host = entities.Host().search(query={'search': f'name={clients[0].hostname}'})[0].read() + host = ( + target_sat.api.Host().search(query={'search': f'name={clients[0].hostname}'})[0].read() + ) host.content_facet_attributes = { 'content_view_id': content_view.id, 'lifecycle_environment_id': new_lce.id, @@ -616,7 +618,7 @@ def test_positive_filter_by_environment( indirect=True, ) def test_positive_content_host_previous_env( - session, module_org_with_parameter, module_repos_collection_with_setup, vm + session, module_org_with_parameter, module_repos_collection_with_setup, vm, module_target_sat ): """Check if the applicable errata are available from the content host's previous environment @@ -639,14 +641,16 @@ def test_positive_content_host_previous_env( hostname = vm.hostname assert _install_client_package(vm, FAKE_1_CUSTOM_PACKAGE, errata_applicability=True) # Promote the latest content view version to a new lifecycle environment - content_view = entities.ContentView( + content_view = module_target_sat.api.ContentView( id=module_repos_collection_with_setup.setup_content_data['content_view']['id'] ).read() content_view_version = content_view.version[-1].read() lce = content_view_version.environment[-1].read() - new_lce = entities.LifecycleEnvironment(organization=module_org, prior=lce).create() + new_lce = module_target_sat.api.LifecycleEnvironment( + organization=module_org, prior=lce + ).create() content_view_version.promote(data={'environment_ids': new_lce.id}) - host = entities.Host().search(query={'search': f'name={hostname}'})[0].read() + host = module_target_sat.api.Host().search(query={'search': f'name={hostname}'})[0].read() host.content_facet_attributes = { 'content_view_id': content_view.id, 'lifecycle_environment_id': new_lce.id, @@ -914,7 +918,7 @@ def test_positive_filtered_errata_status_installable_param( :CaseImportance: Medium """ org = function_entitlement_manifest_org - lce = entities.LifecycleEnvironment(organization=org).create() + lce = target_sat.api.LifecycleEnvironment(organization=org).create() repos_collection = target_sat.cli_factory.RepositoryCollection( distro='rhel7', repositories=[ @@ -931,16 +935,16 @@ def test_positive_filtered_errata_status_installable_param( assert _install_client_package(client, FAKE_1_CUSTOM_PACKAGE, errata_applicability=True) # Adding content view filter and content view filter rule to exclude errata for the # installed package. - content_view = entities.ContentView( + content_view = target_sat.api.ContentView( id=repos_collection.setup_content_data['content_view']['id'] ).read() - cv_filter = entities.ErratumContentViewFilter( + cv_filter = target_sat.api.ErratumContentViewFilter( content_view=content_view, inclusion=False ).create() - errata = entities.Errata(content_view_version=content_view.version[-1]).search( + errata = target_sat.api.Errata(content_view_version=content_view.version[-1]).search( query=dict(search=f'errata_id="{CUSTOM_REPO_ERRATA_ID}"') )[0] - entities.ContentViewFilterRule(content_view_filter=cv_filter, errata=errata).create() + target_sat.api.ContentViewFilterRule(content_view_filter=cv_filter, errata=errata).create() content_view.publish() content_view = content_view.read() content_view_version = content_view.version[-1] diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index 8823accdb70..96957d68fa9 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -18,7 +18,6 @@ import re from airgun.exceptions import DisabledWidgetError, NoSuchElementException -from airgun.session import Session import pytest from wait_for import wait_for import yaml @@ -482,7 +481,7 @@ def test_positive_view_hosts_with_non_admin_user( created_host = target_sat.api.Host( location=smart_proxy_location, organization=module_org ).create() - with Session(test_name, user=user.login, password=user_password) as session: + with target_sat.ui_session(test_name, user=user.login, password=user_password) as session: host = session.host.get_details(created_host.name, widget_names='breadcrumb') assert host['breadcrumb'] == created_host.name content_host = session.contenthost.read(created_host.name, widget_names='breadcrumb') @@ -531,7 +530,7 @@ def test_positive_remove_parameter_non_admin_user( organization=module_org, host_parameters_attributes=[parameter], ).create() - with Session(test_name, user=user.login, password=user_password) as session: + with target_sat.ui_seesion(test_name, user=user.login, password=user_password) as session: values = session.host.read(host.name, 'parameters') assert values['parameters']['host_params'][0] == parameter session.host.update(host.name, {'parameters.host_params': []}) @@ -586,7 +585,7 @@ def test_negative_remove_parameter_non_admin_user( organization=module_org, host_parameters_attributes=[parameter], ).create() - with Session(test_name, user=user.login, password=user_password) as session: + with target_sat.ui_session(test_name, user=user.login, password=user_password) as session: values = session.host.read(host.name, 'parameters') assert values['parameters']['host_params'][0] == parameter with pytest.raises(NoSuchElementException) as context: @@ -695,7 +694,7 @@ def test_positive_check_permissions_affect_create_procedure( 'other_fields_values': {'host.lce': filter_lc_env.name}, }, ] - with Session(test_name, user=user.login, password=user_password) as session: + with target_sat.ui_session(test_name, user=user.login, password=user_password) as session: for host_field in host_fields: values = {host_field['name']: host_field['unexpected_value']} values.update(host_field.get('other_fields_values', {})) @@ -2327,7 +2326,7 @@ def test_positive_host_registration_with_non_admin_user( role = target_sat.cli.Role.info({'name': 'Register hosts'}) target_sat.cli.User.add_role({'id': user.id, 'role-id': role['id']}) - with Session(test_name, user=user.login, password=user_password) as session: + with target_sat.ui_session(test_name, user=user.login, password=user_password) as session: cmd = session.host_new.get_register_command( { @@ -2345,7 +2344,7 @@ def test_positive_host_registration_with_non_admin_user( @pytest.mark.tier2 -def test_all_hosts_delete(session, target_sat, function_org, function_location, new_host_ui): +def test_all_hosts_delete(target_sat, function_org, function_location, new_host_ui): """Create a host and delete it through All Hosts UI :id: 42b4560c-bb57-4c58-928e-e5fd5046b93f @@ -2364,7 +2363,7 @@ def test_all_hosts_delete(session, target_sat, function_org, function_location, @pytest.mark.tier2 -def test_all_hosts_bulk_delete(session, target_sat, function_org, function_location, new_host_ui): +def test_all_hosts_bulk_delete(target_sat, function_org, function_location, new_host_ui): """Create several hosts, and delete them via Bulk Actions in All Hosts UI :id: af1b4a66-dd83-47c3-904b-e8627119cc53 diff --git a/tests/foreman/ui/test_ldap_authentication.py b/tests/foreman/ui/test_ldap_authentication.py index e882f1c489d..8c799a72dc8 100644 --- a/tests/foreman/ui/test_ldap_authentication.py +++ b/tests/foreman/ui/test_ldap_authentication.py @@ -13,9 +13,7 @@ """ import os -from airgun.session import Session from fauxfactory import gen_url -from nailgun import entities from navmazing import NavigationTriesExceeded import pyotp import pytest @@ -55,43 +53,43 @@ def set_certificate_in_satellite(server_type, target_sat, hostname=None): @pytest.fixture -def ldap_usergroup_name(): +def ldap_usergroup_name(target_sat): """Return some random usergroup name, and attempt to delete such usergroup when test finishes. """ usergroup_name = gen_string('alphanumeric') yield usergroup_name - user_groups = entities.UserGroup().search(query={'search': f'name="{usergroup_name}"'}) + user_groups = target_sat.api.UserGroup().search(query={'search': f'name="{usergroup_name}"'}) if user_groups: user_groups[0].delete() @pytest.fixture -def ldap_tear_down(): +def ldap_tear_down(target_sat): """Teardown the all ldap settings user, usergroup and ldap delete""" yield - ldap_auth_sources = entities.AuthSourceLDAP().search() + ldap_auth_sources = target_sat.api.AuthSourceLDAP().search() for ldap_auth in ldap_auth_sources: - users = entities.User(auth_source=ldap_auth).search() + users = target_sat.api.User(auth_source=ldap_auth).search() for user in users: user.delete() ldap_auth.delete() @pytest.fixture -def external_user_count(): +def external_user_count(target_sat): """return the external auth source user count""" - users = entities.User().search() + users = target_sat.api.User().search() return len([user for user in users if user.auth_source_name == 'External']) @pytest.fixture -def groups_teardown(): +def groups_teardown(target_sat): """teardown for groups created for external/remote groups""" yield # tier down groups for group_name in ('sat_users', 'sat_admins', EXTERNAL_GROUP_NAME): - user_groups = entities.UserGroup().search(query={'search': f'name="{group_name}"'}) + user_groups = target_sat.api.UserGroup().search(query={'search': f'name="{group_name}"'}) if user_groups: user_groups[0].delete() @@ -172,7 +170,7 @@ def test_positive_end_to_end(session, ldap_auth_source, ldap_tear_down): @pytest.mark.parametrize('ldap_auth_source', ['AD', 'IPA', 'OPENLDAP'], indirect=True) @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_create_org_and_loc(session, ldap_auth_source, ldap_tear_down): +def test_positive_create_org_and_loc(session, ldap_auth_source, ldap_tear_down, target_sat): """Create LDAP auth_source with org and loc assigned. :id: 4f595af4-fc01-44c6-a614-a9ec827e3c3c @@ -190,8 +188,8 @@ def test_positive_create_org_and_loc(session, ldap_auth_source, ldap_tear_down): :parametrized: yes """ ldap_data, auth_source = ldap_auth_source - org = entities.Organization().create() - loc = entities.Location().create() + org = target_sat.api.Organization().create() + loc = target_sat.api.Location().create() ldap_auth_name = gen_string('alphanumeric') with session: session.ldapauthentication.create( @@ -256,7 +254,7 @@ def test_positive_add_katello_role( auth_source_name = f'LDAP-{auth_source.name}' ak_name = gen_string('alpha') user_permissions = {'Katello::ActivationKey': PERMISSIONS['Katello::ActivationKey']} - katello_role = entities.Role().create() + katello_role = target_sat.api.Role().create() target_sat.api_factory.create_role_permissions(katello_role, user_permissions) with session: session.usergroup.create( @@ -269,7 +267,9 @@ def test_positive_add_katello_role( ) assert session.usergroup.search(ldap_usergroup_name)[0]['Name'] == ldap_usergroup_name session.usergroup.refresh_external_group(ldap_usergroup_name, EXTERNAL_GROUP_NAME) - with Session(test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd']) as session: + with target_sat.ui_session( + test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd'] + ) as session: with pytest.raises(NavigationTriesExceeded): session.architecture.search('') session.activationkey.create({'name': ak_name}) @@ -308,8 +308,8 @@ def test_positive_update_external_roles( ak_name = gen_string('alpha') auth_source_name = f'LDAP-{auth_source.name}' location_name = gen_string('alpha') - foreman_role = entities.Role().create() - katello_role = entities.Role().create() + foreman_role = target_sat.api.Role().create() + katello_role = target_sat.api.Role().create() foreman_permissions = {'Location': PERMISSIONS['Location']} katello_permissions = {'Katello::ActivationKey': PERMISSIONS['Katello::ActivationKey']} target_sat.api_factory.create_role_permissions(foreman_role, foreman_permissions) @@ -324,19 +324,23 @@ def test_positive_update_external_roles( } ) assert session.usergroup.search(ldap_usergroup_name)[0]['Name'] == ldap_usergroup_name - with Session( + with target_sat.ui_session( test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd'] ) as ldapsession: with pytest.raises(NavigationTriesExceeded): ldapsession.architecture.search('') ldapsession.location.create({'name': location_name}) - location = entities.Location().search(query={'search': f'name="{location_name}"'})[0] + location = target_sat.api.Location().search( + query={'search': f'name="{location_name}"'} + )[0] assert location.name == location_name session.usergroup.update( ldap_usergroup_name, {'roles.resources.assigned': [katello_role.name]} ) session.usergroup.refresh_external_group(ldap_usergroup_name, EXTERNAL_GROUP_NAME) - with Session(test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd']) as session: + with target_sat.ui_session( + test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd'] + ) as session: session.activationkey.create({'name': ak_name}) assert session.activationkey.search(ak_name)[0]['Name'] == ak_name current_user = session.activationkey.read(ak_name, 'current_user')['current_user'] @@ -374,7 +378,7 @@ def test_positive_delete_external_roles( ldap_data, auth_source = ldap_auth_source auth_source_name = f'LDAP-{auth_source.name}' location_name = gen_string('alpha') - foreman_role = entities.Role().create() + foreman_role = target_sat.api.Role().create() foreman_permissions = {'Location': PERMISSIONS['Location']} target_sat.api_factory.create_role_permissions(foreman_role, foreman_permissions) with session: @@ -387,18 +391,20 @@ def test_positive_delete_external_roles( } ) assert session.usergroup.search(ldap_usergroup_name)[0]['Name'] == ldap_usergroup_name - with Session( + with target_sat.ui_session( test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd'] ) as ldapsession: with pytest.raises(NavigationTriesExceeded): ldapsession.architecture.search('') ldapsession.location.create({'name': location_name}) - location = entities.Location().search(query={'search': f'name="{location_name}"'})[0] + location = target_sat.api.Location().search( + query={'search': f'name="{location_name}"'} + )[0] assert location.name == location_name session.usergroup.update( ldap_usergroup_name, {'roles.resources.unassigned': [foreman_role.name]} ) - with Session( + with target_sat.ui_session( test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd'] ) as ldapsession: with pytest.raises(NavigationTriesExceeded): @@ -441,8 +447,8 @@ def test_positive_update_external_user_roles( ak_name = gen_string('alpha') auth_source_name = f'LDAP-{auth_source.name}' location_name = gen_string('alpha') - foreman_role = entities.Role().create() - katello_role = entities.Role().create() + foreman_role = target_sat.api.Role().create() + katello_role = target_sat.api.Role().create() foreman_permissions = {'Location': PERMISSIONS['Location']} katello_permissions = {'Katello::ActivationKey': PERMISSIONS['Katello::ActivationKey']} target_sat.api_factory.create_role_permissions(foreman_role, foreman_permissions) @@ -457,17 +463,21 @@ def test_positive_update_external_user_roles( } ) assert session.usergroup.search(ldap_usergroup_name)[0]['Name'] == ldap_usergroup_name - with Session( + with target_sat.ui_session( test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd'] ) as ldapsession: ldapsession.location.create({'name': location_name}) - location = entities.Location().search(query={'search': f'name="{location_name}"'})[0] + location = target_sat.api.Location().search( + query={'search': f'name="{location_name}"'} + )[0] assert location.name == location_name session.location.select(ANY_CONTEXT['location']) session.user.update( ldap_data['ldap_user_name'], {'roles.resources.assigned': [katello_role.name]} ) - with Session(test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd']) as session: + with target_sat.ui_session( + test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd'] + ) as session: with pytest.raises(NavigationTriesExceeded): ldapsession.architecture.search('') session.activationkey.create({'name': ak_name}) @@ -485,6 +495,7 @@ def test_positive_add_admin_role_with_org_loc( module_org, ldap_tear_down, ldap_auth_source, + target_sat, ): """Associate Admin role to User Group with org and loc set. [belonging to external User Group.] @@ -521,7 +532,9 @@ def test_positive_add_admin_role_with_org_loc( } ) assert session.usergroup.search(ldap_usergroup_name)[0]['Name'] == ldap_usergroup_name - with Session(test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd']) as session: + with target_sat.ui_session( + test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd'] + ) as session: session.location.create({'name': location_name}) assert session.location.search(location_name)[0]['Name'] == location_name location = session.location.read(location_name, ['current_user', 'primary']) @@ -576,7 +589,7 @@ def test_positive_add_foreman_role_with_org_loc( 'Location': ['assign_locations'], 'Organization': ['assign_organizations'], } - foreman_role = entities.Role().create() + foreman_role = module_target_sat.api.Role().create() module_target_sat.api_factory.create_role_permissions(foreman_role, user_permissions) with session: session.usergroup.create( @@ -589,7 +602,7 @@ def test_positive_add_foreman_role_with_org_loc( ) assert session.usergroup.search(ldap_usergroup_name)[0]['Name'] == ldap_usergroup_name session.usergroup.refresh_external_group(ldap_usergroup_name, EXTERNAL_GROUP_NAME) - with Session( + with module_target_sat.ui_session( test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd'] ) as ldapsession: with pytest.raises(NavigationTriesExceeded): @@ -641,9 +654,9 @@ def test_positive_add_katello_role_with_org( 'Location': ['assign_locations'], 'Organization': ['assign_organizations'], } - katello_role = entities.Role().create() + katello_role = target_sat.api.Role().create() target_sat.api_factory.create_role_permissions(katello_role, user_permissions) - different_org = entities.Organization().create() + different_org = target_sat.api.Organization().create() with session: session.usergroup.create( { @@ -655,7 +668,7 @@ def test_positive_add_katello_role_with_org( ) assert session.usergroup.search(ldap_usergroup_name)[0]['Name'] == ldap_usergroup_name session.usergroup.refresh_external_group(ldap_usergroup_name, EXTERNAL_GROUP_NAME) - with Session( + with target_sat.ui_session( test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd'] ) as ldapsession: with pytest.raises(NavigationTriesExceeded): @@ -666,7 +679,7 @@ def test_positive_add_katello_role_with_org( session.organization.select(different_org.name) assert not session.activationkey.search(ak_name)[0]['Name'] == ak_name ak = ( - entities.ActivationKey(organization=module_org) + target_sat.api.ActivationKey(organization=module_org) .search(query={'search': f'name={ak_name}'})[0] .read() ) @@ -699,7 +712,7 @@ def test_positive_create_user_in_ldap_mode(session, ldap_auth_source, ldap_tear_ @pytest.mark.parametrize('ldap_auth_source', ['AD', 'IPA'], indirect=True) @pytest.mark.tier2 -def test_positive_login_user_no_roles(test_name, ldap_tear_down, ldap_auth_source): +def test_positive_login_user_no_roles(test_name, ldap_tear_down, ldap_auth_source, target_sat): """Login with LDAP Auth for user with no roles/rights :id: 7dc8d9a7-ff08-4d8e-a842-d370ffd69741 @@ -716,7 +729,7 @@ def test_positive_login_user_no_roles(test_name, ldap_tear_down, ldap_auth_sourc :parametrized: yes """ ldap_data, auth_source = ldap_auth_source - with Session( + with target_sat.ui_session( test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd'] ) as ldapsession: ldapsession.task.read_all() @@ -743,17 +756,17 @@ def test_positive_login_user_basic_roles( """ ldap_data, auth_source = ldap_auth_source name = gen_string('alpha') - role = entities.Role().create() + role = target_sat.api.Role().create() permissions = {'Architecture': PERMISSIONS['Architecture']} target_sat.api_factory.create_role_permissions(role, permissions) - with Session( + with target_sat.ui_session( test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd'] ) as ldapsession: with pytest.raises(NavigationTriesExceeded): ldapsession.usergroup.search('') with session: session.user.update(ldap_data['ldap_user_name'], {'roles.resources.assigned': [role.name]}) - with Session( + with target_sat.ui_session( test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd'] ) as ldapsession: ldapsession.architecture.create({'name': name}) @@ -763,7 +776,7 @@ def test_positive_login_user_basic_roles( @pytest.mark.upgrade @pytest.mark.tier2 def test_positive_login_user_password_otp( - auth_source_ipa, default_ipa_host, test_name, ldap_tear_down + auth_source_ipa, default_ipa_host, test_name, ldap_tear_down, target_sat ): """Login with password with time based OTP @@ -781,16 +794,20 @@ def test_positive_login_user_password_otp( otp_pass = ( f"{default_ipa_host.ldap_user_passwd}{generate_otp(default_ipa_host.time_based_secret)}" ) - with Session(test_name, default_ipa_host.ipa_otp_username, otp_pass) as ldapsession: + with target_sat.ui_session( + test_name, default_ipa_host.ipa_otp_username, otp_pass + ) as ldapsession: with pytest.raises(NavigationTriesExceeded): ldapsession.user.search('') - users = entities.User().search(query={'search': f'login="{default_ipa_host.ipa_otp_username}"'}) + users = target_sat.api.User().search( + query={'search': f'login="{default_ipa_host.ipa_otp_username}"'} + ) assert users[0].login == default_ipa_host.ipa_otp_username @pytest.mark.tier2 def test_negative_login_user_with_invalid_password_otp( - auth_source_ipa, default_ipa_host, test_name, ldap_tear_down + auth_source_ipa, default_ipa_host, test_name, ldap_tear_down, target_sat ): """Login with password with time based OTP @@ -808,7 +825,9 @@ def test_negative_login_user_with_invalid_password_otp( password_with_otp = ( f"{default_ipa_host.ldap_user_passwd}{gen_string(str_type='numeric', length=6)}" ) - with Session(test_name, default_ipa_host.ipa_otp_username, password_with_otp) as ldapsession: + with target_sat.ui_session( + test_name, default_ipa_host.ipa_otp_username, password_with_otp + ) as ldapsession: with pytest.raises(NavigationTriesExceeded) as error: ldapsession.user.search('') assert error.typename == 'NavigationTriesExceeded' @@ -836,7 +855,7 @@ def test_positive_test_connection_functionality(session, ldap_auth_source): @pytest.mark.parametrize('ldap_auth_source', ['AD', 'IPA', 'OPENLDAP'], indirect=True) @pytest.mark.tier2 -def test_negative_login_with_incorrect_password(test_name, ldap_auth_source): +def test_negative_login_with_incorrect_password(test_name, ldap_auth_source, target_sat): """Attempt to login in Satellite an user with the wrong password :id: 3f09de90-a656-11ea-aa43-4ceb42ab8dbc @@ -853,7 +872,7 @@ def test_negative_login_with_incorrect_password(test_name, ldap_auth_source): """ ldap_data, auth_source = ldap_auth_source incorrect_password = gen_string('alphanumeric') - with Session( + with target_sat.ui_session( test_name, user=ldap_data['ldap_user_name'], password=incorrect_password ) as ldapsession: with pytest.raises(NavigationTriesExceeded) as error: @@ -862,7 +881,9 @@ def test_negative_login_with_incorrect_password(test_name, ldap_auth_source): @pytest.mark.tier2 -def test_negative_login_with_disable_user(default_ipa_host, auth_source_ipa, ldap_tear_down): +def test_negative_login_with_disable_user( + default_ipa_host, auth_source_ipa, ldap_tear_down, target_sat +): """Disabled IDM user cannot login :id: 49f28006-aa1f-11ea-90d3-4ceb42ab8dbc @@ -873,7 +894,7 @@ def test_negative_login_with_disable_user(default_ipa_host, auth_source_ipa, lda :expectedresults: Login fails """ - with Session( + with target_sat.ui_session( user=default_ipa_host.disabled_user_ipa, password=default_ipa_host.ldap_user_passwd ) as ldapsession: with pytest.raises(NavigationTriesExceeded) as error: @@ -883,7 +904,7 @@ def test_negative_login_with_disable_user(default_ipa_host, auth_source_ipa, lda @pytest.mark.tier2 def test_email_of_the_user_should_be_copied( - session, default_ipa_host, auth_source_ipa, ldap_tear_down + session, default_ipa_host, auth_source_ipa, ldap_tear_down, target_sat ): """Email of the user created in idm server ( set as external authorization source ) should be copied to the satellite. @@ -904,7 +925,7 @@ def test_email_of_the_user_should_be_copied( if 'Email' in line: _, result = line.split(': ', 2) break - with Session( + with target_sat.ui_session( user=default_ipa_host.ldap_user_name, password=default_ipa_host.ldap_user_passwd ) as ldapsession: ldapsession.bookmark.search('controller = hosts') @@ -931,10 +952,10 @@ def test_deleted_idm_user_should_not_be_able_to_login( """ test_user = gen_string('alpha') default_ipa_host.create_user(test_user) - with Session(user=test_user, password=settings.ipa.password) as ldapsession: + with target_sat.ui_session(user=test_user, password=settings.ipa.password) as ldapsession: ldapsession.bookmark.search('controller = hosts') default_ipa_host.delete_user(test_user) - with Session(user=test_user, password=settings.ipa.password) as ldapsession: + with target_sat.ui_session(user=test_user, password=settings.ipa.password) as ldapsession: with pytest.raises(NavigationTriesExceeded) as error: ldapsession.user.search('') assert error.typename == 'NavigationTriesExceeded' @@ -942,7 +963,7 @@ def test_deleted_idm_user_should_not_be_able_to_login( @pytest.mark.parametrize('ldap_auth_source', ['AD', 'IPA', 'OPENLDAP'], indirect=True) @pytest.mark.tier2 -def test_onthefly_functionality(session, ldap_auth_source, ldap_tear_down): +def test_onthefly_functionality(session, ldap_auth_source, ldap_tear_down, target_sat): """User will not be created automatically in Satellite if onthefly is disabled @@ -977,7 +998,7 @@ def test_onthefly_functionality(session, ldap_auth_source, ldap_tear_down): 'attribute_mappings.mail': LDAP_ATTR['mail'], } ) - with Session( + with target_sat.ui_session( user=ldap_data['ldap_user_name'], password=ldap_data['ldap_user_passwd'] ) as ldapsession: with pytest.raises(NavigationTriesExceeded) as error: @@ -1028,7 +1049,9 @@ def test_timeout_and_cac_card_ejection(): @pytest.mark.parametrize('ldap_auth_source', ['AD', 'IPA', 'OPENLDAP'], indirect=True) @pytest.mark.tier2 @pytest.mark.skip_if_open('BZ:1670397') -def test_verify_attribute_of_users_are_updated(session, ldap_auth_source, ldap_tear_down): +def test_verify_attribute_of_users_are_updated( + session, ldap_auth_source, ldap_tear_down, target_sat +): """Verify if attributes of LDAP user are updated upon first login when onthefly is disabled @@ -1076,7 +1099,7 @@ def test_verify_attribute_of_users_are_updated(session, ldap_auth_source, ldap_t 'roles.admin': True, } ) - with Session( + with target_sat.ui_session( user=ldap_data['ldap_user_name'], password=ldap_data['ldap_user_passwd'] ) as ldapsession: with pytest.raises(NavigationTriesExceeded) as error: @@ -1093,7 +1116,7 @@ def test_verify_attribute_of_users_are_updated(session, ldap_auth_source, ldap_t @pytest.mark.parametrize('ldap_auth_source', ['AD', 'IPA', 'OPENLDAP'], indirect=True) @pytest.mark.tier2 def test_login_failure_if_internal_user_exist( - session, test_name, ldap_auth_source, module_org, module_location, ldap_tear_down + session, test_name, ldap_auth_source, module_org, module_location, ldap_tear_down, target_sat ): """Verify the failure of login for the AD/IPA user in case same username internal user exists @@ -1115,19 +1138,21 @@ def test_login_failure_if_internal_user_exist( try: internal_username = ldap_data['ldap_user_name'] internal_password = gen_string('alphanumeric') - user = entities.User( + user = target_sat.api.User( admin=True, default_organization=module_org, default_location=module_location, login=internal_username, password=internal_password, ).create() - with Session(test_name, internal_username, ldap_data['ldap_user_passwd']) as ldapsession: + with target_sat.ui_session( + test_name, internal_username, ldap_data['ldap_user_passwd'] + ) as ldapsession: with pytest.raises(NavigationTriesExceeded) as error: ldapsession.user.search('') assert error.typename == 'NavigationTriesExceeded' finally: - entities.User(id=user.id).delete() + target_sat.api.User(id=user.id).delete() @pytest.mark.skip_if_open("BZ:1812688") @@ -1165,7 +1190,7 @@ def test_userlist_with_external_admin( auth_source_name = f'LDAP-{auth_source_ipa.name}' user_permissions = {'Katello::ActivationKey': PERMISSIONS['Katello::ActivationKey']} - katello_role = entities.Role().create() + katello_role = target_sat.api.Role().create() target_sat.api_factory.create_role_permissions(katello_role, user_permissions) with session: session.usergroup.create( @@ -1184,12 +1209,14 @@ def test_userlist_with_external_admin( 'external_groups.auth_source': auth_source_name, } ) - with Session(user=idm_user, password=settings.server.ssh_password) as ldapsession: + with target_sat.ui_session(user=idm_user, password=settings.server.ssh_password) as ldapsession: assert idm_user in ldapsession.task.read_all()['current_user'] # verify the users count with local admin and remote/external admin - with Session(user=idm_admin, password=settings.server.ssh_password) as remote_admin_session: - with Session( + with target_sat.ui_session( + user=idm_admin, password=settings.server.ssh_password + ) as remote_admin_session: + with target_sat.ui_session( user=settings.server.admin_username, password=settings.server.admin_password ) as local_admin_session: assert local_admin_session.user.search(idm_user)[0]['Username'] == idm_user @@ -1224,7 +1251,7 @@ def test_positive_group_sync_open_ldap_authsource( ak_name = gen_string('alpha') auth_source_name = f'LDAP-{auth_source_open_ldap.name}' user_permissions = {'Katello::ActivationKey': PERMISSIONS['Katello::ActivationKey']} - katello_role = entities.Role().create() + katello_role = target_sat.api.Role().create() target_sat.api_factory.create_role_permissions(katello_role, user_permissions) with session: session.usergroup.create( @@ -1238,7 +1265,7 @@ def test_positive_group_sync_open_ldap_authsource( assert session.usergroup.search(ldap_usergroup_name)[0]['Name'] == ldap_usergroup_name session.usergroup.refresh_external_group(ldap_usergroup_name, EXTERNAL_GROUP_NAME) user_name = open_ldap_data.open_ldap_user - with Session(test_name, user_name, open_ldap_data.password) as session: + with target_sat.ui_session(test_name, user_name, open_ldap_data.password) as session: with pytest.raises(NavigationTriesExceeded): session.architecture.search('') session.activationkey.create({'name': ak_name}) @@ -1274,7 +1301,7 @@ def test_verify_group_permissions( idm_users = settings.ipa.group_users auth_source_name = f'LDAP-{auth_source_ipa.name}' user_permissions = {None: ['access_dashboard']} - katello_role = entities.Role().create() + katello_role = target_sat.api.Role().create() target_sat.api_factory.create_role_permissions(katello_role, user_permissions) with session: session.usergroup.create( @@ -1294,15 +1321,17 @@ def test_verify_group_permissions( } ) location_name = gen_string('alpha') - with Session(user=idm_users[1], password=settings.server.ssh_password) as ldapsession: + with target_sat.ui_session( + user=idm_users[1], password=settings.server.ssh_password + ) as ldapsession: ldapsession.location.create({'name': location_name}) - location = entities.Location().search(query={'search': f'name="{location_name}"'})[0] + location = target_sat.api.Location().search(query={'search': f'name="{location_name}"'})[0] assert location.name == location_name @pytest.mark.tier2 def test_verify_ldap_filters_ipa( - session, ipa_add_user, auth_source_ipa, default_ipa_host, ldap_tear_down + session, ipa_add_user, auth_source_ipa, default_ipa_host, ldap_tear_down, target_sat ): """Verifying ldap filters in authsource to restrict access @@ -1319,7 +1348,9 @@ def test_verify_ldap_filters_ipa( # 'test_user' able to login before the filter is applied. test_user = ipa_add_user - with Session(user=test_user, password=default_ipa_host.ldap_user_passwd) as ldapsession: + with target_sat.ui_session( + user=test_user, password=default_ipa_host.ldap_user_passwd + ) as ldapsession: ldapsession.task.read_all() # updating the authsource with filter @@ -1328,7 +1359,9 @@ def test_verify_ldap_filters_ipa( session.ldapauthentication.update(auth_source_ipa.name, {'account.ldap_filter': ldap_data}) # 'test_user' not able login as it gets filtered out - with Session(user=test_user, password=default_ipa_host.ldap_user_passwd) as ldapsession: + with target_sat.ui_session( + user=test_user, password=default_ipa_host.ldap_user_passwd + ) as ldapsession: with pytest.raises(NavigationTriesExceeded) as error: ldapsession.user.search('') assert error.typename == 'NavigationTriesExceeded' diff --git a/tests/foreman/ui/test_lifecycleenvironment.py b/tests/foreman/ui/test_lifecycleenvironment.py index e99d9f9c895..9c389fdd19d 100644 --- a/tests/foreman/ui/test_lifecycleenvironment.py +++ b/tests/foreman/ui/test_lifecycleenvironment.py @@ -11,7 +11,6 @@ :CaseImportance: High """ -from airgun.session import Session from navmazing import NavigationTriesExceeded import pytest @@ -298,7 +297,7 @@ def test_positive_custom_user_view_lce(session, test_name, target_sat): lce_values = session.lifecycleenvironment.read_all() assert lce_name in lce_values['lce'] # ensure the created user also can find the created lifecycle environment link - with Session(test_name, user_login, user_password) as non_admin_session: + with target_sat.ui_session(test_name, user_login, user_password) as non_admin_session: # to ensure that the created user has only the assigned # permissions, check that hosts menu tab does not exist with pytest.raises(NavigationTriesExceeded): diff --git a/tests/foreman/ui/test_repository.py b/tests/foreman/ui/test_repository.py index 08d44cc052e..f5382f1ad00 100644 --- a/tests/foreman/ui/test_repository.py +++ b/tests/foreman/ui/test_repository.py @@ -14,8 +14,6 @@ from datetime import datetime, timedelta from random import randint, shuffle -from airgun.session import Session -from nailgun import entities from navmazing import NavigationTriesExceeded import pytest @@ -41,19 +39,19 @@ @pytest.fixture(scope='module') -def module_org(): - return entities.Organization().create() +def module_org(module_target_sat): + return module_target_sat.api.Organization().create() @pytest.fixture(scope='module') -def module_prod(module_org): - return entities.Product(organization=module_org).create() +def module_prod(module_org, module_target_sat): + return module_target_sat.api.Product(organization=module_org).create() @pytest.mark.tier2 @pytest.mark.upgrade @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_create_in_different_orgs(session, module_org): +def test_positive_create_in_different_orgs(session, module_org, module_target_sat): """Create repository in two different orgs with same name :id: 019c2242-8802-4bae-82c5-accf8f793dbc @@ -62,9 +60,9 @@ def test_positive_create_in_different_orgs(session, module_org): organizations """ repo_name = gen_string('alpha') - org2 = entities.Organization().create() - prod1 = entities.Product(organization=module_org).create() - prod2 = entities.Product(organization=org2).create() + org2 = module_target_sat.api.Organization().create() + prod1 = module_target_sat.api.Product(organization=module_org).create() + prod2 = module_target_sat.api.Product(organization=org2).create() with session: for org, prod in [[module_org, prod1], [org2, prod2]]: session.organization.select(org_name=org.name) @@ -107,9 +105,9 @@ def test_positive_create_as_non_admin_user(module_org, test_name, target_sat): 'sync_products', ], } - role = entities.Role().create() + role = target_sat.api.Role().create() target_sat.api_factory.create_role_permissions(role, user_permissions) - entities.User( + target_sat.api.User( login=user_login, password=user_password, role=[role], @@ -117,8 +115,8 @@ def test_positive_create_as_non_admin_user(module_org, test_name, target_sat): default_organization=module_org, organization=[module_org], ).create() - product = entities.Product(organization=module_org).create() - with Session(test_name, user=user_login, password=user_password) as session: + product = target_sat.api.Product(organization=module_org).create() + with target_sat.ui_session(test_name, user=user_login, password=user_password) as session: # ensure that the created user is not a global admin user # check administer->organizations page with pytest.raises(NavigationTriesExceeded): @@ -137,7 +135,7 @@ def test_positive_create_as_non_admin_user(module_org, test_name, target_sat): @pytest.mark.tier2 @pytest.mark.upgrade @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_create_yum_repo_same_url_different_orgs(session, module_prod): +def test_positive_create_yum_repo_same_url_different_orgs(session, module_prod, module_target_sat): """Create two repos with the same URL in two different organizations. :id: f4cb00ed-6faf-4c79-9f66-76cd333299cb @@ -145,12 +143,16 @@ def test_positive_create_yum_repo_same_url_different_orgs(session, module_prod): :expectedresults: Repositories are created and have equal number of packages. """ # Create first repository - repo = entities.Repository(product=module_prod, url=settings.repos.yum_0.url).create() + repo = module_target_sat.api.Repository( + product=module_prod, url=settings.repos.yum_0.url + ).create() repo.sync() # Create second repository - org = entities.Organization().create() - product = entities.Product(organization=org).create() - new_repo = entities.Repository(product=product, url=settings.repos.yum_0.url).create() + org = module_target_sat.api.Organization().create() + product = module_target_sat.api.Product(organization=org).create() + new_repo = module_target_sat.api.Repository( + product=product, url=settings.repos.yum_0.url + ).create() new_repo.sync() with session: # Check packages number in first repository @@ -194,9 +196,9 @@ def test_positive_create_as_non_admin_user_with_cv_published(module_org, test_na 'sync_products', ], } - role = entities.Role().create() + role = target_sat.api.Role().create() target_sat.api_factory.create_role_permissions(role, user_permissions) - entities.User( + target_sat.api.User( login=user_login, password=user_password, role=[role], @@ -204,14 +206,14 @@ def test_positive_create_as_non_admin_user_with_cv_published(module_org, test_na default_organization=module_org, organization=[module_org], ).create() - prod = entities.Product(organization=module_org).create() - repo = entities.Repository(product=prod, url=settings.repos.yum_2.url).create() + prod = target_sat.api.Product(organization=module_org).create() + repo = target_sat.api.Repository(product=prod, url=settings.repos.yum_2.url).create() repo.sync() - content_view = entities.ContentView(organization=module_org).create() + content_view = target_sat.api.ContentView(organization=module_org).create() content_view.repository = [repo] content_view = content_view.update(['repository']) content_view.publish() - with Session(test_name, user_login, user_password) as session: + with target_sat.ui_session(test_name, user_login, user_password) as session: # ensure that the created user is not a global admin user # check administer->users page pswd = gen_string('alphanumeric') @@ -243,7 +245,7 @@ def test_positive_create_as_non_admin_user_with_cv_published(module_org, test_na @pytest.mark.upgrade @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') @pytest.mark.usefixtures('allow_repo_discovery') -def test_positive_discover_repo_via_existing_product(session, module_org): +def test_positive_discover_repo_via_existing_product(session, module_org, module_target_sat): """Create repository via repo-discovery under existing product :id: 9181950c-a756-456f-a46a-059e7a2add3c @@ -251,7 +253,7 @@ def test_positive_discover_repo_via_existing_product(session, module_org): :expectedresults: Repository is discovered and created """ repo_name = 'fakerepo01' - product = entities.Product(organization=module_org).create() + product = module_target_sat.api.Product(organization=module_org).create() with session: session.organization.select(org_name=module_org.name) session.product.discover_repo( @@ -297,7 +299,9 @@ def test_positive_discover_repo_via_new_product(session, module_org): @pytest.mark.upgrade @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') @pytest.mark.usefixtures('allow_repo_discovery') -def test_positive_discover_module_stream_repo_via_existing_product(session, module_org): +def test_positive_discover_module_stream_repo_via_existing_product( + session, module_org, module_target_sat +): """Create repository with module streams via repo-discovery under an existing product. :id: e7b9e2c4-7ecd-4cde-8f74-961fbac8919c @@ -315,7 +319,7 @@ def test_positive_discover_module_stream_repo_via_existing_product(session, modu """ repo_name = gen_string('alpha') repo_label = gen_string('alpha') - product = entities.Product(organization=module_org).create() + product = module_target_sat.api.Product(organization=module_org).create() with session: session.organization.select(org_name=module_org.name) session.product.discover_repo( @@ -336,15 +340,15 @@ def test_positive_discover_module_stream_repo_via_existing_product(session, modu @pytest.mark.tier2 @pytest.mark.upgrade @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_sync_custom_repo_yum(session, module_org): +def test_positive_sync_custom_repo_yum(session, module_org, module_target_sat): """Create Custom yum repos and sync it via the repos page. :id: afa218f4-e97a-4240-a82a-e69538d837a1 :expectedresults: Sync procedure for specific yum repository is successful """ - product = entities.Product(organization=module_org).create() - repo = entities.Repository(url=settings.repos.yum_1.url, product=product).create() + product = module_target_sat.api.Product(organization=module_org).create() + repo = module_target_sat.api.Repository(url=settings.repos.yum_1.url, product=product).create() with session: result = session.repository.synchronize(product.name, repo.name) assert result['result'] == 'success' @@ -357,7 +361,7 @@ def test_positive_sync_custom_repo_yum(session, module_org): @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_sync_custom_repo_docker(session, module_org): +def test_positive_sync_custom_repo_docker(session, module_org, module_target_sat): """Create Custom docker repos and sync it via the repos page. :id: 942e0b4f-3524-4f00-812d-bdad306f81de @@ -365,8 +369,8 @@ def test_positive_sync_custom_repo_docker(session, module_org): :expectedresults: Sync procedure for specific docker repository is successful """ - product = entities.Product(organization=module_org).create() - repo = entities.Repository( + product = module_target_sat.api.Product(organization=module_org).create() + repo = module_target_sat.api.Repository( url=CONTAINER_REGISTRY_HUB, product=product, content_type=REPO_TYPE['docker'] ).create() with session: @@ -376,7 +380,7 @@ def test_positive_sync_custom_repo_docker(session, module_org): @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_resync_custom_repo_after_invalid_update(session, module_org): +def test_positive_resync_custom_repo_after_invalid_update(session, module_org, module_target_sat): """Create Custom yum repo and sync it via the repos page. Then try to change repo url to invalid one and re-sync that repository @@ -389,8 +393,8 @@ def test_positive_resync_custom_repo_after_invalid_update(session, module_org): :BZ: 1487173, 1262313 """ - product = entities.Product(organization=module_org).create() - repo = entities.Repository(url=settings.repos.yum_1.url, product=product).create() + product = module_target_sat.api.Product(organization=module_org).create() + repo = module_target_sat.api.Repository(url=settings.repos.yum_1.url, product=product).create() with session: result = session.repository.synchronize(product.name, repo.name) assert result['result'] == 'success' @@ -408,7 +412,7 @@ def test_positive_resync_custom_repo_after_invalid_update(session, module_org): @pytest.mark.tier2 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_resynchronize_rpm_repo(session, module_prod): +def test_positive_resynchronize_rpm_repo(session, module_prod, module_target_sat): """Check that repository content is resynced after packages were removed from repository @@ -418,7 +422,7 @@ def test_positive_resynchronize_rpm_repo(session, module_prod): :BZ: 1318004 """ - repo = entities.Repository( + repo = module_target_sat.api.Repository( url=settings.repos.yum_1.url, content_type=REPO_TYPE['yum'], product=module_prod ).create() with session: @@ -442,7 +446,7 @@ def test_positive_resynchronize_rpm_repo(session, module_prod): @pytest.mark.tier2 @pytest.mark.upgrade @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_end_to_end_custom_yum_crud(session, module_org, module_prod): +def test_positive_end_to_end_custom_yum_crud(session, module_org, module_prod, module_target_sat): """Perform end to end testing for custom yum repository :id: 8baf11c9-019e-4625-a549-ec4cd9312f75 @@ -455,11 +459,11 @@ def test_positive_end_to_end_custom_yum_crud(session, module_org, module_prod): checksum_type = 'sha256' new_repo_name = gen_string('alphanumeric') new_checksum_type = 'sha1' - gpg_key = entities.GPGKey( + gpg_key = module_target_sat.api.GPGKey( content=DataFile.VALID_GPG_KEY_FILE.read_bytes(), organization=module_org, ).create() - new_gpg_key = entities.GPGKey( + new_gpg_key = module_target_sat.api.GPGKey( content=DataFile.VALID_GPG_KEY_BETA_FILE.read_bytes(), organization=module_org, ).create() @@ -758,7 +762,7 @@ def test_positive_reposet_disable_after_manifest_deleted( :BZ: 1344391 """ org = function_entitlement_manifest_org - sub = entities.Subscription(organization=org) + sub = target_sat.api.Subscription(organization=org) sat_tools_repo = target_sat.cli_factory.SatelliteToolsRepository(distro='rhel7', cdn=True) repository_name = sat_tools_repo.data['repository'] repository_name_orphaned = f'{repository_name} (Orphaned)' @@ -798,7 +802,7 @@ def test_positive_reposet_disable_after_manifest_deleted( @pytest.mark.tier2 -def test_positive_delete_random_docker_repo(session, module_org): +def test_positive_delete_random_docker_repo(session, module_org, module_target_sat): """Create Docker-type repositories on multiple products and delete a random repository from a random product. @@ -808,9 +812,12 @@ def test_positive_delete_random_docker_repo(session, module_org): without altering the other products. """ entities_list = [] - products = [entities.Product(organization=module_org).create() for _ in range(randint(2, 5))] + products = [ + module_target_sat.api.Product(organization=module_org).create() + for _ in range(randint(2, 5)) + ] for product in products: - repo = entities.Repository( + repo = module_target_sat.api.Repository( url=CONTAINER_REGISTRY_HUB, product=product, content_type=REPO_TYPE['docker'] ).create() entities_list.append((product.name, repo.name)) @@ -1178,7 +1185,7 @@ def test_positive_select_org_in_any_context(): @pytest.mark.tier2 @pytest.mark.upgrade @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_sync_repo_and_verify_checksum(session, module_org): +def test_positive_sync_repo_and_verify_checksum(session, module_org, module_target_sat): """Tests that Verify Content Checksum succeeds when executing from the products page :id: 577be1f8-7510-49d2-8b33-600db60bd960 @@ -1194,7 +1201,7 @@ def test_positive_sync_repo_and_verify_checksum(session, module_org): :expectedresults: Verify Content Checksum task succeeds """ repo_name = gen_string('alpha') - product = entities.Product(organization=module_org).create() + product = module_target_sat.api.Product(organization=module_org).create() with session: session.repository.create( product.name, @@ -1210,7 +1217,7 @@ def test_positive_sync_repo_and_verify_checksum(session, module_org): @pytest.mark.tier2 -def test_positive_sync_sha_repo(session, module_org): +def test_positive_sync_sha_repo(session, module_org, module_target_sat): """Sync 'sha' repo successfully :id: 6172035f-96c4-41e4-a79b-acfaa78ad734 @@ -1222,7 +1229,7 @@ def test_positive_sync_sha_repo(session, module_org): :SubComponent: Candlepin """ repo_name = gen_string('alpha') - product = entities.Product(organization=module_org).create() + product = module_target_sat.api.Product(organization=module_org).create() with session: session.repository.create( product.name, @@ -1237,7 +1244,7 @@ def test_positive_sync_sha_repo(session, module_org): @pytest.mark.tier2 -def test_positive_sync_third_party_repo(session, module_org): +def test_positive_sync_third_party_repo(session, module_org, module_target_sat): """Sync third part repo successfully :id: 655161e0-aa90-4c7c-9a0d-cb5b9f56eac3 @@ -1249,7 +1256,7 @@ def test_positive_sync_third_party_repo(session, module_org): :SubComponent: Pulp """ repo_name = gen_string('alpha') - product = entities.Product(organization=module_org).create() + product = module_target_sat.api.Product(organization=module_org).create() with session: session.repository.create( product.name, diff --git a/tests/foreman/ui/test_role.py b/tests/foreman/ui/test_role.py index dd7c9c9ddf3..652bdcbd4f6 100644 --- a/tests/foreman/ui/test_role.py +++ b/tests/foreman/ui/test_role.py @@ -13,8 +13,6 @@ """ import random -from airgun.session import Session -from nailgun import entities from navmazing import NavigationTriesExceeded import pytest @@ -25,7 +23,7 @@ @pytest.mark.e2e @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_end_to_end(session, module_org, module_location): +def test_positive_end_to_end(session, module_org, module_location, module_target_sat): """Perform end to end testing for role component :id: 3284016a-e2df-4a0e-aa24-c95ab132eec1 @@ -45,8 +43,8 @@ def test_positive_end_to_end(session, module_org, module_location): cloned_role_name = gen_string('alpha') new_role_name = gen_string('alpha') new_role_description = gen_string('alpha') - new_org = entities.Organization().create() - new_loc = entities.Location(organization=[new_org]).create() + new_org = module_target_sat.api.Organization().create() + new_loc = module_target_sat.api.Location(organization=[new_org]).create() with session: session.role.create( { @@ -156,7 +154,9 @@ def test_positive_delete_cloned_builtin(session): @pytest.mark.tier2 -def test_positive_create_filter_without_override(session, module_org, module_location, test_name): +def test_positive_create_filter_without_override( + session, module_org, module_location, test_name, module_target_sat +): """Create filter in role w/o overriding it :id: a7f76f6e-6c13-4b34-b38c-19501b65786f @@ -178,7 +178,7 @@ def test_positive_create_filter_without_override(session, module_org, module_loc role_name = gen_string('alpha') username = gen_string('alpha') password = gen_string('alpha') - subnet = entities.Subnet() + subnet = module_target_sat.api.Subnet() subnet.create_missing() subnet_name = subnet.name with session: @@ -222,7 +222,7 @@ def test_positive_create_filter_without_override(session, module_org, module_loc 'locations.resources.assigned': [module_location.name], } ) - with Session(test_name, user=username, password=password) as session: + with module_target_sat.ui_session(test_name, user=username, password=password) as session: session.subnet.create( { 'subnet.name': subnet_name, @@ -239,7 +239,9 @@ def test_positive_create_filter_without_override(session, module_org, module_loc @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_create_non_overridable_filter(session, module_org, module_location, test_name): +def test_positive_create_non_overridable_filter( + session, module_org, module_location, test_name, module_target_sat +): """Create non overridden filter in role :id: 5ee281cf-28fa-439d-888d-b1f9aacc6d57 @@ -262,9 +264,9 @@ def test_positive_create_non_overridable_filter(session, module_org, module_loca username = gen_string('alpha') password = gen_string('alpha') new_name = gen_string('alpha') - user_org = entities.Organization().create() - user_loc = entities.Location().create() - arch = entities.Architecture().create() + user_org = module_target_sat.api.Organization().create() + user_loc = module_target_sat.api.Location().create() + arch = module_target_sat.api.Architecture().create() with session: session.role.create( { @@ -293,7 +295,7 @@ def test_positive_create_non_overridable_filter(session, module_org, module_loca 'locations.resources.assigned': [user_loc.name], } ) - with Session(test_name, user=username, password=password) as session: + with module_target_sat.ui_session(test_name, user=username, password=password) as session: session.architecture.update(arch.name, {'name': new_name}) assert session.architecture.search(new_name)[0]['Name'] == new_name with pytest.raises(NavigationTriesExceeded): @@ -302,7 +304,9 @@ def test_positive_create_non_overridable_filter(session, module_org, module_loca @pytest.mark.tier2 @pytest.mark.upgrade -def test_positive_create_overridable_filter(session, module_org, module_location, test_name): +def test_positive_create_overridable_filter( + session, module_org, module_location, test_name, module_target_sat +): """Create overridden filter in role :id: 325e7e3e-60fc-4182-9585-0449d9660e8d @@ -327,9 +331,9 @@ def test_positive_create_overridable_filter(session, module_org, module_location role_name = gen_string('alpha') username = gen_string('alpha') password = gen_string('alpha') - role_org = entities.Organization().create() - role_loc = entities.Location().create() - subnet = entities.Subnet() + role_org = module_target_sat.api.Organization().create() + role_loc = module_target_sat.api.Location().create() + subnet = module_target_sat.api.Subnet() subnet.create_missing() subnet_name = subnet.name new_subnet_name = gen_string('alpha') @@ -378,7 +382,7 @@ def test_positive_create_overridable_filter(session, module_org, module_location 'locations.resources.assigned': [role_loc.name, module_location.name], } ) - with Session(test_name, user=username, password=password) as session: + with module_target_sat.ui_session(test_name, user=username, password=password) as session: session.organization.select(org_name=module_org.name) session.location.select(loc_name=module_location.name) session.subnet.create( @@ -485,7 +489,7 @@ def test_positive_create_with_sc_parameter_permission(session_puppet_enabled_sat @pytest.mark.tier2 -def test_positive_create_filter_admin_user_with_locs(test_name): +def test_positive_create_filter_admin_user_with_locs(test_name, module_target_sat): """Attempt to create a role filter by admin user, who has 6+ locations assigned. :id: 688ecb7d-1d49-494c-97cc-0d5e715f3bb1 @@ -501,10 +505,10 @@ def test_positive_create_filter_admin_user_with_locs(test_name): role_name = gen_string('alpha') resource_type = 'Architecture' permissions = ['view_architectures', 'edit_architectures'] - org = entities.Organization().create() - locations = [entities.Location(organization=[org]).create() for _ in range(6)] + org = module_target_sat.api.Organization().create() + locations = [module_target_sat.api.Location(organization=[org]).create() for _ in range(6)] password = gen_string('alphanumeric') - user = entities.User( + user = module_target_sat.api.User( admin=True, organization=[org], location=locations, @@ -512,7 +516,7 @@ def test_positive_create_filter_admin_user_with_locs(test_name): default_location=locations[0], password=password, ).create() - with Session(test_name, user=user.login, password=password) as session: + with module_target_sat.ui_session(test_name, user=user.login, password=password) as session: session.role.create({'name': role_name}) assert session.role.search(role_name)[0]['Name'] == role_name session.filter.create( @@ -523,7 +527,7 @@ def test_positive_create_filter_admin_user_with_locs(test_name): @pytest.mark.tier2 -def test_positive_create_filter_admin_user_with_orgs(test_name): +def test_positive_create_filter_admin_user_with_orgs(test_name, module_target_sat): """Attempt to create a role filter by admin user, who has 10 organizations assigned. :id: 04208e17-34b5-46b1-84dd-b8a973521d30 @@ -540,9 +544,9 @@ def test_positive_create_filter_admin_user_with_orgs(test_name): resource_type = 'Architecture' permissions = ['view_architectures', 'edit_architectures'] password = gen_string('alphanumeric') - organizations = [entities.Organization().create() for _ in range(10)] - loc = entities.Location(organization=[organizations[0]]).create() - user = entities.User( + organizations = [module_target_sat.api.Organization().create() for _ in range(10)] + loc = module_target_sat.api.Location(organization=[organizations[0]]).create() + user = module_target_sat.api.User( admin=True, organization=organizations, location=[loc], @@ -550,7 +554,7 @@ def test_positive_create_filter_admin_user_with_orgs(test_name): default_location=loc, password=password, ).create() - with Session(test_name, user=user.login, password=password) as session: + with module_target_sat.ui_session(test_name, user=user.login, password=password) as session: session.role.create({'name': role_name}) assert session.role.search(role_name)[0]['Name'] == role_name session.filter.create( diff --git a/tests/foreman/ui/test_settings.py b/tests/foreman/ui/test_settings.py index aa120bed5d3..72bcf9223f0 100644 --- a/tests/foreman/ui/test_settings.py +++ b/tests/foreman/ui/test_settings.py @@ -13,9 +13,7 @@ """ import math -from airgun.session import Session from fauxfactory import gen_url -from nailgun import entities import pytest from robottelo.config import settings @@ -28,14 +26,14 @@ def invalid_settings_values(): return [' ', '-1', 'text', '0'] -def add_content_views_to_composite(composite_cv, org, repo): +def add_content_views_to_composite(composite_cv, org, repo, module_target_sat): """Add necessary number of content views to the composite one :param composite_cv: Composite content view object :param org: Organisation of satellite :param repo: repository need to added in content view """ - content_view = entities.ContentView(organization=org).create() + content_view = module_target_sat.api.ContentView(organization=org).create() content_view.repository = [repo] content_view.update(['repository']) content_view.publish() @@ -47,7 +45,9 @@ def add_content_views_to_composite(composite_cv, org, repo): @pytest.mark.run_in_one_thread @pytest.mark.tier3 @pytest.mark.parametrize('setting_update', ['restrict_composite_view'], indirect=True) -def test_positive_update_restrict_composite_view(session, setting_update, repo_setup): +def test_positive_update_restrict_composite_view( + session, setting_update, repo_setup, module_target_sat +): """Update settings parameter restrict_composite_view to Yes/True and ensure a composite content view may not be published or promoted, unless the component content view versions that it includes exist in the target environment. @@ -61,9 +61,11 @@ def test_positive_update_restrict_composite_view(session, setting_update, repo_s :CaseImportance: Critical """ property_name = setting_update.name - composite_cv = entities.ContentView(composite=True, organization=repo_setup['org']).create() + composite_cv = module_target_sat.api.ContentView( + composite=True, organization=repo_setup['org'] + ).create() content_view = add_content_views_to_composite( - composite_cv, repo_setup['org'], repo_setup['repo'] + composite_cv, repo_setup['org'], repo_setup['repo'], module_target_sat ) composite_cv.publish() with session: @@ -260,9 +262,9 @@ def test_negative_settings_access_to_non_admin(module_target_sat): """ login = gen_string('alpha') password = gen_string('alpha') - entities.User(admin=False, login=login, password=password).create() + module_target_sat.api.User(admin=False, login=login, password=password).create() try: - with Session(user=login, password=password) as session: + with module_target_sat.ui_session(user=login, password=password) as session: result = session.settings.permission_denied() assert ( result == 'Permission denied You are not authorized to perform this action. ' @@ -375,7 +377,7 @@ def test_positive_update_email_delivery_method_sendmail(session, target_sat): "send_welcome_email": "", } mail_config_default_param = { - content: entities.Setting().search(query={'search': f'name={content}'})[0] + content: target_sat.api.Setting().search(query={'search': f'name={content}'})[0] for content in mail_config_default_param } mail_config_new_params = { diff --git a/tests/foreman/ui/test_subscription.py b/tests/foreman/ui/test_subscription.py index 84c7072d0a2..871adb538ca 100644 --- a/tests/foreman/ui/test_subscription.py +++ b/tests/foreman/ui/test_subscription.py @@ -14,9 +14,7 @@ from tempfile import mkstemp import time -from airgun.session import Session from fauxfactory import gen_string -from nailgun import entities import pytest from robottelo.config import settings @@ -44,18 +42,18 @@ def golden_ticket_host_setup(function_entitlement_manifest_org, module_target_sa reposet=REPOSET['rhst7'], releasever=None, ) - rh_repo = entities.Repository(id=rh_repo_id).read() + rh_repo = module_target_sat.api.Repository(id=rh_repo_id).read() rh_repo.sync() - custom_product = entities.Product(organization=org).create() - custom_repo = entities.Repository( + custom_product = module_target_sat.api.Product(organization=org).create() + custom_repo = module_target_sat.api.Repository( name=gen_string('alphanumeric').upper(), product=custom_product ).create() custom_repo.sync() - ak = entities.ActivationKey( + ak = module_target_sat.api.ActivationKey( content_view=org.default_content_view, max_hosts=100, organization=org, - environment=entities.LifecycleEnvironment(id=org.library.id), + environment=module_target_sat.api.LifecycleEnvironment(id=org.library.id), auto_attach=True, ).create() return org, ak @@ -93,7 +91,7 @@ def test_positive_end_to_end(session, target_sat): 'Note: Deleting a subscription manifest is STRONGLY discouraged.', 'This action should only be taken for debugging purposes.', ] - org = entities.Organization().create() + org = target_sat.api.Organization().create() _, temporary_local_manifest_path = mkstemp(prefix='manifest-', suffix='.zip') with clone() as manifest: with open(temporary_local_manifest_path, 'wb') as file_handler: @@ -147,8 +145,8 @@ def test_positive_access_with_non_admin_user_without_manifest(test_name, target_ :CaseImportance: Critical """ - org = entities.Organization().create() - role = entities.Role(organization=[org]).create() + org = target_sat.api.Organization().create() + role = target_sat.api.Role(organization=[org]).create() target_sat.api_factory.create_role_permissions( role, { @@ -162,14 +160,14 @@ def test_positive_access_with_non_admin_user_without_manifest(test_name, target_ }, ) user_password = gen_string('alphanumeric') - user = entities.User( + user = target_sat.api.User( admin=False, role=[role], password=user_password, organization=[org], default_organization=org, ).create() - with Session(test_name, user=user.login, password=user_password) as session: + with target_sat.ui_session(test_name, user=user.login, password=user_password) as session: assert not session.subscription.has_manifest @@ -193,20 +191,20 @@ def test_positive_access_with_non_admin_user_with_manifest( :CaseImportance: Critical """ org = function_entitlement_manifest_org - role = entities.Role(organization=[org]).create() + role = target_sat.api.Role(organization=[org]).create() target_sat.api_factory.create_role_permissions( role, {'Katello::Subscription': ['view_subscriptions'], 'Organization': ['view_organizations']}, ) user_password = gen_string('alphanumeric') - user = entities.User( + user = target_sat.api.User( admin=False, role=[role], password=user_password, organization=[org], default_organization=org, ).create() - with Session(test_name, user=user.login, password=user_password) as session: + with target_sat.ui_session(test_name, user=user.login, password=user_password) as session: assert ( session.subscription.search(f'name = "{DEFAULT_SUBSCRIPTION_NAME}"')[0]['Name'] == DEFAULT_SUBSCRIPTION_NAME @@ -230,23 +228,23 @@ def test_positive_access_manifest_as_another_admin_user( :CaseImportance: High """ - org = entities.Organization().create() + org = target_sat.api.Organization().create() user1_password = gen_string('alphanumeric') - user1 = entities.User( + user1 = target_sat.api.User( admin=True, password=user1_password, organization=[org], default_organization=org ).create() user2_password = gen_string('alphanumeric') - user2 = entities.User( + user2 = target_sat.api.User( admin=True, password=user2_password, organization=[org], default_organization=org ).create() # use the first admin to upload a manifest - with Session(test_name, user=user1.login, password=user1_password) as session: + with target_sat.ui_session(test_name, user=user1.login, password=user1_password) as session: target_sat.upload_manifest(org.id, function_entitlement_manifest.content) assert session.subscription.has_manifest # store subscriptions that have "Red Hat" in the name for later rh_subs = session.subscription.search("Red Hat") # try to view and delete the manifest with another admin - with Session(test_name, user=user2.login, password=user2_password) as session: + with target_sat.ui_session(test_name, user=user2.login, password=user2_password) as session: assert session.subscription.has_manifest assert rh_subs == session.subscription.search("Red Hat") session.subscription.delete_manifest( @@ -290,7 +288,7 @@ def test_positive_view_vdc_subscription_products( :parametrized: yes """ org = function_entitlement_manifest_org - lce = entities.LifecycleEnvironment(organization=org).create() + lce = target_sat.api.LifecycleEnvironment(organization=org).create() repos_collection = target_sat.cli_factory.RepositoryCollection( distro='rhel7', repositories=[target_sat.cli_factory.RHELAnsibleEngineRepository(cdn=True)], @@ -349,7 +347,7 @@ def test_positive_view_vdc_guest_subscription_products( :parametrized: yes """ org = function_entitlement_manifest_org - lce = entities.LifecycleEnvironment(organization=org).create() + lce = target_sat.api.LifecycleEnvironment(organization=org).create() provisioning_server = settings.libvirt.libvirt_hostname rh_product_repository = target_sat.cli_factory.RHELAnsibleEngineRepository(cdn=True) product_name = rh_product_repository.data['product'] @@ -512,13 +510,15 @@ def test_positive_candlepin_events_processed_by_STOMP( :CaseImportance: High """ org = function_entitlement_manifest_org - repo = entities.Repository(product=entities.Product(organization=org).create()).create() + repo = target_sat.api.Repository( + product=target_sat.api.Product(organization=org).create() + ).create() repo.sync() - ak = entities.ActivationKey( + ak = target_sat.api.ActivationKey( content_view=org.default_content_view, max_hosts=100, organization=org, - environment=entities.LifecycleEnvironment(id=org.library.id), + environment=target_sat.api.LifecycleEnvironment(id=org.library.id), ).create() rhel7_contenthost.install_katello_ca(target_sat) rhel7_contenthost.register_contenthost(org.name, ak.name) @@ -534,6 +534,6 @@ def test_positive_candlepin_events_processed_by_STOMP( rhel7_contenthost.hostname, widget_names='details' )['details']['subscription_status'] assert 'Fully entitled' in updated_sub_status - response = entities.Ping().search_json()['services']['candlepin_events'] + response = target_sat.api.Ping().search_json()['services']['candlepin_events'] assert response['status'] == 'ok' assert '0 Failed' in response['message'] diff --git a/tests/foreman/ui/test_user.py b/tests/foreman/ui/test_user.py index 21147e88adc..a69b58250bf 100644 --- a/tests/foreman/ui/test_user.py +++ b/tests/foreman/ui/test_user.py @@ -13,7 +13,6 @@ """ import random -from airgun.session import Session from fauxfactory import gen_email, gen_string import pytest @@ -83,7 +82,7 @@ def test_positive_end_to_end(session, target_sat, test_name, module_org, module_ assert session.user.search(new_name)[0]['Username'] == new_name assert not session.user.search(name) # Login into application using new user - with Session(test_name, new_name, password) as newsession: + with target_sat.ui_session(test_name, new_name, password) as newsession: newsession.organization.select(module_org.name) newsession.location.select(module_location.name) newsession.activationkey.create({'name': ak_name}) @@ -91,7 +90,7 @@ def test_positive_end_to_end(session, target_sat, test_name, module_org, module_ current_user = newsession.activationkey.read(ak_name, 'current_user')['current_user'] assert current_user == f'{firstname} {lastname}' # Delete user - with Session('deletehostsession') as deletehostsession: + with target_sat.ui_session('deletehostsession') as deletehostsession: deletehostsession.user.delete(new_name) assert not deletehostsession.user.search(new_name) @@ -300,7 +299,7 @@ def test_positive_create_product_with_limited_user_permission( password=password, mail='test@test.com', ).create() - with Session(test_name, username, password) as newsession: + with target_sat.ui_session(test_name, username, password) as newsession: newsession.product.create( {'name': product_name, 'label': product_label, 'description': product_description} ) diff --git a/tests/foreman/virtwho/conftest.py b/tests/foreman/virtwho/conftest.py index 133fa0026a0..9b09a66c0fa 100644 --- a/tests/foreman/virtwho/conftest.py +++ b/tests/foreman/virtwho/conftest.py @@ -1,4 +1,3 @@ -from airgun.session import Session from fauxfactory import gen_string import pytest from requests.exceptions import HTTPError @@ -37,7 +36,7 @@ def module_user(request, module_target_sat, default_org, default_location): @pytest.fixture -def session(test_name, module_user): +def session(test_name, module_user, module_target_sat): """Session fixture which automatically initializes (but does not start!) airgun UI session and correctly passes current test name to it. Uses shared module user credentials to log in. @@ -50,7 +49,7 @@ def test_foo(session): # your ui test steps here session.architecture.create({'name': 'bar'}) """ - return Session(test_name, module_user.login, module_user.password) + return module_target_sat.ui_session(test_name, module_user.login, module_user.password) @pytest.fixture(scope='module') @@ -84,7 +83,7 @@ def module_user_sca(request, module_target_sat, module_org, module_location): @pytest.fixture -def session_sca(test_name, module_user_sca): +def session_sca(test_name, module_user_sca, module_target_sat): """Session fixture which automatically initializes (but does not start!) airgun UI session and correctly passes current test name to it. Uses shared module user credentials to log in. @@ -97,4 +96,4 @@ def test_foo(session): # your ui test steps here session.architecture.create({'name': 'bar'}) """ - return Session(test_name, module_user_sca.login, module_user_sca.password) + return module_target_sat.ui_session(test_name, module_user_sca.login, module_user_sca.password) diff --git a/tests/foreman/virtwho/ui/test_esx.py b/tests/foreman/virtwho/ui/test_esx.py index 5baba61e97d..b81536ab211 100644 --- a/tests/foreman/virtwho/ui/test_esx.py +++ b/tests/foreman/virtwho/ui/test_esx.py @@ -13,7 +13,6 @@ """ from datetime import datetime -from airgun.session import Session from fauxfactory import gen_string import pytest @@ -369,7 +368,7 @@ def test_positive_delete_configure(self, default_org, org_session, form_data_ui) @pytest.mark.tier2 def test_positive_virtwho_reporter_role( - self, default_org, org_session, test_name, form_data_ui + self, default_org, org_session, test_name, form_data_ui, target_sat ): """Verify the virt-who reporter role can TRULY work. @@ -418,13 +417,15 @@ def test_positive_virtwho_reporter_role( assert user['roles']['resources']['assigned'] == ['Virt-who Reporter'] restart_virtwho_service() assert get_virtwho_status() == 'running' - with Session(test_name, username, password) as newsession: + with target_sat.ui_session(test_name, username, password) as newsession: assert not newsession.virtwho_configure.check_create_permission()['can_view'] org_session.user.delete(username) assert not org_session.user.search(username) @pytest.mark.tier2 - def test_positive_virtwho_viewer_role(self, default_org, org_session, test_name, form_data_ui): + def test_positive_virtwho_viewer_role( + self, default_org, org_session, test_name, form_data_ui, target_sat + ): """Verify the virt-who viewer role can TRULY work. :id: bf3be2e4-3853-41cc-9b3e-c8677f0b8c5f @@ -469,7 +470,7 @@ def test_positive_virtwho_viewer_role(self, default_org, org_session, test_name, add_configure_option('rhsm_password', password, config_file) restart_virtwho_service() assert get_virtwho_status() == 'logerror' - with Session(test_name, username, password) as newsession: + with target_sat.ui_session(test_name, username, password) as newsession: create_permission = newsession.virtwho_configure.check_create_permission() update_permission = newsession.virtwho_configure.check_update_permission( config_name @@ -484,7 +485,9 @@ def test_positive_virtwho_viewer_role(self, default_org, org_session, test_name, assert not org_session.user.search(username) @pytest.mark.tier2 - def test_positive_virtwho_manager_role(self, default_org, org_session, test_name, form_data_ui): + def test_positive_virtwho_manager_role( + self, default_org, org_session, test_name, form_data_ui, target_sat + ): """Verify the virt-who manager role can TRULY work. :id: a72023fb-7b23-4582-9adc-c5227dc7859c @@ -520,7 +523,7 @@ def test_positive_virtwho_manager_role(self, default_org, org_session, test_name org_session.user.update(username, {'roles.resources.assigned': ['Virt-who Manager']}) user = org_session.user.read(username) assert user['roles']['resources']['assigned'] == ['Virt-who Manager'] - with Session(test_name, username, password) as newsession: + with target_sat.ui_session(test_name, username, password) as newsession: # create_virt_who_config new_virt_who_name = gen_string('alpha') form_data_ui['name'] = new_virt_who_name From f78b348f2c9a7bd48dc99defb1aa4ef9f94c3f1a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 12 Jan 2024 09:51:11 -0500 Subject: [PATCH 438/586] [6.14.z] Cleanup video recording (#13770) Cleanup video recording (#13713) * Only call related functionallity when record_video is set to true * Add settings options to config template Co-authored-by: dosas (cherry picked from commit ad6b99ffb7d87bcd41ff86d256df900568644d86) Co-authored-by: dosas --- conf/ui.yaml.template | 4 +++- pytest_plugins/video_cleanup.py | 17 +++++++++-------- robottelo/hosts.py | 6 +++--- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/conf/ui.yaml.template b/conf/ui.yaml.template index 750f715f7ed..2817b81120a 100644 --- a/conf/ui.yaml.template +++ b/conf/ui.yaml.template @@ -24,13 +24,15 @@ UI: WEBDRIVER: chrome # Binary location for selected wedriver (not needed if using saucelabs) WEBDRIVER_BINARY: /usr/bin/chromedriver - + RECORD_VIDEO: false + GRID_URL: http://infra-grid.example.com:4444 # Web_Kaifuku Settings (checkout https://github.com/RonnyPfannschmidt/webdriver_kaifuku) WEBKAIFUKU: webdriver: chrome/remote webdriver_options: command_executor: http://localhost:4444/wd/hub desired_capabilities: + se:recordVideo: '@jinja {{ this.ui.record_video }}' browserName: chrome chromeOptions: args: diff --git a/pytest_plugins/video_cleanup.py b/pytest_plugins/video_cleanup.py index 35c6fb5fb13..0eaeb570282 100644 --- a/pytest_plugins/video_cleanup.py +++ b/pytest_plugins/video_cleanup.py @@ -17,15 +17,16 @@ def _clean_video(session_id, test): - logger.info(f"cleaning up video files for session: {session_id} and test: {test}") + if settings.ui.record_video: + logger.info(f"cleaning up video files for session: {session_id} and test: {test}") - if settings.ui.grid_url and session_id: - grid = urlparse(url=settings.ui.grid_url) - infra_grid = Host(hostname=grid.hostname) - infra_grid.execute(command=f'rm -rf /var/www/html/videos/{session_id}') - logger.info(f"video cleanup for session {session_id} is complete") - else: - logger.warning("missing grid_url or session_id. unable to clean video files.") + if settings.ui.grid_url and session_id: + grid = urlparse(url=settings.ui.grid_url) + infra_grid = Host(hostname=grid.hostname) + infra_grid.execute(command=f'rm -rf /var/www/html/videos/{session_id}') + logger.info(f"video cleanup for session {session_id} is complete") + else: + logger.warning("missing grid_url or session_id. unable to clean video files.") def pytest_addoption(parser): diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 4844ca0ba24..e998b0d62f7 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1871,10 +1871,10 @@ def get_caller(): except Exception: raise finally: - video_url = settings.ui.grid_url.replace( - ':4444', f'/videos/{ui_session.ui_session_id}/video.mp4' - ) if self.record_property is not None and settings.ui.record_video: + video_url = settings.ui.grid_url.replace( + ':4444', f'/videos/{ui_session.ui_session_id}/video.mp4' + ) self.record_property('video_url', video_url) self.record_property('session_id', ui_session.ui_session_id) From e531821563c0fe96672c70f9c458ef82ddd2f208 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 12 Jan 2024 14:26:37 -0500 Subject: [PATCH 439/586] [6.14.z] http proxy create location option support (#13774) http proxy create location option support (#13768) (cherry picked from commit 2512f9722485aefc06194f0a27a45a2e74f2e016) Co-authored-by: yanpliu --- robottelo/utils/virtwho.py | 3 ++- tests/foreman/virtwho/api/test_esx.py | 8 +++++--- tests/foreman/virtwho/api/test_esx_sca.py | 8 +++++--- tests/foreman/virtwho/cli/test_esx.py | 8 +++++--- tests/foreman/virtwho/cli/test_esx_sca.py | 11 ++++++++--- tests/foreman/virtwho/ui/test_esx.py | 18 +++++++++++++----- 6 files changed, 38 insertions(+), 18 deletions(-) diff --git a/robottelo/utils/virtwho.py b/robottelo/utils/virtwho.py index 2f176d55341..5e0f3a4657a 100644 --- a/robottelo/utils/virtwho.py +++ b/robottelo/utils/virtwho.py @@ -506,7 +506,7 @@ def virtwho_package_locked(): assert "Packages are locked" in result[1] -def create_http_proxy(org, name=None, url=None, http_type='https'): +def create_http_proxy(org, location, name=None, url=None, http_type='https'): """ Creat a new http-proxy with attributes. :param name: Name of the proxy @@ -524,6 +524,7 @@ def create_http_proxy(org, name=None, url=None, http_type='https'): name=http_proxy_name, url=http_proxy_url, organization=[org.id], + location=[location.id], ).create() return http_proxy.url, http_proxy.name, http_proxy.id diff --git a/tests/foreman/virtwho/api/test_esx.py b/tests/foreman/virtwho/api/test_esx.py index cdea45e72b4..bb6046188a0 100644 --- a/tests/foreman/virtwho/api/test_esx.py +++ b/tests/foreman/virtwho/api/test_esx.py @@ -210,7 +210,7 @@ def test_positive_filter_option( @pytest.mark.tier2 def test_positive_proxy_option( - self, default_org, form_data_api, virtwho_config_api, target_sat + self, default_org, default_location, form_data_api, virtwho_config_api, target_sat ): """Verify http_proxy option by "PUT @@ -232,7 +232,7 @@ def test_positive_proxy_option( assert get_configure_option('no_proxy', ETC_VIRTWHO_CONFIG) == '*' # Check HTTTP Proxy and No_PROXY option http_proxy_url, http_proxy_name, http_proxy_id = create_http_proxy( - http_type='http', org=default_org + http_type='http', org=default_org, location=default_location ) no_proxy = 'test.satellite.com' virtwho_config_api.http_proxy_id = http_proxy_id @@ -245,7 +245,9 @@ def test_positive_proxy_option( assert get_configure_option('http_proxy', ETC_VIRTWHO_CONFIG) == http_proxy_url assert get_configure_option('no_proxy', ETC_VIRTWHO_CONFIG) == no_proxy # Check HTTTPs Proxy option - https_proxy_url, https_proxy_name, https_proxy_id = create_http_proxy(org=default_org) + https_proxy_url, https_proxy_name, https_proxy_id = create_http_proxy( + org=default_org, location=default_location + ) virtwho_config_api.http_proxy_id = https_proxy_id virtwho_config_api.update(['http_proxy_id']) deploy_configure_by_command( diff --git a/tests/foreman/virtwho/api/test_esx_sca.py b/tests/foreman/virtwho/api/test_esx_sca.py index 290e67242f5..2d43f59637a 100644 --- a/tests/foreman/virtwho/api/test_esx_sca.py +++ b/tests/foreman/virtwho/api/test_esx_sca.py @@ -227,7 +227,9 @@ def test_positive_filter_option( assert result.exclude_host_parents == regex @pytest.mark.tier2 - def test_positive_proxy_option(self, module_sca_manifest_org, form_data_api, target_sat): + def test_positive_proxy_option( + self, module_sca_manifest_org, default_location, form_data_api, target_sat + ): """Verify http_proxy option by "PUT /foreman_virt_who_configure/api/v2/configs/:id"" @@ -251,7 +253,7 @@ def test_positive_proxy_option(self, module_sca_manifest_org, form_data_api, tar assert get_configure_option('no_proxy', ETC_VIRTWHO_CONFIG) == '*' # Check HTTTP Proxy and No_PROXY option http_proxy_url, http_proxy_name, http_proxy_id = create_http_proxy( - http_type='http', org=module_sca_manifest_org + http_type='http', org=module_sca_manifest_org, location=default_location ) no_proxy = 'test.satellite.com' virtwho_config.http_proxy_id = http_proxy_id @@ -269,7 +271,7 @@ def test_positive_proxy_option(self, module_sca_manifest_org, form_data_api, tar assert result.no_proxy == no_proxy # Check HTTTPs Proxy option https_proxy_url, https_proxy_name, https_proxy_id = create_http_proxy( - org=module_sca_manifest_org + org=module_sca_manifest_org, location=default_location ) virtwho_config.http_proxy_id = https_proxy_id virtwho_config.update(['http_proxy_id']) diff --git a/tests/foreman/virtwho/cli/test_esx.py b/tests/foreman/virtwho/cli/test_esx.py index c661d31cd67..c18ffb3ab30 100644 --- a/tests/foreman/virtwho/cli/test_esx.py +++ b/tests/foreman/virtwho/cli/test_esx.py @@ -214,7 +214,7 @@ def test_positive_filter_option( @pytest.mark.tier2 def test_positive_proxy_option( - self, default_org, form_data_cli, virtwho_config_cli, target_sat + self, default_org, default_location, form_data_cli, virtwho_config_cli, target_sat ): """Verify http_proxy option by hammer virt-who-config update" @@ -227,7 +227,9 @@ def test_positive_proxy_option( :BZ: 1902199 """ # Check the https proxy option, update it via http proxy name - https_proxy_url, https_proxy_name, https_proxy_id = create_http_proxy(org=default_org) + https_proxy_url, https_proxy_name, https_proxy_id = create_http_proxy( + org=default_org, location=default_location + ) no_proxy = 'test.satellite.com' target_sat.cli.VirtWhoConfig.update( {'id': virtwho_config_cli['id'], 'http-proxy': https_proxy_name, 'no-proxy': no_proxy} @@ -244,7 +246,7 @@ def test_positive_proxy_option( # Check the http proxy option, update it via http proxy id http_proxy_url, http_proxy_name, http_proxy_id = create_http_proxy( - http_type='http', org=default_org + http_type='http', org=default_org, location=default_location ) target_sat.cli.VirtWhoConfig.update( {'id': virtwho_config_cli['id'], 'http-proxy-id': http_proxy_id} diff --git a/tests/foreman/virtwho/cli/test_esx_sca.py b/tests/foreman/virtwho/cli/test_esx_sca.py index 7c5a4f5bc20..6b15b6803f9 100644 --- a/tests/foreman/virtwho/cli/test_esx_sca.py +++ b/tests/foreman/virtwho/cli/test_esx_sca.py @@ -250,7 +250,12 @@ def test_positive_filter_option( @pytest.mark.tier2 def test_positive_proxy_option( - self, module_sca_manifest_org, form_data_cli, virtwho_config_cli, target_sat + self, + module_sca_manifest_org, + default_location, + form_data_cli, + virtwho_config_cli, + target_sat, ): """Verify http_proxy option by hammer virt-who-config update" @@ -266,7 +271,7 @@ def test_positive_proxy_option( """ # Check the https proxy option, update it via http proxy name https_proxy_url, https_proxy_name, https_proxy_id = create_http_proxy( - org=module_sca_manifest_org + org=module_sca_manifest_org, location=default_location ) no_proxy = 'test.satellite.com' target_sat.cli.VirtWhoConfig.update( @@ -284,7 +289,7 @@ def test_positive_proxy_option( # Check the http proxy option, update it via http proxy id http_proxy_url, http_proxy_name, http_proxy_id = create_http_proxy( - http_type='http', org=module_sca_manifest_org + http_type='http', org=module_sca_manifest_org, location=default_location ) target_sat.cli.VirtWhoConfig.update( {'id': virtwho_config_cli['id'], 'http-proxy-id': http_proxy_id} diff --git a/tests/foreman/virtwho/ui/test_esx.py b/tests/foreman/virtwho/ui/test_esx.py index b81536ab211..efd46e6deba 100644 --- a/tests/foreman/virtwho/ui/test_esx.py +++ b/tests/foreman/virtwho/ui/test_esx.py @@ -215,7 +215,9 @@ def test_positive_filtering_option( assert regex == get_configure_option('exclude_host_parents', config_file) @pytest.mark.tier2 - def test_positive_proxy_option(self, default_org, virtwho_config_ui, org_session, form_data_ui): + def test_positive_proxy_option( + self, default_org, default_location, virtwho_config_ui, org_session, form_data_ui + ): """Verify 'HTTP Proxy' and 'Ignore Proxy' options. :id: 6659d577-0135-4bf0-81af-14b930011536 @@ -225,9 +227,11 @@ def test_positive_proxy_option(self, default_org, virtwho_config_ui, org_session :CaseImportance: Medium """ - https_proxy, https_proxy_name, https_proxy_id = create_http_proxy(org=default_org) + https_proxy, https_proxy_name, https_proxy_id = create_http_proxy( + org=default_org, location=default_location + ) http_proxy, http_proxy_name, http_proxy_id = create_http_proxy( - http_type='http', org=default_org + http_type='http', org=default_org, location=default_location ) name = form_data_ui['name'] config_id = get_configure_id(name) @@ -547,7 +551,9 @@ def test_positive_virtwho_manager_role( assert not org_session.user.search(username) @pytest.mark.tier2 - def test_positive_overview_label_name(self, default_org, form_data_ui, org_session): + def test_positive_overview_label_name( + self, default_org, default_location, form_data_ui, org_session + ): """Verify the label name on virt-who config Overview Page. :id: 21df8175-bb41-422e-a263-8677bc3a9565 @@ -561,7 +567,9 @@ def test_positive_overview_label_name(self, default_org, form_data_ui, org_sessi name = gen_string('alpha') form_data_ui['name'] = name hypervisor_type = form_data_ui['hypervisor_type'] - http_proxy_url, proxy_name, proxy_id = create_http_proxy(org=default_org) + http_proxy_url, proxy_name, proxy_id = create_http_proxy( + org=default_org, location=default_location + ) form_data_ui['proxy'] = http_proxy_url form_data_ui['no_proxy'] = 'test.satellite.com' regex = '.*redhat.com' From dc39f4d2866580e678f84a2948349b7c65cd20cc Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 15 Jan 2024 06:07:07 -0500 Subject: [PATCH 440/586] [6.14.z] Adding fix for discoveryrule UI (#13779) --- tests/foreman/ui/test_discoveryrule.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/foreman/ui/test_discoveryrule.py b/tests/foreman/ui/test_discoveryrule.py index 0ca5af87129..83ab23cbcc2 100644 --- a/tests/foreman/ui/test_discoveryrule.py +++ b/tests/foreman/ui/test_discoveryrule.py @@ -210,6 +210,8 @@ def test_positive_list_host_based_on_rule_search_query( target_sat.api_factory.create_discovered_host(options={'physicalprocessorcount': cpu_count + 1}) provisioned_host_name = f'{host.domain.read().name}' with session: + session.organization.select(org_name=module_org.name) + session.location.select(loc_name=module_location.name) values = session.discoveryrule.read_all() assert discovery_rule.name in [rule['Name'] for rule in values] values = session.discoveryrule.read_discovered_hosts(discovery_rule.name) @@ -218,8 +220,8 @@ def test_positive_list_host_based_on_rule_search_query( assert values['table'][0]['IP Address'] == ip_address assert values['table'][0]['CPUs'] == str(cpu_count) # auto provision the discovered host - session.discoveredhosts.apply_action('Auto Provision', [discovered_host['name']]) - assert not session.discoveredhosts.search('name = "{}"'.format(discovered_host['name'])) + result = target_sat.api.DiscoveredHost(id=discovered_host['id']).auto_provision() + assert f'provisioned with rule {discovery_rule.name}' in result['message'] values = session.discoveryrule.read_associated_hosts(discovery_rule.name) host_name = values['table'][0]['Name'] assert values['searchbox'] == f'discovery_rule = "{discovery_rule.name}"' From d3f0ba41112db2e64a8ff055fb1c95e277653b7e Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 15 Jan 2024 07:19:26 -0500 Subject: [PATCH 441/586] [6.14.z] Component Evaluation - add RHEL 8 and 9 host versions (#13783) --- tests/foreman/ui/test_host.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index 96957d68fa9..fc0abe523f1 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -1454,8 +1454,9 @@ def test_global_registration_upgrade_subscription_manager( @pytest.mark.tier3 @pytest.mark.usefixtures('enable_capsule_for_registration') +@pytest.mark.rhel_ver_match('[^6].*') def test_global_re_registration_host_with_force_ignore_error_options( - session, module_activation_key, default_os, default_smart_proxy, rhel7_contenthost + session, module_activation_key, default_os, default_smart_proxy, rhel_contenthost ): """If the ignore_error and force checkbox is checked then registered host can get re-registered without any error. @@ -1473,7 +1474,7 @@ def test_global_re_registration_host_with_force_ignore_error_options( :parametrized: yes """ - client = rhel7_contenthost + client = rhel_contenthost with session: cmd = session.host.get_register_command( { @@ -1485,11 +1486,13 @@ def test_global_re_registration_host_with_force_ignore_error_options( 'advanced.ignore_error': True, } ) - client.execute(cmd) + result = client.execute(cmd) + assert result.status == 0 result = client.execute('subscription-manager identity') assert result.status == 0 # rerun the register command - client.execute(cmd) + result = client.execute(cmd) + assert result.status == 0 result = client.execute('subscription-manager identity') assert result.status == 0 From 69bc75f5b1fac37391af77b44c07c34c39756805 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 15 Jan 2024 10:13:50 -0500 Subject: [PATCH 442/586] [6.14.z] adding logic to remove passed tests video url from junit xml (#13787) adding logic to remove passed tests video url from junit xml (#13784) (cherry picked from commit 9b1fc48cee291512b01dfcb657954e7b9b7c5529) Co-authored-by: Omkar Khatavkar --- pytest_plugins/video_cleanup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytest_plugins/video_cleanup.py b/pytest_plugins/video_cleanup.py index 0eaeb570282..08bbf5721d9 100644 --- a/pytest_plugins/video_cleanup.py +++ b/pytest_plugins/video_cleanup.py @@ -68,6 +68,9 @@ def pytest_runtest_makereport(item): if item.nodeid in test_results: result_info = test_results[item.nodeid] if result_info.outcome == 'passed': + report.user_properties = [ + (key, value) for key, value in report.user_properties if key != 'video_url' + ] session_id_tuple = next( (t for t in report.user_properties if t[0] == 'session_id'), None ) From 425dcd6cc5f866ea9653eeba5bc64dd046563f8c Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 15 Jan 2024 10:54:20 -0500 Subject: [PATCH 443/586] [6.14.z] Add test case for ISS syncable CV export/import (#13793) --- tests/foreman/cli/test_satellitesync.py | 86 +++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index dcbc2848756..d072bc86641 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -1231,6 +1231,92 @@ def test_postive_export_cv_with_mixed_content_repos( # Verify export directory is not empty assert target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) != '' + @pytest.mark.tier3 + def test_postive_export_import_cv_with_mixed_content_syncable( + self, + export_import_cleanup_function, + target_sat, + function_org, + function_synced_custom_repo, + function_synced_file_repo, + function_import_org, + ): + """Export and import CV with mixed content in the syncable format. + + :id: cb1aecac-d48a-4154-9ca7-71788674148f + + :setup: + 1. Synced repositories of syncable-supported content types: yum, file + + :steps: + 1. Create CV, add all setup repos and publish. + 2. Export CV version contents in syncable format. + 3. Import the syncable export, check the content. + + :expectedresults: + 1. Export succeeds and content is exported. + 2. Import succeeds, content is imported and matches the export. + """ + # Create CV, add all setup repos and publish + cv = target_sat.cli_factory.make_content_view({'organization-id': function_org.id}) + repos = [ + function_synced_custom_repo, + function_synced_file_repo, + ] + for repo in repos: + target_sat.cli.ContentView.add_repository( + { + 'id': cv['id'], + 'organization-id': function_org.id, + 'repository-id': repo['id'], + } + ) + target_sat.cli.ContentView.publish({'id': cv['id']}) + exporting_cv = target_sat.cli.ContentView.info({'id': cv['id']}) + exporting_cvv = target_sat.cli.ContentView.version_info( + {'id': exporting_cv['versions'][0]['id']} + ) + exported_packages = target_sat.cli.Package.list( + {'content-view-version-id': exporting_cvv['id']} + ) + exported_files = target_sat.cli.File.list({'content-view-version-id': exporting_cvv['id']}) + + # Export CV version contents in syncable format + assert target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) == '' + export = target_sat.cli.ContentExport.completeVersion( + {'id': exporting_cvv['id'], 'organization-id': function_org.id, 'format': 'syncable'} + ) + assert target_sat.validate_pulp_filepath(function_org, PULP_EXPORT_DIR) != '' + + # Import the syncable export + import_path = target_sat.move_pulp_archive(function_org, export['message']) + target_sat.cli.ContentImport.version( + {'organization-id': function_import_org.id, 'path': import_path} + ) + importing_cv = target_sat.cli.ContentView.info( + {'name': exporting_cv['name'], 'organization-id': function_import_org.id} + ) + assert all( + [exporting_cv[key] == importing_cv[key] for key in ['label', 'name']] + ), 'Imported CV name/label does not match the export' + assert ( + len(exporting_cv['versions']) == len(importing_cv['versions']) == 1 + ), 'CV versions count does not match' + + importing_cvv = target_sat.cli.ContentView.version_info( + {'id': importing_cv['versions'][0]['id']} + ) + assert ( + len(exporting_cvv['repositories']) == len(importing_cvv['repositories']) == len(repos) + ), 'Repositories count does not match' + + imported_packages = target_sat.cli.Package.list( + {'content-view-version-id': importing_cvv['id']} + ) + imported_files = target_sat.cli.File.list({'content-view-version-id': importing_cvv['id']}) + assert exported_packages == imported_packages, 'Imported RPMs do not match the export' + assert exported_files == imported_files, 'Imported Files do not match the export' + @pytest.mark.tier3 def test_postive_export_import_cv_with_file_content( self, From d8a12feb7455cb46a26ba6223fa4765d9ffe2266 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 15 Jan 2024 11:02:22 -0500 Subject: [PATCH 444/586] [6.14.z] Add test case for ISS export with history ID (#13796) --- robottelo/cli/content_export.py | 2 +- tests/foreman/cli/test_satellitesync.py | 76 ++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/robottelo/cli/content_export.py b/robottelo/cli/content_export.py index 064f17a720d..8d67ddfc032 100644 --- a/robottelo/cli/content_export.py +++ b/robottelo/cli/content_export.py @@ -31,7 +31,7 @@ class ContentExport(Base): command_requires_org = True @classmethod - def list(cls, output_format='json', options=None): + def list(cls, options=None, output_format='json'): """ List previous exports """ diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index d072bc86641..7bab61df784 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -1784,6 +1784,8 @@ def test_positive_export_incremental_syncable_check_content( :id: 6ff771cd-39ef-4865-8ae8-629f4baf5f98 + :parametrized: yes + :setup: 1. Enabled and synced RH repository. @@ -1905,22 +1907,80 @@ def test_positive_reimport_repo(self): """ - @pytest.mark.stubbed @pytest.mark.tier3 - def test_negative_export_repo_from_future_datetime(self): - """Incremental export fails with future datetime. + @pytest.mark.parametrize( + 'function_synced_rh_repo', + ['rhae2'], + indirect=True, + ) + def test_export_repo_incremental_with_history_id( + self, + target_sat, + export_import_cleanup_function, + config_export_import_settings, + function_sca_manifest_org, + function_synced_rh_repo, + ): + """Test incremental export with history id. :id: 1e8bc352-198f-4d59-b437-1b184141fab4 + :parametrized: yes + + :setup: + 1. Enabled and synced RH repository. + :steps: - 1. Export the repo incrementally from the future date time. + 1. Run repo complete export, ensure it's listed in history. + 2. Run incremental export using history id of the complete export, + ensure it's listed in history. + 3. Run incremental export using non-existent history id. :expectedresults: - 1. Error is raised for attempting to export from future datetime. - - :CaseAutomation: NotAutomated + 1. First (complete) export succeeds and can be listed including history id. + 2. Second (incremental) export succeeds and can be listed including history id. + 3. Third (incremental) export fails when wrong id is provided. """ + # Verify export directory is empty + assert target_sat.validate_pulp_filepath(function_sca_manifest_org, PULP_EXPORT_DIR) == '' + + # Run repo complete export, ensure it's listed in history. + target_sat.cli.ContentExport.completeRepository({'id': function_synced_rh_repo['id']}) + assert '1.0' in target_sat.validate_pulp_filepath( + function_sca_manifest_org, PULP_EXPORT_DIR + ) + + history = target_sat.cli.ContentExport.list( + {'organization-id': function_sca_manifest_org.id} + ) + assert len(history) == 1, 'Expected just one item in the export history' + + # Run incremental export using history id of the complete export, + # ensure it's listed in history. + target_sat.cli.ContentExport.incrementalRepository( + {'id': function_synced_rh_repo['id'], 'from-history-id': history[0]['id']} + ) + assert '2.0' in target_sat.validate_pulp_filepath( + function_sca_manifest_org, PULP_EXPORT_DIR + ) + + history = target_sat.cli.ContentExport.list( + {'organization-id': function_sca_manifest_org.id} + ) + assert len(history) == 2, 'Expected two items in the export history' + assert int(history[1]['id']) == int(history[0]['id']) + 1, 'Inconsistent history spotted' + + # Run incremental export using non-existent history id. + next_id = int(history[1]['id']) + 1 + with pytest.raises(CLIReturnCodeError) as error: + target_sat.cli.ContentExport.incrementalRepository( + {'id': function_synced_rh_repo['id'], 'from-history-id': next_id} + ) + assert ( + f"Couldn't find Katello::ContentViewVersionExportHistory with 'id'={next_id}" + in error.value.message + ), 'Unexpected error message' @pytest.mark.tier3 @pytest.mark.upgrade @@ -2026,6 +2086,8 @@ def test_positive_export_import_mismatch_label( :id: eb2f3e8e-3ee6-4713-80ab-3811a098e079 + :parametrized: yes + :setup: 1. Enabled and synced RH yum repository. From 0763944439b039126ce5943037940fcc0cceacc3 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 16 Jan 2024 08:49:20 -0500 Subject: [PATCH 445/586] [6.14.z] cli_factory job_invocation fix (#13804) --- robottelo/cli/hammer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/robottelo/cli/hammer.py b/robottelo/cli/hammer.py index cf14cd2f0d7..6e7b896a0b8 100644 --- a/robottelo/cli/hammer.py +++ b/robottelo/cli/hammer.py @@ -48,10 +48,12 @@ def parse_csv(output): """Parse CSV output from Hammer CLI and convert it to python dictionary.""" # ignore warning about puppet and ostree deprecation output.replace('Puppet and OSTree will no longer be supported in Katello 3.16\n', '') + is_rex = True if 'Job invocation' in output else False # Validate if the output is eligible for CSV conversions else return as it is - if not is_csv(output): + if not is_csv(output) and not is_rex: return output - reader = csv.reader(output.splitlines()) + output = output.splitlines()[0:2] if is_rex else output.splitlines() + reader = csv.reader(output) # Generate the key names, spaces will be converted to dashes "-" keys = [_normalize(header) for header in next(reader)] # For each entry, create a dict mapping each key with each value From 68319781ca1f455704d33c95844101bb09dee0ae Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 16 Jan 2024 09:08:22 -0500 Subject: [PATCH 446/586] [6.14.z] Add test case for ISS incomplete archive import (#13799) * Add test case for ISS incomplete archive import (#13600) Add test case for incomplete archive import (cherry picked from commit 9b3b5010218aa71bdae1cdce47797ad2f1d1627d) * update the export file format In 6.14 and older the export file is compressed. --------- Co-authored-by: vsedmik <46570670+vsedmik@users.noreply.github.com> --- tests/foreman/cli/test_satellitesync.py | 85 +++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index 7bab61df784..26d68c50f82 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -1163,6 +1163,91 @@ def test_negative_import_invalid_path(self, module_org, module_target_sat): '--metadata-file option' ) in error.value.message + @pytest.mark.tier3 + @pytest.mark.parametrize( + 'function_synced_rh_repo', + ['rhae2'], + indirect=True, + ) + def test_negative_import_incomplete_archive( + self, + target_sat, + config_export_import_settings, + export_import_cleanup_function, + function_synced_rh_repo, + function_sca_manifest_org, + function_import_org_with_manifest, + ): + """Try to import an incomplete export archive (mock interrupted transfer). + + :id: c3b898bb-c6c8-402d-82f9-b15774d9f0fc + + :parametrized: yes + + :setup: + 1. Enabled and synced RH repository. + + :steps: + 1. Create CV with the setup repo, publish it and export. + 2. Corrupt the export archive so that it's incomplete. + 3. Try to import the incomplete archive. + 4. Verify no content is imported and the import CV can be deleted. + + :expectedresults: + 1. The import should fail. + 2. No content should be added, the empty import CV can be deleted. + """ + # Create CV with the setup repo, publish it and export + cv = target_sat.cli_factory.make_content_view( + { + 'organization-id': function_sca_manifest_org.id, + 'repository-ids': [function_synced_rh_repo['id']], + } + ) + target_sat.cli.ContentView.publish({'id': cv['id']}) + cv = target_sat.cli.ContentView.info({'id': cv['id']}) + assert len(cv['versions']) == 1 + cvv = cv['versions'][0] + export = target_sat.cli.ContentExport.completeVersion( + {'id': cvv['id'], 'organization-id': function_sca_manifest_org.id} + ) + assert '1.0' in target_sat.validate_pulp_filepath( + function_sca_manifest_org, PULP_EXPORT_DIR + ) + + # Corrupt the export archive so that it's incomplete + tar_files = target_sat.execute( + f'find {PULP_EXPORT_DIR}{function_sca_manifest_org.name}/{cv["name"]}/ -name *.tar.gz' + ).stdout.splitlines() + assert len(tar_files) == 1, 'Expected just one tar file in the export' + + size = int(target_sat.execute(f'du -b {tar_files[0]}').stdout.split()[0]) + assert size > 0, 'Export tar should not be empty' + + res = target_sat.execute(f'truncate -s {size // 2} {tar_files[0]}') + assert res.status == 0, 'Truncation of the tar file failed' + + # Try to import the incomplete archive + import_path = target_sat.move_pulp_archive(function_sca_manifest_org, export['message']) + with pytest.raises(CLIReturnCodeError) as error: + target_sat.cli.ContentImport.version( + {'organization-id': function_import_org_with_manifest.id, 'path': import_path} + ) + assert '1 subtask(s) failed' in error.value.message + + # Verify no content is imported and the import CV can be deleted + imported_cv = target_sat.cli.ContentView.info( + {'name': cv['name'], 'organization-id': function_import_org_with_manifest.id} + ) + assert len(imported_cv['versions']) == 0, 'There should be no CV version imported' + + target_sat.cli.ContentView.delete({'id': imported_cv['id']}) + with pytest.raises(CLIReturnCodeError) as error: + target_sat.cli.ContentView.info( + {'name': cv['name'], 'organization-id': function_import_org_with_manifest.id} + ) + assert 'content_view not found' in error.value.message, 'The imported CV should be gone' + @pytest.mark.tier3 def test_postive_export_cv_with_mixed_content_repos( self, From 1202115394171866ddb8826c760a766d4244fdfb Mon Sep 17 00:00:00 2001 From: Lukas Hellebrandt Date: Wed, 13 Dec 2023 11:51:36 +0100 Subject: [PATCH 447/586] bz1784254 (cherry picked from commit 95d0b88c9e6c9452178d30b85ea15d9056c1b593) --- tests/foreman/cli/test_remoteexecution.py | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index b775c42768c..7b56ea3faa7 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -19,6 +19,7 @@ from dateutil.relativedelta import FR, relativedelta from fauxfactory import gen_string import pytest +from wait_for import wait_for from robottelo.cli.host import Host from robottelo.config import settings @@ -472,6 +473,51 @@ def test_positive_run_scheduled_job_template_by_ip(self, rex_contenthost, target sleep(30) assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) + @pytest.mark.tier3 + @pytest.mark.rhel_ver_list([8, 9]) + def test_recurring_with_unreachable_host(self, module_target_sat, rhel_contenthost): + """Run a recurring task against a host that is not reachable and verify it gets rescheduled + + :id: 570f6d75-6bbf-40b0-a1df-6c26b588dca8 + + :expectedresults: The job is being rescheduled indefinitely even though it fails + + :BZ: 1784254 + + :customerscenario: true + + :parametrized: yes + """ + cli = module_target_sat.cli + host = module_target_sat.cli_factory.make_fake_host() + # shutdown the host and wait for it to surely be unreachable + rhel_contenthost.execute("shutdown -h +1") # shutdown after one minute + sleep(120) + invocation = module_target_sat.cli_factory.job_invocation( + { + 'job-template': 'Run Command - Script Default', + 'inputs': 'command=echo this wont ever run', + 'search-query': f'name ~ {host.name}', + 'cron-line': '* * * * *', # every minute + } + ) + cli.RecurringLogic.info( + {'id': cli.JobInvocation.info({'id': invocation.id})['recurring-logic-id']} + ) + # wait for the third task to be planned which verifies the BZ + wait_for( + lambda: int( + cli.RecurringLogic.info( + {'id': cli.JobInvocation.info({'id': invocation.id})['recurring-logic-id']} + )['task-count'] + ) + > 2, + timeout=180, + delay=10, + ) + # check that the first task indeed failed which verifies the test was done correctly + assert cli.JobInvocation.info({'id': invocation.id})['failed'] != '0' + class TestAnsibleREX: """Test class for remote execution via Ansible""" From 7a2861c1cb7ee13eb68748677230724c7d04019e Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 17 Jan 2024 11:03:08 -0500 Subject: [PATCH 448/586] [6.14.z] Add test for BZ 2139834 (#13814) --- tests/foreman/cli/test_contentview.py | 42 +++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/foreman/cli/test_contentview.py b/tests/foreman/cli/test_contentview.py index 6aeea931ff7..0b4ee4041ef 100644 --- a/tests/foreman/cli/test_contentview.py +++ b/tests/foreman/cli/test_contentview.py @@ -4023,6 +4023,48 @@ def test_positive_inc_update_no_lce(self, module_org, module_product, module_tar content_view = module_target_sat.cli.ContentView.info({'id': content_view['id']}) assert '1.1' in [cvv_['version'] for cvv_ in content_view['versions']] + @pytest.mark.tier2 + def test_version_info_by_lce(self, module_org, module_target_sat): + """Hammer version info can be passed the lce id/name argument without error + + :id: 6ab0c46c-c62a-488b-a30f-5500d6c7ec96 + + :steps: + 1. Lookup CV version info passing the lce id as an argument + + :expectedresults: LCE is able to be passed to version info command without error + + :BZ: 2139834 + + :customerscenario: true + """ + content_view = module_target_sat.cli_factory.make_content_view( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.publish({'id': content_view['id']}) + lce = module_target_sat.cli_factory.make_lifecycle_environment( + {'organization-id': module_org.id} + ) + module_target_sat.cli.ContentView.version_promote( + {'id': content_view['id'], 'to-lifecycle-environment-id': lce['id']} + ) + content_view = module_target_sat.cli.ContentView.version_info( + { + 'id': content_view['id'], + 'lifecycle-environment-id': lce['id'], + 'organization-id': module_org.id, + } + ) + assert content_view['version'] == '1.0' + content_view = module_target_sat.cli.ContentView.version_info( + { + 'id': content_view['id'], + 'lifecycle-environment': lce['name'], + 'organization-id': module_org.id, + } + ) + assert content_view['version'] == '1.0' + class TestContentViewFileRepo: """Specific tests for Content Views with File Repositories containing From 737459380499518409d3b3406e530f07dc608f39 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 18 Jan 2024 04:14:46 -0500 Subject: [PATCH 449/586] [6.14.z] Random Fixes in pytest plugins (#13824) --- pytest_plugins/factory_collection.py | 5 ++++- pytest_plugins/requirements/update_requirements.py | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pytest_plugins/factory_collection.py b/pytest_plugins/factory_collection.py index 6472c2b909c..3237fe8465f 100644 --- a/pytest_plugins/factory_collection.py +++ b/pytest_plugins/factory_collection.py @@ -12,6 +12,9 @@ def pytest_collection_modifyitems(session, items, config): factory_fixture_names = [m[0] for m in getmembers(sat_cap_factory, isfunction)] for item in items: - has_factoryfixture = set(item.fixturenames).intersection(set(factory_fixture_names)) + itemfixtures = [ + itm for itm in item.fixturenames if itm not in ('satellite_factory', 'capsule_factory') + ] + has_factoryfixture = set(itemfixtures).intersection(set(factory_fixture_names)) if has_factoryfixture: item.add_marker('factory_instance') diff --git a/pytest_plugins/requirements/update_requirements.py b/pytest_plugins/requirements/update_requirements.py index 964b1025289..e2ad8840185 100644 --- a/pytest_plugins/requirements/update_requirements.py +++ b/pytest_plugins/requirements/update_requirements.py @@ -22,10 +22,10 @@ def pytest_report_header(config): e.g: # Following will update the mandatory requirements - # pytest tests/foreman --collect-only --upgrade-required-reqs + # pytest tests/foreman --collect-only --update-required-reqs # Following will update the mandatory and optional requirements - # pytest tests/foreman --collect-only --upgrade-all-reqs + # pytest tests/foreman --collect-only --update-all-reqs """ if updater.req_deviation: print(f"Mandatory Requirements Mismatch: {' '.join(updater.req_deviation)}") @@ -46,5 +46,5 @@ def pytest_report_header(config): if updater.req_deviation or updater.opt_deviation: print( "To update mismatched requirements, run the pytest command with " - "'--upgrade-required-reqs' OR '--upgrade-all-reqs' option." + "'--update-required-reqs' OR '--update-all-reqs' option." ) From 609af4f07aec2cdd331a822b01bd70247628227a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 18 Jan 2024 09:12:59 -0500 Subject: [PATCH 450/586] [6.14.z] New provisioning cleanup method to workaround BZ:2207698 (#13826) New provisioning cleanup method to workaround BZ:2207698 (#13818) Signed-off-by: Gaurav Talreja (cherry picked from commit 177d566ca9f89c01397bdfb42280e2ae5b994552) Co-authored-by: Gaurav Talreja --- .../component/provision_capsule_pxe.py | 2 +- robottelo/host_helpers/satellite_mixins.py | 13 +++++++ tests/foreman/api/test_discoveredhost.py | 7 +--- tests/foreman/api/test_provisioning.py | 13 ++++--- tests/foreman/api/test_provisioning_puppet.py | 3 +- .../cli/test_computeresource_libvirt.py | 2 +- .../foreman/cli/test_computeresource_rhev.py | 38 +++++++++---------- .../cli/test_computeresource_vmware.py | 2 +- tests/foreman/cli/test_discoveredhost.py | 21 ++++------ .../ui/test_computeresource_libvirt.py | 6 +-- tests/foreman/ui/test_discoveredhost.py | 9 +++++ 11 files changed, 65 insertions(+), 51 deletions(-) diff --git a/pytest_fixtures/component/provision_capsule_pxe.py b/pytest_fixtures/component/provision_capsule_pxe.py index d158e596a88..04a2c4fb475 100644 --- a/pytest_fixtures/component/provision_capsule_pxe.py +++ b/pytest_fixtures/component/provision_capsule_pxe.py @@ -101,7 +101,7 @@ def capsule_provisioning_lce_sync_setup(module_capsule_configured, module_lce_li 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) + sync_status = module_capsule_configured.nailgun_capsule.content_sync(timeout='60m') assert sync_status['result'] == 'success', 'Capsule sync task failed.' diff --git a/robottelo/host_helpers/satellite_mixins.py b/robottelo/host_helpers/satellite_mixins.py index 4b56326f8c1..afb4f5cf9f3 100644 --- a/robottelo/host_helpers/satellite_mixins.py +++ b/robottelo/host_helpers/satellite_mixins.py @@ -342,6 +342,19 @@ def configure_libvirt_cr(self, server_fqdn=settings.libvirt.libvirt_hostname): == 0 ) + def provisioning_cleanup(self, hostname, interface='API'): + if interface == 'CLI': + if self.cli.Host.exists(search=('name', hostname)): + self.cli.Host.delete({'name': hostname}) + assert not self.cli.Host.exists(search=('name', hostname)) + else: + host = self.api.Host().search(query={'search': f'name="{hostname}"'}) + if host: + host[0].delete() + assert not self.api.Host().search(query={'search': f'name={hostname}'}) + # Workaround BZ: 2207698 + assert self.cli.Service.restart().status == 0 + class Factories: """Mixin that provides attributes for each factory type""" diff --git a/tests/foreman/api/test_discoveredhost.py b/tests/foreman/api/test_discoveredhost.py index 1bf31452aeb..40738be8aa2 100644 --- a/tests/foreman/api/test_discoveredhost.py +++ b/tests/foreman/api/test_discoveredhost.py @@ -188,7 +188,6 @@ def test_positive_provision_pxe_host( :Setup: Provisioning and discovery should be configured :steps: - 1. Boot up the host to discover 2. Provision the host @@ -215,8 +214,7 @@ def test_positive_provision_pxe_host( host = sat.api.Host().search(query={"search": f'name={host.name}'})[0] assert host assert_discovered_host_provisioned(shell, module_provisioning_rhel_content.ksrepo) - host.delete() - assert not sat.api.Host().search(query={"search": f'name={host.name}'}) + sat.provisioning_cleanup(host.name) provisioning_host.blank = True @pytest.mark.upgrade @@ -265,8 +263,7 @@ def test_positive_provision_pxe_less_host( host = sat.api.Host().search(query={"search": f'name={host.name}'})[0] assert host assert_discovered_host_provisioned(shell, module_provisioning_rhel_content.ksrepo) - host.delete() - assert not sat.api.Host().search(query={"search": f'name={host.name}'}) + sat.provisioning_cleanup(host.name) pxeless_discovery_host.blank = True @pytest.mark.tier3 diff --git a/tests/foreman/api/test_provisioning.py b/tests/foreman/api/test_provisioning.py index 0364eef8f22..cfacbaaffa6 100644 --- a/tests/foreman/api/test_provisioning.py +++ b/tests/foreman/api/test_provisioning.py @@ -113,7 +113,8 @@ def test_rhel_pxe_provisioning( ).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) + request.addfinalizer(lambda: sat.provisioning_cleanup(host.name)) + # Start the VM, do not ensure that we can connect to SSHD provisioning_host.power_control(ensure=False) @@ -246,7 +247,8 @@ def test_rhel_ipxe_provisioning( ).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) + request.addfinalizer(lambda: sat.provisioning_cleanup(host.name)) + # Start the VM, do not ensure that we can connect to SSHD provisioning_host.power_control(ensure=False) @@ -373,7 +375,8 @@ def test_rhel_httpboot_provisioning( ).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) + request.addfinalizer(lambda: sat.provisioning_cleanup(host.name)) + # Start the VM, do not ensure that we can connect to SSHD provisioning_host.power_control(ensure=False) # check for proper HTTP requests @@ -502,7 +505,7 @@ def test_rhel_pxe_provisioning_fips_enabled( ).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) + request.addfinalizer(lambda: sat.provisioning_cleanup(host.name)) # Start the VM, do not ensure that we can connect to SSHD provisioning_host.power_control(ensure=False) @@ -641,7 +644,7 @@ def test_capsule_pxe_provisioning( ).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) + request.addfinalizer(lambda: sat.provisioning_cleanup(host.name)) # 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 diff --git a/tests/foreman/api/test_provisioning_puppet.py b/tests/foreman/api/test_provisioning_puppet.py index 447a462f05f..ec1791f1a17 100644 --- a/tests/foreman/api/test_provisioning_puppet.py +++ b/tests/foreman/api/test_provisioning_puppet.py @@ -155,7 +155,8 @@ def test_host_provisioning_with_external_puppetserver( ).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) + request.addfinalizer(lambda: sat.provisioning_cleanup(host.name)) + # Start the VM, do not ensure that we can connect to SSHD provisioning_host.power_control(ensure=False) diff --git a/tests/foreman/cli/test_computeresource_libvirt.py b/tests/foreman/cli/test_computeresource_libvirt.py index e2109872f99..7f4fdd7526a 100644 --- a/tests/foreman/cli/test_computeresource_libvirt.py +++ b/tests/foreman/cli/test_computeresource_libvirt.py @@ -486,7 +486,7 @@ def test_positive_provision_end_to_end( } ) # teardown - request.addfinalizer(lambda: sat.cli.Host.delete({'id': host['id']})) + request.addfinalizer(lambda: sat.provisioning_cleanup(host['name'], interface='CLI')) # checks hostname = f'{hostname}.{module_libvirt_provisioning_sat.domain.name}' diff --git a/tests/foreman/cli/test_computeresource_rhev.py b/tests/foreman/cli/test_computeresource_rhev.py index 808f1bbfd68..173b9c1c414 100644 --- a/tests/foreman/cli/test_computeresource_rhev.py +++ b/tests/foreman/cli/test_computeresource_rhev.py @@ -415,11 +415,11 @@ def test_positive_provision_rhev_with_host_group( :CaseAutomation: Automated """ - cli = module_provisioning_sat.sat.cli + sat = module_provisioning_sat.sat cr_name = gen_string('alpha') org_name = module_sca_manifest_org.name loc_name = module_location.name - rhv_cr = cli.ComputeResource.create( + rhv_cr = sat.cli.ComputeResource.create( { 'name': cr_name, 'provider': 'Ovirt', @@ -435,7 +435,7 @@ def test_positive_provision_rhev_with_host_group( assert rhv_cr['name'] == cr_name domain_name = module_provisioning_sat.domain.name subnet_name = module_provisioning_sat.subnet.name - hostgroup = cli.HostGroup.create( + hostgroup = sat.cli.HostGroup.create( { 'name': gen_string('alpha'), 'organization': org_name, @@ -448,13 +448,13 @@ def test_positive_provision_rhev_with_host_group( 'kickstart-repository-id': module_provisioning_rhel_content.ksrepo.id, 'lifecycle-environment-id': module_sca_manifest_org.library.id, 'operatingsystem': module_provisioning_rhel_content.os.title, - 'pxe-loader': "PXELinux BIOS", + 'pxe-loader': 'PXELinux BIOS', 'partition-table': default_partitiontable.name, 'compute-resource-id': rhv_cr.get('id'), } ) host_name = gen_string('alpha').lower() - host = cli.Host.create( + host = sat.cli.Host.create( { 'name': f'{host_name}', 'organization': org_name, @@ -467,7 +467,7 @@ def test_positive_provision_rhev_with_host_group( 'mac': None, 'compute-attributes': f"cluster={rhev.cluster_id}," "cores=1," - "memory=4294967296," # 4 GiB + "memory=6442450944," # 6 GiB "start=1", 'interface': ( f"compute_name=nic1, compute_network=" @@ -478,12 +478,12 @@ def test_positive_provision_rhev_with_host_group( } ) # cleanup - request.addfinalizer(lambda: cli.Host.delete({'id': host['id']})) + request.addfinalizer(lambda: sat.provisioning_cleanup(host['name'], interface='CLI')) # checks hostname = f'{host_name}.{domain_name}' assert hostname == host['name'] - host_info = cli.Host.info({'name': hostname}) + host_info = sat.cli.Host.info({'name': hostname}) # Check on RHV, if VM exists assert rhev.rhv_api.does_vm_exist(hostname) # Get the information of created VM @@ -501,12 +501,12 @@ def test_positive_provision_rhev_with_host_group( # the result of the installation. Wait until Satellite reports that the host is installed. exp_st = 'Pending installation' wait_for( - lambda: cli.Host.info({'id': host['id']})['status']['build-status'] != exp_st, + lambda: sat.cli.Host.info({'id': host['id']})['status']['build-status'] != exp_st, # timeout=200, # no need to wait long, the host was already pingable timeout=4000, # wait long because the ping check has not been done, TODO delay=10, ) - host = cli.Host.info({'id': host['id']}) + host = sat.cli.Host.info({'id': host['id']}) assert host['status']['build-status'] == 'Installed' @@ -577,11 +577,11 @@ def test_positive_provision_rhev_image_based_and_disassociate( :CaseAutomation: Automated """ - cli = module_provisioning_sat.sat.cli + sat = module_provisioning_sat.sat org_name = module_org.name loc_name = module_location.name name = gen_string('alpha') - rhv_cr = cli.ComputeResource.create( + rhv_cr = sat.cli.ComputeResource.create( { 'name': name, 'provider': 'Ovirt', @@ -598,7 +598,7 @@ def test_positive_provision_rhev_image_based_and_disassociate( host_name = gen_string('alpha').lower() # use some RHEL (usually latest) os = module_provisioning_rhel_content.os - image = cli.ComputeResource.image_create( + image = sat.cli.ComputeResource.image_create( { 'compute-resource': rhv_cr['name'], 'name': f'img {gen_string(str_type="alpha")}', @@ -616,7 +616,7 @@ def test_positive_provision_rhev_image_based_and_disassociate( host = None # to avoid UnboundLocalError in finally block rhv_vm = None try: - host = cli.Host.create( + host = sat.cli.Host.create( { 'name': f'{host_name}', 'organization': org_name, @@ -631,7 +631,7 @@ def test_positive_provision_rhev_image_based_and_disassociate( 'mac': None, 'compute-attributes': f"cluster={rhev.cluster_id}," "cores=1," - "memory=4294967296," # 4 GiB + "memory=6442450944," # 6 GiB "start=1", 'interface': ( f"compute_name=nic1, compute_network=" @@ -646,7 +646,7 @@ def test_positive_provision_rhev_image_based_and_disassociate( ) hostname = f'{host_name}.{domain_name}' assert hostname == host['name'] - host_info = cli.Host.info({'name': hostname}) + host_info = sat.cli.Host.info({'name': hostname}) # Check on RHV, if VM exists assert rhev.rhv_api.does_vm_exist(hostname) # Get the information of created VM @@ -661,14 +661,14 @@ def test_positive_provision_rhev_image_based_and_disassociate( # that's enough. # Disassociate the host from the CR, check it's disassociated - cli.Host.disassociate({'name': hostname}) - host_info = cli.Host.info({'name': hostname}) + sat.cli.Host.disassociate({'name': hostname}) + host_info = sat.cli.Host.info({'name': hostname}) assert 'compute-resource' not in host_info finally: # Now, let's just remove the host if host is not None: - cli.Host.delete({'id': host['id']}) + sat.provisioning_cleanup(host['name'], interface='CLI') # Delete the VM since the disassociated VM won't get deleted if rhv_vm is not None: rhv_vm.delete() diff --git a/tests/foreman/cli/test_computeresource_vmware.py b/tests/foreman/cli/test_computeresource_vmware.py index e106fa9f49f..1e9e0f216c7 100644 --- a/tests/foreman/cli/test_computeresource_vmware.py +++ b/tests/foreman/cli/test_computeresource_vmware.py @@ -131,7 +131,7 @@ def test_positive_provision_end_to_end( } ) # teardown - request.addfinalizer(lambda: sat.cli.Host.delete({'id': host['id']})) + request.addfinalizer(lambda: sat.provisioning_cleanup(host['name'], interface='CLI')) hostname = f'{hostname}.{module_provisioning_sat.domain.name}' assert hostname == host['name'] diff --git a/tests/foreman/cli/test_discoveredhost.py b/tests/foreman/cli/test_discoveredhost.py index 5fbeca4ef42..2cd1297224e 100644 --- a/tests/foreman/cli/test_discoveredhost.py +++ b/tests/foreman/cli/test_discoveredhost.py @@ -36,7 +36,6 @@ def test_rhel_pxe_discovery_provisioning( :Setup: Satellite with Provisioning and Discovery features configured :steps: - 1. Boot up the host to discover 2. Provision the host @@ -68,15 +67,14 @@ def test_rhel_pxe_discovery_provisioning( 'location-id': discovered_host.location.id, } ) - # teardown - @request.addfinalizer - def _finalize(): - host.delete() - assert not sat.api.Host().search(query={"search": f'name={host.name}'}) assert 'Host created' in result[0]['message'] host = sat.api.Host().search(query={"search": f'id={discovered_host.id}'})[0] assert host + + # teardown + request.addfinalizer(lambda: sat.provisioning_cleanup(host.name)) + wait_for( lambda: host.read().build_status_label != 'Pending installation', timeout=1500, @@ -131,16 +129,13 @@ def test_rhel_pxeless_discovery_provisioning( 'location-id': discovered_host.location.id, } ) - - # teardown - @request.addfinalizer - def _finalize(): - host.delete() - assert not sat.api.Host().search(query={"search": f'name={host.name}'}) - assert 'Host created' in result[0]['message'] host = sat.api.Host().search(query={"search": f'id={discovered_host.id}'})[0] assert host + + # teardown + request.addfinalizer(lambda: sat.provisioning_cleanup(host.name)) + wait_for( lambda: host.read().build_status_label != 'Pending installation', timeout=1500, diff --git a/tests/foreman/ui/test_computeresource_libvirt.py b/tests/foreman/ui/test_computeresource_libvirt.py index 6938edd2fbd..518e26817b8 100644 --- a/tests/foreman/ui/test_computeresource_libvirt.py +++ b/tests/foreman/ui/test_computeresource_libvirt.py @@ -174,11 +174,7 @@ def test_positive_provision_end_to_end( assert session.host.search(name)[0]['Name'] == name # teardown - @request.addfinalizer - def _finalize(): - host = sat.api.Host().search(query={'search': f'name="{name}"'}) - if host: - host[0].delete() + request.addfinalizer(lambda: sat.provisioning_cleanup(name)) # Check on Libvirt, if VM exists result = sat.execute( diff --git a/tests/foreman/ui/test_discoveredhost.py b/tests/foreman/ui/test_discoveredhost.py index 292ad94f7a4..92ea28d185a 100644 --- a/tests/foreman/ui/test_discoveredhost.py +++ b/tests/foreman/ui/test_discoveredhost.py @@ -51,6 +51,7 @@ def _is_host_reachable(host, retries=12, iteration_sleep=5, expect_reachable=Tru @pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) @pytest.mark.rhel_ver_match('9') def test_positive_provision_pxe_host( + request, session, module_location, module_org, @@ -93,6 +94,10 @@ def test_positive_provision_pxe_host( discovered_host_name = discovered_host.name domain_name = provisioning_hostgroup.domain.read().name host_name = f'{discovered_host_name}.{domain_name}' + + # Teardown + request.addfinalizer(lambda: sat.provisioning_cleanup(host_name)) + with session: session.discoveredhosts.provision( discovered_host_name, @@ -150,6 +155,7 @@ def test_positive_update_name( @pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) @pytest.mark.rhel_ver_match('9') def test_positive_auto_provision_host_with_rule( + request, session, module_org, module_location, @@ -192,6 +198,9 @@ def test_positive_auto_provision_host_with_rule( domain_name = provisioning_hostgroup.domain.read().name host_name = f'{discovered_host_name}.{domain_name}' + # Teardown + request.addfinalizer(lambda: sat.provisioning_cleanup(host_name)) + discovery_rule = sat.api.DiscoveryRule( max_count=10, hostgroup=provisioning_hostgroup, From 89a5ad03a50aa97550d97565922aef13a35cdeba Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 19 Jan 2024 02:19:50 -0500 Subject: [PATCH 451/586] [6.14.z] Fix upload_manifest to handle case where manifest.content is not set (#13835) --- robottelo/host_helpers/satellite_mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robottelo/host_helpers/satellite_mixins.py b/robottelo/host_helpers/satellite_mixins.py index afb4f5cf9f3..27c12c7b3be 100644 --- a/robottelo/host_helpers/satellite_mixins.py +++ b/robottelo/host_helpers/satellite_mixins.py @@ -146,7 +146,7 @@ def upload_manifest(self, org_id, manifest=None, interface='API', timeout=None): """ if not isinstance(manifest, bytes | io.BytesIO): - if manifest.content is None: + if not hasattr(manifest, 'content') or manifest.content is None: manifest = clone() if timeout is None: # Set the timeout to 1500 seconds to align with the API timeout. From 6b03d5efa0f6c6ff8b6fd2a6e07ac83e56f26809 Mon Sep 17 00:00:00 2001 From: Gaurav Talreja Date: Sat, 20 Jan 2024 02:51:08 +0530 Subject: [PATCH 452/586] [6.14.z] Update vlan paramater template test for BZ:2075358 (#13810) Signed-off-by: Gaurav Talreja --- tests/foreman/api/test_provisioningtemplate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/foreman/api/test_provisioningtemplate.py b/tests/foreman/api/test_provisioningtemplate.py index 8735ae4fbe7..2ef9810d7b3 100644 --- a/tests/foreman/api/test_provisioningtemplate.py +++ b/tests/foreman/api/test_provisioningtemplate.py @@ -353,7 +353,7 @@ def test_positive_template_check_vlan_parameter( :expectedresults: The rendered templates should contain the "vlan" parameter expected for respective rhel hosts. - :BZ: 1607706 + :BZ: 1607706, 2075358 :customerscenario: true @@ -399,7 +399,7 @@ def test_positive_template_check_vlan_parameter( provision_template = host.read_template(data={'template_kind': 'provision'})['template'] assert f'interfacename=vlan{tag}' in provision_template ipxe_template = host.read_template(data={'template_kind': 'iPXE'})['template'] - assert f'vlan=vlan{tag}:{identifier}' in ipxe_template + assert f'vlan={identifier}.{tag}:{identifier}' in ipxe_template @pytest.mark.parametrize('module_sync_kickstart_content', [7, 8, 9], indirect=True) @pytest.mark.parametrize('pxe_loader', ['uefi'], indirect=True) From 7aed26329a84e67bf977d650649a576029ad88b0 Mon Sep 17 00:00:00 2001 From: vsedmik <46570670+vsedmik@users.noreply.github.com> Date: Mon, 22 Jan 2024 11:14:23 +0100 Subject: [PATCH 453/586] [6.14.z] Add coverage for BZ#2173756 (#13844) * Add coverage for BZ#2173756 * Remove old stubs They are addressed by the previous commit. * Address comments --- tests/foreman/cli/test_satellitesync.py | 197 ++++++++++++++++++------ 1 file changed, 154 insertions(+), 43 deletions(-) diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index 26d68c50f82..af81b632a5a 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -25,6 +25,7 @@ CONTAINER_UPSTREAM_NAME, DEFAULT_ARCHITECTURE, DEFAULT_CV, + ENVIRONMENT, EXPORT_LIBRARY_NAME, PULP_EXPORT_DIR, PULP_IMPORT_DIR, @@ -1943,34 +1944,6 @@ def test_positive_export_incremental_syncable_check_content( class TestInterSatelliteSync: """Implements InterSatellite Sync tests in CLI""" - @pytest.mark.stubbed - @pytest.mark.tier3 - @pytest.mark.upgrade - def test_positive_export_import_cv_incremental(self): - """Export and Import CV version contents incrementally. - - :id: 3c4dfafb-fabf-406e-bca8-7af1ab551135 - - :steps: - 1. In upstream, Export CV version contents to a directory specified in settings. - 2. In downstream, Import these copied contents from some other org/satellite. - 3. In upstream, don't add any new packages to the CV. - 4. Export the CV incrementally. - 5. In downstream, Import the CV incrementally. - 6. In upstream, add new packages to the CV. - 7. Export the CV incrementally. - 8. In downstream, Import the CV incrementally. - - :expectedresults: - 1. On incremental export, only the new packages are exported. - 2. New directory of incremental export with new packages is created. - 3. On first incremental import, no new packages are imported. - 4. On second incremental import, only the new packages are imported. - - :CaseAutomation: NotAutomated - - """ - @pytest.mark.stubbed @pytest.mark.tier3 @pytest.mark.upgrade @@ -2372,30 +2345,168 @@ def test_positive_custom_cdn_with_credential( repo['content-counts'] == function_synced_rh_repo['content-counts'] ), 'Content counts do not match' - @pytest.mark.stubbed + @pytest.mark.e2e @pytest.mark.tier3 - @pytest.mark.upgrade - def test_positive_install_package_from_imported_repos(self): - """Install packages in client from imported repo of Downstream satellite. + @pytest.mark.rhel_ver_list([8]) + @pytest.mark.parametrize( + 'function_synced_rh_repo', + ['rhsclient8'], + indirect=True, + ) + def test_positive_export_import_consume_incremental_yum_repo( + self, + target_sat, + export_import_cleanup_function, + config_export_import_settings, + function_sca_manifest_org, + function_import_org_with_manifest, + function_synced_rh_repo, + rhel_contenthost, + ): + """Export and import RH yum repo incrementally and consume it on a content host. - :id: a81ffb55-398d-4ad0-bcae-5ed48f504ded + :id: f5515168-c3c9-4351-9f83-ba6265689db3 - :steps: + :setup: + 1. Enabled and synced RH yum repository (RH Satellite Client for this case). + 2. An unregistered RHEL8 host. - 1. Export whole Red Hat YUM repo to a path accessible over HTTP. - 2. Import the Red Hat repository by defining the CDN URL from the - exported HTTP URL. - 3. In downstream satellite create CV, AK with this imported repo. - 4. Register/Subscribe a client with a downstream satellite. - 5. Attempt to install a package on a client from imported repo of - downstream. + :steps: + 1. Create a CV with the RH yum repository. + 2. Add exclude RPM filter to filter out one package, publish version 1 and export it. + 3. On the importing side import version 1, check the package count. + 4. Create an AK with the imported CV, register the content host and check + the package count available to install. Filtered package should be missing. + 5. Update the filter so no package is left behind, publish version 2 and export it. + 6. Import version 2, check the package count. + 7. Check the package count available to install on the content host. + 8. Install the package. :expectedresults: - 1. The package is installed on client from imported repo of downstream satellite. + 1. More packages available for install after version 2 imported. + 2. Packages can be installed successfully. - :CaseAutomation: NotAutomated + :BZ: 2173756 + :customerscenario: true """ + # Create a CV with the RH yum repository. + exp_cv = target_sat.cli_factory.make_content_view( + { + 'organization-id': function_sca_manifest_org.id, + 'repository-ids': [function_synced_rh_repo['id']], + } + ) + + # Add exclude RPM filter to filter out one package, publish version 1 and export it. + filtered_pkg = 'katello-host-tools' + cvf = target_sat.cli_factory.make_content_view_filter( + {'content-view-id': exp_cv['id'], 'type': 'rpm'} + ) + cvf_rule = target_sat.cli_factory.content_view_filter_rule( + {'content-view-filter-id': cvf['filter-id'], 'name': filtered_pkg} + ) + target_sat.cli.ContentView.publish({'id': exp_cv['id']}) + exp_cv = target_sat.cli.ContentView.info({'id': exp_cv['id']}) + assert len(exp_cv['versions']) == 1 + cvv_1 = exp_cv['versions'][0] + pkg_cnt_1 = target_sat.api.ContentViewVersion(id=cvv_1['id']).read().package_count + export_1 = target_sat.cli.ContentExport.completeVersion({'id': cvv_1['id']}) + assert '1.0' in target_sat.validate_pulp_filepath( + function_sca_manifest_org, PULP_EXPORT_DIR + ) + + # On the importing side import version 1, check the package count. + import_path1 = target_sat.move_pulp_archive(function_sca_manifest_org, export_1['message']) + target_sat.cli.ContentImport.version( + {'organization-id': function_import_org_with_manifest.id, 'path': import_path1} + ) + imp_cv = target_sat.cli.ContentView.info( + {'name': exp_cv['name'], 'organization-id': function_import_org_with_manifest.id} + ) + assert len(imp_cv['versions']) == 1 + imp_cvv = imp_cv['versions'][0] + assert target_sat.api.ContentViewVersion(id=imp_cvv['id']).read().package_count == pkg_cnt_1 + + # Create an AK with the imported CV, register the content host and check + # the package count available to install. Filtered package should be missing. + ak = target_sat.cli_factory.make_activation_key( + { + 'content-view': exp_cv['name'], + 'lifecycle-environment': ENVIRONMENT, + 'organization-id': function_import_org_with_manifest.id, + } + ) + target_sat.cli.ActivationKey.content_override( + { + 'id': ak.id, + 'content-label': function_synced_rh_repo['content-label'], + 'value': 'true', + } + ) + res = rhel_contenthost.register( + function_import_org_with_manifest, None, ak.name, target_sat + ) + assert res.status == 0, ( + f'Failed to register host: {rhel_contenthost.hostname}\n' + f'StdOut: {res.stdout}\nStdErr: {res.stderr}' + ) + assert rhel_contenthost.subscribed + res = rhel_contenthost.execute('dnf clean all && dnf repolist -v') + assert res.status == 0 + assert ( + f'Repo-available-pkgs: {pkg_cnt_1}' in res.stdout + ), 'Package count available on the host did not meet the expectation' + + res = rhel_contenthost.execute(f'dnf -y install {filtered_pkg}') + assert res.status, 'Installation of filtered package succeeded unexpectedly' + assert f'No match for argument: {filtered_pkg}' in res.stdout + + # Update the fiter so that no package is left behind, publish version 2 and export it. + target_sat.cli.ContentView.filter.rule.update( + { + 'content-view-filter-id': cvf['filter-id'], + 'id': cvf_rule['rule-id'], + 'name': gen_string('alpha'), + } + ) + target_sat.cli.ContentView.publish({'id': exp_cv['id']}) + exp_cv = target_sat.cli.ContentView.info({'id': exp_cv['id']}) + assert len(exp_cv['versions']) == 2 + cvv_2 = max(exp_cv['versions'], key=lambda x: int(x['id'])) + pkg_cnt_2 = target_sat.api.ContentViewVersion(id=cvv_2['id']).read().package_count + assert pkg_cnt_2 > pkg_cnt_1 + export_2 = target_sat.cli.ContentExport.incrementalVersion({'id': cvv_2['id']}) + assert '2.0' in target_sat.validate_pulp_filepath( + function_sca_manifest_org, PULP_EXPORT_DIR + ) + + # Import version 2, check the package count. + import_path2 = target_sat.move_pulp_archive(function_sca_manifest_org, export_2['message']) + target_sat.cli.ContentImport.version( + {'organization-id': function_import_org_with_manifest.id, 'path': import_path2} + ) + imp_cv = target_sat.cli.ContentView.info( + {'name': exp_cv['name'], 'organization-id': function_import_org_with_manifest.id} + ) + assert len(imp_cv['versions']) == 2 + imp_cvv = max(imp_cv['versions'], key=lambda x: int(x['id'])) + assert ( + target_sat.api.ContentViewVersion(id=imp_cvv['id']).read().package_count + == pkg_cnt_2 + == int(function_synced_rh_repo['content-counts']['packages']) + ), 'Unexpected package count after second import' + + # Check the package count available to install on the content host. + res = rhel_contenthost.execute('dnf clean all && dnf repolist -v') + assert res.status == 0 + assert ( + f'Repo-available-pkgs: {pkg_cnt_2}' in res.stdout + ), 'Package count available on the host did not meet the expectation' + + # Install the package. + res = rhel_contenthost.execute(f'dnf -y install {filtered_pkg}') + assert res.status == 0, f'Installation from the import failed:\n{res.stdout}' @pytest.fixture(scope='module') From f73aece257e5203e1be5588803335a8a0d379fd8 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 22 Jan 2024 22:48:32 -0500 Subject: [PATCH 454/586] [6.14.z] Bump cryptography from 41.0.7 to 42.0.0 (#13863) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f6007645014..f0981ed00d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ betelgeuse==1.11.0 broker[docker]==0.4.1 -cryptography==41.0.7 +cryptography==42.0.0 deepdiff==6.7.1 dynaconf[vault]==3.2.4 fauxfactory==3.1.0 From 7c60d54ab3c254772c7c4d4acdc1bd129eb7632d Mon Sep 17 00:00:00 2001 From: Peter Ondrejka Date: Fri, 19 Jan 2024 16:33:35 +0100 Subject: [PATCH 455/586] oscap helper function fix (cherry picked from commit edf63fff0bc77af1d83cddc4c58ce5a3b6c972fe) --- tests/foreman/longrun/test_oscap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/longrun/test_oscap.py b/tests/foreman/longrun/test_oscap.py index fa3654f8a48..ece650e6de2 100644 --- a/tests/foreman/longrun/test_oscap.py +++ b/tests/foreman/longrun/test_oscap.py @@ -36,7 +36,7 @@ } -def fetch_scap_and_profile_id(scap_name, scap_profile, sat): +def fetch_scap_and_profile_id(sat, scap_name, scap_profile): """Extracts the scap ID and scap profile id :param scap_name: Scap title From e08a85daa7a688a864932fb90ad95a17d13abff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Gajdu=C5=A1ek?= Date: Tue, 23 Jan 2024 10:42:46 +0100 Subject: [PATCH 456/586] [6.14.z] Sync project dependencies (#13854) * Bump actions/setup-python from 4 to 5 (#13302) (cherry picked from commit 6ad7e15d942a3e9e3397412e6e41961312ec7fe1) * Bump lewagon/wait-on-check-action from 1.3.1 to 1.3.3 (#13548) (cherry picked from commit d0c8193677bf71faa4c464811869136202fcbc33) * Bump pytest from 7.4.3 to 7.4.4 (#13578) (cherry picked from commit 55b6ce3add030e946c7af31c1476de605beafd41) * Bump pascalgn/automerge-action from 0.16.0 to 0.16.2 (#13724) (cherry picked from commit a547eecb2261eaf0ac0f23ba1c2a618252035356) --------- Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto_cherry_pick_merge.yaml | 4 ++-- .github/workflows/dependency_merge.yml | 4 ++-- .github/workflows/pull_request.yml | 2 +- .github/workflows/weekly.yml | 2 +- requirements.txt | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/auto_cherry_pick_merge.yaml b/.github/workflows/auto_cherry_pick_merge.yaml index 924bc9fccfd..e0cb8e48173 100644 --- a/.github/workflows/auto_cherry_pick_merge.yaml +++ b/.github/workflows/auto_cherry_pick_merge.yaml @@ -40,7 +40,7 @@ jobs: - name: Wait for other status checks to Pass id: waitforstatuschecks - uses: lewagon/wait-on-check-action@v1.3.1 + uses: lewagon/wait-on-check-action@v1.3.3 with: ref: ${{ github.head_ref }} repo-token: ${{ secrets.CHERRYPICK_PAT }} @@ -67,7 +67,7 @@ jobs: - id: automerge name: Auto merge of cherry-picked PRs. - uses: "pascalgn/automerge-action@v0.15.6" + uses: "pascalgn/automerge-action@v0.16.2" env: GITHUB_TOKEN: "${{ secrets.CHERRYPICK_PAT }}" MERGE_LABELS: "AutoMerge_Cherry_Picked, Auto_Cherry_Picked" diff --git a/.github/workflows/dependency_merge.yml b/.github/workflows/dependency_merge.yml index 4cf94b8255e..22ec56e6d17 100644 --- a/.github/workflows/dependency_merge.yml +++ b/.github/workflows/dependency_merge.yml @@ -37,7 +37,7 @@ jobs: - name: Wait for other status checks to Pass id: waitforstatuschecks - uses: lewagon/wait-on-check-action@v1.3.1 + uses: lewagon/wait-on-check-action@v1.3.3 with: ref: ${{ github.head_ref }} repo-token: ${{ secrets.GITHUB_TOKEN }} @@ -47,7 +47,7 @@ jobs: - id: automerge name: Auto merge of dependabot PRs. - uses: "pascalgn/automerge-action@v0.15.6" + uses: "pascalgn/automerge-action@v0.16.2" env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" MERGE_LABELS: "dependencies" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index fc61b228cbc..47963adca8a 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v4 - name: Set Up Python-${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index dc60df544b3..e767f188ab5 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@v4 - name: Set Up Python-${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/requirements.txt b/requirements.txt index f0981ed00d0..9148455ebfa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ navmazing==1.2.2 productmd==1.38 pyotp==2.9.0 python-box==7.1.1 -pytest==7.4.3 +pytest==7.4.4 pytest-services==2.2.1 pytest-mock==3.12.0 pytest-reportportal==5.3.1 From 3a4aaefd43d0dfd523630160b5ac1e5a6db1217b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 23 Jan 2024 05:47:08 -0500 Subject: [PATCH 457/586] [6.14.z] cli oscap fix name already taken (#13865) --- tests/foreman/cli/test_oscap.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/foreman/cli/test_oscap.py b/tests/foreman/cli/test_oscap.py index ecbc50f06de..c198e5fa636 100644 --- a/tests/foreman/cli/test_oscap.py +++ b/tests/foreman/cli/test_oscap.py @@ -286,8 +286,9 @@ def test_positive_create_scap_content_with_valid_originalfile_name( :CaseImportance: Medium """ + title = gen_string('alpha') scap_content = module_target_sat.cli_factory.scapcontent( - {'original-filename': name, 'scap-file': settings.oscap.content_path} + {'original-filename': name, 'scap-file': settings.oscap.content_path, 'title': title} ) assert scap_content['original-filename'] == name @@ -406,8 +407,9 @@ def test_positive_delete_scap_content_with_id(self, module_target_sat): :CaseImportance: Medium """ + title = gen_string('alpha') scap_content = module_target_sat.cli_factory.scapcontent( - {'scap-file': settings.oscap.content_path} + {'scap-file': settings.oscap.content_path, 'title': title} ) module_target_sat.cli.Scapcontent.delete({'id': scap_content['id']}) with pytest.raises(CLIReturnCodeError): @@ -436,8 +438,9 @@ def test_positive_delete_scap_content_with_title(self, module_target_sat): :CaseImportance: Medium """ + title = gen_string('alpha') scap_content = module_target_sat.cli_factory.scapcontent( - {'scap-file': settings.oscap.content_path} + {'scap-file': settings.oscap.content_path, 'title': title} ) module_target_sat.cli.Scapcontent.delete({'title': scap_content['title']}) with pytest.raises(CLIReturnCodeError): From 1921ab0c5173fc036275519024463e787ad99395 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 23 Jan 2024 05:54:27 -0500 Subject: [PATCH 458/586] [6.14.z] fixes in api role helper functions (#13867) --- tests/foreman/api/test_role.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/foreman/api/test_role.py b/tests/foreman/api/test_role.py index fe6807734e0..e44b0f8d844 100644 --- a/tests/foreman/api/test_role.py +++ b/tests/foreman/api/test_role.py @@ -576,7 +576,7 @@ def test_negative_access_entities_from_org_admin( target_sat, role_taxos=role_taxonomies, user_taxos=filter_taxonomies ) domain = self.create_domain( - orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] + target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) sc = self.user_config(user, target_sat) # Getting the domain from user @@ -608,7 +608,7 @@ def test_negative_access_entities_from_user( target_sat, role_taxos=role_taxonomies, user_taxos=filter_taxonomies ) domain = self.create_domain( - orgs=[filter_taxonomies['org'].id], locs=[filter_taxonomies['loc'].id] + target_sat, orgs=[filter_taxonomies['org'].id], locs=[filter_taxonomies['loc'].id] ) sc = self.user_config(user, target_sat) # Getting the domain from user @@ -1074,7 +1074,9 @@ def test_negative_assign_org_admin_to_user_group( name=ug_name, role=[org_admin.id], user=[user_one.id, user_two.id] ).create() assert user_group.name == ug_name - dom = self.create_domain(orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id]) + dom = self.create_domain( + target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] + ) for user in [user_one, user_two]: sc = self.user_config(user, target_sat) with pytest.raises(HTTPError): @@ -1447,7 +1449,7 @@ def test_positive_access_users_inside_org_admin_taxonomies(self, role_taxonomies user = self.create_org_admin_user( target_sat, role_taxos=role_taxonomies, user_taxos=role_taxonomies ) - test_user = self.create_simple_user(filter_taxos=role_taxonomies) + test_user = self.create_simple_user(target_sat, filter_taxos=role_taxonomies) sc = self.user_config(user, target_sat) try: target_sat.api.User(server_config=sc, id=test_user.id).read() @@ -1521,7 +1523,7 @@ def test_negative_access_users_outside_org_admin_taxonomies( user = self.create_org_admin_user( target_sat, role_taxos=role_taxonomies, user_taxos=role_taxonomies ) - test_user = self.create_simple_user(filter_taxos=filter_taxonomies) + test_user = self.create_simple_user(target_sat, filter_taxos=filter_taxonomies) sc = self.user_config(user, target_sat) with pytest.raises(HTTPError): target_sat.api.User(server_config=sc, id=test_user.id).read() @@ -1648,7 +1650,7 @@ def test_negative_access_entities_from_ldap_org_admin( ) # Creating Domain resource in same taxonomies as Org Admin role to access later domain = self.create_domain( - orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] + target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) sc = ServerConfig( auth=(create_ldap['ldap_user_name'], create_ldap['ldap_user_passwd']), @@ -1692,7 +1694,7 @@ def test_negative_access_entities_from_ldap_user( target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) # Creating Domain resource in different taxonomies to access later - domain = self.create_domain(orgs=[module_org.id], locs=[module_location.id]) + domain = self.create_domain(target_sat, orgs=[module_org.id], locs=[module_location.id]) sc = ServerConfig( auth=(create_ldap['ldap_user_name'], create_ldap['ldap_user_passwd']), url=create_ldap['sat_url'], @@ -1741,6 +1743,7 @@ def test_positive_assign_org_admin_to_ldap_user_group( ) # Creating Domain resource in same taxonomies as Org Admin role to access later domain = self.create_domain( + target_sat, orgs=[create_ldap['authsource'].organization[0].id], locs=[create_ldap['authsource'].location[0].id], ) @@ -1800,7 +1803,7 @@ def test_negative_assign_org_admin_to_ldap_user_group( ) # Creating Domain resource in same taxonomies as Org Admin role to access later domain = self.create_domain( - orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] + target_sat, orgs=[role_taxonomies['org'].id], locs=[role_taxonomies['loc'].id] ) users = [ target_sat.api.User( From 4b76776faffec918e308ec5319391e02d4df3c7b Mon Sep 17 00:00:00 2001 From: Lukas Pramuk Date: Thu, 18 Jan 2024 14:25:28 +0100 Subject: [PATCH 459/586] Fix waiting for ugrade capsule sync (cherry picked from commit 72ec0864d2a926673244c10712cc8af92b16ac3b) --- robottelo/host_helpers/capsule_mixins.py | 4 ++-- tests/upgrades/test_capsule.py | 28 +++++++++++++++--------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/robottelo/host_helpers/capsule_mixins.py b/robottelo/host_helpers/capsule_mixins.py index 0589b5f15a3..16fd7fa5712 100644 --- a/robottelo/host_helpers/capsule_mixins.py +++ b/robottelo/host_helpers/capsule_mixins.py @@ -66,14 +66,14 @@ def wait_for_sync(self, timeout=600, start_time=None): # Assert that a task to sync lifecycle environment to the capsule # is started (or finished already) if start_time is None: - start_time = datetime.utcnow() + start_time = datetime.utcnow().replace(microsecond=0) logger.info(f"Waiting for capsule {self.hostname} sync to finish ...") sync_status = self.nailgun_capsule.content_get_sync() logger.info(f"Active tasks {sync_status['active_sync_tasks']}") assert ( len(sync_status['active_sync_tasks']) or datetime.strptime(sync_status['last_sync_time'], '%Y-%m-%d %H:%M:%S UTC') - > start_time + >= start_time ) # Wait till capsule sync finishes and assert the sync task succeeded diff --git a/tests/upgrades/test_capsule.py b/tests/upgrades/test_capsule.py index a520a316c18..d064b677fbe 100644 --- a/tests/upgrades/test_capsule.py +++ b/tests/upgrades/test_capsule.py @@ -106,8 +106,7 @@ def test_post_user_scenario_capsule_sync( ak_env = ak.environment.read() org = ak.organization.read() content_view = target_sat.api.ContentView(id=pre_upgrade_data.get('content_view_id')).read() - pre_configured_capsule.nailgun_capsule.content_sync(timeout=3600) - pre_configured_capsule.wait_for_sync(timeout=9000) + pre_configured_capsule.nailgun_capsule.content_sync(timeout=7200) sat_repo_url = target_sat.get_published_repo_url( org=org.label, @@ -125,11 +124,16 @@ def test_post_user_scenario_capsule_sync( ) sat_files_urls = get_repo_files_urls_by_url(sat_repo_url) cap_files_urls = get_repo_files_urls_by_url(cap_repo_url) - assert len(sat_files_urls) == len(cap_files_urls) == constants.FAKE_1_YUM_REPOS_COUNT + assert ( + len(sat_files_urls) == constants.FAKE_1_YUM_REPOS_COUNT + ), 'upstream and satellite repo rpm counts are differrent' + assert len(sat_files_urls) == len( + cap_files_urls + ), 'satellite and capsule repo rpm counts are differrent' sat_files = {os.path.basename(f) for f in sat_files_urls} cap_files = {os.path.basename(f) for f in cap_files_urls} - assert sat_files == cap_files + assert sat_files == cap_files, 'satellite and capsule rpm basenames are differrent' for pkg in constants.FAKE_1_YUM_REPO_RPMS: assert pkg in sat_files, f'{pkg=} is not in the {repo=} on satellite' @@ -137,7 +141,7 @@ def test_post_user_scenario_capsule_sync( sat_files_md5 = [target_sat.md5_by_url(url) for url in sat_files_urls] cap_files_md5 = [target_sat.md5_by_url(url) for url in cap_files_urls] - assert sat_files_md5 == cap_files_md5 + assert sat_files_md5 == cap_files_md5, 'satellite and capsule rpm md5sums are differrent' class TestCapsuleSyncNewRepo: @@ -182,8 +186,7 @@ def test_post_user_scenario_capsule_sync_yum_repo( content_view.read().version[0].promote(data={'environment_ids': ak_env.id}) content_view_env = [env.id for env in content_view.read().environment] assert ak_env.id in content_view_env - pre_configured_capsule.nailgun_capsule.content_sync() - pre_configured_capsule.wait_for_sync(timeout=9000) + pre_configured_capsule.nailgun_capsule.content_sync(timeout=7200) sat_repo_url = target_sat.get_published_repo_url( org=default_org.label, @@ -202,11 +205,16 @@ def test_post_user_scenario_capsule_sync_yum_repo( sat_files_urls = get_repo_files_urls_by_url(sat_repo_url) cap_files_urls = get_repo_files_urls_by_url(cap_repo_url) - assert len(sat_files_urls) == len(cap_files_urls) == constants.FAKE_1_YUM_REPOS_COUNT + assert ( + len(sat_files_urls) == constants.FAKE_1_YUM_REPOS_COUNT + ), 'upstream and satellite repo rpm counts are differrent' + assert len(sat_files_urls) == len( + cap_files_urls + ), 'satellite and capsule repo rpm counts are differrent' sat_files = {os.path.basename(f) for f in sat_files_urls} cap_files = {os.path.basename(f) for f in cap_files_urls} - assert sat_files == cap_files + assert sat_files == cap_files, 'satellite and capsule rpm basenames are differrent' for pkg in constants.FAKE_1_YUM_REPO_RPMS: assert pkg in sat_files, f'{pkg=} is not in the {repo=} on satellite' @@ -214,4 +222,4 @@ def test_post_user_scenario_capsule_sync_yum_repo( sat_files_md5 = [target_sat.md5_by_url(url) for url in sat_files_urls] cap_files_md5 = [target_sat.md5_by_url(url) for url in cap_files_urls] - assert sat_files_md5 == cap_files_md5 + assert sat_files_md5 == cap_files_md5, 'satellite and capsule rpm md5sums are differrent' From 6974dca595c7e86eb14975d362aa566d2847a061 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 24 Jan 2024 10:43:13 -0500 Subject: [PATCH 460/586] [6.14.z] cli org test fix (#13891) --- tests/foreman/cli/test_organization.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/foreman/cli/test_organization.py b/tests/foreman/cli/test_organization.py index c7d84466390..da79144f141 100644 --- a/tests/foreman/cli/test_organization.py +++ b/tests/foreman/cli/test_organization.py @@ -520,7 +520,13 @@ def test_positive_add_and_remove_locations(module_org, module_target_sat): {'location': locations[1]['name'], 'id': module_org.id} ) org_info = module_target_sat.cli.Org.info({'id': module_org.id}) - assert not org_info.get('locations'), "Failed to remove locations" + found_locations = ( + org_info.get('locations') + if isinstance(org_info.get('locations'), list) + else [org_info.get('locations')] + ) + assert locations[0]['name'] not in found_locations, "Failed to remove locations" + assert locations[1]['name'] not in found_locations, "Failed to remove locations" @pytest.mark.tier1 From c1eed7a80f1a8166ef00d221ca5db00e83d927e3 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 24 Jan 2024 22:26:04 -0500 Subject: [PATCH 461/586] [6.14.z] Bump cryptography from 42.0.0 to 42.0.1 (#13899) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9148455ebfa..14d3efbb87b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ betelgeuse==1.11.0 broker[docker]==0.4.1 -cryptography==42.0.0 +cryptography==42.0.1 deepdiff==6.7.1 dynaconf[vault]==3.2.4 fauxfactory==3.1.0 From 1f573d3ca735a2055ef63cbba9467caf2607dabb Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 25 Jan 2024 01:59:47 -0500 Subject: [PATCH 462/586] [6.14.z] Adding coverage for BZ2221621 (#13889) coverage for bz2221621 (cherry picked from commit ff8b7f610d6740f312c8d2c354b1a3e9c0af2997) Co-authored-by: Radek Mynar --- tests/foreman/destructive/test_installer.py | 41 +++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/foreman/destructive/test_installer.py b/tests/foreman/destructive/test_installer.py index c130e0fcd20..ebfcfb8d50a 100644 --- a/tests/foreman/destructive/test_installer.py +++ b/tests/foreman/destructive/test_installer.py @@ -179,3 +179,44 @@ def test_positive_installer_puma_worker_count(target_sat): assert f'cluster worker {i}' in result.stdout else: assert f'cluster worker {i}' not in result.stdout + + +def test_negative_handle_invalid_certificate(cert_setup_destructive_teardown): + """Satellite installer should not do any harmful changes to existing satellite after attempt + to use invalid certificates. + + :id: 97b72faf-4684-4d8c-ae0e-1ebd5085620b + + :steps: + 1. Launch satellite installer and attempt to use invalid certificates + + :expectedresults: Satellite installer should fail and Satellite should be running + + :BZ: 2221621, 2238363 + + :customerscenario: true + + """ + cert_data, satellite = cert_setup_destructive_teardown + + # check if satellite is running + result = satellite.execute('hammer ping') + assert result.status == 0, f'Hammer Ping failed:\n{result.stderr}' + + # attempt to use invalid certificates + result = satellite.install( + InstallerCommand( + 'certs-update-server', + 'certs-update-server-ca', + scenario='satellite', + certs_server_cert='/root/certs/invalid.crt', + certs_server_key='/root/certs/invalid.key', + certs_server_ca_cert=f'"/root/{cert_data["ca_bundle_file_name"]}"', + ) + ) + # installer should fail with non-zero value + assert result.status != 0 + assert "verification failed" in result.stdout + + result = satellite.execute('hammer ping') + assert result.status == 0, f'Hammer Ping failed:\n{result.stderr}' From be06c80d9b68449e5deb2fb7169aeeda0d113d6c Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 26 Jan 2024 13:14:51 -0500 Subject: [PATCH 463/586] [6.14.z] Use sca enabled manifest for test_positive_configure_cloud_connector (#13910) --- pytest_fixtures/component/taxonomy.py | 10 ++++++++++ tests/foreman/ui/test_rhc.py | 6 ++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/pytest_fixtures/component/taxonomy.py b/pytest_fixtures/component/taxonomy.py index ebfc9126eac..e6ac87357cd 100644 --- a/pytest_fixtures/component/taxonomy.py +++ b/pytest_fixtures/component/taxonomy.py @@ -196,6 +196,16 @@ def module_extra_rhel_entitlement_manifest(): yield manifest +@pytest.fixture(scope='module') +def module_extra_rhel_sca_manifest(): + """Yields a manifest in sca mode with subscriptions determined by the + 'manifest_category.extra_rhel_entitlement` setting in conf/manifest.yaml.""" + with Manifester( + manifest_category=settings.manifest.extra_rhel_entitlement, simple_content_access="enabled" + ) as manifest: + yield manifest + + @pytest.fixture(scope='module') def module_sca_manifest(): """Yields a manifest in Simple Content Access mode with subscriptions determined by the diff --git a/tests/foreman/ui/test_rhc.py b/tests/foreman/ui/test_rhc.py index d9aa8e1a111..ab973ec079c 100644 --- a/tests/foreman/ui/test_rhc.py +++ b/tests/foreman/ui/test_rhc.py @@ -65,13 +65,11 @@ def fixture_setup_rhc_satellite( request, module_target_sat, module_rhc_org, - module_extra_rhel_entitlement_manifest, + module_extra_rhel_sca_manifest, ): """Create Organization and activation key after successful test execution""" if settings.rh_cloud.crc_env == 'prod': - module_target_sat.upload_manifest( - module_rhc_org.id, module_extra_rhel_entitlement_manifest.content - ) + module_target_sat.upload_manifest(module_rhc_org.id, module_extra_rhel_sca_manifest.content) yield if request.node.rep_call.passed: # Enable and sync required repos From 4e286c2e46b9a8f01eb1bb24ca6c6b6173db0fba Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 29 Jan 2024 08:40:43 -0500 Subject: [PATCH 464/586] [6.14.z] Add host teardown for smart classparameters test (#13917) --- tests/foreman/cli/test_classparameters.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/foreman/cli/test_classparameters.py b/tests/foreman/cli/test_classparameters.py index 433552737da..a4e75c1f76d 100644 --- a/tests/foreman/cli/test_classparameters.py +++ b/tests/foreman/cli/test_classparameters.py @@ -66,6 +66,7 @@ class TestSmartClassParameters: @pytest.mark.e2e def test_positive_list( self, + request, session_puppet_enabled_sat, module_puppet_org, module_puppet_loc, @@ -86,6 +87,7 @@ def test_positive_list( environment=module_puppet['env'].name, ).create() host.add_puppetclass(data={'puppetclass_id': module_puppet['class']['id']}) + request.addfinalizer(host.delete) hostgroup = session_puppet_enabled_sat.cli_factory.hostgroup( { 'puppet-environment-id': module_puppet['env'].id, From 44bb3cf73e077c28c0f4960a4c8376e9e32d4e6c Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 29 Jan 2024 11:42:45 -0500 Subject: [PATCH 465/586] [6.14.z] Update test_positive_configure_cloud_connector (#13926) --- tests/foreman/ui/test_rhc.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/foreman/ui/test_rhc.py b/tests/foreman/ui/test_rhc.py index ab973ec079c..0fdefee5bf1 100644 --- a/tests/foreman/ui/test_rhc.py +++ b/tests/foreman/ui/test_rhc.py @@ -14,6 +14,7 @@ from datetime import datetime, timedelta from fauxfactory import gen_string +from manifester import Manifester import pytest from robottelo import constants @@ -65,11 +66,16 @@ def fixture_setup_rhc_satellite( request, module_target_sat, module_rhc_org, - module_extra_rhel_sca_manifest, ): """Create Organization and activation key after successful test execution""" if settings.rh_cloud.crc_env == 'prod': - module_target_sat.upload_manifest(module_rhc_org.id, module_extra_rhel_sca_manifest.content) + manifester = Manifester( + allocation_name=module_rhc_org.name, + manifest_category=settings.manifest.extra_rhel_entitlement, + simple_content_access="enabled", + ) + rhcloud_manifest = manifester.get_manifest() + module_target_sat.upload_manifest(module_rhc_org.id, rhcloud_manifest.content) yield if request.node.rep_call.passed: # Enable and sync required repos From d9f6baa1e5448deea2d239a544530187a73dbe2f Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 30 Jan 2024 06:17:46 -0500 Subject: [PATCH 466/586] [6.14.z] ensure mqtt capsule is not bypassed (#13932) --- tests/foreman/api/test_remoteexecution.py | 14 +++++++------- tests/foreman/cli/test_remoteexecution.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/foreman/api/test_remoteexecution.py b/tests/foreman/api/test_remoteexecution.py index e8333ced360..f0c887a80f8 100644 --- a/tests/foreman/api/test_remoteexecution.py +++ b/tests/foreman/api/test_remoteexecution.py @@ -73,6 +73,12 @@ def test_positive_run_capsule_upgrade_playbook(module_capsule_configured, target @pytest.mark.tier3 @pytest.mark.no_containers @pytest.mark.rhel_ver_list('8') +@pytest.mark.parametrize( + 'setting_update', + ['remote_execution_global_proxy=False'], + ids=["no_global_proxy"], + indirect=True, +) def test_negative_time_to_pickup( module_org, module_target_sat, @@ -80,6 +86,7 @@ def test_negative_time_to_pickup( module_ak_with_cv, module_capsule_configured_mqtt, rhel_contenthost, + setting_update, ): """Time to pickup setting is honored for host registered to mqtt @@ -131,13 +138,6 @@ def test_negative_time_to_pickup( result = rhel_contenthost.execute('systemctl stop yggdrasild') assert result.status == 0, f'Failed to stop yggdrasil on client: {result.stderr}' - # Make sure the job is executed by the registered-trough capsule - global_ttp = module_target_sat.api.Setting().search( - query={'search': 'name="remote_execution_global_proxy"'} - )[0] - global_ttp.value = False - global_ttp.update(['value']) - # run script provider rex command with time_to_pickup job = module_target_sat.api.JobInvocation().run( synchronous=False, diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index 7b56ea3faa7..0687ed3728b 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -1126,6 +1126,12 @@ class TestPullProviderRex: @pytest.mark.upgrade @pytest.mark.no_containers @pytest.mark.rhel_ver_match('[^6].*') + @pytest.mark.parametrize( + 'setting_update', + ['remote_execution_global_proxy=False'], + ids=["no_global_proxy"], + indirect=True, + ) def test_positive_run_job_on_host_converted_to_pull_provider( self, module_org, @@ -1134,6 +1140,7 @@ def test_positive_run_job_on_host_converted_to_pull_provider( module_target_sat, module_capsule_configured_mqtt, rhel_contenthost, + setting_update, ): """Run custom template on host converted to mqtt @@ -1230,6 +1237,12 @@ def test_positive_run_job_on_host_converted_to_pull_provider( @pytest.mark.e2e @pytest.mark.no_containers @pytest.mark.rhel_ver_match('[^6].*') + @pytest.mark.parametrize( + 'setting_update', + ['remote_execution_global_proxy=False'], + ids=["no_global_proxy"], + indirect=True, + ) def test_positive_run_job_on_host_registered_to_pull_provider( self, module_org, @@ -1238,6 +1251,7 @@ def test_positive_run_job_on_host_registered_to_pull_provider( module_ak_with_cv, module_capsule_configured_mqtt, rhel_contenthost, + setting_update, ): """Run custom template on host registered to mqtt, check effective user setting From f018ec480f3f462333e7d74285ef3cdc42f9bc75 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 30 Jan 2024 07:30:38 -0500 Subject: [PATCH 467/586] [6.14.z] ui_seesion > ui_session (#13935) --- tests/foreman/ui/test_host.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index fc0abe523f1..11d8fa16258 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -530,7 +530,7 @@ def test_positive_remove_parameter_non_admin_user( organization=module_org, host_parameters_attributes=[parameter], ).create() - with target_sat.ui_seesion(test_name, user=user.login, password=user_password) as session: + with target_sat.ui_session(test_name, user=user.login, password=user_password) as session: values = session.host.read(host.name, 'parameters') assert values['parameters']['host_params'][0] == parameter session.host.update(host.name, {'parameters.host_params': []}) From 17918ee5d9a1cb29972be5ca0689dc2d3016bd34 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 30 Jan 2024 12:30:46 -0500 Subject: [PATCH 468/586] [6.14.z] Remove test_positive_insights_puppet_package_availability test (#13937) Remove test_positive_insights_puppet_package_availability test (#13936) (cherry picked from commit 322e5154e13dc08628b31ee21462983871c1d87d) Co-authored-by: Jameer Pathan <21165044+jameerpathan111@users.noreply.github.com> --- tests/foreman/api/test_capsulecontent.py | 26 ------------------------ 1 file changed, 26 deletions(-) diff --git a/tests/foreman/api/test_capsulecontent.py b/tests/foreman/api/test_capsulecontent.py index ee16d1daa1e..4422954f0b5 100644 --- a/tests/foreman/api/test_capsulecontent.py +++ b/tests/foreman/api/test_capsulecontent.py @@ -38,32 +38,6 @@ class TestCapsuleContentManagement: interactions and use capsule. """ - @pytest.mark.tier3 - @pytest.mark.skip_if_not_set('capsule', 'clients', 'fake_manifest') - def test_positive_insights_puppet_package_availability(self, module_capsule_configured): - """Check `redhat-access-insights-puppet` package availability for - capsule - - :BZ: 1315844 - - :id: a31b0e21-aa5d-44e2-a408-5e01b79db3a1 - - :CaseComponent: RHCloud-Insights - - :Team: Platform - - :customerscenario: true - - :expectedresults: `redhat-access-insights-puppet` package is delivered - in capsule repo and is available for installation on capsule via - yum - """ - package_name = 'redhat-access-insights-puppet' - result = module_capsule_configured.run(f'yum list {package_name} | grep @capsule') - if result.status != 0: - result = module_capsule_configured.run(f'yum list available | grep {package_name}') - assert result.status == 0 - @pytest.mark.tier4 @pytest.mark.skip_if_not_set('capsule', 'clients', 'fake_manifest') def test_positive_uploaded_content_library_sync( From 8bb7177b7db6cb96dc87f23620f7d9bb024a5fc7 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 30 Jan 2024 22:56:29 -0500 Subject: [PATCH 469/586] [6.14.z] Bump cryptography from 42.0.1 to 42.0.2 (#13942) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 14d3efbb87b..fcdbb549832 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ betelgeuse==1.11.0 broker[docker]==0.4.1 -cryptography==42.0.1 +cryptography==42.0.2 deepdiff==6.7.1 dynaconf[vault]==3.2.4 fauxfactory==3.1.0 From 407552235a9e1ab1bf66b79f29bd8133c2811279 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 31 Jan 2024 05:11:02 -0500 Subject: [PATCH 470/586] [6.14.z] bz2220965 (#13945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lukáš Hellebrandt --- pytest_fixtures/core/ui.py | 9 ++++++++- tests/foreman/ui/test_remoteexecution.py | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/pytest_fixtures/core/ui.py b/pytest_fixtures/core/ui.py index 298632f59f6..81dbbaa6e8e 100644 --- a/pytest_fixtures/core/ui.py +++ b/pytest_fixtures/core/ui.py @@ -18,9 +18,12 @@ def ui_user(request, module_org, module_location, module_target_sat): test_module_name = request.module.__name__.split('.')[-1].split('_', 1)[-1] login = f"{test_module_name}_{gen_string('alphanumeric')}" password = gen_string('alphanumeric') + admin = request.param.get('admin', True) logger.debug('Creating session user %r', login) user = module_target_sat.api.User( - admin=True, + admin=admin, + organization=[module_org], + location=[module_location], default_organization=module_org, default_location=module_location, description=f'created automatically by airgun for module "{test_module_name}"', @@ -28,6 +31,10 @@ def ui_user(request, module_org, module_location, module_target_sat): password=password, ).create() user.password = password + if not admin: + # give all the permissions + user.role = module_target_sat.api.Role().search(query={'per_page': 'all'}) + user.update(['role']) yield user try: logger.debug('Deleting session user %r', user.login) diff --git a/tests/foreman/ui/test_remoteexecution.py b/tests/foreman/ui/test_remoteexecution.py index 5d9f3900f9a..e1e08d070e7 100644 --- a/tests/foreman/ui/test_remoteexecution.py +++ b/tests/foreman/ui/test_remoteexecution.py @@ -76,7 +76,12 @@ def test_positive_run_default_job_template_by_ip(session, rex_contenthost, modul @pytest.mark.skip_if_open('BZ:2182353') @pytest.mark.rhel_ver_match('8') @pytest.mark.tier3 -def test_positive_run_custom_job_template_by_ip(session, module_org, rex_contenthost): +@pytest.mark.parametrize( + 'ui_user', [{'admin': True}, {'admin': False}], indirect=True, ids=['adminuser', 'nonadminuser'] +) +def test_positive_run_custom_job_template_by_ip( + session, module_org, target_sat, default_location, ui_user, rex_contenthost +): """Run a job template on a host connected by ip :id: 3a59eb15-67c4-46e1-ba5f-203496ec0b0c @@ -93,8 +98,13 @@ def test_positive_run_custom_job_template_by_ip(session, module_org, rex_content :expectedresults: Verify the job was successfully ran against the host :parametrized: yes - """ + :bz: 2220965 + + :customerscenario: true + """ + ui_user.location.append(target_sat.api.Location(id=default_location.id)) + ui_user.update(['location']) hostname = rex_contenthost.hostname job_template_name = gen_string('alpha') with session: From fb55967af16356d12394e4bc8c17c77f58473df1 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:02:24 -0500 Subject: [PATCH 471/586] [6.14.z] small correction in hammer repository create command (#13957) --- tests/foreman/cli/test_repository.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/cli/test_repository.py b/tests/foreman/cli/test_repository.py index b43da6e4e81..84150f2494a 100644 --- a/tests/foreman/cli/test_repository.py +++ b/tests/foreman/cli/test_repository.py @@ -2638,7 +2638,7 @@ def test_positive_sync_ansible_collection_from_satellite(self, repo, target_sat) 'product-id': prod_2['id'], 'url': published_url, 'content-type': 'ansible_collection', - 'ansible-collection-reqirements': '{collections: \ + 'ansible-collection-requirements': '{collections: \ [{ name: theforeman.operations, version: "0.1.0"}]}', } ) From 652d150211de8f2c7b44e8498131526daac52d88 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:33:08 -0500 Subject: [PATCH 472/586] [6.14.z] small fix related to repo sync bulk canceled scenario (#13953) small fix related to repo sync bulk canceled scenario (#13951) (cherry picked from commit 2c726d838557dc18e3dd4b4c58100f37c70917b6) Co-authored-by: vijay sawant --- tests/foreman/api/test_repository.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index 764e14bbfd7..903c8ab3c4a 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -1442,7 +1442,7 @@ def test_positive_bulk_cancel_sync(self, target_sat, module_entitlement_manifest time.sleep(30) target_sat.api.ForemanTask().bulk_cancel(data={"task_ids": sync_ids[5:]}) for sync_id in sync_ids: - sync_result = target_sat.api.ForemanTask(id=sync_id).poll(canceled=True) + sync_result = target_sat.api.ForemanTask(id=sync_id).poll(must_succeed=False) assert ( 'Task canceled' in sync_result['humanized']['errors'] or 'No content added' in sync_result['humanized']['output'] @@ -1703,7 +1703,7 @@ def test_positive_cancel_docker_repo_sync(self, repo, target_sat): # Need to wait for sync to actually start up time.sleep(2) target_sat.api.ForemanTask().bulk_cancel(data={"task_ids": [sync_task['id']]}) - sync_task = target_sat.api.ForemanTask(id=sync_task['id']).poll(canceled=True) + sync_task = target_sat.api.ForemanTask(id=sync_task['id']).poll(must_succeed=False) assert 'Task canceled' in sync_task['humanized']['errors'] assert 'No content added' in sync_task['humanized']['output'] From d20475ad18ec0a860a2220e9565efbcdb6adc37e Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:54:49 -0500 Subject: [PATCH 473/586] [6.14.z] fix in ui_host fixture (#13963) Co-authored-by: Peter Ondrejka fix in ui_host fixture (#13962) --- pytest_fixtures/core/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_fixtures/core/ui.py b/pytest_fixtures/core/ui.py index 81dbbaa6e8e..c266e363c7e 100644 --- a/pytest_fixtures/core/ui.py +++ b/pytest_fixtures/core/ui.py @@ -18,7 +18,7 @@ def ui_user(request, module_org, module_location, module_target_sat): test_module_name = request.module.__name__.split('.')[-1].split('_', 1)[-1] login = f"{test_module_name}_{gen_string('alphanumeric')}" password = gen_string('alphanumeric') - admin = request.param.get('admin', True) + admin = request.param.get('admin', True) if hasattr(request, 'param') else True logger.debug('Creating session user %r', login) user = module_target_sat.api.User( admin=admin, From bca2e1f9a3ee0bc415f4ed3527b13d5215e84277 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 31 Jan 2024 23:07:24 -0500 Subject: [PATCH 474/586] [6.14.z] Bump kentaro-m/auto-assign-action from 1.2.6 to 2.0.0 (#13969) --- .github/workflows/auto_assignment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto_assignment.yaml b/.github/workflows/auto_assignment.yaml index 5b914ed6424..4e09e262542 100644 --- a/.github/workflows/auto_assignment.yaml +++ b/.github/workflows/auto_assignment.yaml @@ -15,6 +15,6 @@ jobs: if: "!contains(github.event.pull_request.labels.*.name, 'Auto_Cherry_Picked')" runs-on: ubuntu-latest steps: - - uses: kentaro-m/auto-assign-action@v1.2.6 + - uses: kentaro-m/auto-assign-action@v2.0.0 with: configuration-path: ".github/auto_assign.yml" From 46eb43596f1702279fa54f4354d0ebebf63965b6 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 2 Feb 2024 07:14:58 -0500 Subject: [PATCH 475/586] [6.14.z] [TestFix] Replacing 'content' with 'file' option in reporttemplate create (#13978) [TestFix] Replacing 'content' with 'file' option in reporttemplate create (#13973) Replacing 'content' with 'file' param (cherry picked from commit 15f49ed8d62d74c315f5e5de1a599f8d3a757b61) Co-authored-by: Cole Higgins --- tests/foreman/cli/test_reporttemplates.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/foreman/cli/test_reporttemplates.py b/tests/foreman/cli/test_reporttemplates.py index ca93b8a7318..01b471d43fd 100644 --- a/tests/foreman/cli/test_reporttemplates.py +++ b/tests/foreman/cli/test_reporttemplates.py @@ -321,9 +321,7 @@ def test_positive_dump_report(module_target_sat): """ name = gen_alpha() content = gen_alpha() - report_template = module_target_sat.cli_factory.report_template( - {'name': name, 'content': content} - ) + report_template = module_target_sat.cli_factory.report_template({'name': name, 'file': content}) result = module_target_sat.cli.ReportTemplate.dump({'id': report_template['id']}) assert content in result @@ -402,9 +400,7 @@ def test_positive_generate_report_sanitized(module_target_sat): } ) - report_template = module_target_sat.cli_factory.report_template( - {'content': REPORT_TEMPLATE_FILE} - ) + report_template = module_target_sat.cli_factory.report_template({'file': REPORT_TEMPLATE_FILE}) result = module_target_sat.cli.ReportTemplate.generate({'name': report_template['name']}) assert 'Name,Operating System' in result # verify header of custom template From 79a723c5bf7638e1956b61a3c33983eaeded0954 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 5 Feb 2024 08:49:24 -0500 Subject: [PATCH 476/586] [6.14.z] Fix virtwho UI cases failed for _GeneratorContextManager object has no attribute virtwho_configure (#13984) Fix virtwho UI cases failed for _GeneratorContextManager object has no attribute virtwho_configure (#13981) * Fix virtwho UI cases failed for _GeneratorContextManager object has no attribute virtwho_configure * pre-check (cherry picked from commit 3ed68126955903e0f4e00d62bd9185dd5de0c97f) Co-authored-by: yanpliu --- tests/foreman/virtwho/conftest.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/foreman/virtwho/conftest.py b/tests/foreman/virtwho/conftest.py index 9b09a66c0fa..a83875fe130 100644 --- a/tests/foreman/virtwho/conftest.py +++ b/tests/foreman/virtwho/conftest.py @@ -49,7 +49,10 @@ def test_foo(session): # your ui test steps here session.architecture.create({'name': 'bar'}) """ - return module_target_sat.ui_session(test_name, module_user.login, module_user.password) + with module_target_sat.ui_session( + test_name, module_user.login, module_user.password + ) as session: + return session @pytest.fixture(scope='module') @@ -96,4 +99,7 @@ def test_foo(session): # your ui test steps here session.architecture.create({'name': 'bar'}) """ - return module_target_sat.ui_session(test_name, module_user_sca.login, module_user_sca.password) + with module_target_sat.ui_session( + test_name, module_user_sca.login, module_user_sca.password + ) as session_sca: + return session_sca From c011f29b08da2c51847a1000e315968b559d52c1 Mon Sep 17 00:00:00 2001 From: Shweta Singh Date: Mon, 5 Feb 2024 22:43:08 +0530 Subject: [PATCH 477/586] Fix registration test cases (#13974) --- pytest_fixtures/component/activationkey.py | 8 +- tests/foreman/api/test_registration.py | 13 +- tests/foreman/ui/test_host.py | 556 +-------------------- tests/foreman/ui/test_registration.py | 519 ++++++++++++++++++- 4 files changed, 528 insertions(+), 568 deletions(-) diff --git a/pytest_fixtures/component/activationkey.py b/pytest_fixtures/component/activationkey.py index fa7b53d0189..98deb66c3cb 100644 --- a/pytest_fixtures/component/activationkey.py +++ b/pytest_fixtures/component/activationkey.py @@ -6,12 +6,12 @@ @pytest.fixture(scope='module') -def module_activation_key(module_sca_manifest_org, module_target_sat): +def module_activation_key(module_entitlement_manifest_org, module_target_sat): """Create activation key using default CV and library environment.""" activation_key = module_target_sat.api.ActivationKey( - content_view=module_sca_manifest_org.default_content_view.id, - environment=module_sca_manifest_org.library.id, - organization=module_sca_manifest_org, + content_view=module_entitlement_manifest_org.default_content_view.id, + environment=module_entitlement_manifest_org.library.id, + organization=module_entitlement_manifest_org, ).create() return activation_key diff --git a/tests/foreman/api/test_registration.py b/tests/foreman/api/test_registration.py index 9a3b98e3550..36dfc59cf47 100644 --- a/tests/foreman/api/test_registration.py +++ b/tests/foreman/api/test_registration.py @@ -89,7 +89,7 @@ def test_host_registration_end_to_end( @pytest.mark.tier3 @pytest.mark.rhel_ver_match('[^6]') def test_positive_allow_reregistration_when_dmi_uuid_changed( - module_sca_manifest_org, + module_entitlement_manifest_org, rhel_contenthost, target_sat, module_activation_key, @@ -108,7 +108,7 @@ def test_positive_allow_reregistration_when_dmi_uuid_changed( """ uuid_1 = str(uuid.uuid1()) uuid_2 = str(uuid.uuid4()) - org = module_sca_manifest_org + org = module_entitlement_manifest_org target_sat.execute(f'echo \'{{"dmi.system.uuid": "{uuid_1}"}}\' > /etc/rhsm/facts/uuid.facts') command = target_sat.api.RegistrationCommand( organization=org, @@ -131,7 +131,7 @@ def test_positive_allow_reregistration_when_dmi_uuid_changed( def test_positive_update_packages_registration( module_target_sat, - module_sca_manifest_org, + module_entitlement_manifest_org, module_location, rhel8_contenthost, module_activation_key, @@ -142,7 +142,8 @@ def test_positive_update_packages_registration( :expectedresults: Package update is successful on host post registration. """ - org = module_sca_manifest_org + org = module_entitlement_manifest_org + org = module_entitlement_manifest_org command = module_target_sat.api.RegistrationCommand( organization=org, location=module_location, @@ -162,7 +163,7 @@ def test_positive_update_packages_registration( @pytest.mark.no_containers def test_positive_rex_interface_for_global_registration( module_target_sat, - module_sca_manifest_org, + module_entitlement_manifest_org, module_location, rhel8_contenthost, module_activation_key, @@ -186,7 +187,7 @@ def test_positive_rex_interface_for_global_registration( add_interface_command = f'ip link add eth1 type dummy;ifconfig eth1 hw ether {mac_address};ip addr add {ip}/24 brd + dev eth1 label eth1:1;ip link set dev eth1 up' result = rhel8_contenthost.execute(add_interface_command) assert result.status == 0 - org = module_sca_manifest_org + org = module_entitlement_manifest_org command = module_target_sat.api.RegistrationCommand( organization=org, location=module_location, diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index 11d8fa16258..0ee25e68234 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -13,23 +13,19 @@ """ import copy import csv -from datetime import datetime import os -import re from airgun.exceptions import DisabledWidgetError, NoSuchElementException import pytest from wait_for import wait_for import yaml -from robottelo import constants from robottelo.config import settings from robottelo.constants import ( ANY_CONTEXT, DEFAULT_CV, DEFAULT_LOC, ENVIRONMENT, - FAKE_1_CUSTOM_PACKAGE, FAKE_7_CUSTOM_PACKAGE, FAKE_8_CUSTOM_PACKAGE, FAKE_8_CUSTOM_PACKAGE_NAME, @@ -105,6 +101,20 @@ def tracer_install_host(rex_contenthost, target_sat): return rex_contenthost +@pytest.fixture +def new_host_ui(target_sat): + """Changes the setting to use the New All Host UI + then returns it back to the normal value""" + all_hosts_setting = target_sat.api.Setting().search( + query={'search': f'name={"new_hosts_page"}'} + )[0] + all_hosts_setting.value = 'True' + all_hosts_setting.update({'value'}) + yield + all_hosts_setting.value = 'False' + all_hosts_setting.update({'value'}) + + @pytest.mark.e2e @pytest.mark.tier2 def test_positive_end_to_end(session, module_global_params, target_sat, host_ui_options): @@ -970,271 +980,6 @@ def test_positive_validate_inherited_cv_lce_ansiblerole(session, target_sat, mod assert host.name in [host.name for host in matching_hosts] -@pytest.mark.tier2 -def test_positive_global_registration_form( - session, module_activation_key, module_org, smart_proxy_location, default_os, target_sat -): - """Host registration form produces a correct curl command for various inputs - - :id: f81c2ec4-85b1-4372-8e63-464ddbf70296 - - :customerscenario: true - - :expectedresults: The curl command contains all required parameters - """ - # rex and insights parameters are only specified in curl when differing from - # inerited parameters - result = ( - target_sat.api.CommonParameter() - .search(query={'search': 'name=host_registration_remote_execution'})[0] - .read() - ) - rex_value = not result.value - result = ( - target_sat.api.CommonParameter() - .search(query={'search': 'name=host_registration_insights'})[0] - .read() - ) - insights_value = not result.value - hostgroup = target_sat.api.HostGroup( - organization=[module_org], location=[smart_proxy_location] - ).create() - iface = 'eth0' - with session: - cmd = session.host.get_register_command( - { - 'advanced.setup_insights': 'Yes (override)' if insights_value else 'No (override)', - 'advanced.setup_rex': 'Yes (override)' if rex_value else 'No (override)', - 'general.insecure': True, - 'general.host_group': hostgroup.name, - 'general.operating_system': default_os.title, - 'general.activation_keys': module_activation_key.name, - 'advanced.update_packages': True, - 'advanced.rex_interface': iface, - } - ) - expected_pairs = [ - f'organization_id={module_org.id}', - f'activation_keys={module_activation_key.name}', - f'hostgroup_id={hostgroup.id}', - f'location_id={smart_proxy_location.id}', - f'operatingsystem_id={default_os.id}', - f'remote_execution_interface={iface}', - f'setup_insights={"true" if insights_value else "false"}', - f'setup_remote_execution={"true" if rex_value else "false"}', - f'{target_sat.hostname}', - 'insecure', - 'update_packages=true', - ] - for pair in expected_pairs: - assert pair in cmd - - -@pytest.mark.e2e -@pytest.mark.no_containers -@pytest.mark.tier3 -@pytest.mark.rhel_ver_match('[^6]') -def test_positive_global_registration_end_to_end( - session, - module_activation_key, - module_org, - smart_proxy_location, - default_os, - default_smart_proxy, - rhel_contenthost, - target_sat, -): - """Host registration form produces a correct registration command and host is - registered successfully with it, remote execution and insights are set up - - :id: a02658bf-097e-47a8-8472-5d9f649ba07a - - :customerscenario: true - - :BZ: 1993874 - - :expectedresults: Host is successfully registered, remote execution and insights - client work out of the box - - :parametrized: yes - """ - # make sure global parameters for rex and insights are set to true - insights_cp = ( - target_sat.api.CommonParameter() - .search(query={'search': 'name=host_registration_insights'})[0] - .read() - ) - rex_cp = ( - target_sat.api.CommonParameter() - .search(query={'search': 'name=host_registration_remote_execution'})[0] - .read() - ) - - if not insights_cp.value: - target_sat.api.CommonParameter(id=insights_cp.id, value=1).update(['value']) - if not rex_cp.value: - target_sat.api.CommonParameter(id=rex_cp.id, value=1).update(['value']) - - # rex interface - iface = 'eth0' - # fill in the global registration form - with session: - cmd = session.host.get_register_command( - { - 'general.operating_system': default_os.title, - 'general.activation_keys': module_activation_key.name, - 'advanced.update_packages': True, - 'advanced.rex_interface': iface, - 'general.insecure': True, - } - ) - expected_pairs = [ - f'organization_id={module_org.id}', - f'activation_keys={module_activation_key.name}', - f'location_id={smart_proxy_location.id}', - f'operatingsystem_id={default_os.id}', - f'{default_smart_proxy.name}', - 'insecure', - 'update_packages=true', - ] - for pair in expected_pairs: - assert pair in cmd - # rhel repo required for insights client installation, - # syncing it to the satellite would take too long - rhelver = rhel_contenthost.os_version.major - if rhelver > 7: - rhel_contenthost.create_custom_repos(**settings.repos[f'rhel{rhelver}_os']) - else: - rhel_contenthost.create_custom_repos( - **{f'rhel{rhelver}_os': settings.repos[f'rhel{rhelver}_os']} - ) - # make sure there will be package availabe for update - if rhel_contenthost.os_version.major == '6': - package = FAKE_1_CUSTOM_PACKAGE - repo_url = settings.repos.yum_1['url'] - else: - package = FAKE_7_CUSTOM_PACKAGE - repo_url = settings.repos.yum_3['url'] - rhel_contenthost.create_custom_repos(fake_yum=repo_url) - rhel_contenthost.execute(f"yum install -y {package}") - # run curl - result = rhel_contenthost.execute(cmd) - assert result.status == 0 - result = rhel_contenthost.execute('subscription-manager identity') - assert result.status == 0 - # Assert that a yum update was made this day ("Update" or "I, U" in history) - timezone_offset = rhel_contenthost.execute('date +"%:z"').stdout.strip() - tzinfo = datetime.strptime(timezone_offset, '%z').tzinfo - result = rhel_contenthost.execute('yum history | grep U') - assert result.status == 0 - assert datetime.now(tzinfo).strftime('%Y-%m-%d') in result.stdout - # Set "Connect to host using IP address" - target_sat.api.Parameter( - host=rhel_contenthost.hostname, - name='remote_execution_connect_by_ip', - parameter_type='boolean', - value='True', - ).create() - # run insights-client via REX - command = "insights-client --status" - invocation_command = target_sat.cli_factory.job_invocation( - { - 'job-template': 'Run Command - Script Default', - 'inputs': f'command={command}', - 'search-query': f"name ~ {rhel_contenthost.hostname}", - } - ) - # results provide all info but job invocation might not be finished yet - result = ( - target_sat.api.JobInvocation() - .search( - query={'search': f'id={invocation_command["id"]} and host={rhel_contenthost.hostname}'} - )[0] - .read() - ) - # make sure that task is finished - task_result = target_sat.wait_for_tasks( - search_query=(f'id = {result.task.id}'), search_rate=2, max_tries=60 - ) - assert task_result[0].result == 'success' - host = ( - target_sat.api.Host() - .search(query={'search': f'name={rhel_contenthost.hostname}'})[0] - .read() - ) - for interface in host.interface: - interface_result = target_sat.api.Interface(host=host.id).search( - query={'search': f'{interface.id}'} - )[0] - # more interfaces can be inside the host - if interface_result.identifier == iface: - assert interface_result.execution - - -@pytest.mark.tier2 -def test_global_registration_form_populate( - module_org, - session, - module_ak_with_cv, - module_lce, - module_promoted_cv, - default_architecture, - default_os, - target_sat, -): - """Host registration form should be populated automatically based on the host-group - - :id: b949e010-36b8-48b8-9907-36138342c72b - - :expectedresults: Some of the fields in the form should be populated based on host-group - e.g. activation key, operating system, life-cycle environment, host parameters for - remote-execution, insights setup. - - :steps: - 1. create and sync repository - 2. create the content view and activation-key - 3. create the host-group with activation key, operating system, host-parameters - 4. Open the global registration form and select the same host-group - 5. check host registration form should be populated automatically based on the host-group - - :BZ: 2056469 - - :CaseAutomation: Automated - """ - hg_name = gen_string('alpha') - iface = gen_string('alpha') - group_params = {'name': 'host_packages', 'value': constants.FAKE_0_CUSTOM_PACKAGE} - target_sat.api.HostGroup( - name=hg_name, - organization=[module_org], - lifecycle_environment=module_lce, - architecture=default_architecture, - operatingsystem=default_os, - content_view=module_promoted_cv, - group_parameters_attributes=[group_params], - ).create() - with session: - session.hostgroup.update( - hg_name, - { - 'activation_keys.activation_keys': module_ak_with_cv.name, - }, - ) - cmd = session.host.get_register_command( - { - 'general.host_group': hg_name, - 'advanced.rex_interface': iface, - 'general.insecure': True, - }, - full_read=True, - ) - - assert hg_name in cmd['general']['host_group'] - assert module_ak_with_cv.name in cmd['general']['activation_key_helper'] - assert module_lce.name in cmd['advanced']['life_cycle_env_helper'] - assert constants.FAKE_0_CUSTOM_PACKAGE in cmd['advanced']['install_packages_helper'] - - @pytest.mark.tier2 def test_global_registration_with_capsule_host( session, @@ -1332,213 +1077,6 @@ def test_global_registration_with_capsule_host( assert module_org.name in result.stdout -@pytest.mark.tier2 -@pytest.mark.usefixtures('enable_capsule_for_registration') -@pytest.mark.no_containers -def test_global_registration_with_gpg_repo_and_default_package( - session, module_activation_key, default_os, default_smart_proxy, rhel8_contenthost -): - """Host registration form produces a correct registration command and host is - registered successfully with gpg repo enabled and have default package - installed. - - :id: b5738b20-e281-4d0b-ac78-dcdc177b8c9f - - :expectedresults: Host is successfully registered, gpg repo is enabled - and default package is installed. - - :steps: - 1. create and sync repository - 2. create the content view and activation-key - 3. update the 'host_packages' parameter in organization with package name e.g. vim - 4. open the global registration form and update the gpg repo and key - 5. check host is registered successfully with installed same package - 6. check gpg repo is exist in registered host - - :parametrized: yes - """ - client = rhel8_contenthost - repo_name = 'foreman_register' - repo_url = settings.repos.gr_yum_repo.url - repo_gpg_url = settings.repos.gr_yum_repo.gpg_url - with session: - cmd = session.host.get_register_command( - { - 'general.operating_system': default_os.title, - 'general.capsule': default_smart_proxy.name, - 'general.activation_keys': module_activation_key.name, - 'general.insecure': True, - 'advanced.force': True, - 'advanced.install_packages': 'mlocate vim', - 'advanced.repository': repo_url, - 'advanced.repository_gpg_key_url': repo_gpg_url, - } - ) - - # rhel repo required for insights client installation, - # syncing it to the satellite would take too long - rhelver = client.os_version.major - if rhelver > 7: - repos = {f'rhel{rhelver}_os': settings.repos[f'rhel{rhelver}_os']['baseos']} - else: - repos = { - 'rhel7_os': settings.repos['rhel7_os'], - 'rhel7_extras': settings.repos['rhel7_extras'], - } - client.create_custom_repos(**repos) - # run curl - result = client.execute(cmd) - assert result.status == 0 - result = client.execute('yum list installed | grep mlocate') - assert result.status == 0 - assert 'mlocate' in result.stdout - result = client.execute(f'yum -v repolist {repo_name}') - assert result.status == 0 - assert repo_url in result.stdout - - -@pytest.mark.tier2 -@pytest.mark.rhel_ver_match('[^6].*') -def test_global_registration_upgrade_subscription_manager( - session, module_activation_key, module_os, rhel_contenthost -): - """Host registration form produces a correct registration command and - subscription-manager can be updated from a custom repository before - registration is completed. - - :id: b7a44f32-90b2-4fd6-b65b-5a3d2a5c5deb - - :customerscenario: true - - :expectedresults: Host is successfully registered, repo is enabled - on advanced tab and subscription-manager is updated. - - :steps: - 1. Create activation-key - 2. Open the global registration form, add repo and activation key - 3. Add 'subscription-manager' to install packages field - 4. Check subscription-manager was installed from repo_name - - :parametrized: yes - - :BZ: 1923320 - """ - client = rhel_contenthost - repo_name = 'foreman_register' - rhel_ver = rhel_contenthost.os_version.major - repo_url = settings.repos.get(f'rhel{rhel_ver}_os') - if isinstance(repo_url, dict): - repo_url = repo_url['baseos'] - # Ensure subs-man is installed from repo_name by removing existing package. - result = client.execute('rpm --erase --nodeps subscription-manager') - assert result.status == 0 - with session: - cmd = session.host.get_register_command( - { - 'general.operating_system': module_os.title, - 'general.activation_keys': module_activation_key.name, - 'general.insecure': True, - 'advanced.force': True, - 'advanced.install_packages': 'subscription-manager', - 'advanced.repository': repo_url, - } - ) - - # run curl - result = client.execute(cmd) - assert result.status == 0 - result = client.execute('yum repolist') - assert repo_name in result.stdout - assert result.status == 0 - - -@pytest.mark.tier3 -@pytest.mark.usefixtures('enable_capsule_for_registration') -@pytest.mark.rhel_ver_match('[^6].*') -def test_global_re_registration_host_with_force_ignore_error_options( - session, module_activation_key, default_os, default_smart_proxy, rhel_contenthost -): - """If the ignore_error and force checkbox is checked then registered host can - get re-registered without any error. - - :id: 8f0ecc13-5d18-4adb-acf5-3f3276dccbb7 - - :expectedresults: Verify the force and ignore checkbox options - - :steps: - 1. create and sync repository - 2. create the content view and activation-key - 3. open the global registration form and select --force and --Ignore Errors option - 4. registered the host with generated curl command - 5. re-register the same host again and check it is getting registered - - :parametrized: yes - """ - client = rhel_contenthost - with session: - cmd = session.host.get_register_command( - { - 'general.operating_system': default_os.title, - 'general.capsule': default_smart_proxy.name, - 'general.activation_keys': module_activation_key.name, - 'general.insecure': True, - 'advanced.force': True, - 'advanced.ignore_error': True, - } - ) - result = client.execute(cmd) - assert result.status == 0 - result = client.execute('subscription-manager identity') - assert result.status == 0 - # rerun the register command - result = client.execute(cmd) - assert result.status == 0 - result = client.execute('subscription-manager identity') - assert result.status == 0 - - -@pytest.mark.tier2 -@pytest.mark.usefixtures('enable_capsule_for_registration') -def test_global_registration_token_restriction( - session, module_activation_key, rhel8_contenthost, default_os, default_smart_proxy, target_sat -): - """Global registration token should be only used for registration call, it - should be restricted for any other api calls. - - :id: 4528b5c6-0a6d-40cd-857a-68b76db2179b - - :expectedresults: global registration token should be restricted for any api calls - other than the registration - - :steps: - 1. open the global registration form and generate the curl token - 2. use that curl token to execute other api calls e.g. GET /hosts, /users - - :parametrized: yes - """ - client = rhel8_contenthost - with session: - cmd = session.host.get_register_command( - { - 'general.operating_system': default_os.title, - 'general.capsule': default_smart_proxy.name, - 'general.activation_keys': module_activation_key.name, - 'general.insecure': True, - } - ) - - pattern = re.compile("Authorization.*(?=')") - auth_header = re.search(pattern, cmd).group() - - # build curl - curl_users = f'curl -X GET -k -H {auth_header} -i {target_sat.url}/api/users/' - curl_hosts = f'curl -X GET -k -H {auth_header} -i {target_sat.url}/api/hosts/' - for curl_cmd in (curl_users, curl_hosts): - result = client.execute(curl_cmd) - assert result.status == 0 - assert 'Unable to authenticate user' in result.stdout - - @pytest.mark.tier4 @pytest.mark.upgrade def test_positive_bulk_delete_host(session, smart_proxy_location, target_sat, function_org): @@ -1661,6 +1199,7 @@ def test_positive_manage_table_columns(session, current_sat_org, current_sat_loc 'Last report': False, 'Comment': False, 'Installable updates': True, + 'RHEL Lifecycle status': False, 'Registered': True, 'Last checkin': True, 'IPv4': True, @@ -1753,9 +1292,7 @@ def test_positive_update_delete_package( """ client = rhel_contenthost client.add_rex_key(target_sat) - module_repos_collection_with_setup.setup_virtual_machine( - client, target_sat, install_katello_agent=False - ) + module_repos_collection_with_setup.setup_virtual_machine(client, target_sat) with session: session.location.select(loc_name=DEFAULT_LOC) if not is_open('BZ:2132680'): @@ -1873,9 +1410,7 @@ def test_positive_apply_erratum( # install package client = rhel_contenthost client.add_rex_key(target_sat) - module_repos_collection_with_setup.setup_virtual_machine( - client, target_sat, install_katello_agent=False - ) + module_repos_collection_with_setup.setup_virtual_machine(client, target_sat) errata_id = settings.repos.yum_3.errata[25] client.run(f'yum install -y {FAKE_7_CUSTOM_PACKAGE}') result = client.run(f'rpm -q {FAKE_7_CUSTOM_PACKAGE}') @@ -1955,9 +1490,7 @@ def test_positive_crud_module_streams( module_name = 'duck' client = rhel_contenthost client.add_rex_key(target_sat) - module_repos_collection_with_setup.setup_virtual_machine( - client, target_sat, install_katello_agent=False - ) + module_repos_collection_with_setup.setup_virtual_machine(client, target_sat) with session: session.location.select(loc_name=DEFAULT_LOC) streams = session.host_new.get_module_streams(client.hostname, module_name) @@ -2293,59 +1826,6 @@ def test_positive_tracer_enable_reload(tracer_install_host, target_sat): assert tracer['title'] == "No applications to restart" -def test_positive_host_registration_with_non_admin_user( - test_name, - module_sca_manifest_org, - module_location, - target_sat, - rhel8_contenthost, - module_activation_key, -): - """Register hosts from a non-admin user with only register_hosts, edit_hosts - and view_organization permissions - - :id: 35458bbc-4556-41b9-ba26-ae0b15179731 - - :expectedresults: User with register hosts permission able to register hosts. - """ - user_password = gen_string('alpha') - org = module_sca_manifest_org - role = target_sat.api.Role(organization=[org]).create() - - user_permissions = { - 'Organization': ['view_organizations'], - 'Host': ['view_hosts'], - } - target_sat.api_factory.create_role_permissions(role, user_permissions) - user = target_sat.api.User( - role=[role], - admin=False, - password=user_password, - organization=[org], - location=[module_location], - default_organization=org, - default_location=module_location, - ).create() - role = target_sat.cli.Role.info({'name': 'Register hosts'}) - target_sat.cli.User.add_role({'id': user.id, 'role-id': role['id']}) - - with target_sat.ui_session(test_name, user=user.login, password=user_password) as session: - - cmd = session.host_new.get_register_command( - { - 'general.insecure': True, - 'general.activation_keys': module_activation_key.name, - } - ) - - result = rhel8_contenthost.execute(cmd) - assert result.status == 0, f'Failed to register host: {result.stderr}' - - # Verify server.hostname and server.port from subscription-manager config - assert target_sat.hostname == rhel8_contenthost.subscription_config['server']['hostname'] - assert constants.CLIENT_PORT == rhel8_contenthost.subscription_config['server']['port'] - - @pytest.mark.tier2 def test_all_hosts_delete(target_sat, function_org, function_location, new_host_ui): """Create a host and delete it through All Hosts UI diff --git a/tests/foreman/ui/test_registration.py b/tests/foreman/ui/test_registration.py index 38f48b8c34b..74860dea043 100644 --- a/tests/foreman/ui/test_registration.py +++ b/tests/foreman/ui/test_registration.py @@ -10,10 +10,17 @@ :Team: Rocket """ +from datetime import datetime +import re + from airgun.exceptions import DisabledWidgetError +from airgun.session import Session +from fauxfactory import gen_string import pytest -from robottelo.utils.datafactory import gen_string +from robottelo import constants +from robottelo.config import settings +from robottelo.constants import FAKE_1_CUSTOM_PACKAGE, FAKE_7_CUSTOM_PACKAGE pytestmark = pytest.mark.tier1 @@ -32,18 +39,17 @@ def test_positive_verify_default_values_for_global_registration( :steps: 1. Check for the default values in the global registration template """ - ak = module_target_sat.cli_factory.make_activation_key( + module_target_sat.cli_factory.make_activation_key( {'organization-id': default_org.id, 'name': gen_string('alpha')} ) with module_target_sat.ui_session() as session: cmd = session.host.get_register_command( - {'general.activation_keys': ak.name}, full_read=True, ) assert cmd['general']['organization'] == 'Default Organization' assert cmd['general']['location'] == 'Default Location' assert cmd['general']['capsule'] == 'Nothing to select.' - assert cmd['general']['activation_keys'][0] == ak.name + assert cmd['general']['operating_system'] == '' assert cmd['general']['host_group'] == 'Nothing to select.' assert cmd['general']['insecure'] is False assert cmd['advanced']['setup_rex'] == 'Inherit from host parameter (yes)' @@ -58,7 +64,7 @@ def test_positive_verify_default_values_for_global_registration( @pytest.mark.tier2 def test_positive_org_loc_change_for_registration( module_activation_key, - module_org, + module_entitlement_manifest_org, module_location, target_sat, ): @@ -70,19 +76,16 @@ def test_positive_org_loc_change_for_registration( :CaseImportance: Medium """ + org = module_entitlement_manifest_org new_org = target_sat.api.Organization().create() new_loc = target_sat.api.Location().create() - new_ak = target_sat.api.ActivationKey(organization=new_org).create() + target_sat.api.ActivationKey(organization=new_org).create() with target_sat.ui_session() as session: - session.organization.select(org_name=module_org.name) + session.organization.select(org_name=org.name) session.location.select(loc_name=module_location.name) - cmd = session.host.get_register_command( - { - 'general.activation_keys': module_activation_key.name, - } - ) + cmd = session.host.get_register_command() expected_pairs = [ - f'organization_id={module_org.id}', + f'organization_id={org.id}', f'location_id={module_location.id}', ] for pair in expected_pairs: @@ -90,11 +93,7 @@ def test_positive_org_loc_change_for_registration( # changing the org and loc to check if correct org and loc is updated on the registration command session.organization.select(org_name=new_org.name) session.location.select(loc_name=new_loc.name) - cmd = session.host.get_register_command( - { - 'general.activation_keys': new_ak.name, - } - ) + cmd = session.host.get_register_command() expected_pairs = [ f'organization_id={new_org.id}', f'location_id={new_loc.id}', @@ -105,7 +104,7 @@ def test_positive_org_loc_change_for_registration( def test_negative_global_registration_without_ak( module_target_sat, - module_org, + module_entitlement_manifest_org, module_location, ): """Attempt to register a host without ActivationKey @@ -114,9 +113,489 @@ def test_negative_global_registration_without_ak( :expectedresults: Generate command is disabled without ActivationKey """ + org = module_entitlement_manifest_org with module_target_sat.ui_session() as session: - session.organization.select(org_name=module_org.name) + session.organization.select(org_name=org.name) session.location.select(loc_name=module_location.name) with pytest.raises(DisabledWidgetError) as context: session.host.get_register_command() assert 'Generate registration command button is disabled' in str(context.value) + + +@pytest.mark.e2e +@pytest.mark.no_containers +@pytest.mark.tier3 +@pytest.mark.rhel_ver_match('[^6]') +def test_positive_global_registration_end_to_end( + session, + module_activation_key, + module_org, + smart_proxy_location, + default_os, + default_smart_proxy, + rhel_contenthost, + target_sat, +): + """Host registration form produces a correct registration command and host is + registered successfully with it, remote execution and insights are set up + + :id: a02658bf-097e-47a8-8472-5d9f649ba07a + + :customerscenario: true + + :BZ: 1993874 + + :expectedresults: Host is successfully registered, remote execution and insights + client work out of the box + + :parametrized: yes + """ + # make sure global parameters for rex and insights are set to true + insights_cp = ( + target_sat.api.CommonParameter() + .search(query={'search': 'name=host_registration_insights'})[0] + .read() + ) + rex_cp = ( + target_sat.api.CommonParameter() + .search(query={'search': 'name=host_registration_remote_execution'})[0] + .read() + ) + + if not insights_cp.value: + target_sat.api.CommonParameter(id=insights_cp.id, value=1).update(['value']) + if not rex_cp.value: + target_sat.api.CommonParameter(id=rex_cp.id, value=1).update(['value']) + + # rex interface + iface = 'eth0' + # fill in the global registration form + with session: + cmd = session.host.get_register_command( + { + 'general.operating_system': default_os.title, + 'general.activation_keys': module_activation_key.name, + 'advanced.update_packages': True, + 'advanced.rex_interface': iface, + 'general.insecure': True, + } + ) + expected_pairs = [ + f'organization_id={module_org.id}', + f'activation_keys={module_activation_key.name}', + f'location_id={smart_proxy_location.id}', + f'operatingsystem_id={default_os.id}', + f'{default_smart_proxy.name}', + 'insecure', + 'update_packages=true', + ] + for pair in expected_pairs: + assert pair in cmd + # rhel repo required for insights client installation, + # syncing it to the satellite would take too long + rhelver = rhel_contenthost.os_version.major + if rhelver > 7: + rhel_contenthost.create_custom_repos(**settings.repos[f'rhel{rhelver}_os']) + else: + rhel_contenthost.create_custom_repos( + **{f'rhel{rhelver}_os': settings.repos[f'rhel{rhelver}_os']} + ) + # make sure there will be package availabe for update + if rhel_contenthost.os_version.major == '6': + package = FAKE_1_CUSTOM_PACKAGE + repo_url = settings.repos.yum_1['url'] + else: + package = FAKE_7_CUSTOM_PACKAGE + repo_url = settings.repos.yum_3['url'] + rhel_contenthost.create_custom_repos(fake_yum=repo_url) + rhel_contenthost.execute(f"yum install -y {package}") + # run curl + result = rhel_contenthost.execute(cmd) + assert result.status == 0 + result = rhel_contenthost.execute('subscription-manager identity') + assert result.status == 0 + # Assert that a yum update was made this day ("Update" or "I, U" in history) + timezone_offset = rhel_contenthost.execute('date +"%:z"').stdout.strip() + tzinfo = datetime.strptime(timezone_offset, '%z').tzinfo + result = rhel_contenthost.execute('yum history | grep U') + assert result.status == 0 + assert datetime.now(tzinfo).strftime('%Y-%m-%d') in result.stdout + # Set "Connect to host using IP address" + target_sat.api.Parameter( + host=rhel_contenthost.hostname, + name='remote_execution_connect_by_ip', + parameter_type='boolean', + value='True', + ).create() + # run insights-client via REX + command = "insights-client --status" + invocation_command = target_sat.cli_factory.job_invocation( + { + 'job-template': 'Run Command - Script Default', + 'inputs': f'command={command}', + 'search-query': f"name ~ {rhel_contenthost.hostname}", + } + ) + # results provide all info but job invocation might not be finished yet + result = ( + target_sat.api.JobInvocation() + .search( + query={'search': f'id={invocation_command["id"]} and host={rhel_contenthost.hostname}'} + )[0] + .read() + ) + # make sure that task is finished + task_result = target_sat.wait_for_tasks( + search_query=(f'id = {result.task.id}'), search_rate=2, max_tries=60 + ) + assert task_result[0].result == 'success' + host = ( + target_sat.api.Host() + .search(query={'search': f'name={rhel_contenthost.hostname}'})[0] + .read() + ) + for interface in host.interface: + interface_result = target_sat.api.Interface(host=host.id).search( + query={'search': f'{interface.id}'} + )[0] + # more interfaces can be inside the host + if interface_result.identifier == iface: + assert interface_result.execution + + +@pytest.mark.tier2 +def test_global_registration_form_populate( + module_org, + session, + module_ak_with_cv, + module_lce, + module_promoted_cv, + default_architecture, + default_os, + target_sat, +): + """Host registration form should be populated automatically based on the host-group + + :id: b949e010-36b8-48b8-9907-36138342c72b + + :expectedresults: Some of the fields in the form should be populated based on host-group + e.g. activation key, operating system, life-cycle environment, host parameters for + remote-execution, insights setup. + + :steps: + 1. create and sync repository + 2. create the content view and activation-key + 3. create the host-group with activation key, operating system, host-parameters + 4. Open the global registration form and select the same host-group + 5. check host registration form should be populated automatically based on the host-group + + :BZ: 2056469, 1994654 + + :customerscenario: true + """ + hg_name = gen_string('alpha') + iface = gen_string('alpha') + group_params = {'name': 'host_packages', 'value': constants.FAKE_0_CUSTOM_PACKAGE} + target_sat.api.HostGroup( + name=hg_name, + organization=[module_org], + lifecycle_environment=module_lce, + architecture=default_architecture, + operatingsystem=default_os, + content_view=module_promoted_cv, + group_parameters_attributes=[group_params], + ).create() + new_org = target_sat.api.Organization().create() + new_ak = target_sat.api.ActivationKey(organization=new_org).create() + with session: + session.hostgroup.update( + hg_name, + { + 'activation_keys.activation_keys': module_ak_with_cv.name, + }, + ) + cmd = session.host.get_register_command( + { + 'general.host_group': hg_name, + 'advanced.rex_interface': iface, + 'general.insecure': True, + }, + full_read=True, + ) + assert hg_name in cmd['general']['host_group'] + assert module_ak_with_cv.name in cmd['general']['activation_key_helper'] + assert constants.FAKE_0_CUSTOM_PACKAGE in cmd['advanced']['install_packages_helper'] + + session.organization.select(org_name=new_org.name) + cmd = session.host.get_register_command( + { + 'general.organization': new_org.name, + 'general.operating_system': default_os.title, + 'general.insecure': True, + }, + full_read=True, + ) + assert new_org.name in cmd['general']['organization'] + assert new_ak.name in cmd['general']['activation_keys'] + + +@pytest.mark.tier2 +@pytest.mark.usefixtures('enable_capsule_for_registration') +@pytest.mark.no_containers +def test_global_registration_with_gpg_repo_and_default_package( + session, module_activation_key, default_os, default_smart_proxy, rhel8_contenthost +): + """Host registration form produces a correct registration command and host is + registered successfully with gpg repo enabled and have default package + installed. + + :id: b5738b20-e281-4d0b-ac78-dcdc177b8c9f + + :expectedresults: Host is successfully registered, gpg repo is enabled + and default package is installed. + + :steps: + 1. create and sync repository + 2. create the content view and activation-key + 3. update the 'host_packages' parameter in organization with package name e.g. vim + 4. open the global registration form and update the gpg repo and key + 5. check host is registered successfully with installed same package + 6. check gpg repo is exist in registered host + + :parametrized: yes + """ + client = rhel8_contenthost + repo_name = 'foreman_register' + repo_url = settings.repos.gr_yum_repo.url + repo_gpg_url = settings.repos.gr_yum_repo.gpg_url + with session: + cmd = session.host.get_register_command( + { + 'general.operating_system': default_os.title, + 'general.capsule': default_smart_proxy.name, + 'general.activation_keys': module_activation_key.name, + 'general.insecure': True, + 'advanced.force': True, + 'advanced.install_packages': 'mlocate vim', + 'advanced.repository': repo_url, + 'advanced.repository_gpg_key_url': repo_gpg_url, + } + ) + + # rhel repo required for insights client installation, + # syncing it to the satellite would take too long + rhelver = client.os_version.major + if rhelver > 7: + repos = {f'rhel{rhelver}_os': settings.repos[f'rhel{rhelver}_os']['baseos']} + else: + repos = { + 'rhel7_os': settings.repos['rhel7_os'], + 'rhel7_extras': settings.repos['rhel7_extras'], + } + client.create_custom_repos(**repos) + # run curl + result = client.execute(cmd) + assert result.status == 0 + result = client.execute('yum list installed | grep mlocate') + assert result.status == 0 + assert 'mlocate' in result.stdout + result = client.execute(f'yum -v repolist {repo_name}') + assert result.status == 0 + assert repo_url in result.stdout + + +@pytest.mark.tier3 +@pytest.mark.usefixtures('enable_capsule_for_registration') +def test_global_re_registration_host_with_force_ignore_error_options( + session, module_activation_key, default_os, default_smart_proxy, rhel7_contenthost +): + """If the ignore_error and force checkbox is checked then registered host can + get re-registered without any error. + + :id: 8f0ecc13-5d18-4adb-acf5-3f3276dccbb7 + + :expectedresults: Verify the force and ignore checkbox options + + :steps: + 1. create and sync repository + 2. create the content view and activation-key + 3. open the global registration form and select --force and --Ignore Errors option + 4. registered the host with generated curl command + 5. re-register the same host again and check it is getting registered + + :parametrized: yes + """ + client = rhel7_contenthost + with session: + cmd = session.host.get_register_command( + { + 'general.operating_system': default_os.title, + 'general.capsule': default_smart_proxy.name, + 'general.activation_keys': module_activation_key.name, + 'general.insecure': True, + 'advanced.force': True, + 'advanced.ignore_error': True, + } + ) + client.execute(cmd) + result = client.execute('subscription-manager identity') + assert result.status == 0 + # rerun the register command + client.execute(cmd) + result = client.execute('subscription-manager identity') + assert result.status == 0 + + +@pytest.mark.tier2 +@pytest.mark.usefixtures('enable_capsule_for_registration') +def test_global_registration_token_restriction( + session, module_activation_key, rhel8_contenthost, default_os, default_smart_proxy, target_sat +): + """Global registration token should be only used for registration call, it + should be restricted for any other api calls. + + :id: 4528b5c6-0a6d-40cd-857a-68b76db2179b + + :expectedresults: global registration token should be restricted for any api calls + other than the registration + + :steps: + 1. open the global registration form and generate the curl token + 2. use that curl token to execute other api calls e.g. GET /hosts, /users + + :parametrized: yes + """ + client = rhel8_contenthost + with session: + cmd = session.host.get_register_command( + { + 'general.operating_system': default_os.title, + 'general.capsule': default_smart_proxy.name, + 'general.activation_keys': module_activation_key.name, + 'general.insecure': True, + } + ) + + pattern = re.compile("Authorization.*(?=')") + auth_header = re.search(pattern, cmd).group() + + # build curl + curl_users = f'curl -X GET -k -H {auth_header} -i {target_sat.url}/api/users/' + curl_hosts = f'curl -X GET -k -H {auth_header} -i {target_sat.url}/api/hosts/' + for curl_cmd in (curl_users, curl_hosts): + result = client.execute(curl_cmd) + assert result.status == 0 + assert 'Unable to authenticate user' in result.stdout + + +def test_positive_host_registration_with_non_admin_user( + test_name, + module_entitlement_manifest_org, + module_location, + target_sat, + rhel8_contenthost, + module_activation_key, +): + """Register hosts from a non-admin user with only register_hosts, edit_hosts + and view_organization permissions + + :id: 35458bbc-4556-41b9-ba26-ae0b15179731 + + :expectedresults: User with register hosts permission able to register hosts. + """ + user_password = gen_string('alpha') + org = module_entitlement_manifest_org + role = target_sat.api.Role(organization=[org]).create() + + user_permissions = { + 'Organization': ['view_organizations'], + 'Host': ['view_hosts'], + } + target_sat.api_factory.create_role_permissions(role, user_permissions) + user = target_sat.api.User( + role=[role], + admin=False, + password=user_password, + organization=[org], + location=[module_location], + default_organization=org, + default_location=module_location, + ).create() + role = target_sat.cli.Role.info({'name': 'Register hosts'}) + target_sat.cli.User.add_role({'id': user.id, 'role-id': role['id']}) + + with Session(test_name, user=user.login, password=user_password) as session: + + cmd = session.host_new.get_register_command( + { + 'general.insecure': True, + 'general.activation_keys': module_activation_key.name, + } + ) + + result = rhel8_contenthost.execute(cmd) + assert result.status == 0, f'Failed to register host: {result.stderr}' + + # Verify server.hostname and server.port from subscription-manager config + assert target_sat.hostname == rhel8_contenthost.subscription_config['server']['hostname'] + assert constants.CLIENT_PORT == rhel8_contenthost.subscription_config['server']['port'] + + +@pytest.mark.tier2 +def test_positive_global_registration_form( + session, module_activation_key, module_org, smart_proxy_location, default_os, target_sat +): + """Host registration form produces a correct curl command for various inputs + + :id: f81c2ec4-85b1-4372-8e63-464ddbf70296 + + :customerscenario: true + + :expectedresults: The curl command contains all required parameters + """ + # rex and insights parameters are only specified in curl when differing from + # inerited parameters + result = ( + target_sat.api.CommonParameter() + .search(query={'search': 'name=host_registration_remote_execution'})[0] + .read() + ) + rex_value = not result.value + result = ( + target_sat.api.CommonParameter() + .search(query={'search': 'name=host_registration_insights'})[0] + .read() + ) + insights_value = not result.value + hostgroup = target_sat.api.HostGroup( + organization=[module_org], location=[smart_proxy_location] + ).create() + iface = 'eth0' + with session: + cmd = session.host.get_register_command( + { + 'advanced.setup_insights': 'Yes (override)' if insights_value else 'No (override)', + 'advanced.setup_rex': 'Yes (override)' if rex_value else 'No (override)', + 'general.insecure': True, + 'general.host_group': hostgroup.name, + 'general.operating_system': default_os.title, + 'general.activation_keys': module_activation_key.name, + 'advanced.update_packages': True, + 'advanced.rex_interface': iface, + } + ) + expected_pairs = [ + f'organization_id={module_org.id}', + f'activation_keys={module_activation_key.name}', + f'hostgroup_id={hostgroup.id}', + f'location_id={smart_proxy_location.id}', + f'operatingsystem_id={default_os.id}', + f'remote_execution_interface={iface}', + f'setup_insights={"true" if insights_value else "false"}', + f'setup_remote_execution={"true" if rex_value else "false"}', + f'{target_sat.hostname}', + 'insecure', + 'update_packages=true', + ] + for pair in expected_pairs: + assert pair in cmd From ff1ab8fc7f3513d012e2f3a00115ba0dd3f6857b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 6 Feb 2024 07:03:26 -0500 Subject: [PATCH 478/586] [6.14.z] job invocation with credentials added (#13993) job invocation with credentials added (#13987) (cherry picked from commit 3d25a46ec3e6b5655b80eb56dcabf3338bdbc98b) Co-authored-by: Peter Ondrejka --- robottelo/host_helpers/cli_factory.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/robottelo/host_helpers/cli_factory.py b/robottelo/host_helpers/cli_factory.py index 0703740760b..0db645d04f6 100644 --- a/robottelo/host_helpers/cli_factory.py +++ b/robottelo/host_helpers/cli_factory.py @@ -153,7 +153,10 @@ def create_object(cli_object, options, values=None, credentials=None): 'host_collection': { 'name': gen_alpha, }, - 'job_invocation': {}, + 'job_invocation': {'_redirect': 'job_invocation_with_credentials'}, + 'job_invocation_with_credentials': { + '_entity_cls': 'JobInvocation', + }, 'job_template': { 'job-category': 'Miscellaneous', 'provider-type': 'SSH', From 972d2ebb7a9cab0b7db96a06c768f02b397bbb19 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:55:57 -0500 Subject: [PATCH 479/586] [6.14.z] Modify VMware tests to run on VMware8 (#13998) Modify VMware tests to run on VMware8 (#13877) Add support to run VMware tests on VMware8 Signed-off-by: Shubham Ganar (cherry picked from commit e8225923cc530951403c6b05406831439604e067) Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> --- conf/vmware.yaml.template | 15 ++++--- pytest_fixtures/component/provision_vmware.py | 13 +++++- .../cli/test_computeresource_vmware.py | 11 +++-- .../foreman/ui/test_computeresource_vmware.py | 43 +++++++++++-------- 4 files changed, 53 insertions(+), 29 deletions(-) diff --git a/conf/vmware.yaml.template b/conf/vmware.yaml.template index 27cbeadf69f..55a8b7f5e6d 100644 --- a/conf/vmware.yaml.template +++ b/conf/vmware.yaml.template @@ -1,7 +1,16 @@ VMWARE: # VMware to be added as a compute resource # VCENTER: vmware vcenter URL - VCENTER: + VCENTER7: + HOSTNAME: + HYPERVISOR: + # mac_address: Mac address of the vm_name + MAC_ADDRESS: + VCENTER8: + HOSTNAME: + HYPERVISOR: + # mac_address: Mac address of the vm_name + MAC_ADDRESS: # USERNAME: Login for vmware USERNAME: # PASSWORD: Password for vmware @@ -14,10 +23,6 @@ VMWARE: DATASTORE: # VM_NAME: Name of VM to power On/Off & delete VM_NAME: - # MAC_ADDRESS: Mac address of the vm - MAC_ADDRESS: - # HYPERVISOR: IP address or hostname of the hypervisor - HYPERVISOR: # VMware Compute resource image data # IMAGE_OS: Operating system of the image IMAGE_OS: diff --git a/pytest_fixtures/component/provision_vmware.py b/pytest_fixtures/component/provision_vmware.py index 4517e9fc904..542d8c7a017 100644 --- a/pytest_fixtures/component/provision_vmware.py +++ b/pytest_fixtures/component/provision_vmware.py @@ -4,12 +4,21 @@ from robottelo.config import settings +@pytest.fixture +def vmware(request): + versions = { + 'vmware7': settings.vmware.vcenter7, + 'vmware8': settings.vmware.vcenter8, + } + return versions[getattr(request, 'param', 'vmware8')] + + @pytest.fixture(scope='module') -def module_vmware_cr(module_provisioning_sat, module_sca_manifest_org, module_location): +def module_vmware_cr(module_provisioning_sat, module_sca_manifest_org, module_location, vmware): vmware_cr = module_provisioning_sat.sat.api.VMWareComputeResource( name=gen_string('alpha'), provider='Vmware', - url=settings.vmware.vcenter, + url=vmware.hostname, user=settings.vmware.username, password=settings.vmware.password, datacenter=settings.vmware.datacenter, diff --git a/tests/foreman/cli/test_computeresource_vmware.py b/tests/foreman/cli/test_computeresource_vmware.py index 1e9e0f216c7..b72a5fca5c5 100644 --- a/tests/foreman/cli/test_computeresource_vmware.py +++ b/tests/foreman/cli/test_computeresource_vmware.py @@ -22,7 +22,8 @@ @pytest.mark.tier1 @pytest.mark.e2e @pytest.mark.upgrade -def test_positive_vmware_cr_end_to_end(target_sat, module_org, module_location): +@pytest.mark.parametrize('vmware', ['vmware7', 'vmware8'], indirect=True) +def test_positive_vmware_cr_end_to_end(target_sat, module_org, module_location, vmware): """Create, Read, Update and Delete VMware compute resources :id: 96faae3f-bc64-4147-a9fc-09c858e0a68f @@ -43,7 +44,7 @@ def test_positive_vmware_cr_end_to_end(target_sat, module_org, module_location): 'organization-ids': module_org.id, 'location-ids': module_location.id, 'provider': FOREMAN_PROVIDERS['vmware'], - 'server': settings.vmware.vcenter, + 'server': vmware.hostname, 'user': settings.vmware.username, 'password': settings.vmware.password, 'datacenter': settings.vmware.datacenter, @@ -52,7 +53,7 @@ def test_positive_vmware_cr_end_to_end(target_sat, module_org, module_location): assert vmware_cr['name'] == cr_name assert vmware_cr['locations'][0] == module_location.name assert vmware_cr['organizations'][0] == module_org.name - assert vmware_cr['server'] == settings.vmware.vcenter + assert vmware_cr['server'] == vmware.hostname assert vmware_cr['datacenter'] == settings.vmware.datacenter # List target_sat.cli.ComputeResource.list({'search': f'name="{cr_name}"'}) @@ -76,6 +77,7 @@ def test_positive_vmware_cr_end_to_end(target_sat, module_org, module_location): @pytest.mark.e2e @pytest.mark.on_premises_provisioning @pytest.mark.parametrize('setting_update', ['destroy_vm_on_host_delete=True'], indirect=True) +@pytest.mark.parametrize('vmware', ['vmware7', 'vmware8'], indirect=True) @pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) @pytest.mark.parametrize('provision_method', ['build', 'bootdisk']) @pytest.mark.rhel_ver_match('[^6]') @@ -90,6 +92,7 @@ def test_positive_provision_end_to_end( module_vmware_cr, module_vmware_hostgroup, provision_method, + vmware, ): """Provision a host on vmware compute resource with the help of hostgroup. @@ -137,7 +140,7 @@ def test_positive_provision_end_to_end( assert hostname == host['name'] # check if vm is created on vmware vmware = VMWareSystem( - hostname=settings.vmware.vcenter, + hostname=vmware.hostname, username=settings.vmware.username, password=settings.vmware.password, ) diff --git a/tests/foreman/ui/test_computeresource_vmware.py b/tests/foreman/ui/test_computeresource_vmware.py index e85a406e141..833d8100e05 100644 --- a/tests/foreman/ui/test_computeresource_vmware.py +++ b/tests/foreman/ui/test_computeresource_vmware.py @@ -56,7 +56,7 @@ def _get_normalized_size(size): return f'{size} {suffixes[suffix_index]}' -def _get_vmware_datastore_summary_string(data_store_name=settings.vmware.datastore): +def _get_vmware_datastore_summary_string(data_store_name=settings.vmware.datastore, vmware=None): """Return the datastore string summary for data_store_name For "Local-Ironforge" datastore the string looks Like: @@ -64,7 +64,7 @@ def _get_vmware_datastore_summary_string(data_store_name=settings.vmware.datasto "Local-Ironforge (free: 1.66 TB, prov: 2.29 TB, total: 2.72 TB)" """ system = VMWareSystem( - hostname=settings.vmware.vcenter, + hostname=vmware.hostname, username=settings.vmware.username, password=settings.vmware.password, ) @@ -81,7 +81,8 @@ def _get_vmware_datastore_summary_string(data_store_name=settings.vmware.datasto @pytest.mark.tier1 -def test_positive_end_to_end(session, module_org, module_location): +@pytest.mark.parametrize('vmware', ['vmware7', 'vmware8'], indirect=True) +def test_positive_end_to_end(session, module_org, module_location, vmware): """Perform end to end testing for compute resource VMware component. :id: 47fc9e77-5b22-46b4-a76c-3217434fde2f @@ -102,7 +103,7 @@ def test_positive_end_to_end(session, module_org, module_location): 'name': cr_name, 'description': description, 'provider': FOREMAN_PROVIDERS['vmware'], - 'provider_content.vcenter': settings.vmware.vcenter, + 'provider_content.vcenter': vmware.hostname, 'provider_content.user': settings.vmware.username, 'provider_content.password': settings.vmware.password, 'provider_content.datacenter.value': settings.vmware.datacenter, @@ -152,7 +153,8 @@ def test_positive_end_to_end(session, module_org, module_location): @pytest.mark.tier2 -def test_positive_retrieve_virtual_machine_list(session): +@pytest.mark.parametrize('vmware', ['vmware7', 'vmware8'], indirect=True) +def test_positive_retrieve_virtual_machine_list(session, vmware): """List the virtual machine list from vmware compute resource :id: 21ade57a-0caa-4144-9c46-c8e22f33414e @@ -173,7 +175,7 @@ def test_positive_retrieve_virtual_machine_list(session): { 'name': cr_name, 'provider': FOREMAN_PROVIDERS['vmware'], - 'provider_content.vcenter': settings.vmware.vcenter, + 'provider_content.vcenter': vmware.hostname, 'provider_content.user': settings.vmware.username, 'provider_content.password': settings.vmware.password, 'provider_content.datacenter.value': settings.vmware.datacenter, @@ -187,7 +189,8 @@ def test_positive_retrieve_virtual_machine_list(session): @pytest.mark.e2e @pytest.mark.tier2 -def test_positive_image_end_to_end(session, target_sat): +@pytest.mark.parametrize('vmware', ['vmware7', 'vmware8'], indirect=True) +def test_positive_image_end_to_end(session, target_sat, vmware): """Perform end to end testing for compute resource VMware component image. :id: 6b7949ef-c684-40aa-b181-11f8d4cd39c6 @@ -204,7 +207,7 @@ def test_positive_image_end_to_end(session, target_sat): { 'name': cr_name, 'provider': FOREMAN_PROVIDERS['vmware'], - 'provider_content.vcenter': settings.vmware.vcenter, + 'provider_content.vcenter': vmware.hostname, 'provider_content.user': settings.vmware.username, 'provider_content.password': settings.vmware.password, 'provider_content.datacenter.value': settings.vmware.datacenter, @@ -245,7 +248,8 @@ def test_positive_image_end_to_end(session, target_sat): @pytest.mark.tier2 @pytest.mark.run_in_one_thread -def test_positive_resource_vm_power_management(session): +@pytest.mark.parametrize('vmware', ['vmware7', 'vmware8'], indirect=True) +def test_positive_resource_vm_power_management(session, vmware): """Read current VMware Compute Resource virtual machine power status and change it to opposite one @@ -260,7 +264,7 @@ def test_positive_resource_vm_power_management(session): { 'name': cr_name, 'provider': FOREMAN_PROVIDERS['vmware'], - 'provider_content.vcenter': settings.vmware.vcenter, + 'provider_content.vcenter': vmware.hostname, 'provider_content.user': settings.vmware.username, 'provider_content.password': settings.vmware.password, 'provider_content.datacenter.value': settings.vmware.datacenter, @@ -287,7 +291,8 @@ def test_positive_resource_vm_power_management(session): @pytest.mark.tier2 -def test_positive_select_vmware_custom_profile_guest_os_rhel7(session): +@pytest.mark.parametrize('vmware', ['vmware7', 'vmware8'], indirect=True) +def test_positive_select_vmware_custom_profile_guest_os_rhel7(session, vmware): """Select custom default (3-Large) compute profile guest OS RHEL7. :id: 24f7bb5f-2aaf-48cb-9a56-d2d0713dfe3d @@ -316,7 +321,7 @@ def test_positive_select_vmware_custom_profile_guest_os_rhel7(session): { 'name': cr_name, 'provider': FOREMAN_PROVIDERS['vmware'], - 'provider_content.vcenter': settings.vmware.vcenter, + 'provider_content.vcenter': vmware.hostname, 'provider_content.user': settings.vmware.username, 'provider_content.password': settings.vmware.password, 'provider_content.datacenter.value': settings.vmware.datacenter, @@ -331,7 +336,8 @@ def test_positive_select_vmware_custom_profile_guest_os_rhel7(session): @pytest.mark.tier2 -def test_positive_access_vmware_with_custom_profile(session): +@pytest.mark.parametrize('vmware', ['vmware7', 'vmware8'], indirect=True) +def test_positive_access_vmware_with_custom_profile(session, vmware): """Associate custom default (3-Large) compute profile :id: 751ef765-5091-4322-a0d9-0c9c73009cc4 @@ -350,7 +356,7 @@ def test_positive_access_vmware_with_custom_profile(session): with provided values. """ cr_name = gen_string('alpha') - data_store_summary_string = _get_vmware_datastore_summary_string() + data_store_summary_string = _get_vmware_datastore_summary_string(vmware=vmware) cr_profile_data = dict( cpus='2', cores_per_socket='2', @@ -412,7 +418,7 @@ def test_positive_access_vmware_with_custom_profile(session): { 'name': cr_name, 'provider': FOREMAN_PROVIDERS['vmware'], - 'provider_content.vcenter': settings.vmware.vcenter, + 'provider_content.vcenter': vmware.hostname, 'provider_content.user': settings.vmware.username, 'provider_content.password': settings.vmware.password, 'provider_content.datacenter.value': settings.vmware.datacenter, @@ -461,7 +467,8 @@ def test_positive_access_vmware_with_custom_profile(session): @pytest.mark.tier2 -def test_positive_virt_card(session, target_sat, module_location, module_org): +@pytest.mark.parametrize('vmware', ['vmware7', 'vmware8'], indirect=True) +def test_positive_virt_card(session, target_sat, module_location, module_org, vmware): """Check to see that the Virtualization card appears for an imported VM :id: 0502d5a6-64c1-422f-a9ba-ac7c2ee7bad2 @@ -524,7 +531,7 @@ def test_positive_virt_card(session, target_sat, module_location, module_org): { 'name': cr_name, 'provider': FOREMAN_PROVIDERS['vmware'], - 'provider_content.vcenter': settings.vmware.vcenter, + 'provider_content.vcenter': vmware.hostname, 'provider_content.user': settings.vmware.username, 'provider_content.password': settings.vmware.password, 'provider_content.datacenter.value': settings.vmware.datacenter, @@ -562,7 +569,7 @@ def test_positive_virt_card(session, target_sat, module_location, module_org): assert virt_card['cluster'] == settings.vmware.cluster assert virt_card['memory'] == '5 GB' assert 'public_ip_address' in virt_card - assert virt_card['mac_address'] == settings.vmware.mac_address + assert virt_card['mac_address'] == vmware.mac_address assert virt_card['cpus'] == '1' if 'disk_label' in virt_card: assert virt_card['disk_label'] == 'Hard disk 1' From 6f5bfdb962bba44ec2a1defdd2ce0b634d8b1a52 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 7 Feb 2024 06:22:31 -0500 Subject: [PATCH 480/586] [6.14.z] Skip Capsule provisioning test (#14006) Skip Capsule provisioning test (#14005) Signed-off-by: Shubham Ganar (cherry picked from commit 3ae318e5e15b49c1d1a01177d73a851992184a5b) Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> --- tests/foreman/api/test_provisioning.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/foreman/api/test_provisioning.py b/tests/foreman/api/test_provisioning.py index cfacbaaffa6..68c66b886df 100644 --- a/tests/foreman/api/test_provisioning.py +++ b/tests/foreman/api/test_provisioning.py @@ -585,6 +585,7 @@ def test_rhel_pxe_provisioning_fips_enabled( @pytest.mark.e2e @pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) +@pytest.mark.skip(reason='Skipping till we have destructive support') @pytest.mark.on_premises_provisioning @pytest.mark.rhel_ver_match('[^6]') def test_capsule_pxe_provisioning( From 85dc8fece958bbac41f07433e498a333634b787f Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 8 Feb 2024 07:14:43 -0500 Subject: [PATCH 481/586] [6.14.z] added test_positive_host_registration_with_non_admin_user (#14012) --- tests/foreman/api/test_registration.py | 68 +++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/tests/foreman/api/test_registration.py b/tests/foreman/api/test_registration.py index 36dfc59cf47..e364c2accca 100644 --- a/tests/foreman/api/test_registration.py +++ b/tests/foreman/api/test_registration.py @@ -13,12 +13,15 @@ """ import uuid -from fauxfactory import gen_ipaddr, gen_mac +from fauxfactory import gen_ipaddr, gen_mac, gen_string import pytest from requests import HTTPError from robottelo import constants -from robottelo.config import settings +from robottelo.config import ( + settings, + user_nailgun_config, +) pytestmark = pytest.mark.tier1 @@ -263,3 +266,64 @@ def test_negative_capsule_without_registration_enabled( "Proxy lacks one of the following features: 'Registration', 'Templates'" in context.value.response.text ) + + +@pytest.mark.rhel_ver_match('[^6]') +def test_positive_host_registration_with_non_admin_user_with_setup_false( + module_org, + module_location, + module_activation_key, + module_target_sat, + rhel_contenthost, +): + """Verify host registration with non admin user with setup false + + :id: 02bdda6a-010d-4098-a7e0-e4b5e8416ce3 + + :steps: + 1. Sync the content repositories, add and publish it in CV + 2. Create a non-admin user and assign "Register Hosts" role to it. + 3. Create an activation key and assign CV and LCE to it. + 4. Create new user and generate curl command to register host + + :expectedresults: Host registered successfully with all setup options set to 'NO' with non-admin user + + :BZ: 2211484 + + :customerscenario: true + """ + register_host_role = module_target_sat.api.Role().search( + query={'search': 'name="Register hosts"'} + ) + login = gen_string('alphanumeric') + password = gen_string('alphanumeric') + module_target_sat.api.User( + role=register_host_role, + admin=False, + login=login, + password=password, + organization=[module_org], + location=[module_location], + ).create() + user_cfg = user_nailgun_config(login, password) + command = module_target_sat.api.RegistrationCommand( + server_config=user_cfg, + organization=module_org, + activation_keys=[module_activation_key.name], + location=module_location, + setup_insights=False, + setup_remote_execution=False, + setup_remote_execution_pull=False, + update_packages=False, + ).create() + result = rhel_contenthost.execute(command) + assert result.status == 0, f'Failed to register host: {result.stderr}' + + # verify package install for insights-client didn't run when Setup Insights is false + assert 'dnf -y install insights-client' not in result.stdout + # verify package install for foreman_ygg_worker didn't run when REX pull mode is false + assert 'dnf -y install foreman_ygg_worker' not in result.stdout + # verify packages aren't getting updated when Update packages is false + assert '# Updating packages' not in result.stdout + # verify foreman-proxy ssh pubkey isn't present when Setup REX is false + assert rhel_contenthost.execute('cat ~/.ssh/authorized_keys | grep foreman-proxy').status == 1 From 951e7df0ebe22907377aed5201174c8e6074679d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 8 Feb 2024 10:51:08 -0500 Subject: [PATCH 482/586] [6.14.z] Fix scope mismatch issue for VMware tests (#14018) Fix scope mismatch issue for VMware tests (#14010) (cherry picked from commit 13d6b8c036fe27c54cc3b5027644af94e1503863) Co-authored-by: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> --- pytest_fixtures/component/provision_vmware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_fixtures/component/provision_vmware.py b/pytest_fixtures/component/provision_vmware.py index 542d8c7a017..507f180a158 100644 --- a/pytest_fixtures/component/provision_vmware.py +++ b/pytest_fixtures/component/provision_vmware.py @@ -4,7 +4,7 @@ from robottelo.config import settings -@pytest.fixture +@pytest.fixture(scope='module') def vmware(request): versions = { 'vmware7': settings.vmware.vcenter7, From 7847b0af50ca86f5ae4009f1f1fe197af9ff08b4 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 8 Feb 2024 11:51:20 -0500 Subject: [PATCH 483/586] [6.14.z] Coverage:Bug 2158702 (#14014) Coverage:Bug 2158702 (#13735) * Coverage:Bug 2158702 * Update test_esx_sca.py Update the password to use gen_string('alpha') (cherry picked from commit 1c4a1a7d230e78eef70b1f44332117c674421412) Co-authored-by: yanpliu --- tests/foreman/virtwho/ui/test_esx_sca.py | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/foreman/virtwho/ui/test_esx_sca.py b/tests/foreman/virtwho/ui/test_esx_sca.py index c0f09e740eb..a4c45dd4960 100644 --- a/tests/foreman/virtwho/ui/test_esx_sca.py +++ b/tests/foreman/virtwho/ui/test_esx_sca.py @@ -619,3 +619,34 @@ def test_positive_deploy_configure_hypervisor_password_with_special_characters( ) org_session.virtwho_configure.delete(name) assert not org_session.virtwho_configure.search(name) + + @pytest.mark.tier2 + def test_positive_hypervisor_password_option( + self, module_sca_manifest_org, virtwho_config_ui, org_session, form_data_ui + ): + """Verify Hypervisor password. + + :id: 8362955a-4daa-4332-9559-b526d9095a61 + + :expectedresults: + hypervisor_password has been set in virt-who-config-{}.conf + hypervisor_password has been encrypted in satellite WEB UI Edit page + hypervisor_password has been encrypted in satellite WEB UI Details page + + :CaseImportance: Medium + + :BZ: 2256927 + """ + name = form_data_ui['name'] + config_id = get_configure_id(name) + config_command = get_configure_command(config_id, module_sca_manifest_org.name) + deploy_configure_by_command( + config_command, form_data_ui['hypervisor_type'], org=module_sca_manifest_org.label + ) + config_file = get_configure_file(config_id) + assert get_configure_option('encrypted_password', config_file) + res = org_session.virtwho_configure.read_edit(name) + assert 'encrypted-' in res['hypervisor_content']['password'] + org_session.virtwho_configure.edit(name, {'hypervisor_password': gen_string('alpha')}) + results = org_session.virtwho_configure.read(name) + assert 'encrypted_password=$cr_password' in results['deploy']['script'] From e34b11a51376e3ec366c0d357e9e9f90ae2c37e1 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 8 Feb 2024 16:02:33 -0500 Subject: [PATCH 484/586] [6.14.z] Temporarily add docker and paramiko deps (#14022) Temporarily add docker and paramiko deps (#14020) Since we're currently installing broker from github, we can't install the extra deps. Until it is back, we need to explicitly add in docker and its ssh support layer paramiko. (cherry picked from commit 0b08acd739ebd805aaf0f3c6b474655f4219d91e) Co-authored-by: Jake Callahan --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index fcdbb549832..52e45716ac1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,11 +4,13 @@ betelgeuse==1.11.0 broker[docker]==0.4.1 cryptography==42.0.2 deepdiff==6.7.1 +docker==7.0.0 # Temporary until Broker is back on PyPi dynaconf[vault]==3.2.4 fauxfactory==3.1.0 jinja2==3.1.3 manifester==0.0.14 navmazing==1.2.2 +paramiko==3.4.0 # Temporary until Broker is back on PyPi productmd==1.38 pyotp==2.9.0 python-box==7.1.1 From 66d4de705109335cfc61e7d01c14f49dbbd841fd Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Sun, 11 Feb 2024 22:21:30 -0500 Subject: [PATCH 485/586] [6.14.z] Bump pre-commit from 3.6.0 to 3.6.1 (#14032) --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 1dcaeaa560f..16079401797 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -2,7 +2,7 @@ flake8==7.0.0 pytest-cov==4.1.0 redis==5.0.1 -pre-commit==3.6.0 +pre-commit==3.6.1 # For generating documentation. sphinx==7.2.6 From 91540442ed08939dfad7dd18abc5b35395c08a90 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 12 Feb 2024 11:34:31 -0500 Subject: [PATCH 486/586] [6.14.z] Add test coverage for BZ:2192939 (#14045) --- robottelo/hosts.py | 16 +++++---- tests/foreman/cli/test_report.py | 61 +++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/robottelo/hosts.py b/robottelo/hosts.py index e998b0d62f7..a393a7a1705 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -993,7 +993,7 @@ def update_known_hosts(self, ssh_key_name, host, user=None): f'Failed to put hostname in ssh known_hosts files:\n{result.stderr}' ) - def configure_puppet(self, proxy_hostname=None): + def configure_puppet(self, proxy_hostname=None, run_puppet_agent=True): """Configures puppet on the virtual machine/Host. :param proxy_hostname: external capsule hostname :return: None. @@ -1033,12 +1033,14 @@ def configure_puppet(self, proxy_hostname=None): self.execute('/opt/puppetlabs/bin/puppet agent -t') proxy_host = Host(hostname=proxy_hostname) proxy_host.execute(f'puppetserver ca sign --certname {cert_name}') - # This particular puppet run would create the host entity under - # 'All Hosts' and let's redirect stderr to /dev/null as errors at - # this stage can be ignored. - result = self.execute('/opt/puppetlabs/bin/puppet agent -t 2> /dev/null') - if result.status: - raise ContentHostError('Failed to configure puppet on the content host') + + if run_puppet_agent: + # This particular puppet run would create the host entity under + # 'All Hosts' and let's redirect stderr to /dev/null as errors at + # this stage can be ignored. + result = self.execute('/opt/puppetlabs/bin/puppet agent -t 2> /dev/null') + if result.status: + raise ContentHostError('Failed to configure puppet on the content host') def execute_foreman_scap_client(self, policy_id=None): """Executes foreman_scap_client on the vm to create security audit report. diff --git a/tests/foreman/cli/test_report.py b/tests/foreman/cli/test_report.py index b46c3881a15..ea4f14371bb 100644 --- a/tests/foreman/cli/test_report.py +++ b/tests/foreman/cli/test_report.py @@ -14,6 +14,7 @@ import random import pytest +from wait_for import wait_for from robottelo.exceptions import CLIReturnCodeError @@ -82,11 +83,69 @@ def test_positive_install_configure_host( for client, puppet_proxy in zip(content_hosts, puppet_infra_host, strict=True): client.configure_puppet(proxy_hostname=puppet_proxy.hostname) report = session_puppet_enabled_sat.cli.ConfigReport.list( - {'search': f'host~{client.hostname},origin=Puppet'} + {'search': f'host~{client.hostname}'} ) assert len(report) + assert report[0]['origin'] == 'Puppet' facts = session_puppet_enabled_sat.cli.Fact.list({'search': f'host~{client.hostname}'}) assert len(facts) + assert [f for f in facts if f['fact'] == 'puppetmaster_fqdn'][0][ + 'value' + ] == puppet_proxy.hostname session_puppet_enabled_sat.cli.ConfigReport.delete({'id': report[0]['id']}) with pytest.raises(CLIReturnCodeError): session_puppet_enabled_sat.cli.ConfigReport.info({'id': report[0]['id']}) + + +@pytest.mark.e2e +@pytest.mark.rhel_ver_match('[^6]') +def test_positive_run_puppet_agent_generate_report_when_no_message( + session_puppet_enabled_sat, rhel_contenthost +): + """Verify that puppet-agent can be installed from the sat-client repo + and configured to report back to the Satellite, and contains the origin + + :id: 07777fbb-4f2e-4fab-ba5a-2b698f9b9f39 + + :setup: + 1. Satellite and Capsule with enabled puppet plugin. + 2. Blank RHEL content host + + :steps: + 1. Configure puppet on the content host. This creates sat-client repository, + installs puppet-agent, configures it, runs it to create the puppet cert, + signs in on the Satellite side and reruns it. + 2. Assert that Config report created in the Satellite for the content host, + and has Puppet origin + 3. Assert that Facts were reported for the content host. + + :expectedresults: + 1. Configuration passes without errors. + 2. Config report is created and contains Puppet origin + 3. Facts are acquired. + + :customerscenario: true + + :BZ: 2192939, 2257327, 2257314 + :parametrized: yes + """ + sat = session_puppet_enabled_sat + client = rhel_contenthost + client.configure_puppet(proxy_hostname=sat.hostname, run_puppet_agent=False) + # Run either 'puppet agent --detailed-exitcodes' or 'systemctl restart puppet' + # to generate Puppet config report for host without any messages + assert client.execute('/opt/puppetlabs/bin/puppet agent --detailed-exitcodes').status == 0 + wait_for( + lambda: sat.cli.ConfigReport.list({'search': f'host~{client.hostname}'}) != [], + timeout=300, + delay=30, + ) + report = sat.cli.ConfigReport.list({'search': f'host~{client.hostname}'}) + assert len(report) + assert report[0]['origin'] == 'Puppet' + facts = sat.cli.Fact.list({'search': f'host~{client.hostname}'}) + assert len(facts) + assert [f for f in facts if f['fact'] == 'puppetmaster_fqdn'][0]['value'] == sat.hostname + sat.cli.ConfigReport.delete({'id': report[0]['id']}) + with pytest.raises(CLIReturnCodeError): + sat.cli.ConfigReport.info({'id': report[0]['id']}) From a3e0f4d42165863c3f33c9e472a63bf35ff92a04 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 13 Feb 2024 07:40:43 -0500 Subject: [PATCH 487/586] [6.14.z] preserving PRT result as part of github comment & PRT pass/fail label (#14038) preserving PRT result as part of github comment & PRT pass/fail label (#13979) preserving the PRT result as part of github comment & PRT pass/fail label (cherry picked from commit 2d0fed68352771fab15748188e1e7592747e20a0) Co-authored-by: Omkar Khatavkar --- .github/workflows/prt_result.yml | 84 ++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 .github/workflows/prt_result.yml diff --git a/.github/workflows/prt_result.yml b/.github/workflows/prt_result.yml new file mode 100644 index 00000000000..93227fcf272 --- /dev/null +++ b/.github/workflows/prt_result.yml @@ -0,0 +1,84 @@ +### The prt result workflow triggered through dispatch request from CI +name: post-prt-result + +# Run on workflow dispatch from CI +on: + workflow_dispatch: + inputs: + pr_number: + type: string + description: pr number for PRT run + build_number: + type: string + description: build number for PRT run + pytest_result: + type: string + description: pytest summary result line + build_status: + type: string + description: status of jenkins build e.g. success, unstable or error + prt_comment: + type: string + description: prt pytest comment triggered the PRT checks + + +jobs: + post-the-prt-result: + runs-on: ubuntu-latest + + steps: + - name: Add last PRT result into the github comment + id: add-prt-comment + if: ${{ always() && github.event.inputs.pytest_result != '' }} + uses: thollander/actions-comment-pull-request@v2 + with: + message: | + **PRT Result** + ``` + Build Number: ${{ github.event.inputs.build_number }} + Build Status: ${{ github.event.inputs.build_status }} + PRT Comment: ${{ github.event.inputs.prt_comment }} + Test Result : ${{ github.event.inputs.pytest_result }} + ``` + pr_number: ${{ github.event.inputs.pr_number }} + GITHUB_TOKEN: ${{ secrets.CHERRYPICK_PAT }} + + - name: Add the PRT passed/failed labels + id: prt-status + if: ${{ always() && github.event.inputs.build_status != '' }} + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.CHERRYPICK_PAT }} + script: | + const prNumber = ${{ github.event.inputs.pr_number }}; + const buildStatus = "${{ github.event.inputs.build_status }}"; + const labelToAdd = buildStatus === "SUCCESS" ? "PRT-Passed" : "PRT-Failed"; + github.rest.issues.addLabels({ + issue_number: prNumber, + owner: context.repo.owner, + repo: context.repo.repo, + labels: [labelToAdd] + }); + - name: Remove failed label on test pass or vice-versa + if: ${{ always() && github.event.inputs.build_status != '' }} + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.CHERRYPICK_PAT }} + script: | + const prNumber = ${{ github.event.inputs.pr_number }}; + const issue = await github.rest.issues.get({ + owner: context.issue.owner, + repo: context.issue.repo, + issue_number: prNumber, + }); + const buildStatus = "${{ github.event.inputs.build_status }}"; + const labelToRemove = buildStatus === "SUCCESS" ? "PRT-Failed" : "PRT-Passed"; + const labelExists = issue.data.labels.some(({ name }) => name === labelToRemove); + if (labelExists) { + github.rest.issues.removeLabel({ + issue_number: prNumber, + owner: context.repo.owner, + repo: context.repo.repo, + name: [labelToRemove] + }); + } From d6dfb8285c2100c87f855b5875510b381d16c702 Mon Sep 17 00:00:00 2001 From: Shweta Singh Date: Tue, 13 Feb 2024 18:11:51 +0530 Subject: [PATCH 488/586] [6.14.z Cherrypick PR#13450] Move Registration test cases (#14052) --- tests/foreman/ui/test_host.py | 98 -------------------------- tests/foreman/ui/test_registration.py | 99 ++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 99 deletions(-) diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index 0ee25e68234..f5a5f9680af 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -32,7 +32,6 @@ OSCAP_PERIOD, OSCAP_WEEKDAY, PERMISSIONS, - REPO_TYPE, ) from robottelo.utils.datafactory import gen_string from robottelo.utils.issue_handlers import is_open @@ -980,103 +979,6 @@ def test_positive_validate_inherited_cv_lce_ansiblerole(session, target_sat, mod assert host.name in [host.name for host in matching_hosts] -@pytest.mark.tier2 -def test_global_registration_with_capsule_host( - session, - capsule_configured, - rhel8_contenthost, - module_org, - module_location, - module_product, - default_os, - module_lce_library, - target_sat, -): - """Host registration form produces a correct registration command and host is - registered successfully with selected capsule from form. - - :id: 6356c6d0-ee45-4ad7-8a0e-484d3490bc58 - - :expectedresults: Host is successfully registered with capsule host, - remote execution and insights client work out of the box - - :steps: - 1. create and sync repository - 2. create the content view and activation-key - 3. integrate capsule and sync content - 4. open the global registration form and select the same capsule - 5. check host is registered successfully with selected capsule - - :parametrized: yes - - :CaseAutomation: Automated - """ - client = rhel8_contenthost - repo = target_sat.api.Repository( - url=settings.repos.yum_1.url, - content_type=REPO_TYPE['yum'], - product=module_product, - ).create() - # Sync all repositories in the product - module_product.sync() - capsule = target_sat.api.Capsule(id=capsule_configured.nailgun_capsule.id).search( - query={'search': f'name={capsule_configured.hostname}'} - )[0] - module_org = target_sat.api.Organization(id=module_org.id).read() - module_org.smart_proxy.append(capsule) - module_location = target_sat.api.Location(id=module_location.id).read() - module_location.smart_proxy.append(capsule) - module_org.update(['smart_proxy']) - module_location.update(['smart_proxy']) - - # Associate the lifecycle environment with the capsule - capsule.content_add_lifecycle_environment(data={'environment_id': module_lce_library.id}) - result = capsule.content_lifecycle_environments() - # TODO result is not used, please add assert once you fix the test - - # Create a content view with the repository - cv = target_sat.api.ContentView(organization=module_org, repository=[repo]).create() - - # Publish new version of the content view - cv.publish() - cv = cv.read() - - assert len(cv.version) == 1 - - activation_key = target_sat.api.ActivationKey( - content_view=cv, environment=module_lce_library, organization=module_org - ).create() - - # Assert that a task to sync lifecycle environment to the capsule - # is started (or finished already) - sync_status = capsule.content_get_sync() - assert len(sync_status['active_sync_tasks']) >= 1 or sync_status['last_sync_time'] - - # Wait till capsule sync finishes - for task in sync_status['active_sync_tasks']: - target_sat.api.ForemanTask(id=task['id']).poll() - with session: - session.organization.select(org_name=module_org.name) - session.location.select(loc_name=module_location.name) - cmd = session.host.get_register_command( - { - 'general.organization': module_org.name, - 'general.location': module_location.name, - 'general.operating_system': default_os.title, - 'general.capsule': capsule_configured.hostname, - 'general.activation_keys': activation_key.name, - 'general.insecure': True, - } - ) - client.create_custom_repos(rhel7=settings.repos.rhel7_os) - # run curl - client.execute(cmd) - result = client.execute('subscription-manager identity') - assert result.status == 0 - assert module_lce_library.name in result.stdout - assert module_org.name in result.stdout - - @pytest.mark.tier4 @pytest.mark.upgrade def test_positive_bulk_delete_host(session, smart_proxy_location, target_sat, function_org): diff --git a/tests/foreman/ui/test_registration.py b/tests/foreman/ui/test_registration.py index 74860dea043..cb1a79264c2 100644 --- a/tests/foreman/ui/test_registration.py +++ b/tests/foreman/ui/test_registration.py @@ -20,7 +20,7 @@ from robottelo import constants from robottelo.config import settings -from robottelo.constants import FAKE_1_CUSTOM_PACKAGE, FAKE_7_CUSTOM_PACKAGE +from robottelo.constants import FAKE_1_CUSTOM_PACKAGE, FAKE_7_CUSTOM_PACKAGE, REPO_TYPE pytestmark = pytest.mark.tier1 @@ -599,3 +599,100 @@ def test_positive_global_registration_form( ] for pair in expected_pairs: assert pair in cmd + + +@pytest.mark.tier2 +def test_global_registration_with_capsule_host( + session, + capsule_configured, + rhel8_contenthost, + module_org, + module_location, + module_product, + default_os, + module_lce_library, + target_sat, +): + """Host registration form produces a correct registration command and host is + registered successfully with selected capsule from form. + + :id: 6356c6d0-ee45-4ad7-8a0e-484d3490bc58 + + :expectedresults: Host is successfully registered with capsule host, + remote execution and insights client work out of the box + + :steps: + 1. create and sync repository + 2. create the content view and activation-key + 3. integrate capsule and sync content + 4. open the global registration form and select the same capsule + 5. check host is registered successfully with selected capsule + + :parametrized: yes + + :CaseAutomation: Automated + """ + client = rhel8_contenthost + repo = target_sat.api.Repository( + url=settings.repos.yum_1.url, + content_type=REPO_TYPE['yum'], + product=module_product, + ).create() + # Sync all repositories in the product + module_product.sync() + capsule = target_sat.api.Capsule(id=capsule_configured.nailgun_capsule.id).search( + query={'search': f'name={capsule_configured.hostname}'} + )[0] + module_org = target_sat.api.Organization(id=module_org.id).read() + module_org.smart_proxy.append(capsule) + module_location = target_sat.api.Location(id=module_location.id).read() + module_location.smart_proxy.append(capsule) + module_org.update(['smart_proxy']) + module_location.update(['smart_proxy']) + + # Associate the lifecycle environment with the capsule + capsule.content_add_lifecycle_environment(data={'environment_id': module_lce_library.id}) + result = capsule.content_lifecycle_environments() + # TODO result is not used, please add assert once you fix the test + + # Create a content view with the repository + cv = target_sat.api.ContentView(organization=module_org, repository=[repo]).create() + + # Publish new version of the content view + cv.publish() + cv = cv.read() + + assert len(cv.version) == 1 + + activation_key = target_sat.api.ActivationKey( + content_view=cv, environment=module_lce_library, organization=module_org + ).create() + + # Assert that a task to sync lifecycle environment to the capsule + # is started (or finished already) + sync_status = capsule.content_get_sync() + assert len(sync_status['active_sync_tasks']) >= 1 or sync_status['last_sync_time'] + + # Wait till capsule sync finishes + for task in sync_status['active_sync_tasks']: + target_sat.api.ForemanTask(id=task['id']).poll() + with session: + session.organization.select(org_name=module_org.name) + session.location.select(loc_name=module_location.name) + cmd = session.host.get_register_command( + { + 'general.organization': module_org.name, + 'general.location': module_location.name, + 'general.operating_system': default_os.title, + 'general.capsule': capsule_configured.hostname, + 'general.activation_keys': activation_key.name, + 'general.insecure': True, + } + ) + client.create_custom_repos(rhel7=settings.repos.rhel7_os) + # run curl + client.execute(cmd) + result = client.execute('subscription-manager identity') + assert result.status == 0 + assert module_lce_library.name in result.stdout + assert module_org.name in result.stdout From a7cb6c512d669f4da39434be526de14bac13f907 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 13 Feb 2024 07:49:18 -0500 Subject: [PATCH 489/586] [6.14.z] Remove BZ check which is migrated to Jira and Add new BZ check causing selinux denials (#14053) --- tests/foreman/sys/test_pulp3_filesystem.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/foreman/sys/test_pulp3_filesystem.py b/tests/foreman/sys/test_pulp3_filesystem.py index af19af65f92..ac747ca5f10 100644 --- a/tests/foreman/sys/test_pulp3_filesystem.py +++ b/tests/foreman/sys/test_pulp3_filesystem.py @@ -27,15 +27,13 @@ def test_selinux_status(target_sat): :expectedresults: SELinux is enabled and there are no denials - :customerscenario: true - - :BZ: 2131031 + :BZ: 2263294 """ # check SELinux is enabled result = target_sat.execute('getenforce') assert 'Enforcing' in result.stdout # check there are no SELinux denials - if not is_open('BZ:2131031'): + if not is_open('BZ:2263294'): result = target_sat.execute('ausearch --input-logs -m avc -ts today --raw') assert result.status == 1, 'Some SELinux denials were found in journal.' From 3bb688f392285fec9dff004d5d105aec3472698a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 14 Feb 2024 12:24:08 -0500 Subject: [PATCH 490/586] [6.14.z] Move HTTP Proxy tests under correct component (#14081) Move HTTP Proxy tests under correct component (#14074) (cherry picked from commit de025600a2f33e2f5a7f08b12c3f8e60e384e18d) Co-authored-by: vsedmik <46570670+vsedmik@users.noreply.github.com> --- testimony.yaml | 1 + tests/foreman/api/test_http_proxy.py | 2 +- tests/foreman/cli/test_http_proxy.py | 2 +- tests/foreman/ui/test_http_proxy.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/testimony.yaml b/testimony.yaml index 1681b97fe0c..4ae19f8e4cc 100644 --- a/testimony.yaml +++ b/testimony.yaml @@ -56,6 +56,7 @@ CaseComponent: - ForemanMaintain - Hammer - Hammer-Content + - HTTPProxy - HostCollections - HostForm - HostGroup diff --git a/tests/foreman/api/test_http_proxy.py b/tests/foreman/api/test_http_proxy.py index 0f8f1cab5c5..4fff1c3bc5f 100644 --- a/tests/foreman/api/test_http_proxy.py +++ b/tests/foreman/api/test_http_proxy.py @@ -2,7 +2,7 @@ :Requirement: HttpProxy -:CaseComponent: Repositories +:CaseComponent: HTTPProxy :team: Phoenix-content diff --git a/tests/foreman/cli/test_http_proxy.py b/tests/foreman/cli/test_http_proxy.py index c6bea60857d..11539a65d6b 100644 --- a/tests/foreman/cli/test_http_proxy.py +++ b/tests/foreman/cli/test_http_proxy.py @@ -2,7 +2,7 @@ :Requirement: HttpProxy -:CaseComponent: Repositories +:CaseComponent: HTTPProxy :team: Phoenix-content diff --git a/tests/foreman/ui/test_http_proxy.py b/tests/foreman/ui/test_http_proxy.py index cc995e84da9..12da7cf798f 100644 --- a/tests/foreman/ui/test_http_proxy.py +++ b/tests/foreman/ui/test_http_proxy.py @@ -2,7 +2,7 @@ :Requirement: HttpProxy -:CaseComponent: Repositories +:CaseComponent: HTTPProxy :team: Phoenix-content From a928ca2a4aeef4bb1c23e9bddc7913be60451728 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 14 Feb 2024 14:50:27 -0500 Subject: [PATCH 491/586] [6.14.z] [Test Fix] test_negative_generate_hostpkgcompare_nonexistent_host (#14078) [Test Fix] test_negative_generate_hostpkgcompare_nonexistent_host (#14068) Updating Assertion for test fix (cherry picked from commit 51ad778640dbc34487eb447f7386478e583d2456) Co-authored-by: Cole Higgins --- tests/foreman/cli/test_reporttemplates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/cli/test_reporttemplates.py b/tests/foreman/cli/test_reporttemplates.py index 01b471d43fd..d3b12f75161 100644 --- a/tests/foreman/cli/test_reporttemplates.py +++ b/tests/foreman/cli/test_reporttemplates.py @@ -985,7 +985,7 @@ def test_negative_generate_hostpkgcompare_nonexistent_host(module_target_sat): 'inputs': 'Host 1 = nonexistent1, ' 'Host 2 = nonexistent2', } ) - assert "At least one of the hosts couldn't be found" in cm.exception.stderr + assert "At least one of the hosts couldn't be found" in cm.value.stderr @pytest.mark.rhel_ver_list([7, 8, 9]) From 131768e0061021839a31302a8f4b33aff7e1255c Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 15 Feb 2024 03:54:37 -0500 Subject: [PATCH 492/586] [6.14.z] create report template w/o name (#14089) --- tests/foreman/api/test_reporttemplates.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/foreman/api/test_reporttemplates.py b/tests/foreman/api/test_reporttemplates.py index 3c3bf97658e..497c27feefe 100644 --- a/tests/foreman/api/test_reporttemplates.py +++ b/tests/foreman/api/test_reporttemplates.py @@ -357,8 +357,7 @@ def test_positive_generate_report_sanitized(): @pytest.mark.tier2 -@pytest.mark.stubbed -def test_negative_create_report_without_name(): +def test_negative_create_report_without_name(module_target_sat): """Try to create a report template with empty name :id: a4b577db-144e-4771-a42e-e93887464986 @@ -373,6 +372,9 @@ def test_negative_create_report_without_name(): :CaseImportance: Medium """ + with pytest.raises(HTTPError) as report_response: + module_target_sat.api.ReportTemplate(name=' ', template=gen_string('alpha')).create() + assert "Name can't be blank" in report_response.value.response.text @pytest.mark.tier2 From 3fc2ed9ce13ba61ffa1ccfda15f8af1acdafeda0 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 15 Feb 2024 09:10:26 -0500 Subject: [PATCH 493/586] [6.14.z] Fix parametrization in test_positive_reboot_all_pxe_hosts (#14084) Fix parametrization in test_positive_reboot_all_pxe_hosts (#13949) Signed-off-by: Gaurav Talreja (cherry picked from commit 55109fbe66d6adbef55b5716584f9a5c22e80b85) Co-authored-by: Gaurav Talreja --- pytest_fixtures/component/provision_pxe.py | 5 +---- tests/foreman/api/test_discoveredhost.py | 9 +++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/pytest_fixtures/component/provision_pxe.py b/pytest_fixtures/component/provision_pxe.py index f39701f408c..2fc375bc5f1 100644 --- a/pytest_fixtures/component/provision_pxe.py +++ b/pytest_fixtures/component/provision_pxe.py @@ -244,13 +244,10 @@ def provision_multiple_hosts(module_ssh_key_file, pxe_loader, request): cd_iso = ( "" # TODO: Make this an optional fixture parameter (update vm_firmware when adding this) ) - # Keeping the default value to 2 - count = request.param if request.param is not None else 2 - with Broker( workflow="deploy-configure-pxe-provisioning-host-rhv", host_class=ContentHost, - _count=count, + _count=getattr(request, 'param', 2), target_vlan_id=vlan_id, target_vm_firmware=pxe_loader.vm_firmware, target_vm_cd_iso=cd_iso, diff --git a/tests/foreman/api/test_discoveredhost.py b/tests/foreman/api/test_discoveredhost.py index 40738be8aa2..59783b9a8b6 100644 --- a/tests/foreman/api/test_discoveredhost.py +++ b/tests/foreman/api/test_discoveredhost.py @@ -397,9 +397,7 @@ def test_positive_reboot_pxe_host( @pytest.mark.on_premises_provisioning @pytest.mark.parametrize('module_provisioning_sat', ['discovery'], indirect=True) - @pytest.mark.parametrize('pxe_loader', ['bios'], indirect=True) @pytest.mark.rhel_ver_match('9') - @pytest.mark.parametrize('provision_multiple_hosts', [2]) @pytest.mark.tier3 def test_positive_reboot_all_pxe_hosts( self, @@ -415,13 +413,15 @@ def test_positive_reboot_all_pxe_hosts( :parametrized: yes - :Setup: Provisioning should be configured and hosts should be discovered via PXE boot. + :setup: Provisioning should be configured and hosts should be discovered via PXE boot. :steps: PUT /api/v2/discovered_hosts/reboot_all :expectedresults: All discovered hosst should be rebooted successfully :CaseImportance: Medium + + :BZ: 2264195 """ sat = module_discovery_sat.sat for host in provision_multiple_hosts: @@ -437,8 +437,9 @@ def test_positive_reboot_all_pxe_hosts( discovered_host.location = provisioning_hostgroup.location[0] discovered_host.organization = provisioning_hostgroup.organization[0] discovered_host.build = True + # Until BZ 2264195 is resolved, reboot_all is expected to fail result = sat.api.DiscoveredHost().reboot_all() - assert 'Discovered hosts are rebooting now' in result['message'] + assert 'Discovered hosts are rebooting now' in result['success_msg'] class TestFakeDiscoveryTests: From eabbc2149b177316e275c1389c33882702d65e89 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 15 Feb 2024 09:24:04 -0500 Subject: [PATCH 494/586] [6.14.z] Test for the change host's content source feature (#14091) --- robottelo/hosts.py | 2 +- tests/foreman/ui/test_host.py | 164 ++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 1 deletion(-) diff --git a/robottelo/hosts.py b/robottelo/hosts.py index a393a7a1705..3404a9b2d6a 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -726,10 +726,10 @@ def register( """Registers content host to the Satellite or Capsule server using a global registration template. - :param target: Satellite or Capusle object to register to, required. :param org: Organization to register content host to. Previously required, pass None to omit :param loc: Location to register content host for, Previously required, pass None to omit. :param activation_keys: Activation key name to register content host with, required. + :param target: Satellite or Capsule object to register to, required. :param setup_insights: Install and register Insights client, requires OS repo. :param setup_remote_execution: Copy remote execution SSH key. :param setup_remote_execution_pull: Deploy pull provider client on host diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index f5a5f9680af..326ef52246e 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -14,6 +14,7 @@ import copy import csv import os +import re from airgun.exceptions import DisabledWidgetError, NoSuchElementException import pytest @@ -32,7 +33,9 @@ OSCAP_PERIOD, OSCAP_WEEKDAY, PERMISSIONS, + REPO_TYPE, ) +from robottelo.constants.repos import CUSTOM_FILE_REPO from robottelo.utils.datafactory import gen_string from robottelo.utils.issue_handlers import is_open @@ -1765,3 +1768,164 @@ def test_all_hosts_bulk_delete(target_sat, function_org, function_location, new_ session.organization.select(function_org.name) session.location.select(function_location.name) assert session.all_hosts.bulk_delete_all() + + +@pytest.fixture(scope='module') +def change_content_source_prep( + module_target_sat, + module_sca_manifest_org, + module_capsule_configured, + module_location, +): + """ + This fixture sets up all the necessary entities for tests + exercising the Change of the hosts's content source. + + It creates a new product in the organization, + creates a new repository in the product, + creates a new lce, + creates a new CV in the organization, adds the repository to the CV, + publishes the CV, and promotes the published version to the lifecycle environment, + creates a new activation key for the CV in the lce, + registers the RHEL content host with the activation key, + updates the capsule's taxonomies + adds the lifecycle environment to the capsule's content. + + Fixture returns module_target_sat, org, lce, capsule, content_view, loc, ak + """ + product_name, lce_name = (gen_string('alpha') for _ in range(2)) + + org = module_sca_manifest_org + loc = module_location + + product = module_target_sat.api.Product( + name=product_name, + organization=org.id, + ).create() + + repository = module_target_sat.api.Repository( + product=product, + content_type=REPO_TYPE['file'], + url=CUSTOM_FILE_REPO, + ).create() + + lce = module_target_sat.cli_factory.make_lifecycle_environment( + {'name': lce_name, 'organization-id': org.id} + ) + + # Create CV + content_view = module_target_sat.api.ContentView(organization=org.id).create() + # Add repos to CV + content_view.repository = [repository] + content_view = content_view.update(['repository']) + # Publish that CV and promote it + content_view.publish() + content_view.read().version[0].promote(data={'environment_ids': lce.id}) + + ak = module_target_sat.api.ActivationKey( + content_view=content_view, organization=org.id, environment=lce.id + ).create() + + # Edit capsule's taxonomies + capsule = module_target_sat.cli.Capsule.update( + { + 'name': module_capsule_configured.hostname, + 'organization-ids': org.id, + 'location-ids': loc.id, + } + ) + + module_target_sat.cli.Capsule.content_add_lifecycle_environment( + { + 'id': module_capsule_configured.nailgun_capsule.id, + 'organization-id': org.id, + 'lifecycle-environment': lce.name, + } + ) + + return module_target_sat, org, lce, capsule, content_view, loc, ak + + +@pytest.mark.no_containers +@pytest.mark.rhel_ver_match('[78]') +def test_change_content_source(session, change_content_source_prep, rhel_contenthost): + """ + This test excercises different ways to change host's content source + + :id: 5add68c3-16b1-496d-9b24-f5388013351d + + :expectedresults: Job invocation page should be correctly generated + by the change content source action, generated script should also be correct + + :CaseComponent:Hosts-Content + + :Team: Phoenix-content + """ + + module_target_sat, org, lce, capsule, content_view, loc, ak = change_content_source_prep + + rhel_contenthost.register(org, loc, ak.name, module_target_sat) + + with module_target_sat.ui_session() as session: + session.organization.select(org_name=org.name) + session.location.select(loc_name=ANY_CONTEXT['location']) + + # STEP 1: Test the part where you use "Update hosts manually" button + # Set the content source to the checked-out capsule + # Check that generated script contains correct name of new content source + rhel_contenthost_pre_values = rhel_contenthost.nailgun_host.content_facet_attributes + generated_script = session.host.change_content_source_get_script( + entities_list=[ + rhel_contenthost.hostname, + ], + content_source=capsule[0]['name'], + lce=lce.name, + content_view=content_view.name, + ) + rhel_contenthost_post_values = rhel_contenthost.nailgun_host.content_facet_attributes + content_source_from_script = re.search(r'--server.hostname=\"(.*?)\"', generated_script) + + assert content_source_from_script.group(1) == capsule[0]['name'] + assert rhel_contenthost_post_values['content_source']['name'] == capsule[0]['name'] + assert rhel_contenthost_post_values['content_view']['name'] == content_view.name + assert rhel_contenthost_post_values['lifecycle_environment']['name'] == lce.name + + session.browser.refresh() + + # Step 2: Test the part where you use "Run job invocation" button + # Change the rhel_contenthost's content source back to what it was before STEP 1 + # Check the prefilled job invocation page + session.host.change_content_source( + entities_list=[ + rhel_contenthost.hostname, + ], + content_source=rhel_contenthost_pre_values['content_source']['name'], + lce=rhel_contenthost_pre_values['lifecycle_environment']['name'], + content_view=rhel_contenthost_pre_values['content_view']['name'], + run_job_invocation=True, + ) + # Getting the data from the prefilled job invocation form + selected_category_and_template = session.jobinvocation.get_job_category_and_template() + selected_targeted_hosts = session.jobinvocation.get_targeted_hosts() + + assert selected_category_and_template['job_category'] == 'Katello' + assert ( + selected_category_and_template['job_template'] + == 'Configure host for new content source' + ) + assert selected_targeted_hosts['selected_hosts'] == [rhel_contenthost.hostname] + + session.jobinvocation.submit_prefilled_view() + rhel_contenthost_post_values = rhel_contenthost.nailgun_host.content_facet_attributes + assert ( + rhel_contenthost_post_values['content_source']['name'] + == rhel_contenthost_pre_values['content_source']['name'] + ) + assert ( + rhel_contenthost_post_values['content_view']['name'] + == rhel_contenthost_post_values['content_view']['name'] + ) + assert ( + rhel_contenthost_post_values['lifecycle_environment']['name'] + == rhel_contenthost_post_values['lifecycle_environment']['name'] + ) From 9b937d82d2614b95d1a1c12570e9718f20df6487 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Sun, 18 Feb 2024 23:17:11 -0500 Subject: [PATCH 495/586] [6.14.z] Bump pre-commit from 3.6.1 to 3.6.2 (#14121) --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 16079401797..eb70684caa4 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -2,7 +2,7 @@ flake8==7.0.0 pytest-cov==4.1.0 redis==5.0.1 -pre-commit==3.6.1 +pre-commit==3.6.2 # For generating documentation. sphinx==7.2.6 From c5e7b95dda55272061e4106dba77b1d1cecafc6a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 20 Feb 2024 03:26:12 -0500 Subject: [PATCH 496/586] [6.14.z] Fix useless asserts in installer tests (#14129) Fix useless asserts in installer tests (#14099) fix useless asserts (cherry picked from commit 44ee8bf96b94c354b749be398189b01241b36eb5) Co-authored-by: rmynar <64528205+rmynar@users.noreply.github.com> --- tests/foreman/destructive/test_installer.py | 8 ++++++-- tests/foreman/installer/test_installer.py | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/foreman/destructive/test_installer.py b/tests/foreman/destructive/test_installer.py index ebfcfb8d50a..a020b66f96a 100644 --- a/tests/foreman/destructive/test_installer.py +++ b/tests/foreman/destructive/test_installer.py @@ -64,7 +64,10 @@ def test_installer_sat_pub_directory_accessibility(target_sat): https_curl_command = f'curl -i {target_sat.url}/pub/ -k' for command in [http_curl_command, https_curl_command]: accessibility_check = target_sat.execute(command) - assert 'HTTP/1.1 200 OK' or 'HTTP/2 200 ' in accessibility_check.stdout.split('\r\n') + assert ( + 'HTTP/1.1 200 OK' in accessibility_check.stdout + or 'HTTP/2 200' in accessibility_check.stdout + ) target_sat.get( local_path='custom-hiera-satellite.yaml', remote_path=f'{custom_hiera_location}', @@ -74,7 +77,8 @@ def test_installer_sat_pub_directory_accessibility(target_sat): assert 'Success!' in command_output.stdout for command in [http_curl_command, https_curl_command]: accessibility_check = target_sat.execute(command) - assert 'HTTP/1.1 200 OK' or 'HTTP/2 200 ' not in accessibility_check.stdout.split('\r\n') + assert 'HTTP/1.1 200 OK' not in accessibility_check.stdout + assert 'HTTP/2 200' not in accessibility_check.stdout target_sat.put( local_path='custom-hiera-satellite.yaml', remote_path=f'{custom_hiera_location}', diff --git a/tests/foreman/installer/test_installer.py b/tests/foreman/installer/test_installer.py index 5b3f81ed9a0..a9de5a4ff0e 100644 --- a/tests/foreman/installer/test_installer.py +++ b/tests/foreman/installer/test_installer.py @@ -1743,7 +1743,10 @@ def test_installer_cap_pub_directory_accessibility(capsule_configured): https_curl_command = f'curl -i {capsule_configured.url}/pub/ -k' for command in [http_curl_command, https_curl_command]: accessibility_check = capsule_configured.execute(command) - assert 'HTTP/1.1 200 OK' or 'HTTP/2 200 ' in accessibility_check.stdout.split('\r\n') + assert ( + 'HTTP/1.1 200 OK' in accessibility_check.stdout + or 'HTTP/2 200' in accessibility_check.stdout + ) capsule_configured.get( local_path='custom-hiera-capsule.yaml', remote_path=f'{custom_hiera_location}', @@ -1753,7 +1756,8 @@ def test_installer_cap_pub_directory_accessibility(capsule_configured): assert 'Success!' in command_output.stdout for command in [http_curl_command, https_curl_command]: accessibility_check = capsule_configured.execute(command) - assert 'HTTP/1.1 200 OK' or 'HTTP/2 200 ' not in accessibility_check.stdout.split('\r\n') + assert 'HTTP/1.1 200 OK' not in accessibility_check.stdout + assert 'HTTP/2 200' not in accessibility_check.stdout capsule_configured.put( local_path='custom-hiera-capsule.yaml', remote_path=f'{custom_hiera_location}', From c12d99d2820131d552ab458befa3b7eb87163850 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 20 Feb 2024 09:38:41 -0500 Subject: [PATCH 497/586] [6.14.z] Remove enable_tools_repo and enable_rhel_repo methods (#14138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Gajdušek --- robottelo/host_helpers/contenthost_mixins.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/robottelo/host_helpers/contenthost_mixins.py b/robottelo/host_helpers/contenthost_mixins.py index f009503666a..96da029d148 100644 --- a/robottelo/host_helpers/contenthost_mixins.py +++ b/robottelo/host_helpers/contenthost_mixins.py @@ -100,26 +100,6 @@ def dogfood_repository(self, repo=None, product=None, release=None, snap=''): product, release, v_major, repo = self._dogfood_helper(product, release, repo) return dogfood_repository(settings.ohsnap, repo, product, release, v_major, snap, self.arch) - def enable_tools_repo(self, organization_id): - return self.satellite.api_factory.enable_rhrepo_and_fetchid( - basearch=constants.DEFAULT_ARCHITECTURE, - org_id=organization_id, - product=constants.PRDS['rhel'], - repo=self.REPOS['rhst']['name'], - reposet=self.REPOSET['rhst'], - releasever=None, - ) - - def enable_rhel_repo(self, organization_id): - return self.satellite.api_factory.enable_rhrepo_and_fetchid( - basearch=constants.DEFAULT_ARCHITECTURE, - org_id=organization_id, - product=constants.PRDS['rhel'], - repo=self.REPOS['rhel']['name'], - reposet=self.REPOSET['rhel'], - releasever=None, - ) - def create_custom_html_repo(self, rpm_url, repo_name=None, update=False, remove_rpm=None): """Creates a custom yum repository, that will be published on https From bf84d3f98569af26daccfb98d942f9453c6669c5 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 20 Feb 2024 16:02:48 -0500 Subject: [PATCH 498/586] [6.14.z] ISS fix for CVV sorting issue (#14146) --- tests/foreman/cli/test_satellitesync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index af81b632a5a..225a834fcbb 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -772,7 +772,7 @@ def test_positive_export_import_filtered_cvv( } ) exporting_cv = target_sat.cli.ContentView.info({'id': exporting_cv['id']}) - exporting_cvv_id = exporting_cv['versions'][0]['id'] + exporting_cvv_id = max(exporting_cv['versions'], key=lambda x: int(x['id']))['id'] # Check presence of 1 rpm due to filter export_packages = target_sat.cli.Package.list({'content-view-version-id': exporting_cvv_id}) assert len(export_packages) == 1 From e46b455f0b572b058c98391533af183ce6a51bc7 Mon Sep 17 00:00:00 2001 From: vijay sawant Date: Wed, 21 Feb 2024 18:00:55 +0530 Subject: [PATCH 499/586] [6.14.z] deleted tc's by considering recommendation present in repositories component audit (#14153) deleted tc's by considering recommendation present in repositories component audit --- tests/foreman/api/test_docker.py | 16 - tests/foreman/api/test_repository.py | 180 ------------ tests/foreman/cli/test_repository.py | 418 --------------------------- tests/foreman/ui/test_repository.py | 28 -- tests/foreman/ui/test_sync.py | 18 -- 5 files changed, 660 deletions(-) diff --git a/tests/foreman/api/test_docker.py b/tests/foreman/api/test_docker.py index b11cc4bc126..eab9d213db5 100644 --- a/tests/foreman/api/test_docker.py +++ b/tests/foreman/api/test_docker.py @@ -191,22 +191,6 @@ def test_positive_create_repos_using_multiple_products(self, module_org, module_ product = product.read() assert repo.id in [repo_.id for repo_ in product.repository] - @pytest.mark.tier1 - def test_positive_sync(self, module_product, module_target_sat): - """Create and sync a Docker-type repository - - :id: 80fbcd84-1c6f-444f-a44e-7d2738a0cba2 - - :expectedresults: A repository is created with a Docker repository and - it is synchronized. - - :CaseImportance: Critical - """ - repo = _create_repository(module_target_sat, module_product) - repo.sync(timeout=600) - repo = repo.read() - assert repo.content_counts['docker_manifest'] >= 1 - @pytest.mark.tier1 @pytest.mark.parametrize('new_name', **parametrized(valid_docker_repository_names())) def test_positive_update_name(self, repo, new_name): diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index 903c8ab3c4a..7d32d58f088 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -689,20 +689,6 @@ def test_positive_update_gpg(self, module_org, module_product, module_target_sat repo = repo.update(['gpg_key']) assert repo.gpg_key.id == gpg_key_2.id - @pytest.mark.tier2 - def test_positive_update_contents(self, repo): - """Create a repository and upload RPM contents. - - :id: 8faa64f9-b620-4c0a-8c80-801e8e6436f1 - - :expectedresults: The repository's contents include one RPM. - - """ - # Upload RPM content. - repo.upload_content(files={'content': DataFile.RPM_TO_UPLOAD.read_bytes()}) - # Verify the repository's contents. - assert repo.read().content_counts['rpm'] == 1 - @pytest.mark.tier1 @pytest.mark.upgrade def test_positive_upload_delete_srpm(self, repo, target_sat): @@ -1631,37 +1617,6 @@ def test_positive_create(self, repo_options, repo): for k in 'name', 'docker_upstream_name', 'content_type': assert getattr(repo, k) == repo_options[k] - @pytest.mark.tier1 - @pytest.mark.parametrize( - 'repo_options', - **datafactory.parametrized( - { - constants.CONTAINER_UPSTREAM_NAME: { - 'content_type': 'docker', - 'docker_upstream_name': constants.CONTAINER_UPSTREAM_NAME, - 'name': gen_string('alphanumeric', 10), - 'url': constants.CONTAINER_REGISTRY_HUB, - } - } - ), - indirect=True, - ) - def test_positive_synchronize(self, repo): - """Create and sync a Docker-type repository - - :id: 27653663-e5a7-4700-a3c1-f6eab6468adf - - :parametrized: yes - - :expectedresults: A repository is created with a Docker repository and - it is synchronized. - - :CaseImportance: Critical - """ - # TODO: add timeout support to sync(). This repo needs more than the default 300 seconds. - repo.sync() - assert repo.read().content_counts['docker_manifest'] >= 1 - @pytest.mark.tier3 @pytest.mark.parametrize( 'repo_options', @@ -1805,44 +1760,6 @@ def test_positive_synchronize_private_registry(self, repo): repo.sync() assert repo.read().content_counts['docker_manifest'] >= 1 - @pytest.mark.tier2 - @pytest.mark.parametrize( - 'repo_options', - **datafactory.parametrized( - { - 'private_registry': { - 'content_type': 'docker', - 'docker_upstream_name': settings.docker.private_registry_name, - 'name': gen_string('alpha'), - 'upstream_username': settings.docker.private_registry_username, - 'upstream_password': 'ThisIsaWrongPassword', - 'url': settings.docker.private_registry_url, - } - } - ), - indirect=True, - ) - def test_negative_synchronize_private_registry_wrong_password(self, repo_options, repo): - """Create and try to sync a Docker-type repository from a private - registry providing wrong credentials the sync must fail with - reasonable error message. - - :id: 2857fce2-fed7-49fc-be20-bf2e4726c9f5 - - :parametrized: yes - - :expectedresults: A repository is created with a private Docker \ - repository and sync fails with reasonable error message. - - :customerscenario: true - - :BZ: 1475121, 1580510 - - """ - msg = "401, message=\'Unauthorized\'" - with pytest.raises(TaskFailedError, match=msg): - repo.sync() - @pytest.mark.tier2 @pytest.mark.parametrize( 'repo_options', @@ -2410,27 +2327,6 @@ def test_positive_upload_file_to_file_repo(self, repo, target_sat): ) assert constants.RPM_TO_UPLOAD == filesearch[0].name - @pytest.mark.stubbed - @pytest.mark.tier1 - def test_positive_file_permissions(self): - """Check file permissions after file upload to File Repository - - :id: 03b4b7dd-0505-4302-ae00-5de33ad420b0 - - :Setup: - 1. Create a File Repository - 2. Upload an arbitrary file to it - - :steps: Retrieve file permissions from File Repository - - :expectedresults: uploaded file permissions are kept after upload - - :CaseImportance: Critical - - :CaseAutomation: NotAutomated - """ - pass - @pytest.mark.tier1 @pytest.mark.upgrade @pytest.mark.parametrize( @@ -2464,82 +2360,6 @@ def test_positive_remove_file(self, repo, target_sat): repo.remove_content(data={'ids': [file_detail[0].id], 'content_type': 'file'}) assert repo.read().content_counts['file'] == 0 - @pytest.mark.stubbed - @pytest.mark.tier2 - @pytest.mark.upgrade - def test_positive_remote_directory_sync(self): - """Check an entire remote directory can be synced to File Repository - through http - - :id: 5c29b758-004a-4c71-a860-7087a0e96747 - - :Setup: - 1. Create a directory to be synced with a pulp manifest on its root - 2. Make the directory available through http - - :steps: - 1. Create a File Repository with url pointing to http url - created on setup - 2. Initialize synchronization - - - :expectedresults: entire directory is synced over http - - :CaseAutomation: NotAutomated - """ - pass - - @pytest.mark.stubbed - @pytest.mark.tier1 - def test_positive_local_directory_sync(self): - """Check an entire local directory can be synced to File Repository - - :id: 178145e6-62e1-4cb9-b825-44d3ab41e754 - - :Setup: - 1. Create a directory to be synced with a pulp manifest on its root - locally (on the Satellite/Foreman host) - - :steps: - 1. Create a File Repository with url pointing to local url - created on setup - 2. Initialize synchronization - - - :expectedresults: entire directory is synced - - :CaseImportance: Critical - - :CaseAutomation: NotAutomated - """ - pass - - @pytest.mark.stubbed - @pytest.mark.tier1 - def test_positive_symlinks_sync(self): - """Check synlinks can be synced to File Repository - - :id: 438a8e21-3502-4995-86db-c67ba0f3c469 - - :Setup: - 1. Create a directory to be synced with a pulp manifest on its root - locally (on the Satellite/Foreman host) - 2. Make sure it contains synlinks - - :steps: - 1. Create a File Repository with url pointing to local url - created on setup - 2. Initialize synchronization - - :expectedresults: entire directory is synced, including files - referred by symlinks - - :CaseImportance: Critical - - :CaseAutomation: NotAutomated - """ - pass - @pytest.mark.skip_if_not_set('container_repo') class TestTokenAuthContainerRepository: diff --git a/tests/foreman/cli/test_repository.py b/tests/foreman/cli/test_repository.py index 84150f2494a..335a0e7d898 100644 --- a/tests/foreman/cli/test_repository.py +++ b/tests/foreman/cli/test_repository.py @@ -213,27 +213,6 @@ def test_positive_create_with_yum_repo(self, repo_options, repo): for key in 'url', 'content-type': assert repo.get(key) == repo_options[key] - @pytest.mark.tier1 - @pytest.mark.upgrade - @pytest.mark.parametrize( - 'repo_options', - **parametrized([{'content-type': 'file', 'url': CUSTOM_FILE_REPO}]), - indirect=True, - ) - def test_positive_create_with_file_repo(self, repo_options, repo): - """Create file repository - - :id: 46f63419-1acc-4ae2-be8c-d97816ba342f - - :parametrized: yes - - :expectedresults: file repository is created - - :CaseImportance: Critical - """ - for key in 'url', 'content-type': - assert repo.get(key) == repo_options[key] - @pytest.mark.tier1 @pytest.mark.parametrize( 'repo_options', @@ -701,40 +680,6 @@ def test_negative_create_non_yum_with_download_policy(self, repo_options, module ): module_target_sat.cli_factory.make_repository(repo_options) - @pytest.mark.tier1 - @pytest.mark.parametrize( - 'repo_options', - **parametrized( - [ - {'content-type': 'yum', 'url': url} - for url in ( - settings.repos.yum_1.url, - settings.repos.yum_3.url, - settings.repos.yum_4.url, - ) - ] - ), - indirect=True, - ) - def test_positive_synchronize_yum_repo(self, repo_options, repo, target_sat): - """Check if repository can be created and synced - - :id: e3a62529-edbd-4062-9246-bef5f33bdcf0 - - :parametrized: yes - - :expectedresults: Repository is created and synced - - :CaseImportance: Critical - """ - # Repo is not yet synced - assert repo['sync']['status'] == 'Not Synced' - # Synchronize it - target_sat.cli.Repository.synchronize({'id': repo['id']}) - # Verify it has finished - repo = target_sat.cli.Repository.info({'id': repo['id']}) - assert repo['sync']['status'] == 'Success' - @pytest.mark.tier1 @pytest.mark.parametrize( 'repo_options', @@ -920,38 +865,6 @@ def test_verify_checksum_container_repo(self, repo, target_sat): new_repo = target_sat.cli.Repository.info({'id': repo['id']}) assert new_repo['sync']['status'] == 'Success' - @pytest.mark.tier2 - @pytest.mark.upgrade - @pytest.mark.parametrize( - 'repo_options', - **parametrized( - [ - { - 'content-type': 'docker', - 'docker-upstream-name': CONTAINER_UPSTREAM_NAME, - 'url': CONTAINER_REGISTRY_HUB, - 'include-tags': 'latest', - } - ] - ), - indirect=True, - ) - def test_positive_synchronize_docker_repo_with_tags_whitelist( - self, repo_options, repo, target_sat - ): - """Check if only whitelisted tags are synchronized - - :id: aa820c65-2de1-4b32-8890-98bd8b4320dc - - :parametrized: yes - - :expectedresults: Only whitelisted tag is synchronized - """ - target_sat.cli.Repository.synchronize({'id': repo['id']}) - repo = _validated_image_tags_count(repo=repo, sat=target_sat) - assert repo_options['include-tags'] in repo['container-image-tags-filter'] - assert int(repo['content-counts']['container-image-tags']) == 1 - @pytest.mark.tier2 @pytest.mark.parametrize( 'repo_options', @@ -1413,27 +1326,6 @@ def test_negative_create_checksum_with_on_demand_policy(self, repo_options, modu with pytest.raises(CLIFactoryError): module_target_sat.cli_factory.make_repository(repo_options) - @pytest.mark.tier1 - @pytest.mark.parametrize( - 'repo_options', - **parametrized([{'name': name} for name in valid_data_list().values()]), - indirect=True, - ) - def test_positive_delete_by_id(self, repo, target_sat): - """Check if repository can be created and deleted - - :id: bcf096db-0033-4138-90a3-cb7355d5dfaf - - :parametrized: yes - - :expectedresults: Repository is created and then deleted - - :CaseImportance: Critical - """ - target_sat.cli.Repository.delete({'id': repo['id']}) - with pytest.raises(CLIReturnCodeError): - target_sat.cli.Repository.info({'id': repo['id']}) - @pytest.mark.tier1 @pytest.mark.upgrade @pytest.mark.parametrize( @@ -2012,50 +1904,6 @@ def test_positive_create_get_update_delete_module_streams( with pytest.raises(CLIReturnCodeError): module_target_sat.cli.Repository.info({'id': repo['id']}) - @pytest.mark.tier1 - @pytest.mark.parametrize( - 'repo_options', - **parametrized([{'content-type': 'yum', 'url': settings.repos.module_stream_0.url}]), - indirect=True, - ) - @pytest.mark.parametrize( - 'repo_options_2', - **parametrized([{'content-type': 'yum', 'url': settings.repos.module_stream_1.url}]), - ) - def test_module_stream_list_validation( - self, module_org, repo, repo_options_2, module_target_sat - ): - """Check module-stream get with list on hammer. - - :id: 9842a0c3-8532-4b16-a00a-534fc3b0a776ff89f23e-cd00-4d20-84d3-add0ea24abf8 - - :parametrized: yes - - :Setup: - 1. valid yum repo with Module Streams. - - :steps: - 1. Create Yum Repositories with url contain module-streams and Products - 2. Initialize synchronization - 3. Verify the module-stream list with various inputs options - - :expectedresults: Verify the module-stream list response. - - :CaseAutomation: Automated - """ - module_target_sat.cli.Repository.synchronize({'id': repo['id']}) - - prod_2 = module_target_sat.cli_factory.make_product({'organization-id': module_org.id}) - repo_options_2['organization-id'] = module_org.id - repo_options_2['product-id'] = prod_2['id'] - repo_2 = module_target_sat.cli_factory.make_repository(repo_options_2) - - module_target_sat.cli.Repository.synchronize({'id': repo_2['id']}) - module_streams = module_target_sat.cli.ModuleStream.list() - assert len(module_streams) > 13, 'Module Streams list failed' - module_streams = module_target_sat.cli.ModuleStream.list({'product-id': prod_2['id']}) - assert len(module_streams) == 7, 'Module Streams list by product failed' - @pytest.mark.tier1 @pytest.mark.parametrize( 'repo_options', @@ -2771,177 +2619,6 @@ def test_positive_sync_publish_promote_cv(self, repo, module_org, module_product assert result.stdout -class TestGitPuppetMirror: - """Tests for creating the hosts via CLI. - - Notes for GIT puppet mirror content - - This feature does not allow us to actually sync / update the content in a - GIT repo. Instead, we essentially "snapshot" a repo's contents at any - given time. The ability to update the GIT puppet mirror is / should - be provided by Pulp itself, via a script. However, we should be able to - # create a sync schedule against the mirror to make sure it is periodically - updated to contain the latest and greatest. - """ - - @pytest.mark.stubbed - @pytest.mark.tier2 - def test_positive_git_local_create(self): - """Create repository with local git puppet mirror. - - :id: 89211cd5-82b8-4391-b729-a7502e57f824 - - :Setup: Assure local GIT puppet has been created and found by pulp - - :steps: Create link to local puppet mirror via cli - - :expectedresults: Content source containing local GIT puppet mirror - content is created - - :CaseAutomation: NotAutomated - """ - - @pytest.mark.stubbed - @pytest.mark.tier2 - def test_positive_git_local_update(self): - """Update repository with local git puppet mirror. - - :id: 341f40f2-3501-4754-9acf-7cda1a61f7db - - :Setup: Assure local GIT puppet has been created and found by pulp - - :steps: Modify details for existing puppet repo (name, etc.) via cli - - :expectedresults: Content source containing local GIT puppet mirror - content is modified - - :CaseAutomation: NotAutomated - """ - - @pytest.mark.stubbed - @pytest.mark.tier2 - @pytest.mark.upgrade - def test_positive_git_local_delete(self): - """Delete repository with local git puppet mirror. - - :id: a243f5bb-5186-41b3-8e8a-07d5cc784ccd - - :Setup: Assure local GIT puppet has been created and found by pulp - - :steps: Delete link to local puppet mirror via cli - - :expectedresults: Content source containing local GIT puppet mirror - content no longer exists/is available. - - :CaseAutomation: NotAutomated - """ - - @pytest.mark.stubbed - @pytest.mark.tier2 - def test_positive_git_remote_create(self): - """Create repository with remote git puppet mirror. - - :id: 8582529f-3112-4b49-8d8f-f2bbf7dceca7 - - :Setup: Assure remote GIT puppet has been created and found by pulp - - :steps: Create link to local puppet mirror via cli - - :expectedresults: Content source containing remote GIT puppet mirror - content is created - - :CaseAutomation: NotAutomated - """ - - @pytest.mark.stubbed - @pytest.mark.tier2 - def test_positive_git_remote_update(self): - """Update repository with remote git puppet mirror. - - :id: 582c50b3-3b90-4244-b694-97642b1b13a9 - - :Setup: Assure remote GIT puppet has been created and found by pulp - - :steps: modify details for existing puppet repo (name, etc.) via cli - - :expectedresults: Content source containing remote GIT puppet mirror - content is modified - - :CaseAutomation: NotAutomated - """ - - @pytest.mark.stubbed - @pytest.mark.tier2 - @pytest.mark.upgrade - def test_positive_git_remote_delete(self): - """Delete repository with remote git puppet mirror. - - :id: 0a23f969-b202-4c6c-b12e-f651a0b7d049 - - :Setup: Assure remote GIT puppet has been created and found by pulp - - :steps: Delete link to remote puppet mirror via cli - - :expectedresults: Content source containing remote GIT puppet mirror - content no longer exists/is available. - - :CaseAutomation: NotAutomated - """ - - @pytest.mark.stubbed - @pytest.mark.tier2 - def test_positive_git_sync(self): - """Sync repository with git puppet mirror. - - :id: a46c16bd-0986-48db-8e62-aeb3907ba4d2 - - :Setup: git mirror (local or remote) exists as a content source - - :steps: Attempt to sync content from mirror via cli - - :expectedresults: Content is pulled down without error - - :expectedresults: Confirmation that various resources actually exist in - local content repo - - :CaseAutomation: NotAutomated - """ - - @pytest.mark.stubbed - @pytest.mark.tier2 - def test_positive_git_sync_schedule(self): - """Scheduled sync of git puppet mirror. - - :id: 0d58d180-9836-4524-b608-66b67f9cab12 - - :Setup: git mirror (local or remote) exists as a content source - - :steps: Attempt to create a scheduled sync content from mirror, via cli - - :expectedresults: Content is pulled down without error on expected - schedule - - :CaseAutomation: NotAutomated - """ - - @pytest.mark.stubbed - @pytest.mark.tier2 - def test_positive_git_view_content(self): - """View content in synced git puppet mirror - - :id: 02f06092-dd6c-49fa-be9f-831e52476e41 - - :Setup: git mirror (local or remote) exists as a content source - - :steps: Attempt to list contents of repo via cli - - :expectedresults: Spot-checked items (filenames, dates, perhaps - checksums?) are correct. - - :CaseAutomation: NotAutomated - """ - - class TestFileRepository: """Specific tests for File Repositories""" @@ -2988,26 +2665,6 @@ def test_positive_upload_file_to_file_repo(self, repo_options, repo, target_sat) ) assert RPM_TO_UPLOAD == filesearch[0].name - @pytest.mark.stubbed - @pytest.mark.tier1 - def test_positive_file_permissions(self): - """Check file permissions after file upload to File Repository - - :id: 03da888a-69ba-492f-b204-c62d85948d8a - - :Setup: - 1. Create a File Repository - 2. Upload an arbitrary file to it - - :steps: Retrieve file permissions from File Repository - - :expectedresults: uploaded file permissions are kept after upload - - :CaseAutomation: NotAutomated - - :CaseImportance: Critical - """ - @pytest.mark.tier1 @pytest.mark.upgrade @pytest.mark.parametrize( @@ -3242,81 +2899,6 @@ def test_file_repo_contains_only_newer_file(self, repo_options, repo, target_sat assert 'Second File' in textfile.text -@pytest.mark.stubbed -@pytest.mark.tier2 -def test_copy_package_group_between_repos(): - """ - Copy a group of packages from one repo to another. - - :id: 18d832fc-7e27-4067-99ea-5da9eef22253 - - :Setup: - 1. Add a product and sync a repo which has package groups (repo 1) - 2. Create another product and create a yum repo (repo 2) - 3. Select the package group from repo 1 and sync it to repo 2 - - :steps: - Assert the list of package in repo 2 matches the group list from repo 1 - - :CaseAutomation: NotAutomated - - :CaseImportance: Medium - """ - - -@pytest.mark.stubbed -@pytest.mark.tier2 -def test_include_and_exclude_content_units(): - """ - Select two packages and include and exclude some dependencies - and then copy them from one repo to another. - - :id: 073a0ade-6860-4b34-b64f-0f1a75025356 - - :Setup: - 1. Add a product and sync a repo which has packages with dependencies (repo 1) - 2. Create another product and create a yum repo (repo 2) - 3. Select a package and include its dependencies - 4. Select a package and exclude its dependencies - 5. Copy packages from repo 1 to repo 2 - - :steps: - Assert the list of packages in repo 2 matches the packages selected in repo 1, - including only those dependencies expected. - - :CaseAutomation: NotAutomated - - :CaseImportance: Medium - """ - - -@pytest.mark.stubbed -@pytest.mark.tier2 -def test_copy_erratum_and_RPMs_within_a_date_range(): - """ - Select some packages, filer by date range, - and then copy them from one repo to another. - - :id: da48011b-841a-4706-84b5-2dcfe371c30a - - :Setup: - 1. Add a product and sync a repo which has packages with dependencies (repo 1) - 2. Create another product and create a yum repo (repo 2) - 3. Select some packages and include dependencies - 4. Filter by date range - 5. Copy filtered list of items from repo 1 to repo 2 - 6. Repeat using errata in place of RPMs - - :steps: - Assert the list of packages or errata in repo 2 matches those selected - and filtered in repo 1, including those dependencies expected. - - :CaseAutomation: NotAutomated - - :CaseImportance: Medium - """ - - @pytest.mark.tier2 def test_positive_syncable_yum_format_repo_import(target_sat, module_org): """Verify that you can import syncable yum format repositories diff --git a/tests/foreman/ui/test_repository.py b/tests/foreman/ui/test_repository.py index f5382f1ad00..4cdebdcce5a 100644 --- a/tests/foreman/ui/test_repository.py +++ b/tests/foreman/ui/test_repository.py @@ -31,7 +31,6 @@ ) from robottelo.constants.repos import ( ANSIBLE_GALAXY, - CUSTOM_3RD_PARTY_REPO, CUSTOM_RPM_SHA, ) from robottelo.hosts import get_sat_version @@ -1243,33 +1242,6 @@ def test_positive_sync_sha_repo(session, module_org, module_target_sat): assert result['result'] == 'success' -@pytest.mark.tier2 -def test_positive_sync_third_party_repo(session, module_org, module_target_sat): - """Sync third part repo successfully - - :id: 655161e0-aa90-4c7c-9a0d-cb5b9f56eac3 - - :customerscenario: true - - :BZ: 1920511 - - :SubComponent: Pulp - """ - repo_name = gen_string('alpha') - product = module_target_sat.api.Product(organization=module_org).create() - with session: - session.repository.create( - product.name, - { - 'name': repo_name, - 'repo_type': REPO_TYPE['yum'], - 'repo_content.upstream_url': CUSTOM_3RD_PARTY_REPO, - }, - ) - result = session.repository.synchronize(product.name, repo_name) - assert result['result'] == 'success' - - @pytest.mark.tier2 def test_positive_able_to_disable_and_enable_rhel_repos( session, module_org_with_manifest, target_sat diff --git a/tests/foreman/ui/test_sync.py b/tests/foreman/ui/test_sync.py index c5ea76e0fa4..308ef7b485a 100644 --- a/tests/foreman/ui/test_sync.py +++ b/tests/foreman/ui/test_sync.py @@ -37,24 +37,6 @@ def module_custom_product(module_org): return entities.Product(organization=module_org).create() -@pytest.mark.tier2 -@pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_sync_custom_repo(session, module_custom_product): - """Create Content Custom Sync with minimal input parameters - - :id: 00fb0b04-0293-42c2-92fa-930c75acee89 - - :expectedresults: Sync procedure is successful - - :CaseImportance: Critical - """ - repo = entities.Repository(url=settings.repos.yum_1.url, product=module_custom_product).create() - with session: - results = session.sync_status.synchronize([(module_custom_product.name, repo.name)]) - assert len(results) == 1 - assert results[0] == 'Syncing Complete.' - - @pytest.mark.run_in_one_thread @pytest.mark.skip_if_not_set('fake_manifest') @pytest.mark.tier2 From 763aff395729248fbe9ab2189909a651a9574200 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:12:35 -0500 Subject: [PATCH 500/586] [6.14.z] Bump pytest-fixturecollection from 0.1.1 to 0.1.2 (#14166) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 52e45716ac1..7c6f39b36f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ pytest-services==2.2.1 pytest-mock==3.12.0 pytest-reportportal==5.3.1 pytest-xdist==3.5.0 -pytest-fixturecollection==0.1.1 +pytest-fixturecollection==0.1.2 pytest-ibutsu==2.2.4 PyYAML==6.0.1 requests==2.31.0 From 250daf7bdfa8ec5e21f952ab2a4b1e07bd28a272 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 26 Feb 2024 09:41:56 -0500 Subject: [PATCH 501/586] [6.14.z] correct component names and description (#14183) --- tests/foreman/api/test_repository.py | 4 ++-- tests/foreman/cli/test_repository.py | 6 +++--- tests/foreman/ui/test_repository.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index 7d32d58f088..4c366dbdcd3 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -1451,7 +1451,7 @@ def test_positive_bulk_cancel_sync(self, target_sat, module_entitlement_manifest indirect=True, ) def test_positive_sync_sha_repo(self, repo, target_sat): - """Sync a 'sha' repo successfully + """Sync repository with 'sha' checksum, which uses 'sha1' in particular actually :id: b842a21d-639a-48aa-baf3-9244d8bc1415 @@ -1461,7 +1461,7 @@ def test_positive_sync_sha_repo(self, repo, target_sat): :BZ: 2024889 - :SubComponent: Candlepin + :SubComponent: Pulp """ sync_result = repo.sync() assert sync_result['result'] == 'success' diff --git a/tests/foreman/cli/test_repository.py b/tests/foreman/cli/test_repository.py index 335a0e7d898..35269a110dd 100644 --- a/tests/foreman/cli/test_repository.py +++ b/tests/foreman/cli/test_repository.py @@ -847,7 +847,7 @@ def test_positive_synchronize_docker_repo( indirect=True, ) def test_verify_checksum_container_repo(self, repo, target_sat): - """Check if Verify Content Checksum can be run on non container repos + """Check if Verify Content Checksum can be run on container repos :id: c8f0eb45-3cb6-41b2-aad9-52ac847d7bf8 @@ -2026,7 +2026,7 @@ def test_positive_accessible_content_status( indirect=True, ) def test_positive_sync_sha_repo(self, repo_options, module_target_sat): - """Sync a 'sha' repo successfully + """Sync repository with 'sha' checksum, which uses 'sha1' in particular actually :id: 20579f52-a67b-4d3f-be07-41eec059a891 @@ -2036,7 +2036,7 @@ def test_positive_sync_sha_repo(self, repo_options, module_target_sat): :BZ: 2024889 - :SubComponent: Candlepin + :SubComponent: Pulp """ sha_repo = module_target_sat.cli_factory.make_repository(repo_options) sha_repo = module_target_sat.cli.Repository.info({'id': sha_repo['id']}) diff --git a/tests/foreman/ui/test_repository.py b/tests/foreman/ui/test_repository.py index 4cdebdcce5a..5f6e4355b6a 100644 --- a/tests/foreman/ui/test_repository.py +++ b/tests/foreman/ui/test_repository.py @@ -1217,7 +1217,7 @@ def test_positive_sync_repo_and_verify_checksum(session, module_org, module_targ @pytest.mark.tier2 def test_positive_sync_sha_repo(session, module_org, module_target_sat): - """Sync 'sha' repo successfully + """Sync repository with 'sha' checksum, which uses 'sha1' in particular actually :id: 6172035f-96c4-41e4-a79b-acfaa78ad734 @@ -1225,7 +1225,7 @@ def test_positive_sync_sha_repo(session, module_org, module_target_sat): :BZ: 2024889 - :SubComponent: Candlepin + :SubComponent: Pulp """ repo_name = gen_string('alpha') product = module_target_sat.api.Product(organization=module_org).create() From 2e3bad0ef2dd55ee799957208eeda630b7e0fce0 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 26 Feb 2024 12:38:43 -0500 Subject: [PATCH 502/586] [6.14.z] Force keyscan to use ipv4 (#14175) --- pytest_fixtures/component/templatesync.py | 2 +- tests/foreman/cli/test_templatesync.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest_fixtures/component/templatesync.py b/pytest_fixtures/component/templatesync.py index 1e195ec838f..e6c93096a8b 100644 --- a/pytest_fixtures/component/templatesync.py +++ b/pytest_fixtures/component/templatesync.py @@ -66,7 +66,7 @@ def git_pub_key(session_target_sat, git_port): id = res.json()['id'] # add ssh key to known host session_target_sat.execute( - f'ssh-keyscan -t rsa -p {git.ssh_port} {git.hostname} > {key_path}/known_hosts' + f'ssh-keyscan -4 -t rsa -p {git.ssh_port} {git.hostname} > {key_path}/known_hosts' ) yield res = requests.delete( diff --git a/tests/foreman/cli/test_templatesync.py b/tests/foreman/cli/test_templatesync.py index 3f8472d829e..9959ae18459 100644 --- a/tests/foreman/cli/test_templatesync.py +++ b/tests/foreman/cli/test_templatesync.py @@ -161,7 +161,7 @@ def test_positive_update_templates_in_git( 'repo': url, 'branch': git_branch, 'organization-id': module_org.id, - 'filter': 'Atomic Kickstart default', + 'filter': 'User - Registered Users', 'dirname': dirname, } ).split('\n') From b10e46dad9369c26db64901801af7f055466ed3f Mon Sep 17 00:00:00 2001 From: rmynar <64528205+rmynar@users.noreply.github.com> Date: Tue, 27 Feb 2024 21:01:00 +0100 Subject: [PATCH 503/586] [6.14.z] Fix pit marker in installer (#14180) (#14199) Fix pit marker in installer (#14180) (cherry picked from commit b5bbc224c88a164c3be2cab906645518d9dbc84d) --- tests/foreman/installer/test_installer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/foreman/installer/test_installer.py b/tests/foreman/installer/test_installer.py index a9de5a4ff0e..b8756fe3191 100644 --- a/tests/foreman/installer/test_installer.py +++ b/tests/foreman/installer/test_installer.py @@ -1394,7 +1394,7 @@ def sat_non_default_install(module_sat_ready_rhels): @pytest.mark.e2e @pytest.mark.tier1 -@pytest.mark.pit_client +@pytest.mark.pit_server def test_capsule_installation(sat_default_install, cap_ready_rhel, default_org): """Run a basic Capsule installation @@ -1769,7 +1769,7 @@ def test_installer_cap_pub_directory_accessibility(capsule_configured): @pytest.mark.tier1 @pytest.mark.build_sanity @pytest.mark.first_sanity -@pytest.mark.pit_client +@pytest.mark.pit_server def test_satellite_installation(installer_satellite): """Run a basic Satellite installation From 3ccd7ac94a12fdab00bf4331a0836bd107719bfc Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 27 Feb 2024 15:40:37 -0500 Subject: [PATCH 504/586] [6.14.z] Fix OEL Convert2Rhel tests (#14201) --- tests/foreman/api/test_convert2rhel.py | 116 ++++++++++++++----------- 1 file changed, 65 insertions(+), 51 deletions(-) diff --git a/tests/foreman/api/test_convert2rhel.py b/tests/foreman/api/test_convert2rhel.py index 781f571ed02..37bbfa30058 100644 --- a/tests/foreman/api/test_convert2rhel.py +++ b/tests/foreman/api/test_convert2rhel.py @@ -6,6 +6,8 @@ :CaseComponent: Registration +:CaseImportance: Critical + :Team: Rocket """ @@ -53,33 +55,20 @@ def create_activation_key(sat, org, lce, cv, subscription_id): def update_cv(sat, cv, lce, repos): """Update and publish Content view with repos""" - cv = sat.api.ContentView(id=cv.id, repository=repos).update(["repository"]) + cv = sat.api.ContentView(id=cv.id, repository=repos).update(['repository']) cv.publish() cv = cv.read() cv.version[-1].promote(data={'environment_ids': lce.id, 'force': False}) return cv -def register_host(sat, act_key, org, module_loc, host, ubi=None): - """Register host to satellite""" - # generate registration command - command = sat.api.RegistrationCommand( - organization=org, - activation_keys=[act_key.name], - location=module_loc, - insecure=True, - repo=ubi, - ).create() - assert host.execute(command).status == 0 - - @pytest.fixture(scope='module') -def ssl_cert(module_target_sat, module_org): +def ssl_cert(module_target_sat, module_entitlement_manifest_org): """Create credetial with SSL cert for Oracle Linux""" res = requests.get(settings.repos.convert2rhel.ssl_cert_oracle) res.raise_for_status() return module_target_sat.api.ContentCredential( - content=res.text, organization=module_org, content_type='cert' + content=res.text, organization=module_entitlement_manifest_org, content_type='cert' ).create() @@ -152,10 +141,8 @@ def centos( enable_rhel_subscriptions, ): """Deploy and register Centos host""" - # updating centos packages on CentOS 8 is necessary for conversion major = version.split('.')[0] - if major == '8': - centos_host.execute('yum -y update centos-*') + assert centos_host.execute('yum -y update').status == 0 repo_url = settings.repos.convert2rhel.convert_to_rhel_repo.format(major) repo = create_repo(module_target_sat, module_entitlement_manifest_org, repo_url) cv = update_cv( @@ -167,16 +154,19 @@ def centos( act_key = create_activation_key( module_target_sat, module_entitlement_manifest_org, module_lce, cv, c2r_sub.id ) - register_host( - module_target_sat, - act_key, - module_entitlement_manifest_org, - smart_proxy_location, - centos_host, - ) - centos_host.execute('yum -y update kernel*') + + # Register CentOS host with Satellite + command = module_target_sat.api.RegistrationCommand( + organization=module_entitlement_manifest_org, + activation_keys=[act_key.name], + location=smart_proxy_location, + insecure=True, + ).create() + assert centos_host.execute(command).status == 0 + if centos_host.execute('needs-restarting -r').status == 1: centos_host.power_control(state='reboot') + yield centos_host # close ssh session before teardown, because of reboot in conversion it may cause problems centos_host.close() @@ -195,17 +185,33 @@ def oracle( enable_rhel_subscriptions, ): """Deploy and register Oracle host""" + major = version.split('.')[0] + assert oracle_host.execute('yum -y update').status == 0 # disable rhn-client-tools because it obsoletes the subscription manager package oracle_host.execute('echo "exclude=rhn-client-tools" >> /etc/yum.conf') - # install and set correct kernel, based on convert2rhel docs + + # Install and set correct RHEL compatible kernel and using non-UEK kernel, based on C2R docs result = oracle_host.execute( 'yum install -y kernel && ' 'grubby --set-default /boot/vmlinuz-' '`rpm -q --qf "%{BUILDTIME}\t%{EVR}.%{ARCH}\n" kernel | sort -nr | head -1 | cut -f2`' ) assert result.status == 0 - oracle_host.power_control(state='reboot') - major = version.split('.')[0] + + if major == '8': + # needs-restarting missing in OEL8 + assert oracle_host.execute('dnf install -y yum-utils').status == 0 + # Fix inhibitor CHECK_FIREWALLD_AVAILABILITY::FIREWALLD_MODULES_CLEANUP_ON_EXIT_CONFIG - + # Firewalld is set to cleanup modules after exit + result = oracle_host.execute( + 'sed -i -- "s/CleanupModulesOnExit=yes/CleanupModulesOnExit=no/g" ' + '/etc/firewalld/firewalld.conf && firewall-cmd --reload' + ) + assert result.status == 0 + + if oracle_host.execute('needs-restarting -r').status == 1: + oracle_host.power_control(state='reboot') + repo_url = settings.repos.convert2rhel.convert_to_rhel_repo.format(major) repo = create_repo(module_target_sat, module_entitlement_manifest_org, repo_url, ssl_cert) cv = update_cv( @@ -217,17 +223,19 @@ def oracle( act_key = create_activation_key( module_target_sat, module_entitlement_manifest_org, module_lce, cv, c2r_sub.id ) + # UBI repo required for subscription-manager packages on Oracle ubi_url = settings.repos.convert2rhel.ubi7 if major == '7' else settings.repos.convert2rhel.ubi8 - ubi = create_repo(module_target_sat, module_entitlement_manifest_org, ubi_url) - ubi_repo = ubi.full_path.replace('https', 'http') - register_host( - module_target_sat, - act_key, - module_entitlement_manifest_org, - smart_proxy_location, - oracle_host, - ubi_repo, - ) + + # Register Oracle host with Satellite + command = module_target_sat.api.RegistrationCommand( + organization=module_entitlement_manifest_org, + activation_keys=[act_key.name], + location=smart_proxy_location, + insecure=True, + repo=ubi_url, + ).create() + assert oracle_host.execute(command).status == 0 + yield oracle_host # close ssh session before teardown, because of reboot in conversion it may cause problems oracle_host.close() @@ -240,11 +248,7 @@ def version(request): @pytest.mark.e2e -@pytest.mark.parametrize( - "version", - ['oracle7', 'oracle8'], - indirect=True, -) +@pytest.mark.parametrize('version', ['oracle7', 'oracle8'], indirect=True) def test_convert2rhel_oracle(module_target_sat, oracle, activation_key_rhel, version): """Convert Oracle linux to RHEL @@ -259,9 +263,16 @@ def test_convert2rhel_oracle(module_target_sat, oracle, activation_key_rhel, ver and subscription status :parametrized: yes - - :CaseImportance: Medium """ + major = version.split('.')[0] + assert oracle.execute('yum -y update').status == 0 + if major == '8': + # Fix inhibitor TAINTED_KMODS::TAINTED_KMODS_DETECTED - Tainted kernel modules detected + blacklist_cfg = '/etc/modprobe.d/blacklist.conf' + assert oracle.execute('modprobe -r nvme_tcp').status == 0 + assert oracle.execute(f'echo "blacklist nvme_tcp" >> {blacklist_cfg}').status == 0 + assert oracle.execute(f'echo "install nvme_tcp /bin/false" >> {blacklist_cfg}').status == 0 + host_content = module_target_sat.api.Host(id=oracle.hostname).read_json() assert host_content['operatingsystem_name'] == f"OracleLinux {version}" @@ -291,14 +302,19 @@ def test_convert2rhel_oracle(module_target_sat, oracle, activation_key_rhel, ver # check facts: correct os and valid subscription status host_content = module_target_sat.api.Host(id=oracle.hostname).read_json() - + # workaround for BZ 2080347 + assert ( + host_content['operatingsystem_name'].startswith(f'RHEL Server {version}') + or host_content['operatingsystem_name'].startswith(f'RedHat {version}') + or host_content['operatingsystem_name'].startswith(f'RHEL {version}') + ) assert host_content['subscription_status'] == 0 @pytest.mark.e2e @pytest.mark.parametrize('version', ['centos7', 'centos8'], indirect=True) def test_convert2rhel_centos(module_target_sat, centos, activation_key_rhel, version): - """Convert Centos linux to RHEL + """Convert CentOS linux to RHEL :id: 6f698440-7d85-4deb-8dd9-363ea9003b92 @@ -311,8 +327,6 @@ def test_convert2rhel_centos(module_target_sat, centos, activation_key_rhel, ver and subscription status :parametrized: yes - - :CaseImportance: Medium """ host_content = module_target_sat.api.Host(id=centos.hostname).read_json() major = version.split('.')[0] From fed520654b4b6273e48d09e8098bbc30d40d0380 Mon Sep 17 00:00:00 2001 From: vijay sawant Date: Wed, 28 Feb 2024 19:58:41 +0530 Subject: [PATCH 505/586] [6.14.z & 6.13.z] add test steps, refresh uploaded manifest file (#14208) --- tests/foreman/api/test_repository.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index 4c366dbdcd3..07058f6f331 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -1557,14 +1557,22 @@ def test_positive_sync_kickstart_check_os( ), indirect=True, ) - def test_missing_content_id(self, repo): + def test_missing_content_id(self, repo, function_entitlement_manifest_org, target_sat): """Handle several cases of missing content ID correctly :id: f507790a-933b-4b3f-ac93-cade6967fbd2 :parametrized: yes - :expectedresults: Repository URL can be set to something new and the repo can be deleted + :setup: + 1. Create product and repo, sync repo + + :steps: + 1. Try to update repo URL + 2. Attempt to delete repo + 3. Refresh manifest file + + :expectedresults: Repo URL can be updated, repo can be deleted and manifest refresh works after repo delete :BZ:2032040 """ @@ -1582,6 +1590,10 @@ def test_missing_content_id(self, repo): repo.delete() with pytest.raises(HTTPError): repo.read() + output = target_sat.cli.Subscription.refresh_manifest( + {'organization-id': function_entitlement_manifest_org.id} + ) + assert 'Candlepin job status: SUCCESS' in output, 'Failed to refresh manifest' class TestDockerRepository: From 28bf6cbdc393236d7a6014282ccc2b35f85c7fca Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 28 Feb 2024 10:23:40 -0500 Subject: [PATCH 506/586] [6.14.z] host package_list fix (#14213) --- robottelo/cli/hammer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/robottelo/cli/hammer.py b/robottelo/cli/hammer.py index 6e7b896a0b8..13cdfb41dbb 100644 --- a/robottelo/cli/hammer.py +++ b/robottelo/cli/hammer.py @@ -49,8 +49,9 @@ def parse_csv(output): # ignore warning about puppet and ostree deprecation output.replace('Puppet and OSTree will no longer be supported in Katello 3.16\n', '') is_rex = True if 'Job invocation' in output else False + is_pkg_list = True if 'Nvra' in output else False # Validate if the output is eligible for CSV conversions else return as it is - if not is_csv(output) and not is_rex: + if not is_csv(output) and not is_rex and not is_pkg_list: return output output = output.splitlines()[0:2] if is_rex else output.splitlines() reader = csv.reader(output) From 0818b0d4ddbe0eab65e36097042bc4bebb2d8c1c Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 28 Feb 2024 22:24:38 -0500 Subject: [PATCH 507/586] [6.14.z] Bump redis from 5.0.1 to 5.0.2 (#14222) --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index eb70684caa4..3971b77f3d9 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -1,7 +1,7 @@ # For running tests and checking code quality using these modules. flake8==7.0.0 pytest-cov==4.1.0 -redis==5.0.1 +redis==5.0.2 pre-commit==3.6.2 # For generating documentation. From 300f1e846871db77c417acc0ee7a898a1d7a58de Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 29 Feb 2024 11:49:09 -0500 Subject: [PATCH 508/586] [6.14.z] Add pit marker to ansible test (#14229) Add pit marker to ansible test (#14227) (cherry picked from commit 81a58af724b56760f1a3ebc7d29887e0c9426148) Co-authored-by: Shweta Singh --- tests/foreman/ui/test_ansible.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/foreman/ui/test_ansible.py b/tests/foreman/ui/test_ansible.py index a7a2ed28c06..2026ca8c095 100644 --- a/tests/foreman/ui/test_ansible.py +++ b/tests/foreman/ui/test_ansible.py @@ -91,6 +91,7 @@ def test_positive_create_variable_with_overrides(target_sat): assert session.ansiblevariables.search(key)[0]['Name'] == key +@pytest.mark.pit_server @pytest.mark.no_containers @pytest.mark.rhel_ver_match('[^6]') def test_positive_config_report_ansible(session, target_sat, module_org, rhel_contenthost): From 360c09ef650cb21c01ef4576f5fb6e48cde30c03 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 1 Mar 2024 10:40:16 -0500 Subject: [PATCH 509/586] [6.14.z] Call function parameters by key-value (#14240) --- tests/foreman/api/test_role.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/foreman/api/test_role.py b/tests/foreman/api/test_role.py index e44b0f8d844..1ce1014f269 100644 --- a/tests/foreman/api/test_role.py +++ b/tests/foreman/api/test_role.py @@ -1195,7 +1195,7 @@ def test_positive_taxonomies_control_to_superadmin_with_org_admin( # Creating resource dom_name = gen_string('alpha') dom = target_sat.api.Domain( - sc, + server_config=sc, name=dom_name, organization=[role_taxonomies['org']], location=[role_taxonomies['loc']], @@ -1239,7 +1239,7 @@ def test_positive_taxonomies_control_to_superadmin_without_org_admin( # Creating resource dom_name = gen_string('alpha') dom = target_sat.api.Domain( - sc, + server_config=sc, name=dom_name, organization=[role_taxonomies['org']], location=[role_taxonomies['loc']], From 65b1ab9c571b470fcfc7af88ee220f4f6c608fa8 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 4 Mar 2024 07:39:48 -0500 Subject: [PATCH 510/586] [6.14.z] More meaningful parametrization id (#14245) More meaningful parametrization id (#14223) (cherry picked from commit 413734e75568d5956bff88aa4fbf80b11dc12263) Co-authored-by: dosas --- tests/foreman/api/test_webhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/api/test_webhook.py b/tests/foreman/api/test_webhook.py index 82512e8eb10..bb6c3637c2b 100644 --- a/tests/foreman/api/test_webhook.py +++ b/tests/foreman/api/test_webhook.py @@ -75,7 +75,7 @@ def test_negative_invalid_event(self, target_sat): target_sat.api.Webhooks(event='invalid_event').create() @pytest.mark.tier2 - @pytest.mark.parametrize('event', **parametrized(WEBHOOK_EVENTS)) + @pytest.mark.parametrize('event', WEBHOOK_EVENTS) def test_positive_valid_event(self, event, target_sat): """Test positive webhook creation with a valid event From 12f201590aeb92fc9424807381be035b70abd649 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:37:07 -0500 Subject: [PATCH 511/586] [6.14.z] forgotten target sat in host cli tests (#14254) --- tests/foreman/cli/test_host.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/tests/foreman/cli/test_host.py b/tests/foreman/cli/test_host.py index 0390f79bd3a..6e0f025e76c 100644 --- a/tests/foreman/cli/test_host.py +++ b/tests/foreman/cli/test_host.py @@ -2456,15 +2456,15 @@ def test_positive_host_with_puppet( location=[host_template.location], ).update(['location', 'organization']) - session_puppet_enabled_sat.cli.target_sat.cli.Host.update( + session_puppet_enabled_sat.cli.Host.update( { 'name': host.name, 'puppet-environment': module_puppet_environment.name, } ) - host = session_puppet_enabled_sat.cli.target_sat.cli.Host.info({'id': host['id']}) + host = session_puppet_enabled_sat.cli.Host.info({'id': host['id']}) assert host['puppet-environment'] == module_puppet_environment.name - session_puppet_enabled_sat.cli.target_sat.cli.Host.delete({'id': host['id']}) + session_puppet_enabled_sat.cli.Host.delete({'id': host['id']}) @pytest.fixture @@ -2536,9 +2536,7 @@ class are listed scp_id = choice(sc_params_list)['id'] session_puppet_enabled_sat.cli.SmartClassParameter.update({'id': scp_id, 'override': 1}) # Verify that affected sc-param is listed - host_scparams = session_puppet_enabled_sat.cli.target_sat.cli.Host.sc_params( - {'host': host['name']} - ) + host_scparams = session_puppet_enabled_sat.cli.Host.sc_params({'host': host['name']}) assert scp_id in [scp['id'] for scp in host_scparams] @@ -2573,9 +2571,7 @@ def test_positive_create_with_puppet_class_name( 'puppet-proxy-id': session_puppet_enabled_proxy.id, } ) - host_classes = session_puppet_enabled_sat.cli.target_sat.cli.Host.puppetclasses( - {'host': host['name']} - ) + host_classes = session_puppet_enabled_sat.cli.Host.puppetclasses({'host': host['name']}) assert module_puppet_classes[0].name in [puppet['name'] for puppet in host_classes] @@ -2616,21 +2612,17 @@ def test_positive_update_host_owner_and_verify_puppet_class_name( 'puppet-proxy-id': session_puppet_enabled_proxy.id, } ) - host_classes = session_puppet_enabled_sat.cli.target_sat.cli.Host.puppetclasses( - {'host': host['name']} - ) + host_classes = session_puppet_enabled_sat.cli.Host.puppetclasses({'host': host['name']}) assert module_puppet_classes[0].name in [puppet['name'] for puppet in host_classes] - session_puppet_enabled_sat.cli.target_sat.cli.Host.update( + session_puppet_enabled_sat.cli.Host.update( {'id': host['id'], 'owner': module_puppet_user.login, 'owner-type': 'User'} ) - host = session_puppet_enabled_sat.cli.target_sat.cli.Host.info({'id': host['id']}) + host = session_puppet_enabled_sat.cli.Host.info({'id': host['id']}) assert int(host['additional-info']['owner-id']) == module_puppet_user.id assert host['additional-info']['owner-type'] == 'User' - host_classes = session_puppet_enabled_sat.cli.target_sat.cli.Host.puppetclasses( - {'host': host['name']} - ) + host_classes = session_puppet_enabled_sat.cli.Host.puppetclasses({'host': host['name']}) assert module_puppet_classes[0].name in [puppet['name'] for puppet in host_classes] From 2221fa315e6805f7d4985727850ee0150b5d53aa Mon Sep 17 00:00:00 2001 From: Gaurav Talreja Date: Tue, 5 Mar 2024 13:12:32 +0530 Subject: [PATCH 512/586] [6.14.z] Bump Pytest to 8.0.2 (#14256) Signed-off-by: Gaurav Talreja --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7c6f39b36f5..bd6333abfe5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ paramiko==3.4.0 # Temporary until Broker is back on PyPi productmd==1.38 pyotp==2.9.0 python-box==7.1.1 -pytest==7.4.4 +pytest==8.0.2 pytest-services==2.2.1 pytest-mock==3.12.0 pytest-reportportal==5.3.1 From cd79a3dcec4c025b35becd3727a67d9e74ed84b1 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 5 Mar 2024 03:18:25 -0500 Subject: [PATCH 513/586] [6.14.z] Bump cryptography from 42.0.2 to 42.0.5 (#14262) [6.15.z] Bump cryptography from 42.0.2 to 42.0.5 (#14255) Signed-off-by: Gaurav Talreja (cherry picked from commit ca81848b56b6e01a1281d0e8fdac409d0d36316d) Co-authored-by: Gaurav Talreja --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bd6333abfe5..58981e9d167 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ betelgeuse==1.11.0 broker[docker]==0.4.1 -cryptography==42.0.2 +cryptography==42.0.5 deepdiff==6.7.1 docker==7.0.0 # Temporary until Broker is back on PyPi dynaconf[vault]==3.2.4 From f621765b4c096e44011c7b3758d1a69fe50fa06b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 5 Mar 2024 10:15:13 -0500 Subject: [PATCH 514/586] [6.14.z] Fail curl command registration (#14267) --- tests/foreman/api/test_registration.py | 37 ++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/foreman/api/test_registration.py b/tests/foreman/api/test_registration.py index e364c2accca..c982b02ab90 100644 --- a/tests/foreman/api/test_registration.py +++ b/tests/foreman/api/test_registration.py @@ -327,3 +327,40 @@ def test_positive_host_registration_with_non_admin_user_with_setup_false( assert '# Updating packages' not in result.stdout # verify foreman-proxy ssh pubkey isn't present when Setup REX is false assert rhel_contenthost.execute('cat ~/.ssh/authorized_keys | grep foreman-proxy').status == 1 + + +@pytest.mark.rhel_ver_match('[^6]') +def test_negative_verify_bash_exit_status_failing_host_registration( + module_sca_manifest_org, + module_location, + module_target_sat, + rhel_contenthost, +): + """Verify status code, when curl command registration fail intentionally + + :id: 4789e8da-6391-4ea4-aa0d-73c93220ce44 + + :steps: + 1. Generate a curl command and make the registration fail intentionally. + 2. Check the exit code for the command. + + :expectedresults: Exit code returns 1 if registration fails. + + :BZ: 2155444 + + :customerscenario: true + + :parametrized: yes + """ + ak = module_target_sat.api.ActivationKey(name=gen_string('alpha')).create() + # Try registration command generated with AK not in same as selected organization + command = module_target_sat.api.RegistrationCommand( + organization=module_sca_manifest_org, + activation_keys=[ak.name], + location=module_location, + ).create() + result = rhel_contenthost.execute(command) + + # verify status code when registrationCommand fails to register on host + assert result.status == 1 + assert 'Couldn\'t find activation key' in result.stderr From bcdc276aa869c87ba8efb9313b597b4bb51ba6e8 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 6 Mar 2024 02:58:04 -0500 Subject: [PATCH 515/586] [6.14.z] Update HTTP Proxy e2e test cases (#14258) * Update HTTP Proxy e2e test cases (#14215) * Extend HTTP proxy e2e test by other content types * Minor updates of other HTTP proxy e2es (cherry picked from commit 485fbad5d2eb07a74c19f95181887c2ffd91286e) * Add FileRepository * Add AnsibleCollection support --------- Co-authored-by: vsedmik <46570670+vsedmik@users.noreply.github.com> --- robottelo/constants/__init__.py | 2 +- robottelo/host_helpers/repository_mixins.py | 34 ++++ tests/foreman/api/test_http_proxy.py | 191 +++++++++++--------- tests/foreman/cli/test_http_proxy.py | 45 +++-- tests/foreman/ui/test_http_proxy.py | 22 ++- tests/foreman/ui/test_repository.py | 2 +- 6 files changed, 181 insertions(+), 115 deletions(-) diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index e789e5e6bdf..4526f338648 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -223,7 +223,7 @@ class Colored(Box): 'yum': "yum", 'ostree': "ostree", 'docker': "docker", - 'ansible_collection': "ansible collection", + 'ansible_collection': "ansible_collection", 'file': "file", } diff --git a/robottelo/host_helpers/repository_mixins.py b/robottelo/host_helpers/repository_mixins.py index 6becbe29325..f3cf54d3580 100644 --- a/robottelo/host_helpers/repository_mixins.py +++ b/robottelo/host_helpers/repository_mixins.py @@ -123,6 +123,12 @@ class YumRepository(BaseRepository): _type = constants.REPO_TYPE['yum'] +class FileRepository(BaseRepository): + """Custom File repository""" + + _type = constants.REPO_TYPE['file'] + + class DebianRepository(BaseRepository): """Custom Debian repository.""" @@ -198,6 +204,34 @@ def create(self, organization_id, product_id, download_policy=None, synchronize= return repo_info +class AnsibleRepository(BaseRepository): + """Custom Ansible Collection repository""" + + _type = constants.REPO_TYPE['ansible_collection'] + + def __init__(self, url=None, distro=None, requirements=None): + self._requirements = requirements + super().__init__(url=url, distro=distro) + + @property + def requirements(self): + return self._requirements + + def create(self, organization_id, product_id, download_policy=None, synchronize=True): + repo_info = self.satellite.cli_factory.make_repository( + { + 'product-id': product_id, + 'content-type': self.content_type, + 'url': self.url, + 'ansible-collection-requirements': f'{{collections: {self.requirements}}}', + } + ) + self._repo_info = repo_info + if synchronize: + self.synchronize() + return repo_info + + class OSTreeRepository(BaseRepository): """Custom OSTree repository""" diff --git a/tests/foreman/api/test_http_proxy.py b/tests/foreman/api/test_http_proxy.py index 4fff1c3bc5f..d69fc7e61d5 100644 --- a/tests/foreman/api/test_http_proxy.py +++ b/tests/foreman/api/test_http_proxy.py @@ -16,6 +16,11 @@ from robottelo import constants from robottelo.config import settings +from robottelo.constants import ( + CONTAINER_REGISTRY_HUB, + CONTAINER_UPSTREAM_NAME, +) +from robottelo.constants.repos import ANSIBLE_GALAXY, CUSTOM_FILE_REPO @pytest.mark.tier2 @@ -24,27 +29,58 @@ @pytest.mark.run_in_one_thread @pytest.mark.parametrize( 'setup_http_proxy', - [None, True, False], + [True, False], indirect=True, - ids=['no_http_proxy', 'auth_http_proxy', 'unauth_http_proxy'], + ids=['auth_http_proxy', 'unauth_http_proxy'], ) -def test_positive_end_to_end(setup_http_proxy, module_target_sat, module_manifest_org): +@pytest.mark.parametrize( + 'module_repos_collection_with_manifest', + [ + { + 'distro': 'rhel7', + 'RHELAnsibleEngineRepository': {'cdn': True}, + 'YumRepository': {'url': settings.repos.module_stream_1.url}, + 'FileRepository': {'url': CUSTOM_FILE_REPO}, + 'DockerRepository': { + 'url': CONTAINER_REGISTRY_HUB, + 'upstream_name': CONTAINER_UPSTREAM_NAME, + }, + 'AnsibleRepository': { + 'url': ANSIBLE_GALAXY, + 'requirements': [ + {'name': 'theforeman.foreman', 'version': '2.1.0'}, + {'name': 'theforeman.operations', 'version': '0.1.0'}, + ], + }, + } + ], + indirect=True, +) +def test_positive_end_to_end( + setup_http_proxy, module_target_sat, module_org, module_repos_collection_with_manifest +): """End-to-end test for HTTP Proxy related scenarios. :id: 38df5479-9127-49f3-a30e-26b33655971a :customerscenario: true - :steps: - 1. Set Http proxy settings for Satellite. - 2. Enable and sync redhat repository. - 3. Assign Http Proxy to custom repository and perform repo sync. - 4. Discover yum type repo. - 5. Discover docker type repo. + :setup: + 1. Create HTTP proxy entity at the Satellite. + 2. Create RH yum repository. + 3. Create custom repo of each content type (yum, file, docker, ansible collection). - :expectedresults: HTTP Proxy works with other satellite components. + :steps: + 1. Set immediate download policy where applicable for complete sync testing. + 2. For each repo set global default HTTP proxy and sync it. + 3. For each repo set specific HTTP proxy and sync it. + 4. For each repo set no HTTP proxy and sync it. + 5. Discover yum type repo through HTTP proxy. + 6. Discover docker type repo through HTTP proxy. - :Team: Phoenix-content + :expectedresults: + 1. All repository updates and syncs succeed. + 2. Yum and docker repos can be discovered through HTTP proxy. :BZ: 2011303, 2042473, 2046337 @@ -52,59 +88,36 @@ def test_positive_end_to_end(setup_http_proxy, module_target_sat, module_manifes :CaseImportance: Critical """ - http_proxy, http_proxy_type = setup_http_proxy - http_proxy_id = http_proxy.id if http_proxy_type is not None else None - http_proxy_policy = 'use_selected_http_proxy' if http_proxy_type is not None else 'none' - # Assign http_proxy to Redhat repository and perform repository sync. - rh_repo_id = module_target_sat.api_factory.enable_rhrepo_and_fetchid( - basearch=constants.DEFAULT_ARCHITECTURE, - org_id=module_manifest_org.id, - product=constants.PRDS['rhae'], - repo=constants.REPOS['rhae2']['name'], - reposet=constants.REPOSET['rhae2'], - releasever=None, - ) - module_target_sat.api.Repository(id=rh_repo_id).sync() - rh_repo = module_target_sat.api.Repository( - id=rh_repo_id, - http_proxy_policy=http_proxy_policy, - http_proxy_id=http_proxy_id, - download_policy='immediate', - ).update() - assert rh_repo.http_proxy_policy == http_proxy_policy - assert rh_repo.http_proxy_id == http_proxy_id - assert rh_repo.download_policy == 'immediate' - rh_repo.sync() - assert rh_repo.read().content_counts['rpm'] >= 1 - - # Assign http_proxy to Repositories and perform repository sync. - repo_options = { - 'http_proxy_policy': http_proxy_policy, - 'http_proxy_id': http_proxy_id, - } - repo = module_target_sat.api.Repository(**repo_options).create() - - assert repo.http_proxy_policy == http_proxy_policy - assert repo.http_proxy_id == http_proxy_id - repo.sync() - assert repo.read().content_counts['rpm'] >= 1 - - # Use global_default_http_proxy - repo_options['http_proxy_policy'] = 'global_default_http_proxy' - repo_2 = module_target_sat.api.Repository(**repo_options).create() - repo_2.sync() - assert repo_2.http_proxy_policy == 'global_default_http_proxy' - - # Update to selected_http_proxy - repo_2.http_proxy_policy = 'none' - repo_2.update(['http_proxy_policy']) - assert repo_2.read().http_proxy_policy == 'none' - - # test scenario for yum type repo discovery. + # Set immediate download policy where applicable for complete sync testing + for repo in module_repos_collection_with_manifest.repos_info: + if repo['content-type'] in ['yum', 'docker']: + module_target_sat.api.Repository(id=repo['id'], download_policy='immediate').update() + + # For each repo set global/specific/no HTTP proxy and sync it + for policy in ['global_default_http_proxy', 'use_selected_http_proxy', 'none']: + for repo in module_repos_collection_with_manifest.repos_info: + repo = module_target_sat.api.Repository( + id=repo['id'], + http_proxy_policy=policy, + http_proxy_id=setup_http_proxy[0].id if 'selected' in policy else None, + ).update() + assert ( + repo.http_proxy_policy == policy + ), f'Policy update failed for {repo.content_type} repo with {policy} HTTP policy' + assert ( + repo.http_proxy_id == setup_http_proxy[0].id + if 'selected' in policy + else repo.http_proxy_id is None + ), f'Proxy id update failed for {repo.content_type} repo with {policy} HTTP policy' + assert ( + 'success' in module_target_sat.api.Repository(id=repo.id).sync()['result'] + ), f'Sync of a {repo.content_type} repo with {policy} HTTP policy failed' + + # Discover yum type repo through HTTP proxy repo_name = 'fakerepo01' - yum_repo = module_target_sat.api.Organization(id=module_manifest_org.id).repo_discover( + yum_repo = module_target_sat.api.Organization(id=module_org.id).repo_discover( data={ - "id": module_manifest_org.id, + "id": module_org.id, "url": settings.repos.repo_discovery.url, "content_type": "yum", } @@ -112,10 +125,10 @@ def test_positive_end_to_end(setup_http_proxy, module_target_sat, module_manifes assert len(yum_repo['output']) == 1 assert yum_repo['output'][0] == f'{settings.repos.repo_discovery.url}/{repo_name}/' - # test scenario for docker type repo discovery. - docker_repo = module_target_sat.api.Organization(id=module_manifest_org.id).repo_discover( + # Discover docker type repo through HTTP proxy + docker_repo = module_target_sat.api.Organization(id=module_org.id).repo_discover( data={ - "id": module_manifest_org.id, + "id": module_org.id, "url": 'quay.io', "content_type": "docker", "search": 'foreman/foreman', @@ -200,38 +213,42 @@ def test_positive_auto_attach_with_http_proxy( @pytest.mark.e2e @pytest.mark.tier2 -def test_positive_assign_http_proxy_to_products(target_sat): +def test_positive_assign_http_proxy_to_products(target_sat, function_org): """Assign http_proxy to Products and check whether http-proxy is used during sync. :id: c9d23aa1-3325-4abd-a1a6-d5e75c12b08a - :expectedresults: HTTP Proxy is assigned to all repos present - in Products and sync operation uses assigned http-proxy. + :setup: + 1. Create an Organization. - :Team: Phoenix-content - - :CaseImportance: Critical + :steps: + 1. Create two HTTP proxies. + 2. Create two products and two repos in each product with various HTTP proxy policies. + 3. Set the HTTP proxy through bulk action for both products. + 4. Bulk sync one product. + + :expectedresults: + 1. HTTP Proxy is assigned to all repos present in Products + and sync operation uses assigned http-proxy and pass. """ - org = target_sat.api.Organization().create() - # create HTTP proxies + # Create two HTTP proxies http_proxy_a = target_sat.api.HTTPProxy( name=gen_string('alpha', 15), url=settings.http_proxy.un_auth_proxy_url, - organization=[org], + organization=[function_org], ).create() - http_proxy_b = target_sat.api.HTTPProxy( name=gen_string('alpha', 15), url=settings.http_proxy.auth_proxy_url, username=settings.http_proxy.username, password=settings.http_proxy.password, - organization=[org], + organization=[function_org], ).create() - # Create products and repositories - product_a = target_sat.api.Product(organization=org).create() - product_b = target_sat.api.Product(organization=org).create() + # Create two products and two repos in each product with various HTTP proxy policies + product_a = target_sat.api.Product(organization=function_org).create() + product_b = target_sat.api.Product(organization=function_org).create() repo_a1 = target_sat.api.Repository(product=product_a, http_proxy_policy='none').create() repo_a2 = target_sat.api.Repository( product=product_a, @@ -242,7 +259,8 @@ def test_positive_assign_http_proxy_to_products(target_sat): repo_b2 = target_sat.api.Repository( product=product_b, http_proxy_policy='global_default_http_proxy' ).create() - # Add http_proxy to products + + # Set the HTTP proxy through bulk action for both products target_sat.api.ProductBulkAction().http_proxy( data={ "ids": [product_a.id, product_b.id], @@ -250,13 +268,11 @@ def test_positive_assign_http_proxy_to_products(target_sat): "http_proxy_id": http_proxy_b.id, } ) - for repo in repo_a1, repo_a2, repo_b1, repo_b2: r = repo.read() assert r.http_proxy_policy == "use_selected_http_proxy" assert r.http_proxy_id == http_proxy_b.id - - product_a.sync({'async': True}) + assert 'success' in product_a.sync()['result'], 'Product sync failed' @pytest.mark.tier2 @@ -286,6 +302,11 @@ def test_positive_sync_proxy_with_certificate(request, target_sat, module_org, m # Create and fetch new cerfiticate target_sat.custom_cert_generate(proxy_host) + + @request.addfinalizer + def _finalize(): + target_sat.custom_certs_cleanup() + cacert = target_sat.execute(f'cat {cacert_path}').stdout assert 'END CERTIFICATE' in cacert @@ -312,7 +333,3 @@ def test_positive_sync_proxy_with_certificate(request, target_sat, module_org, m assert response.get('errors') is None assert repo.read().last_sync is not None assert repo.read().content_counts['rpm'] >= 1 - - @request.addfinalizer - def _finalize(): - target_sat.custom_certs_cleanup() diff --git a/tests/foreman/cli/test_http_proxy.py b/tests/foreman/cli/test_http_proxy.py index 11539a65d6b..fc0028eed78 100644 --- a/tests/foreman/cli/test_http_proxy.py +++ b/tests/foreman/cli/test_http_proxy.py @@ -111,8 +111,6 @@ def test_insights_client_registration_with_http_proxy(): works with http proxy set. :CaseAutomation: NotAutomated - - :CaseImportance: High """ @@ -132,8 +130,6 @@ def test_positive_set_content_default_http_proxy(block_fake_repo_access, target_ 4. Sync a repo. :expectedresults: Repo is synced - - :CaseImportance: High """ org = target_sat.api.Organization().create() proxy_name = gen_string('alpha', 15) @@ -186,8 +182,6 @@ def test_positive_environment_variable_unset_set(): :expectedresults: satellite-installer unsets system proxy and SSL environment variables only for the duration of install and sets back those in the end. - :CaseImportance: High - :CaseAutomation: NotAutomated """ @@ -200,12 +194,22 @@ def test_positive_assign_http_proxy_to_products(module_org, module_target_sat): :id: 6af7b2b8-15d5-4d9f-9f87-e76b404a966f - :expectedresults: HTTP Proxy is assigned to all repos present - in Products and sync operation performed successfully. + :steps: + 1. Create two HTTP proxies. + 2. Create two products and two repos in each product with various HTTP proxy policies. + 3. Set the HTTP proxy through bulk action for both products to the selected proxy. + 4. Bulk sync both products and verify packages counts. + 5. Set the HTTP proxy through bulk action for both products to None. + + :expectedresults: + 1. HTTP Proxy is assigned to all repos present in Products + and sync operation uses assigned http-proxy and pass. - :CaseImportance: High + :expectedresults: + 1. HTTP Proxy is assigned to all repos present in Products + and sync operation performed successfully. """ - # create HTTP proxies + # Create two HTTP proxies http_proxy_a = module_target_sat.cli.HttpProxy.create( { 'name': gen_string('alpha', 15), @@ -222,7 +226,8 @@ def test_positive_assign_http_proxy_to_products(module_org, module_target_sat): 'organization-id': module_org.id, }, ) - # Create products and repositories + + # Create two products and two repos in each product with various HTTP proxy policies product_a = module_target_sat.cli_factory.make_product({'organization-id': module_org.id}) product_b = module_target_sat.cli_factory.make_product({'organization-id': module_org.id}) repo_a1 = module_target_sat.cli_factory.make_repository( @@ -257,31 +262,37 @@ def test_positive_assign_http_proxy_to_products(module_org, module_target_sat): 'url': settings.repos.yum_0.url, }, ) - # Add http_proxy to products - module_target_sat.cli.Product.update_proxy( + + # Set the HTTP proxy through bulk action for both products to the selected proxy + res = module_target_sat.cli.Product.update_proxy( { 'ids': f"{product_a['id']},{product_b['id']}", 'http-proxy-policy': 'use_selected_http_proxy', 'http-proxy-id': http_proxy_b['id'], } ) + assert 'Product proxy updated' in res for repo in repo_a1, repo_a2, repo_b1, repo_b2: result = module_target_sat.cli.Repository.info({'id': repo['id']}) assert result['http-proxy']['http-proxy-policy'] == 'use_selected_http_proxy' assert result['http-proxy']['id'] == http_proxy_b['id'] - # Perform sync and verify packages count + + # Bulk sync both products and verify packages counts module_target_sat.cli.Product.synchronize( {'id': product_a['id'], 'organization-id': module_org.id} ) module_target_sat.cli.Product.synchronize( {'id': product_b['id'], 'organization-id': module_org.id} ) + for repo in repo_a1, repo_a2, repo_b1, repo_b2: + info = module_target_sat.cli.Repository.info({'id': repo['id']}) + assert int(info['content-counts']['packages']) == FAKE_0_YUM_REPO_PACKAGES_COUNT - module_target_sat.cli.Product.update_proxy( + # Set the HTTP proxy through bulk action for both products to None + res = module_target_sat.cli.Product.update_proxy( {'ids': f"{product_a['id']},{product_b['id']}", 'http-proxy-policy': 'none'} ) - + assert 'Product proxy updated' in res for repo in repo_a1, repo_a2, repo_b1, repo_b2: result = module_target_sat.cli.Repository.info({'id': repo['id']}) assert result['http-proxy']['http-proxy-policy'] == 'none' - assert int(result['content-counts']['packages']) == FAKE_0_YUM_REPO_PACKAGES_COUNT diff --git a/tests/foreman/ui/test_http_proxy.py b/tests/foreman/ui/test_http_proxy.py index 12da7cf798f..9e3f1e5b10b 100644 --- a/tests/foreman/ui/test_http_proxy.py +++ b/tests/foreman/ui/test_http_proxy.py @@ -73,12 +73,18 @@ def test_positive_assign_http_proxy_to_products_repositories( :id: 2b803f9c-8d5d-4467-8eba-18244ebc0201 - :expectedresults: HTTP Proxy is assigned to all repos present - in Products. + :setup: + 1. Create an Organization and Location. - :CaseImportance: Critical + :steps: + 1. Create two HTTP proxies. + 2. Create two products and two repos in each product with various HTTP proxy policies. + 3. Set the HTTP proxy through bulk action for both products. + + :expectedresults: + 1. HTTP Proxy is assigned to all repos present in Products. """ - # create HTTP proxies + # Create two HTTP proxies http_proxy_a = target_sat.api.HTTPProxy( name=gen_string('alpha', 15), url=settings.http_proxy.un_auth_proxy_url, @@ -93,14 +99,14 @@ def test_positive_assign_http_proxy_to_products_repositories( organization=[module_org.id], location=[module_location.id], ).create() - # Create products + # Create two products product_a = target_sat.api.Product( organization=module_org.id, ).create() product_b = target_sat.api.Product( organization=module_org.id, ).create() - # Create repositories from UI. + # Create two repositories in each product from UI with target_sat.ui_session() as session: repo_a1_name = gen_string('alpha') session.organization.select(org_name=module_org.name) @@ -152,7 +158,7 @@ def test_positive_assign_http_proxy_to_products_repositories( 'repo_content.http_proxy_policy': 'No HTTP Proxy', }, ) - # Add http_proxy to products + # Set the HTTP proxy through bulk action for both products session.product.search('') session.product.manage_http_proxy( [product_a.name, product_b.name], @@ -338,8 +344,6 @@ def test_positive_repo_discovery(setup_http_proxy, module_target_sat, module_org :expectedresults: Repository is discovered and created. - :team: Phoenix-content - :BZ: 2011303, 2042473 :parametrized: yes diff --git a/tests/foreman/ui/test_repository.py b/tests/foreman/ui/test_repository.py index 5f6e4355b6a..1d9d40e10bb 100644 --- a/tests/foreman/ui/test_repository.py +++ b/tests/foreman/ui/test_repository.py @@ -673,7 +673,7 @@ def test_positive_sync_ansible_collection_gallaxy_repo(session, module_prod): module_prod.name, { 'name': repo_name, - 'repo_type': REPO_TYPE['ansible_collection'], + 'repo_type': REPO_TYPE['ansible_collection'].replace('_', ' '), 'repo_content.requirements': requirements, 'repo_content.upstream_url': ANSIBLE_GALAXY, }, From e715d3e604f008356be8d5b6546552d8c9800426 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Mar 2024 04:27:39 -0500 Subject: [PATCH 516/586] [6.14.z] add verify virt-who data is uploaded on satellite post satellite upgrade (#14277) add verify virt-who data is uploaded on satellite post satellite upgrade (#14260) (cherry picked from commit d7b6d8b11e74d7cf1b3341d8046202f960ce46f0) Co-authored-by: yanpliu --- tests/upgrades/test_virtwho.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/upgrades/test_virtwho.py b/tests/upgrades/test_virtwho.py index ca785ab0149..3691e3a8d70 100644 --- a/tests/upgrades/test_virtwho.py +++ b/tests/upgrades/test_virtwho.py @@ -118,6 +118,8 @@ def test_pre_create_virt_who_configuration( 'hypervisor_name': hypervisor_name, 'guest_name': guest_name, 'org_id': org.id, + 'org_name': org.name, + 'org_label': org.label, } ) @@ -131,15 +133,19 @@ def test_post_crud_virt_who_configuration(self, form_data, pre_upgrade_data, tar 1. Post upgrade, Verify virt-who exists and has same status. 2. Verify the connection of the guest on Content host. 3. Verify the virt-who config-file exists. - 4. Update virt-who config with new name. - 5. Delete virt-who config. + 4. Verify Report is sent to satellite. + 5. Update virt-who config with new name. + 6. Delete virt-who config. :expectedresults: 1. virt-who config is intact post upgrade. 2. the config and guest connection have the same status. - 3. virt-who config should update and delete successfully. + 3. Report is sent to satellite. + 4. virt-who config should update and delete successfully. """ org_id = pre_upgrade_data.get('org_id') + org_name = pre_upgrade_data.get('org_name') + org_label = pre_upgrade_data.get('org_label') # Post upgrade, Verify virt-who exists and has same status. vhd = target_sat.api.VirtWhoConfig(organization_id=org_id).search( @@ -172,6 +178,18 @@ def test_post_crud_virt_who_configuration(self, form_data, pre_upgrade_data, tar config_file = get_configure_file(vhd.id) get_configure_option('hypervisor_id', config_file), + # Verify Report is sent to satellite. + command = get_configure_command(vhd.id, org=org_name) + deploy_configure_by_command( + command, form_data['hypervisor_type'], debug=True, org=org_label + ) + virt_who_instance = ( + target_sat.api.VirtWhoConfig(organization_id=org_id) + .search(query={'search': f'name={form_data["name"]}'})[0] + .status + ) + assert virt_who_instance == 'ok' + # Update virt-who config modify_name = gen_string('alpha') vhd.name = modify_name From 2cf618d50f5922c9b978253caac285fb72728aca Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Mar 2024 07:12:49 -0500 Subject: [PATCH 517/586] [6.14.z] Fix pytest deprecation warning (#14283) Fix pytest deprecation warning (#14205) Fix pytest deprecation warning: Applying a mark to a fixture function never had any effect but is a common user error. see: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function (cherry picked from commit 215ddea047c05d936f764a93800a9a6ee9da6a97) Co-authored-by: dosas --- pytest_fixtures/component/host.py | 1 - pytest_fixtures/component/puppet.py | 2 -- pytest_fixtures/component/satellite_auth.py | 8 -------- tests/foreman/api/test_contentview.py | 7 ------- tests/foreman/cli/test_computeresource_libvirt.py | 1 - tests/foreman/cli/test_host.py | 1 - tests/foreman/destructive/test_ldap_authentication.py | 1 - 7 files changed, 21 deletions(-) diff --git a/pytest_fixtures/component/host.py b/pytest_fixtures/component/host.py index 11645f89f84..ece968e28e7 100644 --- a/pytest_fixtures/component/host.py +++ b/pytest_fixtures/component/host.py @@ -21,7 +21,6 @@ def module_model(): return entities.Model().create() -@pytest.mark.skip_if_not_set('clients', 'fake_manifest') @pytest.fixture(scope="module") def setup_rhst_repo(module_target_sat): """Prepare Satellite tools repository for usage in specified organization""" diff --git a/pytest_fixtures/component/puppet.py b/pytest_fixtures/component/puppet.py index 0ff20685674..38cad7163c0 100644 --- a/pytest_fixtures/component/puppet.py +++ b/pytest_fixtures/component/puppet.py @@ -1,7 +1,6 @@ # Puppet Environment fixtures import pytest -from robottelo.config import settings from robottelo.constants import ENVIRONMENT @@ -55,7 +54,6 @@ def module_puppet_environment(module_puppet_org, module_puppet_loc, session_pupp return session_puppet_enabled_sat.api.Environment(id=environment.id).read() -@pytest.mark.skipif((not settings.robottelo.repos_hosting_url), reason='Missing repos_hosting_url') @pytest.fixture(scope='module') def module_import_puppet_module(session_puppet_enabled_sat): """Returns custom puppet environment name that contains imported puppet module diff --git a/pytest_fixtures/component/satellite_auth.py b/pytest_fixtures/component/satellite_auth.py index 0a1031c9e25..4eb984390f6 100644 --- a/pytest_fixtures/component/satellite_auth.py +++ b/pytest_fixtures/component/satellite_auth.py @@ -334,7 +334,6 @@ def enable_external_auth_rhsso( default_sso_host.set_the_redirect_uri() -@pytest.mark.external_auth @pytest.fixture(scope='module') def module_enroll_idm_and_configure_external_auth(module_target_sat): ipa_host = IPAHost(module_target_sat) @@ -343,7 +342,6 @@ def module_enroll_idm_and_configure_external_auth(module_target_sat): ipa_host.disenroll_idm() -@pytest.mark.external_auth @pytest.fixture def func_enroll_idm_and_configure_external_auth(target_sat): ipa_host = IPAHost(target_sat) @@ -410,19 +408,16 @@ def rhsso_setting_setup_with_timeout(module_target_sat, rhsso_setting_setup): setting_entity.update({'value'}) -@pytest.mark.external_auth @pytest.fixture(scope='module') def module_enroll_ad_and_configure_external_auth(ad_data, module_target_sat): module_target_sat.enroll_ad_and_configure_external_auth(ad_data) -@pytest.mark.external_auth @pytest.fixture def func_enroll_ad_and_configure_external_auth(ad_data, target_sat): target_sat.enroll_ad_and_configure_external_auth(ad_data) -@pytest.mark.external_auth @pytest.fixture def configure_hammer_no_creds(parametrized_enrolled_sat): """Configures hammer to use sessions and negotiate auth.""" @@ -433,7 +428,6 @@ def configure_hammer_no_creds(parametrized_enrolled_sat): parametrized_enrolled_sat.execute(f'mv -f {HAMMER_CONFIG}.backup {HAMMER_CONFIG}') -@pytest.mark.external_auth @pytest.fixture def configure_hammer_negotiate(parametrized_enrolled_sat, configure_hammer_no_creds): """Configures hammer to use sessions and negotiate auth.""" @@ -448,7 +442,6 @@ def configure_hammer_negotiate(parametrized_enrolled_sat, configure_hammer_no_cr parametrized_enrolled_sat.execute(f'mv -f {HAMMER_CONFIG}.backup {HAMMER_CONFIG}') -@pytest.mark.external_auth @pytest.fixture def configure_hammer_no_negotiate(parametrized_enrolled_sat): """Configures hammer not to use automatic negotiation.""" @@ -458,7 +451,6 @@ def configure_hammer_no_negotiate(parametrized_enrolled_sat): parametrized_enrolled_sat.execute(f'mv -f {HAMMER_CONFIG}.backup {HAMMER_CONFIG}') -@pytest.mark.external_auth @pytest.fixture def hammer_logout(parametrized_enrolled_sat): """Logout in Hammer.""" diff --git a/tests/foreman/api/test_contentview.py b/tests/foreman/api/test_contentview.py index 0ef7ac02105..ee6cfafdd00 100644 --- a/tests/foreman/api/test_contentview.py +++ b/tests/foreman/api/test_contentview.py @@ -369,9 +369,6 @@ def test_negative_create_with_invalid_name(self, name, target_sat): class TestContentViewPublishPromote: """Tests for publishing and promoting content views.""" - @pytest.mark.skipif( - (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' - ) @pytest.fixture(scope='class', autouse=True) def class_setup(self, request, module_product, class_target_sat): """Set up organization, product and repositories for tests.""" @@ -1421,9 +1418,6 @@ def test_negative_non_readonly_user_actions(target_sat, content_view, function_r class TestOstreeContentView: """Tests for ostree contents in content views.""" - @pytest.mark.skipif( - (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' - ) @pytest.fixture(scope='class', autouse=True) def initiate_testclass(self, request, module_product, class_target_sat): """Set up organization, product and repositories for tests.""" @@ -1528,7 +1522,6 @@ def test_positive_publish_promote_with_custom_ostree_and_other(self, content_vie class TestContentViewRedHatOstreeContent: """Tests for publishing and promoting cv with RH ostree contents.""" - @pytest.mark.run_in_one_thread @pytest.fixture(scope='class', autouse=True) def initiate_testclass(self, request, module_entitlement_manifest_org, class_target_sat): """Set up organization, product and repositories for tests.""" diff --git a/tests/foreman/cli/test_computeresource_libvirt.py b/tests/foreman/cli/test_computeresource_libvirt.py index 7f4fdd7526a..a53959f277b 100644 --- a/tests/foreman/cli/test_computeresource_libvirt.py +++ b/tests/foreman/cli/test_computeresource_libvirt.py @@ -101,7 +101,6 @@ def invalid_update_data(): @pytest.fixture(scope="module") -@pytest.mark.skip_if_not_set('libvirt') def libvirt_url(): return LIBVIRT_RESOURCE_URL % settings.libvirt.libvirt_hostname diff --git a/tests/foreman/cli/test_host.py b/tests/foreman/cli/test_host.py index 6e0f025e76c..f80f1c1b68a 100644 --- a/tests/foreman/cli/test_host.py +++ b/tests/foreman/cli/test_host.py @@ -1797,7 +1797,6 @@ def test_positive_install_package_via_rex( # -------------------------- HOST SUBSCRIPTION SUBCOMMAND FIXTURES -------------------------- -@pytest.mark.skip_if_not_set('clients') @pytest.fixture def host_subscription_client(rhel7_contenthost, target_sat): rhel7_contenthost.install_katello_ca(target_sat) diff --git a/tests/foreman/destructive/test_ldap_authentication.py b/tests/foreman/destructive/test_ldap_authentication.py index ecedf803e00..304c608734a 100644 --- a/tests/foreman/destructive/test_ldap_authentication.py +++ b/tests/foreman/destructive/test_ldap_authentication.py @@ -115,7 +115,6 @@ def rhsso_groups_teardown(module_target_sat, default_sso_host): default_sso_host.delete_rhsso_group(group_name) -@pytest.mark.external_auth @pytest.fixture def configure_hammer_session(parametrized_enrolled_sat, enable=True): """Take backup of the hammer config file and enable use_sessions""" From 70b311692f7d7917fce04c07c9f85b040b18b136 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Mar 2024 07:20:25 -0500 Subject: [PATCH 518/586] [6.14.z] Wait for product HTTP Proxy update to finish (#14286) --- tests/foreman/cli/test_http_proxy.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/foreman/cli/test_http_proxy.py b/tests/foreman/cli/test_http_proxy.py index fc0028eed78..68ea95559ca 100644 --- a/tests/foreman/cli/test_http_proxy.py +++ b/tests/foreman/cli/test_http_proxy.py @@ -272,6 +272,13 @@ def test_positive_assign_http_proxy_to_products(module_org, module_target_sat): } ) assert 'Product proxy updated' in res + module_target_sat.wait_for_tasks( + search_query=( + f'Actions::Katello::Repository::Update and organization_id = {module_org.id}' + ), + max_tries=5, + poll_rate=10, + ) for repo in repo_a1, repo_a2, repo_b1, repo_b2: result = module_target_sat.cli.Repository.info({'id': repo['id']}) assert result['http-proxy']['http-proxy-policy'] == 'use_selected_http_proxy' @@ -293,6 +300,13 @@ def test_positive_assign_http_proxy_to_products(module_org, module_target_sat): {'ids': f"{product_a['id']},{product_b['id']}", 'http-proxy-policy': 'none'} ) assert 'Product proxy updated' in res + module_target_sat.wait_for_tasks( + search_query=( + f'Actions::Katello::Repository::Update and organization_id = {module_org.id}' + ), + max_tries=5, + poll_rate=10, + ) for repo in repo_a1, repo_a2, repo_b1, repo_b2: result = module_target_sat.cli.Repository.info({'id': repo['id']}) assert result['http-proxy']['http-proxy-policy'] == 'none' From bb2a0b43e1165950078ff4c2d197e37ecd001370 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Mar 2024 07:24:37 -0500 Subject: [PATCH 519/586] [6.14.z] Bump pytest-reportportal from 5.3.1 to 5.4.0 (#14294) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 58981e9d167..ee161f9fe07 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ python-box==7.1.1 pytest==8.0.2 pytest-services==2.2.1 pytest-mock==3.12.0 -pytest-reportportal==5.3.1 +pytest-reportportal==5.4.0 pytest-xdist==3.5.0 pytest-fixturecollection==0.1.2 pytest-ibutsu==2.2.4 From d60fff56d25c2e23dcba4453e7f11cc561bea9ff Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 7 Mar 2024 07:34:22 -0500 Subject: [PATCH 520/586] [6.14.z] Extend ansible-roles add/remove test for nested HG (#14288) --- tests/foreman/api/test_ansible.py | 44 ++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/tests/foreman/api/test_ansible.py b/tests/foreman/api/test_ansible.py index 730e469b0e0..cc491b29ea8 100644 --- a/tests/foreman/api/test_ansible.py +++ b/tests/foreman/api/test_ansible.py @@ -209,6 +209,7 @@ def test_positive_ansible_job_on_multiple_host( @pytest.mark.e2e @pytest.mark.tier2 +@pytest.mark.upgrade def test_add_and_remove_ansible_role_hostgroup(target_sat): """ Test add and remove functionality for ansible roles in hostgroup via API @@ -216,11 +217,12 @@ def test_add_and_remove_ansible_role_hostgroup(target_sat): :id: 7672cf86-fa31-11ed-855a-0fd307d2d66b :steps: - 1. Create a hostgroup - 2. Sync few ansible roles + 1. Create a hostgroup and a nested hostgroup + 2. Sync a few ansible roles 3. Assign a few ansible roles with the host group 4. Add some ansible role with the host group - 5. Remove the added ansible roles from the host group + 5. Add some ansible roles to the nested hostgroup + 6. Remove the added ansible roles from the parent and nested hostgroup :expectedresults: 1. Ansible role assign/add/remove functionality should work as expected in API @@ -231,29 +233,57 @@ def test_add_and_remove_ansible_role_hostgroup(target_sat): 'theforeman.foreman_scap_client', 'redhat.satellite.hostgroups', 'RedHatInsights.insights-client', + 'redhat.satellite.compute_resources', ] hg = target_sat.api.HostGroup(name=gen_string('alpha')).create() + hg_nested = target_sat.api.HostGroup(name=gen_string('alpha'), parent=hg).create() proxy_id = target_sat.nailgun_smart_proxy.id target_sat.api.AnsibleRoles().sync(data={'proxy_id': proxy_id, 'role_names': ROLE_NAMES}) ROLES = [ target_sat.api.AnsibleRoles().search(query={'search': f'name={role}'})[0].id for role in ROLE_NAMES ] + # Assign first 2 roles to HG and verify it target_sat.api.HostGroup(id=hg.id).assign_ansible_roles(data={'ansible_role_ids': ROLES[:2]}) for r1, r2 in zip( target_sat.api.HostGroup(id=hg.id).list_ansible_roles(), ROLE_NAMES[:2], strict=True ): assert r1['name'] == r2 + + # Add next role from list to HG and verify it target_sat.api.HostGroup(id=hg.id).add_ansible_role(data={'ansible_role_id': ROLES[2]}) for r1, r2 in zip( - target_sat.api.HostGroup(id=hg.id).list_ansible_roles(), ROLE_NAMES, strict=True + target_sat.api.HostGroup(id=hg.id).list_ansible_roles(), ROLE_NAMES[:3], strict=True ): assert r1['name'] == r2 - for role in ROLES: + # Add next role to nested HG, and verify roles are also nested to HG along with assigned role + # Also, ensure the parent HG does not contain the roles assigned to nested HGs + target_sat.api.HostGroup(id=hg_nested.id).add_ansible_role(data={'ansible_role_id': ROLES[3]}) + for r1, r2 in zip( + target_sat.api.HostGroup(id=hg_nested.id).list_ansible_roles(), + [ROLE_NAMES[-1]] + ROLE_NAMES[:-1], + strict=True, + ): + assert r1['name'] == r2 + + for r1, r2 in zip( + target_sat.api.HostGroup(id=hg.id).list_ansible_roles(), ROLE_NAMES[:3], strict=True + ): + assert r1['name'] == r2 + + # Remove roles assigned one by one from HG and nested HG + for role in ROLES[:3]: target_sat.api.HostGroup(id=hg.id).remove_ansible_role(data={'ansible_role_id': role}) - host_roles = target_sat.api.HostGroup(id=hg.id).list_ansible_roles() - assert len(host_roles) == 0 + hg_roles = target_sat.api.HostGroup(id=hg.id).list_ansible_roles() + assert len(hg_roles) == 0 + + for role in ROLES: + target_sat.api.HostGroup(id=hg_nested.id).remove_ansible_role( + data={'ansible_role_id': role} + ) + hg_nested_roles = target_sat.api.HostGroup(id=hg_nested.id).list_ansible_roles() + assert len(hg_nested_roles) == 0 @pytest.fixture From 102f9457d29ec0d2bfe4f538f79365c7f7a2fd86 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 8 Mar 2024 11:08:51 -0500 Subject: [PATCH 521/586] [6.14.z] Fix failing ISS tests (#14318) --- tests/foreman/cli/test_satellitesync.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index 225a834fcbb..c81585c698a 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -1235,6 +1235,14 @@ def test_negative_import_incomplete_archive( {'organization-id': function_import_org_with_manifest.id, 'path': import_path} ) assert '1 subtask(s) failed' in error.value.message + target_sat.wait_for_tasks( + search_query=( + 'Actions::Katello::ContentView::Remove and ' + f'organization_id = {function_import_org_with_manifest.id}' + ), + max_tries=5, + poll_rate=10, + ) # Verify no content is imported and the import CV can be deleted imported_cv = target_sat.cli.ContentView.info( @@ -1577,6 +1585,7 @@ def test_positive_export_rerun_failed_import( assert len(importing_cvv) == 1 @pytest.mark.tier3 + @pytest.mark.skip_if_open("BZ:2262379") def test_postive_export_import_ansible_collection_repo( self, target_sat, From ce766e6188832171ea205765131ee9304cdb49ce Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Sun, 10 Mar 2024 23:36:10 -0400 Subject: [PATCH 522/586] [6.14.z] Bump redis from 5.0.2 to 5.0.3 (#14324) --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 3971b77f3d9..e776154c7a1 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -1,7 +1,7 @@ # For running tests and checking code quality using these modules. flake8==7.0.0 pytest-cov==4.1.0 -redis==5.0.2 +redis==5.0.3 pre-commit==3.6.2 # For generating documentation. From d2a8ef10dfd4fecc6fac69848a9bb3f457666a07 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 11 Mar 2024 08:51:38 -0400 Subject: [PATCH 523/586] [6.14.z] session name for bookmarks (#14333) session name for bookmarks (#14242) (cherry picked from commit 2c2de86bc782c60bf4fc2c291717543252f088e1) Co-authored-by: Peter Ondrejka --- robottelo/constants/__init__.py | 116 +++++++++++++++++++---------- tests/foreman/ui/test_bookmarks.py | 17 ++--- 2 files changed, 85 insertions(+), 48 deletions(-) diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index 4526f338648..e29d67c3a11 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -1647,56 +1647,85 @@ class Colored(Box): ] BOOKMARK_ENTITIES = [ - {'name': 'ActivationKey', 'controller': 'katello_activation_keys'}, - {'name': 'Dashboard', 'controller': 'dashboard', 'skip_for_ui': True}, - {'name': 'Audit', 'controller': 'audits', 'skip_for_ui': True}, - {'name': 'Report', 'controller': 'config_reports', 'skip_for_ui': True}, - {'name': 'Task', 'controller': 'foreman_tasks_tasks', 'skip_for_ui': True}, + { + 'name': 'ActivationKey', + 'controller': 'katello_activation_keys', + 'session_name': 'activationkey', + }, + {'name': 'Dashboard', 'controller': 'dashboard', 'session_name': 'dashboard'}, + {'name': 'Audit', 'controller': 'audits', 'session_name': 'audit'}, + { + 'name': 'Report', + 'controller': 'config_reports', + 'setup': entities.Report, + 'session_name': 'configreport', + }, + {'name': 'Task', 'controller': 'foreman_tasks_tasks', 'session_name': 'task'}, # TODO Load manifest for the test_positive_end_to_end from the ui/test_bookmarks.py - # {'name': 'Subscriptions', 'controller': 'subscriptions', 'skip_for_ui': True}, - {'name': 'Product', 'controller': 'katello_products'}, - {'name': 'Repository', 'controller': 'katello_repositories', 'skip_for_ui': True}, - {'name': 'ContentCredential', 'controller': 'katello_content_credentials'}, - {'name': 'SyncPlan', 'controller': 'katello_sync_plans'}, - {'name': 'ContentView', 'controller': 'katello_content_views'}, - {'name': 'Errata', 'controller': 'katello_errata', 'skip_for_ui': True}, - {'name': 'Package', 'controller': 'katello_erratum_packages', 'skip_for_ui': True}, - {'name': 'ContainerImageTag', 'controller': 'katello_docker_tags', 'skip_for_ui': True}, - {'name': 'Host', 'controller': 'hosts', 'setup': entities.Host}, - {'name': 'ContentHost', 'controller': 'hosts', 'skip_for_ui': True}, - {'name': 'HostCollection', 'controller': 'katello_host_collections'}, - {'name': 'Architecture', 'controller': 'architectures'}, + # {'name': 'Subscriptions', 'controller': 'subscriptions','session_name': 'subscription' }, + {'name': 'Product', 'controller': 'katello_products', 'session_name': 'product'}, + {'name': 'Repository', 'controller': 'katello_repositories', 'session_name': 'repository'}, + { + 'name': 'ContentCredential', + 'controller': 'katello_content_credentials', + 'session_name': 'contentcredential', + }, + {'name': 'SyncPlan', 'controller': 'katello_sync_plans', 'session_name': 'syncplan'}, + {'name': 'ContentView', 'controller': 'katello_content_views', 'session_name': 'contentview'}, + {'name': 'Errata', 'controller': 'katello_errata', 'session_name': 'errata'}, + {'name': 'Package', 'controller': 'katello_erratum_packages', 'session_name': 'package'}, + { + 'name': 'ContainerImageTag', + 'controller': 'katello_docker_tags', + 'session_name': 'containerimagetag', + }, + {'name': 'Host', 'controller': 'hosts', 'setup': entities.Host, 'session_name': 'host_new'}, + {'name': 'ContentHost', 'controller': 'hosts', 'session_name': 'contenthost'}, + { + 'name': 'HostCollection', + 'controller': 'katello_host_collections', + 'session_name': 'hostcollection', + }, + {'name': 'Architecture', 'controller': 'architectures', 'session_name': 'architecture'}, { 'name': 'HardwareModel', 'controller': 'models', 'setup': entities.Model, - 'skip_for_ui': True, + 'session_name': 'hardwaremodel', }, { 'name': 'InstallationMedia', 'controller': 'media', + 'session_name': 'media', 'setup': entities.Media, - 'skip_for_ui': True, }, - {'name': 'OperatingSystem', 'controller': 'operatingsystems'}, + { + 'name': 'OperatingSystem', + 'controller': 'operatingsystems', + 'session_name': 'operatingsystem', + }, { 'name': 'PartitionTable', 'controller': 'ptables', 'setup': entities.PartitionTable, - 'skip_for_ui': False, + 'session_name': 'partitiontable', + }, + { + 'name': 'ProvisioningTemplate', + 'controller': 'provisioning_templates', + 'session_name': 'provisioningtemplate', }, - {'name': 'ProvisioningTemplate', 'controller': 'provisioning_templates'}, { 'name': 'HostGroup', 'controller': 'hostgroups', 'setup': entities.HostGroup, - 'skip_for_ui': True, + 'session_name': 'hostgroup', }, { 'name': 'DiscoveryRule', 'controller': 'discovery_rules', - 'skip_for_ui': True, 'setup': entities.DiscoveryRule, + 'session_name': 'discoveryrule', }, { 'name': 'GlobalParameter', @@ -1704,24 +1733,35 @@ class Colored(Box): 'setup': entities.CommonParameter, 'skip_for_ui': True, }, - {'name': 'Role', 'controller': 'ansible_roles', 'setup': entities.Role}, - {'name': 'Variables', 'controller': 'ansible_variables', 'skip_for_ui': True}, - {'name': 'SmartProxy', 'controller': 'smart_proxies', 'skip_for_ui': True}, + {'name': 'Role', 'controller': 'ansible_roles', 'setup': entities.Role, 'session_name': 'role'}, + {'name': 'Variables', 'controller': 'ansible_variables', 'session_name': 'ansiblevariables'}, + {'name': 'Capsules', 'controller': 'smart_proxies', 'session_name': 'capsule'}, { 'name': 'ComputeResource', 'controller': 'compute_resources', 'setup': entities.LibvirtComputeResource, + 'session_name': 'computeresource', + }, + { + 'name': 'ComputeProfile', + 'controller': 'compute_profiles', + 'setup': entities.ComputeProfile, + 'session_name': 'computeprofile', + }, + {'name': 'Subnet', 'controller': 'subnets', 'setup': entities.Subnet, 'session_name': 'subnet'}, + {'name': 'Domain', 'controller': 'domains', 'setup': entities.Domain, 'session_name': 'domain'}, + {'name': 'Realm', 'controller': 'realms', 'setup': entities.Realm, 'session_name': 'realm'}, + {'name': 'Location', 'controller': 'locations', 'session_name': 'location'}, + {'name': 'Organization', 'controller': 'organizations', 'session_name': 'organization'}, + {'name': 'User', 'controller': 'users', 'session_name': 'user'}, + { + 'name': 'UserGroup', + 'controller': 'usergroups', + 'setup': entities.UserGroup, + 'session_name': 'usergroup', }, - {'name': 'ComputeProfile', 'controller': 'compute_profiles', 'setup': entities.ComputeProfile}, - {'name': 'Subnet', 'controller': 'subnets', 'setup': entities.Subnet}, - {'name': 'Domain', 'controller': 'domains', 'setup': entities.Domain}, - {'name': 'Realm', 'controller': 'realms', 'setup': entities.Realm, 'skip_for_ui': True}, - {'name': 'Location', 'controller': 'locations'}, - {'name': 'Organization', 'controller': 'organizations'}, - {'name': 'User', 'controller': 'users'}, - {'name': 'UserGroup', 'controller': 'usergroups', 'setup': entities.UserGroup}, - {'name': 'Role', 'controller': 'roles'}, - {'name': 'Settings', 'controller': 'settings', 'skip_for_ui': True}, + {'name': 'Role', 'controller': 'roles', 'session_name': 'role'}, + {'name': 'Settings', 'controller': 'settings', 'session_name': 'settings'}, ] STRING_TYPES = ['alpha', 'numeric', 'alphanumeric', 'latin1', 'utf8', 'cjk', 'html'] diff --git a/tests/foreman/ui/test_bookmarks.py b/tests/foreman/ui/test_bookmarks.py index f2622ae84a8..563a19102a7 100644 --- a/tests/foreman/ui/test_bookmarks.py +++ b/tests/foreman/ui/test_bookmarks.py @@ -17,7 +17,6 @@ from robottelo.config import user_nailgun_config from robottelo.constants import BOOKMARK_ENTITIES -from robottelo.utils.issue_handlers import is_open @pytest.fixture( @@ -28,23 +27,21 @@ def ui_entity(module_org, module_location, request): required preconditions. """ entity = request.param + entity_name, entity_setup = entity['name'], entity.get('setup') + # Skip the entities, which can't be tested ATM (not implemented in + # airgun) + skip = entity.get('skip_for_ui') + if skip: + pytest.skip(f'{entity_name} not implemented in airgun') # Some pages require at least 1 existing entity for search bar to # appear. Creating 1 entity for such pages - entity_name, entity_setup = entity['name'], entity.get('setup') if entity_setup: - # Skip the entities, which can't be tested ATM (not implemented in - # airgun or have open BZs) - skip = entity.get('skip_for_ui') - if isinstance(skip, tuple | list): - open_issues = {issue for issue in skip if is_open(issue)} - pytest.skip(f'There is/are an open issue(s) {open_issues} with entity {entity_name}') # entities with 1 organization and location if entity_name in ('Host',): entity_setup(organization=module_org, location=module_location).create() # entities with no organizations and locations elif entity_name in ( 'ComputeProfile', - 'GlobalParameter', 'HardwareModel', 'UserGroup', ): @@ -117,7 +114,7 @@ def test_positive_create_bookmark_public( public_name = gen_string('alphanumeric') nonpublic_name = gen_string('alphanumeric') with session: - ui_lib = getattr(session, ui_entity['name'].lower()) + ui_lib = getattr(session, ui_entity['session_name']) for name in (public_name, nonpublic_name): ui_lib.create_bookmark( {'name': name, 'query': gen_string('alphanumeric'), 'public': name == public_name} From 507ed161fc910f9474098797406975d454fe7935 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:44:05 -0400 Subject: [PATCH 524/586] [6.14.z] Fix flaky SyncPlans in cli_factory (#14352) --- robottelo/cli/syncplan.py | 11 +++++++++++ robottelo/host_helpers/cli_factory.py | 4 +++- tests/foreman/cli/test_syncplan.py | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/robottelo/cli/syncplan.py b/robottelo/cli/syncplan.py index 3f8b9b8b347..151a72acb84 100644 --- a/robottelo/cli/syncplan.py +++ b/robottelo/cli/syncplan.py @@ -17,9 +17,20 @@ update """ from robottelo.cli.base import Base +from robottelo.exceptions import CLIError class SyncPlan(Base): """Manipulates Katello engine's sync-plan command.""" command_base = 'sync-plan' + + @classmethod + def create(cls, options=None): + """Create a SyncPlan""" + cls.command_sub = 'create' + + if options.get('interval') == 'custom cron' and options.get('cron-expression') is None: + raise CLIError('Missing "cron-expression" option for "custom cron" interval.') + + return super().create(options) diff --git a/robottelo/host_helpers/cli_factory.py b/robottelo/host_helpers/cli_factory.py index 0db645d04f6..84ef66de1c2 100644 --- a/robottelo/host_helpers/cli_factory.py +++ b/robottelo/host_helpers/cli_factory.py @@ -140,7 +140,9 @@ def create_object(cli_object, options, values=None, credentials=None): 'sync_plan': { 'description': gen_alpha, 'enabled': 'true', - 'interval': lambda: random.choice(list(constants.SYNC_INTERVAL.values())), + 'interval': lambda: random.choice( + [i for i in constants.SYNC_INTERVAL.values() if i != 'custom cron'] + ), 'name': gen_alpha, 'sync-date': lambda: datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), }, diff --git a/tests/foreman/cli/test_syncplan.py b/tests/foreman/cli/test_syncplan.py index 86f33b68ffb..8669cd49a18 100644 --- a/tests/foreman/cli/test_syncplan.py +++ b/tests/foreman/cli/test_syncplan.py @@ -705,7 +705,7 @@ def test_positive_synchronize_rh_product_future_sync_date( # Verify product has not been synced yet with pytest.raises(AssertionError): validate_task_status(target_sat, repo['id'], org.id, max_tries=1) - validate_repo_content(repo, ['errata', 'packages'], after_sync=False) + validate_repo_content(target_sat, repo, ['errata', 'packages'], after_sync=False) # Wait the rest of expected time logger.info( f"Waiting {(delay * 4 / 5)} seconds to check product {product['name']}" @@ -714,7 +714,7 @@ def test_positive_synchronize_rh_product_future_sync_date( sleep(delay * 4 / 5) # Verify product was synced successfully validate_task_status(target_sat, repo['id'], org.id) - validate_repo_content(repo, ['errata', 'packages']) + validate_repo_content(target_sat, repo, ['errata', 'packages']) @pytest.mark.tier3 From 7a5e6fdbd3b3c69f5cbe4b8053e13c036a6339e5 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:55:58 -0400 Subject: [PATCH 525/586] [6.14.z] remove the PRT labels for new commits (#14306) remove the PRT labels for new commits (#14093) (cherry picked from commit a98258c2b99711584c19e0301b1145e7257a25f5) Co-authored-by: Omkar Khatavkar --- .github/workflows/prt_labels.yml | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/prt_labels.yml diff --git a/.github/workflows/prt_labels.yml b/.github/workflows/prt_labels.yml new file mode 100644 index 00000000000..311516fa682 --- /dev/null +++ b/.github/workflows/prt_labels.yml @@ -0,0 +1,49 @@ +name: Remove the PRT label, for the new commit + +on: + pull_request: + types: ["synchronize"] + +jobs: + prt_labels_remover: + name: remove the PRT label when amendments or new commits added to PR + runs-on: ubuntu-latest + if: "(contains(github.event.pull_request.labels.*.name, 'PRT-Passed') || contains(github.event.pull_request.labels.*.name, 'PRT-Failed'))" + steps: + - name: Avoid the race condition as PRT result will be cleaned + run: | + echo "Avoiding the race condition if prt result will be cleaned" && sleep 60 + + - name: Fetch the PRT status + id: prt + uses: omkarkhatavkar/wait-for-status-checks@main + with: + ref: ${{ github.head_ref }} + context: 'Robottelo-Runner' + wait-interval: 2 + count: 5 + + - name: remove the PRT Passed/Failed label, for new commit + if: always() && ${{steps.prt.outputs.result}} == 'not_found' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.CHERRYPICK_PAT }} + script: | + const prNumber = '${{ github.event.number }}'; + const issue = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + const labelsToRemove = ['PRT-Failed', 'PRT-Passed']; + const labelsToRemoveFiltered = labelsToRemove.filter(label => issue.data.labels.some(({ name }) => name === label)); + if (labelsToRemoveFiltered.length > 0) { + await Promise.all(labelsToRemoveFiltered.map(async label => { + await github.rest.issues.removeLabel({ + issue_number: prNumber, + owner: context.repo.owner, + repo: context.repo.repo, + name: label + }); + })); + } From 842fc890dd404b8776336ccc1a39d48ad50db4df Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:09:40 -0400 Subject: [PATCH 526/586] [6.14.z] Mark fake secret as "notsecret" (#14358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mark fake secret as "notsecret" (#14336) (cherry picked from commit 98f375db26787e031e259ffe332740330bea45ba) Co-authored-by: Ondřej Gajdušek --- tests/robottelo/test_dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/robottelo/test_dependencies.py b/tests/robottelo/test_dependencies.py index 185f0866737..2a6b0a9fb81 100644 --- a/tests/robottelo/test_dependencies.py +++ b/tests/robottelo/test_dependencies.py @@ -81,7 +81,7 @@ def test_productmd(): def test_pyotp(): import pyotp - fake_secret = 'JBSWY3DPEHPK3PXP' + fake_secret = 'JBSWY3DPEHPK3PXP' # notsecret totp = pyotp.TOTP(fake_secret) assert totp.now() From e22942f4c4205cc2c5fc79330875e64951920827 Mon Sep 17 00:00:00 2001 From: Jitendra Yejare Date: Tue, 12 Mar 2024 16:07:44 +0530 Subject: [PATCH 527/586] [6.14.z][Component Refresh] Component Names are updated (#14268) (#14367) [Component Refresh] Component Names are updated (#14268) Component Names are updated --- testimony.yaml | 14 ++++---------- tests/foreman/api/test_capsule.py | 2 +- tests/foreman/api/test_rhc.py | 2 +- tests/foreman/api/test_rhcloud_inventory.py | 4 ++-- tests/foreman/cli/test_capsule.py | 4 ++-- tests/foreman/cli/test_installer.py | 4 ++-- tests/foreman/cli/test_rhcloud_inventory.py | 4 ++-- tests/foreman/destructive/test_capsule.py | 4 ++-- .../destructive/test_capsule_loadbalancer.py | 2 +- tests/foreman/destructive/test_installer.py | 4 ++-- .../destructive/test_katello_certs_check.py | 2 +- tests/foreman/destructive/test_packages.py | 2 +- tests/foreman/destructive/test_rename.py | 2 +- tests/foreman/installer/test_installer.py | 4 ++-- tests/foreman/maintain/test_advanced.py | 2 +- tests/foreman/maintain/test_backup_restore.py | 2 +- tests/foreman/maintain/test_health.py | 2 +- tests/foreman/maintain/test_maintenance_mode.py | 2 +- tests/foreman/maintain/test_offload_DB.py | 4 ++-- tests/foreman/maintain/test_packages.py | 2 +- tests/foreman/maintain/test_service.py | 2 +- tests/foreman/maintain/test_upgrade.py | 2 +- tests/foreman/sys/test_katello_certs_check.py | 2 +- tests/foreman/ui/test_rhc.py | 2 +- tests/foreman/ui/test_rhcloud_insights.py | 4 ++-- tests/foreman/ui/test_rhcloud_inventory.py | 4 ++-- tests/upgrades/test_capsule.py | 2 +- tests/upgrades/test_performance_tuning.py | 2 +- tests/upgrades/test_satellite_maintain.py | 2 +- 29 files changed, 42 insertions(+), 48 deletions(-) diff --git a/testimony.yaml b/testimony.yaml index 4ae19f8e4cc..92085a2c2fb 100644 --- a/testimony.yaml +++ b/testimony.yaml @@ -25,9 +25,8 @@ CaseComponent: - Branding - BVT - Candlepin - - Capsule + - ForemanProxy - Capsule-Content - - Certificates - ComputeResources - ComputeResources-Azure - ComputeResources-CNV @@ -53,7 +52,7 @@ CaseComponent: - ErrataManagement - Fact - ForemanDebug - - ForemanMaintain + - SatelliteMaintain - Hammer - Hammer-Content - HTTPProxy @@ -64,7 +63,7 @@ CaseComponent: - Hosts-Content - Infobloxintegration - Infrastructure - - Installer + - Installation - InterSatelliteSync - katello-agent - katello-tracer @@ -77,7 +76,6 @@ CaseComponent: - Networking - Notifications - OrganizationsandLocations - - Packaging - Parameters - Provisioning - ProvisioningTemplates @@ -90,16 +88,12 @@ CaseComponent: - RemoteExecution - Reporting - Repositories - - RHCloud-CloudConnector - - RHCloud-Insights - - RHCloud-Inventory + - RHCloud - rubygem-foreman-redhat_access - - satellite-change-hostname - SatelliteClone - SCAPPlugin - Search - Security - - SELinux - Settings - SubscriptionManagement - Subscriptions-virt-who diff --git a/tests/foreman/api/test_capsule.py b/tests/foreman/api/test_capsule.py index 3d499ccf16e..8372c192174 100644 --- a/tests/foreman/api/test_capsule.py +++ b/tests/foreman/api/test_capsule.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: Capsule +:CaseComponent: ForemanProxy :Team: Platform diff --git a/tests/foreman/api/test_rhc.py b/tests/foreman/api/test_rhc.py index 521b1cb15da..30559fe406e 100644 --- a/tests/foreman/api/test_rhc.py +++ b/tests/foreman/api/test_rhc.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: RHCloud-CloudConnector +:CaseComponent: RHCloud :Team: Platform diff --git a/tests/foreman/api/test_rhcloud_inventory.py b/tests/foreman/api/test_rhcloud_inventory.py index b673e639abd..1f7af05b92a 100644 --- a/tests/foreman/api/test_rhcloud_inventory.py +++ b/tests/foreman/api/test_rhcloud_inventory.py @@ -1,10 +1,10 @@ """API tests for RH Cloud - Inventory, also known as Insights Inventory Upload -:Requirement: RH Cloud - Inventory +:Requirement: RHCloud :CaseAutomation: Automated -:CaseComponent: RHCloud-Inventory +:CaseComponent: RHCloud :Team: Platform diff --git a/tests/foreman/cli/test_capsule.py b/tests/foreman/cli/test_capsule.py index e41759fbc2b..ddcc100326a 100644 --- a/tests/foreman/cli/test_capsule.py +++ b/tests/foreman/cli/test_capsule.py @@ -1,10 +1,10 @@ """Test class for the capsule CLI. -:Requirement: Capsule +:Requirement: ForemanProxy :CaseAutomation: Automated -:CaseComponent: Capsule +:CaseComponent: ForemanProxy :Team: Platform diff --git a/tests/foreman/cli/test_installer.py b/tests/foreman/cli/test_installer.py index 4a42eaa1315..cf0eb838ee0 100644 --- a/tests/foreman/cli/test_installer.py +++ b/tests/foreman/cli/test_installer.py @@ -1,10 +1,10 @@ """Tests For Disconnected Satellite Installation -:Requirement: Installer (disconnected satellite installation) +:Requirement: Installation (disconnected satellite installation) :CaseAutomation: Automated -:CaseComponent: Installer +:CaseComponent: Installation :Team: Platform diff --git a/tests/foreman/cli/test_rhcloud_inventory.py b/tests/foreman/cli/test_rhcloud_inventory.py index e127f6e7382..31847984e03 100644 --- a/tests/foreman/cli/test_rhcloud_inventory.py +++ b/tests/foreman/cli/test_rhcloud_inventory.py @@ -1,10 +1,10 @@ """CLI tests for RH Cloud - Inventory, aka Insights Inventory Upload -:Requirement: RH Cloud - Inventory +:Requirement: RHCloud :CaseAutomation: Automated -:CaseComponent: RHCloud-Inventory +:CaseComponent: RHCloud :Team: Platform diff --git a/tests/foreman/destructive/test_capsule.py b/tests/foreman/destructive/test_capsule.py index 40935c114b9..1cc63f4fbbe 100644 --- a/tests/foreman/destructive/test_capsule.py +++ b/tests/foreman/destructive/test_capsule.py @@ -1,10 +1,10 @@ """Test class for the capsule CLI. -:Requirement: Capsule +:Requirement: ForemanProxy :CaseAutomation: Automated -:CaseComponent: Capsule +:CaseComponent: ForemanProxy :Team: Platform diff --git a/tests/foreman/destructive/test_capsule_loadbalancer.py b/tests/foreman/destructive/test_capsule_loadbalancer.py index a392e0907d5..27b3c9ad69f 100644 --- a/tests/foreman/destructive/test_capsule_loadbalancer.py +++ b/tests/foreman/destructive/test_capsule_loadbalancer.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: Capsule +:CaseComponent: ForemanProxy :Team: Platform diff --git a/tests/foreman/destructive/test_installer.py b/tests/foreman/destructive/test_installer.py index a020b66f96a..36776213c13 100644 --- a/tests/foreman/destructive/test_installer.py +++ b/tests/foreman/destructive/test_installer.py @@ -1,10 +1,10 @@ """Smoke tests to check installation health -:Requirement: Installer +:Requirement: Installation :CaseAutomation: Automated -:CaseComponent: Installer +:CaseComponent: Installation :Team: Platform diff --git a/tests/foreman/destructive/test_katello_certs_check.py b/tests/foreman/destructive/test_katello_certs_check.py index 4b297f0f979..db133e99749 100644 --- a/tests/foreman/destructive/test_katello_certs_check.py +++ b/tests/foreman/destructive/test_katello_certs_check.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: Certificates +:CaseComponent: Installation :Team: Platform diff --git a/tests/foreman/destructive/test_packages.py b/tests/foreman/destructive/test_packages.py index 382f0639e2d..f052948002f 100644 --- a/tests/foreman/destructive/test_packages.py +++ b/tests/foreman/destructive/test_packages.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: ForemanMaintain +:CaseComponent: SatelliteMaintain :Team: Platform diff --git a/tests/foreman/destructive/test_rename.py b/tests/foreman/destructive/test_rename.py index d5af1d6cfee..03fd9ff0d6f 100644 --- a/tests/foreman/destructive/test_rename.py +++ b/tests/foreman/destructive/test_rename.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: satellite-change-hostname +:CaseComponent: Installation :Team: Platform diff --git a/tests/foreman/installer/test_installer.py b/tests/foreman/installer/test_installer.py index b8756fe3191..e6a3e9c7ba5 100644 --- a/tests/foreman/installer/test_installer.py +++ b/tests/foreman/installer/test_installer.py @@ -1,10 +1,10 @@ """Smoke tests to check installation health -:Requirement: Installer +:Requirement: Installation :CaseAutomation: Automated -:CaseComponent: Installer +:CaseComponent: Installation :Team: Platform diff --git a/tests/foreman/maintain/test_advanced.py b/tests/foreman/maintain/test_advanced.py index 2771994d568..1167e907102 100644 --- a/tests/foreman/maintain/test_advanced.py +++ b/tests/foreman/maintain/test_advanced.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: ForemanMaintain +:CaseComponent: SatelliteMaintain :Team: Platform diff --git a/tests/foreman/maintain/test_backup_restore.py b/tests/foreman/maintain/test_backup_restore.py index a845e71a166..f81b268e844 100644 --- a/tests/foreman/maintain/test_backup_restore.py +++ b/tests/foreman/maintain/test_backup_restore.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: ForemanMaintain +:CaseComponent: SatelliteMaintain :Team: Platform diff --git a/tests/foreman/maintain/test_health.py b/tests/foreman/maintain/test_health.py index 200928fe943..f58ee15b644 100644 --- a/tests/foreman/maintain/test_health.py +++ b/tests/foreman/maintain/test_health.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: ForemanMaintain +:CaseComponent: SatelliteMaintain :Team: Platform diff --git a/tests/foreman/maintain/test_maintenance_mode.py b/tests/foreman/maintain/test_maintenance_mode.py index 3cc340f774b..50d130ac55a 100644 --- a/tests/foreman/maintain/test_maintenance_mode.py +++ b/tests/foreman/maintain/test_maintenance_mode.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: ForemanMaintain +:CaseComponent: SatelliteMaintain :Team: Platform diff --git a/tests/foreman/maintain/test_offload_DB.py b/tests/foreman/maintain/test_offload_DB.py index ca0f64f87d8..0e1b3ced9d9 100644 --- a/tests/foreman/maintain/test_offload_DB.py +++ b/tests/foreman/maintain/test_offload_DB.py @@ -4,7 +4,7 @@ :CaseAutomation: ManualOnly -:CaseComponent: ForemanMaintain +:CaseComponent: SatelliteMaintain :Team: Platform @@ -28,5 +28,5 @@ def test_offload_internal_db_to_external_db_host(): :expectedresults: Installed successful, all services running - :CaseComponent: Installer + :CaseComponent: Installation """ diff --git a/tests/foreman/maintain/test_packages.py b/tests/foreman/maintain/test_packages.py index 9931d144352..a4c56eac531 100644 --- a/tests/foreman/maintain/test_packages.py +++ b/tests/foreman/maintain/test_packages.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: ForemanMaintain +:CaseComponent: SatelliteMaintain :Team: Platform diff --git a/tests/foreman/maintain/test_service.py b/tests/foreman/maintain/test_service.py index dc0b6366e8b..9b2970a8a64 100644 --- a/tests/foreman/maintain/test_service.py +++ b/tests/foreman/maintain/test_service.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: ForemanMaintain +:CaseComponent: SatelliteMaintain :Team: Platform diff --git a/tests/foreman/maintain/test_upgrade.py b/tests/foreman/maintain/test_upgrade.py index cb855208e44..686f036cd80 100644 --- a/tests/foreman/maintain/test_upgrade.py +++ b/tests/foreman/maintain/test_upgrade.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: ForemanMaintain +:CaseComponent: SatelliteMaintain :Team: Platform diff --git a/tests/foreman/sys/test_katello_certs_check.py b/tests/foreman/sys/test_katello_certs_check.py index 5bfeed3eee5..640add5a0ae 100644 --- a/tests/foreman/sys/test_katello_certs_check.py +++ b/tests/foreman/sys/test_katello_certs_check.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: Certificates +:CaseComponent: Installation :Team: Platform diff --git a/tests/foreman/ui/test_rhc.py b/tests/foreman/ui/test_rhc.py index 0fdefee5bf1..f3ab5eae14f 100644 --- a/tests/foreman/ui/test_rhc.py +++ b/tests/foreman/ui/test_rhc.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: RHCloud-CloudConnector +:CaseComponent: RHCloud :Team: Platform diff --git a/tests/foreman/ui/test_rhcloud_insights.py b/tests/foreman/ui/test_rhcloud_insights.py index 9dcd309055c..2a3cabf789f 100644 --- a/tests/foreman/ui/test_rhcloud_insights.py +++ b/tests/foreman/ui/test_rhcloud_insights.py @@ -1,10 +1,10 @@ """Tests for RH Cloud - Inventory -:Requirement: RH Cloud - Inventory +:Requirement: RHCloud :CaseAutomation: Automated -:CaseComponent: RHCloud-Inventory +:CaseComponent: RHCloud :Team: Platform diff --git a/tests/foreman/ui/test_rhcloud_inventory.py b/tests/foreman/ui/test_rhcloud_inventory.py index e242366bf0a..642bef3b84b 100644 --- a/tests/foreman/ui/test_rhcloud_inventory.py +++ b/tests/foreman/ui/test_rhcloud_inventory.py @@ -1,10 +1,10 @@ """Tests for RH Cloud - Inventory, also known as Insights Inventory Upload -:Requirement: RH Cloud - Inventory +:Requirement: RHCloud :CaseAutomation: Automated -:CaseComponent: RHCloud-Inventory +:CaseComponent: RHCloud :Team: Platform diff --git a/tests/upgrades/test_capsule.py b/tests/upgrades/test_capsule.py index d064b677fbe..c7cf63e9977 100644 --- a/tests/upgrades/test_capsule.py +++ b/tests/upgrades/test_capsule.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: Capsule +:CaseComponent: ForemanProxy :Team: Platform diff --git a/tests/upgrades/test_performance_tuning.py b/tests/upgrades/test_performance_tuning.py index 94e40a33ba7..33237e8f561 100644 --- a/tests/upgrades/test_performance_tuning.py +++ b/tests/upgrades/test_performance_tuning.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: Installer +:CaseComponent: Installation :Team: Platform diff --git a/tests/upgrades/test_satellite_maintain.py b/tests/upgrades/test_satellite_maintain.py index 5091f8508fd..8035416e39a 100644 --- a/tests/upgrades/test_satellite_maintain.py +++ b/tests/upgrades/test_satellite_maintain.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: ForemanMaintain +:CaseComponent: SatelliteMaintain :Team: Platform From 19b68f362f9c08a7015ca3ede4b3c0482f164223 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 12 Mar 2024 09:21:40 -0400 Subject: [PATCH 528/586] [6.14.z] Update Ansible tests to reflect the new split Ansible components (#14365) Update Ansible tests to reflect the new split Ansible components (#14302) Signed-off-by: Gaurav Talreja (cherry picked from commit 73b7bd28bf050135ebf7c563c8cc5a53bb361d9d) Co-authored-by: Gaurav Talreja --- testimony.yaml | 3 +- tests/foreman/api/test_ansible.py | 91 +++++++++++------------ tests/foreman/cli/test_ansible.py | 12 +-- tests/foreman/cli/test_remoteexecution.py | 2 +- tests/foreman/destructive/test_ansible.py | 11 ++- tests/foreman/ui/test_ansible.py | 8 +- tests/foreman/ui/test_jobinvocation.py | 4 +- tests/foreman/ui/test_remoteexecution.py | 22 +++--- 8 files changed, 73 insertions(+), 80 deletions(-) diff --git a/testimony.yaml b/testimony.yaml index 92085a2c2fb..5f7162199f5 100644 --- a/testimony.yaml +++ b/testimony.yaml @@ -15,7 +15,8 @@ CaseComponent: # No spaces allowed - ActivationKeys - AlternateContentSources - - Ansible + - Ansible-ConfigurationManagement + - Ansible-RemoteExecution - AnsibleCollection - API - AuditLog diff --git a/tests/foreman/api/test_ansible.py b/tests/foreman/api/test_ansible.py index cc491b29ea8..515d432423f 100644 --- a/tests/foreman/api/test_ansible.py +++ b/tests/foreman/api/test_ansible.py @@ -4,11 +4,11 @@ :CaseAutomation: Automated -:CaseComponent: Ansible +:CaseComponent: Ansible-ConfigurationManagement :Team: Rocket -:CaseImportance: High +:CaseImportance: Critical """ from fauxfactory import gen_string @@ -19,6 +19,46 @@ from robottelo.utils.issue_handlers import is_open +@pytest.fixture +def filtered_user(target_sat, module_org, module_location): + """ + :steps: + 1. Create a role with a host view filtered + 2. Create a user with that role + 3. Setup a host + """ + role = target_sat.api.Role( + name=gen_string('alpha'), location=[module_location], organization=[module_org] + ).create() + # assign view_hosts (with a filter, to test BZ 1699188), + # view_hostgroups, view_facts permissions to the role + permission_hosts = target_sat.api.Permission().search(query={'search': 'name="view_hosts"'}) + permission_hostgroups = target_sat.api.Permission().search( + query={'search': 'name="view_hostgroups"'} + ) + permission_facts = target_sat.api.Permission().search(query={'search': 'name="view_facts"'}) + target_sat.api.Filter( + permission=permission_hosts, search='name != nonexistent', role=role + ).create() + target_sat.api.Filter(permission=permission_hostgroups, role=role).create() + target_sat.api.Filter(permission=permission_facts, role=role).create() + + password = gen_string('alpha') + user = target_sat.api.User( + role=[role], password=password, location=[module_location], organization=[module_org] + ).create() + + return user, password + + +@pytest.fixture +def rex_host_in_org_and_loc(target_sat, module_org, module_location, rex_contenthost): + host = target_sat.api.Host().search(query={'search': f'name={rex_contenthost.hostname}'})[0] + target_sat.api.Host(id=host.id, organization=[module_org.id]).update(['organization']) + target_sat.api.Host(id=host.id, location=module_location.id).update(['location']) + return host + + @pytest.mark.e2e def test_fetch_and_sync_ansible_playbooks(target_sat): """ @@ -39,8 +79,6 @@ def test_fetch_and_sync_ansible_playbooks(target_sat): 1. Playbooks should be fetched and synced successfully. :BZ: 2115686 - - :CaseAutomation: Automated """ target_sat.execute( "ansible-galaxy collection install -p /usr/share/ansible/collections " @@ -88,9 +126,7 @@ def test_positive_ansible_job_on_host( :BZ: 2164400 - :CaseAutomation: Automated - - :CaseImportance: Critical + :CaseComponent: Ansible-RemoteExecution """ SELECTED_ROLE = 'RedHatInsights.insights-client' if rhel_contenthost.os_version.major <= 7: @@ -161,7 +197,7 @@ def test_positive_ansible_job_on_multiple_host( :BZ: 2167396, 2190464, 2184117 - :CaseAutomation: Automated + :CaseComponent: Ansible-RemoteExecution """ hosts = [rhel9_contenthost, rhel8_contenthost, rhel7_contenthost] SELECTED_ROLE = 'RedHatInsights.insights-client' @@ -286,45 +322,6 @@ def test_add_and_remove_ansible_role_hostgroup(target_sat): assert len(hg_nested_roles) == 0 -@pytest.fixture -def filtered_user(target_sat, module_org, module_location): - """ - :steps: - 1. Create a role with a host view filtered - 2. Create a user with that role - 3. Setup a host - """ - api = target_sat.api - role = api.Role( - name=gen_string('alpha'), location=[module_location], organization=[module_org] - ).create() - # assign view_hosts (with a filter, to test BZ 1699188), - # view_hostgroups, view_facts permissions to the role - permission_hosts = api.Permission().search(query={'search': 'name="view_hosts"'}) - permission_hostgroups = api.Permission().search(query={'search': 'name="view_hostgroups"'}) - permission_facts = api.Permission().search(query={'search': 'name="view_facts"'}) - api.Filter(permission=permission_hosts, search='name != nonexistent', role=role).create() - api.Filter(permission=permission_hostgroups, role=role).create() - api.Filter(permission=permission_facts, role=role).create() - - password = gen_string('alpha') - user = api.User( - role=[role], password=password, location=[module_location], organization=[module_org] - ).create() - - return user, password - - -@pytest.fixture -def rex_host_in_org_and_loc(target_sat, module_org, module_location, rex_contenthost): - api = target_sat.api - host = api.Host().search(query={'search': f'name={rex_contenthost.hostname}'})[0] - host_id = host.id - api.Host(id=host_id, organization=[module_org.id]).update(['organization']) - api.Host(id=host_id, location=module_location.id).update(['location']) - return host - - @pytest.mark.rhel_ver_match('[78]') @pytest.mark.tier2 def test_positive_read_facts_with_filter( diff --git a/tests/foreman/cli/test_ansible.py b/tests/foreman/cli/test_ansible.py index b628c9d8fa2..8cffe15b20d 100644 --- a/tests/foreman/cli/test_ansible.py +++ b/tests/foreman/cli/test_ansible.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: Ansible +:CaseComponent: Ansible-ConfigurationManagement :Team: Rocket @@ -41,8 +41,6 @@ def test_positive_ansible_e2e(target_sat, module_org, rhel_contenthost): 2. Job execution must be successful. 3. Operations performed with hammer must be successful. - :CaseAutomation: Automated - :BZ: 2154184 :customerscenario: true @@ -53,7 +51,7 @@ def test_positive_ansible_e2e(target_sat, module_org, rhel_contenthost): SELECTED_ROLE_1 = 'theforeman.foreman_scap_client' SELECTED_VAR = gen_string('alpha') # disable batch tasks to test BZ#2154184 - target_sat.cli.Settings.set({'name': "foreman_tasks_proxy_batch_trigger", 'value': "false"}) + target_sat.cli.Settings.set({'name': 'foreman_tasks_proxy_batch_trigger', 'value': 'false'}) if rhel_contenthost.os_version.major <= 7: rhel_contenthost.create_custom_repos(rhel7=settings.repos.rhel7_os) assert rhel_contenthost.execute('yum install -y insights-client').status == 0 @@ -121,7 +119,7 @@ def test_positive_ansible_e2e(target_sat, module_org, rhel_contenthost): @pytest.mark.tier2 def test_add_and_remove_ansible_role_hostgroup(target_sat): """ - Test add and remove functionality for ansible roles in hostgroup via cli + Test add and remove functionality for ansible roles in hostgroup via CLI :id: 2c6fda14-4cd2-490a-b7ef-7a08f8164fad @@ -135,11 +133,9 @@ def test_add_and_remove_ansible_role_hostgroup(target_sat): 5. Remove the added ansible roles from the host group :expectedresults: - 1. Ansible role assign/add/remove functionality should work as expected in cli + 1. Ansible role assign/add/remove functionality should work as expected in CLI :BZ: 2029402 - - :CaseAutomation: Automated """ ROLES = [ 'theforeman.foreman_scap_client', diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index 0687ed3728b..fc812d9e649 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -854,7 +854,7 @@ def test_positive_install_ansible_collection( :id: ad25aee5-4ea3-4743-a301-1c6271856f79 - :CaseComponent: Ansible + :CaseComponent: Ansible-RemoteExecution :Team: Rocket """ diff --git a/tests/foreman/destructive/test_ansible.py b/tests/foreman/destructive/test_ansible.py index 0ff8fc67a1d..01779d6a14a 100644 --- a/tests/foreman/destructive/test_ansible.py +++ b/tests/foreman/destructive/test_ansible.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: Ansible +:CaseComponent: Ansible-ConfigurationManagement :Team: Rocket @@ -21,10 +21,6 @@ def test_positive_persistent_ansible_cfg_change(target_sat): :id: c22fcd47-8627-4230-aa1f-7d4fc8517a0e - :BZ: 1786358 - - :customerscenario: true - :steps: 1. Update value in ansible.cfg. 2. Verify value is updated in the file. @@ -33,6 +29,10 @@ def test_positive_persistent_ansible_cfg_change(target_sat): :expectedresults: Changes in ansible.cfg are persistent after running "satellite-installer". + + :BZ: 1786358 + + :customerscenario: true """ ansible_cfg = '/etc/ansible/ansible.cfg' param = 'local_tmp = /tmp' @@ -49,7 +49,6 @@ def test_positive_import_all_roles(target_sat): :id: 53fe3857-a08f-493d-93c7-3fed331ed391 :steps: - 1. Navigate to the Configure > Roles page. 2. Click the `Import from [hostname]` button. 3. Get total number of importable roles from pagination. diff --git a/tests/foreman/ui/test_ansible.py b/tests/foreman/ui/test_ansible.py index 2026ca8c095..82ffd5bf187 100644 --- a/tests/foreman/ui/test_ansible.py +++ b/tests/foreman/ui/test_ansible.py @@ -4,11 +4,11 @@ :CaseAutomation: Automated -:CaseComponent: Ansible +:CaseComponent: Ansible-ConfigurationManagement :Team: Rocket -:CaseImportance: High +:CaseImportance: Critical """ from fauxfactory import gen_string @@ -111,7 +111,7 @@ def test_positive_config_report_ansible(session, target_sat, module_org, rhel_co 1. Host should be assigned the proper role. 2. Job report should be created. - :CaseImportance: Critical + :CaseComponent: Ansible-RemoteExecution """ SELECTED_ROLE = 'RedHatInsights.insights-client' if rhel_contenthost.os_version.major <= 7: @@ -176,7 +176,7 @@ def test_positive_ansible_custom_role(target_sat, session, module_org, rhel_cont :BZ: 2155392 - :CaseAutomation: Automated + :CaseComponent: Ansible-RemoteExecution """ SELECTED_ROLE = 'custom_role' playbook = f'{robottelo_tmp_dir}/playbook.yml' diff --git a/tests/foreman/ui/test_jobinvocation.py b/tests/foreman/ui/test_jobinvocation.py index c98e512f527..d79d7ee3355 100644 --- a/tests/foreman/ui/test_jobinvocation.py +++ b/tests/foreman/ui/test_jobinvocation.py @@ -126,7 +126,7 @@ def test_positive_schedule_recurring_host_job(self): :id: 5052be04-28ab-4349-8bee-851ef76e4ffa - :caseComponent: Ansible + :caseComponent: Ansible-RemoteExecution :Team: Rocket @@ -152,7 +152,7 @@ def test_positive_schedule_recurring_hostgroup_job(self): :id: c65db99b-11fe-4a32-89d0-0a4692b07efe - :caseComponent: Ansible + :caseComponent: Ansible-RemoteExecution :Team: Rocket diff --git a/tests/foreman/ui/test_remoteexecution.py b/tests/foreman/ui/test_remoteexecution.py index e1e08d070e7..cdfe9c7ec4e 100644 --- a/tests/foreman/ui/test_remoteexecution.py +++ b/tests/foreman/ui/test_remoteexecution.py @@ -288,7 +288,7 @@ def test_positive_ansible_job_check_mode(session): :CaseAutomation: NotAutomated - :CaseComponent: Ansible + :CaseComponent: Ansible-RemoteExecution :Team: Rocket """ @@ -311,7 +311,7 @@ def test_positive_ansible_config_report_failed_tasks_errors(session): :CaseAutomation: NotAutomated - :CaseComponent: Ansible + :CaseComponent: Ansible-ConfigurationManagement :Team: Rocket """ @@ -335,7 +335,7 @@ def test_positive_ansible_config_report_changes_notice(session): :CaseAutomation: NotAutomated - :CaseComponent: Ansible + :CaseComponent: Ansible-ConfigurationManagement :Team: Rocket """ @@ -356,7 +356,7 @@ def test_positive_ansible_variables_imported_with_roles(session): :CaseAutomation: NotAutomated - :CaseComponent: Ansible + :CaseComponent: Ansible-ConfigurationManagement :Team: Rocket """ @@ -377,7 +377,7 @@ def test_positive_roles_import_in_background(session): :CaseAutomation: NotAutomated - :CaseComponent: Ansible + :CaseComponent: Ansible-ConfigurationManagement :Team: Rocket """ @@ -399,7 +399,7 @@ def test_positive_ansible_roles_ignore_list(session): :CaseAutomation: NotAutomated - :CaseComponent: Ansible + :CaseComponent: Ansible-ConfigurationManagement :Team: Rocket """ @@ -423,7 +423,7 @@ def test_positive_ansible_variables_installed_with_collection(session): :CaseAutomation: NotAutomated - :CaseComponent: Ansible + :CaseComponent: Ansible-ConfigurationManagement :Team: Rocket """ @@ -449,7 +449,7 @@ def test_positive_install_ansible_collection_via_job_invocation(session): :CaseAutomation: NotAutomated - :CaseComponent: Ansible + :CaseComponent: Ansible-RemoteExecution :Team: Rocket """ @@ -474,7 +474,7 @@ def test_positive_set_ansible_role_order_per_host(session): :CaseAutomation: NotAutomated - :CaseComponent: Ansible + :CaseComponent: Ansible-ConfigurationManagement :Team: Rocket """ @@ -501,7 +501,7 @@ def test_positive_set_ansible_role_order_per_hostgroup(session): :CaseAutomation: NotAutomated - :CaseComponent: Ansible + :CaseComponent: Ansible-ConfigurationManagement :Team: Rocket """ @@ -527,7 +527,7 @@ def test_positive_matcher_field_highlight(session): :CaseAutomation: NotAutomated - :CaseComponent: Ansible + :CaseComponent: Ansible-ConfigurationManagement :Team: Rocket """ From a042923a53fedff9d652f79d0dd73b2002f55eff Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 12 Mar 2024 09:46:58 -0400 Subject: [PATCH 529/586] [6.14.z] Capsule N-Minus testing (#14343) Capsule N-Minus testing (#12939) (cherry picked from commit 64262657ea61aeb20f174ed67d466c8fbcee8ffa) Co-authored-by: Jitendra Yejare --- conf/capsule.yaml.template | 2 + conf/dynaconf_hooks.py | 6 +- conftest.py | 1 + pytest_fixtures/core/sat_cap_factory.py | 40 +++++++++++--- pytest_plugins/capsule_n-minus.py | 55 +++++++++++++++++++ robottelo/exceptions.py | 6 ++ robottelo/hosts.py | 20 ++++++- tests/foreman/api/test_capsule.py | 15 ++++- tests/foreman/api/test_capsulecontent.py | 3 + .../foreman/destructive/test_registration.py | 2 + 10 files changed, 135 insertions(+), 15 deletions(-) create mode 100644 pytest_plugins/capsule_n-minus.py 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..610f0ba4c4d 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.rhel_version, + snap=settings.capsule.version.snap, ) data['SATELLITE_REPO'] = get_ohsnap_repo_url( diff --git a/conftest.py b/conftest.py index 4aefcff26b0..f54b9cce479 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 d58ea9e8b44..bc4c16b852f 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..f903e239757 --- /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.get_host_by_hostname(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 3404a9b2d6a..953a0560aa8 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1614,6 +1614,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): + """Enable 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() @@ -1701,7 +1716,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 8372c192174..86aaa089922 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] @@ -68,6 +68,17 @@ def test_positive_update_capsule(target_sat, module_capsule_configured): capsule = capsule.update(['name']) assert capsule.name == new_name + @request.addfinalizer + def _finalize(): + # Updating the hostname back + if ( + cap := target_sat.api.SmartProxy().search(query={'search': f'name = {new_name}'}) + and pytestconfig.option.n_minus + ): + cap = cap[0] + cap.name = module_capsule_configured.hostname + cap.update(['name']) + # serching for non-default capsule BZ#2077824 capsules = target_sat.api.SmartProxy().search(query={'search': 'id != 1'}) assert len(capsules) > 0 diff --git a/tests/foreman/api/test_capsulecontent.py b/tests/foreman/api/test_capsulecontent.py index 4422954f0b5..46a04b47845 100644 --- a/tests/foreman/api/test_capsulecontent.py +++ b/tests/foreman/api/test_capsulecontent.py @@ -1259,6 +1259,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, @@ -1288,6 +1289,8 @@ def test_positive_remove_capsule_orphans( :BZ: 22043089, 2211962 """ + if not pytestconfig.option.n_minus: + pytest.skip('Test cannot be run on n-minus setups session-scoped capsule') # Enable RHST repo and sync it to the Library LCE. repo_id = target_sat.api_factory.enable_rhrepo_and_fetchid( basearch='x86_64', 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 From e2ef49e5c1e0098ec9d6cff1d00d6b9d4baec3f8 Mon Sep 17 00:00:00 2001 From: Jake Callahan Date: Tue, 12 Mar 2024 11:52:41 -0400 Subject: [PATCH 530/586] [6.14.z] Updates to requirements.txt (#14107) This backport resolves some missed/failed cherry-picks, bringing requirements back in line with master. --- requirements.txt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index ee161f9fe07..39a46d11762 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # Version updates managed by dependabot betelgeuse==1.11.0 -broker[docker]==0.4.1 +# broker[docker]==0.4.1 - Temporarily disabled, see below cryptography==42.0.5 deepdiff==6.7.1 docker==7.0.0 # Temporary until Broker is back on PyPi @@ -28,7 +28,12 @@ testimony==2.3.0 wait-for==1.2.0 wrapanapi==3.6.0 -# Get airgun, nailgun and upgrade from master +# Get airgun, nailgun and upgrade from 6.14.z git+https://github.com/SatelliteQE/airgun.git@6.14.z#egg=airgun git+https://github.com/SatelliteQE/nailgun.git@6.14.z#egg=nailgun +# Broker currently is unable to push to PyPi due to [1] and [2] +# In the meantime, we install directly from the repo +# [1] - https://github.com/ParallelSSH/ssh2-python/issues/193 +# [2] - https://github.com/pypi/warehouse/issues/7136 +git+https://github.com/SatelliteQE/broker.git@0.4.5#egg=broker --editable . From 0ce73038b7d7b42e8fc8e88f5ec6f32b972f8fad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Gajdu=C5=A1ek?= Date: Tue, 12 Mar 2024 16:56:02 +0100 Subject: [PATCH 531/586] [6.14.z] Add flake8-simplify and flake8-return to ruff config (#14100) (#14341) * Add flake8-simplify and flake8-return to ruff config (#14100) (cherry picked from commit db19610b89dd4577b59f27555382c4aa736eaa3d) * fixed sanity suite, AttributeError due to not matching scope (#14309) (cherry picked from commit 063ac02e6d350a02b6cce2eb4fad9376f0a365a5) --------- Co-authored-by: Omkar Khatavkar --- conf/dynaconf_hooks.py | 3 +- pyproject.toml | 2 + pytest_fixtures/component/activationkey.py | 12 +-- pytest_fixtures/component/architecture.py | 6 +- pytest_fixtures/component/contentview.py | 3 +- pytest_fixtures/component/os.py | 3 +- pytest_fixtures/component/provision_azure.py | 24 ++--- .../component/provision_capsule_pxe.py | 2 +- pytest_fixtures/component/provision_gce.py | 18 ++-- pytest_fixtures/component/provision_pxe.py | 2 +- pytest_fixtures/component/provision_vmware.py | 3 +- .../component/provisioning_template.py | 7 +- pytest_fixtures/component/puppet.py | 6 +- pytest_fixtures/component/repository.py | 6 +- pytest_fixtures/component/rh_cloud.py | 3 +- pytest_fixtures/component/satellite_auth.py | 3 +- pytest_fixtures/component/user.py | 3 +- pytest_fixtures/core/broker.py | 1 + pytest_fixtures/core/reporting.py | 15 +-- pytest_fixtures/core/ui.py | 9 +- pytest_fixtures/core/upgrade.py | 3 +- pytest_plugins/fixture_markers.py | 3 +- pytest_plugins/logging_hooks.py | 64 +++++++------ pytest_plugins/rerun_rp/rerun_rp.py | 2 +- pytest_plugins/sanity_plugin.py | 10 +- pytest_plugins/settings_skip.py | 2 +- pytest_plugins/upgrade/scenario_workers.py | 1 + pytest_plugins/video_cleanup.py | 23 +++-- robottelo/cli/base.py | 18 +--- robottelo/cli/capsule.py | 36 ++------ robottelo/cli/hammer.py | 10 +- robottelo/cli/host.py | 16 +--- robottelo/cli/lifecycleenvironment.py | 4 +- robottelo/cli/operatingsys.py | 24 ++--- robottelo/cli/product.py | 12 +-- robottelo/cli/srpm.py | 8 +- robottelo/cli/template.py | 8 +- robottelo/cli/template_sync.py | 8 +- robottelo/cli/webhook.py | 3 +- robottelo/config/__init__.py | 2 +- robottelo/host_helpers/api_factory.py | 13 +-- robottelo/host_helpers/capsule_mixins.py | 6 +- robottelo/host_helpers/cli_factory.py | 59 ++++++------ robottelo/host_helpers/repository_mixins.py | 3 +- robottelo/host_helpers/satellite_mixins.py | 16 ++-- robottelo/hosts.py | 12 ++- robottelo/ssh.py | 3 +- robottelo/utils/datafactory.py | 25 ++--- robottelo/utils/decorators/func_locker.py | 12 +-- .../utils/decorators/func_shared/shared.py | 3 +- robottelo/utils/io/__init__.py | 1 + robottelo/utils/issue_handlers/__init__.py | 5 +- robottelo/utils/ohsnap.py | 7 +- robottelo/utils/report_portal/portal.py | 3 +- robottelo/utils/ssh.py | 3 +- robottelo/utils/vault.py | 62 ++++++------- robottelo/utils/virtwho.py | 42 ++++----- scripts/config_helpers.py | 4 +- scripts/customer_scenarios.py | 18 ++-- scripts/fixture_cli.py | 14 +-- scripts/graph_entities.py | 4 +- tests/foreman/api/test_activationkey.py | 6 +- .../api/test_computeresource_azurerm.py | 2 +- tests/foreman/api/test_contentview.py | 3 +- tests/foreman/api/test_errata.py | 4 +- tests/foreman/api/test_filter.py | 3 +- tests/foreman/api/test_hostcollection.py | 3 +- tests/foreman/api/test_multiple_paths.py | 18 ++-- tests/foreman/api/test_organization.py | 2 +- .../foreman/api/test_provisioningtemplate.py | 2 +- tests/foreman/api/test_registration.py | 4 +- tests/foreman/api/test_repository.py | 4 +- tests/foreman/api/test_role.py | 2 +- tests/foreman/api/test_subscription.py | 3 +- .../foreman/api/test_template_combination.py | 4 +- tests/foreman/api/test_templatesync.py | 24 ++--- tests/foreman/cli/test_activationkey.py | 7 +- .../cli/test_computeresource_azurerm.py | 91 +++++++++---------- .../cli/test_computeresource_libvirt.py | 2 +- tests/foreman/cli/test_contentview.py | 2 +- tests/foreman/cli/test_docker.py | 2 +- tests/foreman/cli/test_domain.py | 8 +- tests/foreman/cli/test_errata.py | 2 +- tests/foreman/cli/test_filter.py | 3 +- tests/foreman/cli/test_host.py | 10 +- tests/foreman/cli/test_leapp_client.py | 5 +- tests/foreman/cli/test_registration.py | 8 +- tests/foreman/cli/test_remoteexecution.py | 2 +- tests/foreman/cli/test_reporttemplates.py | 2 +- tests/foreman/cli/test_repository.py | 4 +- tests/foreman/cli/test_role.py | 2 +- tests/foreman/cli/test_satellitesync.py | 6 +- tests/foreman/cli/test_subscription.py | 5 +- tests/foreman/cli/test_templatesync.py | 2 +- tests/foreman/cli/test_user.py | 4 +- tests/foreman/cli/test_usergroup.py | 7 +- tests/foreman/conftest.py | 9 +- .../destructive/test_capsule_loadbalancer.py | 4 +- .../destructive/test_discoveredhost.py | 3 +- .../destructive/test_ldap_authentication.py | 5 +- tests/foreman/destructive/test_realm.py | 4 +- tests/foreman/longrun/test_inc_updates.py | 6 +- tests/foreman/longrun/test_oscap.py | 5 +- tests/foreman/maintain/test_advanced.py | 3 +- tests/foreman/ui/test_acs.py | 2 +- tests/foreman/ui/test_containerimagetag.py | 2 +- tests/foreman/ui/test_discoveredhost.py | 3 +- tests/foreman/ui/test_host.py | 17 ++-- tests/foreman/ui/test_hostcollection.py | 11 +-- tests/foreman/ui/test_ldap_authentication.py | 28 +++--- tests/foreman/ui/test_organization.py | 2 +- tests/foreman/ui/test_provisioningtemplate.py | 2 +- tests/foreman/ui/test_registration.py | 2 +- tests/foreman/ui/test_subscription.py | 5 +- tests/robottelo/conftest.py | 7 +- tests/robottelo/test_cli.py | 18 ++-- tests/robottelo/test_decorators.py | 4 +- tests/robottelo/test_dependencies.py | 5 +- tests/robottelo/test_func_locker.py | 53 ++++++----- tests/upgrades/conftest.py | 29 +++--- tests/upgrades/test_activation_key.py | 3 +- tests/upgrades/test_classparameter.py | 2 +- tests/upgrades/test_host.py | 3 +- tests/upgrades/test_puppet.py | 3 +- tests/upgrades/test_satellite_maintain.py | 3 +- tests/upgrades/test_subscription.py | 2 +- 126 files changed, 524 insertions(+), 682 deletions(-) diff --git a/conf/dynaconf_hooks.py b/conf/dynaconf_hooks.py index 610f0ba4c4d..85baf7d52cb 100644 --- a/conf/dynaconf_hooks.py +++ b/conf/dynaconf_hooks.py @@ -157,7 +157,7 @@ def get_dogfood_satclient_repos(settings): def get_ohsnap_repo_url(settings, repo, product=None, release=None, os_release=None, snap=''): - repourl = dogfood_repository( + return dogfood_repository( settings.ohsnap, repo=repo, product=product, @@ -165,4 +165,3 @@ def get_ohsnap_repo_url(settings, repo, product=None, release=None, os_release=N os_release=os_release, snap=snap, ).baseurl - return repourl diff --git a/pyproject.toml b/pyproject.toml index 4f5c8af613f..72a8e2b8f3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,8 @@ select = [ "I", # isort # "Q", # flake8-quotes "PT", # flake8-pytest + "RET", # flake8-return + "SIM", # flake8-simplify "UP", # pyupgrade "W", # pycodestyle ] diff --git a/pytest_fixtures/component/activationkey.py b/pytest_fixtures/component/activationkey.py index 98deb66c3cb..e2a68959d29 100644 --- a/pytest_fixtures/component/activationkey.py +++ b/pytest_fixtures/component/activationkey.py @@ -8,31 +8,28 @@ @pytest.fixture(scope='module') def module_activation_key(module_entitlement_manifest_org, module_target_sat): """Create activation key using default CV and library environment.""" - activation_key = module_target_sat.api.ActivationKey( + module_target_sat.api.ActivationKey( content_view=module_entitlement_manifest_org.default_content_view.id, environment=module_entitlement_manifest_org.library.id, organization=module_entitlement_manifest_org, ).create() - return activation_key @pytest.fixture(scope='module') def module_ak(module_lce, module_org, module_target_sat): - ak = module_target_sat.api.ActivationKey( + return module_target_sat.api.ActivationKey( environment=module_lce, organization=module_org, ).create() - return ak @pytest.fixture(scope='module') def module_ak_with_cv(module_lce, module_org, module_promoted_cv, module_target_sat): - ak = module_target_sat.api.ActivationKey( + return module_target_sat.api.ActivationKey( content_view=module_promoted_cv, environment=module_lce, organization=module_org, ).create() - return ak @pytest.fixture(scope='module') @@ -45,7 +42,7 @@ def module_ak_with_synced_repo(module_org, module_target_sat): {'product-id': new_product['id'], 'content-type': 'yum'} ) Repository.synchronize({'id': new_repo['id']}) - ak = module_target_sat.cli_factory.make_activation_key( + return module_target_sat.cli_factory.make_activation_key( { 'lifecycle-environment': 'Library', 'content-view': 'Default Organization View', @@ -53,4 +50,3 @@ def module_ak_with_synced_repo(module_org, module_target_sat): 'auto-attach': False, } ) - return ak diff --git a/pytest_fixtures/component/architecture.py b/pytest_fixtures/component/architecture.py index 96d758f3d64..5ff90f3cb37 100644 --- a/pytest_fixtures/component/architecture.py +++ b/pytest_fixtures/component/architecture.py @@ -6,22 +6,20 @@ @pytest.fixture(scope='session') def default_architecture(session_target_sat): - arch = ( + return ( session_target_sat.api.Architecture() .search(query={'search': f'name="{DEFAULT_ARCHITECTURE}"'})[0] .read() ) - return arch @pytest.fixture(scope='session') def session_puppet_default_architecture(session_puppet_enabled_sat): - arch = ( + return ( session_puppet_enabled_sat.api.Architecture() .search(query={'search': f'name="{DEFAULT_ARCHITECTURE}"'})[0] .read() ) - return arch @pytest.fixture(scope='module') diff --git a/pytest_fixtures/component/contentview.py b/pytest_fixtures/component/contentview.py index c1879f43338..79d66014949 100644 --- a/pytest_fixtures/component/contentview.py +++ b/pytest_fixtures/component/contentview.py @@ -36,12 +36,11 @@ def module_ak_cv_lce(module_org, module_lce, module_published_cv, module_target_ content_view_version = module_published_cv.version[0] content_view_version.promote(data={'environment_ids': module_lce.id}) module_published_cv = module_published_cv.read() - module_ak_with_cv_lce = module_target_sat.api.ActivationKey( + return module_target_sat.api.ActivationKey( content_view=module_published_cv, environment=module_lce, organization=module_org, ).create() - return module_ak_with_cv_lce @pytest.fixture(scope='module') diff --git a/pytest_fixtures/component/os.py b/pytest_fixtures/component/os.py index e6039c1b6f9..0c94baa09ce 100644 --- a/pytest_fixtures/component/os.py +++ b/pytest_fixtures/component/os.py @@ -26,8 +26,7 @@ def default_os( os.ptable.append(default_partitiontable) os.provisioning_template.append(default_pxetemplate) os.update(['architecture', 'ptable', 'provisioning_template']) - os = entities.OperatingSystem(id=os.id).read() - return os + return entities.OperatingSystem(id=os.id).read() @pytest.fixture(scope='module') diff --git a/pytest_fixtures/component/provision_azure.py b/pytest_fixtures/component/provision_azure.py index 27bcea16e1f..7e2a05f0707 100644 --- a/pytest_fixtures/component/provision_azure.py +++ b/pytest_fixtures/component/provision_azure.py @@ -44,17 +44,16 @@ def sat_azure_default_os(sat_azure): @pytest.fixture(scope='module') def sat_azure_default_architecture(sat_azure): - arch = ( + return ( sat_azure.api.Architecture() .search(query={'search': f'name="{DEFAULT_ARCHITECTURE}"'})[0] .read() ) - return arch @pytest.fixture(scope='session') def azurerm_settings(): - deps = { + return { 'tenant': settings.azurerm.tenant_id, 'app_ident': settings.azurerm.client_id, 'sub_id': settings.azurerm.subscription_id, @@ -62,7 +61,6 @@ def azurerm_settings(): 'secret': settings.azurerm.client_secret, 'region': settings.azurerm.azure_region.lower().replace(' ', ''), } - return deps @pytest.fixture(scope='session') @@ -86,7 +84,7 @@ def azurermclient(azurerm_settings): @pytest.fixture(scope='module') def module_azurerm_cr(azurerm_settings, sat_azure_org, sat_azure_loc, sat_azure): """Create AzureRM Compute Resource""" - azure_cr = sat_azure.api.AzureRMComputeResource( + return sat_azure.api.AzureRMComputeResource( name=gen_string('alpha'), provider='AzureRm', tenant=azurerm_settings['tenant'], @@ -97,7 +95,6 @@ def module_azurerm_cr(azurerm_settings, sat_azure_org, sat_azure_loc, sat_azure) organization=[sat_azure_org], location=[sat_azure_loc], ).create() - return azure_cr @pytest.fixture(scope='module') @@ -108,7 +105,7 @@ def module_azurerm_finishimg( module_azurerm_cr, ): """Creates Finish Template image on AzureRM Compute Resource""" - finish_image = sat_azure.api.Image( + return sat_azure.api.Image( architecture=sat_azure_default_architecture, compute_resource=module_azurerm_cr, name=gen_string('alpha'), @@ -116,7 +113,6 @@ def module_azurerm_finishimg( username=settings.azurerm.username, uuid=AZURERM_RHEL7_FT_IMG_URN, ).create() - return finish_image @pytest.fixture(scope='module') @@ -127,7 +123,7 @@ def module_azurerm_byos_finishimg( sat_azure, ): """Creates BYOS Finish Template image on AzureRM Compute Resource""" - finish_image = sat_azure.api.Image( + return sat_azure.api.Image( architecture=sat_azure_default_architecture, compute_resource=module_azurerm_cr, name=gen_string('alpha'), @@ -135,7 +131,6 @@ def module_azurerm_byos_finishimg( username=settings.azurerm.username, uuid=AZURERM_RHEL7_FT_BYOS_IMG_URN, ).create() - return finish_image @pytest.fixture(scope='module') @@ -146,7 +141,7 @@ def module_azurerm_cloudimg( module_azurerm_cr, ): """Creates cloudinit image on AzureRM Compute Resource""" - finish_image = sat_azure.api.Image( + return sat_azure.api.Image( architecture=sat_azure_default_architecture, compute_resource=module_azurerm_cr, name=gen_string('alpha'), @@ -155,7 +150,6 @@ def module_azurerm_cloudimg( uuid=AZURERM_RHEL7_UD_IMG_URN, user_data=True, ).create() - return finish_image @pytest.fixture(scope='module') @@ -166,7 +160,7 @@ def module_azurerm_gallery_finishimg( module_azurerm_cr, ): """Creates Shared Gallery Finish Template image on AzureRM Compute Resource""" - finish_image = sat_azure.api.Image( + return sat_azure.api.Image( architecture=sat_azure_default_architecture, compute_resource=module_azurerm_cr, name=gen_string('alpha'), @@ -174,7 +168,6 @@ def module_azurerm_gallery_finishimg( username=settings.azurerm.username, uuid=AZURERM_RHEL7_FT_GALLERY_IMG_URN, ).create() - return finish_image @pytest.fixture(scope='module') @@ -185,7 +178,7 @@ def module_azurerm_custom_finishimg( module_azurerm_cr, ): """Creates Custom Finish Template image on AzureRM Compute Resource""" - finish_image = sat_azure.api.Image( + return sat_azure.api.Image( architecture=sat_azure_default_architecture, compute_resource=module_azurerm_cr, name=gen_string('alpha'), @@ -193,4 +186,3 @@ def module_azurerm_custom_finishimg( username=settings.azurerm.username, uuid=AZURERM_RHEL7_FT_CUSTOM_IMG_URN, ).create() - return finish_image diff --git a/pytest_fixtures/component/provision_capsule_pxe.py b/pytest_fixtures/component/provision_capsule_pxe.py index 04a2c4fb475..4360ba6e4bd 100644 --- a/pytest_fixtures/component/provision_capsule_pxe.py +++ b/pytest_fixtures/component/provision_capsule_pxe.py @@ -209,7 +209,7 @@ def capsule_provisioning_rhel_content( releasever=constants.REPOS['kickstart'][name]['version'], ) # do not sync content repos for discovery based provisioning. - if not capsule_provisioning_sat.provisioning_type == 'discovery': + if 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, diff --git a/pytest_fixtures/component/provision_gce.py b/pytest_fixtures/component/provision_gce.py index 4c11ecb0918..64b706d772e 100644 --- a/pytest_fixtures/component/provision_gce.py +++ b/pytest_fixtures/component/provision_gce.py @@ -53,12 +53,11 @@ def sat_gce_default_partition_table(sat_gce): @pytest.fixture(scope='module') def sat_gce_default_architecture(sat_gce): - arch = ( + return ( sat_gce.api.Architecture() .search(query={'search': f'name="{DEFAULT_ARCHITECTURE}"'})[0] .read() ) - return arch @pytest.fixture(scope='session') @@ -96,8 +95,7 @@ def gce_latest_rhel_uuid(googleclient): filter_expr=f'name:{GCE_TARGET_RHEL_IMAGE_NAME}*', ) latest_template_name = max(tpl.name for tpl in templates) - latest_template_uuid = next(tpl for tpl in templates if tpl.name == latest_template_name).uuid - return latest_template_uuid + return next(tpl for tpl in templates if tpl.name == latest_template_name).uuid @pytest.fixture(scope='session') @@ -116,7 +114,7 @@ def session_default_os(session_target_sat): @pytest.fixture(scope='module') def module_gce_compute(sat_gce, sat_gce_org, sat_gce_loc, gce_cert): - gce_cr = sat_gce.api.GCEComputeResource( + return sat_gce.api.GCEComputeResource( name=gen_string('alphanumeric'), provider='GCE', key_path=settings.gce.cert_path, @@ -124,7 +122,6 @@ def module_gce_compute(sat_gce, sat_gce_org, sat_gce_loc, gce_cert): organization=[sat_gce_org], location=[sat_gce_loc], ).create() - return gce_cr @pytest.fixture(scope='module') @@ -206,7 +203,7 @@ def gce_hostgroup( googleclient, ): """Sets Hostgroup for GCE Host Provisioning""" - hgroup = sat_gce.api.HostGroup( + return sat_gce.api.HostGroup( architecture=sat_gce_default_architecture, compute_resource=module_gce_compute, domain=sat_gce_domain, @@ -216,7 +213,6 @@ def gce_hostgroup( organization=[sat_gce_org], ptable=sat_gce_default_partition_table, ).create() - return hgroup @pytest.fixture(scope='module') @@ -250,7 +246,7 @@ def module_gce_cloudimg( sat_gce, ): """Creates cloudinit image on GCE Compute Resource""" - cloud_image = sat_gce.api.Image( + return sat_gce.api.Image( architecture=sat_gce_default_architecture, compute_resource=module_gce_compute, name=gen_string('alpha'), @@ -259,7 +255,6 @@ def module_gce_cloudimg( uuid=gce_custom_cloudinit_uuid, user_data=True, ).create() - return cloud_image @pytest.fixture(scope='module') @@ -271,7 +266,7 @@ def module_gce_finishimg( sat_gce, ): """Creates finish image on GCE Compute Resource""" - finish_image = sat_gce.api.Image( + return sat_gce.api.Image( architecture=sat_gce_default_architecture, compute_resource=module_gce_compute, name=gen_string('alpha'), @@ -279,7 +274,6 @@ def module_gce_finishimg( username=gen_string('alpha'), uuid=gce_latest_rhel_uuid, ).create() - return finish_image @pytest.fixture diff --git a/pytest_fixtures/component/provision_pxe.py b/pytest_fixtures/component/provision_pxe.py index 2fc375bc5f1..beef717d94d 100644 --- a/pytest_fixtures/component/provision_pxe.py +++ b/pytest_fixtures/component/provision_pxe.py @@ -70,7 +70,7 @@ def module_provisioning_rhel_content( releasever=constants.REPOS['kickstart'][name]['version'], ) # do not sync content repos for discovery based provisioning. - if not module_provisioning_sat.provisioning_type == 'discovery': + if module_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, diff --git a/pytest_fixtures/component/provision_vmware.py b/pytest_fixtures/component/provision_vmware.py index 507f180a158..385f49bae38 100644 --- a/pytest_fixtures/component/provision_vmware.py +++ b/pytest_fixtures/component/provision_vmware.py @@ -15,7 +15,7 @@ def vmware(request): @pytest.fixture(scope='module') def module_vmware_cr(module_provisioning_sat, module_sca_manifest_org, module_location, vmware): - vmware_cr = module_provisioning_sat.sat.api.VMWareComputeResource( + return module_provisioning_sat.sat.api.VMWareComputeResource( name=gen_string('alpha'), provider='Vmware', url=vmware.hostname, @@ -25,7 +25,6 @@ def module_vmware_cr(module_provisioning_sat, module_sca_manifest_org, module_lo organization=[module_sca_manifest_org], location=[module_location], ).create() - return vmware_cr @pytest.fixture diff --git a/pytest_fixtures/component/provisioning_template.py b/pytest_fixtures/component/provisioning_template.py index cb4ea6d84d5..7b797e1da7c 100644 --- a/pytest_fixtures/component/provisioning_template.py +++ b/pytest_fixtures/component/provisioning_template.py @@ -17,8 +17,7 @@ def module_provisioningtemplate_default(module_org, module_location): provisioning_template.organization.append(module_org) provisioning_template.location.append(module_location) provisioning_template.update(['organization', 'location']) - provisioning_template = entities.ProvisioningTemplate(id=provisioning_template.id).read() - return provisioning_template + return entities.ProvisioningTemplate(id=provisioning_template.id).read() @pytest.fixture(scope='module') @@ -30,8 +29,7 @@ def module_provisioningtemplate_pxe(module_org, module_location): pxe_template.organization.append(module_org) pxe_template.location.append(module_location) pxe_template.update(['organization', 'location']) - pxe_template = entities.ProvisioningTemplate(id=pxe_template.id).read() - return pxe_template + return entities.ProvisioningTemplate(id=pxe_template.id).read() @pytest.fixture(scope='session') @@ -39,6 +37,7 @@ def default_partitiontable(): ptables = entities.PartitionTable().search(query={'search': f'name="{DEFAULT_PTABLE}"'}) if ptables: return ptables[0].read() + return None @pytest.fixture(scope='module') diff --git a/pytest_fixtures/component/puppet.py b/pytest_fixtures/component/puppet.py index 38cad7163c0..c531c7888ac 100644 --- a/pytest_fixtures/component/puppet.py +++ b/pytest_fixtures/component/puppet.py @@ -44,6 +44,7 @@ def default_puppet_environment(module_puppet_org, session_puppet_enabled_sat): ) if environments: return environments[0].read() + return None @pytest.fixture(scope='module') @@ -101,10 +102,7 @@ def module_puppet_classes( @pytest.fixture(scope='session', params=[True, False], ids=["puppet_enabled", "puppet_disabled"]) def parametrized_puppet_sat(request, session_target_sat, session_puppet_enabled_sat): - if request.param: - sat = session_puppet_enabled_sat - else: - sat = session_target_sat + sat = session_puppet_enabled_sat if request.param else session_target_sat return {'sat': sat, 'enabled': request.param} diff --git a/pytest_fixtures/component/repository.py b/pytest_fixtures/component/repository.py index a3a48083d1c..b7a26560980 100644 --- a/pytest_fixtures/component/repository.py +++ b/pytest_fixtures/component/repository.py @@ -67,8 +67,7 @@ def repo_setup(): product = entities.Product(organization=org).create() repo = entities.Repository(name=repo_name, product=product).create() lce = entities.LifecycleEnvironment(organization=org).create() - details = {'org': org, 'product': product, 'repo': repo, 'lce': lce} - return details + return {'org': org, 'product': product, 'repo': repo, 'lce': lce} @pytest.fixture(scope='module') @@ -189,7 +188,7 @@ def repos_collection(request, target_sat): """ repos = getattr(request, 'param', []) repo_distro, repos = _simplify_repos(request, repos) - _repos_collection = target_sat.cli_factory.RepositoryCollection( + return target_sat.cli_factory.RepositoryCollection( distro=repo_distro or request.getfixturevalue('distro'), repositories=[ getattr(target_sat.cli_factory, repo_name)(**repo_params) @@ -197,7 +196,6 @@ def repos_collection(request, target_sat): for repo_name, repo_params in repo.items() ], ) - return _repos_collection @pytest.fixture(scope='module') diff --git a/pytest_fixtures/component/rh_cloud.py b/pytest_fixtures/component/rh_cloud.py index ead92f68db2..fd035f653c3 100644 --- a/pytest_fixtures/component/rh_cloud.py +++ b/pytest_fixtures/component/rh_cloud.py @@ -15,7 +15,7 @@ def rhcloud_manifest_org(module_target_sat, module_extra_rhel_entitlement_manife def rhcloud_activation_key(module_target_sat, rhcloud_manifest_org): """A module-level fixture to create an Activation key in module_org""" purpose_addons = "test-addon1, test-addon2" - ak = module_target_sat.api.ActivationKey( + return module_target_sat.api.ActivationKey( content_view=rhcloud_manifest_org.default_content_view, organization=rhcloud_manifest_org, environment=module_target_sat.api.LifecycleEnvironment(id=rhcloud_manifest_org.library.id), @@ -25,7 +25,6 @@ def rhcloud_activation_key(module_target_sat, rhcloud_manifest_org): purpose_role='test-role', auto_attach=False, ).create() - return ak @pytest.fixture(scope='module') diff --git a/pytest_fixtures/component/satellite_auth.py b/pytest_fixtures/component/satellite_auth.py index 4eb984390f6..c6df8df1897 100644 --- a/pytest_fixtures/component/satellite_auth.py +++ b/pytest_fixtures/component/satellite_auth.py @@ -271,11 +271,12 @@ def auth_data(request, ad_data, ipa_data): ad_data['attr_login'] = LDAP_ATTR['login_ad'] ad_data['auth_type'] = auth_type return ad_data - elif auth_type == 'ipa': + if auth_type == 'ipa': ipa_data['server_type'] = LDAP_SERVER_TYPE['UI']['ipa'] ipa_data['attr_login'] = LDAP_ATTR['login'] ipa_data['auth_type'] = auth_type return ipa_data + return None @pytest.fixture(scope='module') diff --git a/pytest_fixtures/component/user.py b/pytest_fixtures/component/user.py index 3f4d1034e6e..a1b9b7a1a9f 100644 --- a/pytest_fixtures/component/user.py +++ b/pytest_fixtures/component/user.py @@ -9,5 +9,4 @@ def user_not_exists(request): if users: users[0].delete() return True - else: - return False + return False diff --git a/pytest_fixtures/core/broker.py b/pytest_fixtures/core/broker.py index 9d207fcd9d7..f3356dbbef5 100644 --- a/pytest_fixtures/core/broker.py +++ b/pytest_fixtures/core/broker.py @@ -16,6 +16,7 @@ def _default_sat(align_to_satellite): return Satellite.get_host_by_hostname(settings.server.hostname) except ContentHostError: return Satellite() + return None @contextmanager diff --git a/pytest_fixtures/core/reporting.py b/pytest_fixtures/core/reporting.py index baf9d3cde7d..c89c87d46da 100644 --- a/pytest_fixtures/core/reporting.py +++ b/pytest_fixtures/core/reporting.py @@ -35,13 +35,14 @@ def pytest_sessionstart(session): remove if resolved and set autouse=True for record_testsuite_timestamp_xml fixture """ - if get_xdist_worker_id(session) == 'master': - if session.config.pluginmanager.hasplugin('junitxml'): - xml = session.config._store.get(xml_key, None) - if xml: - xml.add_global_property( - 'start_time', datetime.datetime.utcnow().strftime(FMT_XUNIT_TIME) - ) + if get_xdist_worker_id(session) == 'master' and session.config.pluginmanager.hasplugin( + 'junitxml' + ): + xml = session.config._store.get(xml_key, None) + if xml: + xml.add_global_property( + 'start_time', datetime.datetime.utcnow().strftime(FMT_XUNIT_TIME) + ) @pytest.fixture(autouse=False, scope='session') diff --git a/pytest_fixtures/core/ui.py b/pytest_fixtures/core/ui.py index c266e363c7e..1c24d6f04a9 100644 --- a/pytest_fixtures/core/ui.py +++ b/pytest_fixtures/core/ui.py @@ -102,7 +102,8 @@ def ui_session_record_property(request, record_property): test_file_path = request.node.fspath.strpath if any(directory in test_file_path for directory in test_directories): for fixture in request.node.fixturenames: - if request.fixturename != fixture: - if isinstance(request.getfixturevalue(fixture), Satellite): - sat = request.getfixturevalue(fixture) - sat.record_property = record_property + if request.fixturename != fixture and isinstance( + request.getfixturevalue(fixture), Satellite + ): + sat = request.getfixturevalue(fixture) + sat.record_property = record_property diff --git a/pytest_fixtures/core/upgrade.py b/pytest_fixtures/core/upgrade.py index caf4532713b..5726f9f80a9 100644 --- a/pytest_fixtures/core/upgrade.py +++ b/pytest_fixtures/core/upgrade.py @@ -10,12 +10,11 @@ def dependent_scenario_name(request): """ This fixture is used to collect the dependent test case name. """ - depend_test_name = [ + return [ mark.kwargs['depend_on'].__name__ for mark in request.node.own_markers if 'depend_on' in mark.kwargs ][0] - return depend_test_name @pytest.fixture(scope="session") diff --git a/pytest_plugins/fixture_markers.py b/pytest_plugins/fixture_markers.py index 41e12b85bab..6fe2f4df4af 100644 --- a/pytest_plugins/fixture_markers.py +++ b/pytest_plugins/fixture_markers.py @@ -16,7 +16,7 @@ def pytest_generate_tests(metafunc): content_host_fixture = ''.join([i for i in TARGET_FIXTURES if i in metafunc.fixturenames]) if content_host_fixture in metafunc.fixturenames: function_marks = getattr(metafunc.function, 'pytestmark', []) - no_containers = any('no_containers' == mark.name for mark in function_marks) + no_containers = any(mark.name == 'no_containers' for mark in function_marks) # process eventual rhel_version_list markers matchers = [i.args for i in function_marks if i.name == 'rhel_ver_list'] list_params = [] @@ -86,6 +86,7 @@ def chost_rhelver(params): for param in params: if 'contenthost' in param: return params[param].get('rhel_version') + return None content_host_fixture_names = [m[0] for m in getmembers(contenthosts, isfunction)] for item in items: diff --git a/pytest_plugins/logging_hooks.py b/pytest_plugins/logging_hooks.py index 9cd7ea4c84f..c15136a647a 100644 --- a/pytest_plugins/logging_hooks.py +++ b/pytest_plugins/logging_hooks.py @@ -1,3 +1,4 @@ +import contextlib import logging import logzero @@ -13,10 +14,8 @@ robottelo_log_file, ) -try: +with contextlib.suppress(ImportError): from pytest_reportportal import RPLogger, RPLogHandler -except ImportError: - pass @pytest.fixture(autouse=True, scope='session') @@ -37,37 +36,36 @@ def configure_logging(request, worker_id): if use_rp_logger: logging.setLoggerClass(RPLogger) - if is_xdist_worker(request): - if f'{worker_id}' not in [h.get_name() for h in logger.handlers]: - # Track the core logger's file handler level, set it in case core logger wasn't set - worker_log_level = 'INFO' - handlers_to_remove = [ - h - for h in logger.handlers - if isinstance(h, logging.FileHandler) - and getattr(h, 'baseFilename', None) == str(robottelo_log_file) - ] - for handler in handlers_to_remove: - logger.removeHandler(handler) - worker_log_level = handler.level - worker_handler = logging.FileHandler( - robottelo_log_dir.joinpath(f'robottelo_{worker_id}.log') - ) - worker_handler.set_name(f'{worker_id}') - worker_handler.setFormatter(worker_formatter) - worker_handler.setLevel(worker_log_level) - logger.addHandler(worker_handler) - broker_log_setup( - level=logging_yaml.broker.level, - file_level=logging_yaml.broker.fileLevel, - formatter=worker_formatter, - path=robottelo_log_dir.joinpath(f'robottelo_{worker_id}.log'), - ) + if is_xdist_worker(request) and f'{worker_id}' not in [h.get_name() for h in logger.handlers]: + # Track the core logger's file handler level, set it in case core logger wasn't set + worker_log_level = 'INFO' + handlers_to_remove = [ + h + for h in logger.handlers + if isinstance(h, logging.FileHandler) + and getattr(h, 'baseFilename', None) == str(robottelo_log_file) + ] + for handler in handlers_to_remove: + logger.removeHandler(handler) + worker_log_level = handler.level + worker_handler = logging.FileHandler( + robottelo_log_dir.joinpath(f'robottelo_{worker_id}.log') + ) + worker_handler.set_name(f'{worker_id}') + worker_handler.setFormatter(worker_formatter) + worker_handler.setLevel(worker_log_level) + logger.addHandler(worker_handler) + broker_log_setup( + level=logging_yaml.broker.level, + file_level=logging_yaml.broker.fileLevel, + formatter=worker_formatter, + path=robottelo_log_dir.joinpath(f'robottelo_{worker_id}.log'), + ) - if use_rp_logger: - rp_handler = RPLogHandler(request.node.config.py_test_service) - rp_handler.setFormatter(worker_formatter) - # logger.addHandler(rp_handler) + if use_rp_logger: + rp_handler = RPLogHandler(request.node.config.py_test_service) + rp_handler.setFormatter(worker_formatter) + # logger.addHandler(rp_handler) def pytest_runtest_logstart(nodeid, location): diff --git a/pytest_plugins/rerun_rp/rerun_rp.py b/pytest_plugins/rerun_rp/rerun_rp.py index 5b14873a885..7007880ce4b 100644 --- a/pytest_plugins/rerun_rp/rerun_rp.py +++ b/pytest_plugins/rerun_rp/rerun_rp.py @@ -123,7 +123,7 @@ def pytest_collection_modifyitems(items, config): test_args['status'].append('SKIPPED') if fail_args: test_args['status'].append('FAILED') - if not fail_args == 'all': + if fail_args != 'all': defect_types = fail_args.split(',') allowed_args = [*rp.defect_types.keys()] if not set(defect_types).issubset(set(allowed_args)): diff --git a/pytest_plugins/sanity_plugin.py b/pytest_plugins/sanity_plugin.py index f955d9cf4ca..1d93a4b45f3 100644 --- a/pytest_plugins/sanity_plugin.py +++ b/pytest_plugins/sanity_plugin.py @@ -43,10 +43,12 @@ def pytest_collection_modifyitems(session, items, config): deselected.append(item) continue # Remove parametrization from organization test - if 'test_positive_create_with_name_and_description' in item.name: - if 'alphanumeric' not in item.name: - deselected.append(item) - continue + if ( + 'test_positive_create_with_name_and_description' in item.name + and 'alphanumeric' not in item.name + ): + deselected.append(item) + continue # Else select selected.append(item) diff --git a/pytest_plugins/settings_skip.py b/pytest_plugins/settings_skip.py index 6ee053adee5..e345338d5d8 100644 --- a/pytest_plugins/settings_skip.py +++ b/pytest_plugins/settings_skip.py @@ -22,7 +22,7 @@ def pytest_runtest_setup(item): skip_marker = item.get_closest_marker('skip_if_not_set', None) if skip_marker and skip_marker.args: options_set = {arg.upper() for arg in skip_marker.args} - settings_set = {key for key in settings.keys() if not key.endswith('_FOR_DYNACONF')} + settings_set = {key for key in settings if not key.endswith('_FOR_DYNACONF')} if not options_set.issubset(settings_set): invalid = options_set.difference(settings_set) raise ValueError( diff --git a/pytest_plugins/upgrade/scenario_workers.py b/pytest_plugins/upgrade/scenario_workers.py index 099ff1c0fe3..9deeb0ddf25 100644 --- a/pytest_plugins/upgrade/scenario_workers.py +++ b/pytest_plugins/upgrade/scenario_workers.py @@ -23,6 +23,7 @@ def save_worker_hostname(test_name, target_sat): def shared_workers(): if json_file.exists(): return json.loads(json_file.read_text()) + return None def get_worker_hostname_from_testname(test_name, shared_workers): diff --git a/pytest_plugins/video_cleanup.py b/pytest_plugins/video_cleanup.py index 08bbf5721d9..320864b7060 100644 --- a/pytest_plugins/video_cleanup.py +++ b/pytest_plugins/video_cleanup.py @@ -64,15 +64,14 @@ def pytest_runtest_makereport(item): 'longrepr': str(report.longrepr), } ) - if report.when == "teardown": - if item.nodeid in test_results: - result_info = test_results[item.nodeid] - if result_info.outcome == 'passed': - report.user_properties = [ - (key, value) for key, value in report.user_properties if key != 'video_url' - ] - session_id_tuple = next( - (t for t in report.user_properties if t[0] == 'session_id'), None - ) - session_id = session_id_tuple[1] if session_id_tuple else None - _clean_video(session_id, item.nodeid) + if report.when == "teardown" and item.nodeid in test_results: + result_info = test_results[item.nodeid] + if result_info.outcome == 'passed': + report.user_properties = [ + (key, value) for key, value in report.user_properties if key != 'video_url' + ] + session_id_tuple = next( + (t for t in report.user_properties if t[0] == 'session_id'), None + ) + session_id = session_id_tuple[1] if session_id_tuple else None + _clean_video(session_id, item.nodeid) diff --git a/robottelo/cli/base.py b/robottelo/cli/base.py index 2d3e6dbf850..2a3ad35ff4f 100644 --- a/robottelo/cli/base.py +++ b/robottelo/cli/base.py @@ -139,9 +139,7 @@ def delete_parameter(cls, options=None): cls.command_sub = 'delete-parameter' - result = cls.execute(cls._construct_command(options)) - - return result + return cls.execute(cls._construct_command(options)) @classmethod def dump(cls, options=None): @@ -151,9 +149,7 @@ def dump(cls, options=None): cls.command_sub = 'dump' - result = cls.execute(cls._construct_command(options)) - - return result + return cls.execute(cls._construct_command(options)) @classmethod def _get_username_password(cls, username=None, password=None): @@ -211,16 +207,14 @@ def execute( ) if return_raw_response: return response - else: - return cls._handle_response(response, ignore_stderr=ignore_stderr) + return cls._handle_response(response, ignore_stderr=ignore_stderr) @classmethod def sm_execute(cls, command, hostname=None, timeout=None, **kwargs): """Executes the satellite-maintain cli commands on the server via ssh""" env_var = kwargs.get('env_var') or '' client = get_client(hostname=hostname or cls.hostname) - result = client.execute(f'{env_var} satellite-maintain {command}', timeout=timeout) - return result + return client.execute(f'{env_var} satellite-maintain {command}', timeout=timeout) @classmethod def exists(cls, options=None, search=None): @@ -375,6 +369,4 @@ def _construct_command(cls, options=None): if isinstance(val, list): val = ','.join(str(el) for el in val) tail += f' --{key}="{val}"' - cmd = f"{cls.command_base or ''} {cls.command_sub or ''} {tail.strip()} {cls.command_end or ''}" - - return cmd + return f"{cls.command_base or ''} {cls.command_sub or ''} {tail.strip()} {cls.command_end or ''}" diff --git a/robottelo/cli/capsule.py b/robottelo/cli/capsule.py index 9f126e5a2a4..1e4e4a20b36 100644 --- a/robottelo/cli/capsule.py +++ b/robottelo/cli/capsule.py @@ -35,9 +35,7 @@ def content_add_lifecycle_environment(cls, options): cls.command_sub = 'content add-lifecycle-environment' - result = cls.execute(cls._construct_command(options), output_format='csv') - - return result + return cls.execute(cls._construct_command(options), output_format='csv') @classmethod def content_available_lifecycle_environments(cls, options): @@ -45,9 +43,7 @@ def content_available_lifecycle_environments(cls, options): cls.command_sub = 'content available-lifecycle-environments' - result = cls.execute(cls._construct_command(options), output_format='csv') - - return result + return cls.execute(cls._construct_command(options), output_format='csv') @classmethod def content_info(cls, options): @@ -55,9 +51,7 @@ def content_info(cls, options): cls.command_sub = 'content info' - result = cls.execute(cls._construct_command(options), output_format='json') - - return result + return cls.execute(cls._construct_command(options), output_format='json') @classmethod def content_lifecycle_environments(cls, options): @@ -65,9 +59,7 @@ def content_lifecycle_environments(cls, options): cls.command_sub = 'content lifecycle-environments' - result = cls.execute(cls._construct_command(options), output_format='csv') - - return result + return cls.execute(cls._construct_command(options), output_format='csv') @classmethod def content_remove_lifecycle_environment(cls, options): @@ -75,9 +67,7 @@ def content_remove_lifecycle_environment(cls, options): cls.command_sub = 'content remove-lifecycle-environment' - result = cls.execute(cls._construct_command(options), output_format='csv') - - return result + return cls.execute(cls._construct_command(options), output_format='csv') @classmethod def content_synchronization_status(cls, options): @@ -85,9 +75,7 @@ def content_synchronization_status(cls, options): cls.command_sub = 'content synchronization-status' - result = cls.execute(cls._construct_command(options), output_format='csv') - - return result + return cls.execute(cls._construct_command(options), output_format='csv') @classmethod def content_synchronize(cls, options, return_raw_response=None, timeout=3600000): @@ -95,7 +83,7 @@ def content_synchronize(cls, options, return_raw_response=None, timeout=3600000) cls.command_sub = 'content synchronize' - result = cls.execute( + return cls.execute( cls._construct_command(options), output_format='csv', ignore_stderr=True, @@ -103,17 +91,13 @@ def content_synchronize(cls, options, return_raw_response=None, timeout=3600000) timeout=timeout, ) - return result - @classmethod def import_classes(cls, options): """Import puppet classes from puppet Capsule.""" cls.command_sub = 'import-classes' - result = cls.execute(cls._construct_command(options), output_format='csv') - - return result + return cls.execute(cls._construct_command(options), output_format='csv') @classmethod def refresh_features(cls, options): @@ -121,6 +105,4 @@ def refresh_features(cls, options): cls.command_sub = 'refresh-features' - result = cls.execute(cls._construct_command(options), output_format='csv') - - return result + return cls.execute(cls._construct_command(options), output_format='csv') diff --git a/robottelo/cli/hammer.py b/robottelo/cli/hammer.py index 13cdfb41dbb..b84e6bf2158 100644 --- a/robottelo/cli/hammer.py +++ b/robottelo/cli/hammer.py @@ -26,10 +26,10 @@ def _normalize_obj(obj): """ if isinstance(obj, dict): return {_normalize(k): _normalize_obj(v) for k, v in obj.items()} - elif isinstance(obj, list): + if isinstance(obj, list): return [_normalize_obj(v) for v in obj] # doing this to conform to csv parser - elif isinstance(obj, int) and not isinstance(obj, bool): + if isinstance(obj, int) and not isinstance(obj, bool): return str(obj) return obj @@ -48,8 +48,8 @@ def parse_csv(output): """Parse CSV output from Hammer CLI and convert it to python dictionary.""" # ignore warning about puppet and ostree deprecation output.replace('Puppet and OSTree will no longer be supported in Katello 3.16\n', '') - is_rex = True if 'Job invocation' in output else False - is_pkg_list = True if 'Nvra' in output else False + is_rex = 'Job invocation' in output + is_pkg_list = 'Nvra' in output # Validate if the output is eligible for CSV conversions else return as it is if not is_csv(output) and not is_rex and not is_pkg_list: return output @@ -199,7 +199,7 @@ def parse_info(output): if line.startswith(' '): # sub-properties are indented # values are separated by ':' or '=>', but not by '::' which can be # entity name like 'test::params::keys' - if line.find(':') != -1 and not line.find('::') != -1: + if line.find(':') != -1 and line.find('::') == -1: key, value = line.lstrip().split(":", 1) elif line.find('=>') != -1 and len(line.lstrip().split(" =>", 1)) == 2: key, value = line.lstrip().split(" =>", 1) diff --git a/robottelo/cli/host.py b/robottelo/cli/host.py index 6ec096cfe10..05f042f9dde 100644 --- a/robottelo/cli/host.py +++ b/robottelo/cli/host.py @@ -215,9 +215,7 @@ def reboot(cls, options=None): cls.command_sub = 'reboot' - result = cls.execute(cls._construct_command(options)) - - return result + return cls.execute(cls._construct_command(options)) @classmethod def reports(cls, options=None): @@ -268,9 +266,7 @@ def start(cls, options=None): cls.command_sub = 'start' - result = cls.execute(cls._construct_command(options)) - - return result + return cls.execute(cls._construct_command(options)) @classmethod def status(cls, options=None): @@ -290,9 +286,7 @@ def status(cls, options=None): cls.command_sub = 'status' - result = cls.execute(cls._construct_command(options)) - - return result + return cls.execute(cls._construct_command(options)) @classmethod def stop(cls, options=None): @@ -313,9 +307,7 @@ def stop(cls, options=None): cls.command_sub = 'stop' - result = cls.execute(cls._construct_command(options)) - - return result + return cls.execute(cls._construct_command(options)) @classmethod def subscription_register(cls, options=None): diff --git a/robottelo/cli/lifecycleenvironment.py b/robottelo/cli/lifecycleenvironment.py index 72435669b62..c22d2268746 100644 --- a/robottelo/cli/lifecycleenvironment.py +++ b/robottelo/cli/lifecycleenvironment.py @@ -29,9 +29,7 @@ class LifecycleEnvironment(Base): @classmethod def list(cls, options=None, per_page=False): - result = super().list(options, per_page=per_page) - - return result + return super().list(options, per_page=per_page) @classmethod def paths(cls, options=None): diff --git a/robottelo/cli/operatingsys.py b/robottelo/cli/operatingsys.py index 1a1a63bdebd..8bf4411d56d 100644 --- a/robottelo/cli/operatingsys.py +++ b/robottelo/cli/operatingsys.py @@ -45,9 +45,7 @@ def add_architecture(cls, options=None): cls.command_sub = 'add-architecture' - result = cls.execute(cls._construct_command(options)) - - return result + return cls.execute(cls._construct_command(options)) @classmethod def add_provisioning_template(cls, options=None): @@ -57,9 +55,7 @@ def add_provisioning_template(cls, options=None): cls.command_sub = 'add-provisioning-template' - result = cls.execute(cls._construct_command(options)) - - return result + return cls.execute(cls._construct_command(options)) @classmethod def add_ptable(cls, options=None): @@ -69,9 +65,7 @@ def add_ptable(cls, options=None): cls.command_sub = 'add-ptable' - result = cls.execute(cls._construct_command(options)) - - return result + return cls.execute(cls._construct_command(options)) @classmethod def remove_architecture(cls, options=None): @@ -81,9 +75,7 @@ def remove_architecture(cls, options=None): cls.command_sub = 'remove-architecture' - result = cls.execute(cls._construct_command(options)) - - return result + return cls.execute(cls._construct_command(options)) @classmethod def remove_provisioning_template(cls, options=None): @@ -93,9 +85,7 @@ def remove_provisioning_template(cls, options=None): cls.command_sub = 'remove-provisioning-template' - result = cls.execute(cls._construct_command(options)) - - return result + return cls.execute(cls._construct_command(options)) @classmethod def remove_ptable(cls, options=None): @@ -105,6 +95,4 @@ def remove_ptable(cls, options=None): cls.command_sub = 'remove-ptable ' - result = cls.execute(cls._construct_command(options)) - - return result + return cls.execute(cls._construct_command(options)) diff --git a/robottelo/cli/product.py b/robottelo/cli/product.py index efbf7ed09e1..7a7f03eeff4 100644 --- a/robottelo/cli/product.py +++ b/robottelo/cli/product.py @@ -39,9 +39,7 @@ def remove_sync_plan(cls, options=None): cls.command_sub = 'remove-sync-plan' - result = cls.execute(cls._construct_command(options)) - - return result + return cls.execute(cls._construct_command(options)) @classmethod def set_sync_plan(cls, options=None): @@ -51,9 +49,7 @@ def set_sync_plan(cls, options=None): cls.command_sub = 'set-sync-plan' - result = cls.execute(cls._construct_command(options)) - - return result + return cls.execute(cls._construct_command(options)) @classmethod def synchronize(cls, options=None): @@ -69,6 +65,4 @@ def update_proxy(cls, options=None): cls.command_sub = 'update-proxy' - result = cls.execute(cls._construct_command(options)) - - return result + return cls.execute(cls._construct_command(options)) diff --git a/robottelo/cli/srpm.py b/robottelo/cli/srpm.py index b0e71d8969d..62578bbaa69 100644 --- a/robottelo/cli/srpm.py +++ b/robottelo/cli/srpm.py @@ -25,15 +25,11 @@ def info(cls, options=None): """Show a SRPM Info""" cls.command_sub = 'info' - result = cls.execute(cls._construct_command(options), output_format='csv') - - return result + return cls.execute(cls._construct_command(options), output_format='csv') @classmethod def list(cls, options=None): """List SRPMs""" cls.command_sub = 'list' - result = cls.execute(cls._construct_command(options), output_format='csv') - - return result + return cls.execute(cls._construct_command(options), output_format='csv') diff --git a/robottelo/cli/template.py b/robottelo/cli/template.py index 4dbe709c33b..3729fc4bf76 100644 --- a/robottelo/cli/template.py +++ b/robottelo/cli/template.py @@ -46,18 +46,14 @@ def add_operatingsystem(cls, options=None): """Adds operating system, requires "id" and "operatingsystem-id".""" cls.command_sub = 'add-operatingsystem' - result = cls.execute(cls._construct_command(options), output_format='csv') - - return result + return cls.execute(cls._construct_command(options), output_format='csv') @classmethod def remove_operatingsystem(cls, options=None): """Remove operating system, requires "id" and "operatingsystem-id".""" cls.command_sub = 'remove-operatingsystem' - result = cls.execute(cls._construct_command(options), output_format='csv') - - return result + return cls.execute(cls._construct_command(options), output_format='csv') @classmethod def clone(cls, options=None): diff --git a/robottelo/cli/template_sync.py b/robottelo/cli/template_sync.py index 6da3b683653..72ee70fa00f 100644 --- a/robottelo/cli/template_sync.py +++ b/robottelo/cli/template_sync.py @@ -50,15 +50,11 @@ def exports(cls, options=None): """Export Satellite Templates to Git/Local Directory.""" cls.command_base = 'export-templates' - result = cls.execute(cls._construct_command(options)) - - return result + return cls.execute(cls._construct_command(options)) @classmethod def imports(cls, options=None): """Import Satellite Templates to Git/Local Directory.""" cls.command_base = 'import-templates' - result = cls.execute(cls._construct_command(options)) - - return result + return cls.execute(cls._construct_command(options)) diff --git a/robottelo/cli/webhook.py b/robottelo/cli/webhook.py index 379555630c5..c7b93f76d99 100644 --- a/robottelo/cli/webhook.py +++ b/robottelo/cli/webhook.py @@ -41,5 +41,4 @@ def create(cls, options=None): 'See See "hammer webhook create --help" for the list of supported methods' ) - result = super().create(options) - return result + return super().create(options) diff --git a/robottelo/config/__init__.py b/robottelo/config/__init__.py index 2d66d00054b..cca2258c7e6 100644 --- a/robottelo/config/__init__.py +++ b/robottelo/config/__init__.py @@ -183,7 +183,7 @@ def configure_airgun(): 'webdriver': settings.ui.webdriver, 'webdriver_binary': settings.ui.webdriver_binary, }, - 'webkaifuku': {'config': settings.ui.webkaifuku} or {}, + 'webkaifuku': {'config': settings.ui.webkaifuku}, } ) diff --git a/robottelo/host_helpers/api_factory.py b/robottelo/host_helpers/api_factory.py index 0fff2579352..da6a39ecd76 100644 --- a/robottelo/host_helpers/api_factory.py +++ b/robottelo/host_helpers/api_factory.py @@ -50,6 +50,7 @@ def make_http_proxy(self, org, http_proxy_type): password=settings.http_proxy.password, organization=[org.id], ).create() + return None def cv_publish_promote(self, name=None, env_name=None, repo_id=None, org_id=None): """Create, publish and promote CV to selected environment""" @@ -606,10 +607,9 @@ def update_provisioning_template(self, name=None, old=None, new=None): self.temp.template = self.temp.template.replace(old, new, 1) update = self.temp.update(['template']) return new in update.template - elif new in self.temp.template: + if new in self.temp.template: return True - else: - raise ValueError(f'{old} does not exists in template {name}') + raise ValueError(f'{old} does not exists in template {name}') def disable_syncplan(self, sync_plan): """ @@ -681,10 +681,7 @@ def wait_for_errata_applicability_task( if ( task.label == 'Actions::Katello::Host::GenerateApplicability' and host_id in task.input['host_ids'] - ): - task.poll(poll_rate=poll_rate, timeout=poll_timeout) - tasks_finished += 1 - elif ( + ) or ( task.label == 'Actions::Katello::Host::UploadPackageProfile' and host_id == task.input['host']['id'] ): @@ -752,7 +749,7 @@ def wait_for_syncplan_tasks(self, repo_backend_id=None, timeout=10, repo_name=No if len(req.content) > 2: if req.json()[0].get('state') in ['finished']: return True - elif req.json()[0].get('error'): + if req.json()[0].get('error'): raise AssertionError( f"Pulp task with repo_id {repo_backend_id} error or not found: " f"'{req.json().get('error')}'" diff --git a/robottelo/host_helpers/capsule_mixins.py b/robottelo/host_helpers/capsule_mixins.py index 16fd7fa5712..89274435ec2 100644 --- a/robottelo/host_helpers/capsule_mixins.py +++ b/robottelo/host_helpers/capsule_mixins.py @@ -55,8 +55,7 @@ def wait_for_tasks( for task in tasks: task.poll(poll_rate=poll_rate, timeout=poll_timeout, must_succeed=must_succeed) break - else: - time.sleep(search_rate) + time.sleep(search_rate) else: raise AssertionError(f"No task was found using query '{search_query}'") return tasks @@ -94,5 +93,4 @@ def get_published_repo_url(self, org, prod, repo, lce=None, cv=None): """ if lce and cv: return f'{self.url}/pulp/content/{org}/{lce}/{cv}/custom/{prod}/{repo}/' - else: - return f'{self.url}/pulp/content/{org}/Library/custom/{prod}/{repo}/' + return f'{self.url}/pulp/content/{org}/Library/custom/{prod}/{repo}/' diff --git a/robottelo/host_helpers/cli_factory.py b/robottelo/host_helpers/cli_factory.py index 84ef66de1c2..7d3f9f1afcb 100644 --- a/robottelo/host_helpers/cli_factory.py +++ b/robottelo/host_helpers/cli_factory.py @@ -278,15 +278,13 @@ def __getattr__(self, name): # evaluate functions that provide default values fields = self._evaluate_functions(fields) return partial(create_object, entity_cls, fields) - else: - raise AttributeError(f'unknown factory method name: {name}') + raise AttributeError(f'unknown factory method name: {name}') def _evaluate_function(self, function): """Some functions may require an instance reference""" if 'self' in inspect.signature(function).parameters: return function(self) - else: - return function() + return function() def _evaluate_functions(self, iterable): """Run functions that are used to populate data in lists/dicts""" @@ -298,6 +296,7 @@ def _evaluate_functions(self, iterable): for key, item in iterable.items() if not key.startswith('_') } + return None @lru_cache def _find_entity_class(self, entity_name): @@ -305,6 +304,7 @@ def _find_entity_class(self, entity_name): for name, class_obj in self._satellite.cli.__dict__.items(): if entity_name == name.lower(): return class_obj + return None def make_content_credential(self, options=None): """Creates a content credential. @@ -532,7 +532,7 @@ def make_template(self, options=None): } # Write content to file or random text - if options is not None and 'content' in options.keys(): + if options is not None and 'content' in options: content = options.pop('content') else: content = gen_alphanumeric() @@ -880,32 +880,31 @@ def setup_org_for_a_rh_repo( custom_repo_url = settings.repos.capsule_repo if force_use_cdn or settings.robottelo.cdn or not custom_repo_url: return self._setup_org_for_a_rh_repo(options) - else: - options['url'] = custom_repo_url - result = self.setup_org_for_a_custom_repo(options) - if force_manifest_upload: - with clone() as manifest: - self._satellite.put(manifest.path, manifest.name) - try: - self._satellite.cli.Subscription.upload( - { - 'file': manifest.name, - 'organization-id': result.get('organization-id'), - } - ) - except CLIReturnCodeError as err: - raise CLIFactoryError(f'Failed to upload manifest\n{err.msg}') from err + options['url'] = custom_repo_url + result = self.setup_org_for_a_custom_repo(options) + if force_manifest_upload: + with clone() as manifest: + self._satellite.put(manifest.path, manifest.name) + try: + self._satellite.cli.Subscription.upload( + { + 'file': manifest.name, + 'organization-id': result.get('organization-id'), + } + ) + except CLIReturnCodeError as err: + raise CLIFactoryError(f'Failed to upload manifest\n{err.msg}') from err - # Add default subscription to activation-key, if SCA mode is disabled - if self._satellite.is_sca_mode_enabled(result['organization-id']) is False: - self.activationkey_add_subscription_to_repo( - { - 'activationkey-id': result['activationkey-id'], - 'organization-id': result['organization-id'], - 'subscription': constants.DEFAULT_SUBSCRIPTION_NAME, - } - ) - return result + # Add default subscription to activation-key, if SCA mode is disabled + if self._satellite.is_sca_mode_enabled(result['organization-id']) is False: + self.activationkey_add_subscription_to_repo( + { + 'activationkey-id': result['activationkey-id'], + 'organization-id': result['organization-id'], + 'subscription': constants.DEFAULT_SUBSCRIPTION_NAME, + } + ) + return result @staticmethod def _get_capsule_vm_distro_repos(distro): diff --git a/robottelo/host_helpers/repository_mixins.py b/robottelo/host_helpers/repository_mixins.py index f3cf54d3580..09a5fb1529d 100644 --- a/robottelo/host_helpers/repository_mixins.py +++ b/robottelo/host_helpers/repository_mixins.py @@ -388,8 +388,7 @@ def __repr__(self): f'' ) - else: - return f'' + return f'' def create( self, diff --git a/robottelo/host_helpers/satellite_mixins.py b/robottelo/host_helpers/satellite_mixins.py index 27c12c7b3be..3c26862c3bf 100644 --- a/robottelo/host_helpers/satellite_mixins.py +++ b/robottelo/host_helpers/satellite_mixins.py @@ -145,9 +145,10 @@ def upload_manifest(self, org_id, manifest=None, interface='API', timeout=None): :returns: the manifest upload result """ - if not isinstance(manifest, bytes | io.BytesIO): - if not hasattr(manifest, 'content') or manifest.content is None: - manifest = clone() + if not isinstance(manifest, bytes | io.BytesIO) and ( + not hasattr(manifest, 'content') or manifest.content is None + ): + manifest = clone() if timeout is None: # Set the timeout to 1500 seconds to align with the API timeout. timeout = 1500000 @@ -191,8 +192,7 @@ def publish_content_view(self, org, repo_list): repo = repo_list if isinstance(repo_list, list) else [repo_list] content_view = self.api.ContentView(organization=org, repository=repo).create() content_view.publish() - content_view = content_view.read() - return content_view + return content_view.read() def move_pulp_archive(self, org, export_message): """ @@ -208,11 +208,7 @@ def move_pulp_archive(self, org, export_message): # removes everything before export path, # replaces EXPORT_PATH by IMPORT_PATH, # removes metadata filename - import_path = os.path.dirname( - re.sub(rf'.*{PULP_EXPORT_DIR}', PULP_IMPORT_DIR, export_message) - ) - - return import_path + return os.path.dirname(re.sub(rf'.*{PULP_EXPORT_DIR}', PULP_IMPORT_DIR, export_message)) class SystemInfo: diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 953a0560aa8..12cc3dfd0e9 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -75,8 +75,7 @@ def lru_sat_ready_rhel(rhel_ver): 'promtail_config_template_file': 'config_sat.j2', 'workflow': settings.server.deploy_workflows.os, } - sat_ready_rhel = Broker(**deploy_args, host_class=Satellite).checkout() - return sat_ready_rhel + return Broker(**deploy_args, host_class=Satellite).checkout() def get_sat_version(): @@ -244,8 +243,8 @@ def nailgun_host(self): logger.error(f'Failed to get nailgun host for {self.hostname}: {err}') host = None return host - else: - logger.warning(f'Host {self.hostname} not registered to {self.satellite.hostname}') + logger.warning(f'Host {self.hostname} not registered to {self.satellite.hostname}') + return None @property def subscribed(self): @@ -514,6 +513,7 @@ def enable_repo(self, repo, force=False): downstream_repo = settings.repos.capsule_repo if force or settings.robottelo.cdn or not downstream_repo: return self.execute(f'subscription-manager repos --enable {repo}') + return None def subscription_manager_list_repos(self): return self.execute('subscription-manager repos --list') @@ -1601,6 +1601,7 @@ def check_services(self): for line in result.stdout.splitlines(): if error_msg in line: return line.replace(error_msg, '').strip() + return None def install(self, installer_obj=None, cmd_args=None, cmd_kwargs=None): """General purpose installer""" @@ -1787,7 +1788,7 @@ def _swap_nailgun(self, new_version): pip_main(['uninstall', '-y', 'nailgun']) pip_main(['install', f'https://github.com/SatelliteQE/nailgun/archive/{new_version}.zip']) self._api = type('api', (), {'_configured': False}) - to_clear = [k for k in sys.modules.keys() if 'nailgun' in k] + to_clear = [k for k in sys.modules if 'nailgun' in k] [sys.modules.pop(k) for k in to_clear] @property @@ -1877,6 +1878,7 @@ def get_caller(): for frame in inspect.stack(): if frame.function.startswith('test_'): return frame.function + return None try: ui_session = Session( diff --git a/robottelo/ssh.py b/robottelo/ssh.py index c82a7ebc071..8b72bed3497 100644 --- a/robottelo/ssh.py +++ b/robottelo/ssh.py @@ -16,13 +16,12 @@ def get_client( from robottelo.config import settings from robottelo.hosts import ContentHost - client = ContentHost( + return ContentHost( hostname=hostname or settings.server.hostname, username=username or settings.server.ssh_username, password=password or settings.server.ssh_password, port=port or settings.server.ssh_client.port, ) - return client def command( diff --git a/robottelo/utils/datafactory.py b/robottelo/utils/datafactory.py index 7de716e1046..c5ccc38ba14 100644 --- a/robottelo/utils/datafactory.py +++ b/robottelo/utils/datafactory.py @@ -64,11 +64,10 @@ def parametrized(data): 'ids': list(data.keys()), 'argvalues': list(data.values()), } - else: - return { - 'ids': [str(i) for i in range(len(data))], - 'argvalues': list(data), - } + return { + 'ids': [str(i) for i in range(len(data))], + 'argvalues': list(data), + } @filtered_datapoint @@ -196,14 +195,13 @@ def valid_domain_names(interface=None, length=None): length = random.randint(1, max_len) if length > max_len: raise ValueError(f'length is too large, max: {max_len}') - names = { + return { 'alphanumeric': DOMAIN % gen_string('alphanumeric', length), 'alpha': DOMAIN % gen_string('alpha', length), 'numeric': DOMAIN % gen_string('numeric', length), 'latin1': DOMAIN % gen_string('latin1', length), 'utf8': DOMAIN % gen_utf8(length), } - return names @filtered_datapoint @@ -243,8 +241,8 @@ def invalid_values_list(interface=None): raise InvalidArgumentError('Valid interface values are api, cli, ui only') if interface == 'ui': return ['', ' '] + invalid_names_list() - else: # interface = api or cli or None - return ['', ' ', '\t'] + invalid_names_list() + # else: interface = api or cli or None + return ['', ' ', '\t'] + invalid_names_list() @filtered_datapoint @@ -274,7 +272,7 @@ def valid_data_list(interface=None): @filtered_datapoint def valid_docker_repository_names(): """Generates a list of valid names for Docker repository.""" - names = [ + return [ gen_string('alphanumeric', random.randint(1, 255)), gen_string('alpha', random.randint(1, 255)), gen_string('cjk', random.randint(1, 85)), @@ -283,7 +281,6 @@ def valid_docker_repository_names(): gen_string('utf8', random.randint(1, 85)), gen_string('html', random.randint(1, 85)), ] - return names @filtered_datapoint @@ -512,8 +509,7 @@ def valid_http_credentials(url_encoded=False): } for cred in credentials ] - else: - return credentials + return credentials def invalid_http_credentials(url_encoded=False): @@ -535,8 +531,7 @@ def invalid_http_credentials(url_encoded=False): } for cred in credentials ] - else: - return credentials + return credentials @filtered_datapoint diff --git a/robottelo/utils/decorators/func_locker.py b/robottelo/utils/decorators/func_locker.py index 4d82c3add28..08f4073c614 100644 --- a/robottelo/utils/decorators/func_locker.py +++ b/robottelo/utils/decorators/func_locker.py @@ -112,10 +112,7 @@ def _get_scope_path(scope, scope_kwargs=None, scope_context=None, create=True): scope_path_list = [_get_temp_lock_function_dir(create=create)] if scope: - if callable(scope): - scope_dir_name = scope(**scope_kwargs) - else: - scope_dir_name = scope + scope_dir_name = scope(**scope_kwargs) if callable(scope) else scope if scope_dir_name: scope_path_list.append(scope_dir_name) if scope_context: @@ -168,8 +165,8 @@ def _check_deadlock(lock_file_path, process_id): """ if os.path.exists(lock_file_path): try: - lock_file_handler = open(lock_file_path) - lock_file_content = lock_file_handler.read() + with open(lock_file_path) as lock_file_handler: + lock_file_content = lock_file_handler.read() except OSError as exp: # do nothing, but anyway log the exception logger.exception(exp) @@ -265,8 +262,7 @@ def wait_function(func): if function: return main_wrapper(function) - else: - return wait_function + return wait_function @contextmanager diff --git a/robottelo/utils/decorators/func_shared/shared.py b/robottelo/utils/decorators/func_shared/shared.py index 072ebc3fb47..73961df50b5 100644 --- a/robottelo/utils/decorators/func_shared/shared.py +++ b/robottelo/utils/decorators/func_shared/shared.py @@ -565,5 +565,4 @@ def wait_function(func): if function_: return main_wrapper(function_) - else: - return wait_function + return wait_function diff --git a/robottelo/utils/io/__init__.py b/robottelo/utils/io/__init__.py index 33a4381fd4c..ac23b5e83b5 100644 --- a/robottelo/utils/io/__init__.py +++ b/robottelo/utils/io/__init__.py @@ -82,6 +82,7 @@ def get_remote_report_checksum(satellite, org_id): continue checksum, _ = result.stdout.split(maxsplit=1) return checksum + return None def get_report_data(report_path): diff --git a/robottelo/utils/issue_handlers/__init__.py b/robottelo/utils/issue_handlers/__init__.py index 4789bbc8a74..d59c97aec63 100644 --- a/robottelo/utils/issue_handlers/__init__.py +++ b/robottelo/utils/issue_handlers/__init__.py @@ -2,7 +2,7 @@ from robottelo.utils.issue_handlers import bugzilla handler_methods = {'BZ': bugzilla.is_open_bz} -SUPPORTED_HANDLERS = tuple(f"{handler}:" for handler in handler_methods.keys()) +SUPPORTED_HANDLERS = tuple(f"{handler}:" for handler in handler_methods) def add_workaround(data, matches, usage, validation=(lambda *a, **k: True), **kwargs): @@ -17,10 +17,11 @@ def should_deselect(issue, data=None): """Check if test should be deselected based on marked issue.""" # Handlers can be extended to support different issue trackers. handlers = {'BZ': bugzilla.should_deselect_bz} - supported_handlers = tuple(f"{handler}:" for handler in handlers.keys()) + supported_handlers = tuple(f"{handler}:" for handler in handlers) if str(issue).startswith(supported_handlers): handler_code = str(issue).partition(":")[0] return handlers[handler_code.strip()](issue.strip(), data) + return None def is_open(issue, data=None): diff --git a/robottelo/utils/ohsnap.py b/robottelo/utils/ohsnap.py index 89eb6a97e22..96241a759b6 100644 --- a/robottelo/utils/ohsnap.py +++ b/robottelo/utils/ohsnap.py @@ -121,7 +121,8 @@ def ohsnap_snap_rpms(ohsnap, sat_version, snap_version, os_major, is_all=True): rpm_repos = [f'satellite {sat_xy}', f'maintenance {sat_xy}'] if res.status_code == 200: for repo_data in res.json(): - if repo_data['rhel'] == os_major: - if any(repo in repo_data['repository'].lower() for repo in rpm_repos): - rpms += repo_data['rpms'] + if repo_data['rhel'] == os_major and any( + repo in repo_data['repository'].lower() for repo in rpm_repos + ): + rpms += repo_data['rpms'] return rpms diff --git a/robottelo/utils/report_portal/portal.py b/robottelo/utils/report_portal/portal.py index 3d44ac6c691..c7e0df5d54e 100644 --- a/robottelo/utils/report_portal/portal.py +++ b/robottelo/utils/report_portal/portal.py @@ -101,10 +101,9 @@ def get_launches( resp.raise_for_status() # this should further filter out unfinished launches as RP API currently doesn't # support usage of the same filter type multiple times (filter.ne.status) - launches = [ + return [ launch for launch in resp.json()['content'] if launch['status'] not in ['INTERRUPTED'] ] - return launches @retry( stop=stop_after_attempt(6), diff --git a/robottelo/utils/ssh.py b/robottelo/utils/ssh.py index c82a7ebc071..8b72bed3497 100644 --- a/robottelo/utils/ssh.py +++ b/robottelo/utils/ssh.py @@ -16,13 +16,12 @@ def get_client( from robottelo.config import settings from robottelo.hosts import ContentHost - client = ContentHost( + return ContentHost( hostname=hostname or settings.server.hostname, username=username or settings.server.ssh_username, password=password or settings.server.ssh_password, port=port or settings.server.ssh_client.port, ) - return client def command( diff --git a/robottelo/utils/vault.py b/robottelo/utils/vault.py index d447331ac15..97f95755bbd 100644 --- a/robottelo/utils/vault.py +++ b/robottelo/utils/vault.py @@ -10,7 +10,6 @@ class Vault: - HELP_TEXT = ( "Vault CLI in not installed in your system, " "refer link https://learn.hashicorp.com/tutorials/vault/getting-started-install to " @@ -41,12 +40,15 @@ def export_vault_addr(self): os.environ['VAULT_ADDR'] = vaulturl # Dynaconf Vault Env Vars - if self.vault_enabled and self.vault_enabled in ['True', 'true']: - if 'localhost:8200' in vaulturl: - raise InvalidVaultURLForOIDC( - f"{vaulturl} doesn't support OIDC login," - "please change url to corp vault in env file!" - ) + if ( + self.vault_enabled + and self.vault_enabled in ['True', 'true'] + and 'localhost:8200' in vaulturl + ): + raise InvalidVaultURLForOIDC( + f"{vaulturl} doesn't support OIDC login," + "please change url to corp vault in env file!" + ) def exec_vault_command(self, command: str, **kwargs): """A wrapper to execute the vault CLI commands @@ -74,31 +76,29 @@ def login(self, **kwargs): self.vault_enabled and self.vault_enabled in ['True', 'true'] and 'VAULT_SECRET_ID_FOR_DYNACONF' not in os.environ + and self.status(**kwargs).returncode != 0 ): - if self.status(**kwargs).returncode != 0: - logger.info( - "Warning! The browser is about to open for vault OIDC login, " - "close the tab once the sign-in is done!" - ) - if ( - self.exec_vault_command(command="vault login -method=oidc", **kwargs).returncode - == 0 - ): - self.exec_vault_command(command="vault token renew -i 10h", **kwargs) - logger.info("Success! Vault OIDC Logged-In and extended for 10 hours!") - # Fetching tokens - token = self.exec_vault_command("vault token lookup --format json").stdout - token = json.loads(str(token.decode('UTF-8')))['data']['id'] - # Setting new token in env file - _envdata = re.sub( - '.*VAULT_TOKEN_FOR_DYNACONF=.*', - f"VAULT_TOKEN_FOR_DYNACONF={token}", - self.envdata, - ) - self.env_path.write_text(_envdata) - logger.info( - "Success! New OIDC token added to .env file to access secrets from vault!" - ) + logger.info( + "Warning! The browser is about to open for vault OIDC login, " + "close the tab once the sign-in is done!" + ) + if ( + self.exec_vault_command(command="vault login -method=oidc", **kwargs).returncode + == 0 + ): + self.exec_vault_command(command="vault token renew -i 10h", **kwargs) + logger.info("Success! Vault OIDC Logged-In and extended for 10 hours!") + # Fetching tokens + token = self.exec_vault_command("vault token lookup --format json").stdout + token = json.loads(str(token.decode('UTF-8')))['data']['id'] + # Setting new token in env file + _envdata = re.sub( + '.*VAULT_TOKEN_FOR_DYNACONF=.*', + f"VAULT_TOKEN_FOR_DYNACONF={token}", + self.envdata, + ) + self.env_path.write_text(_envdata) + logger.info("Success! New OIDC token added to .env file to access secrets from vault!") def logout(self): # Teardown - Setting dymmy token in env file diff --git a/robottelo/utils/virtwho.py b/robottelo/utils/virtwho.py index 5e0f3a4657a..e159a487394 100644 --- a/robottelo/utils/virtwho.py +++ b/robottelo/utils/virtwho.py @@ -47,16 +47,15 @@ def get_system(system_type): 'password': getattr(settings.virtwho, system_type).guest_password, 'port': getattr(settings.virtwho, system_type).guest_port, } - elif system_type == 'satellite': + if system_type == 'satellite': return { 'hostname': settings.server.hostname, 'username': settings.server.ssh_username, 'password': settings.server.ssh_password, } - else: - raise VirtWhoError( - f'"{system_type}" system type is not supported. Please use one of {system_type_list}' - ) + raise VirtWhoError( + f'"{system_type}" system type is not supported. Please use one of {system_type_list}' + ) def get_guest_info(hypervisor_type): @@ -115,8 +114,7 @@ def register_system(system, activation_key=None, org='Default_Organization', env ret, stdout = runcmd(cmd, system) if ret == 0 or "system has been registered" in stdout: return True - else: - raise VirtWhoError(f'Failed to register system: {system}') + raise VirtWhoError(f'Failed to register system: {system}') def virtwho_cleanup(): @@ -150,10 +148,9 @@ def get_virtwho_status(): return 'logerror' if any(key in stdout for key in running_stauts): return 'running' - elif any(key in stdout for key in stopped_status): + if any(key in stdout for key in stopped_status): return 'stopped' - else: - return 'undefined' + return 'undefined' def get_configure_id(name): @@ -164,8 +161,7 @@ def get_configure_id(name): config = VirtWhoConfig.info({'name': name}) if 'id' in config['general-information']: return config['general-information']['id'] - else: - raise VirtWhoError(f"No configure id found for {name}") + raise VirtWhoError(f"No configure id found for {name}") def get_configure_command(config_id, org=DEFAULT_ORG): @@ -198,10 +194,8 @@ def get_configure_option(option, filename): cmd = f"grep -v '^#' {filename} | grep ^{option}" ret, stdout = runcmd(cmd) if ret == 0 and option in stdout: - value = stdout.split('=')[1].strip() - return value - else: - raise VirtWhoError(f"option {option} is not exist or not be enabled in {filename}") + return stdout.split('=')[1].strip() + raise VirtWhoError(f"option {option} is not exist or not be enabled in {filename}") def get_rhsm_log(): @@ -264,8 +258,7 @@ def _get_hypervisor_mapping(hypervisor_type): break if hypervisor_name: return hypervisor_name, guest_name - else: - raise VirtWhoError(f"Failed to get the hypervisor_name for guest {guest_name}") + raise VirtWhoError(f"Failed to get the hypervisor_name for guest {guest_name}") def get_hypervisor_ahv_mapping(hypervisor_type): @@ -348,6 +341,7 @@ def deploy_configure_by_command(command, hypervisor_type, debug=False, org='Defa raise VirtWhoError(f"Failed to deploy configure by {command}") if debug: return deploy_validation(hypervisor_type) + return None def deploy_configure_by_script( @@ -371,6 +365,7 @@ def deploy_configure_by_script( raise VirtWhoError(f"Failed to deploy configure by {script_filename}") if debug: return deploy_validation(hypervisor_type) + return None def deploy_configure_by_command_check(command): @@ -389,8 +384,7 @@ def deploy_configure_by_command_check(command): else: if ret != 0 or 'Finished successfully' not in stdout: raise VirtWhoError(f"Failed to deploy configure by {command}") - else: - return 'Finished successfully' + return 'Finished successfully' def restart_virtwho_service(): @@ -469,8 +463,7 @@ def hypervisor_json_create(hypervisors, guests): name = str(uuid.uuid4()) hypervisor = {"guestIds": guest_list, "name": name, "hypervisorId": {"hypervisorId": name}} hypervisors_list.append(hypervisor) - mapping = {"hypervisors": hypervisors_list} - return mapping + return {"hypervisors": hypervisors_list} def create_fake_hypervisor_content(org_label, hypervisors, guests): @@ -537,7 +530,8 @@ def get_configure_command_option(deploy_type, args, org=DEFAULT_ORG): username, password = Base._get_username_password() if deploy_type == 'location-id': return f"hammer -u {username} -p {password} virt-who-config deploy --id {args['id']} --location-id '{args['location-id']}' " - elif deploy_type == 'organization-title': + if deploy_type == 'organization-title': return f"hammer -u {username} -p {password} virt-who-config deploy --id {args['id']} --organization-title '{args['organization-title']}' " - elif deploy_type == 'name': + if deploy_type == 'name': return f"hammer -u {username} -p {password} virt-who-config deploy --name {args['name']} --organization '{org}' " + return None diff --git a/scripts/config_helpers.py b/scripts/config_helpers.py index feb37c9bd62..cf422588b05 100644 --- a/scripts/config_helpers.py +++ b/scripts/config_helpers.py @@ -35,7 +35,7 @@ def merge_nested_dictionaries(original, new, overwrite=False): user_choice = click.prompt(choice_prompt, type=int, default=1, show_default=True) if user_choice == 1: continue - elif user_choice == 2: + if user_choice == 2: original[key] = value elif user_choice == 3 and isinstance(value, list): original[key] = original[key] + value @@ -110,7 +110,7 @@ def merge(from_, strategy): user_choice = click.prompt(choice_prompt, type=int, default=1, show_default=True) if user_choice == 1: continue - elif user_choice == 2: + if user_choice == 2: logger.warning(f"Overwriting {real_name} with {file}") real_name.unlink() real_name.write_text(file.read_text()) diff --git a/scripts/customer_scenarios.py b/scripts/customer_scenarios.py index eb806468e21..b81137042b9 100755 --- a/scripts/customer_scenarios.py +++ b/scripts/customer_scenarios.py @@ -19,8 +19,7 @@ def make_path_list(path_list): paths = path_list.split(',') paths = [path for path in paths if any(target in path for target in targets)] return set(paths) - else: - return targets + return targets def get_bz_data(paths): @@ -31,12 +30,10 @@ def get_bz_data(paths): for test in tests: test_dict = test.to_dict() test_data = {**test_dict['tokens'], **test_dict['invalid-tokens']} - if 'bz' in test_data.keys(): - if ( - 'customerscenario' not in test_data.keys() - or test_data['customerscenario'] == 'false' - ): - path_result.append([test.name, test_data['bz']]) + if 'bz' in test_data and ( + 'customerscenario' not in test_data or test_data['customerscenario'] == 'false' + ): + path_result.append([test.name, test_data['bz']]) if path_result: result[path] = path_result return result @@ -64,8 +61,7 @@ def get_response(bzs): ) assert response.status_code == 200, 'BZ query unsuccessful' assert response.json().get('error') is not True, response.json().get('message') - bugs = response.json().get('bugs') - return bugs + return response.json().get('bugs') def query_bz(data): @@ -75,7 +71,7 @@ def query_bz(data): for test in tests: bugs = get_response(test[1]) for bug in bugs: - if 'external_bugs' in bug.keys() and len(bug['external_bugs']) > 1: + if 'external_bugs' in bug and len(bug['external_bugs']) > 1: customer_cases = [ case for case in bug['external_bugs'] diff --git a/scripts/fixture_cli.py b/scripts/fixture_cli.py index 5963394e21f..c68559d3c1b 100644 --- a/scripts/fixture_cli.py +++ b/scripts/fixture_cli.py @@ -16,13 +16,13 @@ def fixture_to_test(fixture_name): """ if ":" not in fixture_name: return f"def test_runfake_{fixture_name}({fixture_name}):\n assert True" - else: - fixture_name, params = fixture_name.split(":") - params = params.split(",") - return ( - f"@pytest.mark.parametrize('{fixture_name}', {params}, indirect=True)\n" - f"def test_runfake_{fixture_name}({fixture_name}):\n assert True" - ) + + fixture_name, params = fixture_name.split(":") + params = params.split(",") + return ( + f"@pytest.mark.parametrize('{fixture_name}', {params}, indirect=True)\n" + f"def test_runfake_{fixture_name}({fixture_name}):\n assert True" + ) @click.command() diff --git a/scripts/graph_entities.py b/scripts/graph_entities.py index a404c616911..46038a3e3b0 100755 --- a/scripts/graph_entities.py +++ b/scripts/graph_entities.py @@ -24,9 +24,7 @@ def graph(): for entity_name, entity in entities_.items(): # Graph out which entities this entity depends on. for field_name, field in entity.get_fields().items(): - if isinstance(field, entity_mixins.OneToOneField) or isinstance( - field, entity_mixins.OneToManyField - ): + if isinstance(field, (entity_mixins.OneToOneField | entity_mixins.OneToManyField)): print( '{} -> {} [label="{}"{}]'.format( entity_name, diff --git a/tests/foreman/api/test_activationkey.py b/tests/foreman/api/test_activationkey.py index 56513c03d85..ef5add381fc 100644 --- a/tests/foreman/api/test_activationkey.py +++ b/tests/foreman/api/test_activationkey.py @@ -171,7 +171,7 @@ def test_positive_update_limited_host(max_host, target_sat): for key, value in want.items(): setattr(act_key, key, value) act_key = act_key.update(want.keys()) - actual = {attr: getattr(act_key, attr) for attr in want.keys()} + actual = {attr: getattr(act_key, attr) for attr in want} assert want == actual @@ -219,7 +219,7 @@ def test_negative_update_limit(max_host, target_sat): with pytest.raises(HTTPError): act_key.update(want.keys()) act_key = act_key.read() - actual = {attr: getattr(act_key, attr) for attr in want.keys()} + actual = {attr: getattr(act_key, attr) for attr in want} assert want == actual @@ -293,7 +293,7 @@ def test_positive_get_releases_content(target_sat): """ act_key = target_sat.api.ActivationKey().create() response = client.get(act_key.path('releases'), auth=get_credentials(), verify=False).json() - assert 'results' in response.keys() + assert 'results' in response assert isinstance(response['results'], list) diff --git a/tests/foreman/api/test_computeresource_azurerm.py b/tests/foreman/api/test_computeresource_azurerm.py index 66f25f74b2b..47bf8313066 100644 --- a/tests/foreman/api/test_computeresource_azurerm.py +++ b/tests/foreman/api/test_computeresource_azurerm.py @@ -580,4 +580,4 @@ def test_positive_azurerm_custom_image_host_provisioned( # Azure cloud assert self.hostname.lower() == azureclient_host.name - assert AZURERM_VM_SIZE_DEFAULT == azureclient_host.type + assert azureclient_host.type == AZURERM_VM_SIZE_DEFAULT diff --git a/tests/foreman/api/test_contentview.py b/tests/foreman/api/test_contentview.py index ee6cfafdd00..03c95999417 100644 --- a/tests/foreman/api/test_contentview.py +++ b/tests/foreman/api/test_contentview.py @@ -96,8 +96,7 @@ def apply_package_filter(content_view, repo, package, target_sat, inclusion=True assert cv_filter.id == cv_filter_rule.content_view_filter.id content_view.publish() content_view = content_view.read() - content_view_version_info = content_view.version[0].read() - return content_view_version_info + return content_view.version[0].read() class TestContentView: diff --git a/tests/foreman/api/test_errata.py b/tests/foreman/api/test_errata.py index df24f7c66d2..cb831425778 100644 --- a/tests/foreman/api/test_errata.py +++ b/tests/foreman/api/test_errata.py @@ -34,10 +34,9 @@ @pytest.fixture(scope='module') def activation_key(module_org, module_lce, module_target_sat): - activation_key = module_target_sat.api.ActivationKey( + return module_target_sat.api.ActivationKey( environment=module_lce, organization=module_org ).create() - return activation_key @pytest.fixture(scope='module') @@ -626,6 +625,7 @@ def _run_remote_command_on_content_host(module_org, command, vm, return_result=F assert result.status == 0 if return_result: return result.stdout + return None def _set_prerequisites_for_swid_repos(module_org, vm): diff --git a/tests/foreman/api/test_filter.py b/tests/foreman/api/test_filter.py index efcf26e7b49..b676a67c6da 100644 --- a/tests/foreman/api/test_filter.py +++ b/tests/foreman/api/test_filter.py @@ -23,10 +23,9 @@ @pytest.fixture(scope='module') def module_perms(module_target_sat): """Search for provisioning template permissions. Set ``cls.ct_perms``.""" - ct_perms = module_target_sat.api.Permission().search( + return module_target_sat.api.Permission().search( query={'search': 'resource_type="ProvisioningTemplate"'} ) - return ct_perms @pytest.mark.tier1 diff --git a/tests/foreman/api/test_hostcollection.py b/tests/foreman/api/test_hostcollection.py index aea239ac1ad..985f989718e 100644 --- a/tests/foreman/api/test_hostcollection.py +++ b/tests/foreman/api/test_hostcollection.py @@ -28,8 +28,7 @@ @pytest.fixture(scope='module') def fake_hosts(module_org, module_target_sat): """Create content hosts that can be shared by tests.""" - hosts = [module_target_sat.api.Host(organization=module_org).create() for _ in range(2)] - return hosts + return [module_target_sat.api.Host(organization=module_org).create() for _ in range(2)] @pytest.mark.parametrize('name', **parametrized(valid_data_list())) diff --git a/tests/foreman/api/test_multiple_paths.py b/tests/foreman/api/test_multiple_paths.py index 30531f77025..8c683ca309a 100644 --- a/tests/foreman/api/test_multiple_paths.py +++ b/tests/foreman/api/test_multiple_paths.py @@ -130,7 +130,7 @@ def test_positive_get_status_code(self, entity_cls): logger.info('test_get_status_code arg: %s', entity_cls) response = client.get(entity_cls().path(), auth=get_credentials(), verify=False) response.raise_for_status() - assert http.client.OK == response.status_code + assert response.status_code == http.client.OK assert 'application/json' in response.headers['content-type'] @pytest.mark.tier1 @@ -151,7 +151,7 @@ def test_negative_get_unauthorized(self, entity_cls): """ logger.info('test_get_unauthorized arg: %s', entity_cls) response = client.get(entity_cls().path(), auth=(), verify=False) - assert http.client.UNAUTHORIZED == response.status_code + assert response.status_code == http.client.UNAUTHORIZED @pytest.mark.tier3 @pytest.mark.parametrize( @@ -173,7 +173,7 @@ def test_positive_post_status_code(self, entity_cls): :BZ: 1118015 """ response = entity_cls().create_raw() - assert http.client.CREATED == response.status_code + assert response.status_code == http.client.CREATED assert 'application/json' in response.headers['content-type'] @pytest.mark.tier1 @@ -195,7 +195,7 @@ def test_negative_post_unauthorized(self, entity_cls): """ server_cfg = user_nailgun_config() return_code = entity_cls(server_cfg).create_raw(create_missing=False).status_code - assert http.client.UNAUTHORIZED == return_code + assert return_code == http.client.UNAUTHORIZED class TestEntityId: @@ -217,7 +217,7 @@ def test_positive_get_status_code(self, entity_cls): """ entity = entity_cls(id=entity_cls().create_json()['id']) response = entity.read_raw() - assert http.client.OK == response.status_code + assert response.status_code == http.client.OK assert 'application/json' in response.headers['content-type'] @pytest.mark.tier1 @@ -248,7 +248,7 @@ def test_positive_put_status_code(self, entity_cls): auth=get_credentials(), verify=False, ) - assert http.client.OK == response.status_code + assert response.status_code == http.client.OK assert 'application/json' in response.headers['content-type'] @pytest.mark.tier1 @@ -325,7 +325,7 @@ def test_positive_put_and_get_requests(self, entity_cls): payload = _get_readable_attributes(new_entity) entity_attrs = entity_cls(id=entity['id']).read_json() for key, value in payload.items(): - assert key in entity_attrs.keys() + assert key in entity_attrs assert value == entity_attrs[key] @pytest.mark.tier1 @@ -349,7 +349,7 @@ def test_positive_post_and_get_requests(self, entity_cls): payload = _get_readable_attributes(entity) entity_attrs = entity_cls(id=entity_id).read_json() for key, value in payload.items(): - assert key in entity_attrs.keys() + assert key in entity_attrs assert value == entity_attrs[key] @pytest.mark.tier1 @@ -369,7 +369,7 @@ def test_positive_delete_and_get_requests(self, entity_cls): # Create an entity, delete it and get it. entity = entity_cls(id=entity_cls().create_json()['id']) entity.delete() - assert http.client.NOT_FOUND == entity.read_raw().status_code + assert entity.read_raw().status_code == http.client.NOT_FOUND class TestEntityRead: diff --git a/tests/foreman/api/test_organization.py b/tests/foreman/api/test_organization.py index 89988935f98..7edc09d0dc2 100644 --- a/tests/foreman/api/test_organization.py +++ b/tests/foreman/api/test_organization.py @@ -77,7 +77,7 @@ def test_positive_create(self, target_sat): if is_open('BZ:2228820'): assert response.status_code in [http.client.UNSUPPORTED_MEDIA_TYPE, 500] else: - assert http.client.UNSUPPORTED_MEDIA_TYPE == response.status_code + assert response.status_code == http.client.UNSUPPORTED_MEDIA_TYPE @pytest.mark.tier1 @pytest.mark.build_sanity diff --git a/tests/foreman/api/test_provisioningtemplate.py b/tests/foreman/api/test_provisioningtemplate.py index 2ef9810d7b3..b154898498d 100644 --- a/tests/foreman/api/test_provisioningtemplate.py +++ b/tests/foreman/api/test_provisioningtemplate.py @@ -120,7 +120,7 @@ def tftpboot(module_org, module_target_sat): for setting in default_settings: if setting.value is None: setting.value = '' - setting.update(fields=['value'] or '') + setting.update(fields=['value']) class TestProvisioningTemplate: diff --git a/tests/foreman/api/test_registration.py b/tests/foreman/api/test_registration.py index c982b02ab90..ae50d84364c 100644 --- a/tests/foreman/api/test_registration.py +++ b/tests/foreman/api/test_registration.py @@ -62,7 +62,7 @@ def test_host_registration_end_to_end( # Verify server.hostname and server.port from subscription-manager config assert module_target_sat.hostname == rhel_contenthost.subscription_config['server']['hostname'] - assert constants.CLIENT_PORT == rhel_contenthost.subscription_config['server']['port'] + assert rhel_contenthost.subscription_config['server']['port'] == constants.CLIENT_PORT # Update module_capsule_configured to include module_org/module_location nc = module_capsule_configured.nailgun_smart_proxy @@ -86,7 +86,7 @@ def test_host_registration_end_to_end( module_capsule_configured.hostname == rhel_contenthost.subscription_config['server']['hostname'] ) - assert constants.CLIENT_PORT == rhel_contenthost.subscription_config['server']['port'] + assert rhel_contenthost.subscription_config['server']['port'] == constants.CLIENT_PORT @pytest.mark.tier3 diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index 07058f6f331..4d64c3866fc 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -488,7 +488,7 @@ def test_negative_update_to_invalid_download_policy(self, repo, target_sat): **datafactory.parametrized( [ {'content_type': content_type, 'download_policy': 'on_demand'} - for content_type in constants.REPO_TYPE.keys() + for content_type in constants.REPO_TYPE if content_type != 'yum' ] ), @@ -2337,7 +2337,7 @@ def test_positive_upload_file_to_file_repo(self, repo, target_sat): filesearch = target_sat.api.File().search( query={"search": f"name={constants.RPM_TO_UPLOAD}"} ) - assert constants.RPM_TO_UPLOAD == filesearch[0].name + assert filesearch[0].name == constants.RPM_TO_UPLOAD @pytest.mark.tier1 @pytest.mark.upgrade diff --git a/tests/foreman/api/test_role.py b/tests/foreman/api/test_role.py index 1ce1014f269..7d5a12bc591 100644 --- a/tests/foreman/api/test_role.py +++ b/tests/foreman/api/test_role.py @@ -72,7 +72,7 @@ def create_org_admin_role(self, target_sat, name=None, orgs=None, locs=None): :return dict: This function returns dict representation of cloned role data returned from 'clone' function """ - name = gen_string('alpha') if not name else name + name = name if name else gen_string('alpha') default_org_admin = target_sat.api.Role().search( query={'search': 'name="Organization admin"'} ) diff --git a/tests/foreman/api/test_subscription.py b/tests/foreman/api/test_subscription.py index 316ef4e7448..282dac528b0 100644 --- a/tests/foreman/api/test_subscription.py +++ b/tests/foreman/api/test_subscription.py @@ -54,7 +54,7 @@ def custom_repo(rh_repo, module_sca_manifest_org, module_target_sat): @pytest.fixture(scope='module') def module_ak(module_sca_manifest_org, rh_repo, custom_repo, module_target_sat): """rh_repo and custom_repo are included here to ensure their execution before the AK""" - module_ak = module_target_sat.api.ActivationKey( + return module_target_sat.api.ActivationKey( content_view=module_sca_manifest_org.default_content_view, max_hosts=100, organization=module_sca_manifest_org, @@ -63,7 +63,6 @@ def module_ak(module_sca_manifest_org, rh_repo, custom_repo, module_target_sat): ), auto_attach=True, ).create() - return module_ak @pytest.mark.tier1 diff --git a/tests/foreman/api/test_template_combination.py b/tests/foreman/api/test_template_combination.py index a10125e0a54..7e9d5f11be8 100644 --- a/tests/foreman/api/test_template_combination.py +++ b/tests/foreman/api/test_template_combination.py @@ -49,10 +49,10 @@ def test_positive_end_to_end_template_combination(request, module_target_sat, mo assert module_hostgroup.id == combination.hostgroup.id # DELETE - assert 1 == len(template.read().template_combinations) + assert len(template.read().template_combinations) == 1 combination.delete() with pytest.raises(HTTPError): combination.read() - assert 0 == len(template.read().template_combinations) + assert len(template.read().template_combinations) == 0 template.delete() module_hostgroup.delete() diff --git a/tests/foreman/api/test_templatesync.py b/tests/foreman/api/test_templatesync.py index d91c8ad378d..bda401ef373 100644 --- a/tests/foreman/api/test_templatesync.py +++ b/tests/foreman/api/test_templatesync.py @@ -693,7 +693,7 @@ def test_positive_import_json_output_name_key( template = target_sat.api.Template().imports( data={'repo': dir_path, 'organization_ids': [module_org.id]} ) - assert 'name' in template['message']['templates'][0].keys() + assert 'name' in template['message']['templates'][0] assert template_name == template['message']['templates'][0]['name'] @pytest.mark.tier2 @@ -748,7 +748,7 @@ def test_positive_import_json_output_file_key( template = module_target_sat.api.Template().imports( data={'repo': dir_path, 'organization_ids': [module_org.id]} ) - assert 'example_template.erb' == template['message']['templates'][0]['file'] + assert template['message']['templates'][0]['file'] == 'example_template.erb' @pytest.mark.tier2 def test_positive_import_json_output_corrupted_metadata( @@ -780,7 +780,7 @@ def test_positive_import_json_output_corrupted_metadata( ) assert not bool(template['message']['templates'][0]['imported']) assert ( - 'Failed to parse metadata' == template['message']['templates'][0]['additional_errors'] + template['message']['templates'][0]['additional_errors'] == 'Failed to parse metadata' ) @pytest.mark.skip_if_open('BZ:1787355') @@ -816,8 +816,8 @@ def test_positive_import_json_output_filtered_skip_message( ) assert not bool(template['message']['templates'][0]['imported']) assert ( - "Skipping, 'name' filtered out based on 'filter' and 'negate' settings" - == template['message']['templates'][0]['additional_info'] + template['message']['templates'][0]['additional_info'] + == "Skipping, 'name' filtered out based on 'filter' and 'negate' settings" ) @pytest.mark.tier2 @@ -850,8 +850,8 @@ def test_positive_import_json_output_no_name_error( ) assert not bool(template['message']['templates'][0]['imported']) assert ( - "No 'name' found in metadata" - == template['message']['templates'][0]['additional_errors'] + template['message']['templates'][0]['additional_errors'] + == "No 'name' found in metadata" ) @pytest.mark.tier2 @@ -884,8 +884,8 @@ def test_positive_import_json_output_no_model_error( ) assert not bool(template['message']['templates'][0]['imported']) assert ( - "No 'model' found in metadata" - == template['message']['templates'][0]['additional_errors'] + template['message']['templates'][0]['additional_errors'] + == "No 'model' found in metadata" ) @pytest.mark.tier2 @@ -918,8 +918,8 @@ def test_positive_import_json_output_blank_model_error( ) assert not bool(template['message']['templates'][0]['imported']) assert ( - "Template type was not found, are you missing a plugin?" - == template['message']['templates'][0]['additional_errors'] + template['message']['templates'][0]['additional_errors'] + == "Template type was not found, are you missing a plugin?" ) @pytest.mark.tier2 @@ -965,7 +965,7 @@ def test_positive_export_json_output( template['exported'] for template in exported_templates['message']['templates'] ].count(True) assert exported_count == 17 - assert 'name' in exported_templates['message']['templates'][0].keys() + assert 'name' in exported_templates['message']['templates'][0] assert ( target_sat.execute( f'[ -d {dir_path}/job_templates ] && ' diff --git a/tests/foreman/cli/test_activationkey.py b/tests/foreman/cli/test_activationkey.py index 7a0d5f57dd4..cbcfaea5700 100644 --- a/tests/foreman/cli/test_activationkey.py +++ b/tests/foreman/cli/test_activationkey.py @@ -257,9 +257,8 @@ def test_negative_create_with_usage_limit_with_not_integers(module_org, limit, m module_target_sat.cli_factory.make_activation_key( {'organization-id': module_org.id, 'max-hosts': limit} ) - if isinstance(limit, int): - if limit < 1: - assert 'Max hosts cannot be less than one' in str(raise_ctx) + if isinstance(limit, int) and limit < 1: + assert 'Max hosts cannot be less than one' in str(raise_ctx) if isinstance(limit, str): assert 'Numeric value is required.' in str(raise_ctx) @@ -1404,7 +1403,7 @@ def test_positive_update_autoattach(module_org, module_target_sat): result = module_target_sat.cli.ActivationKey.update( {'auto-attach': new_value, 'id': new_ak['id'], 'organization-id': module_org.id} ) - assert 'Activation key updated.' == result[0]['message'] + assert result[0]['message'] == 'Activation key updated.' @pytest.mark.tier2 diff --git a/tests/foreman/cli/test_computeresource_azurerm.py b/tests/foreman/cli/test_computeresource_azurerm.py index 1621b48d12f..1756a0ed674 100644 --- a/tests/foreman/cli/test_computeresource_azurerm.py +++ b/tests/foreman/cli/test_computeresource_azurerm.py @@ -37,7 +37,7 @@ def azurerm_hostgroup( ): """Sets Hostgroup for AzureRm Host Provisioning""" - hgroup = sat_azure.api.HostGroup( + return sat_azure.api.HostGroup( architecture=sat_azure_default_architecture, compute_resource=module_azurerm_cr, domain=sat_azure_domain, @@ -46,7 +46,6 @@ def azurerm_hostgroup( operatingsystem=sat_azure_default_os, organization=[sat_azure_org], ).create() - return hgroup class TestAzureRMComputeResourceTestCase: @@ -344,29 +343,30 @@ def class_host_ft( Provisions the host on AzureRM using Finish template Later in tests this host will be used to perform assertions """ - with sat_azure.hammer_api_timeout(): - with sat_azure.skip_yum_update_during_provisioning(template='Kickstart default finish'): - host = sat_azure.cli.Host.create( - { - 'name': self.hostname, - 'compute-resource': module_azurerm_cr.name, - 'compute-attributes': self.compute_attrs, - 'interface': self.interfaces_attributes, - 'location-id': sat_azure_loc.id, - 'organization-id': sat_azure_org.id, - 'domain-id': sat_azure_domain.id, - 'domain': sat_azure_domain.name, - 'architecture-id': sat_azure_default_architecture.id, - 'operatingsystem-id': sat_azure_default_os.id, - 'root-password': gen_string('alpha'), - 'image': module_azurerm_custom_finishimg.name, - }, - timeout=1800000, - ) - yield host - with sat_azure.api_factory.satellite_setting('destroy_vm_on_host_delete=True'): - if sat_azure.cli.Host.exists(search=('name', host['name'])): - sat_azure.cli.Host.delete({'name': self.fullhostname}, timeout=1800000) + with sat_azure.hammer_api_timeout(), sat_azure.skip_yum_update_during_provisioning( + template='Kickstart default finish' + ): + host = sat_azure.cli.Host.create( + { + 'name': self.hostname, + 'compute-resource': module_azurerm_cr.name, + 'compute-attributes': self.compute_attrs, + 'interface': self.interfaces_attributes, + 'location-id': sat_azure_loc.id, + 'organization-id': sat_azure_org.id, + 'domain-id': sat_azure_domain.id, + 'domain': sat_azure_domain.name, + 'architecture-id': sat_azure_default_architecture.id, + 'operatingsystem-id': sat_azure_default_os.id, + 'root-password': gen_string('alpha'), + 'image': module_azurerm_custom_finishimg.name, + }, + timeout=1800000, + ) + yield host + with sat_azure.api_factory.satellite_setting('destroy_vm_on_host_delete=True'): + if sat_azure.cli.Host.exists(search=('name', host['name'])): + sat_azure.cli.Host.delete({'name': self.fullhostname}, timeout=1800000) @pytest.fixture(scope='class') def azureclient_host(self, azurermclient, class_host_ft): @@ -472,27 +472,26 @@ def class_host_ud( Provisions the host on AzureRM using UserData template Later in tests this host will be used to perform assertions """ - with sat_azure.hammer_api_timeout(): - with sat_azure.skip_yum_update_during_provisioning( - template='Kickstart default user data' - ): - host = sat_azure.cli.Host.create( - { - 'name': self.hostname, - 'compute-attributes': self.compute_attrs, - 'interface': self.interfaces_attributes, - 'image': module_azurerm_cloudimg.name, - 'hostgroup': azurerm_hostgroup.name, - 'location': sat_azure_loc.name, - 'organization': sat_azure_org.name, - 'operatingsystem-id': sat_azure_default_os.id, - }, - timeout=1800000, - ) - yield host - with sat_azure.api_factory.satellite_setting('destroy_vm_on_host_delete=True'): - if sat_azure.cli.Host.exists(search=('name', host['name'])): - sat_azure.cli.Host.delete({'name': self.fullhostname}, timeout=1800000) + with sat_azure.hammer_api_timeout(), sat_azure.skip_yum_update_during_provisioning( + template='Kickstart default user data' + ): + host = sat_azure.cli.Host.create( + { + 'name': self.hostname, + 'compute-attributes': self.compute_attrs, + 'interface': self.interfaces_attributes, + 'image': module_azurerm_cloudimg.name, + 'hostgroup': azurerm_hostgroup.name, + 'location': sat_azure_loc.name, + 'organization': sat_azure_org.name, + 'operatingsystem-id': sat_azure_default_os.id, + }, + timeout=1800000, + ) + yield host + with sat_azure.api_factory.satellite_setting('destroy_vm_on_host_delete=True'): + if sat_azure.cli.Host.exists(search=('name', host['name'])): + sat_azure.cli.Host.delete({'name': self.fullhostname}, timeout=1800000) @pytest.fixture(scope='class') def azureclient_host(self, azurermclient, class_host_ud): diff --git a/tests/foreman/cli/test_computeresource_libvirt.py b/tests/foreman/cli/test_computeresource_libvirt.py index a53959f277b..2a4720750a0 100644 --- a/tests/foreman/cli/test_computeresource_libvirt.py +++ b/tests/foreman/cli/test_computeresource_libvirt.py @@ -364,7 +364,7 @@ def test_negative_update(libvirt_url, options, module_target_sat): # check attributes have not changed assert result['name'] == comp_res['name'] options.pop('new-name', None) - for key in options.keys(): + for key in options: assert comp_res[key] == result[key] diff --git a/tests/foreman/cli/test_contentview.py b/tests/foreman/cli/test_contentview.py index 0b4ee4041ef..9b261f57218 100644 --- a/tests/foreman/cli/test_contentview.py +++ b/tests/foreman/cli/test_contentview.py @@ -233,7 +233,7 @@ def test_positive_update_filter(self, repo_setup, module_target_sat): } ) cvf = module_target_sat.cli.ContentView.filter.info({'id': cvf['filter-id']}) - assert 'security' == cvf['rules'][0]['types'] + assert cvf['rules'][0]['types'] == 'security' @pytest.mark.tier1 def test_positive_delete_by_id(self, module_org, module_target_sat): diff --git a/tests/foreman/cli/test_docker.py b/tests/foreman/cli/test_docker.py index a4dd51c17c2..10b1d1dd4fc 100644 --- a/tests/foreman/cli/test_docker.py +++ b/tests/foreman/cli/test_docker.py @@ -114,7 +114,7 @@ def test_positive_read_docker_tags(self, repo, module_target_sat): manifests_list = module_target_sat.cli.Docker.manifest.list({'repository-id': repo['id']}) # Some manifests do not have tags associated with it, ignore those # because we want to check the tag information - manifests = [m_iter for m_iter in manifests_list if not m_iter['tags'] == ''] + manifests = [m_iter for m_iter in manifests_list if m_iter['tags'] != ''] assert manifests tags_list = module_target_sat.cli.Docker.tag.list({'repository-id': repo['id']}) # Extract tag names for the repository out of docker tag list diff --git a/tests/foreman/cli/test_domain.py b/tests/foreman/cli/test_domain.py index 8b1c157c657..32f81369592 100644 --- a/tests/foreman/cli/test_domain.py +++ b/tests/foreman/cli/test_domain.py @@ -42,8 +42,7 @@ def valid_create_params(): @filtered_datapoint def invalid_create_params(): """Returns a list of invalid domain create parameters""" - params = [{'name': gen_string(str_type='utf8', length=256)}] - return params + return [{'name': gen_string(str_type='utf8', length=256)}] @filtered_datapoint @@ -66,8 +65,7 @@ def valid_update_params(): @filtered_datapoint def invalid_update_params(): """Returns a list of invalid domain update parameters""" - params = [{'name': ''}, {'name': gen_string(str_type='utf8', length=256)}] - return params + return [{'name': ''}, {'name': gen_string(str_type='utf8', length=256)}] @filtered_datapoint @@ -218,7 +216,7 @@ def test_negative_update(module_domain, options, module_target_sat): module_target_sat.cli.Domain.update(dict(options, id=module_domain.id)) # check - domain not updated result = module_target_sat.cli.Domain.info({'id': module_domain.id}) - for key in options.keys(): + for key in options: assert result[key] == getattr(module_domain, key) diff --git a/tests/foreman/cli/test_errata.py b/tests/foreman/cli/test_errata.py index 6cdb4a13cdf..b32faed5020 100644 --- a/tests/foreman/cli/test_errata.py +++ b/tests/foreman/cli/test_errata.py @@ -360,7 +360,7 @@ def filter_sort_errata(sat, org, sort_by_date='issued', filter_by_org=None): elif filter_by_org == 'label': list_param['organization-label'] = org.label - sort_reversed = True if sort_order == 'DESC' else False + sort_reversed = sort_order == 'DESC' errata_list = sat.cli.Erratum.list(list_param) assert len(errata_list) > 0 diff --git a/tests/foreman/cli/test_filter.py b/tests/foreman/cli/test_filter.py index 9c12b239089..4108ce07d6b 100644 --- a/tests/foreman/cli/test_filter.py +++ b/tests/foreman/cli/test_filter.py @@ -19,13 +19,12 @@ @pytest.fixture(scope='module') def module_perms(module_target_sat): """Search for provisioning template permissions. Set ``cls.ct_perms``.""" - perms = [ + return [ permission['name'] for permission in module_target_sat.cli.Filter.available_permissions( {"search": "resource_type=User"} ) ] - return perms @pytest.fixture diff --git a/tests/foreman/cli/test_host.py b/tests/foreman/cli/test_host.py index f80f1c1b68a..37864e0f763 100644 --- a/tests/foreman/cli/test_host.py +++ b/tests/foreman/cli/test_host.py @@ -232,7 +232,7 @@ def parse_cli_entity_list_help_message(help_message): name = name[:-1] # remove colon from name if 'Usage' in name: continue - elif 'Options' in name: + if 'Options' in name: # used together with previous_line when line (message) is appended to previous line options = parse_two_columns(content, options_start_with_dash=True) elif 'field sets' in name: @@ -1061,18 +1061,18 @@ def test_positive_parameter_crud(function_host, target_sat): {'host-id': function_host['id'], 'name': name, 'value': value} ) host = target_sat.cli.Host.info({'id': function_host['id']}) - assert name in host['parameters'].keys() + assert name in host['parameters'] assert value == host['parameters'][name] new_value = valid_data_list()[name] target_sat.cli.Host.set_parameter({'host-id': host['id'], 'name': name, 'value': new_value}) host = target_sat.cli.Host.info({'id': host['id']}) - assert name in host['parameters'].keys() + assert name in host['parameters'] assert new_value == host['parameters'][name] target_sat.cli.Host.delete_parameter({'host-id': host['id'], 'name': name}) host = target_sat.cli.Host.info({'id': host['id']}) - assert name not in host['parameters'].keys() + assert name not in host['parameters'] # -------------------------- HOST PARAMETER SCENARIOS ------------------------- @@ -1098,7 +1098,7 @@ def test_negative_add_parameter(function_host, target_sat): } ) host = target_sat.cli.Host.info({'id': function_host['id']}) - assert name not in host['parameters'].keys() + assert name not in host['parameters'] @pytest.mark.cli_host_parameter diff --git a/tests/foreman/cli/test_leapp_client.py b/tests/foreman/cli/test_leapp_client.py index 6ab248b0f92..4ea36bbf37a 100644 --- a/tests/foreman/cli/test_leapp_client.py +++ b/tests/foreman/cli/test_leapp_client.py @@ -91,8 +91,7 @@ def function_leapp_cv(module_target_sat, module_sca_manifest_org, leapp_repos, m function_leapp_cv.publish() cvv = function_leapp_cv.read().version[0] cvv.promote(data={'environment_ids': module_leapp_lce.id, 'force': True}) - function_leapp_cv = function_leapp_cv.read() - return function_leapp_cv + return function_leapp_cv.read() @pytest.fixture @@ -121,7 +120,7 @@ def leapp_repos( source = upgrade_path['source_version'] target = upgrade_path['target_version'] all_repos = [] - for rh_repo_key in RHEL_REPOS.keys(): + for rh_repo_key in RHEL_REPOS: release_version = RHEL_REPOS[rh_repo_key]['releasever'] if release_version in str(source) or release_version in target: prod = 'rhel' if 'rhel7' in rh_repo_key else rh_repo_key.split('_')[0] diff --git a/tests/foreman/cli/test_registration.py b/tests/foreman/cli/test_registration.py index f4260cb1181..89d560ab862 100644 --- a/tests/foreman/cli/test_registration.py +++ b/tests/foreman/cli/test_registration.py @@ -53,7 +53,7 @@ def test_host_registration_end_to_end( # Verify server.hostname and server.port from subscription-manager config assert module_target_sat.hostname == rhel_contenthost.subscription_config['server']['hostname'] - assert CLIENT_PORT == rhel_contenthost.subscription_config['server']['port'] + assert rhel_contenthost.subscription_config['server']['port'] == CLIENT_PORT # Update module_capsule_configured to include module_org/module_location module_target_sat.cli.Capsule.update( @@ -78,7 +78,7 @@ def test_host_registration_end_to_end( module_capsule_configured.hostname == rhel_contenthost.subscription_config['server']['hostname'] ) - assert CLIENT_PORT == rhel_contenthost.subscription_config['server']['port'] + assert rhel_contenthost.subscription_config['server']['port'] == CLIENT_PORT def test_upgrade_katello_ca_consumer_rpm( @@ -115,7 +115,7 @@ def test_upgrade_katello_ca_consumer_rpm( f'rpm -Uvh "http://{target_sat.hostname}/pub/{consumer_cert_name}-1.0-1.noarch.rpm"' ) # Check server URL is not Red Hat CDN's "subscription.rhsm.redhat.com" - assert 'subscription.rhsm.redhat.com' != vm.subscription_config['server']['hostname'] + assert vm.subscription_config['server']['hostname'] != 'subscription.rhsm.redhat.com' assert target_sat.hostname == vm.subscription_config['server']['hostname'] # Get consumer cert source file @@ -136,7 +136,7 @@ def test_upgrade_katello_ca_consumer_rpm( # Install new rpmbuild/RPMS/noarch/katello-ca-consumer-*-2.noarch.rpm assert vm.execute(f'yum install -y rpmbuild/RPMS/noarch/{new_consumer_cert_rpm}') # Check server URL is not Red Hat CDN's "subscription.rhsm.redhat.com" - assert 'subscription.rhsm.redhat.com' != vm.subscription_config['server']['hostname'] + assert vm.subscription_config['server']['hostname'] != 'subscription.rhsm.redhat.com' assert target_sat.hostname == vm.subscription_config['server']['hostname'] # Register as final check diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index fc812d9e649..0fbffea78c7 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -31,7 +31,7 @@ @pytest.fixture def fixture_sca_vmsetup(request, module_sca_manifest_org, target_sat): """Create VM and register content host to Simple Content Access organization""" - if '_count' in request.param.keys(): + if '_count' in request.param: with Broker( nick=request.param['nick'], host_class=ContentHost, diff --git a/tests/foreman/cli/test_reporttemplates.py b/tests/foreman/cli/test_reporttemplates.py index d3b12f75161..a27123201f1 100644 --- a/tests/foreman/cli/test_reporttemplates.py +++ b/tests/foreman/cli/test_reporttemplates.py @@ -706,7 +706,7 @@ def test_positive_generate_ansible_template(module_target_sat): :CaseImportance: Medium """ settings = module_target_sat.cli.Settings.list({'search': 'name=ansible_inventory_template'}) - assert 1 == len(settings) + assert len(settings) == 1 template_name = settings[0]['value'] report_list = module_target_sat.cli.ReportTemplate.list() diff --git a/tests/foreman/cli/test_repository.py b/tests/foreman/cli/test_repository.py index 35269a110dd..466ffbb8319 100644 --- a/tests/foreman/cli/test_repository.py +++ b/tests/foreman/cli/test_repository.py @@ -652,7 +652,7 @@ def test_negative_update_to_invalid_download_policy(self, repo_options, repo, ta **parametrized( [ {'content-type': content_type, 'download-policy': 'on_demand'} - for content_type in REPO_TYPE.keys() + for content_type in REPO_TYPE if content_type != 'yum' if content_type != 'ostree' ] @@ -2663,7 +2663,7 @@ def test_positive_upload_file_to_file_repo(self, repo_options, repo, target_sat) filesearch = entities.File().search( query={"search": f"name={RPM_TO_UPLOAD} and repository={repo['name']}"} ) - assert RPM_TO_UPLOAD == filesearch[0].name + assert filesearch[0].name == RPM_TO_UPLOAD @pytest.mark.tier1 @pytest.mark.upgrade diff --git a/tests/foreman/cli/test_role.py b/tests/foreman/cli/test_role.py index 5eb4e8569f8..7ccdafdf4f1 100644 --- a/tests/foreman/cli/test_role.py +++ b/tests/foreman/cli/test_role.py @@ -296,7 +296,7 @@ def test_system_admin_role_end_to_end(self, target_sat): ).set({'name': "outofsync_interval", 'value': "32"}) sync_time = target_sat.cli.Settings.list({'search': 'name=outofsync_interval'})[0] # Asserts if the setting was updated successfully - assert '32' == sync_time['value'] + assert sync_time['value'] == '32' # Create another System Admin user using the first one system_admin = target_sat.cli.User.with_user( diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index c81585c698a..5e793916136 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -85,8 +85,7 @@ def export_import_cleanup_module(target_sat, module_org): @pytest.fixture def function_import_org(target_sat): """Creates an Organization for content import.""" - org = target_sat.api.Organization().create() - return org + return target_sat.api.Organization().create() @pytest.fixture @@ -152,14 +151,13 @@ def function_synced_rh_repo(request, target_sat, function_sca_manifest_org): # Update the download policy to 'immediate' and sync target_sat.cli.Repository.update({'download-policy': 'immediate', 'id': repo['id']}) target_sat.cli.Repository.synchronize({'id': repo['id']}, timeout=7200000) - repo = target_sat.cli.Repository.info( + return target_sat.cli.Repository.info( { 'organization-id': function_sca_manifest_org.id, 'name': repo_dict['name'], 'product': repo_dict['product'], } ) - return repo @pytest.fixture diff --git a/tests/foreman/cli/test_subscription.py b/tests/foreman/cli/test_subscription.py index c6d053a4659..a7a74f4360c 100644 --- a/tests/foreman/cli/test_subscription.py +++ b/tests/foreman/cli/test_subscription.py @@ -28,7 +28,7 @@ def golden_ticket_host_setup(request, module_sca_manifest_org, module_target_sat ) new_repo = module_target_sat.cli_factory.make_repository({'product-id': new_product['id']}) module_target_sat.cli.Repository.synchronize({'id': new_repo['id']}) - new_ak = module_target_sat.cli_factory.make_activation_key( + return module_target_sat.cli_factory.make_activation_key( { 'lifecycle-environment': 'Library', 'content-view': 'Default Organization View', @@ -36,7 +36,6 @@ def golden_ticket_host_setup(request, module_sca_manifest_org, module_target_sat 'auto-attach': False, } ) - return new_ak @pytest.mark.tier1 @@ -166,7 +165,7 @@ def test_positive_subscription_list(function_entitlement_manifest_org, module_ta {'organization-id': function_entitlement_manifest_org.id}, per_page=False ) for column in ['start-date', 'end-date']: - assert column in subscription_list[0].keys() + assert column in subscription_list[0] @pytest.mark.tier2 diff --git a/tests/foreman/cli/test_templatesync.py b/tests/foreman/cli/test_templatesync.py index 9959ae18459..b0cc31b2003 100644 --- a/tests/foreman/cli/test_templatesync.py +++ b/tests/foreman/cli/test_templatesync.py @@ -44,7 +44,7 @@ def setUpClass(self, module_target_sat): """ # Check all Downloadable templates exists - if not requests.get(FOREMAN_TEMPLATE_IMPORT_URL).status_code == 200: + if requests.get(FOREMAN_TEMPLATE_IMPORT_URL).status_code != 200: pytest.fail('The foreman templates git url is not accessible') # Download the Test Template in test running folder diff --git a/tests/foreman/cli/test_user.py b/tests/foreman/cli/test_user.py index c549b9e667a..3646e301db0 100644 --- a/tests/foreman/cli/test_user.py +++ b/tests/foreman/cli/test_user.py @@ -56,8 +56,8 @@ def roles_helper(): for role_name in valid_usernames_list() + include_list: yield module_target_sat.cli_factory.make_role({'name': role_name}) - stubbed_roles = {role['id']: role for role in roles_helper()} - return stubbed_roles + # return stubbed roles + return {role['id']: role for role in roles_helper()} @pytest.mark.parametrize('email', **parametrized(valid_emails_list())) @pytest.mark.tier2 diff --git a/tests/foreman/cli/test_usergroup.py b/tests/foreman/cli/test_usergroup.py index 6ad115d96f6..1c757b4e766 100644 --- a/tests/foreman/cli/test_usergroup.py +++ b/tests/foreman/cli/test_usergroup.py @@ -22,8 +22,7 @@ @pytest.fixture def function_user_group(target_sat): """Create new usergroup per each test""" - user_group = target_sat.cli_factory.usergroup() - return user_group + return target_sat.cli_factory.usergroup() @pytest.mark.tier1 @@ -230,9 +229,9 @@ def test_negative_automate_bz1437578(ldap_auth_source, function_user_group, modu } ) assert ( - 'Could not create external user group: ' + result == 'Could not create external user group: ' 'Name is not found in the authentication source' 'Name Domain Users is a special group in AD.' ' Unfortunately, we cannot obtain membership information' - ' from a LDAP search and therefore sync it.' == result + ' from a LDAP search and therefore sync it.' ) diff --git a/tests/foreman/conftest.py b/tests/foreman/conftest.py index 59616f233b7..e867e118be3 100644 --- a/tests/foreman/conftest.py +++ b/tests/foreman/conftest.py @@ -66,7 +66,8 @@ def ui_session_record_property(request, record_property): test_file_path = request.node.fspath.strpath if any(directory in test_file_path for directory in test_directories): for fixture in request.node.fixturenames: - if request.fixturename != fixture: - if isinstance(request.getfixturevalue(fixture), Satellite): - sat = request.getfixturevalue(fixture) - sat.record_property = record_property + if request.fixturename != fixture and isinstance( + request.getfixturevalue(fixture), Satellite + ): + sat = request.getfixturevalue(fixture) + sat.record_property = record_property diff --git a/tests/foreman/destructive/test_capsule_loadbalancer.py b/tests/foreman/destructive/test_capsule_loadbalancer.py index 27b3c9ad69f..16b6450d254 100644 --- a/tests/foreman/destructive/test_capsule_loadbalancer.py +++ b/tests/foreman/destructive/test_capsule_loadbalancer.py @@ -273,7 +273,7 @@ def test_client_register_through_lb( loadbalancer_setup['setup_haproxy']['haproxy'].hostname in rhel_contenthost.subscription_config['server']['hostname'] ) - assert CLIENT_PORT == rhel_contenthost.subscription_config['server']['port'] + assert rhel_contenthost.subscription_config['server']['port'] == CLIENT_PORT # Host registration by Second Capsule through Loadbalancer result = rhel_contenthost.register( @@ -288,7 +288,7 @@ def test_client_register_through_lb( loadbalancer_setup['setup_haproxy']['haproxy'].hostname in rhel_contenthost.subscription_config['server']['hostname'] ) - assert CLIENT_PORT == rhel_contenthost.subscription_config['server']['port'] + assert rhel_contenthost.subscription_config['server']['port'] == CLIENT_PORT hosts = loadbalancer_setup['module_target_sat'].cli.Host.list( {'organization-id': loadbalancer_setup['module_org'].id} diff --git a/tests/foreman/destructive/test_discoveredhost.py b/tests/foreman/destructive/test_discoveredhost.py index bcf73a14ed7..8034957ec72 100644 --- a/tests/foreman/destructive/test_discoveredhost.py +++ b/tests/foreman/destructive/test_discoveredhost.py @@ -34,7 +34,7 @@ def _read_log(ch, pattern): def _wait_for_log(channel, pattern, timeout=5, delay=0.2): """_read_log method enclosed in wait_for method""" - matching_log = wait_for( + return wait_for( _read_log, func_args=( channel, @@ -45,7 +45,6 @@ def _wait_for_log(channel, pattern, timeout=5, delay=0.2): delay=delay, logger=logger, ) - return matching_log def _assert_discovered_host(host, channel=None, user_config=None, sat=None): diff --git a/tests/foreman/destructive/test_ldap_authentication.py b/tests/foreman/destructive/test_ldap_authentication.py index 304c608734a..58f3121dbec 100644 --- a/tests/foreman/destructive/test_ldap_authentication.py +++ b/tests/foreman/destructive/test_ldap_authentication.py @@ -198,9 +198,8 @@ def test_positive_create_with_https( assert ldap_source['ldap_server']['port'] == '636' with module_target_sat.ui_session( test_name, username, auth_data['ldap_user_passwd'] - ) as ldapsession: - with pytest.raises(NavigationTriesExceeded): - ldapsession.user.search('') + ) as ldapsession, pytest.raises(NavigationTriesExceeded): + ldapsession.user.search('') assert module_target_sat.api.User().search(query={'search': f'login="{username}"'}) diff --git a/tests/foreman/destructive/test_realm.py b/tests/foreman/destructive/test_realm.py index a4baa15e7bf..cbddcdc3bc8 100644 --- a/tests/foreman/destructive/test_realm.py +++ b/tests/foreman/destructive/test_realm.py @@ -87,7 +87,7 @@ def test_positive_realm_info_name( ) request.addfinalizer(lambda: module_target_sat.cli.Realm.delete({'id': realm['id']})) info = module_target_sat.cli.Realm.info({'name': realm['name']}) - for key in info.keys(): + for key in info: assert info[key] == realm[key] @@ -115,7 +115,7 @@ def test_positive_realm_info_id( ) request.addfinalizer(lambda: module_target_sat.cli.Realm.delete({'id': realm['id']})) info = module_target_sat.cli.Realm.info({'id': realm['id']}) - for key in info.keys(): + for key in info: assert info[key] == realm[key] assert info == module_target_sat.cli.Realm.info({'id': realm['id']}) diff --git a/tests/foreman/longrun/test_inc_updates.py b/tests/foreman/longrun/test_inc_updates.py index 652f1a20e56..8ffb84fb36c 100644 --- a/tests/foreman/longrun/test_inc_updates.py +++ b/tests/foreman/longrun/test_inc_updates.py @@ -54,10 +54,9 @@ def dev_lce(module_entitlement_manifest_org): @pytest.fixture(scope='module') def qe_lce(module_entitlement_manifest_org, dev_lce): - qe_lce = entities.LifecycleEnvironment( + return entities.LifecycleEnvironment( name='QE', prior=dev_lce, organization=module_entitlement_manifest_org ).create() - return qe_lce @pytest.fixture(scope='module') @@ -95,8 +94,7 @@ def module_cv(module_entitlement_manifest_org, sat_client_repo, custom_repo): repository=[sat_client_repo.id, custom_repo.id], ).create() module_cv.publish() - module_cv = module_cv.read() - return module_cv + return module_cv.read() @pytest.fixture(scope='module') diff --git a/tests/foreman/longrun/test_oscap.py b/tests/foreman/longrun/test_oscap.py index ece650e6de2..ecf4da2857c 100644 --- a/tests/foreman/longrun/test_oscap.py +++ b/tests/foreman/longrun/test_oscap.py @@ -70,10 +70,7 @@ def default_proxy(module_target_sat): @pytest.fixture(scope='module') def lifecycle_env(module_org): """Create lifecycle environment""" - lce_env = entities.LifecycleEnvironment( - organization=module_org, name=gen_string('alpha') - ).create() - return lce_env + return entities.LifecycleEnvironment(organization=module_org, name=gen_string('alpha')).create() @pytest.fixture(scope='module') diff --git a/tests/foreman/maintain/test_advanced.py b/tests/foreman/maintain/test_advanced.py index 1167e907102..6a66c9aefcf 100644 --- a/tests/foreman/maintain/test_advanced.py +++ b/tests/foreman/maintain/test_advanced.py @@ -28,13 +28,12 @@ def get_satellite_capsule_repos( os_major_ver = get_sat_rhel_version().major if product == 'capsule': product = 'satellite-capsule' - repos = [ + return [ f'{product}-{x_y_release}-for-rhel-{os_major_ver}-x86_64-rpms', f'satellite-maintenance-{x_y_release}-for-rhel-{os_major_ver}-x86_64-rpms', f'rhel-{os_major_ver}-for-x86_64-baseos-rpms', f'rhel-{os_major_ver}-for-x86_64-appstream-rpms', ] - return repos def test_positive_advanced_run_service_restart(sat_maintain): diff --git a/tests/foreman/ui/test_acs.py b/tests/foreman/ui/test_acs.py index a657d44719f..8874519a63b 100644 --- a/tests/foreman/ui/test_acs.py +++ b/tests/foreman/ui/test_acs.py @@ -239,7 +239,7 @@ def gen_params(): # It loops through the keys in the parameters dictionary, and uses the keys to create a scenario ID # and then it uses the scenario ID to access the scenario values from the parameters dictionary. # The code then adds the scenario values to the list of scenario values. - for acs in parameters_dict.keys(): + for acs in parameters_dict: if not acs.startswith('_'): for cnt in parameters_dict[acs]: if not cnt.startswith('_'): diff --git a/tests/foreman/ui/test_containerimagetag.py b/tests/foreman/ui/test_containerimagetag.py index 86fb97ebab1..b46cc15bc4e 100644 --- a/tests/foreman/ui/test_containerimagetag.py +++ b/tests/foreman/ui/test_containerimagetag.py @@ -71,5 +71,5 @@ def test_positive_search(session, module_org, module_product, module_repository) None, ) assert module_product.name == repo_line['Product'] - assert DEFAULT_CV == repo_line['Content View'] + assert repo_line['Content View'] == DEFAULT_CV assert 'Success' in repo_line['Last Sync'] diff --git a/tests/foreman/ui/test_discoveredhost.py b/tests/foreman/ui/test_discoveredhost.py index 92ea28d185a..64476188f89 100644 --- a/tests/foreman/ui/test_discoveredhost.py +++ b/tests/foreman/ui/test_discoveredhost.py @@ -40,8 +40,7 @@ def _is_host_reachable(host, retries=12, iteration_sleep=5, expect_reachable=Tru result = ssh.command(cmd.format(retries, host, operator, iteration_sleep)) if expect_reachable: return not result.status - else: - return bool(result.status) + return bool(result.status) @pytest.mark.tier3 diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index 326ef52246e..e3de3c10dd1 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -60,7 +60,7 @@ def ui_user(ui_user, smart_proxy_location, module_target_sat): @pytest.fixture def scap_policy(scap_content, target_sat): - scap_policy = target_sat.cli_factory.make_scap_policy( + return target_sat.cli_factory.make_scap_policy( { 'name': gen_string('alpha'), 'deploy-by': 'ansible', @@ -70,7 +70,6 @@ def scap_policy(scap_content, target_sat): 'weekday': OSCAP_WEEKDAY['friday'].lower(), } ) - return scap_policy @pytest.fixture(scope='module') @@ -1077,8 +1076,8 @@ def test_rex_new_ui(session, target_sat, rex_contenthost): task_status = target_sat.api.ForemanTask(id=task_result[0].id).poll() assert task_status['result'] == 'success' recent_jobs = session.host_new.get_details(hostname, "overview.recent_jobs")['overview'] - assert "Run ls" == recent_jobs['recent_jobs']['finished']['table'][0]['column0'] - assert "succeeded" == recent_jobs['recent_jobs']['finished']['table'][0]['column2'] + assert recent_jobs['recent_jobs']['finished']['table'][0]['column0'] == "Run ls" + assert recent_jobs['recent_jobs']['finished']['table'][0]['column2'] == "succeeded" @pytest.mark.tier4 @@ -1203,18 +1202,18 @@ def test_positive_update_delete_package( if not is_open('BZ:2132680'): product_name = module_repos_collection_with_setup.custom_product.name repos = session.host_new.get_repo_sets(client.hostname, product_name) - assert 'Enabled' == repos[0].status + assert repos[0].status == 'Enabled' session.host_new.override_repo_sets( client.hostname, product_name, "Override to disabled" ) - assert 'Disabled' == repos[0].status + assert repos[0].status == 'Disabled' session.host_new.install_package(client.hostname, FAKE_8_CUSTOM_PACKAGE_NAME) result = client.run(f'yum install -y {FAKE_7_CUSTOM_PACKAGE}') assert result.status != 0 session.host_new.override_repo_sets( client.hostname, product_name, "Override to enabled" ) - assert 'Enabled' == repos[0].status + assert repos[0].status == 'Enabled' # refresh repos on system client.run('subscription-manager repos') # install package @@ -1271,7 +1270,7 @@ def test_positive_update_delete_package( task_status = target_sat.api.ForemanTask(id=task_result[0].id).poll() assert task_status['result'] == 'success' packages = session.host_new.get_packages(client.hostname, FAKE_8_CUSTOM_PACKAGE_NAME) - assert 'table' not in packages.keys() + assert 'table' not in packages result = client.run(f'rpm -q {FAKE_8_CUSTOM_PACKAGE}') assert result.status != 0 @@ -1348,7 +1347,7 @@ def test_positive_apply_erratum( assert task_status['result'] == 'success' # verify values = session.host_new.get_details(client.hostname, widget_names='content.errata') - assert 'table' not in values['content']['errata'].keys() + assert 'table' not in values['content']['errata'] result = client.run( 'yum update --assumeno --security | grep "No packages needed for security"' ) diff --git a/tests/foreman/ui/test_hostcollection.py b/tests/foreman/ui/test_hostcollection.py index c2acfd7a26a..06af17c4a5a 100644 --- a/tests/foreman/ui/test_hostcollection.py +++ b/tests/foreman/ui/test_hostcollection.py @@ -95,10 +95,9 @@ def vm_host_collection(module_target_sat, module_org_with_parameter, vm_content_ module_target_sat.api.Host().search(query={'search': f'name={host.hostname}'})[0].id for host in vm_content_hosts ] - host_collection = module_target_sat.api.HostCollection( + return module_target_sat.api.HostCollection( host=host_ids, organization=module_org_with_parameter ).create() - return host_collection @pytest.fixture @@ -109,10 +108,9 @@ def vm_host_collection_module_stream( module_target_sat.api.Host().search(query={'search': f'name={host.hostname}'})[0].id for host in vm_content_hosts_module_stream ] - host_collection = module_target_sat.api.HostCollection( + return module_target_sat.api.HostCollection( host=host_ids, organization=module_org_with_parameter ).create() - return host_collection def _run_remote_command_on_content_hosts(command, vm_clients): @@ -138,7 +136,7 @@ def _is_package_installed( if result.status == 0 and expect_installed: installed += 1 break - elif result.status != 0 and not expect_installed: + if result.status != 0 and not expect_installed: installed -= 1 break if ind < retries - 1: @@ -148,8 +146,7 @@ def _is_package_installed( if expect_installed: return installed == len(vm_clients) - else: - return bool(installed) + return bool(installed) def _install_package_with_assertion(vm_clients, package_name): diff --git a/tests/foreman/ui/test_ldap_authentication.py b/tests/foreman/ui/test_ldap_authentication.py index 8c799a72dc8..6fcb71a7efa 100644 --- a/tests/foreman/ui/test_ldap_authentication.py +++ b/tests/foreman/ui/test_ldap_authentication.py @@ -406,9 +406,8 @@ def test_positive_delete_external_roles( ) with target_sat.ui_session( test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd'] - ) as ldapsession: - with pytest.raises(NavigationTriesExceeded): - ldapsession.location.create({'name': gen_string('alpha')}) + ) as ldapsession, pytest.raises(NavigationTriesExceeded): + ldapsession.location.create({'name': gen_string('alpha')}) @pytest.mark.parametrize('ldap_auth_source', ['AD', 'IPA'], indirect=True) @@ -677,7 +676,7 @@ def test_positive_add_katello_role_with_org( results = session.activationkey.search(ak_name) assert results[0]['Name'] == ak_name session.organization.select(different_org.name) - assert not session.activationkey.search(ak_name)[0]['Name'] == ak_name + assert session.activationkey.search(ak_name)[0]['Name'] != ak_name ak = ( target_sat.api.ActivationKey(organization=module_org) .search(query={'search': f'name={ak_name}'})[0] @@ -761,9 +760,8 @@ def test_positive_login_user_basic_roles( target_sat.api_factory.create_role_permissions(role, permissions) with target_sat.ui_session( test_name, ldap_data['ldap_user_name'], ldap_data['ldap_user_passwd'] - ) as ldapsession: - with pytest.raises(NavigationTriesExceeded): - ldapsession.usergroup.search('') + ) as ldapsession, pytest.raises(NavigationTriesExceeded): + ldapsession.usergroup.search('') with session: session.user.update(ldap_data['ldap_user_name'], {'roles.resources.assigned': [role.name]}) with target_sat.ui_session( @@ -796,9 +794,8 @@ def test_positive_login_user_password_otp( ) with target_sat.ui_session( test_name, default_ipa_host.ipa_otp_username, otp_pass - ) as ldapsession: - with pytest.raises(NavigationTriesExceeded): - ldapsession.user.search('') + ) as ldapsession, pytest.raises(NavigationTriesExceeded): + ldapsession.user.search('') users = target_sat.api.User().search( query={'search': f'login="{default_ipa_host.ipa_otp_username}"'} ) @@ -1215,12 +1212,11 @@ def test_userlist_with_external_admin( # verify the users count with local admin and remote/external admin with target_sat.ui_session( user=idm_admin, password=settings.server.ssh_password - ) as remote_admin_session: - with target_sat.ui_session( - user=settings.server.admin_username, password=settings.server.admin_password - ) as local_admin_session: - assert local_admin_session.user.search(idm_user)[0]['Username'] == idm_user - assert remote_admin_session.user.search(idm_user)[0]['Username'] == idm_user + ) as remote_admin_session, target_sat.ui_session( + user=settings.server.admin_username, password=settings.server.admin_password + ) as local_admin_session: + assert local_admin_session.user.search(idm_user)[0]['Username'] == idm_user + assert remote_admin_session.user.search(idm_user)[0]['Username'] == idm_user @pytest.mark.skip_if_open('BZ:1883209') diff --git a/tests/foreman/ui/test_organization.py b/tests/foreman/ui/test_organization.py index a3d6d50c47d..3aa913484fb 100644 --- a/tests/foreman/ui/test_organization.py +++ b/tests/foreman/ui/test_organization.py @@ -342,4 +342,4 @@ def test_positive_product_view_organization_switch(session, module_org, module_p with session: assert session.product.search(module_product.name) session.organization.select(org_name="Default Organization") - assert not session.product.search(module_product.name) == module_product.name + assert session.product.search(module_product.name) != module_product.name diff --git a/tests/foreman/ui/test_provisioningtemplate.py b/tests/foreman/ui/test_provisioningtemplate.py index bc88d687f88..da2b8446dce 100644 --- a/tests/foreman/ui/test_provisioningtemplate.py +++ b/tests/foreman/ui/test_provisioningtemplate.py @@ -200,7 +200,7 @@ def test_positive_verify_supported_templates_rhlogo(target_sat, module_org, modu with target_sat.ui_session() as session: session.organization.select(org_name=module_org.name) session.location.select(loc_name=module_location.name) - for template in random_templates.keys(): + for template in random_templates: assert ( session.provisioningtemplate.is_locked(template) == random_templates[template]['locked'] diff --git a/tests/foreman/ui/test_registration.py b/tests/foreman/ui/test_registration.py index cb1a79264c2..d8b979ba0c2 100644 --- a/tests/foreman/ui/test_registration.py +++ b/tests/foreman/ui/test_registration.py @@ -538,7 +538,7 @@ def test_positive_host_registration_with_non_admin_user( # Verify server.hostname and server.port from subscription-manager config assert target_sat.hostname == rhel8_contenthost.subscription_config['server']['hostname'] - assert constants.CLIENT_PORT == rhel8_contenthost.subscription_config['server']['port'] + assert rhel8_contenthost.subscription_config['server']['port'] == constants.CLIENT_PORT @pytest.mark.tier2 diff --git a/tests/foreman/ui/test_subscription.py b/tests/foreman/ui/test_subscription.py index 871adb538ca..348398a1de2 100644 --- a/tests/foreman/ui/test_subscription.py +++ b/tests/foreman/ui/test_subscription.py @@ -93,9 +93,8 @@ def test_positive_end_to_end(session, target_sat): ] org = target_sat.api.Organization().create() _, temporary_local_manifest_path = mkstemp(prefix='manifest-', suffix='.zip') - with clone() as manifest: - with open(temporary_local_manifest_path, 'wb') as file_handler: - file_handler.write(manifest.content.read()) + with clone() as manifest, open(temporary_local_manifest_path, 'wb') as file_handler: + file_handler.write(manifest.content.read()) with session: session.organization.select(org.name) # Ignore "Danger alert: Katello::Errors::UpstreamConsumerNotFound'" as server will connect diff --git a/tests/robottelo/conftest.py b/tests/robottelo/conftest.py index d8ffb1ad75b..dbd1fe41b50 100644 --- a/tests/robottelo/conftest.py +++ b/tests/robottelo/conftest.py @@ -1,3 +1,4 @@ +import contextlib import glob import os from pathlib import Path @@ -44,8 +45,6 @@ def exec_test(request, dummy_test): yield report_file for logfile in glob.glob('robottelo*.log'): os.remove(logfile) - try: - os.remove(report_file) - except OSError: + with contextlib.suppress(OSError): # the file might not exist if the test fails prematurely - pass + os.remove(report_file) diff --git a/tests/robottelo/test_cli.py b/tests/robottelo/test_cli.py index fcc583eaee0..debbc8c6e42 100644 --- a/tests/robottelo/test_cli.py +++ b/tests/robottelo/test_cli.py @@ -146,9 +146,9 @@ def assert_response_error(self, expected_error, stderr='some error'): def test_add_operating_system(self, construct, execute): """Check command_sub edited when executing add_operating_system""" options = {'foo': 'bar'} - assert 'add-operatingsystem' != Base.command_sub + assert Base.command_sub != 'add-operatingsystem' assert execute.return_value == Base.add_operating_system(options) - assert 'add-operatingsystem' == Base.command_sub + assert Base.command_sub == 'add-operatingsystem' construct.called_once_with(options) execute.called_once_with(construct.return_value) @@ -158,7 +158,7 @@ def test_add_create_with_empty_result(self, construct, execute): """Check command create when result is empty""" execute.return_value = [] assert execute.return_value == Base.create() - assert 'create' == Base.command_sub + assert Base.command_sub == 'create' construct.called_once_with({}) execute.called_once_with(construct.return_value, output_format='csv') @@ -169,7 +169,7 @@ def test_add_create_with_result_dct_without_id(self, construct, execute, info): """Check command create when result has dct but dct hasn't id key""" execute.return_value = [{'not_id': 'foo'}] assert execute.return_value == Base.create() - assert 'create' == Base.command_sub + assert Base.command_sub == 'create' construct.called_once_with({}) execute.called_once_with(construct.return_value, output_format='csv') assert not info.called @@ -184,7 +184,7 @@ def test_add_create_with_result_dct_with_id_not_required_org(self, construct, ex execute.return_value = [{'id': 'foo', 'bar': 'bas'}] Base.command_requires_org = False assert execute.return_value == Base.create() - assert 'create' == Base.command_sub + assert Base.command_sub == 'create' construct.called_once_with({}) execute.called_once_with(construct.return_value, output_format='csv') info.called_once_with({'id': 'foo'}) @@ -199,7 +199,7 @@ def test_add_create_with_result_dct_with_id_required_org(self, construct, execut execute.return_value = [{'id': 'foo', 'bar': 'bas'}] Base.command_requires_org = True assert execute.return_value == Base.create({'organization-id': 'org-id'}) - assert 'create' == Base.command_sub + assert Base.command_sub == 'create' construct.called_once_with({}) execute.called_once_with(construct.return_value, output_format='csv') info.called_once_with({'id': 'foo', 'organization-id': 'org-id'}) @@ -214,7 +214,7 @@ def test_add_create_with_result_dct_id_required_org_error(self, construct, execu Base.command_requires_org = True with pytest.raises(CLIError): Base.create() - assert 'create' == Base.command_sub + assert Base.command_sub == 'create' construct.called_once_with({}) execute.called_once_with(construct.return_value, output_format='csv') @@ -298,7 +298,7 @@ def test_exists_with_option_and_no_empty_return(self, lst_method): my_options = {'search': 'foo=bar'} response = Base.exists(my_options, search=['id', 1]) lst_method.assert_called_once_with(my_options) - assert 1 == response + assert response == 1 @mock.patch('robottelo.cli.base.Base.command_requires_org') def test_info_requires_organization_id(self, _): # noqa: PT019 - not a fixture @@ -347,7 +347,7 @@ def test_info_parsing_response(self, construct, execute, parse): def test_list_with_default_per_page(self, construct, execute): """Check list method set per_page as 1000 by default""" assert execute.return_value == Base.list(options={'organization-id': 1}) - assert 'list' == Base.command_sub + assert Base.command_sub == 'list' construct.called_once_with({'per-page': 1000}) execute.called_once_with(construct.return_value, output_format='csv') diff --git a/tests/robottelo/test_decorators.py b/tests/robottelo/test_decorators.py index 0f8ac441ce9..6c5e60f568a 100644 --- a/tests/robottelo/test_decorators.py +++ b/tests/robottelo/test_decorators.py @@ -28,12 +28,12 @@ def test_create_and_not_add_to_cache(self, make_foo): """ make_foo(cached=False) assert 'foo' not in decorators.OBJECT_CACHE - assert decorators.OBJECT_CACHE == {} + assert {} == decorators.OBJECT_CACHE def test_build_cache(self, make_foo): """Create a new object and add it to the cache.""" obj = make_foo(cached=True) - assert decorators.OBJECT_CACHE == {'foo': {'id': 42}} + assert {'foo': {'id': 42}} == decorators.OBJECT_CACHE assert id(decorators.OBJECT_CACHE['foo']) == id(obj) def test_return_from_cache(self, make_foo): diff --git a/tests/robottelo/test_dependencies.py b/tests/robottelo/test_dependencies.py index 2a6b0a9fb81..950cbeebec4 100644 --- a/tests/robottelo/test_dependencies.py +++ b/tests/robottelo/test_dependencies.py @@ -1,4 +1,5 @@ """Test important behavior in robottelo's direct dependencies""" +import contextlib def test_cryptography(): @@ -115,10 +116,8 @@ def test_tenacity(): def test(): raise Exception('test') - try: + with contextlib.suppress(Exception): test() - except Exception: - pass def test_testimony(): diff --git a/tests/robottelo/test_func_locker.py b/tests/robottelo/test_func_locker.py index ad4ab5c74aa..d0da97431e0 100644 --- a/tests/robottelo/test_func_locker.py +++ b/tests/robottelo/test_func_locker.py @@ -35,9 +35,7 @@ def __init__(self): def read(self): with open(self.file_name) as cf: - content = cf.read() - - return content + return cf.read() def write(self, content): with open(self.file_name, 'wb') as cf: @@ -98,9 +96,10 @@ def simple_recursive_locking_function(): """try to trigger the same lock from the same process, an exception should be expected """ - with func_locker.locking_function(simple_locked_function): - with func_locker.locking_function(simple_locked_function): - pass + with func_locker.locking_function(simple_locked_function), func_locker.locking_function( + simple_locked_function + ): + pass return 'I should not be reached' @@ -126,9 +125,10 @@ def simple_function_to_lock(): def simple_with_locking_function(index=None): global counter_file time.sleep(0.05) - with func_locker.locking_function(simple_locked_function): - with open(_get_function_lock_path('simple_locked_function')) as rf: - content = rf.read() + with func_locker.locking_function(simple_locked_function), open( + _get_function_lock_path('simple_locked_function') + ) as rf: + content = rf.read() if index is not None: saved_counter = int(counter_file.read()) @@ -169,7 +169,7 @@ def simple_scoped_lock_function(): """This function do nothing, when called the lock function must create a lock file """ - return None + return @func_locker.lock_function @@ -182,14 +182,14 @@ def simple_scoped_locking_function(): ): pass - return None + return def simple_function_not_locked(): """This function do nothing, when called with locking, exception must be raised that this function is not locked """ - return None + return class TestFuncLocker: @@ -234,9 +234,10 @@ def test_locker_file_location_when_in_class(self): content = '' assert str(os.getpid()) != content - with func_locker.locking_function(SimpleClass.simple_function_to_lock): - with open(file_path) as rf: - content = rf.read() + with func_locker.locking_function(SimpleClass.simple_function_to_lock), open( + file_path + ) as rf: + content = rf.read() assert str(os.getpid()) == content @@ -248,9 +249,10 @@ def test_locker_file_location_when_in_class(self): content = '' assert str(os.getpid()) != content - with func_locker.locking_function(SimpleClass.simple_function_to_lock_cls): - with open(file_path) as rf: - content = rf.read() + with func_locker.locking_function(SimpleClass.simple_function_to_lock_cls), open( + file_path + ) as rf: + content = rf.read() assert str(os.getpid()) == content @@ -294,9 +296,10 @@ def test_locker_file_location_when_in_class(self): else: content = '' assert str(os.getpid()) != content - with func_locker.locking_function(SimpleClass.SubClass.simple_function_to_lock_cls): - with open(file_path) as rf: - content = rf.read() + with func_locker.locking_function(SimpleClass.SubClass.simple_function_to_lock_cls), open( + file_path + ) as rf: + content = rf.read() assert str(os.getpid()) == content @@ -407,7 +410,7 @@ def test_scoped_with_locking(self): assert os.path.exists(lock_file_path) def test_negative_with_locking_not_locked(self): - - with pytest.raises(func_locker.FunctionLockerError, match=r'.*Cannot ensure locking.*'): - with func_locker.locking_function(simple_function_not_locked): - pass + with pytest.raises( + func_locker.FunctionLockerError, match=r'.*Cannot ensure locking.*' + ), func_locker.locking_function(simple_function_not_locked): + pass diff --git a/tests/upgrades/conftest.py b/tests/upgrades/conftest.py index cfa87f2bd95..1853efaf073 100644 --- a/tests/upgrades/conftest.py +++ b/tests/upgrades/conftest.py @@ -154,8 +154,7 @@ def get_entity_data(scenario_name): """ with open('scenario_entities') as pref: entity_data = json.load(pref) - entity_data = entity_data.get(scenario_name) - return entity_data + return entity_data.get(scenario_name) def get_all_entity_data(): @@ -171,8 +170,7 @@ def get_all_entity_data(): with scenario_name as keys and corresponding attribute data as values. """ with open('scenario_entities') as pref: - entity_data = json.load(pref) - return entity_data + return json.load(pref) def _read_test_data(test_node_id): @@ -304,17 +302,18 @@ def __initiate(config): global POST_UPGRADE global PRE_UPGRADE_TESTS_FILE_PATH PRE_UPGRADE_TESTS_FILE_PATH = getattr(config.option, PRE_UPGRADE_TESTS_FILE_OPTION) - if not [ - upgrade_mark - for upgrade_mark in (PRE_UPGRADE_MARK, POST_UPGRADE_MARK) - if upgrade_mark in config.option.markexpr - ]: - # Raise only if the `tests/upgrades` directory is selected - if 'upgrades' in config.args[0]: - pytest.fail( - f'For upgrade scenarios either {PRE_UPGRADE_MARK} or {POST_UPGRADE_MARK} mark ' - 'must be provided' - ) + if ( + not [ + upgrade_mark + for upgrade_mark in (PRE_UPGRADE_MARK, POST_UPGRADE_MARK) + if upgrade_mark in config.option.markexpr + ] + and 'upgrades' in config.args[0] + ): # Raise only if the `tests/upgrades` directory is selected + pytest.fail( + f'For upgrade scenarios either {PRE_UPGRADE_MARK} or {POST_UPGRADE_MARK} mark ' + 'must be provided' + ) if PRE_UPGRADE_MARK in config.option.markexpr: pre_upgrade_failed_tests = [] PRE_UPGRADE = True diff --git a/tests/upgrades/test_activation_key.py b/tests/upgrades/test_activation_key.py index bac55d78c35..11d37ccdab0 100644 --- a/tests/upgrades/test_activation_key.py +++ b/tests/upgrades/test_activation_key.py @@ -39,8 +39,7 @@ def activation_key_setup(self, request, target_sat): ak = target_sat.api.ActivationKey( content_view=cv, organization=org, name=f"{request.param}_ak" ).create() - ak_details = {'org': org, "cv": cv, 'ak': ak, 'custom_repo': custom_repo} - return ak_details + return {'org': org, "cv": cv, 'ak': ak, 'custom_repo': custom_repo} @pytest.mark.pre_upgrade @pytest.mark.parametrize( diff --git a/tests/upgrades/test_classparameter.py b/tests/upgrades/test_classparameter.py index ba63148b102..b7f8d1c6ed1 100644 --- a/tests/upgrades/test_classparameter.py +++ b/tests/upgrades/test_classparameter.py @@ -82,7 +82,7 @@ def _validate_value(self, data, sc_param): :param sc_param: The Actual Value of parameter """ if data['sc_type'] == 'boolean': - assert sc_param.default_value == (True if data['value'] == '1' else False) + assert sc_param.default_value == (data['value'] == '1') elif data['sc_type'] == 'array': string_list = [str(element) for element in sc_param.default_value] assert str(string_list) == data['value'] diff --git a/tests/upgrades/test_host.py b/tests/upgrades/test_host.py index 642fef7f2b5..5d0e9fda247 100644 --- a/tests/upgrades/test_host.py +++ b/tests/upgrades/test_host.py @@ -72,7 +72,7 @@ def class_host( Later in tests this host will be used to perform assertions """ - host = sat_gce.api.Host( + return sat_gce.api.Host( architecture=sat_gce_default_architecture, compute_attributes=self.compute_attrs, domain=sat_gce_domain, @@ -85,7 +85,6 @@ def class_host( image=module_gce_finishimg, root_pass=gen_string('alphanumeric'), ).create() - return host def google_host(self, googleclient): """Returns the Google Client Host object to perform the assertions""" diff --git a/tests/upgrades/test_puppet.py b/tests/upgrades/test_puppet.py index 5cf7b4e9ae6..b0c96394cc8 100644 --- a/tests/upgrades/test_puppet.py +++ b/tests/upgrades/test_puppet.py @@ -18,8 +18,7 @@ def upgrade_server(request, module_target_sat, pre_configured_capsule): if request.param: return module_target_sat - else: - return pre_configured_capsule + return pre_configured_capsule class TestPuppet: diff --git a/tests/upgrades/test_satellite_maintain.py b/tests/upgrades/test_satellite_maintain.py index 8035416e39a..03abb569e94 100644 --- a/tests/upgrades/test_satellite_maintain.py +++ b/tests/upgrades/test_satellite_maintain.py @@ -35,8 +35,7 @@ def satellite_upgradable_version_list(sat_obj): cmd = 'satellite-maintain upgrade list-versions --disable-self-upgrade' list_versions = sat_obj.execute(cmd).stdout regex = re.compile(r'^\d+\.\d+') - upgradeable_versions = [version for version in list_versions if regex.match(version)] - return upgradeable_versions + return [version for version in list_versions if regex.match(version)] @pytest.mark.pre_upgrade def test_pre_satellite_maintain_upgrade_list_versions(self, target_sat): diff --git a/tests/upgrades/test_subscription.py b/tests/upgrades/test_subscription.py index 4028cfc410c..917952adaa1 100644 --- a/tests/upgrades/test_subscription.py +++ b/tests/upgrades/test_subscription.py @@ -70,7 +70,7 @@ def test_post_manifest_scenario_refresh(self, request, target_sat, pre_upgrade_d history = target_sat.api.Subscription(organization=org).manifest_history( data={'organization_id': org.id} ) - assert "Subscriptions deleted by foreman_admin" == history[0]['statusMessage'] + assert history[0]['statusMessage'] == "Subscriptions deleted by foreman_admin" class TestSubscriptionAutoAttach: From de752665b6180a877e1c2dc82bea4d253bf90b9b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 13 Mar 2024 07:10:39 -0400 Subject: [PATCH 532/586] [6.14.z] Broker dependency updated to resolve ssh2-python limitation/packagever issue (#14381) Broker dependency updated to resolve ssh2-python limitation/packagever issue (#14275) Broker dependency updated to resolve limitation issue (cherry picked from commit d9011761b7d4ed4cb4524f75f68cec38124fff3b) Co-authored-by: Jitendra Yejare --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 39a46d11762..fde313e4410 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,5 +35,5 @@ git+https://github.com/SatelliteQE/nailgun.git@6.14.z#egg=nailgun # In the meantime, we install directly from the repo # [1] - https://github.com/ParallelSSH/ssh2-python/issues/193 # [2] - https://github.com/pypi/warehouse/issues/7136 -git+https://github.com/SatelliteQE/broker.git@0.4.5#egg=broker +git+https://github.com/SatelliteQE/broker.git@0.4.7#egg=broker --editable . From f39a3231cc604c1451821f4fee8f447a4d624522 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 13 Mar 2024 10:23:14 -0400 Subject: [PATCH 533/586] [6.14.z] component name changes (#14389) component name changes (#14380) (cherry picked from commit 1f96a99e16e9b318bf7fb4198c085c7463e6c505) Co-authored-by: Peter Ondrejka --- testimony.yaml | 3 --- tests/foreman/api/test_ldapauthsource.py | 2 +- tests/foreman/cli/test_ldapauthsource.py | 2 +- tests/foreman/cli/test_sso.py | 2 +- tests/foreman/destructive/test_ldap_authentication.py | 2 +- tests/foreman/destructive/test_ldapauthsource.py | 2 +- tests/foreman/sys/test_dynflow.py | 4 ++-- tests/foreman/ui/test_ldap_authentication.py | 2 +- 8 files changed, 8 insertions(+), 11 deletions(-) diff --git a/testimony.yaml b/testimony.yaml index 5f7162199f5..fed691dd424 100644 --- a/testimony.yaml +++ b/testimony.yaml @@ -47,8 +47,6 @@ CaseComponent: - DiscoveryImage - DiscoveryPlugin - Documentation - - Dynflow - - Email - Entitlements - ErrataManagement - Fact @@ -68,7 +66,6 @@ CaseComponent: - InterSatelliteSync - katello-agent - katello-tracer - - LDAP - Leappintegration - LifecycleEnvironments - LocalizationInternationalization diff --git a/tests/foreman/api/test_ldapauthsource.py b/tests/foreman/api/test_ldapauthsource.py index fbcc7039193..885d290e741 100644 --- a/tests/foreman/api/test_ldapauthsource.py +++ b/tests/foreman/api/test_ldapauthsource.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: LDAP +:CaseComponent: Authentication :Team: Endeavour diff --git a/tests/foreman/cli/test_ldapauthsource.py b/tests/foreman/cli/test_ldapauthsource.py index 528a47420a5..ce7b625c3a6 100644 --- a/tests/foreman/cli/test_ldapauthsource.py +++ b/tests/foreman/cli/test_ldapauthsource.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: LDAP +:CaseComponent: Authentication :Team: Endeavour diff --git a/tests/foreman/cli/test_sso.py b/tests/foreman/cli/test_sso.py index d8b772a936f..949e021ff48 100644 --- a/tests/foreman/cli/test_sso.py +++ b/tests/foreman/cli/test_sso.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: LDAP +:CaseComponent: Authentication :Team: Endeavour diff --git a/tests/foreman/destructive/test_ldap_authentication.py b/tests/foreman/destructive/test_ldap_authentication.py index 58f3121dbec..33539fff89e 100644 --- a/tests/foreman/destructive/test_ldap_authentication.py +++ b/tests/foreman/destructive/test_ldap_authentication.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: LDAP +:CaseComponent: Authentication :Team: Endeavour diff --git a/tests/foreman/destructive/test_ldapauthsource.py b/tests/foreman/destructive/test_ldapauthsource.py index 020aad9f716..0cebd782532 100644 --- a/tests/foreman/destructive/test_ldapauthsource.py +++ b/tests/foreman/destructive/test_ldapauthsource.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: LDAP +:CaseComponent: Authentication :Team: Endeavour diff --git a/tests/foreman/sys/test_dynflow.py b/tests/foreman/sys/test_dynflow.py index f08f07d4f8d..2b758bcb208 100644 --- a/tests/foreman/sys/test_dynflow.py +++ b/tests/foreman/sys/test_dynflow.py @@ -2,11 +2,11 @@ :CaseAutomation: Automated -:CaseComponent: Dynflow +:CaseComponent: TasksPlugin :Team: Endeavour -:Requirement: Dynflow +:Requirement: TasksPlugin :CaseImportance: High diff --git a/tests/foreman/ui/test_ldap_authentication.py b/tests/foreman/ui/test_ldap_authentication.py index 6fcb71a7efa..a6e3af6d612 100644 --- a/tests/foreman/ui/test_ldap_authentication.py +++ b/tests/foreman/ui/test_ldap_authentication.py @@ -4,7 +4,7 @@ :CaseAutomation: Automated -:CaseComponent: LDAP +:CaseComponent: Authentication :Team: Endeavour From 51062396bcf30f86d100e4c96eb24f5143f86d6e Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 13 Mar 2024 13:36:38 -0400 Subject: [PATCH 534/586] [6.14.z] changes in test_positive_run_job_on_host_converted_to_pull_provider (#14395) --- tests/foreman/cli/test_remoteexecution.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index 0fbffea78c7..1b3bca14dc0 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -1199,15 +1199,11 @@ def test_positive_run_job_on_host_converted_to_pull_provider( assert_job_invocation_result( module_target_sat, invocation_command['id'], rhel_contenthost.hostname ) - # check katello-agent runs along ygdrassil (SAT-1671) - result = rhel_contenthost.execute('systemctl status goferd') - assert result.status == 0, 'Failed to start goferd on client' - # run Ansible rex command to prove ssh provider works, remove katello-agent invocation_command = module_target_sat.cli_factory.job_invocation( { - 'job-template': 'Package Action - Ansible Default', - 'inputs': 'state=absent, name=katello-agent', + 'job-template': 'Remove Package - Katello Script Default', + 'inputs': 'package=katello-agent', 'search-query': f"name ~ {rhel_contenthost.hostname}", } ) From cea7f4628d6832a0bb63d68ff0f22df7461fd2c9 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:34:25 -0400 Subject: [PATCH 535/586] [6.14.z] Skip orphan cleanup case for n_minus scenario (#14408) Skip orphan cleanup case for n_minus scenario (#14371) (cherry picked from commit 672ba8dbe3a81c29b5b36e1548950975346af189) Co-authored-by: vsedmik <46570670+vsedmik@users.noreply.github.com> --- tests/foreman/api/test_capsulecontent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/api/test_capsulecontent.py b/tests/foreman/api/test_capsulecontent.py index 46a04b47845..cdb09ffc883 100644 --- a/tests/foreman/api/test_capsulecontent.py +++ b/tests/foreman/api/test_capsulecontent.py @@ -1289,7 +1289,7 @@ def test_positive_remove_capsule_orphans( :BZ: 22043089, 2211962 """ - if not pytestconfig.option.n_minus: + if pytestconfig.option.n_minus: pytest.skip('Test cannot be run on n-minus setups session-scoped capsule') # Enable RHST repo and sync it to the Library LCE. repo_id = target_sat.api_factory.enable_rhrepo_and_fetchid( From f8e2c21f0ab95cbbaa793a7d2fdaa150eb48dee1 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:12:41 -0400 Subject: [PATCH 536/586] [6.14.z] Integrate FAM pipeline into robottelo (#14157) * Integrate FAM pipeline into robottelo (#14028) (cherry picked from commit 7aca395efcc61aa74809491d02e1c7e1175d36f3) * Fix manifest file transer for fam --------- Co-authored-by: Griffin Sullivan <48397354+Griffin-Sullivan@users.noreply.github.com> Co-authored-by: Griffin-Sullivan --- robottelo/constants/__init__.py | 119 +++++++++++++++++++++++++++++++- tests/foreman/sys/test_fam.py | 63 ++++++++++++++++- 2 files changed, 178 insertions(+), 4 deletions(-) diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index e29d67c3a11..bca0dbdf9b2 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -1923,9 +1923,122 @@ class Colored(Box): "user", ] -FAM_MODULE_PATH = ( - '/usr/share/ansible/collections/ansible_collections/redhat/satellite/plugins/modules' -) +FAM_TEST_PLAYBOOKS = [ + "activation_keys_role", + "activation_key", + "architecture", + "auth_source_ldap", + "auth_sources_ldap_role", + "bookmark", + "compute_attribute", + "compute_profile_ovirt", + "compute_profiles_role", + "compute_profile", + "compute_resources_role", + "compute_resource", + "config_group", + "content_credentials_role", + "content_credential", + "content_export_info", + "content_export_library", + "content_export_repository", + "content_export_version", + "content_rhel_role", + "content_upload_ostree", + "content_upload", + "content_view_filter_info", + "content_view_filter_rule_info", + "content_view_filter_rule", + "content_view_filter", + "content_view_info", + "content_view_publish_role", + "content_views_role", + "content_view_version_cleanup_role", + "content_view_version_info", + "content_view_version", + "content_view", + "convert2rhel", + "discovery_rule", + "domain_info", + "domains_role", + "domain", + "external_usergroup", + "filters", + "global_parameter", + "hardware_model", + "host_collection", + "host_errata_info", + "hostgroup_info", + "hostgroups_role", + "hostgroup", + "host_info", + "host_interface_attributes", + "host_power", + "host", + "http_proxy", + "image", + "installation_medium", + "inventory_plugin_ansible", + "inventory_plugin", + "job_invocation", + "job_template", + "katello_hostgroup", + "katello_smart_proxy", + "lifecycle_environments_role", + "lifecycle_environment", + "locations_role", + "location", + "luna_hostgroup", + "manifest_role", + "module_defaults", + "operatingsystems_role", + "operatingsystem", + "organization_info", + "organizations_role", + "organization", + "os_default_template", + "partition_table", + "product", + "provisioning_templates_role", + "provisioning_template", + "puppetclasses_import", + "puppet_environment", + "realm", + "redhat_manifest", + "repositories_role", + "repository_info", + "repository_ostree", + "repository_set_info", + "repository_set", + "repository_sync", + "repository", + "resource_info", + "role", + "scap_content", + "scap_tailoring_file", + "setting_info", + "settings_role", + "setting", + "smart_class_parameter_override_value", + "smart_class_parameter", + "smart_proxy", + "status_info", + "subnet_info", + "subnets_role", + "subnet", + "subscription_info", + "subscription_manifest", + "sync_plans_role", + "sync_plan", + "templates_import", + "usergroup", + "user", + "wait_for_task", +] + +FAM_ROOT_DIR = '/usr/share/ansible/collections/ansible_collections/redhat/satellite' + +FAM_MODULE_PATH = f'{FAM_ROOT_DIR}/plugins/modules' RH_SAT_ROLES = [ 'activation_keys', diff --git a/tests/foreman/sys/test_fam.py b/tests/foreman/sys/test_fam.py index ebc9d155769..f4500a2b599 100644 --- a/tests/foreman/sys/test_fam.py +++ b/tests/foreman/sys/test_fam.py @@ -11,9 +11,17 @@ :Team: Platform """ +from broker import Broker import pytest -from robottelo.constants import FAM_MODULE_PATH, FOREMAN_ANSIBLE_MODULES, RH_SAT_ROLES +from robottelo.config import settings +from robottelo.constants import ( + FAM_MODULE_PATH, + FAM_ROOT_DIR, + FAM_TEST_PLAYBOOKS, + FOREMAN_ANSIBLE_MODULES, + RH_SAT_ROLES, +) @pytest.fixture @@ -32,6 +40,42 @@ def sync_roles(target_sat): target_sat.cli.Ansible.roles_delete({'id': role_id}) +@pytest.fixture(scope='module') +def setup_fam(module_target_sat, module_sca_manifest): + # Execute AAP WF for FAM setup + Broker().execute(workflow='fam-test-setup', source_vm=module_target_sat.name) + + # Edit Makefile to not try to rebuild the collection when tests run + module_target_sat.execute(f"sed -i '/^live/ s/$(MANIFEST)//' {FAM_ROOT_DIR}/Makefile") + + # Upload manifest to test playbooks directory + module_target_sat.put(str(module_sca_manifest.path), str(module_sca_manifest.name)) + module_target_sat.execute( + f'mv {module_sca_manifest.name} {FAM_ROOT_DIR}/tests/test_playbooks/data' + ) + + # Edit config file + config_file = f'{FAM_ROOT_DIR}/tests/test_playbooks/vars/server.yml' + module_target_sat.execute( + f'cp {FAM_ROOT_DIR}/tests/test_playbooks/vars/server.yml.example {config_file}' + ) + module_target_sat.execute( + f'sed -i "s/foreman.example.com/{module_target_sat.hostname}/g" {config_file}' + ) + module_target_sat.execute( + f'sed -i "s/rhsm_pool_id:.*/rhsm_pool_id: {settings.subscription.rhn_poolid}/g" {config_file}' + ) + module_target_sat.execute( + f'''sed -i 's/rhsm_username:.*/rhsm_username: "{settings.subscription.rhn_username}"/g' {config_file}''' + ) + module_target_sat.execute( + f'''sed -i 's|subscription_manifest_path:.*|subscription_manifest_path: "data/{module_sca_manifest.name}"|g' {config_file}''' + ) + module_target_sat.execute( + f'''sed -i 's/rhsm_password:.*/rhsm_password: "{settings.subscription.rhn_password}"/g' {config_file}''' + ) + + @pytest.mark.pit_server @pytest.mark.run_in_one_thread def test_positive_ansible_modules_installation(target_sat): @@ -71,3 +115,20 @@ def test_positive_import_run_roles(sync_roles, target_sat): target_sat.cli.Host.ansible_roles_assign({'ansible-roles': roles, 'name': target_sat.hostname}) play = target_sat.cli.Host.ansible_roles_play({'name': target_sat.hostname}) assert 'Ansible roles are being played' in play[0]['message'] + + +@pytest.mark.e2e +@pytest.mark.parametrize('ansible_module', FAM_TEST_PLAYBOOKS) +def test_positive_run_modules_and_roles(module_target_sat, setup_fam, ansible_module): + """Run all FAM modules and roles on the Satellite + + :id: b595756f-627c-44ea-b738-aa17ff5b1d39 + + :expectedresults: All modules and roles run successfully + """ + # Execute test_playbook + result = module_target_sat.execute( + f'export NO_COLOR=True && . ~/localenv/bin/activate && cd {FAM_ROOT_DIR} && make livetest_{ansible_module}' + ) + assert 'PASSED' in result.stdout + assert result.status == 0 From e2e3944f361cb9bb57ab12f246d1446eeb4a74c6 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 15 Mar 2024 15:55:20 -0400 Subject: [PATCH 537/586] [6.14.z] Add posibility to filter tests that require manifester (#14418) Add posibility to filter tests that require manifester (#14270) (cherry picked from commit 0397a0d1db879dccd84a099e7ee0765df53dda61) Co-authored-by: dosas --- pytest_plugins/markers.py | 1 + tests/foreman/conftest.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pytest_plugins/markers.py b/pytest_plugins/markers.py index abf54997bd8..b7e0f8f6346 100644 --- a/pytest_plugins/markers.py +++ b/pytest_plugins/markers.py @@ -24,6 +24,7 @@ def pytest_configure(config): "no_containers: Disable container hosts from being used in favor of VMs", "include_capsule: For satellite-maintain tests to run on Satellite and Capsule both", "capsule_only: For satellite-maintain tests to run only on Capsules", + "manifester: Tests that require manifester", ] markers.extend(module_markers()) for marker in markers: diff --git a/tests/foreman/conftest.py b/tests/foreman/conftest.py index e867e118be3..339eb6016f9 100644 --- a/tests/foreman/conftest.py +++ b/tests/foreman/conftest.py @@ -30,6 +30,8 @@ def pytest_collection_modifyitems(session, items, config): deselected_items = [] for item in items: + if any("manifest" in f for f in getattr(item, "fixturenames", ())): + item.add_marker("manifester") # 1. Deselect tests marked with @pytest.mark.deselect # WONTFIX BZs makes test to be dynamically marked as deselect. deselect = item.get_closest_marker('deselect') From 251d630ff480f7c2e2483a9899e5c68093db19f2 Mon Sep 17 00:00:00 2001 From: rmynar <64528205+rmynar@users.noreply.github.com> Date: Fri, 15 Mar 2024 20:57:33 +0100 Subject: [PATCH 538/586] [6.14.z] remove log check causing false negatives (#14410) remove log check causing false negatives (#14314) --- tests/foreman/installer/test_installer.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/foreman/installer/test_installer.py b/tests/foreman/installer/test_installer.py index e6a3e9c7ba5..451c3898e9d 100644 --- a/tests/foreman/installer/test_installer.py +++ b/tests/foreman/installer/test_installer.py @@ -1374,8 +1374,7 @@ def sat_default_install(module_sat_ready_rhels): f'foreman-initial-admin-password {settings.server.admin_password}', ] install_satellite(module_sat_ready_rhels[0], installer_args) - yield module_sat_ready_rhels[0] - common_sat_install_assertions(module_sat_ready_rhels[0]) + return module_sat_ready_rhels[0] @pytest.fixture(scope='module') @@ -1388,8 +1387,7 @@ def sat_non_default_install(module_sat_ready_rhels): 'foreman-proxy-content-pulpcore-hide-guarded-distributions false', ] install_satellite(module_sat_ready_rhels[1], installer_args) - yield module_sat_ready_rhels[1] - common_sat_install_assertions(module_sat_ready_rhels[1]) + return module_sat_ready_rhels[1] @pytest.mark.e2e From 3f57a779b9690863047c638fd01a51b362ea3c70 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 15 Mar 2024 16:29:36 -0400 Subject: [PATCH 539/586] [6.14.z] change a way to upload content into satellite (#14404) --- tests/foreman/api/test_repository.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index 4d64c3866fc..e73a4ae35e5 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -2331,13 +2331,14 @@ def test_positive_upload_file_to_file_repo(self, repo, target_sat): :CaseAutomation: Automated """ - repo.upload_content(files={'content': DataFile.RPM_TO_UPLOAD.read_bytes()}) + with open(DataFile.FAKE_FILE_NEW_NAME, 'rb') as handle: + repo.upload_content(files={'content': handle}) assert repo.read().content_counts['file'] == 1 filesearch = target_sat.api.File().search( - query={"search": f"name={constants.RPM_TO_UPLOAD}"} + query={"search": f"name={constants.FAKE_FILE_NEW_NAME}"} ) - assert filesearch[0].name == constants.RPM_TO_UPLOAD + assert filesearch[0].name == constants.FAKE_FILE_NEW_NAME @pytest.mark.tier1 @pytest.mark.upgrade From 9c253bfad69b1e170a995ece80f89b0945356e67 Mon Sep 17 00:00:00 2001 From: David Moore <109112035+damoore044@users.noreply.github.com> Date: Mon, 18 Mar 2024 18:07:11 -0400 Subject: [PATCH 540/586] [6.14.z] Eval CapsuleContent::API Assertion Errors, time delta/format in 'wait_for_sync()' (#14413) [6.14.z CP] API::CapsuleContent Eval change to sca-only orgs, entitlement orgs are failing assert capsule sync task(s) when invoked, and sync status pre-commit fix, refactor wait_for_sync() into concise steps --- robottelo/host_helpers/capsule_mixins.py | 93 +++++++-- tests/foreman/api/test_capsulecontent.py | 249 +++++++++++++---------- 2 files changed, 221 insertions(+), 121 deletions(-) diff --git a/robottelo/host_helpers/capsule_mixins.py b/robottelo/host_helpers/capsule_mixins.py index 89274435ec2..9ae7edfb761 100644 --- a/robottelo/host_helpers/capsule_mixins.py +++ b/robottelo/host_helpers/capsule_mixins.py @@ -1,6 +1,8 @@ -from datetime import datetime +from datetime import datetime, timedelta import time +from dateutil.parser import parse + from robottelo.constants import PUPPET_CAPSULE_INSTALLER, PUPPET_COMMON_INSTALLER_OPTS from robottelo.logging import logger from robottelo.utils.installer import InstallerCommand @@ -60,26 +62,85 @@ def wait_for_tasks( raise AssertionError(f"No task was found using query '{search_query}'") return tasks - def wait_for_sync(self, timeout=600, start_time=None): - """Wait for capsule sync to finish and assert the sync task succeeded""" - # Assert that a task to sync lifecycle environment to the capsule - # is started (or finished already) + def wait_for_sync(self, start_time=None, timeout=600): + """Wait for capsule sync to finish and assert success. + Assert that a task to sync lifecycle environment to the + capsule is started (or finished already), and succeeded. + :raises: ``AssertionError``: If a capsule sync verification fails based on the conditions. + + - Found some active sync task(s) for capsule, or it just finished (recent sync time). + - Any active sync task(s) polled, succeeded, and the capsule last_sync_time is updated. + - last_sync_time after final task is on or newer than start_time. + - The total sync time duration (seconds) is within timeout and not negative. + + :param start_time: (datetime): UTC time to compare against capsule's last_sync_time. + Default: None (current UTC). + :param timeout: (int) maximum seconds for active task(s) and queries to finish. + + :return: + list of polled finished tasks that were in-progress from `active_sync_tasks`. + """ + # Fetch initial capsule sync status + logger.info(f"Waiting for capsule {self.hostname} sync to finish ...") + sync_status = self.nailgun_capsule.content_get_sync(timeout=timeout, synchronous=True) + # Current UTC time for start_time, if not provided if start_time is None: start_time = datetime.utcnow().replace(microsecond=0) - logger.info(f"Waiting for capsule {self.hostname} sync to finish ...") - sync_status = self.nailgun_capsule.content_get_sync() - logger.info(f"Active tasks {sync_status['active_sync_tasks']}") + # 1s margin of safety for rounding + start_time = ( + (start_time - timedelta(seconds=1)) + .replace(microsecond=0) + .strftime('%Y-%m-%d %H:%M:%S UTC') + ) + # Assert presence of recent sync activity: + # one or more ongoing sync tasks for the capsule, + # Or, capsule's last_sync_time is on or after start_time + assert len(sync_status['active_sync_tasks']) or ( + parse(sync_status['last_sync_time']) >= parse(start_time) + ), ( + f"No active or recent sync found for capsule {self.hostname}." + f" `active_sync_tasks` was empty: {sync_status['active_sync_tasks']}," + f" and the `last_sync_time`: {sync_status['last_sync_time']}," + f" was prior to the `start_time`: {start_time}." + ) + sync_tasks = [] + # Poll and verify succeeds, any active sync task from initial status. + logger.info(f"Active tasks: {sync_status['active_sync_tasks']}") + for task in sync_status['active_sync_tasks']: + sync_tasks.append(self.satellite.api.ForemanTask(id=task['id']).poll(timeout=timeout)) + logger.info(f"Active sync task :id {task['id']} succeeded.") + + # Fetch updated capsule status (expect no ongoing sync) + logger.info(f"Querying updated sync status from capsule {self.hostname}.") + updated_status = self.nailgun_capsule.content_get_sync(timeout=timeout, synchronous=True) + + # Total time taken is not negative (sync prior to start_time), + # and did not exceed timeout. assert ( - len(sync_status['active_sync_tasks']) - or datetime.strptime(sync_status['last_sync_time'], '%Y-%m-%d %H:%M:%S UTC') - >= start_time + timedelta(seconds=0) + <= parse(updated_status['last_sync_time']) - parse(start_time) + <= timedelta(seconds=timeout) + ), ( + f"No recent sync task(s) were found for capsule: {self.hostname}, or task(s) timed out." + f" `last_sync_time`: ({updated_status['last_sync_time']}) was prior to `start_time`: ({start_time})" + f" or exceeded timeout ({timeout}s)." ) + # No failed or active tasks remaining + assert len(updated_status['last_failed_sync_tasks']) == 0 + assert len(updated_status['active_sync_tasks']) == 0 - # Wait till capsule sync finishes and assert the sync task succeeded - for task in sync_status['active_sync_tasks']: - self.satellite.api.ForemanTask(id=task['id']).poll(timeout=timeout) - sync_status = self.nailgun_capsule.content_get_sync() - assert len(sync_status['last_failed_sync_tasks']) == 0 + # Last sync task end time is the same as capsule's last sync time. + if len(sync_status['active_sync_tasks']): + final_task_id = sync_status['active_sync_tasks'][-1]['id'] + final_task_end_time = self.satellite.api.ForemanTask(id=final_task_id).read().ended_at + + assert parse(final_task_end_time) == parse(updated_status['last_sync_time']), ( + f"Final Task :id: {final_task_id}," + f" `end_time` does not match capsule `last_sync_time`. Capsule: {self.hostname}" + ) + + # return any polled sync tasks, that were initially in-progress + return sync_tasks def get_published_repo_url(self, org, prod, repo, lce=None, cv=None): """Forms url of a repo or CV published on a Satellite or Capsule. diff --git a/tests/foreman/api/test_capsulecontent.py b/tests/foreman/api/test_capsulecontent.py index cdb09ffc883..3fc0f8e7e3f 100644 --- a/tests/foreman/api/test_capsulecontent.py +++ b/tests/foreman/api/test_capsulecontent.py @@ -12,7 +12,8 @@ :CaseImportance: High """ -from datetime import datetime + +from datetime import datetime, timedelta import re from time import sleep @@ -20,9 +21,24 @@ from nailgun.entity_mixins import call_entity_method_with_timeout import pytest -from robottelo import constants from robottelo.config import settings -from robottelo.constants import DataFile +from robottelo.constants import ( + CONTAINER_CLIENTS, + ENVIRONMENT, + FAKE_1_YUM_REPOS_COUNT, + FAKE_3_YUM_REPO_RPMS, + FAKE_3_YUM_REPOS_COUNT, + FAKE_FILE_LARGE_COUNT, + FAKE_FILE_LARGE_URL, + FAKE_FILE_NEW_NAME, + KICKSTART_CONTENT, + PRDS, + REPOS, + REPOSET, + RH_CONTAINER_REGISTRY_HUB, + RPM_TO_UPLOAD, + DataFile, +) from robottelo.constants.repos import ANSIBLE_GALAXY from robottelo.content_info import ( get_repo_files_by_url, @@ -79,14 +95,14 @@ def test_positive_uploaded_content_library_sync( assert repo.read().content_counts['rpm'] == 1 + timestamp = datetime.utcnow().replace(microsecond=0) # Publish new version of the content view cv.publish() + # query sync status as publish invokes sync, task succeeds + module_capsule_configured.wait_for_sync(start_time=timestamp) cv = cv.read() - assert len(cv.version) == 1 - module_capsule_configured.wait_for_sync() - # Verify the RPM published on Capsule caps_repo_url = module_capsule_configured.get_published_repo_url( org=function_org.label, @@ -97,7 +113,7 @@ def test_positive_uploaded_content_library_sync( ) caps_files = get_repo_files_by_url(caps_repo_url) assert len(caps_files) == 1 - assert caps_files[0] == constants.RPM_TO_UPLOAD + assert caps_files[0] == RPM_TO_UPLOAD @pytest.mark.tier4 @pytest.mark.skip_if_not_set('capsule', 'clients', 'fake_manifest') @@ -141,17 +157,17 @@ def test_positive_checksum_sync( repo = repo.read() cv.publish() cv = cv.read() - assert len(cv.version) == 1 cvv = cv.version[-1].read() + timestamp = datetime.utcnow() + # promote to capsule lce, invoking sync tasks cvv.promote(data={'environment_ids': function_lce.id}) - cvv = cvv.read() + module_capsule_configured.wait_for_sync(start_time=timestamp) + cvv = cvv.read() assert len(cvv.environment) == 2 - module_capsule_configured.wait_for_sync() - # Verify repodata's checksum type is sha256, not sha1 on capsule repo_url = module_capsule_configured.get_published_repo_url( org=function_org.label, @@ -173,18 +189,17 @@ def test_positive_checksum_sync( repo.sync() cv.publish() cv = cv.read() - assert len(cv.version) == 2 cv.version.sort(key=lambda version: version.id) cvv = cv.version[-1].read() + timestamp = datetime.utcnow() cvv.promote(data={'environment_ids': function_lce.id}) - cvv = cvv.read() + module_capsule_configured.wait_for_sync(start_time=timestamp) + cvv = cvv.read() assert len(cvv.environment) == 2 - module_capsule_configured.wait_for_sync() - # Verify repodata's checksum type has updated to sha1 on capsule repomd = get_repomd(repo_url) checksum_types = re.findall(r'(?<=checksum type=").*?(?=")', repomd) @@ -253,12 +268,13 @@ def test_positive_sync_updated_repo( assert len(cv.version) == 1 cvv = cv.version[-1].read() + timestamp = datetime.utcnow() cvv.promote(data={'environment_ids': function_lce.id}) + + module_capsule_configured.wait_for_sync(start_time=timestamp) cvv = cvv.read() assert len(cvv.environment) == 2 - module_capsule_configured.wait_for_sync() - # Upload more content to the repository with open(DataFile.SRPM_TO_UPLOAD, 'rb') as handle: repo.upload_content(files={'content': handle}) @@ -272,12 +288,13 @@ def test_positive_sync_updated_repo( cv.version.sort(key=lambda version: version.id) cvv = cv.version[-1].read() + timestamp = datetime.utcnow() cvv.promote(data={'environment_ids': function_lce.id}) + + module_capsule_configured.wait_for_sync(start_time=timestamp) cvv = cvv.read() assert len(cvv.environment) == 2 - module_capsule_configured.wait_for_sync() - # Check the content is synced on the Capsule side properly sat_repo_url = target_sat.get_published_repo_url( org=function_org.label, @@ -349,14 +366,22 @@ def test_positive_capsule_sync( # Publish new version of the content view cv.publish() cv = cv.read() - assert len(cv.version) == 1 - cvv = cv.version[-1].read() - # Promote content view to lifecycle environment + + # prior to trigger (promoting), assert no active sync tasks + active_tasks = module_capsule_configured.nailgun_capsule.content_get_sync( + synchronous=False, timeout=600 + )['active_sync_tasks'] + assert len(active_tasks) == 0 + + # Promote content view to lifecycle environment, + # invoking capsule sync task(s) + timestamp = datetime.utcnow() cvv.promote(data={'environment_ids': function_lce.id}) + # wait for and validate the invoked task(s) + module_capsule_configured.wait_for_sync(start_time=timestamp) cvv = cvv.read() - assert len(cvv.environment) == 2 # Content of the published content view in @@ -364,8 +389,6 @@ def test_positive_capsule_sync( # repository assert repo.content_counts['rpm'] == cvv.package_count - module_capsule_configured.wait_for_sync() - # Assert that the content published on the capsule is exactly the # same as in repository on satellite sat_repo_url = target_sat.get_published_repo_url( @@ -400,14 +423,14 @@ def test_positive_capsule_sync( cv = cv.read() cv.version.sort(key=lambda version: version.id) cvv = cv.version[-1].read() - # Promote new content view version to lifecycle environment + # Promote new content view version to lifecycle environment, + # capsule sync task(s) invoked and succeed + timestamp = datetime.utcnow() cvv.promote(data={'environment_ids': function_lce.id}) - cvv = cvv.read() + module_capsule_configured.wait_for_sync(start_time=timestamp) + cvv = cvv.read() assert len(cvv.environment) == 2 - - module_capsule_configured.wait_for_sync() - # Assert that the value of repomd revision of repository in # lifecycle environment on the capsule has not changed new_lce_revision_capsule = get_repomd_revision(caps_repo_url) @@ -423,21 +446,22 @@ def test_positive_capsule_sync( cv = cv.read() cv.version.sort(key=lambda version: version.id) cvv = cv.version[-1].read() + + timestamp = datetime.utcnow() cvv.promote(data={'environment_ids': function_lce.id}) - cvv = cvv.read() + module_capsule_configured.wait_for_sync(start_time=timestamp) + cvv = cvv.read() assert len(cvv.environment) == 2 # Assert that packages count in the repository is updated - assert repo.content_counts['rpm'] == (constants.FAKE_1_YUM_REPOS_COUNT + 1) + assert repo.content_counts['rpm'] == (FAKE_1_YUM_REPOS_COUNT + 1) # Assert that the content of the published content view in # lifecycle environment is exactly the same as content of the # repository assert repo.content_counts['rpm'] == cvv.package_count - module_capsule_configured.wait_for_sync() - # Assert that the content published on the capsule is exactly the # same as in the repository sat_files = get_repo_files_by_url(sat_repo_url) @@ -447,7 +471,7 @@ def test_positive_capsule_sync( @pytest.mark.tier4 @pytest.mark.skip_if_not_set('capsule', 'clients') def test_positive_iso_library_sync( - self, module_capsule_configured, module_entitlement_manifest_org, module_target_sat + self, module_capsule_configured, module_sca_manifest_org, module_target_sat ): """Ensure RH repo with ISOs after publishing to Library is synchronized to capsule automatically @@ -463,18 +487,18 @@ def test_positive_iso_library_sync( # Enable & sync RH repository with ISOs rh_repo_id = module_target_sat.api_factory.enable_rhrepo_and_fetchid( basearch='x86_64', - org_id=module_entitlement_manifest_org.id, - product=constants.PRDS['rhsc'], - repo=constants.REPOS['rhsc7_iso']['name'], - reposet=constants.REPOSET['rhsc7_iso'], + org_id=module_sca_manifest_org.id, + product=PRDS['rhsc'], + repo=REPOS['rhsc7_iso']['name'], + reposet=REPOSET['rhsc7_iso'], releasever=None, ) rh_repo = module_target_sat.api.Repository(id=rh_repo_id).read() call_entity_method_with_timeout(rh_repo.sync, timeout=2500) # Find "Library" lifecycle env for specific organization lce = module_target_sat.api.LifecycleEnvironment( - organization=module_entitlement_manifest_org - ).search(query={'search': f'name={constants.ENVIRONMENT}'})[0] + organization=module_sca_manifest_org + ).search(query={'search': f'name={ENVIRONMENT}'})[0] # Associate the lifecycle environment with the capsule module_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( @@ -487,23 +511,23 @@ def test_positive_iso_library_sync( # Create a content view with the repository cv = module_target_sat.api.ContentView( - organization=module_entitlement_manifest_org, repository=[rh_repo] + organization=module_sca_manifest_org, repository=[rh_repo] ).create() # Publish new version of the content view + timestamp = datetime.utcnow() cv.publish() - cv = cv.read() + module_capsule_configured.wait_for_sync(start_time=timestamp) + cv = cv.read() assert len(cv.version) == 1 # Verify ISOs are present on satellite sat_isos = get_repo_files_by_url(rh_repo.full_path, extension='iso') assert len(sat_isos) == 4 - module_capsule_configured.wait_for_sync() - # Verify all the ISOs are present on capsule caps_path = ( - f'{module_capsule_configured.url}/pulp/content/{module_entitlement_manifest_org.label}' + f'{module_capsule_configured.url}/pulp/content/{module_sca_manifest_org.label}' f'/{lce.label}/{cv.label}/content/dist/rhel/server/7/7Server/x86_64/sat-capsule/6.4/' 'iso/' ) @@ -536,8 +560,8 @@ def test_positive_on_demand_sync( the original package from the upstream repo """ repo_url = settings.repos.yum_3.url - packages_count = constants.FAKE_3_YUM_REPOS_COUNT - package = constants.FAKE_3_YUM_REPO_RPMS[0] + packages_count = FAKE_3_YUM_REPOS_COUNT + package = FAKE_3_YUM_REPO_RPMS[0] repo = target_sat.api.Repository( download_policy='on_demand', mirroring_policy='mirror_complete', @@ -569,13 +593,13 @@ def test_positive_on_demand_sync( cvv = cv.version[-1].read() # Promote content view to lifecycle environment + timestamp = datetime.utcnow() cvv.promote(data={'environment_ids': function_lce.id}) - cvv = cvv.read() + module_capsule_configured.wait_for_sync(start_time=timestamp) + cvv = cvv.read() assert len(cvv.environment) == 2 - module_capsule_configured.wait_for_sync() - # Verify packages on Capsule match the source caps_repo_url = module_capsule_configured.get_published_repo_url( org=function_org.label, @@ -620,7 +644,7 @@ def test_positive_update_with_immediate_sync( filesystem contains valid links to packages """ repo_url = settings.repos.yum_1.url - packages_count = constants.FAKE_1_YUM_REPOS_COUNT + packages_count = FAKE_1_YUM_REPOS_COUNT repo = target_sat.api.Repository( download_policy='on_demand', mirroring_policy='mirror_complete', @@ -651,13 +675,13 @@ def test_positive_update_with_immediate_sync( cvv = cv.version[-1].read() # Promote content view to lifecycle environment + timestamp = datetime.utcnow() cvv.promote(data={'environment_ids': function_lce.id}) - cvv = cvv.read() + module_capsule_configured.wait_for_sync(start_time=timestamp) + cvv = cvv.read() assert len(cvv.environment) == 2 - module_capsule_configured.wait_for_sync() - # Update download policy to 'immediate' repo.download_policy = 'immediate' repo = repo.update(['download_policy']) @@ -679,13 +703,13 @@ def test_positive_update_with_immediate_sync( cv.version.sort(key=lambda version: version.id) cvv = cv.version[-1].read() # Promote content view to lifecycle environment + timestamp = datetime.utcnow() cvv.promote(data={'environment_ids': function_lce.id}) - cvv = cvv.read() + module_capsule_configured.wait_for_sync(start_time=timestamp) + cvv = cvv.read() assert len(cvv.environment) == 2 - module_capsule_configured.wait_for_sync() - # Verify the count of RPMs published on Capsule caps_repo_url = module_capsule_configured.get_published_repo_url( org=function_org.label, @@ -726,7 +750,7 @@ def test_positive_capsule_pub_url_accessible(self, module_capsule_configured): @pytest.mark.skip_if_not_set('capsule', 'clients') @pytest.mark.parametrize('distro', ['rhel7', 'rhel8_bos', 'rhel9_bos']) def test_positive_sync_kickstart_repo( - self, target_sat, module_capsule_configured, function_entitlement_manifest_org, distro + self, target_sat, module_capsule_configured, function_sca_manifest_org, distro ): """Sync kickstart repository to the capsule. @@ -747,16 +771,14 @@ def test_positive_sync_kickstart_repo( """ repo_id = target_sat.api_factory.enable_rhrepo_and_fetchid( basearch='x86_64', - org_id=function_entitlement_manifest_org.id, - product=constants.REPOS['kickstart'][distro]['product'], - reposet=constants.REPOS['kickstart'][distro]['reposet'], - repo=constants.REPOS['kickstart'][distro]['name'], - releasever=constants.REPOS['kickstart'][distro]['version'], + org_id=function_sca_manifest_org.id, + product=REPOS['kickstart'][distro]['product'], + reposet=REPOS['kickstart'][distro]['reposet'], + repo=REPOS['kickstart'][distro]['name'], + releasever=REPOS['kickstart'][distro]['version'], ) repo = target_sat.api.Repository(id=repo_id).read() - lce = target_sat.api.LifecycleEnvironment( - organization=function_entitlement_manifest_org - ).create() + lce = target_sat.api.LifecycleEnvironment(organization=function_sca_manifest_org).create() # Associate the lifecycle environment with the capsule module_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( data={'environment_id': lce.id} @@ -771,7 +793,7 @@ def test_positive_sync_kickstart_repo( # Create a content view with the repository cv = target_sat.api.ContentView( - organization=function_entitlement_manifest_org, repository=[repo] + organization=function_sca_manifest_org, repository=[repo] ).create() # Sync repository repo.sync(timeout='10m') @@ -784,26 +806,26 @@ def test_positive_sync_kickstart_repo( cvv = cv.version[-1].read() # Promote content view to lifecycle environment + timestamp = datetime.utcnow() cvv.promote(data={'environment_ids': lce.id}) - cvv = cvv.read() + module_capsule_configured.wait_for_sync(start_time=timestamp) + cvv = cvv.read() assert len(cvv.environment) == 2 - module_capsule_configured.wait_for_sync() - # Check for kickstart content on SAT and CAPS tail = ( - f'rhel/server/7/{constants.REPOS["kickstart"][distro]["version"]}/x86_64/kickstart' + f'rhel/server/7/{REPOS["kickstart"][distro]["version"]}/x86_64/kickstart' if distro == 'rhel7' - else f'{distro.split("_")[0]}/{constants.REPOS["kickstart"][distro]["version"]}/x86_64/baseos/kickstart' # noqa:E501 + else f'{distro.split("_")[0]}/{REPOS["kickstart"][distro]["version"]}/x86_64/baseos/kickstart' # noqa:E501 ) url_base = ( - f'pulp/content/{function_entitlement_manifest_org.label}/{lce.label}/{cv.label}/' + f'pulp/content/{function_sca_manifest_org.label}/{lce.label}/{cv.label}/' f'content/dist/{tail}' ) # Check kickstart specific files - for file in constants.KICKSTART_CONTENT: + for file in KICKSTART_CONTENT: sat_file = target_sat.md5_by_url(f'{target_sat.url}/{url_base}/{file}') caps_file = target_sat.md5_by_url(f'{module_capsule_configured.url}/{url_base}/{file}') assert sat_file == caps_file @@ -883,12 +905,13 @@ def test_positive_sync_container_repo_end_to_end( # Promote the latest CV version into capsule's LCE cvv = cv.version[-1].read() + timestamp = datetime.utcnow() cvv.promote(data={'environment_ids': function_lce.id}) + + module_capsule_configured.wait_for_sync(start_time=timestamp) cvv = cvv.read() assert len(cvv.environment) == 2 - module_capsule_configured.wait_for_sync() - # Pull the images from capsule to the content host repo_paths = [ ( @@ -898,7 +921,7 @@ def test_positive_sync_container_repo_end_to_end( for repo in repos ] - for con_client in constants.CONTAINER_CLIENTS: + for con_client in CONTAINER_CLIENTS: result = container_contenthost.execute( f'{con_client} login -u {settings.server.admin_username}' f' -p {settings.server.admin_password} {module_capsule_configured.hostname}' @@ -1001,10 +1024,12 @@ def test_positive_sync_collection_repo( assert function_lce_library.id in [capsule_lce['id'] for capsule_lce in result['results']] # Sync the repo + timestamp = datetime.utcnow() repo.sync(timeout=600) + + module_capsule_configured.wait_for_sync(start_time=timestamp) repo = repo.read() assert repo.content_counts['ansible_collection'] == 2 - module_capsule_configured.wait_for_sync() repo_path = repo.full_path.replace(target_sat.hostname, module_capsule_configured.hostname) coll_path = './collections' @@ -1059,7 +1084,7 @@ def test_positive_sync_file_repo( repo = target_sat.api.Repository( content_type='file', product=function_product, - url=constants.FAKE_FILE_LARGE_URL, + url=FAKE_FILE_LARGE_URL, ).create() repo.sync() @@ -1083,12 +1108,13 @@ def test_positive_sync_file_repo( # Promote the latest CV version into capsule's LCE cvv = cv.version[-1].read() + timestamp = datetime.utcnow() cvv.promote(data={'environment_ids': function_lce.id}) + + module_capsule_configured.wait_for_sync(start_time=timestamp) cvv = cvv.read() assert len(cvv.environment) == 2 - module_capsule_configured.wait_for_sync() - # Run one more sync, check for status (BZ#1985122) sync_status = module_capsule_configured.nailgun_capsule.content_sync() assert sync_status['result'] == 'success' @@ -1110,8 +1136,8 @@ def test_positive_sync_file_repo( ) sat_files = get_repo_files_by_url(sat_repo_url, extension='iso') caps_files = get_repo_files_by_url(caps_repo_url, extension='iso') - assert len(sat_files) == len(caps_files) == constants.FAKE_FILE_LARGE_COUNT + 1 - assert constants.FAKE_FILE_NEW_NAME in caps_files + assert len(sat_files) == len(caps_files) == FAKE_FILE_LARGE_COUNT + 1 + assert FAKE_FILE_NEW_NAME in caps_files assert sat_files == caps_files for file in sat_files: @@ -1122,7 +1148,7 @@ def test_positive_sync_file_repo( @pytest.mark.tier4 @pytest.mark.skip_if_not_set('capsule') def test_positive_sync_CV_to_multiple_LCEs( - self, target_sat, module_capsule_configured, module_manifest_org + self, target_sat, module_capsule_configured, module_sca_manifest_org ): """Synchronize a CV to multiple LCEs at the same time. All sync tasks should succeed. @@ -1147,19 +1173,19 @@ def test_positive_sync_CV_to_multiple_LCEs( # Sync a repository to the Satellite. repo_id = target_sat.api_factory.enable_rhrepo_and_fetchid( basearch='x86_64', - org_id=module_manifest_org.id, - product=constants.PRDS['rhel'], - repo=constants.REPOS['rhel7_extra']['name'], - reposet=constants.REPOSET['rhel7_extra'], + org_id=module_sca_manifest_org.id, + product=PRDS['rhel'], + repo=REPOS['rhel7_extra']['name'], + reposet=REPOSET['rhel7_extra'], releasever=None, ) repo = target_sat.api.Repository(id=repo_id).read() repo.sync() # Create two LCEs, assign them to the Capsule. - lce1 = target_sat.api.LifecycleEnvironment(organization=module_manifest_org).create() + lce1 = target_sat.api.LifecycleEnvironment(organization=module_sca_manifest_org).create() lce2 = target_sat.api.LifecycleEnvironment( - organization=module_manifest_org, prior=lce1 + organization=module_sca_manifest_org, prior=lce1 ).create() module_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( data={'environment_id': [lce1.id, lce2.id]} @@ -1171,7 +1197,7 @@ def test_positive_sync_CV_to_multiple_LCEs( # Create a Content View, add the repository and publish it. cv = target_sat.api.ContentView( - organization=module_manifest_org, repository=[repo] + organization=module_sca_manifest_org, repository=[repo] ).create() cv.publish() cv = cv.read() @@ -1179,16 +1205,20 @@ def test_positive_sync_CV_to_multiple_LCEs( # Promote the CV to both Capsule's LCEs without waiting for Capsule sync task completion. cvv = cv.version[-1].read() + assert len(cvv.environment) == 1 + timestamp = datetime.utcnow() cvv.promote(data={'environment_ids': lce1.id}) + + module_capsule_configured.wait_for_sync(start_time=timestamp) cvv = cvv.read() assert len(cvv.environment) == 2 + timestamp = datetime.utcnow() cvv.promote(data={'environment_ids': lce2.id}) + + module_capsule_configured.wait_for_sync(start_time=timestamp) cvv = cvv.read() assert len(cvv.environment) == 3 - # Check all sync tasks finished without errors. - module_capsule_configured.wait_for_sync() - @pytest.mark.tier4 @pytest.mark.skip_if_not_set('capsule') def test_positive_capsule_sync_status_persists( @@ -1231,7 +1261,8 @@ def test_positive_capsule_sync_status_persists( cvv = cv.version[-1].read() timestamp = datetime.utcnow() cvv.promote(data={'environment_ids': function_lce.id}) - module_capsule_configured.wait_for_sync() + + module_capsule_configured.wait_for_sync(start_time=timestamp) # Delete all capsule sync tasks so that we fall back for audits. task_result = target_sat.execute( @@ -1261,7 +1292,7 @@ def test_positive_remove_capsule_orphans( target_sat, pytestconfig, capsule_configured, - function_entitlement_manifest_org, + function_sca_manifest_org, function_lce_library, ): """Synchronize RPM content to the capsule, disassociate the capsule form the content @@ -1294,10 +1325,10 @@ def test_positive_remove_capsule_orphans( # Enable RHST repo and sync it to the Library LCE. repo_id = target_sat.api_factory.enable_rhrepo_and_fetchid( basearch='x86_64', - org_id=function_entitlement_manifest_org.id, - product=constants.REPOS['rhst8']['product'], - repo=constants.REPOS['rhst8']['name'], - reposet=constants.REPOSET['rhst8'], + org_id=function_sca_manifest_org.id, + product=REPOS['rhst8']['product'], + repo=REPOS['rhst8']['name'], + reposet=REPOSET['rhst8'], ) repo = target_sat.api.Repository(id=repo_id).read() repo.sync() @@ -1330,13 +1361,20 @@ def test_positive_remove_capsule_orphans( sync_status = capsule_configured.nailgun_capsule.content_sync() assert sync_status['result'] == 'success', 'Capsule sync task failed.' + # datetime string (local time) to search for proper task. + timestamp = (datetime.now().replace(microsecond=0) - timedelta(seconds=1)).strftime( + '%B %d, %Y at %I:%M:%S %p' + ) # Run orphan cleanup for the capsule. target_sat.execute( 'foreman-rake katello:delete_orphaned_content RAILS_ENV=production ' f'SMART_PROXY_ID={capsule_configured.nailgun_capsule.id}' ) target_sat.wait_for_tasks( - search_query=('label = Actions::Katello::OrphanCleanup::RemoveOrphans'), + search_query=( + 'label = Actions::Katello::OrphanCleanup::RemoveOrphans' + f' and started_at >= "{timestamp}"' + ), search_rate=5, max_tries=10, ) @@ -1387,7 +1425,7 @@ def test_positive_capsule_sync_openstack_container_repos( content_type='docker', docker_upstream_name=ups_name, product=function_product, - url=constants.RH_CONTAINER_REGISTRY_HUB, + url=RH_CONTAINER_REGISTRY_HUB, upstream_username=settings.subscription.rhn_username, upstream_password=settings.subscription.rhn_password, ).create() @@ -1410,8 +1448,9 @@ def test_positive_capsule_sync_openstack_container_repos( # Promote the latest CV version into capsule's LCE cvv = cv.version[-1].read() + timestamp = datetime.utcnow() cvv.promote(data={'environment_ids': function_lce.id}) + + module_capsule_configured.wait_for_sync(start_time=timestamp) cvv = cvv.read() assert len(cvv.environment) == 2 - - module_capsule_configured.wait_for_sync() From 9d447e7331e112f3fe78ced81c6376731bd87d56 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 19 Mar 2024 00:41:18 -0400 Subject: [PATCH 541/586] [6.14.z] Bump dynaconf[vault] from 3.2.4 to 3.2.5 (#14440) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fde313e4410..4826ba6ecb6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ betelgeuse==1.11.0 cryptography==42.0.5 deepdiff==6.7.1 docker==7.0.0 # Temporary until Broker is back on PyPi -dynaconf[vault]==3.2.4 +dynaconf[vault]==3.2.5 fauxfactory==3.1.0 jinja2==3.1.3 manifester==0.0.14 From 82fe99bc691f103b01c7540bcec213b76f6aad06 Mon Sep 17 00:00:00 2001 From: Shweta Singh Date: Tue, 19 Mar 2024 14:55:22 +0530 Subject: [PATCH 542/586] Fix module_hostgroup fixture (#14444) --- tests/foreman/api/test_discoveryrule.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/foreman/api/test_discoveryrule.py b/tests/foreman/api/test_discoveryrule.py index 5e0666c9b2d..f5dffd5be14 100644 --- a/tests/foreman/api/test_discoveryrule.py +++ b/tests/foreman/api/test_discoveryrule.py @@ -21,6 +21,7 @@ @pytest.fixture(scope='module') def module_hostgroup(module_org, module_target_sat): module_hostgroup = module_target_sat.api.HostGroup(organization=[module_org]).create() + yield module_hostgroup module_hostgroup.delete() From 10412af95fed0e7de5260a91b28fa9cca8ad4369 Mon Sep 17 00:00:00 2001 From: Peter Ondrejka Date: Tue, 19 Mar 2024 10:33:30 +0100 Subject: [PATCH 543/586] [6.14] addFinalizer in test body (#14036) (#14445) addFinalizer in test body (#14036) * reordered addFinalizer in test body * Update tests/foreman/cli/test_errata.py * Update tests/foreman/api/test_discoveryrule.py * Update tests/foreman/cli/test_errata.py * Update tests/foreman/destructive/test_capsule_loadbalancer.py --------- Co-authored-by: Gaurav Talreja --- .../api/test_computeresource_libvirt.py | 13 +-- tests/foreman/api/test_discoveryrule.py | 13 +-- tests/foreman/api/test_http_proxy.py | 5 + tests/foreman/api/test_parameters.py | 9 +- tests/foreman/cli/test_classparameters.py | 2 +- tests/foreman/cli/test_computeresource_osp.py | 2 +- tests/foreman/cli/test_discoveredhost.py | 8 +- tests/foreman/cli/test_errata.py | 14 ++- tests/foreman/cli/test_hammer.py | 13 +-- tests/foreman/cli/test_satellitesync.py | 2 +- tests/foreman/cli/test_subnet.py | 4 +- .../destructive/test_capsule_loadbalancer.py | 8 +- tests/foreman/maintain/test_advanced.py | 63 +++++++------ tests/foreman/maintain/test_health.py | 91 ++++++++++--------- .../foreman/maintain/test_maintenance_mode.py | 9 +- tests/foreman/maintain/test_packages.py | 28 +++--- tests/foreman/ui/test_ansible.py | 13 +-- tests/foreman/ui/test_computeresource_gce.py | 31 ++++--- .../ui/test_computeresource_libvirt.py | 4 +- tests/upgrades/test_contentview.py | 2 +- tests/upgrades/test_host.py | 2 +- tests/upgrades/test_provisioningtemplate.py | 2 +- tests/upgrades/test_subscription.py | 2 +- tests/upgrades/test_usergroup.py | 4 +- 24 files changed, 174 insertions(+), 170 deletions(-) diff --git a/tests/foreman/api/test_computeresource_libvirt.py b/tests/foreman/api/test_computeresource_libvirt.py index 5189af9d445..ad95fb9f202 100644 --- a/tests/foreman/api/test_computeresource_libvirt.py +++ b/tests/foreman/api/test_computeresource_libvirt.py @@ -113,9 +113,9 @@ def test_positive_create_with_name_description( location=[module_location], url=LIBVIRT_URL, ).create() + request.addfinalizer(compresource.delete) assert compresource.name == name assert compresource.description == name - request.addfinalizer(compresource.delete) @pytest.mark.tier2 @@ -134,9 +134,9 @@ def test_positive_create_with_orgs_and_locs(request, module_target_sat): compresource = module_target_sat.api.LibvirtComputeResource( location=locs, organization=orgs, url=LIBVIRT_URL ).create() + request.addfinalizer(compresource.delete) assert {org.name for org in orgs} == {org.read().name for org in compresource.organization} assert {loc.name for loc in locs} == {loc.read().name for loc in compresource.location} - request.addfinalizer(compresource.delete) @pytest.mark.tier2 @@ -175,8 +175,8 @@ def test_negative_create_with_same_name(request, module_target_sat, module_org, cr = module_target_sat.api.LibvirtComputeResource( location=[module_location], name=name, organization=[module_org], url=LIBVIRT_URL ).create() - assert cr.name == name request.addfinalizer(cr.delete) + assert cr.name == name with pytest.raises(HTTPError): module_target_sat.api.LibvirtComputeResource( name=name, @@ -245,19 +245,16 @@ def test_negative_update_same_name(request, module_target_sat, module_org, modul compresource = module_target_sat.api.LibvirtComputeResource( location=[module_location], name=name, organization=[module_org], url=LIBVIRT_URL ).create() + request.addfinalizer(compresource.delete) new_compresource = module_target_sat.api.LibvirtComputeResource( location=[module_location], organization=[module_org], url=LIBVIRT_URL ).create() + request.addfinalizer(new_compresource.delete) new_compresource.name = name with pytest.raises(HTTPError): new_compresource.update(['name']) assert new_compresource.read().name != name - @request.addfinalizer - def _finalize(): - compresource.delete() - new_compresource.delete() - @pytest.mark.tier2 @pytest.mark.parametrize('url', **parametrized({'random': gen_string('alpha'), 'empty': ''})) diff --git a/tests/foreman/api/test_discoveryrule.py b/tests/foreman/api/test_discoveryrule.py index f5dffd5be14..3d1c1f4bf48 100644 --- a/tests/foreman/api/test_discoveryrule.py +++ b/tests/foreman/api/test_discoveryrule.py @@ -171,8 +171,11 @@ def test_positive_multi_provision_with_rule_limit( :CaseImportance: High """ + discovered_host1 = module_target_sat.api_factory.create_discovered_host() + request.addfinalizer(module_target_sat.api.Host(id=discovered_host1['id']).delete) discovered_host2 = module_target_sat.api_factory.create_discovered_host() + request.addfinalizer(module_target_sat.api.DiscoveredHost(id=discovered_host2['id']).delete) rule = module_target_sat.api.DiscoveryRule( max_count=1, hostgroup=module_discovery_hostgroup, @@ -181,14 +184,6 @@ def test_positive_multi_provision_with_rule_limit( organization=[discovery_org], priority=1000, ).create() + request.addfinalizer(rule.delete) result = module_target_sat.api.DiscoveredHost().auto_provision_all() assert '1 discovered hosts were provisioned' in result['message'] - - # Delete discovery rule - @request.addfinalizer - def _finalize(): - rule.delete() - module_target_sat.api.Host(id=discovered_host1['id']).delete() - module_target_sat.api.DiscoveredHost(id=discovered_host2['id']).delete() - with pytest.raises(HTTPError): - rule.read() diff --git a/tests/foreman/api/test_http_proxy.py b/tests/foreman/api/test_http_proxy.py index d69fc7e61d5..51ceb485361 100644 --- a/tests/foreman/api/test_http_proxy.py +++ b/tests/foreman/api/test_http_proxy.py @@ -295,6 +295,11 @@ def test_positive_sync_proxy_with_certificate(request, target_sat, module_org, m :customerscenario: true """ + + @request.addfinalizer + def _finalize(): + target_sat.custom_certs_cleanup() + # Cleanup any existing certs that may conflict target_sat.custom_certs_cleanup() proxy_host = settings.http_proxy.auth_proxy_url.replace('http://', '').replace(':3128', '') diff --git a/tests/foreman/api/test_parameters.py b/tests/foreman/api/test_parameters.py index 1e415d15dc8..5d70374a546 100644 --- a/tests/foreman/api/test_parameters.py +++ b/tests/foreman/api/test_parameters.py @@ -39,7 +39,9 @@ def test_positive_parameter_precedence_impact( param_value = gen_string('alpha') cp = module_target_sat.api.CommonParameter(name=param_name, value=param_value).create() + request.addfinalizer(cp.delete) host = module_target_sat.api.Host(organization=module_org, location=module_location).create() + request.addfinalizer(host.delete) result = [res for res in host.all_parameters if res['name'] == param_name] assert result[0]['name'] == param_name assert result[0]['associated_type'] == 'global' @@ -48,6 +50,7 @@ def test_positive_parameter_precedence_impact( organization=[module_org], group_parameters_attributes=[{'name': param_name, 'value': param_value}], ).create() + request.addfinalizer(hg.delete) host.hostgroup = hg host = host.update(['hostgroup']) result = [res for res in host.all_parameters if res['name'] == param_name] @@ -55,12 +58,6 @@ def test_positive_parameter_precedence_impact( assert result[0]['associated_type'] != 'global' assert result[0]['associated_type'] == 'host group' - @request.addfinalizer - def _finalize(): - host.delete() - hg.delete() - cp.delete() - host.host_parameters_attributes = [{'name': param_name, 'value': param_value}] host = host.update(['host_parameters_attributes']) result = [res for res in host.all_parameters if res['name'] == param_name] diff --git a/tests/foreman/cli/test_classparameters.py b/tests/foreman/cli/test_classparameters.py index a4e75c1f76d..022ceed0aac 100644 --- a/tests/foreman/cli/test_classparameters.py +++ b/tests/foreman/cli/test_classparameters.py @@ -86,8 +86,8 @@ def test_positive_list( location=module_puppet_loc.id, environment=module_puppet['env'].name, ).create() - host.add_puppetclass(data={'puppetclass_id': module_puppet['class']['id']}) request.addfinalizer(host.delete) + host.add_puppetclass(data={'puppetclass_id': module_puppet['class']['id']}) hostgroup = session_puppet_enabled_sat.cli_factory.hostgroup( { 'puppet-environment-id': module_puppet['env'].id, diff --git a/tests/foreman/cli/test_computeresource_osp.py b/tests/foreman/cli/test_computeresource_osp.py index d88daaf1ad8..ff6779056ae 100644 --- a/tests/foreman/cli/test_computeresource_osp.py +++ b/tests/foreman/cli/test_computeresource_osp.py @@ -76,6 +76,7 @@ def test_crud_and_duplicate_name(self, request, id_type, osp_version, target_sat 'url': osp_version, } ) + request.addfinalizer(lambda: self.cr_cleanup(compute_resource['id'], id_type, target_sat)) assert compute_resource['name'] == name assert target_sat.cli.ComputeResource.exists(search=(id_type, compute_resource[id_type])) @@ -102,7 +103,6 @@ def test_crud_and_duplicate_name(self, request, id_type, osp_version, target_sat else: compute_resource = target_sat.cli.ComputeResource.info({'id': compute_resource['id']}) assert new_name == compute_resource['name'] - request.addfinalizer(lambda: self.cr_cleanup(compute_resource['id'], id_type, target_sat)) @pytest.mark.tier3 def test_negative_create_osp_with_url(self, target_sat): diff --git a/tests/foreman/cli/test_discoveredhost.py b/tests/foreman/cli/test_discoveredhost.py index 2cd1297224e..e578f35e1ac 100644 --- a/tests/foreman/cli/test_discoveredhost.py +++ b/tests/foreman/cli/test_discoveredhost.py @@ -70,10 +70,8 @@ def test_rhel_pxe_discovery_provisioning( assert 'Host created' in result[0]['message'] host = sat.api.Host().search(query={"search": f'id={discovered_host.id}'})[0] - assert host - - # teardown request.addfinalizer(lambda: sat.provisioning_cleanup(host.name)) + assert host wait_for( lambda: host.read().build_status_label != 'Pending installation', @@ -131,10 +129,8 @@ def test_rhel_pxeless_discovery_provisioning( ) assert 'Host created' in result[0]['message'] host = sat.api.Host().search(query={"search": f'id={discovered_host.id}'})[0] - assert host - - # teardown request.addfinalizer(lambda: sat.provisioning_cleanup(host.name)) + assert host wait_for( lambda: host.read().build_status_label != 'Pending installation', diff --git a/tests/foreman/cli/test_errata.py b/tests/foreman/cli/test_errata.py index b32faed5020..52eff37bda4 100644 --- a/tests/foreman/cli/test_errata.py +++ b/tests/foreman/cli/test_errata.py @@ -794,15 +794,15 @@ def test_positive_list_affected_chosts_by_erratum_restrict_flag( } ) - @request.addfinalizer - def cleanup(): - cv_filter_cleanup( + request.addfinalizer( + lambda: cv_filter_cleanup( target_sat, cv_filter['filter-id'], module_cv, module_entitlement_manifest_org, module_lce, ) + ) # Make rule to hide the RPM that creates the need for the installable erratum target_sat.cli_factory.content_view_filter_rule( @@ -966,17 +966,15 @@ def test_host_errata_search_commands( 'inclusion': 'false', } ) - - @request.addfinalizer - def cleanup(): - cv_filter_cleanup( + request.addfinalizer( + lambda: cv_filter_cleanup( target_sat, cv_filter['filter-id'], module_cv, module_entitlement_manifest_org, module_lce, ) - + ) # Make rule to exclude the specified bugfix package target_sat.cli_factory.content_view_filter_rule( { diff --git a/tests/foreman/cli/test_hammer.py b/tests/foreman/cli/test_hammer.py index c795352c5ed..aa3bfcf4aeb 100644 --- a/tests/foreman/cli/test_hammer.py +++ b/tests/foreman/cli/test_hammer.py @@ -136,6 +136,13 @@ def test_positive_disable_hammer_defaults(request, function_product, target_sat) :BZ: 1640644, 1368173 """ + + @request.addfinalizer + def _finalize(): + target_sat.cli.Defaults.delete({'param-name': 'organization_id'}) + result = target_sat.execute('hammer defaults list') + assert str(function_product.organization.id) not in result.stdout + target_sat.cli.Defaults.add( {'param-name': 'organization_id', 'param-value': function_product.organization.id} ) @@ -154,12 +161,6 @@ def test_positive_disable_hammer_defaults(request, function_product, target_sat) assert result.status == 0 assert function_product.name in result.stdout - @request.addfinalizer - def _finalize(): - target_sat.cli.Defaults.delete({'param-name': 'organization_id'}) - result = target_sat.execute('hammer defaults list') - assert str(function_product.organization.id) not in result.stdout - @pytest.mark.upgrade def test_positive_check_debug_log_levels(target_sat): diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index 5e793916136..d4b2b883c44 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -2270,6 +2270,7 @@ def test_positive_custom_cdn_with_credential( meta_file = 'metadata.json' crt_file = 'source.crt' pub_dir = '/var/www/html/pub/repos' + request.addfinalizer(lambda: target_sat.execute(f'rm -rf {pub_dir}')) # Export the repository in syncable format and move it # to /var/www/html/pub/repos to mimic custom CDN. @@ -2286,7 +2287,6 @@ def test_positive_custom_cdn_with_credential( exp_dir = exp_dir[0].replace(meta_file, '') assert target_sat.execute(f'mv {exp_dir} {pub_dir}').status == 0 - request.addfinalizer(lambda: target_sat.execute(f'rm -rf {pub_dir}')) target_sat.execute(f'semanage fcontext -a -t httpd_sys_content_t "{pub_dir}(/.*)?"') target_sat.execute(f'restorecon -R {pub_dir}') diff --git a/tests/foreman/cli/test_subnet.py b/tests/foreman/cli/test_subnet.py index 12272ed57c2..74a6e1727fc 100644 --- a/tests/foreman/cli/test_subnet.py +++ b/tests/foreman/cli/test_subnet.py @@ -199,8 +199,8 @@ def test_negative_update_attributes(request, options, module_target_sat): :CaseImportance: Medium """ subnet = module_target_sat.cli_factory.make_subnet() - options['id'] = subnet['id'] request.addfinalizer(lambda: module_target_sat.cli.Subnet.delete({'id': subnet['id']})) + options['id'] = subnet['id'] with pytest.raises(CLIReturnCodeError, match='Could not update the subnet:'): module_target_sat.cli.Subnet.update(options) # check - subnet is not updated @@ -223,8 +223,8 @@ def test_negative_update_address_pool(request, options, module_target_sat): :CaseImportance: Medium """ subnet = module_target_sat.cli_factory.make_subnet() - opts = {'id': subnet['id']} request.addfinalizer(lambda: module_target_sat.cli.Subnet.delete({'id': subnet['id']})) + opts = {'id': subnet['id']} # generate pool range from network address for key, val in options.items(): opts[key] = re.sub(r'\d+$', str(val), subnet['network-addr']) diff --git a/tests/foreman/destructive/test_capsule_loadbalancer.py b/tests/foreman/destructive/test_capsule_loadbalancer.py index 16b6450d254..9f94f13402c 100644 --- a/tests/foreman/destructive/test_capsule_loadbalancer.py +++ b/tests/foreman/destructive/test_capsule_loadbalancer.py @@ -191,6 +191,7 @@ def test_loadbalancer_install_package( registration. """ + # Register content host result = rhel7_contenthost.register( org=module_org, @@ -219,6 +220,9 @@ def test_loadbalancer_install_package( if loadbalancer_setup['setup_capsules']['capsule_1'].hostname in result.stdout else loadbalancer_setup['setup_capsules']['capsule_2'] ) + request.addfinalizer( + lambda: registered_to_capsule.power_control(state=VmState.RUNNING, ensure=True) + ) # Remove the packages from the client result = rhel7_contenthost.execute('yum remove -y tree') @@ -231,10 +235,6 @@ def test_loadbalancer_install_package( result = rhel7_contenthost.execute('yum install -y tree') assert result.status == 0 - @request.addfinalizer - def _finalize(): - registered_to_capsule.power_control(state=VmState.RUNNING, ensure=True) - @pytest.mark.rhel_ver_match('[^6]') @pytest.mark.tier1 diff --git a/tests/foreman/maintain/test_advanced.py b/tests/foreman/maintain/test_advanced.py index 6a66c9aefcf..68b00e5c071 100644 --- a/tests/foreman/maintain/test_advanced.py +++ b/tests/foreman/maintain/test_advanced.py @@ -75,6 +75,22 @@ def test_positive_advanced_run_hammer_setup(request, sat_maintain): :BZ: 1830355 """ + + @request.addfinalizer + def _finalize(): + result = sat_maintain.execute( + f'hammer -u admin -p admin user update --login admin --password {default_admin_pass}' + ) + assert result.status == 0 + # Make default admin creds available in MAINTAIN_HAMMER_YML + assert sat_maintain.cli.Advanced.run_hammer_setup().status == 0 + # Make sure default password available in MAINTAIN_HAMMER_YML + result = sat_maintain.execute( + f"grep -i ':password: {default_admin_pass}' {MAINTAIN_HAMMER_YML}" + ) + assert result.status == 0 + assert default_admin_pass in result.stdout + default_admin_pass = settings.server.admin_password result = sat_maintain.execute( f'hammer -u admin -p {default_admin_pass} user update --login admin --password admin' @@ -100,21 +116,6 @@ def test_positive_advanced_run_hammer_setup(request, sat_maintain): assert result.status == 0 assert 'admin' in result.stdout - @request.addfinalizer - def _finalize(): - result = sat_maintain.execute( - f'hammer -u admin -p admin user update --login admin --password {default_admin_pass}' - ) - assert result.status == 0 - # Make default admin creds available in MAINTAIN_HAMMER_YML - assert sat_maintain.cli.Advanced.run_hammer_setup().status == 0 - # Make sure default password available in MAINTAIN_HAMMER_YML - result = sat_maintain.execute( - f"grep -i ':password: {default_admin_pass}' {MAINTAIN_HAMMER_YML}" - ) - assert result.status == 0 - assert default_admin_pass in result.stdout - @pytest.mark.e2e @pytest.mark.upgrade @@ -131,6 +132,12 @@ def test_positive_advanced_run_packages(request, sat_maintain): :expectedresults: packages should install/downgrade/check-update/update. """ + + @request.addfinalizer + def _finalize(): + assert sat_maintain.execute('dnf remove -y walrus').status == 0 + sat_maintain.execute('rm -rf /etc/yum.repos.d/custom_repo.repo') + # Setup custom_repo and install walrus package sat_maintain.create_custom_repos(custom_repo=settings.repos.yum_0.url) result = sat_maintain.cli.Advanced.run_packages_install( @@ -160,11 +167,6 @@ def test_positive_advanced_run_packages(request, sat_maintain): assert result.status == 0 assert 'walrus-5.21-1' in result.stdout - @request.addfinalizer - def _finalize(): - assert sat_maintain.execute('dnf remove -y walrus').status == 0 - sat_maintain.execute('rm -rf /etc/yum.repos.d/custom_repo.repo') - @pytest.mark.parametrize( 'tasks_state', @@ -250,6 +252,7 @@ def test_positive_sync_plan_with_hammer_defaults(request, sat_maintain, module_o :customerscenario: true """ + sat_maintain.cli.Defaults.add({'param-name': 'organization_id', 'param-value': module_org.id}) sync_plans = [] @@ -258,16 +261,6 @@ def test_positive_sync_plan_with_hammer_defaults(request, sat_maintain, module_o sat_maintain.api.SyncPlan(enabled=True, name=name, organization=module_org).create() ) - result = sat_maintain.cli.Advanced.run_sync_plans_disable() - assert 'FAIL' not in result.stdout - assert result.status == 0 - - sync_plans[0].delete() - - result = sat_maintain.cli.Advanced.run_sync_plans_enable() - assert 'FAIL' not in result.stdout - assert result.status == 0 - @request.addfinalizer def _finalize(): sat_maintain.cli.Defaults.delete({'param-name': 'organization_id'}) @@ -278,6 +271,16 @@ def _finalize(): if sync_plan: sync_plans[0].delete() + result = sat_maintain.cli.Advanced.run_sync_plans_disable() + assert 'FAIL' not in result.stdout + assert result.status == 0 + + sync_plans[0].delete() + + result = sat_maintain.cli.Advanced.run_sync_plans_enable() + assert 'FAIL' not in result.stdout + assert result.status == 0 + @pytest.mark.e2e def test_positive_satellite_repositories_setup(sat_maintain): diff --git a/tests/foreman/maintain/test_health.py b/tests/foreman/maintain/test_health.py index f58ee15b644..e8bc468d410 100644 --- a/tests/foreman/maintain/test_health.py +++ b/tests/foreman/maintain/test_health.py @@ -201,6 +201,13 @@ def test_negative_health_check_upstream_repository(sat_maintain, request): :expectedresults: check-upstream-repository health check should fail. """ + + @request.addfinalizer + def _finalize(): + for name in upstream_url: + sat_maintain.execute(f'rm -fr /etc/yum.repos.d/{name}.repo') + sat_maintain.execute('dnf clean all') + for name, url in upstream_url.items(): sat_maintain.create_custom_repos(**{name: url}) result = sat_maintain.cli.Health.check( @@ -216,12 +223,6 @@ def test_negative_health_check_upstream_repository(sat_maintain, request): elif name in ['foreman_repo', 'puppet_repo']: assert 'enabled=0' in result.stdout - @request.addfinalizer - def _finalize(): - for name in upstream_url: - sat_maintain.execute(f'rm -fr /etc/yum.repos.d/{name}.repo') - sat_maintain.execute('dnf clean all') - def test_positive_health_check_available_space(sat_maintain): """Verify available-space check @@ -260,15 +261,16 @@ def test_positive_hammer_defaults_set(sat_maintain, request): :customerscenario: true """ - sat_maintain.cli.Defaults.add({'param-name': 'organization_id', 'param-value': 1}) - result = sat_maintain.cli.Health.check(options={'assumeyes': True}) - assert result.status == 0 - assert 'FAIL' not in result.stdout @request.addfinalizer def _finalize(): sat_maintain.cli.Defaults.delete({'param-name': 'organization_id'}) + sat_maintain.cli.Defaults.add({'param-name': 'organization_id', 'param-value': 1}) + result = sat_maintain.cli.Health.check(options={'assumeyes': True}) + assert result.status == 0 + assert 'FAIL' not in result.stdout + @pytest.mark.include_capsule def test_positive_health_check_hotfix_installed(sat_maintain, request): @@ -288,6 +290,13 @@ def test_positive_health_check_hotfix_installed(sat_maintain, request): :expectedresults: check-hotfix-installed check should detect modified file and installed hotfix. """ + + @request.addfinalizer + def _finalize(): + sat_maintain.execute('rm -fr /etc/yum.repos.d/custom_repo.repo') + sat_maintain.execute('dnf remove -y hotfix-package') + assert sat_maintain.execute(f'sed -i "/#modifying_file/d" {fpath.stdout}').status == 0 + # Verify check-hotfix-installed without hotfix package. result = sat_maintain.cli.Health.check(options={'label': 'check-hotfix-installed'}) assert result.status == 0 @@ -307,12 +316,6 @@ def test_positive_health_check_hotfix_installed(sat_maintain, request): assert 'WARNING' in result.stdout assert 'hotfix-package' in result.stdout - @request.addfinalizer - def _finalize(): - sat_maintain.execute('rm -fr /etc/yum.repos.d/custom_repo.repo') - sat_maintain.execute('dnf remove -y hotfix-package') - assert sat_maintain.execute(f'sed -i "/#modifying_file/d" {fpath.stdout}').status == 0 - @pytest.mark.include_capsule def test_positive_health_check_validate_yum_config(sat_maintain): @@ -365,6 +368,11 @@ def test_negative_health_check_epel_repository(request, sat_maintain): :expectedresults: check-non-redhat-repository health check should fail. """ + + @request.addfinalizer + def _finalize(): + assert sat_maintain.execute('dnf remove -y epel-release').status == 0 + epel_repo = 'https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm' sat_maintain.execute(f'dnf install -y {epel_repo}') result = sat_maintain.cli.Health.check(options={'label': 'check-non-redhat-repository'}) @@ -372,10 +380,6 @@ def test_negative_health_check_epel_repository(request, sat_maintain): assert result.status == 1 assert 'FAIL' in result.stdout - @request.addfinalizer - def _finalize(): - assert sat_maintain.execute('dnf remove -y epel-release').status == 0 - def test_positive_health_check_old_foreman_tasks(sat_maintain): """Verify check-old-foreman-tasks. @@ -471,6 +475,14 @@ def test_positive_health_check_tftp_storage(sat_maintain, request): :expectedresults: check-tftp-storage health check should pass. """ + + @request.addfinalizer + def _finalize(): + sat_maintain.cli.Settings.set({'name': 'token_duration', 'value': '360'}) + assert ( + sat_maintain.cli.Settings.list({'search': 'name=token_duration'})[0]['value'] == '360' + ) + sat_maintain.cli.Settings.set({'name': 'token_duration', 'value': '2'}) assert sat_maintain.cli.Settings.list({'search': 'name=token_duration'})[0]['value'] == '2' files_to_delete = [ @@ -504,13 +516,6 @@ def test_positive_health_check_tftp_storage(sat_maintain, request): assert result.status == 0 assert 'FAIL' not in result.stdout - @request.addfinalizer - def _finalize(): - sat_maintain.cli.Settings.set({'name': 'token_duration', 'value': '360'}) - assert ( - sat_maintain.cli.Settings.list({'search': 'name=token_duration'})[0]['value'] == '360' - ) - @pytest.mark.include_capsule def test_positive_health_check_env_proxy(sat_maintain): @@ -661,10 +666,20 @@ def test_positive_health_check_corrupted_roles(sat_maintain, request): :BZ: 1703041, 1908846 """ - # Check the filter created to verify the role, resource type, and permissions assigned. role_name = 'test_role' resource_type = gen_string("alpha") sat_maintain.cli.Role.create(options={'name': role_name}) + + @request.addfinalizer + def _finalize(): + resource_type = r"'\''Host'\''" + sat_maintain.execute( + f'''sudo su - postgres -c "psql -d foreman -c 'UPDATE permissions SET + resource_type = {resource_type} WHERE name = {permission_name};'"''' + ) + sat_maintain.cli.Role.delete(options={'name': role_name}) + + # Check the filter created to verify the role, resource type, and permissions assigned. sat_maintain.cli.Filter.create( options={'role': role_name, 'permissions': ['view_hosts', 'console_hosts']} ) @@ -686,15 +701,6 @@ def test_positive_health_check_corrupted_roles(sat_maintain, request): result = sat_maintain.cli.Filter.list(options={'search': role_name}, output_format='yaml') assert result.count('Id') == 4 - @request.addfinalizer - def _finalize(): - resource_type = r"'\''Host'\''" - sat_maintain.execute( - f'''sudo su - postgres -c "psql -d foreman -c 'UPDATE permissions SET - resource_type = {resource_type} WHERE name = {permission_name};'"''' - ) - sat_maintain.cli.Role.delete(options={'name': role_name}) - @pytest.mark.include_capsule def test_positive_health_check_non_rh_packages(sat_maintain, request): @@ -719,6 +725,12 @@ def test_positive_health_check_non_rh_packages(sat_maintain, request): :CaseImportance: High """ + + @request.addfinalizer + def _finalize(): + assert sat_maintain.execute('dnf remove -y walrus').status == 0 + assert sat_maintain.execute('rm -fr /etc/yum.repos.d/custom_repo.repo').status == 0 + sat_maintain.create_custom_repos(custom_repo=settings.repos.yum_0.url) assert ( sat_maintain.cli.Packages.install(packages='walrus', options={'assumeyes': True}).status @@ -730,11 +742,6 @@ def test_positive_health_check_non_rh_packages(sat_maintain, request): assert result.status == 78 assert 'WARNING' in result.stdout - @request.addfinalizer - def _finalize(): - assert sat_maintain.execute('dnf remove -y walrus').status == 0 - assert sat_maintain.execute('rm -fr /etc/yum.repos.d/custom_repo.repo').status == 0 - def test_positive_health_check_duplicate_permissions(sat_maintain): """Verify duplicate-permissions check diff --git a/tests/foreman/maintain/test_maintenance_mode.py b/tests/foreman/maintain/test_maintenance_mode.py index 50d130ac55a..b8d5024e1b1 100644 --- a/tests/foreman/maintain/test_maintenance_mode.py +++ b/tests/foreman/maintain/test_maintenance_mode.py @@ -47,6 +47,11 @@ def test_positive_maintenance_mode(request, sat_maintain, setup_sync_plan): to disable/enable sync-plan, stop/start crond.service and is able to add FOREMAN_MAINTAIN_TABLE rule in nftables. """ + + @request.addfinalizer + def _finalize(): + assert sat_maintain.cli.MaintenanceMode.stop().status == 0 + enable_sync_ids = setup_sync_plan data_yml_path = '/var/lib/foreman-maintain/data.yml' local_data_yml_path = f'{robottelo_tmp_dir}/data.yml' @@ -142,7 +147,3 @@ def test_positive_maintenance_mode(request, sat_maintain, setup_sync_plan): assert 'OK' in result.stdout assert result.status == 1 assert 'Maintenance mode is Off' in result.stdout - - @request.addfinalizer - def _finalize(): - assert sat_maintain.cli.MaintenanceMode.stop().status == 0 diff --git a/tests/foreman/maintain/test_packages.py b/tests/foreman/maintain/test_packages.py index a4c56eac531..f0ae22d6d10 100644 --- a/tests/foreman/maintain/test_packages.py +++ b/tests/foreman/maintain/test_packages.py @@ -160,6 +160,15 @@ def test_positive_fm_packages_install(request, sat_maintain): :expectedresults: Packages get install/update when lock/unlocked. """ + + @request.addfinalizer + def _finalize(): + assert sat_maintain.execute('dnf remove -y zsh').status == 0 + if sat_maintain.__class__.__name__ == 'Satellite': + result = sat_maintain.install(InstallerCommand('lock-package-versions')) + assert result.status == 0 + assert 'Success!' in result.stdout + # Test whether packages are locked or not result = sat_maintain.install(InstallerCommand('lock-package-versions')) assert result.status == 0 @@ -216,14 +225,6 @@ def test_positive_fm_packages_install(request, sat_maintain): assert result.status == 0 assert 'Use foreman-maintain packages install/update ' not in result.stdout - @request.addfinalizer - def _finalize(): - assert sat_maintain.execute('dnf remove -y zsh').status == 0 - if sat_maintain.__class__.__name__ == 'Satellite': - result = sat_maintain.install(InstallerCommand('lock-package-versions')) - assert result.status == 0 - assert 'Success!' in result.stdout - @pytest.mark.include_capsule def test_positive_fm_packages_update(request, sat_maintain): @@ -244,6 +245,12 @@ def test_positive_fm_packages_update(request, sat_maintain): :customerscenario: true """ + + @request.addfinalizer + def _finalize(): + assert sat_maintain.execute('dnf remove -y walrus').status == 0 + sat_maintain.execute('rm -rf /etc/yum.repos.d/custom_repo.repo') + # Setup custom_repo and packages update sat_maintain.create_custom_repos(custom_repo=settings.repos.yum_0.url) disableplugin = '--disableplugin=foreman-protector' @@ -263,8 +270,3 @@ def test_positive_fm_packages_update(request, sat_maintain): result = sat_maintain.execute('rpm -qa walrus') assert result.status == 0 assert 'walrus-5.21-1' in result.stdout - - @request.addfinalizer - def _finalize(): - assert sat_maintain.execute('dnf remove -y walrus').status == 0 - sat_maintain.execute('rm -rf /etc/yum.repos.d/custom_repo.repo') diff --git a/tests/foreman/ui/test_ansible.py b/tests/foreman/ui/test_ansible.py index 82ffd5bf187..89eba62ff56 100644 --- a/tests/foreman/ui/test_ansible.py +++ b/tests/foreman/ui/test_ansible.py @@ -178,6 +178,13 @@ def test_positive_ansible_custom_role(target_sat, session, module_org, rhel_cont :CaseComponent: Ansible-RemoteExecution """ + + @request.addfinalizer + def _finalize(): + result = target_sat.cli.Ansible.roles_delete({'name': SELECTED_ROLE}) + assert f'Ansible role [{SELECTED_ROLE}] was deleted.' in result[0]['message'] + target_sat.execute('rm -rvf /etc/ansible/roles/custom_role') + SELECTED_ROLE = 'custom_role' playbook = f'{robottelo_tmp_dir}/playbook.yml' data = { @@ -231,12 +238,6 @@ def test_positive_ansible_custom_role(target_sat, session, module_org, rhel_cont session.configreport.delete(rhel_contenthost.hostname) assert len(session.configreport.read()['table']) == 0 - @request.addfinalizer - def _finalize(): - result = target_sat.cli.Ansible.roles_delete({'name': SELECTED_ROLE}) - assert f'Ansible role [{SELECTED_ROLE}] was deleted.' in result[0]['message'] - target_sat.execute('rm -rvf /etc/ansible/roles/custom_role') - @pytest.mark.tier2 def test_positive_host_role_information(target_sat, function_host): diff --git a/tests/foreman/ui/test_computeresource_gce.py b/tests/foreman/ui/test_computeresource_gce.py index 73f4fb11daf..a25edb1da44 100644 --- a/tests/foreman/ui/test_computeresource_gce.py +++ b/tests/foreman/ui/test_computeresource_gce.py @@ -161,8 +161,17 @@ def test_positive_gce_provision_end_to_end( :expectedresults: Host is provisioned successfully """ + name = f'test{gen_string("alpha", 4).lower()}' hostname = f'{name}.{gce_domain.name}' + + @request.addfinalizer + def _finalize(): + gcehost = sat_gce.api.Host().search(query={'search': f'name={hostname}'}) + if gcehost: + gcehost[0].delete() + googleclient.disconnect() + gceapi_vmname = hostname.replace('.', '-') root_pwd = gen_string('alpha', 15) storage = [{'size': 20}] @@ -214,13 +223,6 @@ def test_positive_gce_provision_end_to_end( # 2.2 GCE Backend Assertions assert gceapi_vm.is_stopping or gceapi_vm.is_stopped - @request.addfinalizer - def _finalize(): - gcehost = sat_gce.api.Host().search(query={'search': f'name={hostname}'}) - if gcehost: - gcehost[0].delete() - googleclient.disconnect() - @pytest.mark.tier4 @pytest.mark.upgrade @@ -247,6 +249,14 @@ def test_positive_gce_cloudinit_provision_end_to_end( """ name = f'test{gen_string("alpha", 4).lower()}' hostname = f'{name}.{gce_domain.name}' + + @request.addfinalizer + def _finalize(): + gcehost = sat_gce.api.Host().search(query={'search': f'name={hostname}'}) + if gcehost: + gcehost[0].delete() + googleclient.disconnect() + gceapi_vmname = hostname.replace('.', '-') storage = [{'size': 20}] root_pwd = gen_string('alpha', random.choice([8, 15])) @@ -290,10 +300,3 @@ def test_positive_gce_cloudinit_provision_end_to_end( assert not sat_gce.api.Host().search(query={'search': f'name="{hostname}"'}) # 2.2 GCE Backend Assertions assert gceapi_vm.is_stopping or gceapi_vm.is_stopped - - @request.addfinalizer - def _finalize(): - gcehost = sat_gce.api.Host().search(query={'search': f'name={hostname}'}) - if gcehost: - gcehost[0].delete() - googleclient.disconnect() diff --git a/tests/foreman/ui/test_computeresource_libvirt.py b/tests/foreman/ui/test_computeresource_libvirt.py index 518e26817b8..42477b8d46a 100644 --- a/tests/foreman/ui/test_computeresource_libvirt.py +++ b/tests/foreman/ui/test_computeresource_libvirt.py @@ -171,10 +171,8 @@ def test_positive_provision_end_to_end( } ) name = f'{hostname}.{module_libvirt_provisioning_sat.domain.name}' - assert session.host.search(name)[0]['Name'] == name - - # teardown request.addfinalizer(lambda: sat.provisioning_cleanup(name)) + assert session.host.search(name)[0]['Name'] == name # Check on Libvirt, if VM exists result = sat.execute( diff --git a/tests/upgrades/test_contentview.py b/tests/upgrades/test_contentview.py index 9b7fa90bb56..2b581297b1e 100644 --- a/tests/upgrades/test_contentview.py +++ b/tests/upgrades/test_contentview.py @@ -87,6 +87,7 @@ def test_cv_postupgrade_scenario(self, request, target_sat, pre_upgrade_data): cv = target_sat.api.ContentView(organization=org.id).search( query={'search': f'name="{cv_name}"'} )[0] + request.addfinalizer(cv.delete) yum_repo = target_sat.api.Repository(organization=org.id).search( query={'search': f'name="{pre_test_name}_yum_repo"'} )[0] @@ -95,7 +96,6 @@ def test_cv_postupgrade_scenario(self, request, target_sat, pre_upgrade_data): query={'search': f'name="{pre_test_name}_file_repo"'} )[0] request.addfinalizer(file_repo.delete) - request.addfinalizer(cv.delete) cv.repository = [] cv.update(['repository']) assert len(cv.read_json()['repositories']) == 0 diff --git a/tests/upgrades/test_host.py b/tests/upgrades/test_host.py index 5d0e9fda247..b60585d63bd 100644 --- a/tests/upgrades/test_host.py +++ b/tests/upgrades/test_host.py @@ -161,6 +161,7 @@ def test_post_create_gce_cr_and_host( pre_upgrade_host = sat_gce.api.Host().search( query={'search': f'name={pre_upgrade_data.provision_host_name}'} )[0] + request.addfinalizer(pre_upgrade_host.delete) org = sat_gce.api.Organization(id=pre_upgrade_host.organization.id).read() loc = sat_gce.api.Location(id=pre_upgrade_host.location.id).read() domain = sat_gce.api.Domain(id=pre_upgrade_host.domain.id).read() @@ -185,7 +186,6 @@ def test_post_create_gce_cr_and_host( image=image, root_pass=gen_string('alphanumeric'), ).create() - request.addfinalizer(pre_upgrade_host.delete) request.addfinalizer(host.delete) assert host.name == f"{self.hostname.lower()}.{domain.name}" assert host.build_status_label == 'Installed' diff --git a/tests/upgrades/test_provisioningtemplate.py b/tests/upgrades/test_provisioningtemplate.py index 9d681a56ead..7c603c30a53 100644 --- a/tests/upgrades/test_provisioningtemplate.py +++ b/tests/upgrades/test_provisioningtemplate.py @@ -105,6 +105,7 @@ def test_post_scenario_provisioning_templates( pre_upgrade_host = module_target_sat.api.Host().search( query={'search': f'id={pre_upgrade_data.provision_host_id}'} )[0] + request.addfinalizer(pre_upgrade_host.delete) org = module_target_sat.api.Organization(id=pre_upgrade_host.organization.id).read() loc = module_target_sat.api.Location(id=pre_upgrade_host.location.id).read() domain = module_target_sat.api.Domain(id=pre_upgrade_host.domain.id).read() @@ -129,7 +130,6 @@ def test_post_scenario_provisioning_templates( root_pass=settings.provisioning.host_root_password, pxe_loader=pxe_loader, ).create() - request.addfinalizer(pre_upgrade_host.delete) request.addfinalizer(new_host.delete) for kind in provisioning_template_kinds: diff --git a/tests/upgrades/test_subscription.py b/tests/upgrades/test_subscription.py index 917952adaa1..b1dd84597a1 100644 --- a/tests/upgrades/test_subscription.py +++ b/tests/upgrades/test_subscription.py @@ -167,5 +167,5 @@ def test_post_subscription_scenario_auto_attach(self, request, target_sat, pre_u sub.delete_manifest(data={'organization_id': org.id}) assert len(sub.search()) == 0 manifester = Manifester(manifest_category=settings.manifest.entitlement) - manifester.allocation_uuid = pre_upgrade_data.allocation_uuid request.addfinalizer(manifester.delete_subscription_allocation) + manifester.allocation_uuid = pre_upgrade_data.allocation_uuid diff --git a/tests/upgrades/test_usergroup.py b/tests/upgrades/test_usergroup.py index 0832602dbb3..11ac95e2af8 100644 --- a/tests/upgrades/test_usergroup.py +++ b/tests/upgrades/test_usergroup.py @@ -102,16 +102,16 @@ def test_post_verify_user_group_membership( user_group = target_sat.api.UserGroup().search( query={'search': f'name={pre_upgrade_data["user_group_name"]}'} ) + request.addfinalizer(user_group[0].delete) auth_source = target_sat.api.AuthSourceLDAP().search( query={'search': f'name={pre_upgrade_data["auth_source_name"]}'} )[0] request.addfinalizer(auth_source.delete) - request.addfinalizer(user_group[0].delete) user = target_sat.api.User().search(query={'search': f'login={ad_data["ldap_user_name"]}'})[ 0 ] - assert user.read().id == user_group[0].read().user[0].id request.addfinalizer(user.delete) + assert user.read().id == user_group[0].read().user[0].id role_list = target_sat.cli.Role.with_user( username=ad_data['ldap_user_name'], password=ad_data['ldap_user_passwd'] ).list() From acfed708c7840881af313d77972f96d5d92f85c6 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:22:22 -0400 Subject: [PATCH 544/586] [6.14.z] fix in test_permission (#13895) --- tests/foreman/destructive/test_auth.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/foreman/destructive/test_auth.py b/tests/foreman/destructive/test_auth.py index d97c592d05b..fad1d3d7276 100644 --- a/tests/foreman/destructive/test_auth.py +++ b/tests/foreman/destructive/test_auth.py @@ -18,8 +18,6 @@ from robottelo.constants import HAMMER_CONFIG LOGEDIN_MSG = "Session exists, currently logged in as '{0}'" -LOGEDOFF_MSG = "Using sessions, you are currently not logged in" -NOTCONF_MSG = "Credentials are not configured." password = gen_string('alpha') pytestmark = pytest.mark.destructive @@ -36,7 +34,7 @@ def test_positive_password_reset(target_sat): """ result = target_sat.execute('foreman-rake permissions:reset') assert result.status == 0 - reset_password = result.stdout.splitlines()[0].split('password: ')[1] + reset_password = result.stdout.splitlines()[1].split('password: ')[1] result = target_sat.execute( f'''sed -i -e '/username/d;/password/d;/use_sessions/d' {HAMMER_CONFIG};\ echo ' :use_sessions: true' >> {HAMMER_CONFIG}''' @@ -46,5 +44,5 @@ def test_positive_password_reset(target_sat): {'username': settings.server.admin_username, 'password': reset_password} ) result = target_sat.cli.Auth.with_user().status() - assert LOGEDIN_MSG.format(settings.server.admin_username) in result[0]['message'] + assert LOGEDIN_MSG.format(settings.server.admin_username) in result.split("\n")[1] assert target_sat.cli.Org.with_user().list() From 47414634b6b7771571e682896375f258de14ebeb Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 20 Mar 2024 08:13:54 -0400 Subject: [PATCH 545/586] [6.14.z] duplicate bookmark negative test fix (#14454) duplicate bookmark negative test fix (#14232) (cherry picked from commit e44a553097568f44bdbb589cbd125b499a00eaed) Co-authored-by: Peter Ondrejka --- robottelo/constants/__init__.py | 22 ++++++++++++++++++---- tests/foreman/ui/test_bookmarks.py | 12 +++++++++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index bca0dbdf9b2..c6133a1e39f 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -1651,6 +1651,7 @@ class Colored(Box): 'name': 'ActivationKey', 'controller': 'katello_activation_keys', 'session_name': 'activationkey', + 'old_ui': True, }, {'name': 'Dashboard', 'controller': 'dashboard', 'session_name': 'dashboard'}, {'name': 'Audit', 'controller': 'audits', 'session_name': 'audit'}, @@ -1663,28 +1664,41 @@ class Colored(Box): {'name': 'Task', 'controller': 'foreman_tasks_tasks', 'session_name': 'task'}, # TODO Load manifest for the test_positive_end_to_end from the ui/test_bookmarks.py # {'name': 'Subscriptions', 'controller': 'subscriptions','session_name': 'subscription' }, - {'name': 'Product', 'controller': 'katello_products', 'session_name': 'product'}, + { + 'name': 'Product', + 'controller': 'katello_products', + 'session_name': 'product', + 'old_ui': True, + }, {'name': 'Repository', 'controller': 'katello_repositories', 'session_name': 'repository'}, { 'name': 'ContentCredential', 'controller': 'katello_content_credentials', 'session_name': 'contentcredential', + 'old_ui': True, + }, + { + 'name': 'SyncPlan', + 'controller': 'katello_sync_plans', + 'session_name': 'syncplan', + 'old_ui': True, }, - {'name': 'SyncPlan', 'controller': 'katello_sync_plans', 'session_name': 'syncplan'}, {'name': 'ContentView', 'controller': 'katello_content_views', 'session_name': 'contentview'}, - {'name': 'Errata', 'controller': 'katello_errata', 'session_name': 'errata'}, + {'name': 'Errata', 'controller': 'katello_errata', 'session_name': 'errata', 'old_ui': True}, {'name': 'Package', 'controller': 'katello_erratum_packages', 'session_name': 'package'}, { 'name': 'ContainerImageTag', 'controller': 'katello_docker_tags', 'session_name': 'containerimagetag', + 'old_ui': True, }, {'name': 'Host', 'controller': 'hosts', 'setup': entities.Host, 'session_name': 'host_new'}, - {'name': 'ContentHost', 'controller': 'hosts', 'session_name': 'contenthost'}, + {'name': 'ContentHost', 'controller': 'hosts', 'session_name': 'contenthost', 'old_ui': True}, { 'name': 'HostCollection', 'controller': 'katello_host_collections', 'session_name': 'hostcollection', + 'old_ui': True, }, {'name': 'Architecture', 'controller': 'architectures', 'session_name': 'architecture'}, { diff --git a/tests/foreman/ui/test_bookmarks.py b/tests/foreman/ui/test_bookmarks.py index 563a19102a7..037af909046 100644 --- a/tests/foreman/ui/test_bookmarks.py +++ b/tests/foreman/ui/test_bookmarks.py @@ -11,7 +11,7 @@ :CaseImportance: High """ -from airgun.exceptions import NoSuchElementException +from airgun.exceptions import DisabledWidgetError, NoSuchElementException from fauxfactory import gen_string import pytest @@ -256,8 +256,14 @@ def test_negative_create_with_duplicate_name(session, ui_entity, module_target_s existing_bookmark = session.bookmark.search(bookmark.name)[0] assert existing_bookmark['Name'] == bookmark.name ui_lib = getattr(session, ui_entity['name'].lower()) - # this fails but does not raise UI error, BZ#1992652 closed wontfix - ui_lib.create_bookmark({'name': bookmark.name, 'query': query, 'public': True}) + # this fails but does not raise UI error in old style dialog, BZ#1992652 closed + # wontfix, but new style dialog raises error, both situations occur + old_ui = ui_entity.get('old_ui') + if old_ui: + ui_lib.create_bookmark({'name': bookmark.name, 'query': query, 'public': True}) + else: + with pytest.raises((DisabledWidgetError, NoSuchElementException)): + ui_lib.create_bookmark({'name': bookmark.name, 'query': query, 'public': True}) # assert there are no duplicate bookmarks new_search = session.bookmark.search(bookmark.name) assert len(new_search) == 1 From a67b44df5a2d5009d25505d9e7e9755ea1d98703 Mon Sep 17 00:00:00 2001 From: Jake Callahan Date: Wed, 20 Mar 2024 13:42:55 -0400 Subject: [PATCH 546/586] [6.14.z] Bump Broker to 0.4.9 (#14460) Bump Broker to 0.4.9 (#14399) * Bump Broker to 0.4.8 Now that Broker is back on PyPI, let's clean up the requirements * update airgun and nailgun dependency definitions these are in line with the newer standard, and required for people using uv. * update broker to 0.4.9 --- requirements.txt | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4826ba6ecb6..c239e80ccc0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,14 @@ # Version updates managed by dependabot betelgeuse==1.11.0 -# broker[docker]==0.4.1 - Temporarily disabled, see below +broker[docker]==0.4.9 cryptography==42.0.5 deepdiff==6.7.1 -docker==7.0.0 # Temporary until Broker is back on PyPi dynaconf[vault]==3.2.5 fauxfactory==3.1.0 jinja2==3.1.3 manifester==0.0.14 navmazing==1.2.2 -paramiko==3.4.0 # Temporary until Broker is back on PyPi productmd==1.38 pyotp==2.9.0 python-box==7.1.1 @@ -29,11 +27,6 @@ wait-for==1.2.0 wrapanapi==3.6.0 # Get airgun, nailgun and upgrade from 6.14.z -git+https://github.com/SatelliteQE/airgun.git@6.14.z#egg=airgun -git+https://github.com/SatelliteQE/nailgun.git@6.14.z#egg=nailgun -# Broker currently is unable to push to PyPi due to [1] and [2] -# In the meantime, we install directly from the repo -# [1] - https://github.com/ParallelSSH/ssh2-python/issues/193 -# [2] - https://github.com/pypi/warehouse/issues/7136 -git+https://github.com/SatelliteQE/broker.git@0.4.7#egg=broker +airgun @ git+https://github.com/SatelliteQE/airgun.git@6.14.z#egg=airgun +nailgun @ git+https://github.com/SatelliteQE/nailgun.git@6.14.z#egg=nailgun --editable . From b80e986074fb1cf6cb17130e5dfa6dab9468ba46 Mon Sep 17 00:00:00 2001 From: Omkar Khatavkar Date: Wed, 20 Mar 2024 23:18:31 +0530 Subject: [PATCH 547/586] =?UTF-8?q?[6.14.z]-fixed=20the=20auto-cherry-pick?= =?UTF-8?q?ing=20issue=20cause=20due=20github=20action=20=E2=80=A6=20(#144?= =?UTF-8?q?28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [6.14.z]-fixed the auto-cherry-picking issue cause due github action failure --- .github/workflows/auto_cherry_pick.yml | 41 ++++++++++++++++++++------ 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/.github/workflows/auto_cherry_pick.yml b/.github/workflows/auto_cherry_pick.yml index 0ce0b8e9956..301d9181b9d 100644 --- a/.github/workflows/auto_cherry_pick.yml +++ b/.github/workflows/auto_cherry_pick.yml @@ -3,7 +3,7 @@ name: auto_cherry_pick_commits on: pull_request_target: - types: [closed, labeled] + types: [closed] # Github & Parent PR Env vars env: @@ -23,11 +23,12 @@ jobs: prt_comment: ${{steps.fc.outputs.comment-body}} steps: - name: Find Comment - uses: peter-evans/find-comment@v2 + uses: peter-evans/find-comment@v3 id: fc with: issue-number: ${{ env.number }} body-includes: "trigger: test-robottelo" + direction: last # Auto CherryPicking and Failure Recording auto-cherry-pick: @@ -40,6 +41,12 @@ jobs: label: ${{ github.event.pull_request.labels.*.name }} steps: + # Needed to avoid out-of-memory error + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 10 + ## Robottelo Repo Checkout - uses: actions/checkout@v4 if: ${{ startsWith(matrix.label, '6.') && matrix.label != github.base_ref }} @@ -69,12 +76,13 @@ jobs: - name: Add Parent PR's PRT comment to Auto_Cherry_Picked PR's id: add-parent-prt-comment - if: ${{ always() && steps.cherrypick.outcome == 'success' }} - uses: mshick/add-pr-comment@v2 + if: ${{ always() && needs.find-the-parent-prt-comment.outputs.prt_comment != '' && steps.cherrypick.outcome == 'success' }} + uses: thollander/actions-comment-pull-request@v2 with: - issue: ${{ steps.cherrypick.outputs.number }} - message: ${{ needs.find-the-parent-prt-comment.outputs.prt_comment }} - repo-token: ${{ secrets.CHERRYPICK_PAT }} + message: | + ${{ needs.find-the-parent-prt-comment.outputs.prt_comment }} + pr_number: ${{ steps.cherrypick.outputs.number }} + GITHUB_TOKEN: ${{ secrets.CHERRYPICK_PAT }} - name: is autoMerging enabled for Auto CherryPicked PRs ? if: ${{ always() && steps.cherrypick.outcome == 'success' && contains(github.event.pull_request.labels.*.name, 'AutoMerge_Cherry_Picked') }} @@ -89,10 +97,25 @@ jobs: labels: ["AutoMerge_Cherry_Picked"] }) - ## Failure Logging to issues and GChat Group + - name: Check if cherrypick pr is created + id: search_pr + if: always() + run: | + PR_TITLE="[${{ matrix.label }}] ${{ env.title }}" + API_URL="https://api.github.com/repos/${{ github.repository }}/pulls?state=open" + PR_SEARCH_RESULT=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$API_URL" | jq --arg title "$PR_TITLE" '.[] | select(.title == $title)') + if [ -n "$PR_SEARCH_RESULT" ]; then + echo "pr_found=true" >> $GITHUB_OUTPUT + echo "PR is Found with title $PR_TITLE" + else + echo "pr_found=false" >> $GITHUB_OUTPUT + echo "PR is not Found with title $PR_TITLE" + fi + + ## Failure Logging to issues - name: Create Github issue on cherrypick failure id: create-issue - if: ${{ always() && steps.cherrypick.outcome != 'success' && startsWith(matrix.label, '6.') && matrix.label != github.base_ref }} + if: ${{ always() && steps.search_pr.outputs.pr_found == 'false' && steps.cherrypick.outcome != 'success' && startsWith(matrix.label, '6.') && matrix.label != github.base_ref }} uses: dacbd/create-issue-action@main with: token: ${{ secrets.CHERRYPICK_PAT }} From b66e0526e7ea51196ef94adce2b810fb54d27a38 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 21 Mar 2024 05:43:56 -0400 Subject: [PATCH 548/586] [6.14.z] moving to use pull_request_target for prt_label.yml GHA (#14471) moving to use pull_request_target for prt_label.yml GHA (#14467) moving to use pull_request_target for prt_labels.yml GHA (cherry picked from commit 59164b53505ee4955e3a1b2317bf407e896ddf41) Co-authored-by: Omkar Khatavkar --- .github/workflows/prt_labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prt_labels.yml b/.github/workflows/prt_labels.yml index 311516fa682..52dd68589b3 100644 --- a/.github/workflows/prt_labels.yml +++ b/.github/workflows/prt_labels.yml @@ -1,7 +1,7 @@ name: Remove the PRT label, for the new commit on: - pull_request: + pull_request_target: types: ["synchronize"] jobs: From dd51661105b2d72681086f2fd3b918c4f312da13 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 22 Mar 2024 00:15:56 -0400 Subject: [PATCH 549/586] [6.14.z] Bump pytest-mock from 3.12.0 to 3.14.0 (#14486) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c239e80ccc0..eecb47ecdfa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ pyotp==2.9.0 python-box==7.1.1 pytest==8.0.2 pytest-services==2.2.1 -pytest-mock==3.12.0 +pytest-mock==3.14.0 pytest-reportportal==5.4.0 pytest-xdist==3.5.0 pytest-fixturecollection==0.1.2 From fbf1f6ddfaa1a976d80858c69d4540b7746d5207 Mon Sep 17 00:00:00 2001 From: Peter Ondrejka Date: Fri, 22 Mar 2024 10:52:45 +0100 Subject: [PATCH 550/586] [6.14] eol banner (#14436) --- tests/foreman/ui/test_eol_banner.py | 99 +++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 tests/foreman/ui/test_eol_banner.py diff --git a/tests/foreman/ui/test_eol_banner.py b/tests/foreman/ui/test_eol_banner.py new file mode 100644 index 00000000000..c19b19838e6 --- /dev/null +++ b/tests/foreman/ui/test_eol_banner.py @@ -0,0 +1,99 @@ +"""Test module for Dashboard UI + +:Requirement: Dashboard + +:CaseAutomation: Automated + +:CaseComponent: Dashboard + +:Team: Endeavour + +:CaseImportance: High + +""" +from datetime import datetime, timedelta + +from airgun.session import Session +from navmazing import NavigationTriesExceeded +import pytest + +from robottelo.utils.datafactory import gen_string + + +def set_eol_date(target_sat, eol_date): + target_sat.execute( + rf'''sed -i "/end_of_life/c\ 'end_of_life': '{eol_date}'" /usr/share/satellite/lifecycle-metadata.yml''' + ) + target_sat.restart_services() + + +@pytest.mark.upgrade +@pytest.mark.run_in_one_thread +@pytest.mark.tier2 +def test_positive_eol_banner_e2e(session, target_sat, test_name): + """Check if the EOL banner is displayed correctly + + :id: 0ce6c11c-d969-4e7e-a934-cd1683de62a3 + + :Steps: + + 1. Set the EOL date witin 6 months, assert warning banner + 2. Check non-admin users can't see warning banner + 3. Dismiss banner + 4. Move EOL date to the past, assert error banner + 5. Check non-admin users can't see error banner + 6. Dismiss banner + + :expectedresults: Banner shows up when it should + """ + # non-admin user + username = gen_string('alpha') + password = gen_string('alpha') + target_sat.api.User(login=username, password=password, mail='test@example.com').create() + # admin user + admin_username = gen_string('alpha') + admin_password = gen_string('alpha') + target_sat.api.User( + login=admin_username, password=admin_password, admin=True, mail='admin@example.com' + ).create() + + # eol in 3 months + eol_date = (datetime.now() + timedelta(days=90)).strftime("%Y-%m-%d") + message_date = (datetime.now() + timedelta(days=90)).strftime("%B %Y") + set_eol_date(target_sat, eol_date) + + # non-admin can't see banner + with Session(test_name, username, password) as newsession: + with pytest.raises(NavigationTriesExceeded) as error: + newsession.eol_banner.read() + assert error.typename == 'NavigationTriesExceeded' + + # admin can see warning banner + with Session(test_name, admin_username, admin_password) as adminsession: + banner = adminsession.eol_banner.read() + assert message_date in banner["name"] + assert adminsession.eol_banner.is_warning() + adminsession.eol_banner.dismiss() + with pytest.raises(NavigationTriesExceeded) as error: + adminsession.eol_banner.read() + assert error.typename == 'NavigationTriesExceeded' + + # past eol_date + eol_date = (datetime.now() - timedelta(days=5)).strftime("%Y-%m-%d") + set_eol_date(target_sat, eol_date) + + # non-admin can't see danger banner + with Session(test_name, username, password) as newsession: + with pytest.raises(NavigationTriesExceeded) as error: + newsession.eol_banner.read() + assert error.typename == 'NavigationTriesExceeded' + + # admin can see danger banner + with Session(test_name, admin_username, admin_password) as adminsession: + banner = adminsession.eol_banner.read() + assert eol_date in banner["name"] + assert adminsession.eol_banner.is_danger() + adminsession.eol_banner.dismiss() + with pytest.raises(NavigationTriesExceeded) as error: + adminsession.eol_banner.read() + assert error.typename == 'NavigationTriesExceeded' From b35f7f9fe6722274a0962e73e11cb24b4fb2ed80 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 22 Mar 2024 06:55:09 -0400 Subject: [PATCH 551/586] [6.14.z] Add upgrade marker on ansible destructive tests (#14491) Add upgrade marker on ansible destructive tests (#14488) Signed-off-by: Gaurav Talreja (cherry picked from commit 17e284f2d9669b465bd3a8d5060f2cbc7ded8614) Co-authored-by: Gaurav Talreja --- tests/foreman/destructive/test_ansible.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/foreman/destructive/test_ansible.py b/tests/foreman/destructive/test_ansible.py index 01779d6a14a..b57ba89c58f 100644 --- a/tests/foreman/destructive/test_ansible.py +++ b/tests/foreman/destructive/test_ansible.py @@ -13,7 +13,7 @@ """ import pytest -pytestmark = pytest.mark.destructive +pytestmark = [pytest.mark.destructive, pytest.mark.upgrade] def test_positive_persistent_ansible_cfg_change(target_sat): From 34477c9fcee78745c37d461b4bc62b8df0a98b2e Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 22 Mar 2024 07:07:38 -0400 Subject: [PATCH 552/586] [6.14.z] Add test to sync repo having zst compression (#14493) --- robottelo/constants/__init__.py | 1 - robottelo/constants/repos.py | 2 +- tests/foreman/api/test_repositories.py | 40 +++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index c6133a1e39f..c45d7a38f1c 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -2142,7 +2142,6 @@ class Colored(Box): 'option is not present in the /etc/dnf/dnf.conf' ) - # Data File Paths class DataFile(Box): """The boxed Data directory class with its attributes pointing to the Data directory files""" diff --git a/robottelo/constants/repos.py b/robottelo/constants/repos.py index 9738894a689..a0b2a8adeec 100644 --- a/robottelo/constants/repos.py +++ b/robottelo/constants/repos.py @@ -25,6 +25,6 @@ FAKE_0_YUM_REPO_STRING_BASED_VERSIONS = ( 'https://fixtures.pulpproject.org/rpm-string-version-updateinfo/' ) - +FAKE_ZST_REPO = 'https://fixtures.pulpproject.org/rpm-zstd-metadata' ANSIBLE_GALAXY = 'https://galaxy.ansible.com/' ANSIBLE_HUB = 'https://cloud.redhat.com/api/automation-hub/' diff --git a/tests/foreman/api/test_repositories.py b/tests/foreman/api/test_repositories.py index 9d576b55f38..cce7e307cc4 100644 --- a/tests/foreman/api/test_repositories.py +++ b/tests/foreman/api/test_repositories.py @@ -18,7 +18,12 @@ from robottelo import constants from robottelo.config import settings -from robottelo.constants import DEFAULT_ARCHITECTURE, MIRRORING_POLICIES, REPOS +from robottelo.constants import ( + DEFAULT_ARCHITECTURE, + MIRRORING_POLICIES, + REPOS, +) +from robottelo.constants.repos import FAKE_ZST_REPO from robottelo.exceptions import CLIReturnCodeError from robottelo.utils.datafactory import parametrized @@ -186,6 +191,39 @@ def test_positive_sync_kickstart_repo(module_entitlement_manifest_org, target_sa assert rh_repo.content_counts['rpm'] > 0 +def test_positive_sync_upstream_repo_with_zst_compression( + module_org, module_product, module_target_sat +): + """Sync upstream repo having zst compression and verify it succeeds. + + :id: 1eddff2a-b6b5-420b-a0e8-ba6a05c11ca4 + + :expectedresults: Repo sync is successful and no zst type compression errors are present in /var/log/messages. + + :steps: + + 1. Sync upstream repository having zst type compression. + 2. Assert that no errors related to compression type are present in + /var/log/messages. + 3. Assert that sync was executed properly. + + :BZ: 2241934 + + :customerscenario: true + """ + repo = module_target_sat.api.Repository( + product=module_product, content_type='yum', url=FAKE_ZST_REPO + ).create() + assert repo.read().content_counts['rpm'] == 0 + sync = module_product.sync() + assert sync['result'] == 'success' + assert repo.read().content_counts['rpm'] > 0 + result = module_target_sat.execute( + 'grep pulp /var/log/messages | grep "Cannot detect compression type"' + ) + assert result.status == 1 + + @pytest.mark.tier1 def test_negative_upload_expired_manifest(module_org, target_sat): """Upload an expired manifest and attempt to refresh it From 522e9898e6a70940ba16c726fcfd77a7eaa962e7 Mon Sep 17 00:00:00 2001 From: David Moore <109112035+damoore044@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:11:47 -0400 Subject: [PATCH 553/586] [6.14.z] CP of API-Errata modifications (#13832) API-errata-CP, component-eval, updates to global registration and host content Please enter the commit message for your changes. Lines starting --- pytest_fixtures/component/repository.py | 22 + robottelo/constants/__init__.py | 4 + robottelo/host_helpers/api_factory.py | 32 +- robottelo/host_helpers/contenthost_mixins.py | 6 + tests/foreman/api/test_errata.py | 1545 ++++++++++++------ 5 files changed, 1120 insertions(+), 489 deletions(-) diff --git a/pytest_fixtures/component/repository.py b/pytest_fixtures/component/repository.py index b7a26560980..ce49a1d7f88 100644 --- a/pytest_fixtures/component/repository.py +++ b/pytest_fixtures/component/repository.py @@ -245,3 +245,25 @@ def module_repos_collection_with_manifest( ) _repos_collection.setup_content(module_entitlement_manifest_org.id, module_lce.id) return _repos_collection + + +@pytest.fixture +def function_repos_collection_with_manifest( + request, target_sat, function_sca_manifest_org, function_lce +): + """This fixture and its usage is very similar to repos_collection fixture above with extra + setup_content and uploaded manifest capabilities using function_lce and + function_sca_manifest_org fixtures + """ + repos = getattr(request, 'param', []) + repo_distro, repos = _simplify_repos(request, repos) + _repos_collection = target_sat.cli_factory.RepositoryCollection( + distro=repo_distro, + repositories=[ + getattr(target_sat.cli_factory, repo_name)(**repo_params) + for repo in repos + for repo_name, repo_params in repo.items() + ], + ) + _repos_collection.setup_content(function_sca_manifest_org.id, function_lce.id) + return _repos_collection diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index c45d7a38f1c..f9673326022 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -752,6 +752,9 @@ class Colored(Box): REAL_RHEL7_0_1_PACKAGE_FILENAME = 'python-pulp-common-2.21.0.2-1.el7sat.noarch.rpm' REAL_RHEL7_0_2_PACKAGE_NAME = 'python2-psutil' # for RHBA-2021:1314 REAL_RHEL7_0_2_PACKAGE_FILENAME = 'python2-psutil-5.7.2-2.el7sat.x86_64.rpm' +REAL_RHEL8_1_PACKAGE_NAME = 'puppet-agent' # for RHSA-2022:4867 +REAL_RHEL8_1_PACKAGE_FILENAME = 'puppet-agent-6.19.1-1.el8sat.x86_64' +REAL_RHEL8_2_PACKAGE_FILENAME = 'puppet-agent-6.26.0-1.el8sat.x86_64' FAKE_0_CUSTOM_PACKAGE_GROUP_NAME = 'birds' FAKE_3_YUM_OUTDATED_PACKAGES = [ 'acme-package-1.0.1-1.noarch', @@ -806,6 +809,7 @@ class Colored(Box): FAKE_2_ERRATA_ID = 'RHSA-2012:0055' # for FAKE_1_CUSTOM_PACKAGE REAL_RHEL7_0_ERRATA_ID = 'RHBA-2020:3615' # for REAL_RHEL7_0_0_PACKAGE REAL_RHEL7_1_ERRATA_ID = 'RHBA-2017:0395' # tcsh bug fix update +REAL_RHEL8_1_ERRATA_ID = 'RHSA-2022:4867' # for REAL_RHEL8_1_PACKAGE FAKE_1_YUM_REPOS_COUNT = 32 FAKE_3_YUM_REPOS_COUNT = 78 FAKE_9_YUM_SECURITY_ERRATUM = [ diff --git a/robottelo/host_helpers/api_factory.py b/robottelo/host_helpers/api_factory.py index da6a39ecd76..bae286dc8ac 100644 --- a/robottelo/host_helpers/api_factory.py +++ b/robottelo/host_helpers/api_factory.py @@ -3,6 +3,7 @@ example: my_satellite.api_factory.api_method() """ from contextlib import contextmanager +from datetime import datetime import time from fauxfactory import gen_ipaddr, gen_mac, gen_string @@ -647,12 +648,18 @@ def attach_custom_product_subscription(self, prod_name=None, host_name=None): ) def wait_for_errata_applicability_task( - self, host_id, from_when, search_rate=1, max_tries=10, poll_rate=None, poll_timeout=15 + self, + host_id, + from_when, + search_rate=1, + max_tries=10, + poll_rate=None, + poll_timeout=15, ): """Search the generate applicability task for given host and make sure it finishes :param int host_id: Content host ID of the host where we are regenerating applicability. - :param int from_when: Timestamp (in UTC) to limit number of returned tasks to investigate. + :param int from_when: Epoch Time (seconds in UTC) to limit number of returned tasks to investigate. :param int search_rate: Delay between searches. :param int max_tries: How many times search should be executed. :param int poll_rate: Delay between the end of one task check-up and @@ -666,23 +673,30 @@ def wait_for_errata_applicability_task( assert isinstance(host_id, int), 'Param host_id have to be int' assert isinstance(from_when, int), 'Param from_when have to be int' now = int(time.time()) - assert from_when <= now, 'Param from_when have to be timestamp in the past' + assert from_when <= now, 'Param from_when have to be epoch time in the past' for _ in range(max_tries): now = int(time.time()) - max_age = now - from_when + 1 + # Format epoch time for search, one second prior margin of safety + timestamp = datetime.fromtimestamp(from_when - 1).strftime('%m-%d-%Y %H:%M:%S') + # Long format to match search: ex. 'January 03, 2024 at 03:08:08 PM' + long_format = datetime.strptime(timestamp, '%m-%d-%Y %H:%M:%S').strftime( + '%B %d, %Y at %I:%M:%S %p' + ) search_query = ( - '( label = Actions::Katello::Host::GenerateApplicability OR label = ' - 'Actions::Katello::Host::UploadPackageProfile ) AND started_at > "%s seconds ago"' - % max_age + '( label = Actions::Katello::Applicability::Hosts::BulkGenerate OR' + ' label = Actions::Katello::Host::UploadPackageProfile ) AND' + f' started_at >= "{long_format}" ' ) tasks = self._satellite.api.ForemanTask().search(query={'search': search_query}) tasks_finished = 0 for task in tasks: if ( - task.label == 'Actions::Katello::Host::GenerateApplicability' + task.label == 'Actions::Katello::Applicability::Hosts::BulkGenerate' + and 'host_ids' in task.input and host_id in task.input['host_ids'] ) or ( task.label == 'Actions::Katello::Host::UploadPackageProfile' + and 'host' in task.input and host_id == task.input['host']['id'] ): task.poll(poll_rate=poll_rate, timeout=poll_timeout) @@ -692,7 +706,7 @@ def wait_for_errata_applicability_task( time.sleep(search_rate) else: raise AssertionError( - f"No task was found using query '{search_query}' for host '{host_id}'" + f'No task was found using query " {search_query} " for host id: {host_id}' ) def wait_for_syncplan_tasks(self, repo_backend_id=None, timeout=10, repo_name=None): diff --git a/robottelo/host_helpers/contenthost_mixins.py b/robottelo/host_helpers/contenthost_mixins.py index 96da029d148..feac3cc89ae 100644 --- a/robottelo/host_helpers/contenthost_mixins.py +++ b/robottelo/host_helpers/contenthost_mixins.py @@ -136,6 +136,12 @@ def applicable_errata_count(self): """return the applicable errata count for a host""" return self.nailgun_host.read().content_facet_attributes['errata_counts']['total'] + @property + def applicable_package_count(self): + """return the applicable package count for a host""" + self.run('subscription-manager repos') + return self.nailgun_host.read().content_facet_attributes['applicable_package_count'] + class SystemFacts: """Helpers mixin that enables getting/setting subscription-manager facts on a host""" diff --git a/tests/foreman/api/test_errata.py b/tests/foreman/api/test_errata.py index cb831425778..45ad3774a11 100644 --- a/tests/foreman/api/test_errata.py +++ b/tests/foreman/api/test_errata.py @@ -12,14 +12,28 @@ """ # For ease of use hc refers to host-collection throughout this document -from time import sleep +from time import sleep, time -from nailgun import entities import pytest -from robottelo import constants from robottelo.config import settings -from robottelo.constants import DEFAULT_SUBSCRIPTION_NAME +from robottelo.constants import ( + DEFAULT_ARCHITECTURE, + FAKE_1_CUSTOM_PACKAGE, + FAKE_2_CUSTOM_PACKAGE, + FAKE_2_CUSTOM_PACKAGE_NAME, + FAKE_4_CUSTOM_PACKAGE, + FAKE_4_CUSTOM_PACKAGE_NAME, + FAKE_5_CUSTOM_PACKAGE, + FAKE_9_YUM_OUTDATED_PACKAGES, + FAKE_9_YUM_SECURITY_ERRATUM, + FAKE_9_YUM_UPDATED_PACKAGES, + PRDS, + REAL_RHEL8_1_ERRATA_ID, + REAL_RHEL8_1_PACKAGE_FILENAME, + REPOS, + REPOSET, +) pytestmark = [ pytest.mark.run_in_one_thread, @@ -30,25 +44,53 @@ CUSTOM_REPO_URL = settings.repos.yum_9.url CUSTOM_REPO_ERRATA_ID = settings.repos.yum_6.errata[2] +ERRATA = [ + { + 'id': settings.repos.yum_6.errata[2], # security advisory + 'old_package': FAKE_1_CUSTOM_PACKAGE, + 'new_package': FAKE_2_CUSTOM_PACKAGE, + 'package_name': FAKE_2_CUSTOM_PACKAGE_NAME, + }, + { + 'id': settings.repos.yum_6.errata[0], # bugfix advisory + 'old_package': FAKE_4_CUSTOM_PACKAGE, + 'new_package': FAKE_5_CUSTOM_PACKAGE, + 'package_name': FAKE_4_CUSTOM_PACKAGE_NAME, + }, +] +REPO_WITH_ERRATA = { + 'url': settings.repos.yum_9.url, + 'errata': ERRATA, + 'errata_ids': settings.repos.yum_9.errata, +} @pytest.fixture(scope='module') -def activation_key(module_org, module_lce, module_target_sat): +def activation_key(module_sca_manifest_org, module_cv, module_lce, module_target_sat): + """A new Activation Key associated with published version + of module_cv, promoted to module_lce.""" + _cv = cv_publish_promote( + module_target_sat, + module_sca_manifest_org, + module_cv, + module_lce, + )['content-view'] return module_target_sat.api.ActivationKey( - environment=module_lce, organization=module_org + organization=module_sca_manifest_org, + environment=module_lce, + content_view=_cv, ).create() @pytest.fixture(scope='module') -def rh_repo( - module_entitlement_manifest_org, module_lce, module_cv, activation_key, module_target_sat -): +def rh_repo(module_sca_manifest_org, module_lce, module_cv, activation_key, module_target_sat): + "rhel8 rh repos with errata and outdated/updated packages" return module_target_sat.cli_factory.setup_org_for_a_rh_repo( { - 'product': constants.PRDS['rhel'], - 'repository-set': constants.REPOSET['rhst7'], - 'repository': constants.REPOS['rhst7']['name'], - 'organization-id': module_entitlement_manifest_org.id, + 'product': PRDS['rhel'], + 'repository-set': REPOSET['rhst8'], + 'repository': REPOS['rhst8']['name'], + 'organization-id': module_sca_manifest_org.id, 'content-view-id': module_cv.id, 'lifecycle-environment-id': module_lce.id, 'activationkey-id': activation_key.id, @@ -57,11 +99,12 @@ def rh_repo( @pytest.fixture(scope='module') -def custom_repo(module_org, module_lce, module_cv, activation_key, module_target_sat): +def custom_repo(module_sca_manifest_org, module_lce, module_cv, activation_key, module_target_sat): + "zoo repos with errata and outdated/updated packages" return module_target_sat.cli_factory.setup_org_for_a_custom_repo( { - 'url': settings.repos.yum_9.url, - 'organization-id': module_org.id, + 'url': CUSTOM_REPO_URL, + 'organization-id': module_sca_manifest_org.id, 'content-view-id': module_cv.id, 'lifecycle-environment-id': module_lce.id, 'activationkey-id': activation_key.id, @@ -69,54 +112,7 @@ def custom_repo(module_org, module_lce, module_cv, activation_key, module_target ) -def _install_package( - module_org, clients, host_ids, package_name, via_ssh=True, rpm_package_name=None -): - """Install package via SSH CLI if via_ssh is True, otherwise - install via http api: PUT /api/v2/hosts/bulk/install_content - """ - if via_ssh: - for client in clients: - result = client.run(f'yum install -y {package_name}') - assert result.status == 0 - result = client.run(f'rpm -q {package_name}') - assert result.status == 0 - else: - entities.Host().install_content( - data={ - 'organization_id': module_org.id, - 'included': {'ids': host_ids}, - 'content_type': 'package', - 'content': [package_name], - } - ) - _validate_package_installed(clients, rpm_package_name) - - -def _validate_package_installed(hosts, package_name, expected_installed=True, timeout=240): - """Check whether package was installed on the list of hosts.""" - for host in hosts: - for _ in range(timeout // 15): - result = host.run(f'rpm -q {package_name}') - if ( - result.status == 0 - and expected_installed - or result.status != 0 - and not expected_installed - ): - break - sleep(15) - else: - pytest.fail( - 'Package {} was not {} host {}'.format( - package_name, - 'installed on' if expected_installed else 'removed from', - host.hostname, - ) - ) - - -def _validate_errata_counts(module_org, host, errata_type, expected_value, timeout=120): +def _validate_errata_counts(host, errata_type, expected_value, timeout=120): """Check whether host contains expected errata counts.""" for _ in range(timeout // 5): host = host.read() @@ -135,113 +131,687 @@ def _validate_errata_counts(module_org, host, errata_type, expected_value, timeo ) -def _fetch_available_errata(module_org, host, expected_amount, timeout=120): +def _fetch_available_errata(host, expected_amount=None, timeout=120): """Fetch available errata for host.""" errata = host.errata() for _ in range(timeout // 5): - if len(errata['results']) == expected_amount: + if expected_amount is None or len(errata['results']) == expected_amount: return errata['results'] sleep(5) errata = host.errata() else: pytest.fail( 'Host {} contains {} available errata, but expected to ' - 'contain {} of them'.format(host.name, len(errata['results']), expected_amount) + 'contain {} of them'.format( + host.name, + len(errata['results']), + expected_amount if not None else 'No expected_amount provided', + ) + ) + + +def _fetch_available_errata_instances(sat, host, expected_amount=None, timeout=120): + """Fetch list of instances of avaliable errata for host.""" + _errata_dict = _fetch_available_errata(host.nailgun_host, expected_amount, timeout) + _errata_ids = [errata['id'] for errata in _errata_dict] + instances = [sat.api.Errata(id=_id).read() for _id in _errata_ids] + assert ( + len(instances) == len(_errata_dict) == host.applicable_errata_count + ), 'Length of errata instances list or api result differs from expected applicable count.' + return instances + + +def errata_id_set(erratum_list): + """Return a set of unique errata id's, passing list of errata instances, or dictionary. + :raise: `AssertionError`: if errata_id could not be found from a list entry. + :return: set{string} + """ + result = set() + try: + # erratum_list is a list of errata instances + result = set(e.errata_id for e in erratum_list) + except Exception: + try: + # erratum_list is a list of errata dictionary references + result = set(e['errata_id'] for e in erratum_list) + except Exception as err: + # some errata_id cannot be extracted from an entry in erratum_list + raise AssertionError( + 'Must take a dictionary ref or list of erratum instances, each entry needs attribute or key "errata_id".' + f' An entry in the given erratum_list had no discernible "errata_id". Errata(s): {erratum_list}.' + ) from err + return result + + +def package_applicability_changed_as_expected( + sat, + host, + package_filename, + prior_applicable_errata_list, + prior_applicable_errata_count, + prior_applicable_package_count, + return_applicables=False, +): + """Checks that after installing some package, updated any impacted errata(s) + status and host applicability count, and changed applicable package count by one. + + That one of the following occured: + - A non-applicable package was modified, or the same prior version was installed, + the amount of applicable errata and applicable packages remains the same. + Return False, as no applicability changes occured. + + - An Outdated applicable package was installed. Errata applicability increased + by the number of found applicable errata containing that package, + if the errata were not already applicable prior to install. + The number of applicable packages increased by one. + + - An Updated applicable package was installed. Errata applicability decreased + by the amount of found errata containing that package, if the errata are + no longer applicable, but they were prior to install, if any. + The number of applicable packages decreased by one. + + :param string: package_filename: + the full filename of the package version installed. + :param list: prior_applicable_errata_list: + list of all erratum instances from search, that were applicable before modifying package. + :param int prior_applicable_errata_count: + number of total applicable errata prior to modifying package. + :param int prior_applicable_package_count: + number of total applicable packages prior to modifying package. + :param boolean return_applicables (False): if set to True, and method's 'result' is not False: + return a dict containing result, and relevant package and errata information. + + :raise: `AssertionError` if: + Expected changes are not found. + Changes are made to unexpected errata or packages. + A non-readable prior list of erratum was passed. + :return: result(boolean), or relevant applicables(dict) + False if found that no applicable package was modified. + True if method finished executing, expected changes were found. + + :return_applicables: if True: return dict of relevant applicable and changed entities: + result boolean: True, method finished executing + errata_count int: current applicable errata count + package_count int: current applicable package count + current_package string: current version filename of package + prior_package string: previous version filename of package + change_in_errata int: positive, negative, or zero + changed_errata list[string]: of modified errata_ids + """ + assert ( + len(prior_applicable_errata_list) == prior_applicable_errata_count + ), 'Length of "prior_applicable_errata_list" passed, must equal "prior_applicable_errata_count" passed.' + if len(prior_applicable_errata_list) != 0: + try: + prior_applicable_errata_list[0].read() + except Exception as err: + raise AssertionError( + 'Exception on read of index zero in passed parameter "prior_applicable_errata_list".' + ' Must pass a list of readable erratum instances, or empty list.' + ) from err + # schedule errata applicability recalculate for most current status + task = None + epoch_timestamp = int(time() - 1) + result = host.execute('subscription-manager repos') + assert ( + result.status == 0 + ), f'Command "subscription-manager repos" failed to execute on host: {host.hostname},\n{result}' + + try: + task = sat.api_factory.wait_for_errata_applicability_task( + host_id=host.nailgun_host.id, + from_when=epoch_timestamp, + ) + except AssertionError: + # No task for forced applicability regenerate, + # applicability was already up to date + assert task is None + package_basename = str(package_filename.split("-", 1)[0]) # 'package-4.0-1.rpm' > 'package' + prior_unique_errata_ids = errata_id_set(prior_applicable_errata_list) + current_applicable_errata = _fetch_available_errata_instances(sat, host) + app_unique_errata_ids = errata_id_set(current_applicable_errata) + app_errata_with_package_diff = [] + app_errata_diff_ids = set() + + if prior_applicable_errata_count == host.applicable_errata_count: + # Applicable errata count had no change. + # we expect applicable errata id(s) from search also did not change. + assert ( + prior_unique_errata_ids == app_unique_errata_ids + ), 'Expected list of applicable erratum to remain the same.' + if prior_applicable_package_count == host.applicable_package_count: + # no applicable packages were modified + return False + + if prior_applicable_errata_count != host.applicable_errata_count: + # Modifying package changed errata applicability. + # we expect one or more errata id(s) from search to be added or removed. + difference = abs(prior_applicable_errata_count - host.applicable_errata_count) + # Check list of errata id(s) from search matches expected difference + assert ( + len(app_unique_errata_ids) == prior_applicable_errata_count + difference + ), 'Length of applicable errata found by search, does not match applicability count difference.' + # modifying package increased errata applicability count (outdated ver installed) + if prior_applicable_errata_count < host.applicable_errata_count: + # save the new errata(s) found, ones added since package modify + app_errata_with_package_diff = [ + errata + for errata in current_applicable_errata + if ( + any(package_basename in p for p in errata.packages) + and errata.errata_id not in prior_unique_errata_ids + ) + ] + # modifying package decreased errata applicability count (updated ver installed) + elif prior_applicable_errata_count > host.applicable_errata_count: + # save the old errata(s) found, ones removed since package modify + app_errata_with_package_diff = [ + errata + for errata in current_applicable_errata + if ( + not any(package_basename in p.filename for p in errata.packages) + and errata.errata_id in prior_unique_errata_ids + ) + ] + app_errata_diff_ids = errata_id_set(app_errata_with_package_diff) + assert len(app_errata_diff_ids) > 0, ( + f'Applicable errata count changed by {difference}, after modifying {package_filename},' + ' but could not find any affected errata(s) with packages list' + f' that contains a matching package_basename: {package_basename}.' ) + # Check that applicable_package_count changed, + # if not, an applicable package was not modified. + if prior_applicable_package_count == host.applicable_package_count: + # if applicable packages remains the same, errata should also be the same + assert prior_applicable_errata_count == host.applicable_errata_count + assert prior_unique_errata_ids == app_unique_errata_ids + # no applicable errata were impaced by package install + return False + # is current errata list different from one prior to package install ? + if app_unique_errata_ids != prior_unique_errata_ids: + difference = len(app_unique_errata_ids) - len(prior_unique_errata_ids) + # check diff in applicable counts, is equal to diff in length of errata search results. + assert prior_applicable_errata_count + difference == host.applicable_errata_count + + """ Check applicable_package count changed by one. + we expect applicable_errata_count increased/decrease, + only by number of 'new' or 'removed' applicable errata, if any. + """ + if app_errata_with_package_diff: + if host.applicable_errata_count > prior_applicable_errata_count: + """Current applicable errata count is higher than before install, + An outdated package is expected to have been installed. + Check applicable package count increased by one. + Check applicable errata count increased by number + of newly applicable errata. + """ + assert prior_applicable_package_count + 1 == host.applicable_package_count + expected_increase = 0 + if app_unique_errata_ids != prior_unique_errata_ids: + difference = len(app_unique_errata_ids) - prior_applicable_errata_count + assert prior_applicable_errata_count + difference == host.applicable_errata_count + expected_increase = len(app_errata_diff_ids) + assert prior_applicable_errata_count + expected_increase == host.applicable_errata_count + + elif host.applicable_errata_count < prior_applicable_errata_count: + """Current applicable errata count is lower than before install, + An updated package is expected to have been installed. + Check applicable package count decreased by one. + Check applicable errata count decreased by number of + prior applicable errata, that are no longer found. + """ + if host.applicable_errata_count < prior_applicable_errata_count: + assert host.applicable_package_count == prior_applicable_package_count - 1 + expected_decrease = 0 + if app_unique_errata_ids != prior_unique_errata_ids: + difference = len(app_unique_errata_ids) - len(prior_applicable_errata_count) + assert prior_applicable_errata_count + difference == host.applicable_errata_count + expected_decrease = len(app_errata_diff_ids) + assert prior_applicable_errata_count - expected_decrease == host.applicable_errata_count + else: + # We found by search an errata that was added or removed compared to prior install, + # But we also found that applicable_errata_count had not changed. + raise AssertionError( + f'Found one or more different errata: {app_errata_diff_ids},' + ' from those present prior to install, but applicable count did not change,' + f' {host.applicable_errata_count} were found, but expected {host.applicable_errata_count + len(app_errata_diff_ids)}.' + ) + else: + # already checked that applicable package count changed, + # but found applicable erratum list should not change, + # check the errata count and list remained the same. + assert ( + host.applicable_errata_count == prior_applicable_errata_count + ), 'Expected current applicable errata count, to equal prior applicable errata count.' + assert ( + len(current_applicable_errata) == prior_applicable_errata_count + ), 'Expected current applicable errata list length, to equal to prior applicable count.' + assert prior_unique_errata_ids == app_unique_errata_ids, ( + f'Expected set of prior applicable errata_ids: {prior_unique_errata_ids},' + f' to be equivalent to set of current applicable errata_ids: {app_unique_errata_ids}.' + ) + if return_applicables is True: + change_in_errata = len(app_unique_errata_ids) - prior_applicable_errata_count + output = host.execute(f'rpm -q {package_basename}').stdout + current_package = output[:-1] + assert package_basename in current_package + if current_package == package_filename: + # we have already checked if applicable package count changed, + # in case the same version as prior was installed and present. + prior_package = None # package must not have been present before this modification + else: + prior_package = package_filename + return { + 'result': True, + 'errata_count': host.applicable_errata_count, + 'package_count': host.applicable_package_count, + 'current_package': current_package, + 'prior_package': prior_package, + 'change_in_errata': change_in_errata, + 'changed_errata': list(app_errata_diff_ids), + } + return True + + +def cv_publish_promote(sat, org, cv, lce=None, needs_publish=True): + """Publish & promote Content View Version with all content visible in org. + + :param lce: if None, default to 'Library', + pass a single instance of lce, or list of instances. + :param bool needs_publish: if False, skip publish of a new version + :return dictionary: + 'content-view': instance of updated cv + 'content-view-version': instance of newest cv version + """ + # Default to 'Library' lce, if None passed + # Take a single instance of lce, or list of instances + lce_ids = 'Library' + if lce is not None: + lce_ids = [lce.id] if not isinstance(lce, list) else [_lce.id for _lce in lce] + + if needs_publish is True: + _publish_and_wait(sat, org, cv) + # Content-view must have at least one published version + cv = sat.api.ContentView(id=cv.id).read() + assert cv.version, f'No version(s) are published to the Content-View: {cv.id}' + # Find highest version id, will be the latest + cvv_id = max(cvv.id for cvv in cv.version) + # Promote to lifecycle-environment(s) + if lce_ids == 'Library': + library_lce = cv.environment[0].read() + sat.api.ContentViewVersion(id=cvv_id).promote( + data={'environment_ids': library_lce.id, 'force': 'True'} + ) + else: + sat.api.ContentViewVersion(id=cvv_id).promote(data={'environment_ids': lce_ids}) + _result = { + 'content-view': sat.api.ContentView(id=cv.id).read(), + 'content-view-version': sat.api.ContentViewVersion(id=cvv_id).read(), + } + assert all( + entry for entry in _result.values() + ), f'One or more necessary components are missing: {_result}' + return _result + + +def _publish_and_wait(sat, org, cv): + """Publish a new version of content-view to organization, wait for task(s) completion.""" + task_id = sat.api.ContentView(id=cv.id).publish({'id': cv.id, 'organization': org})['id'] + assert task_id, f'No task was invoked to publish the Content-View: {cv.id}.' + # Should take < 1 minute, check in 5s intervals + sat.wait_for_tasks( + search_query=(f'label = Actions::Katello::ContentView::Publish and id = {task_id}'), + search_rate=5, + max_tries=12, + ), ( + f'Failed to publish the Content-View: {cv.id}, in time.' + f'Task: {task_id} failed, or timed out (60s).' + ) @pytest.mark.upgrade @pytest.mark.tier3 -@pytest.mark.rhel_ver_list([7, 8, 9]) +@pytest.mark.rhel_ver_match('[^6]') @pytest.mark.no_containers -def test_positive_install_in_hc(module_org, activation_key, custom_repo, target_sat, content_hosts): +@pytest.mark.e2e +def test_positive_install_in_hc( + module_sca_manifest_org, + activation_key, + module_cv, + module_lce, + custom_repo, + target_sat, + content_hosts, +): """Install errata in a host-collection :id: 6f0242df-6511-4c0f-95fc-3fa32c63a064 - :Setup: Errata synced on satellite server. + :Setup: + 1. Some Unregistered hosts. + 2. Errata synced on satellite server. - :steps: PUT /api/v2/hosts/bulk/update_content + :Steps: + 1. Setup custom repo for each client, publish & promote content-view. + 2. Register clients as content hosts, install one outdated custom package on each client. + 3. Create Host Collection from clients, install errata to clients by Host Collection. + 4. PUT /api/v2/hosts/bulk/update_content + + :expectedresults: + 1. package install invokes errata applicability recalculate + 2. errata is installed in the host-collection + 3. errata installation invokes applicability recalculate + 4. updated custom package is found on the contained hosts + + :CaseImportance: Medium - :expectedresults: errata is installed in the host-collection. :BZ: 1983043 """ + # custom_repo already in published a module_cv version + repo_id = custom_repo['repository-id'] + # just promote to lce, do not publish + cv_publish_promote( + target_sat, module_sca_manifest_org, module_cv, module_lce, needs_publish=False + ) + # Each client: create custom repo, register as content host to cv, install outdated package for client in content_hosts: - client.install_katello_ca(target_sat) - client.register_contenthost(module_org.label, activation_key.name) - assert client.subscribed + _repo = target_sat.api.Repository(id=repo_id).read() + client.create_custom_repos(**{f'{_repo.name}': _repo.url}) + result = client.register( + org=module_sca_manifest_org, + activation_keys=activation_key.name, + target=target_sat, + loc=None, + ) + assert ( + result.status == 0 + ), f'Failed to register the host - {client.hostname}: {result.stderr}' client.add_rex_key(satellite=target_sat) - host_ids = [client.nailgun_host.id for client in content_hosts] - _install_package( - module_org, - clients=content_hosts, - host_ids=host_ids, - package_name=constants.FAKE_1_CUSTOM_PACKAGE, - ) - host_collection = target_sat.api.HostCollection(organization=module_org).create() + assert client.subscribed + client.run(r'subscription-manager repos --enable \*') + # Remove custom package by name + client.run(f'yum remove -y {FAKE_2_CUSTOM_PACKAGE_NAME}') + # No applicable errata or packages to start + assert (pre_errata_count := client.applicable_errata_count) == 0 + assert (pre_package_count := client.applicable_package_count) == 0 + prior_app_errata = _fetch_available_errata_instances(target_sat, client, expected_amount=0) + # 1s margin of safety for rounding + epoch_timestamp = int(time() - 1) + # install outdated version + assert client.run(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}').status == 0 + target_sat.api_factory.wait_for_errata_applicability_task( + host_id=client.nailgun_host.id, + from_when=epoch_timestamp, + ) + assert client.run(f'rpm -q {FAKE_1_CUSTOM_PACKAGE}').status == 0 + # One errata now applicable on client + assert client.applicable_errata_count == 1 + # One package now has an applicable errata + assert client.applicable_package_count == 1 + # Fetch the new errata instance(s), expecting only one + _fetch_available_errata_instances(target_sat, client, expected_amount=1) + + """ Did installing outdated package, update applicability as expected? + * Call method package_applicability_changed_as_expected * + returns: False if no applicability change occured or expected (package not applicable). + True if applicability changes were expected and occured (package is applicable). + raises: `AssertionError` if any expected changes did not occur, or unexpected changes were found. + + Expected: that each outdated package install: updated one or more errata to applicable, + if those now applicable errata(s) were not already applicable to some package prior. + """ + passed_checks = package_applicability_changed_as_expected( + target_sat, + client, + FAKE_1_CUSTOM_PACKAGE, + prior_app_errata, + pre_errata_count, + pre_package_count, + ) + assert ( + passed_checks is True + ), f'The package: {FAKE_1_CUSTOM_PACKAGE}, was not applicable to any erratum present on host: {client.hostname}.' + # Setup host collection using client ids + host_collection = target_sat.api.HostCollection(organization=module_sca_manifest_org).create() host_ids = [client.nailgun_host.id for client in content_hosts] host_collection.host_ids = host_ids host_collection = host_collection.update(['host_ids']) + # Install erratum to host collection task_id = target_sat.api.JobInvocation().run( data={ 'feature': 'katello_errata_install', 'inputs': {'errata': str(CUSTOM_REPO_ERRATA_ID)}, 'targeting_type': 'static_query', 'search_query': f'host_collection_id = {host_collection.id}', - 'organization_id': module_org.id, + 'organization_id': module_sca_manifest_org.id, }, )['id'] target_sat.wait_for_tasks( search_query=(f'label = Actions::RemoteExecution::RunHostsJob and id = {task_id}'), search_rate=15, max_tries=10, + ), ( + f'Could not install erratum: {CUSTOM_REPO_ERRATA_ID}, to Host-Collection.' + f' Task: {task_id} failed, or timed out.' ) for client in content_hosts: - result = client.run(f'rpm -q {constants.FAKE_2_CUSTOM_PACKAGE}') - assert result.status == 0 + # No applicable errata after install on all clients + assert ( + client.applicable_errata_count == 0 + ), f'A client in Host-Collection: {client.hostname}, had {client.applicable_errata_count} ' + 'applicable errata, expected 0.' + # Updated package is present on all clients + result = client.run(f'rpm -q {FAKE_2_CUSTOM_PACKAGE}') + assert result.status == 0, ( + f'The client in Host-Collection: {client.hostname},' + f' could not find the updated package: {FAKE_2_CUSTOM_PACKAGE}' + ) + # No applicable packages on client + assert client.applicable_package_count == 0, ( + f'A client in Host-Collection: {client.hostname}, had {client.applicable_package_count} ' + f'applicable package(s) after installing erratum: {CUSTOM_REPO_ERRATA_ID}, but expected 0.' + ) @pytest.mark.tier3 -@pytest.mark.rhel_ver_list([7, 8, 9]) +@pytest.mark.rhel_ver_match('[^6]') @pytest.mark.no_containers @pytest.mark.e2e def test_positive_install_multiple_in_host( - module_org, activation_key, custom_repo, rhel_contenthost, target_sat + target_sat, rhel_contenthost, module_org, activation_key, module_lce ): """For a host with multiple applicable errata install one and ensure - the rest of errata is still available + the rest of errata is still available, repeat for some list of errata. + After each package or errata install, check applicability updates + as expected. :id: 67b7e95b-9809-455a-a74e-f1815cc537fc + :setup: + 1. An Unregistered host. + 2. Errata synced on satellite server. + + :steps: + 1. Setup content for a content host (repos, cv, etc) + 2. Register vm as a content host + 3. Remove any impacted custom packages present + - no applicable errata to start + 4. Install outdated versions of the custom packages + - some expected applicable errata + 5. Install any applicable security errata + - errata applicability drops after each install + - applicable packages drops by amount updated + - impacted package(s) updated and found + + :expectedresults: + 1. Package installation succeeded, if the package makes a + new errata applicable; available errata counter + increased by one. + 2. Errata apply task succeeded, available errata + counter decreased by one; it is possible to schedule + another errata installation. + 3. Applicable package counter decreased by number + of updated packages. Updated package(s) found. + 4. Errata recalculate applicability task is invoked + automatically, after install command of applicable package, + and errata apply task. Task(s) found and finish successfully. + :customerscenario: true :BZ: 1469800, 1528275, 1983043, 1905560 - :expectedresults: errata installation task succeeded, available errata - counter decreased by one; it's possible to schedule another errata - installation - :CaseImportance: Medium :parametrized: yes + """ - rhel_contenthost.install_katello_ca(target_sat) - rhel_contenthost.register_contenthost(module_org.label, activation_key.name) + # Associate custom repos with org, lce, ak: + custom_repo_id = target_sat.cli_factory.setup_org_for_a_custom_repo( + { + 'url': settings.repos.yum_9.url, + 'organization-id': module_org.id, + 'lifecycle-environment-id': module_lce.id, + 'activationkey-id': activation_key.id, + } + )['repository-id'] + rhel_contenthost.register( + activation_keys=activation_key.name, + target=target_sat, + org=module_org, + loc=None, + ) assert rhel_contenthost.subscribed - host = rhel_contenthost.nailgun_host - for package in constants.FAKE_9_YUM_OUTDATED_PACKAGES: - _install_package( - module_org, clients=[rhel_contenthost], host_ids=[host.id], package_name=package + # 1s margin of safety for rounding + epoch_timestamp = int(time() - 1) + # Remove any packages errata could apply to, verify none are present on host + for package in FAKE_9_YUM_OUTDATED_PACKAGES: + pkg_name = str(package.split("-", 1)[0]) # 'bear-4.0-1.noarch' > 'bear' + result = rhel_contenthost.run(f'yum remove -y {pkg_name}') + assert rhel_contenthost.run(f'rpm -q {pkg_name}').status == 1 + + # Wait for any recalculate task(s), possibly invoked by yum remove, + # catch AssertionError raised if no task was generated + try: + target_sat.api_factory.wait_for_errata_applicability_task( + host_id=rhel_contenthost.nailgun_host.id, + from_when=epoch_timestamp, ) - applicable_errata_count = rhel_contenthost.applicable_errata_count - assert applicable_errata_count > 1 - rhel_contenthost.add_rex_key(satellite=target_sat) - for errata in settings.repos.yum_9.errata[1:4]: + except AssertionError: + # Yum remove did not trigger any errata recalculate task, + # assert any YUM_9 packages were/are not present, then continue + present_packages = set( + [ + package.filename + for package in target_sat.api.Package(repository=custom_repo_id).search() + ] + ) + assert not set(FAKE_9_YUM_OUTDATED_PACKAGES).intersection(present_packages) + assert not set(FAKE_9_YUM_UPDATED_PACKAGES).intersection(present_packages) + + # No applicable errata to start + assert rhel_contenthost.applicable_errata_count == 0 + present_applicable_packages = [] + # Installing all YUM_9 outdated custom packages + for i in range(len(FAKE_9_YUM_OUTDATED_PACKAGES)): + # record params prior to install, for post-install checks + package_filename = FAKE_9_YUM_OUTDATED_PACKAGES[i] + FAKE_9_YUM_UPDATED_PACKAGES[i] + pre_errata_count = rhel_contenthost.applicable_errata_count + pre_package_count = rhel_contenthost.applicable_package_count + prior_app_errata = _fetch_available_errata_instances(target_sat, rhel_contenthost) + # 1s margin of safety for rounding + epoch_timestamp = int(time() - 1) + assert rhel_contenthost.run(f'yum install -y {package_filename}').status == 0 + # Wait for async errata recalculate task(s), invoked by yum install, + # searching back 1s prior to install. + target_sat.api_factory.wait_for_errata_applicability_task( + host_id=rhel_contenthost.nailgun_host.id, + from_when=epoch_timestamp, + ) + # outdated package found on host + assert rhel_contenthost.run(f'rpm -q {package_filename}').status == 0 + """ + Modifying the applicable package did all: + 1. changed package applicability count by one and only one. + 2. changed errata applicability count by number of affected errata, whose + applicability status changed after package was modified. + 3. changed lists of applicable packages and applicable errata accordingly. + - otherwise raise `AssertionError` in below method; + """ + passed_checks = package_applicability_changed_as_expected( + target_sat, + rhel_contenthost, + package_filename, + prior_app_errata, + pre_errata_count, + pre_package_count, + ) + # If passed_checks is False, this package was not applicable, continue to next. + if passed_checks is True: + present_applicable_packages.append(package_filename) + + # Some applicable errata(s) now expected for outdated packages + assert rhel_contenthost.applicable_errata_count > 0 + # Expected applicable package(s) now for the applicable errata + assert rhel_contenthost.applicable_package_count == len(present_applicable_packages) + post_app_errata = _fetch_available_errata_instances(target_sat, rhel_contenthost) + """Installing all YUM_9 security errata sequentially, if applicable. + after each install, applicable-errata-count should drop by one, + one or more of the erratum's listed packages should be updated. + """ + installed_errata = [] + updated_packages = [] + expected_errata_to_install = [ + errata.errata_id + for errata in post_app_errata + if errata.errata_id in FAKE_9_YUM_SECURITY_ERRATUM + ] + all_applicable_packages = set( + package for errata in post_app_errata for package in errata.packages + ) + security_packages_to_install = set() + for errata_id in FAKE_9_YUM_SECURITY_ERRATUM: + errata_instance = ( + target_sat.api.Errata().search(query={'search': f'errata_id="{errata_id}"'})[0].read() + ) + present_packages_impacted_by_errata = [ + package + for package in errata_instance.packages + if package in FAKE_9_YUM_UPDATED_PACKAGES + ] + security_packages_to_install.update(present_packages_impacted_by_errata) + # Are expected security errata packages found in all applicable packages ? + assert security_packages_to_install.issubset(all_applicable_packages) + # Try to install each ERRATUM in FAKE_9_YUM_SECURITY_ERRATUM list, + # Each time, check lists of applicable erratum and packages, and counts + for ERRATUM in FAKE_9_YUM_SECURITY_ERRATUM: + pre_errata_count = rhel_contenthost.applicable_errata_count + ERRATUM_instance = ( + target_sat.api.Errata().search(query={'search': f'errata_id="{ERRATUM}"'})[0].read() + ) + # Check each time before each install + applicable_errata = _fetch_available_errata_instances(target_sat, rhel_contenthost) + # If this ERRATUM is not applicable, continue to next + if (len(applicable_errata) == 0) or ( + ERRATUM not in [_errata.errata_id for _errata in applicable_errata] + ): + continue + assert pre_errata_count >= 1 + errata_packages = [] + pre_package_count = rhel_contenthost.applicable_package_count + # From search result, find this ERRATUM by erratum_id, + # save the relevant list of package(s) + for _errata in applicable_errata: + if _errata.errata_id == ERRATUM: + errata_packages = _errata.packages + assert len(errata_packages) >= 1 + epoch_timestamp = int(time() - 1) + # Install this ERRATUM to host, wait for REX task task_id = target_sat.api.JobInvocation().run( data={ 'feature': 'katello_errata_install', - 'inputs': {'errata': str(errata)}, + 'inputs': {'errata': str(ERRATUM)}, 'targeting_type': 'static_query', 'search_query': f'name = {rhel_contenthost.hostname}', 'organization_id': module_org.id, @@ -252,23 +822,102 @@ def test_positive_install_multiple_in_host( search_rate=20, max_tries=15, ) - applicable_errata_count -= 1 - assert rhel_contenthost.applicable_errata_count == applicable_errata_count + # Wait for async errata recalculate task(s), invoked by REX task + target_sat.api_factory.wait_for_errata_applicability_task( + host_id=rhel_contenthost.nailgun_host.id, + from_when=epoch_timestamp, + ) + # Host Applicable Errata count decreased by one + assert ( + rhel_contenthost.applicable_errata_count == pre_errata_count - 1 + ), f'Host applicable errata did not decrease by one, after installation of {ERRATUM}' + # Applying this ERRATUM updated one or more of the erratum's listed packages + found_updated_packages = [] + for package in errata_packages: + result = rhel_contenthost.run(f'rpm -q {package}') + if result.status == 0: + assert ( + package in FAKE_9_YUM_UPDATED_PACKAGES + ), f'An unexpected package: "{package}", was updated by this errata: {ERRATUM}.' + if package in ERRATUM_instance.packages: + found_updated_packages.append(package) + + assert len(found_updated_packages) > 0, ( + f'None of the expected errata.packages: {errata_packages}, were found on host: "{rhel_contenthost.hostname}",' + f' after installing the applicable errata: {ERRATUM}.' + ) + # Host Applicable Packages count dropped by number of packages updated + assert rhel_contenthost.applicable_package_count == pre_package_count - len( + found_updated_packages + ), ( + f'Host: "{rhel_contenthost.hostname}" applicable package count did not decrease by {len(found_updated_packages)},' + f' after errata: {ERRATUM} installed updated packages: {found_updated_packages}' + ) + installed_errata.append(ERRATUM) + updated_packages.extend(found_updated_packages) + + # In case no ERRATUM in list are applicable: + # Lack of any package or errata install will raise `AssertionError`. + assert ( + len(installed_errata) > 0 + ), f'No applicable errata were found or installed from list: {FAKE_9_YUM_SECURITY_ERRATUM}.' + assert ( + len(updated_packages) > 0 + ), f'No applicable packages were found or installed from list: {FAKE_9_YUM_UPDATED_PACKAGES}.' + # Each expected erratum and packages installed only once + pkg_set = set(updated_packages) + errata_set = set(installed_errata) + assert len(pkg_set) == len( + updated_packages + ), f'Expect no repeat packages in install list: {updated_packages}.' + assert len(errata_set) == len( + installed_errata + ), f'Expected no repeat errata in install list: {installed_errata}.' + # Only the expected YUM_9 packages were installed + assert set(updated_packages).issubset(set(FAKE_9_YUM_UPDATED_PACKAGES)) + # Only the expected YUM_9 errata were updated + assert set(installed_errata).issubset(set(FAKE_9_YUM_SECURITY_ERRATUM)) + # Check number of installed errata id(s) matches expected + assert len(installed_errata) == len(expected_errata_to_install), ( + f'Expected to install {len(expected_errata_to_install)} errata from list: {FAKE_9_YUM_SECURITY_ERRATUM},' + f' but installed: {len(installed_errata)}.' + ) + # Check sets of installed errata id(s) strings, matches expected + assert set(installed_errata) == set( + expected_errata_to_install + ), 'Expected errata id(s) and installed errata id(s) are not the same.' + # Check number of updated package version filename(s) matches expected + assert len(updated_packages) == len(security_packages_to_install), ( + f'Expected to install {len(security_packages_to_install)} packages from list: {FAKE_9_YUM_UPDATED_PACKAGES},' + f' but installed {len(updated_packages)}.' + ) + # Check sets of installed package filename(s) strings, matches expected + assert set(updated_packages) == set( + security_packages_to_install + ), 'Expected package version filename(s) and installed package version filenam(s) are not the same.' @pytest.mark.tier3 @pytest.mark.skipif((not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url') -def test_positive_list(module_org, custom_repo, target_sat): - """View all errata specific to repository +def test_positive_list_sorted_filtered(custom_repo, target_sat): + """View, sort, and filter all errata specific to repository. :id: 1efceabf-9821-4804-bacf-2213ac0c7550 :Setup: Errata synced on satellite server. - :steps: Create two repositories each synced and containing errata + :Steps: + + 1. Create two repositories each synced and containing errata + 2. GET /katello/api/errata + + :expectedresults: + + 1. Check that the errata belonging to one repo is not + showing in the other. + 2. Check that the errata can be sorted by updated date, + issued date, and filtered by CVE. - :expectedresults: Check that the errata belonging to one repo is not - showing in the other. """ repo1 = target_sat.api.Repository(id=custom_repo['repository-id']).read() repo2 = target_sat.api.Repository( @@ -281,6 +930,7 @@ def test_positive_list(module_org, custom_repo, target_sat): repo2_errata_ids = [ errata['errata_id'] for errata in repo2.errata(data={'per_page': '1000'})['results'] ] + # Check errata are viewable, errata for one repo is not showing in the other assert len(repo1_errata_ids) == len(settings.repos.yum_9.errata) assert len(repo2_errata_ids) == len(settings.repos.yum_3.errata) assert CUSTOM_REPO_ERRATA_ID in repo1_errata_ids @@ -288,19 +938,7 @@ def test_positive_list(module_org, custom_repo, target_sat): assert settings.repos.yum_3.errata[5] in repo2_errata_ids assert settings.repos.yum_3.errata[5] not in repo1_errata_ids - -@pytest.mark.tier3 -def test_positive_list_updated(module_org, custom_repo, target_sat): - """View all errata in an Org sorted by Updated - - :id: 560d6584-70bd-4d1b-993a-cc7665a9e600 - - :Setup: Errata synced on satellite server. - - :steps: GET /katello/api/errata - - :expectedresults: Errata is filtered by Org and sorted by Updated date. - """ + # View all errata in Org sorted by Updated repo = target_sat.api.Repository(id=custom_repo['repository-id']).read() assert repo.sync()['result'] == 'success' erratum_list = target_sat.api.Errata(repository=repo).search( @@ -309,31 +947,17 @@ def test_positive_list_updated(module_org, custom_repo, target_sat): updated = [errata.updated for errata in erratum_list] assert updated == sorted(updated) - -@pytest.mark.tier3 -def test_positive_sorted_issue_date_and_filter_by_cve(module_org, custom_repo, target_sat): - """Sort by issued date and filter errata by CVE - - :id: a921d4c2-8d3d-4462-ba6c-fbd4b898a3f2 - - :Setup: Errata synced on satellite server. - - :steps: GET /katello/api/errata - - :expectedresults: Errata is sorted by issued date and filtered by CVE. - """ # Errata is sorted by issued date. erratum_list = target_sat.api.Errata(repository=custom_repo['repository-id']).search( query={'order': 'issued ASC', 'per_page': '1000'} ) issued = [errata.issued for errata in erratum_list] assert issued == sorted(issued) - # Errata is filtered by CVE erratum_list = target_sat.api.Errata(repository=custom_repo['repository-id']).search( query={'order': 'cve DESC', 'per_page': '1000'} ) - # Most of Errata don't have any CVEs. Removing empty CVEs from results + # Most Errata won't have any CVEs. Removing empty CVEs from results erratum_cves = [errata.cves for errata in erratum_list if errata.cves] # Verifying each errata have its CVEs sorted in DESC order for errata_cves in erratum_cves: @@ -342,66 +966,67 @@ def test_positive_sorted_issue_date_and_filter_by_cve(module_org, custom_repo, t @pytest.fixture(scope='module') -def setup_content_rhel6(module_entitlement_manifest_org, module_target_sat): - """Setup content fot rhel6 content host - Using `Red Hat Enterprise Virtualization Agents for RHEL 6 Server (RPMs)` - from manifest, SATTOOLS_REPO for host-tools and yum_9 repo as custom repo. - - :return: Activation Key, Organization, subscription list - """ - org = module_entitlement_manifest_org - rh_repo_id_rhva = module_target_sat.api_factory.enable_rhrepo_and_fetchid( - basearch='x86_64', - org_id=org.id, - product=constants.PRDS['rhel'], - repo=constants.REPOS['rhva6']['name'], - reposet=constants.REPOSET['rhva6'], - releasever=constants.DEFAULT_RELEASE_VERSION, - ) - rh_repo = module_target_sat.api.Repository(id=rh_repo_id_rhva).read() - rh_repo.sync() +def setup_content_rhel8( + module_sca_manifest_org, + rh_repo_module_manifest, + activation_key, + module_lce, + module_cv, + module_target_sat, + return_result=True, +): + """Setup content for rhel8 content host + Using RH SAT-TOOLS RHEL8 for sat-tools, and FAKE_YUM_9 as custom-repo. + Published to content-view and promoted to lifecycle-environment. - host_tools_product = module_target_sat.api.Product(organization=org).create() - host_tools_repo = module_target_sat.api.Repository( - product=host_tools_product, - ).create() - host_tools_repo.url = settings.repos.SATCLIENT_REPO.RHEL6 - host_tools_repo = host_tools_repo.update(['url']) - host_tools_repo.sync() + Raises `AssertionError` if one or more of the setup components read are empty. - custom_product = module_target_sat.api.Product(organization=org).create() - custom_repo = module_target_sat.api.Repository( - product=custom_product, - ).create() - custom_repo.url = CUSTOM_REPO_URL - custom_repo = custom_repo.update(['url']) + :return: if return_result is True: otherwise None + A dictionary (_result) with the satellite instances of activaton-key, organization, + content-view, lifecycle-environment, rh_repo, custom_repo. + """ + org = module_sca_manifest_org + # Setup Custom and RH repos + custom_repo_id = module_target_sat.cli_factory.setup_org_for_a_custom_repo( + { + 'url': CUSTOM_REPO_URL, + 'organization-id': org.id, + 'lifecycle-environment-id': module_lce.id, + 'activationkey-id': activation_key.id, + 'content-view-id': module_cv.id, + } + )['repository-id'] + custom_repo = module_target_sat.api.Repository(id=custom_repo_id).read() custom_repo.sync() - - lce = module_target_sat.api.LifecycleEnvironment(organization=org).create() - - cv = module_target_sat.api.ContentView( - organization=org, - repository=[rh_repo_id_rhva, host_tools_repo.id, custom_repo.id], - ).create() - cv.publish() - cvv = cv.read().version[0].read() - cvv.promote(data={'environment_ids': lce.id, 'force': False}) - - ak = module_target_sat.api.ActivationKey( - content_view=cv, organization=org, environment=lce - ).create() - - sub_list = [DEFAULT_SUBSCRIPTION_NAME, host_tools_product.name, custom_product.name] - for sub_name in sub_list: - subscription = module_target_sat.api.Subscription(organization=org).search( - query={'search': f'name="{sub_name}"'} - )[0] - ak.add_subscriptions(data={'subscription_id': subscription.id}) - return ak, org, sub_list + # Sync and add RH repo + rh_repo = module_target_sat.api.Repository(id=rh_repo_module_manifest.id).read() + rh_repo.sync() + module_target_sat.cli.ContentView.add_repository( + {'id': module_cv.id, 'organization-id': org.id, 'repository-id': rh_repo.id} + ) + _cv = cv_publish_promote(module_target_sat, org, module_cv, module_lce) + module_cv = _cv['content-view'] + latest_cvv = _cv['content-view-version'] + + _result = { + 'activation-key': activation_key.read(), + 'organization': org.read(), + 'content-view': module_cv.read(), + 'content-view-version': latest_cvv.read(), + 'lifecycle-environment': module_lce.read(), + 'rh_repo': rh_repo.read(), + 'custom_repo': custom_repo.read(), + } + assert all( + entry for entry in _result.values() + ), f'One or more necessary components are not present: {_result}' + return _result if return_result else None -@pytest.mark.tier3 -def test_positive_get_count_for_host(setup_content_rhel6, rhel6_contenthost, target_sat): +@pytest.mark.tier2 +def test_positive_get_count_for_host( + setup_content_rhel8, activation_key, rhel8_contenthost, module_target_sat +): """Available errata count when retrieving Host :id: 2f35933f-8026-414e-8f75-7f4ec048faae @@ -409,49 +1034,82 @@ def test_positive_get_count_for_host(setup_content_rhel6, rhel6_contenthost, tar :Setup: 1. Errata synced on satellite server. - 2. Some Content hosts present. + 2. Some client host present. + 3. Some rh repo and custom repo, added to content-view. + + :Steps: - :steps: GET /api/v2/hosts + 1. Register content host + 2. Install some outdated packages + 3. GET /api/v2/hosts + + :expectedresults: The applicable errata count is retrieved. - :expectedresults: The available errata count is retrieved. :parametrized: yes :CaseImportance: Medium """ - ak_name = setup_content_rhel6[0].name - org_label = setup_content_rhel6[1].label - org_id = setup_content_rhel6[1].id - sub_list = setup_content_rhel6[2] - rhel6_contenthost.install_katello_ca(target_sat) - rhel6_contenthost.register_contenthost(org_label, ak_name) - assert rhel6_contenthost.subscribed - pool_id = rhel6_contenthost.subscription_manager_get_pool(sub_list=sub_list) - pool_list = [pool_id[0][0]] - rhel6_contenthost.subscription_manager_attach_pool(pool_list=pool_list) - rhel6_contenthost.install_katello_host_tools() - rhel6_contenthost.enable_repo(constants.REPOS['rhva6']['id']) - host = rhel6_contenthost.nailgun_host + org = setup_content_rhel8['organization'] + custom_repo = setup_content_rhel8['rh_repo'] + rhel8_contenthost.create_custom_repos(**{f'{custom_repo.name}': custom_repo.url}) + result = rhel8_contenthost.register( + org=org, + activation_keys=activation_key.name, + target=module_target_sat, + loc=None, + ) + assert ( + result.status == 0 + ), f'Failed to register the host - {rhel8_contenthost.hostname}: {result.stderr}' + assert rhel8_contenthost.subscribed + rhel8_contenthost.execute(r'subscription-manager repos --enable \*') + host = rhel8_contenthost.nailgun_host.read() + # No applicable errata to start + assert rhel8_contenthost.applicable_errata_count == 0 for errata in ('security', 'bugfix', 'enhancement'): - _validate_errata_counts(org_id, host, errata_type=errata, expected_value=0) - rhel6_contenthost.run(f'yum install -y {constants.FAKE_1_CUSTOM_PACKAGE}') - _validate_errata_counts(org_id, host, errata_type='security', expected_value=1) - rhel6_contenthost.run(f'yum install -y {constants.REAL_0_RH_PACKAGE}') - _validate_errata_counts(org_id, host, errata_type='bugfix', expected_value=2) + _validate_errata_counts(host, errata_type=errata, expected_value=0) + # One bugfix errata after installing outdated Kangaroo + result = rhel8_contenthost.execute(f'yum install -y {FAKE_9_YUM_OUTDATED_PACKAGES[7]}') + assert result.status == 0, f'Failed to install package {FAKE_9_YUM_OUTDATED_PACKAGES[7]}' + _validate_errata_counts(host, errata_type='bugfix', expected_value=1) + # One enhancement errata after installing outdated Gorilla + result = rhel8_contenthost.execute(f'yum install -y {FAKE_9_YUM_OUTDATED_PACKAGES[3]}') + assert result.status == 0, f'Failed to install package {FAKE_9_YUM_OUTDATED_PACKAGES[3]}' + _validate_errata_counts(host, errata_type='enhancement', expected_value=1) + # Install and check two outdated packages, with applicable security erratum + # custom_repo outdated Walrus + result = rhel8_contenthost.execute(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}') + assert result.status == 0, f'Failed to install package {FAKE_1_CUSTOM_PACKAGE}' + _validate_errata_counts(host, errata_type='security', expected_value=1) + # rh_repo outdated Puppet-agent + result = rhel8_contenthost.execute(f'yum install -y {REAL_RHEL8_1_PACKAGE_FILENAME}') + assert result.status == 0, f'Failed to install package {REAL_RHEL8_1_PACKAGE_FILENAME}' + _validate_errata_counts(host, errata_type='security', expected_value=2) + # All avaliable errata present + assert rhel8_contenthost.applicable_errata_count == 4 @pytest.mark.upgrade @pytest.mark.tier3 -def test_positive_get_applicable_for_host(setup_content_rhel6, rhel6_contenthost, target_sat): +def test_positive_get_applicable_for_host( + setup_content_rhel8, activation_key, rhel8_contenthost, target_sat +): """Get applicable errata ids for a host :id: 51d44d51-eb3f-4ee4-a1df-869629d427ac :Setup: + 1. Errata synced on satellite server. - 2. Some Content hosts present. + 2. Some client hosts present. + 3. Some rh repo and custom repo, added to content-view. - :steps: GET /api/v2/hosts/:id/errata + :Steps: + + 1. Register vm as a content host + 2. Install some outdated packages + 3. GET /api/v2/hosts/:id/errata :expectedresults: The available errata is retrieved. @@ -459,30 +1117,44 @@ def test_positive_get_applicable_for_host(setup_content_rhel6, rhel6_contenthost :CaseImportance: Medium """ - ak_name = setup_content_rhel6[0].name - org_label = setup_content_rhel6[1].label - org_id = setup_content_rhel6[1].id - rhel6_contenthost.install_katello_ca(target_sat) - rhel6_contenthost.register_contenthost(org_label, ak_name) - assert rhel6_contenthost.subscribed - pool_id = rhel6_contenthost.subscription_manager_get_pool(sub_list=setup_content_rhel6[2]) - pool_list = [pool_id[0][0]] - rhel6_contenthost.subscription_manager_attach_pool(pool_list=pool_list) - rhel6_contenthost.install_katello_host_tools() - rhel6_contenthost.enable_repo(constants.REPOS['rhva6']['id']) - host = rhel6_contenthost.nailgun_host - erratum = _fetch_available_errata(org_id, host, expected_amount=0) + org = setup_content_rhel8['organization'] + custom_repo = setup_content_rhel8['rh_repo'] + + rhel8_contenthost.create_custom_repos(**{f'{custom_repo.name}': custom_repo.url}) + result = rhel8_contenthost.register( + activation_keys=activation_key.name, + target=target_sat, + org=org, + loc=None, + ) + assert ( + result.status == 0 + ), f'Failed to register the host - {rhel8_contenthost.hostname}: {result.stderr}' + assert rhel8_contenthost.subscribed + rhel8_contenthost.execute(r'subscription-manager repos --enable \*') + for errata in REPO_WITH_ERRATA['errata']: + # Remove custom package if present, old or new. + package_name = errata['package_name'] + result = rhel8_contenthost.execute(f'yum erase -y {package_name}') + if result.status != 0: + pytest.fail(f'Failed to remove {package_name}: {result.stdout} {result.stderr}') + + rhel8_contenthost.execute('subscription-manager repos') + assert rhel8_contenthost.applicable_errata_count == 0 + host = rhel8_contenthost.nailgun_host.read() + # Check no applicable errata to start + erratum = _fetch_available_errata(host, expected_amount=0) assert len(erratum) == 0 - rhel6_contenthost.run(f'yum install -y {constants.FAKE_1_CUSTOM_PACKAGE}') - erratum = _fetch_available_errata(org_id, host, 1) + # Install outdated applicable custom package + rhel8_contenthost.run(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}') + erratum = _fetch_available_errata(host, 1) assert len(erratum) == 1 assert CUSTOM_REPO_ERRATA_ID in [errata['errata_id'] for errata in erratum] - rhel6_contenthost.run(f'yum install -y {constants.REAL_0_RH_PACKAGE}') - erratum = _fetch_available_errata(org_id, host, 3) - assert len(erratum) == 3 - assert {constants.REAL_1_ERRATA_ID, constants.REAL_2_ERRATA_ID}.issubset( - {errata['errata_id'] for errata in erratum} - ) + # Install outdated applicable real package (from RH repo) + rhel8_contenthost.run(f'yum install -y {REAL_RHEL8_1_PACKAGE_FILENAME}') + erratum = _fetch_available_errata(host, 2) + assert len(erratum) == 2 + assert REAL_RHEL8_1_ERRATA_ID in [errata['errata_id'] for errata in erratum] @pytest.mark.tier3 @@ -497,15 +1169,17 @@ def test_positive_get_diff_for_cv_envs(target_sat): 1. Errata synced on satellite server. 2. Multiple environments present. - :steps: GET /katello/api/compare + :Steps: GET /katello/api/compare :expectedresults: Difference in errata between a set of environments for a content view is retrieved. + """ org = target_sat.api.Organization().create() env = target_sat.api.LifecycleEnvironment(organization=org).create() content_view = target_sat.api.ContentView(organization=org).create() activation_key = target_sat.api.ActivationKey(environment=env, organization=org).create() + # Published content-view-version with repos will be created for repo_url in [settings.repos.yum_9.url, CUSTOM_REPO_URL]: target_sat.cli_factory.setup_org_for_a_custom_repo( { @@ -517,32 +1191,34 @@ def test_positive_get_diff_for_cv_envs(target_sat): } ) new_env = target_sat.api.LifecycleEnvironment(organization=org, prior=env).create() - cvvs = content_view.read().version[-2:] - cvvs[-1].promote(data={'environment_ids': new_env.id, 'force': False}) + # no need to publish a new version, just promote newest + cv_publish_promote( + sat=target_sat, org=org, cv=content_view, lce=[env, new_env], needs_publish=False + ) + content_view = target_sat.api.ContentView(id=content_view.id).read() + # Get last two versions by id to compare + cvv_ids = sorted(cvv.id for cvv in content_view.version)[-2:] result = target_sat.api.Errata().compare( - data={'content_view_version_ids': [cvv.id for cvv in cvvs], 'per_page': '9999'} + data={'content_view_version_ids': [cvv_id for cvv_id in cvv_ids], 'per_page': '9999'} ) cvv2_only_errata = next( errata for errata in result['results'] if errata['errata_id'] == CUSTOM_REPO_ERRATA_ID ) - assert cvvs[-1].id in cvv2_only_errata['comparison'] + assert cvv_ids[-1] in cvv2_only_errata['comparison'] both_cvvs_errata = next( - errata - for errata in result['results'] - if errata['errata_id'] in constants.FAKE_9_YUM_SECURITY_ERRATUM + errata for errata in result['results'] if errata['errata_id'] in FAKE_9_YUM_SECURITY_ERRATUM ) - assert {cvv.id for cvv in cvvs} == set(both_cvvs_errata['comparison']) + assert {cvv_id for cvv_id in cvv_ids} == set(both_cvvs_errata['comparison']) @pytest.mark.tier3 def test_positive_incremental_update_required( - module_org, + module_sca_manifest_org, module_lce, activation_key, module_cv, - custom_repo, - rh_repo, - rhel7_contenthost, + rh_repo_module_manifest, + rhel8_contenthost, target_sat, ): """Given a set of hosts and errata, check for content view version @@ -553,7 +1229,7 @@ def test_positive_incremental_update_required( :Setup: 1. Errata synced on satellite server - :steps: + :Steps: 1. Create VM as Content Host, registering to CV with custom errata 2. Install package in VM so it needs one erratum 3. Check if incremental_updates required: @@ -573,27 +1249,38 @@ def test_positive_incremental_update_required( :BZ: 2013093 """ - rhel7_contenthost.install_katello_ca(target_sat) - rhel7_contenthost.register_contenthost(module_org.label, activation_key.name) - assert rhel7_contenthost.subscribed - rhel7_contenthost.enable_repo(constants.REPOS['rhst7']['id']) - rhel7_contenthost.install_katello_agent() - host = rhel7_contenthost.nailgun_host - # install package to create demand for an Erratum - _install_package( - module_org, - [rhel7_contenthost], - [host.id], - constants.FAKE_1_CUSTOM_PACKAGE, - via_ssh=True, - rpm_package_name=constants.FAKE_1_CUSTOM_PACKAGE, + org = module_sca_manifest_org + rh_repo = target_sat.api.Repository( + id=rh_repo_module_manifest.id, + ).read() + rh_repo.sync() + # Add RH repo to content-view + target_sat.cli.ContentView.add_repository( + {'id': module_cv.id, 'organization-id': org.id, 'repository-id': rh_repo.id} ) + module_cv = target_sat.api.ContentView(id=module_cv.id).read() + _cv = cv_publish_promote(target_sat, org, module_cv, module_lce) + module_cv = _cv['content-view'] + + result = rhel8_contenthost.register( + org=org, + activation_keys=activation_key.name, + target=target_sat, + loc=None, + ) + assert result.status == 0, f'Failed to register the host: {rhel8_contenthost.hostname}' + assert rhel8_contenthost.subscribed + rhel8_contenthost.execute(r'subscription-manager repos --enable \*') + host = rhel8_contenthost.nailgun_host.read() + # install package to create demand for an Erratum + result = rhel8_contenthost.run(f'yum install -y {REAL_RHEL8_1_PACKAGE_FILENAME}') + assert result.status == 0, f'Failed to install package: {REAL_RHEL8_1_PACKAGE_FILENAME}' # Call nailgun to make the API POST to see if any incremental updates are required response = target_sat.api.Host().bulk_available_incremental_updates( data={ - 'organization_id': module_org.id, + 'organization_id': org.id, 'included': {'ids': [host.id]}, - 'errata_ids': [settings.repos.yum_6.errata[2]], + 'errata_ids': [REAL_RHEL8_1_ERRATA_ID], }, ) assert not response, 'Incremental update should not be required at this point' @@ -602,25 +1289,22 @@ def test_positive_incremental_update_required( target_sat.api.RPMContentViewFilter( content_view=module_cv, inclusion=True, name='Include Nothing' ).create() - module_cv.publish() - module_cv = module_cv.read() - CV1V = module_cv.version[-1].read() - # Must promote a CV version into a new Environment before we can add errata - CV1V.promote(data={'environment_ids': module_lce.id, 'force': False}) - module_cv = module_cv.read() + module_cv = target_sat.api.ContentView(id=module_cv.id).read() + module_cv = cv_publish_promote(target_sat, org, module_cv, module_lce)['content-view'] + rhel8_contenthost.execute('subscription-manager repos') # Call nailgun to make the API POST to ensure an incremental update is required response = target_sat.api.Host().bulk_available_incremental_updates( data={ - 'organization_id': module_org.id, + 'organization_id': org.id, 'included': {'ids': [host.id]}, - 'errata_ids': [settings.repos.yum_6.errata[2]], + 'errata_ids': [REAL_RHEL8_1_ERRATA_ID], }, ) - assert 'next_version' in response[0], 'Incremental update should be suggested' - 'at this point' + assert response, 'Nailgun response for host(s) with avaliable incremental update was None' + assert 'next_version' in response[0], 'Incremental update should be suggested at this point' -def _run_remote_command_on_content_host(module_org, command, vm, return_result=False): +def _run_remote_command_on_content_host(command, vm, return_result=False): result = vm.run(command) assert result.status == 0 if return_result: @@ -628,18 +1312,18 @@ def _run_remote_command_on_content_host(module_org, command, vm, return_result=F return None -def _set_prerequisites_for_swid_repos(module_org, vm): +def _set_prerequisites_for_swid_repos(vm): _run_remote_command_on_content_host( - module_org, f'curl --insecure --remote-name {settings.repos.swid_tools_repo}', vm + f'curl --insecure --remote-name {settings.repos.swid_tools_repo}', vm ) - _run_remote_command_on_content_host(module_org, "mv *swid*.repo /etc/yum.repos.d", vm) - _run_remote_command_on_content_host(module_org, "yum install -y swid-tools", vm) - _run_remote_command_on_content_host(module_org, "dnf install -y dnf-plugin-swidtags", vm) + _run_remote_command_on_content_host('mv *swid*.repo /etc/yum.repos.d', vm) + _run_remote_command_on_content_host('yum install -y swid-tools', vm) + _run_remote_command_on_content_host('yum install -y dnf-plugin-swidtags', vm) -def _validate_swid_tags_installed(module_org, vm, module_name): +def _validate_swid_tags_installed(vm, module_name): result = _run_remote_command_on_content_host( - module_org, f"swidq -i -n {module_name} | grep 'Name'", vm, return_result=True + f"swidq -i -n {module_name} | grep 'Name'", vm, return_result=True ) assert module_name in result @@ -647,14 +1331,13 @@ def _validate_swid_tags_installed(module_org, vm, module_name): @pytest.mark.tier3 @pytest.mark.upgrade @pytest.mark.pit_client -@pytest.mark.parametrize( - 'module_repos_collection_with_manifest', - [{'YumRepository': {'url': settings.repos.swid_tag.url, 'distro': 'rhel8'}}], - indirect=True, -) @pytest.mark.no_containers def test_errata_installation_with_swidtags( - module_org, module_lce, module_repos_collection_with_manifest, rhel8_contenthost, target_sat + module_sca_manifest_org, + module_lce, + module_cv, + rhel8_contenthost, + target_sat, ): """Verify errata installation with swid_tags and swid tags get updated after module stream update. @@ -663,15 +1346,18 @@ def test_errata_installation_with_swidtags( :steps: - 1. create product and repository having swid tags - 2. create content view and published it with repository - 3. create activation key and register content host - 4. create rhel8, swid repos on content host - 5. install swid-tools, dnf-plugin-swidtags packages on content host - 6. install older module stream and generate errata, swid tag - 7. assert errata count, swid tags are generated - 8. install errata vis updating module stream - 9. assert errata count and swid tag after module update + 1. promote empty content view and create activation key + 2. create product and repository having swid tags + 3. create rhel8, swid repos on content host + 4. create custom repo with applicable module stream packages + 5. associate repositories and cv / ak to contenthost, sync all content + 6. publish & promote content view version with all content + 7. register host using cv's activation key + 8. install swid-tools, dnf-plugin-swidtags packages on content host + 9. install older module stream and generate errata, swid tag + 10. assert errata count, swid tags are generated + 11. install errata via updating module stream + 12. assert errata count and swid tag changed after module update :expectedresults: swid tags should get updated after errata installation via module stream update @@ -681,215 +1367,114 @@ def test_errata_installation_with_swidtags( :parametrized: yes :CaseImportance: Critical + """ module_name = 'kangaroo' version = '20180704111719' - # setup rhel8 and sat_tools_repos - rhel8_contenthost.create_custom_repos( - **{ - 'baseos': settings.repos.rhel8_os.baseos, - 'appstream': settings.repos.rhel8_os.appstream, - } + # new cv version for ak + cv_publish_promote( + target_sat, + module_sca_manifest_org, + module_cv, + module_lce, ) - module_repos_collection_with_manifest.setup_virtual_machine( - rhel8_contenthost, install_katello_agent=False + _ak = target_sat.api.ActivationKey( + organization=module_sca_manifest_org, + environment=module_lce, + content_view=module_cv, + ).create() + # needed repos for module stream, swid tags, prereqs + _repos = { + 'base_os': settings.repos.rhel8_os.baseos, + 'sat_tools': settings.repos.rhel8_os.appstream, + 'swid_tags': settings.repos.swid_tag.url, + } + # associate repos with host, org, lce, and sync + rhel8_contenthost.create_custom_repos(**_repos) + for _key in _repos: + target_sat.cli_factory.setup_org_for_a_custom_repo( + { + 'url': _repos[_key], + 'name': _key, + 'organization-id': module_sca_manifest_org.id, + 'lifecycle-environment-id': module_lce.id, + 'activationkey-id': _ak.id, + }, + ) + # promote new version with all repos/content + cv_publish_promote( + target_sat, + module_sca_manifest_org, + module_cv, + module_lce, ) - - # install older module stream - rhel8_contenthost.add_rex_key(satellite=target_sat) - _set_prerequisites_for_swid_repos(module_org, vm=rhel8_contenthost) - _run_remote_command_on_content_host( - module_org, f'dnf -y module install {module_name}:0:{version}', rhel8_contenthost + # register host with ak, succeeds + result = rhel8_contenthost.register( + activation_keys=_ak.name, + target=target_sat, + org=module_sca_manifest_org, + loc=None, ) - target_sat.cli.Host.errata_recalculate({'host-id': rhel8_contenthost.nailgun_host.id}) + assert result.status == 0, f'Failed to register the host {target_sat.hostname},\n{result}' + rhel8_contenthost.add_rex_key(satellite=target_sat) + assert ( + rhel8_contenthost.subscribed + ), f'Failed to subscribe the host {target_sat.hostname}, to content.' + rhel8_contenthost.install_katello_ca(target_sat) + result = rhel8_contenthost.execute(r'subscription-manager repos --enable \*') + assert result.status == 0, f'Failed to enable repositories with subscription-manager,\n{result}' + + # install outdated module stream package + _set_prerequisites_for_swid_repos(rhel8_contenthost) + result = rhel8_contenthost.execute(f'dnf -y module install {module_name}:0:{version}') + assert ( + result.status == 0 + ), f'Failed to install module stream package: {module_name}:0:{version}.\n{result.stdout}' + # recalculate errata after install of old module stream + rhel8_contenthost.execute('subscription-manager repos') + # validate swid tags Installed before_errata_apply_result = _run_remote_command_on_content_host( - module_org, f"swidq -i -n {module_name} | grep 'File' | grep -o 'rpm-.*.swidtag'", rhel8_contenthost, return_result=True, ) assert before_errata_apply_result != '' - applicable_errata_count = rhel8_contenthost.applicable_errata_count - assert applicable_errata_count == 1 + assert (applicable_errata_count := rhel8_contenthost.applicable_errata_count) == 1 # apply modular errata - _run_remote_command_on_content_host( - module_org, f'dnf -y module update {module_name}', rhel8_contenthost - ) - _run_remote_command_on_content_host(module_org, 'dnf -y upload-profile', rhel8_contenthost) - target_sat.cli.Host.errata_recalculate({'host-id': rhel8_contenthost.nailgun_host.id}) + result = rhel8_contenthost.execute(f'dnf -y module update {module_name}') + assert ( + result.status == 0 + ), f'Failed to updated module stream package: {module_name}.\n{result.stdout}' + assert rhel8_contenthost.execute('dnf -y upload-profile').status == 0 + + # recalculate and check errata after modular update + rhel8_contenthost.execute('subscription-manager repos') applicable_errata_count -= 1 assert rhel8_contenthost.applicable_errata_count == applicable_errata_count + # swidtags were updated based on package version after_errata_apply_result = _run_remote_command_on_content_host( - module_org, - f"swidq -i -n {module_name} | grep 'File'| grep -o 'rpm-.*.swidtag'", + f"swidq -i -n {module_name} | grep 'File' | grep -o 'rpm-.*.swidtag'", rhel8_contenthost, return_result=True, ) - - # swidtags get updated based on package version assert before_errata_apply_result != after_errata_apply_result -"""Section for tests using RHEL8 Content Host. - The applicability tests using Default Content View are related to the introduction of Pulp3. - """ - - @pytest.fixture(scope='module') -def rh_repo_module_manifest(module_entitlement_manifest_org, module_target_sat): +def rh_repo_module_manifest(module_sca_manifest_org, module_target_sat): """Use module manifest org, creates tools repo, syncs and returns RH repo.""" # enable rhel repo and return its ID rh_repo_id = module_target_sat.api_factory.enable_rhrepo_and_fetchid( - basearch=constants.DEFAULT_ARCHITECTURE, - org_id=module_entitlement_manifest_org.id, - product=constants.PRDS['rhel8'], - repo=constants.REPOS['rhst8']['name'], - reposet=constants.REPOSET['rhst8'], + basearch=DEFAULT_ARCHITECTURE, + org_id=module_sca_manifest_org.id, + product=PRDS['rhel8'], + repo=REPOS['rhst8']['name'], + reposet=REPOSET['rhst8'], releasever='None', ) # Sync step because repo is not synced by default rh_repo = module_target_sat.api.Repository(id=rh_repo_id).read() rh_repo.sync() return rh_repo - - -@pytest.fixture(scope='module') -def rhel8_custom_repo_cv(module_entitlement_manifest_org, module_target_sat): - """Create repo and publish CV so that packages are in Library""" - return module_target_sat.cli_factory.setup_org_for_a_custom_repo( - { - 'url': settings.repos.module_stream_1.url, - 'organization-id': module_entitlement_manifest_org.id, - } - ) - - -@pytest.fixture(scope='module') -def rhel8_module_ak( - module_entitlement_manifest_org, - default_lce, - rh_repo_module_manifest, - rhel8_custom_repo_cv, - module_target_sat, -): - rhel8_module_ak = module_target_sat.api.ActivationKey( - content_view=module_entitlement_manifest_org.default_content_view, - environment=module_target_sat.api.LifecycleEnvironment( - id=module_entitlement_manifest_org.library.id - ), - organization=module_entitlement_manifest_org, - ).create() - # Ensure tools repo is enabled in the activation key - rhel8_module_ak.content_override( - data={ - 'content_overrides': [{'content_label': constants.REPOS['rhst8']['id'], 'value': '1'}] - } - ) - # Fetch available subscriptions - subs = module_target_sat.api.Subscription(organization=module_entitlement_manifest_org).search( - query={'search': f'{constants.DEFAULT_SUBSCRIPTION_NAME}'} - ) - assert subs - # Add default subscription to activation key - rhel8_module_ak.add_subscriptions(data={'subscription_id': subs[0].id}) - # Add custom subscription to activation key - product = module_target_sat.api.Product(organization=module_entitlement_manifest_org).search( - query={'search': 'redhat=false'} - ) - custom_sub = module_target_sat.api.Subscription( - organization=module_entitlement_manifest_org - ).search(query={'search': f'name={product[0].name}'}) - rhel8_module_ak.add_subscriptions(data={'subscription_id': custom_sub[0].id}) - return rhel8_module_ak - - -@pytest.mark.tier2 -def test_apply_modular_errata_using_default_content_view( - module_entitlement_manifest_org, - default_lce, - rhel8_contenthost, - rhel8_module_ak, - rhel8_custom_repo_cv, - target_sat, -): - """ - Registering a RHEL8 system to the default content view with no modules enabled results in - no modular errata or packages showing as applicable or installable - - Enabling a module on a RHEL8 system assigned to the default content view and installing an - older package should result in the modular errata and package showing as applicable and - installable - - :id: 030981dd-19ba-4f8b-9c24-0aee90aaa4c4 - - Steps: - 1. Register host with AK, install tools - 2. Assert no errata indicated - 3. Install older version of stream - 4. Assert errata is applicable - 5. Update module stream - 6. Assert errata is no longer applicable - - :expectedresults: Errata enumeration works with module streams when using default Content View - - :CaseAutomation: Automated - - :parametrized: yes - """ - module_name = 'duck' - stream = '0' - version = '20180704244205' - - rhel8_contenthost.install_katello_ca(target_sat) - rhel8_contenthost.register_contenthost( - module_entitlement_manifest_org.label, rhel8_module_ak.name - ) - assert rhel8_contenthost.subscribed - host = rhel8_contenthost.nailgun_host - host = host.read() - # Assert no errata on host, no packages applicable or installable - errata = _fetch_available_errata(module_entitlement_manifest_org, host, expected_amount=0) - assert len(errata) == 0 - rhel8_contenthost.install_katello_host_tools() - # Install older version of module stream to generate the errata - result = rhel8_contenthost.execute( - f'yum -y module install {module_name}:{stream}:{version}', - ) - assert result.status == 0 - # Check that there is now two errata applicable - errata = _fetch_available_errata(module_entitlement_manifest_org, host, 2) - target_sat.cli.Host.errata_recalculate({'host-id': rhel8_contenthost.nailgun_host.id}) - assert len(errata) == 2 - # Assert that errata package is required - assert constants.FAKE_3_CUSTOM_PACKAGE in errata[0]['module_streams'][0]['packages'] - # Update module - result = rhel8_contenthost.execute( - f'yum -y module update {module_name}:{stream}:{version}', - ) - assert result.status == 0 - # Check that there is now no errata applicable - errata = _fetch_available_errata(module_entitlement_manifest_org, host, 0) - assert len(errata) == 0 - - @pytest.mark.tier2 - @pytest.mark.skip("Uses old large_errata repo from repos.fedorapeople") - def test_positive_sync_repos_with_large_errata(target_sat): - """Attempt to synchronize 2 repositories containing large (or lots of) - errata. - - :id: d6680b9f-4c88-40b4-8b96-3d170664cb28 - - :customerscenario: true - - :BZ: 1463811 - - :expectedresults: both repositories were successfully synchronized - """ - org = target_sat.api.Organization().create() - for _ in range(2): - product = target_sat.api.Product(organization=org).create() - repo = target_sat.api.Repository(product=product, url=settings.repos.yum_7.url).create() - response = repo.sync() - assert response, f"Repository {repo} failed to sync." From 1cac6b106e0cd87143c0e76a43632e87f7443145 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 22 Mar 2024 12:31:41 -0400 Subject: [PATCH 554/586] [6.14.z] Modify VMware compute profile test (#14496) --- robottelo/constants/__init__.py | 1 - .../foreman/ui/test_computeresource_vmware.py | 241 +++++++----------- 2 files changed, 98 insertions(+), 144 deletions(-) diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index f9673326022..2adda82a9bc 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -1786,7 +1786,6 @@ class Colored(Box): VMWARE_CONSTANTS = { 'folder': 'vm', - 'guest_os': 'Red Hat Enterprise Linux 8 (64 bit)', 'scsicontroller': 'LSI Logic Parallel', 'virtualhw_version': 'Default', 'pool': 'Resources', diff --git a/tests/foreman/ui/test_computeresource_vmware.py b/tests/foreman/ui/test_computeresource_vmware.py index 833d8100e05..9ef57de8e6c 100644 --- a/tests/foreman/ui/test_computeresource_vmware.py +++ b/tests/foreman/ui/test_computeresource_vmware.py @@ -27,6 +27,7 @@ VMWARE_CONSTANTS, ) from robottelo.utils.datafactory import gen_string +from robottelo.utils.issue_handlers import is_open pytestmark = [pytest.mark.skip_if_not_set('vmware')] @@ -290,32 +291,64 @@ def test_positive_resource_vm_power_management(session, vmware): raise AssertionError('Timed out waiting for VM to toggle power state') from err +@pytest.mark.e2e +@pytest.mark.upgrade @pytest.mark.tier2 @pytest.mark.parametrize('vmware', ['vmware7', 'vmware8'], indirect=True) -def test_positive_select_vmware_custom_profile_guest_os_rhel7(session, vmware): - """Select custom default (3-Large) compute profile guest OS RHEL7. +def test_positive_vmware_custom_profile_end_to_end(session, vmware, request, target_sat): + """Perform end to end testing for VMware compute profile. :id: 24f7bb5f-2aaf-48cb-9a56-d2d0713dfe3d :customerscenario: true - :setup: vmware hostname and credentials. - :steps: 1. Create a compute resource of type vmware. - 2. Provide valid hostname, username and password. - 3. Select the created vmware CR. - 4. Click Compute Profile tab. - 5. Select 3-Large profile - 6. Set Guest OS field to RHEL7 OS. + 2. Update a compute profile with all values - :expectedresults: Guest OS RHEL7 is selected successfully. + :expectedresults: Compute profiles are updated successfully with all the values. - :BZ: 1315277 + :BZ: 1315277, 2266672 """ cr_name = gen_string('alpha') - guest_os_name = 'Red Hat Enterprise Linux 7 (64-bit)' + guest_os_names = [ + 'Red Hat Enterprise Linux 7 (64-bit)', + 'Red Hat Enterprise Linux 8 (64 bit)', + 'Red Hat Enterprise Linux 9 (64 bit)', + ] + compute_profile = ['1-Small', '2-Medium', '3-Large'] + cpus = ['2', '4', '6'] + vm_memory = ['4000', '6000', '8000'] + annotation_notes = gen_string('alpha') + firmware_type = ['Automatic', 'BIOS', 'EFI'] + resource_pool = VMWARE_CONSTANTS['pool'] + folder = VMWARE_CONSTANTS['folder'] + virtual_hw_version = VMWARE_CONSTANTS['virtualhw_version'] + memory_hot_add = True + cpu_hot_add = True + cdrom_drive = True + disk_size = '10 GB' + network = 'VLAN 1001' # hardcoding network here as this test won't be doing actual provisioning + data_store_summary_string = _get_vmware_datastore_summary_string(vmware=vmware) + storage_data = { + 'storage': { + 'controller': VMWARE_CONSTANTS['scsicontroller'], + 'disks': [ + { + 'data_store': data_store_summary_string, + 'size': disk_size, + 'thin_provision': True, + } + ], + } + } + network_data = { + 'network_interfaces': { + 'nic_type': VMWARE_CONSTANTS['network_interface_name'], + 'network': network, + } + } with session: session.computeresource.create( { @@ -327,143 +360,65 @@ def test_positive_select_vmware_custom_profile_guest_os_rhel7(session, vmware): 'provider_content.datacenter.value': settings.vmware.datacenter, } ) - assert session.computeresource.search(cr_name)[0]['Name'] == cr_name - session.computeresource.update_computeprofile( - cr_name, COMPUTE_PROFILE_LARGE, {'provider_content.guest_os': guest_os_name} - ) - values = session.computeresource.read_computeprofile(cr_name, COMPUTE_PROFILE_LARGE) - assert values['provider_content']['guest_os'] == guest_os_name - - -@pytest.mark.tier2 -@pytest.mark.parametrize('vmware', ['vmware7', 'vmware8'], indirect=True) -def test_positive_access_vmware_with_custom_profile(session, vmware): - """Associate custom default (3-Large) compute profile - :id: 751ef765-5091-4322-a0d9-0c9c73009cc4 + @request.addfinalizer + def _finalize(): + cr = target_sat.api.VMWareComputeResource().search(query={'search': f'name={cr_name}'}) + if cr: + target_sat.api.VMWareComputeResource(id=cr[0].id).delete() - :setup: vmware hostname and credentials. - - :steps: - - 1. Create a compute resource of type vmware. - 2. Provide valid hostname, username and password. - 3. Select the created vmware CR. - 4. Click Compute Profile tab. - 5. Edit (3-Large) with valid configurations and submit. - - :expectedresults: The Compute Resource created and associated to compute profile (3-Large) - with provided values. - """ - cr_name = gen_string('alpha') - data_store_summary_string = _get_vmware_datastore_summary_string(vmware=vmware) - cr_profile_data = dict( - cpus='2', - cores_per_socket='2', - memory='1024', - firmware='EFI', - cluster=settings.vmware.cluster, - resource_pool=VMWARE_CONSTANTS.get('pool'), - folder=VMWARE_CONSTANTS.get('folder'), - guest_os=VMWARE_CONSTANTS.get('guest_os'), - virtual_hw_version=VMWARE_CONSTANTS.get('virtualhw_version'), - memory_hot_add=True, - cpu_hot_add=True, - cdrom_drive=True, - annotation_notes=gen_string('alpha'), - network_interfaces=[] - if not settings.provisioning.vlan_id - else [ - dict( - nic_type=VMWARE_CONSTANTS.get('network_interface_name'), - network='VLAN 1001', # hardcoding network here as these test won't be doing actual provisioning - ), - dict( - nic_type=VMWARE_CONSTANTS.get('network_interface_name'), - network='VLAN 1001', - ), - ], - storage=[ - dict( - controller=VMWARE_CONSTANTS.get('scsicontroller'), - disks=[ - dict( - data_store=data_store_summary_string, - size='10 GB', - thin_provision=True, - ), - dict( - data_store=data_store_summary_string, - size='20 GB', - thin_provision=False, - eager_zero=False, - ), - ], - ), - dict( - controller=VMWARE_CONSTANTS.get('scsicontroller'), - disks=[ - dict( - data_store=data_store_summary_string, - size='30 GB', - thin_provision=False, - eager_zero=True, - ) - ], - ), - ], - ) - with session: - session.computeresource.create( - { - 'name': cr_name, - 'provider': FOREMAN_PROVIDERS['vmware'], - 'provider_content.vcenter': vmware.hostname, - 'provider_content.user': settings.vmware.username, - 'provider_content.password': settings.vmware.password, - 'provider_content.datacenter.value': settings.vmware.datacenter, - } - ) assert session.computeresource.search(cr_name)[0]['Name'] == cr_name - session.computeresource.update_computeprofile( - cr_name, - COMPUTE_PROFILE_LARGE, - {f'provider_content.{key}': value for key, value in cr_profile_data.items()}, - ) - values = session.computeresource.read_computeprofile(cr_name, COMPUTE_PROFILE_LARGE) - provider_content = values['provider_content'] - # assert main compute resource profile data updated successfully. - excluded_keys = ['network_interfaces', 'storage'] - expected_value = { - key: value for key, value in cr_profile_data.items() if key not in excluded_keys - } - provided_value = { - key: value for key, value in provider_content.items() if key in expected_value - } - assert provided_value == expected_value - # assert compute resource profile network data updated successfully. - for network_index, expected_network_value in enumerate( - cr_profile_data['network_interfaces'] + for guest_os_name, cprofile, cpu, memory, firmware in zip( + guest_os_names, compute_profile, cpus, vm_memory, firmware_type, strict=True ): - provided_network_value = { - key: value - for key, value in provider_content['network_interfaces'][network_index].items() - if key in expected_network_value - } - assert provided_network_value == expected_network_value - # assert compute resource profile storage data updated successfully. - for controller_index, expected_controller_value in enumerate(cr_profile_data['storage']): - provided_controller_value = provider_content['storage'][controller_index] + session.computeresource.update_computeprofile( + cr_name, + cprofile, + { + 'provider_content.guest_os': guest_os_name, + 'provider_content.cpus': cpu, + 'provider_content.memory': memory, + 'provider_content.cluster': settings.vmware.cluster, + 'provider_content.annotation_notes': annotation_notes, + 'provider_content.virtual_hw_version': virtual_hw_version, + 'provider_content.firmware': firmware, + 'provider_content.resource_pool': resource_pool, + 'provider_content.folder': folder, + 'provider_content.memory_hot_add': memory_hot_add, + 'provider_content.cpu_hot_add': cpu_hot_add, + 'provider_content.cdrom_drive': cdrom_drive, + 'provider_content.storage': [value for value in storage_data.values()], + 'provider_content.network_interfaces': [ + value for value in network_data.values() + ], + }, + ) + values = session.computeresource.read_computeprofile(cr_name, cprofile) + provider_content = values['provider_content'] + assert provider_content['guest_os'] == guest_os_name + assert provider_content['cpus'] == cpu + assert provider_content['memory'] == memory + assert provider_content['cluster'] == settings.vmware.cluster + assert provider_content['annotation_notes'] == annotation_notes + assert provider_content['virtual_hw_version'] == virtual_hw_version + if not is_open('BZ:2266672'): + assert values['provider_content']['firmware'] == firmware + assert provider_content['resource_pool'] == resource_pool + assert provider_content['folder'] == folder + assert provider_content['memory_hot_add'] == memory_hot_add + assert provider_content['cpu_hot_add'] == cpu_hot_add + assert provider_content['cdrom_drive'] == cdrom_drive assert ( - provided_controller_value['controller'] == expected_controller_value['controller'] + provider_content['storage'][0]['controller'] == VMWARE_CONSTANTS['scsicontroller'] ) - for disk_index, expected_disk_value in enumerate(expected_controller_value['disks']): - provided_disk_value = { - key: value - for key, value in provided_controller_value['disks'][disk_index].items() - if key in expected_disk_value - } - assert provided_disk_value == expected_disk_value + assert provider_content['storage'][0]['disks'][0]['size'] == disk_size + assert ( + provider_content['network_interfaces'][0]['nic_type'] + == VMWARE_CONSTANTS['network_interface_name'] + ) + assert provider_content['network_interfaces'][0]['network'] == network + session.computeresource.delete(cr_name) + assert not session.computeresource.search(cr_name) @pytest.mark.tier2 From a889a72228aac56e1ef9bc1bf01149d2c5cea424 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:31:16 -0400 Subject: [PATCH 555/586] [6.14.z] virt-who config upgrade duplicate config issue fix (#14499) virt-who config upgrade duplicate config issue fix (#14378) (cherry picked from commit 980392e8245745e1a030ab97eaadd0d372f3b7f6) Co-authored-by: yanpliu --- tests/upgrades/test_virtwho.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/upgrades/test_virtwho.py b/tests/upgrades/test_virtwho.py index 3691e3a8d70..95b36ec5749 100644 --- a/tests/upgrades/test_virtwho.py +++ b/tests/upgrades/test_virtwho.py @@ -37,7 +37,7 @@ def form_data(target_sat): 'satellite_url': target_sat.hostname, 'hypervisor_username': esx.hypervisor_username, 'hypervisor_password': esx.hypervisor_password, - 'name': 'preupgrade_virt_who', + 'name': f'preupgrade_virt_who_{gen_string("alpha")}', } @@ -120,6 +120,7 @@ def test_pre_create_virt_who_configuration( 'org_id': org.id, 'org_name': org.name, 'org_label': org.label, + 'name': vhd.name, } ) @@ -146,15 +147,16 @@ def test_post_crud_virt_who_configuration(self, form_data, pre_upgrade_data, tar org_id = pre_upgrade_data.get('org_id') org_name = pre_upgrade_data.get('org_name') org_label = pre_upgrade_data.get('org_label') + name = pre_upgrade_data.get('name') # Post upgrade, Verify virt-who exists and has same status. vhd = target_sat.api.VirtWhoConfig(organization_id=org_id).search( - query={'search': f'name={form_data["name"]}'} + query={'search': f'name={name}'} )[0] if not is_open('BZ:1802395'): assert vhd.status == 'ok' # Verify virt-who status via CLI as we cannot check it via API now - vhd_cli = target_sat.cli.VirtWhoConfig.exists(search=('name', form_data['name'])) + vhd_cli = target_sat.cli.VirtWhoConfig.exists(search=('name', name)) assert ( target_sat.cli.VirtWhoConfig.info({'id': vhd_cli['id']})['general-information'][ 'status' @@ -185,7 +187,7 @@ def test_post_crud_virt_who_configuration(self, form_data, pre_upgrade_data, tar ) virt_who_instance = ( target_sat.api.VirtWhoConfig(organization_id=org_id) - .search(query={'search': f'name={form_data["name"]}'})[0] + .search(query={'search': f'name={name}'})[0] .status ) assert virt_who_instance == 'ok' From 4be5fae156bacc2456c68e43f81a531388a6801d Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 25 Mar 2024 00:14:52 -0400 Subject: [PATCH 556/586] [6.14.z] Bump pytest-cov from 4.1.0 to 5.0.0 (#14504) --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index e776154c7a1..13934c6e896 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -1,6 +1,6 @@ # For running tests and checking code quality using these modules. flake8==7.0.0 -pytest-cov==4.1.0 +pytest-cov==5.0.0 redis==5.0.3 pre-commit==3.6.2 From 51120932e0a8ac15e31bd9c5bb6f6b77d9b6a5e9 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 25 Mar 2024 00:16:04 -0400 Subject: [PATCH 557/586] [6.14.z] Bump pre-commit from 3.6.2 to 3.7.0 (#14507) --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 13934c6e896..9cc9c0300f1 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -2,7 +2,7 @@ flake8==7.0.0 pytest-cov==5.0.0 redis==5.0.3 -pre-commit==3.6.2 +pre-commit==3.7.0 # For generating documentation. sphinx==7.2.6 From 340cdc55c5d6695fa0bb44ba03aa4f2cbf86ba1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Va=C5=A1ina?= Date: Wed, 27 Mar 2024 09:45:49 +0100 Subject: [PATCH 558/586] [6.14.z] Cherrypick of FixesForEndeavour (#14518) * [6.14.z] Cherrypick of FixesForEndeavour * Change host details acquisition so the test is more stable * Pre commit --- pytest_fixtures/core/contenthosts.py | 10 +++ pytest_plugins/fixture_markers.py | 1 + tests/foreman/ui/test_host.py | 37 --------- tests/foreman/ui/test_jobinvocation.py | 98 ++++++++++++++++-------- tests/foreman/ui/test_remoteexecution.py | 45 ++++++----- 5 files changed, 103 insertions(+), 88 deletions(-) diff --git a/pytest_fixtures/core/contenthosts.py b/pytest_fixtures/core/contenthosts.py index 485591ed7d9..72f39b50796 100644 --- a/pytest_fixtures/core/contenthosts.py +++ b/pytest_fixtures/core/contenthosts.py @@ -161,6 +161,16 @@ def rex_contenthost(request, module_org, target_sat, module_ak_with_cv): yield host +@pytest.fixture +def rex_contenthosts(request, module_org, target_sat, module_ak_with_cv): + request.param['no_containers'] = True + with Broker(**host_conf(request), host_class=ContentHost, _count=2) as hosts: + for host in hosts: + repo = settings.repos['SATCLIENT_REPO'][f'RHEL{host.os_version.major}'] + host.register(module_org, None, module_ak_with_cv.name, target_sat, repo=repo) + yield hosts + + @pytest.fixture def katello_host_tools_tracer_host(rex_contenthost, target_sat): """Install katello-host-tools-tracer, create custom diff --git a/pytest_plugins/fixture_markers.py b/pytest_plugins/fixture_markers.py index 6fe2f4df4af..5905114132d 100644 --- a/pytest_plugins/fixture_markers.py +++ b/pytest_plugins/fixture_markers.py @@ -9,6 +9,7 @@ 'module_provisioning_rhel_content', 'capsule_provisioning_rhel_content', 'rex_contenthost', + 'rex_contenthosts', ] diff --git a/tests/foreman/ui/test_host.py b/tests/foreman/ui/test_host.py index e3de3c10dd1..7ccf497c927 100644 --- a/tests/foreman/ui/test_host.py +++ b/tests/foreman/ui/test_host.py @@ -1043,43 +1043,6 @@ def test_positive_read_details_page_from_new_ui(session, host_ui_options): assert values['overview']['details']['details']['comment'] == 'Host with fake data' -@pytest.mark.tier4 -@pytest.mark.rhel_ver_match('8') -def test_rex_new_ui(session, target_sat, rex_contenthost): - """Run remote execution using the new host details page - - :id: ee625595-4995-43b2-9e6d-633c9b33ff93 - - :steps: - 1. Navigate to Overview tab - 2. Schedule a job - 3. Wait for the job to finish - 4. Job is visible in Recent jobs card - - :expectedresults: Remote execution succeeded and the job is visible on Recent jobs card on - Overview tab - """ - hostname = rex_contenthost.hostname - job_args = { - 'job_category': 'Commands', - 'job_template': 'Run Command - Script Default', - 'template_content.command': 'ls', - } - with session: - session.location.select(loc_name=DEFAULT_LOC) - session.host_new.schedule_job(hostname, job_args) - task_result = target_sat.wait_for_tasks( - search_query=(f'Remote action: Run ls on {hostname}'), - search_rate=2, - max_tries=30, - ) - task_status = target_sat.api.ForemanTask(id=task_result[0].id).poll() - assert task_status['result'] == 'success' - recent_jobs = session.host_new.get_details(hostname, "overview.recent_jobs")['overview'] - assert recent_jobs['recent_jobs']['finished']['table'][0]['column0'] == "Run ls" - assert recent_jobs['recent_jobs']['finished']['table'][0]['column2'] == "succeeded" - - @pytest.mark.tier4 def test_positive_manage_table_columns(session, current_sat_org, current_sat_location): """Set custom columns of the hosts table. diff --git a/tests/foreman/ui/test_jobinvocation.py b/tests/foreman/ui/test_jobinvocation.py index d79d7ee3355..d7662c24bc8 100644 --- a/tests/foreman/ui/test_jobinvocation.py +++ b/tests/foreman/ui/test_jobinvocation.py @@ -17,19 +17,12 @@ from robottelo.utils.datafactory import gen_string -@pytest.fixture -def module_rhel_client_by_ip(module_org, smart_proxy_location, rhel7_contenthost, target_sat): - """Setup a broker rhel client to be used in remote execution by ip""" - rhel7_contenthost.configure_rex(satellite=target_sat, org=module_org) - target_sat.api_factory.update_vm_host_location( - rhel7_contenthost, location_id=smart_proxy_location.id - ) - return rhel7_contenthost - - -@pytest.mark.tier4 -def test_positive_run_default_job_template_by_ip( - session, module_org, smart_proxy_location, module_rhel_client_by_ip +@pytest.mark.rhel_ver_match('8') +def test_positive_run_default_job_template( + session, + target_sat, + rex_contenthost, + module_org, ): """Run a job template on a host connected by ip @@ -39,7 +32,7 @@ def test_positive_run_default_job_template_by_ip( :steps: - 1. Set remote_execution_connect_by_ip on host to true + 1. Get contenthost with rex enabled 2. Navigate to an individual host and click Run Job 3. Select the job and appropriate template 4. Run the job @@ -48,17 +41,19 @@ def test_positive_run_default_job_template_by_ip( :parametrized: yes """ - hostname = module_rhel_client_by_ip.hostname - with session: + + hostname = rex_contenthost.hostname + + with target_sat.ui_session() as session: session.organization.select(module_org.name) - session.location.select(smart_proxy_location.name) assert session.host.search(hostname)[0]['Name'] == hostname session.jobinvocation.run( { - 'job_category': 'Commands', - 'job_template': 'Run Command - Script Default', - 'search_query': f'name ^ {hostname}', - 'template_content.command': 'ls', + 'category_and_template.job_category': 'Commands', + 'category_and_template.job_template': 'Run Command - Script Default', + 'target_hosts_and_inputs.targetting_type': 'Hosts', + 'target_hosts_and_inputs.targets': hostname, + 'target_hosts_and_inputs.command': 'ls', } ) session.jobinvocation.wait_job_invocation_state(entity_name='Run ls', host_name=hostname) @@ -67,9 +62,49 @@ def test_positive_run_default_job_template_by_ip( @pytest.mark.tier4 -def test_positive_run_custom_job_template_by_ip( - session, module_org, smart_proxy_location, module_rhel_client_by_ip -): +@pytest.mark.rhel_ver_match('8') +def test_rex_through_host_details(session, target_sat, rex_contenthost, module_org): + """Run remote execution using the new host details page + + :id: ee625595-4995-43b2-9e6d-633c9b33ff93 + + :steps: + 1. Navigate to Overview tab + 2. Schedule a job + 3. Wait for the job to finish + 4. Job is visible in Recent jobs card + + :expectedresults: Remote execution succeeded and the job is visible on Recent jobs card on + Overview tab + """ + + hostname = rex_contenthost.hostname + + job_args = { + 'category_and_template.job_category': 'Commands', + 'category_and_template.job_template': 'Run Command - Script Default', + 'target_hosts_and_inputs.command': 'ls', + } + with target_sat.ui_session() as session: + session.organization.select(module_org.name) + session.host_new.schedule_job(hostname, job_args) + task_result = target_sat.wait_for_tasks( + search_query=(f'Remote action: Run ls on {hostname}'), + search_rate=2, + max_tries=30, + ) + task_status = target_sat.api.ForemanTask(id=task_result[0].id).poll() + assert task_status['result'] == 'success' + recent_jobs = session.host_new.get_details(hostname, "overview.recent_jobs") + assert recent_jobs['overview']['recent_jobs']['finished']['table'][0]['column0'] == "Run ls" + assert ( + recent_jobs['overview']['recent_jobs']['finished']['table'][0]['column2'] == "succeeded" + ) + + +@pytest.mark.tier4 +@pytest.mark.rhel_ver_match('8') +def test_positive_run_custom_job_template(session, module_org, target_sat, rex_contenthost): """Run a job template on a host connected by ip :id: e283ae09-8b14-4ce1-9a76-c1bbd511d58c @@ -87,11 +122,12 @@ def test_positive_run_custom_job_template_by_ip( :parametrized: yes """ - hostname = module_rhel_client_by_ip.hostname + + hostname = rex_contenthost.hostname + job_template_name = gen_string('alpha') - with session: + with target_sat.ui_session() as session: session.organization.select(module_org.name) - session.location.select(smart_proxy_location.name) assert session.host.search(hostname)[0]['Name'] == hostname session.jobtemplate.create( { @@ -105,10 +141,10 @@ def test_positive_run_custom_job_template_by_ip( assert session.jobtemplate.search(job_template_name)[0]['Name'] == job_template_name session.jobinvocation.run( { - 'job_category': 'Miscellaneous', - 'job_template': job_template_name, - 'search_query': f'name ^ {hostname}', - 'template_content.command': 'ls', + 'category_and_template.job_category': 'Miscellaneous', + 'category_and_template.job_template': job_template_name, + 'target_hosts_and_inputs.targets': hostname, + 'target_hosts_and_inputs.command': 'ls', } ) job_description = f'{camelize(job_template_name.lower())} with inputs command="ls"' diff --git a/tests/foreman/ui/test_remoteexecution.py b/tests/foreman/ui/test_remoteexecution.py index cdfe9c7ec4e..04642763812 100644 --- a/tests/foreman/ui/test_remoteexecution.py +++ b/tests/foreman/ui/test_remoteexecution.py @@ -140,7 +140,7 @@ def test_positive_run_custom_job_template_by_ip( @pytest.mark.tier3 @pytest.mark.rhel_ver_list([8]) def test_positive_run_job_template_multiple_hosts_by_ip( - session, module_org, target_sat, registered_hosts + session, module_org, target_sat, rex_contenthosts ): """Run a job template against multiple hosts by ip @@ -158,22 +158,24 @@ def test_positive_run_job_template_multiple_hosts_by_ip( :expectedresults: Verify the job was successfully ran against the hosts """ + host_names = [] - for vm in registered_hosts: + for vm in rex_contenthosts: + # for vm in rex_contenthost: host_names.append(vm.hostname) vm.configure_rex(satellite=target_sat, org=module_org) - with session: + with target_sat.ui_session() as session: session.organization.select(module_org.name) - session.location.select('Default Location') - hosts = session.host.search(' or '.join([f'name="{hostname}"' for hostname in host_names])) - assert {host['Name'] for host in hosts} == set(host_names) + # session.location.select('Default Location') + for host in host_names: + assert session.host.search(host)[0]['Name'] == host + session.host.reset_search() job_status = session.host.schedule_remote_job( host_names, { 'category_and_template.job_category': 'Commands', 'category_and_template.job_template': 'Run Command - Script Default', - 'target_hosts_and_inputs.command': 'ls', - 'schedule.immediate': True, + 'target_hosts_and_inputs.command': 'sleep 5', }, ) assert job_status['overview']['job_status'] == 'Success' @@ -211,19 +213,20 @@ def test_positive_run_scheduled_job_template_by_ip(session, module_org, rex_cont :parametrized: yes """ - job_time = 10 * 60 + job_time = 6 * 60 hostname = rex_contenthost.hostname with session: session.organization.select(module_org.name) session.location.select('Default Location') assert session.host.search(hostname)[0]['Name'] == hostname plan_time = session.browser.get_client_datetime() + datetime.timedelta(seconds=job_time) + command_to_run = 'sleep 10' job_status = session.host.schedule_remote_job( [hostname], { 'category_and_template.job_category': 'Commands', 'category_and_template.job_template': 'Run Command - Script Default', - 'target_hosts_and_inputs.command': 'ls', + 'target_hosts_and_inputs.command': command_to_run, 'schedule.future': True, 'schedule_future_execution.start_at_date': plan_time.strftime("%Y/%m/%d"), 'schedule_future_execution.start_at_time': plan_time.strftime("%H:%M"), @@ -237,34 +240,36 @@ def test_positive_run_scheduled_job_template_by_ip(session, module_org, rex_cont # the job_time must be significantly greater than job creation time. assert job_left_time > 0 assert job_status['overview']['hosts_table'][0]['Host'] == hostname - assert job_status['overview']['hosts_table'][0]['Status'] == 'N/A' + assert job_status['overview']['hosts_table'][0]['Status'] in ('Awaiting start', 'N/A') # sleep 3/4 of the left time time.sleep(job_left_time * 3 / 4) - job_status = session.jobinvocation.read('Run ls', hostname, 'overview.hosts_table') + job_status = session.jobinvocation.read( + f'Run {command_to_run}', hostname, 'overview.hosts_table' + ) assert job_status['overview']['hosts_table'][0]['Host'] == hostname - assert job_status['overview']['hosts_table'][0]['Status'] == 'N/A' + assert job_status['overview']['hosts_table'][0]['Status'] in ('Awaiting start', 'N/A') # recalculate the job left time to be more accurate job_left_time = (plan_time - session.browser.get_client_datetime()).total_seconds() # the last read time should not take more than 1/4 of the last left time assert job_left_time > 0 wait_for( - lambda: session.jobinvocation.read('Run ls', hostname, 'overview.hosts_table')[ - 'overview' - ]['hosts_table'][0]['Status'] + lambda: session.jobinvocation.read( + f'Run {command_to_run}', hostname, 'overview.hosts_table' + )['overview']['hosts_table'][0]['Status'] == 'running', timeout=(job_left_time + 30), delay=1, ) # wait the job to change status to "success" wait_for( - lambda: session.jobinvocation.read('Run ls', hostname, 'overview.hosts_table')[ - 'overview' - ]['hosts_table'][0]['Status'] + lambda: session.jobinvocation.read( + f'Run {command_to_run}', hostname, 'overview.hosts_table' + )['overview']['hosts_table'][0]['Status'] == 'success', timeout=30, delay=1, ) - job_status = session.jobinvocation.read('Run ls', hostname, 'overview') + job_status = session.jobinvocation.read(f'Run {command_to_run}', hostname, 'overview') assert job_status['overview']['job_status'] == 'Success' assert job_status['overview']['hosts_table'][0]['Host'] == hostname assert job_status['overview']['hosts_table'][0]['Status'] == 'success' From 31fa21a5b32f452e20c4df29517bbe27c2d58318 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 27 Mar 2024 13:16:59 -0400 Subject: [PATCH 559/586] [6.14.z] [6.15] cli e2e fix (#14543) --- tests/foreman/endtoend/test_cli_endtoend.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/foreman/endtoend/test_cli_endtoend.py b/tests/foreman/endtoend/test_cli_endtoend.py index 30de18bd960..e14085f39a7 100644 --- a/tests/foreman/endtoend/test_cli_endtoend.py +++ b/tests/foreman/endtoend/test_cli_endtoend.py @@ -266,14 +266,22 @@ def test_positive_cli_end_to_end(function_entitlement_manifest, target_sat, rhel ) content_host = target_sat.cli.Host.with_user(user['login'], user['password']).info( - {'id': content_host['id']} + {'id': content_host['id']}, output_format='json' ) + # check that content view matches what we passed - assert content_host['content-information']['content-view']['name'] == content_view['name'] + assert ( + content_host['content-information']['content-view-environments']['1']['content-view'][ + 'name' + ] + == content_view['name'] + ) # check that lifecycle environment matches assert ( - content_host['content-information']['lifecycle-environment']['name'] + content_host['content-information']['content-view-environments']['1'][ + 'lifecycle-environment' + ]['name'] == lifecycle_environment['name'] ) From 55a98f8b1b706de1ec8a94ca582daaa75f8823f0 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:12:44 -0400 Subject: [PATCH 560/586] [6.14.z] Bump pytest-reportportal from 5.4.0 to 5.4.1 (#14548) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index eecb47ecdfa..44a9f259eae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ python-box==7.1.1 pytest==8.0.2 pytest-services==2.2.1 pytest-mock==3.14.0 -pytest-reportportal==5.4.0 +pytest-reportportal==5.4.1 pytest-xdist==3.5.0 pytest-fixturecollection==0.1.2 pytest-ibutsu==2.2.4 From 6fe06dd7aa2d787352f4e01c5dc8abc47ab23270 Mon Sep 17 00:00:00 2001 From: Omkar Khatavkar Date: Thu, 28 Mar 2024 12:34:00 +0530 Subject: [PATCH 561/586] [6.14.z] Bump pytest from 8.1.0 to 8.1.1 (#14532) Bump pytest from 8.1.0 to 8.1.1 (#14323) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 44a9f259eae..54bea16b88e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,8 @@ navmazing==1.2.2 productmd==1.38 pyotp==2.9.0 python-box==7.1.1 -pytest==8.0.2 +pytest==8.1.1 +pytest-order==1.2.0 pytest-services==2.2.1 pytest-mock==3.14.0 pytest-reportportal==5.4.1 From 76504ebd8e4d3947d1284cd552f22ef68db6d056 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 28 Mar 2024 03:13:36 -0400 Subject: [PATCH 562/586] [6.14.z] Bump fauxfactory from 3.1.0 to 3.1.1 (#14528) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 54bea16b88e..e6a79f733cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ broker[docker]==0.4.9 cryptography==42.0.5 deepdiff==6.7.1 dynaconf[vault]==3.2.5 -fauxfactory==3.1.0 +fauxfactory==3.1.1 jinja2==3.1.3 manifester==0.0.14 navmazing==1.2.2 From fb700d072a600a9165315a9d7186ff2be443a03b Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Thu, 28 Mar 2024 06:33:54 -0400 Subject: [PATCH 563/586] [6.14.z] Add test coverage for BZ:2187967 (#14553) --- tests/foreman/api/test_ansible.py | 91 +++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/tests/foreman/api/test_ansible.py b/tests/foreman/api/test_ansible.py index 515d432423f..04b25f2484b 100644 --- a/tests/foreman/api/test_ansible.py +++ b/tests/foreman/api/test_ansible.py @@ -322,6 +322,97 @@ def test_add_and_remove_ansible_role_hostgroup(target_sat): assert len(hg_nested_roles) == 0 +@pytest.mark.e2e +@pytest.mark.tier2 +@pytest.mark.upgrade +def test_positive_ansible_roles_inherited_from_hostgroup( + request, target_sat, module_org, module_location +): + """Verify ansible roles inheritance functionality for host with parent/nested hostgroup via API + + :id: 7672cf86-fa31-11ed-855a-0fd307d2d66g + + :steps: + 1. Create a host, hostgroup and nested hostgroup + 2. Sync a few ansible roles + 3. Assign a few ansible roles to the host, hostgroup, nested hostgroup and verify it. + 4. Update host to be in parent/nested hostgroup and verify roles assigned + + :expectedresults: + 1. Hosts in parent/nested hostgroups must have direct and indirect roles correctly assigned. + + :BZ: 2187967 + + :customerscenario: true + """ + ROLE_NAMES = [ + 'theforeman.foreman_scap_client', + 'RedHatInsights.insights-client', + 'redhat.satellite.compute_resources', + ] + proxy_id = target_sat.nailgun_smart_proxy.id + host = target_sat.api.Host(organization=module_org, location=module_location).create() + hg = target_sat.api.HostGroup(name=gen_string('alpha'), organization=[module_org]).create() + hg_nested = target_sat.api.HostGroup( + name=gen_string('alpha'), parent=hg, organization=[module_org] + ).create() + + @request.addfinalizer + def _finalize(): + host.delete() + hg_nested.delete() + hg.delete() + + target_sat.api.AnsibleRoles().sync(data={'proxy_id': proxy_id, 'role_names': ROLE_NAMES}) + ROLES = [ + target_sat.api.AnsibleRoles().search(query={'search': f'name={role}'})[0].id + for role in ROLE_NAMES + ] + + # Assign roles to Host/Hostgroup/Nested Hostgroup and verify it + target_sat.api.Host(id=host.id).add_ansible_role(data={'ansible_role_id': ROLES[0]}) + assert ROLE_NAMES[0] == target_sat.api.Host(id=host.id).list_ansible_roles()[0]['name'] + + target_sat.api.HostGroup(id=hg.id).add_ansible_role(data={'ansible_role_id': ROLES[1]}) + assert ROLE_NAMES[1] == target_sat.api.HostGroup(id=hg.id).list_ansible_roles()[0]['name'] + + target_sat.api.HostGroup(id=hg_nested.id).add_ansible_role(data={'ansible_role_id': ROLES[2]}) + listroles = target_sat.api.HostGroup(id=hg_nested.id).list_ansible_roles() + assert ROLE_NAMES[2] == listroles[0]['name'] + assert listroles[0]['directly_assigned'] + assert ROLE_NAMES[1] == listroles[1]['name'] + assert not listroles[1]['directly_assigned'] + + # Update host to be in nested hostgroup and verify roles assigned + host.hostgroup = hg_nested + host = host.update(['hostgroup']) + listroles_host = target_sat.api.Host(id=host.id).list_ansible_roles() + assert ROLE_NAMES[0] == listroles_host[0]['name'] + assert listroles_host[0]['directly_assigned'] + assert ROLE_NAMES[1] == listroles_host[1]['name'] + assert not listroles_host[1]['directly_assigned'] + assert ROLE_NAMES[2] == listroles_host[2]['name'] + assert not listroles_host[1]['directly_assigned'] + # Verify nested hostgroup doesn't contains the roles assigned to host + listroles_nested_hg = target_sat.api.HostGroup(id=hg_nested.id).list_ansible_roles() + assert ROLE_NAMES[0] not in [role['name'] for role in listroles_nested_hg] + assert ROLE_NAMES[2] == listroles_nested_hg[0]['name'] + assert ROLE_NAMES[1] == listroles_nested_hg[1]['name'] + + # Update host to be in parent hostgroup and verify roles assigned + host.hostgroup = hg + host = host.update(['hostgroup']) + listroles = target_sat.api.Host(id=host.id).list_ansible_roles() + assert ROLE_NAMES[0] == listroles[0]['name'] + assert listroles[0]['directly_assigned'] + assert ROLE_NAMES[1] == listroles[1]['name'] + assert not listroles[1]['directly_assigned'] + # Verify parent hostgroup doesn't contains the roles assigned to host + listroles_hg = target_sat.api.HostGroup(id=hg.id).list_ansible_roles() + assert ROLE_NAMES[0] not in [role['name'] for role in listroles_hg] + assert ROLE_NAMES[1] == listroles_hg[0]['name'] + + @pytest.mark.rhel_ver_match('[78]') @pytest.mark.tier2 def test_positive_read_facts_with_filter( From 51281100cc8661ac52e5998d2b18a534580ab595 Mon Sep 17 00:00:00 2001 From: Peter Ondrejka Date: Thu, 28 Mar 2024 18:03:05 +0100 Subject: [PATCH 564/586] [6.14 ] rex ui test deduplication (#14557) rex ui test deduplication --- tests/foreman/ui/test_jobinvocation.py | 207 ----------------------- tests/foreman/ui/test_remoteexecution.py | 180 +++++++++++++++----- 2 files changed, 138 insertions(+), 249 deletions(-) delete mode 100644 tests/foreman/ui/test_jobinvocation.py diff --git a/tests/foreman/ui/test_jobinvocation.py b/tests/foreman/ui/test_jobinvocation.py deleted file mode 100644 index d7662c24bc8..00000000000 --- a/tests/foreman/ui/test_jobinvocation.py +++ /dev/null @@ -1,207 +0,0 @@ -"""Test class for Job Invocation procedure - -:Requirement: JobInvocation - -:CaseAutomation: Automated - -:CaseComponent: RemoteExecution - -:Team: Endeavour - -:CaseImportance: High - -""" -from inflection import camelize -import pytest - -from robottelo.utils.datafactory import gen_string - - -@pytest.mark.rhel_ver_match('8') -def test_positive_run_default_job_template( - session, - target_sat, - rex_contenthost, - module_org, -): - """Run a job template on a host connected by ip - - :id: 9a90aa9a-00b4-460e-b7e6-250360ee8e4d - - :Setup: Use pre-defined job template. - - :steps: - - 1. Get contenthost with rex enabled - 2. Navigate to an individual host and click Run Job - 3. Select the job and appropriate template - 4. Run the job - - :expectedresults: Verify the job was successfully ran against the host - - :parametrized: yes - """ - - hostname = rex_contenthost.hostname - - with target_sat.ui_session() as session: - session.organization.select(module_org.name) - assert session.host.search(hostname)[0]['Name'] == hostname - session.jobinvocation.run( - { - 'category_and_template.job_category': 'Commands', - 'category_and_template.job_template': 'Run Command - Script Default', - 'target_hosts_and_inputs.targetting_type': 'Hosts', - 'target_hosts_and_inputs.targets': hostname, - 'target_hosts_and_inputs.command': 'ls', - } - ) - session.jobinvocation.wait_job_invocation_state(entity_name='Run ls', host_name=hostname) - status = session.jobinvocation.read(entity_name='Run ls', host_name=hostname) - assert status['overview']['hosts_table'][0]['Status'] == 'success' - - -@pytest.mark.tier4 -@pytest.mark.rhel_ver_match('8') -def test_rex_through_host_details(session, target_sat, rex_contenthost, module_org): - """Run remote execution using the new host details page - - :id: ee625595-4995-43b2-9e6d-633c9b33ff93 - - :steps: - 1. Navigate to Overview tab - 2. Schedule a job - 3. Wait for the job to finish - 4. Job is visible in Recent jobs card - - :expectedresults: Remote execution succeeded and the job is visible on Recent jobs card on - Overview tab - """ - - hostname = rex_contenthost.hostname - - job_args = { - 'category_and_template.job_category': 'Commands', - 'category_and_template.job_template': 'Run Command - Script Default', - 'target_hosts_and_inputs.command': 'ls', - } - with target_sat.ui_session() as session: - session.organization.select(module_org.name) - session.host_new.schedule_job(hostname, job_args) - task_result = target_sat.wait_for_tasks( - search_query=(f'Remote action: Run ls on {hostname}'), - search_rate=2, - max_tries=30, - ) - task_status = target_sat.api.ForemanTask(id=task_result[0].id).poll() - assert task_status['result'] == 'success' - recent_jobs = session.host_new.get_details(hostname, "overview.recent_jobs") - assert recent_jobs['overview']['recent_jobs']['finished']['table'][0]['column0'] == "Run ls" - assert ( - recent_jobs['overview']['recent_jobs']['finished']['table'][0]['column2'] == "succeeded" - ) - - -@pytest.mark.tier4 -@pytest.mark.rhel_ver_match('8') -def test_positive_run_custom_job_template(session, module_org, target_sat, rex_contenthost): - """Run a job template on a host connected by ip - - :id: e283ae09-8b14-4ce1-9a76-c1bbd511d58c - - :Setup: Create a working job template. - - :steps: - - 1. Set remote_execution_connect_by_ip on host to true - 2. Navigate to an individual host and click Run Job - 3. Select the job and appropriate template - 4. Run the job - - :expectedresults: Verify the job was successfully ran against the host - - :parametrized: yes - """ - - hostname = rex_contenthost.hostname - - job_template_name = gen_string('alpha') - with target_sat.ui_session() as session: - session.organization.select(module_org.name) - assert session.host.search(hostname)[0]['Name'] == hostname - session.jobtemplate.create( - { - 'template.name': job_template_name, - 'template.template_editor.rendering_options': 'Editor', - 'template.template_editor.editor': '<%= input("command") %>', - 'job.provider_type': 'Script', - 'inputs': [{'name': 'command', 'required': True, 'input_type': 'User input'}], - } - ) - assert session.jobtemplate.search(job_template_name)[0]['Name'] == job_template_name - session.jobinvocation.run( - { - 'category_and_template.job_category': 'Miscellaneous', - 'category_and_template.job_template': job_template_name, - 'target_hosts_and_inputs.targets': hostname, - 'target_hosts_and_inputs.command': 'ls', - } - ) - job_description = f'{camelize(job_template_name.lower())} with inputs command="ls"' - session.jobinvocation.wait_job_invocation_state( - entity_name=job_description, host_name=hostname - ) - status = session.jobinvocation.read(entity_name=job_description, host_name=hostname) - assert status['overview']['hosts_table'][0]['Status'] == 'success' - - -@pytest.mark.stubbed -@pytest.mark.tier2 -def test_positive_schedule_recurring_host_job(self): - """Using the new Host UI, schedule a recurring job on a Host - - :id: 5052be04-28ab-4349-8bee-851ef76e4ffa - - :caseComponent: Ansible-RemoteExecution - - :Team: Rocket - - :steps: - 1. Register a RHEL host to Satellite. - 2. Import all roles available by default. - 3. Assign a role to host. - 4. Navigate to the new UI for the given Host. - 5. Select the Jobs subtab. - 6. Click the Schedule Recurring Job button, and using the popup, schedule a - recurring Job. - 7. Navigate to Job Invocations. - - :expectedresults: The scheduled Job appears in the Job Invocation list at the appointed - time - """ - - -@pytest.mark.stubbed -@pytest.mark.tier2 -def test_positive_schedule_recurring_hostgroup_job(self): - """Using the new recurring job scheduler, schedule a recurring job on a Hostgroup - - :id: c65db99b-11fe-4a32-89d0-0a4692b07efe - - :caseComponent: Ansible-RemoteExecution - - :Team: Rocket - - :steps: - 1. Register a RHEL host to Satellite. - 2. Import all roles available by default. - 3. Assign a role to host. - 4. Navigate to the Host Group page. - 5. Select the "Configure Ansible Job" action. - 6. Click the Schedule Recurring Job button, and using the popup, schedule a - recurring Job. - 7. Navigate to Job Invocations. - - :expectedresults: The scheduled Job appears in the Job Invocation list at the appointed - time - """ diff --git a/tests/foreman/ui/test_remoteexecution.py b/tests/foreman/ui/test_remoteexecution.py index 04642763812..9eb06939376 100644 --- a/tests/foreman/ui/test_remoteexecution.py +++ b/tests/foreman/ui/test_remoteexecution.py @@ -1,4 +1,4 @@ -"""Test class for Remote Execution Management UI +"""Test class for Job Invocation procedure :Requirement: Remoteexecution @@ -14,17 +14,23 @@ import datetime import time +from inflection import camelize import pytest from wait_for import wait_for -from robottelo.utils.datafactory import gen_string +from robottelo.utils.datafactory import ( + gen_string, +) -@pytest.mark.skip_if_open('BZ:2182353') @pytest.mark.rhel_ver_match('8') -@pytest.mark.tier3 -def test_positive_run_default_job_template_by_ip(session, rex_contenthost, module_org): - """Run a job template against a single host by ip +def test_positive_run_default_job_template( + session, + target_sat, + rex_contenthost, + module_org, +): + """Run a job template on a host :id: a21eac46-1a22-472d-b4ce-66097159a868 @@ -32,38 +38,38 @@ def test_positive_run_default_job_template_by_ip(session, rex_contenthost, modul :steps: - 1. Navigate to an individual host and click Run Job - 2. Select the job and appropriate template - 3. Run the job + 1. Get contenthost with rex enabled + 2. Navigate to an individual host and click Run Job + 3. Select the job and appropriate template + 4. Run the job - :expectedresults: Verify the job was successfully ran against the host + :expectedresults: Verify the job was successfully ran against the host, check also using the job widget on the main dashboard :parametrized: yes - :bz: 1898656 + :bz: 1898656, 2182353 :customerscenario: true """ + hostname = rex_contenthost.hostname - with session: + + with target_sat.ui_session() as session: session.organization.select(module_org.name) - session.location.select('Default Location') assert session.host.search(hostname)[0]['Name'] == hostname command = 'ls' - job_status = session.host.schedule_remote_job( - [hostname], + session.jobinvocation.run( { 'category_and_template.job_category': 'Commands', 'category_and_template.job_template': 'Run Command - Script Default', + 'target_hosts_and_inputs.targetting_type': 'Hosts', + 'target_hosts_and_inputs.targets': hostname, 'target_hosts_and_inputs.command': command, - 'advanced_fields.execution_order_randomized': True, - 'schedule.immediate': True, - }, + } ) - assert job_status['overview']['job_status'] == 'Success' - assert job_status['overview']['execution_order'] == 'Execution order: randomized' - assert job_status['overview']['hosts_table'][0]['Host'] == hostname - assert job_status['overview']['hosts_table'][0]['Status'] == 'success' + session.jobinvocation.wait_job_invocation_state(entity_name='Run ls', host_name=hostname) + status = session.jobinvocation.read(entity_name='Run ls', host_name=hostname) + assert status['overview']['hosts_table'][0]['Status'] == 'success' # check status also on the job dashboard job_name = f'Run {command}' @@ -73,16 +79,54 @@ def test_positive_run_default_job_template_by_ip(session, rex_contenthost, modul assert job_name in [job['Name'] for job in success_jobs] -@pytest.mark.skip_if_open('BZ:2182353') +@pytest.mark.tier4 +@pytest.mark.rhel_ver_match('8') +def test_rex_through_host_details(session, target_sat, rex_contenthost, module_org): + """Run remote execution using the new host details page + + :id: ee625595-4995-43b2-9e6d-633c9b33ff93 + + :steps: + 1. Navigate to Overview tab + 2. Schedule a job + 3. Wait for the job to finish + 4. Job is visible in Recent jobs card + + :expectedresults: Remote execution succeeded and the job is visible on Recent jobs card on + Overview tab + """ + + hostname = rex_contenthost.hostname + + job_args = { + 'category_and_template.job_category': 'Commands', + 'category_and_template.job_template': 'Run Command - Script Default', + 'target_hosts_and_inputs.command': 'ls', + } + with target_sat.ui_session() as session: + session.organization.select(module_org.name) + session.host_new.schedule_job(hostname, job_args) + task_result = target_sat.wait_for_tasks( + search_query=(f'Remote action: Run ls on {hostname}'), + search_rate=2, + max_tries=30, + ) + task_status = target_sat.api.ForemanTask(id=task_result[0].id).poll() + assert task_status['result'] == 'success' + recent_jobs = session.host_new.get_details(hostname, "overview.recent_jobs")['overview'] + assert recent_jobs['recent_jobs']['finished']['table'][0]['column0'] == "Run ls" + assert recent_jobs['recent_jobs']['finished']['table'][0]['column2'] == "succeeded" + + +@pytest.mark.tier4 @pytest.mark.rhel_ver_match('8') -@pytest.mark.tier3 @pytest.mark.parametrize( 'ui_user', [{'admin': True}, {'admin': False}], indirect=True, ids=['adminuser', 'nonadminuser'] ) -def test_positive_run_custom_job_template_by_ip( - session, module_org, target_sat, default_location, ui_user, rex_contenthost +def test_positive_run_custom_job_template( + session, module_org, default_location, target_sat, ui_user, rex_contenthost ): - """Run a job template on a host connected by ip + """Run a job template on a host :id: 3a59eb15-67c4-46e1-ba5f-203496ec0b0c @@ -103,13 +147,14 @@ def test_positive_run_custom_job_template_by_ip( :customerscenario: true """ + + hostname = rex_contenthost.hostname ui_user.location.append(target_sat.api.Location(id=default_location.id)) ui_user.update(['location']) - hostname = rex_contenthost.hostname job_template_name = gen_string('alpha') - with session: + with target_sat.ui_session() as session: session.organization.select(module_org.name) - session.location.select('Default Location') + assert session.host.search(hostname)[0]['Name'] == hostname session.jobtemplate.create( { 'template.name': job_template_name, @@ -120,29 +165,29 @@ def test_positive_run_custom_job_template_by_ip( } ) assert session.jobtemplate.search(job_template_name)[0]['Name'] == job_template_name - assert session.host.search(hostname)[0]['Name'] == hostname - job_status = session.host.schedule_remote_job( - [hostname], + session.jobinvocation.run( { 'category_and_template.job_category': 'Miscellaneous', 'category_and_template.job_template': job_template_name, + 'target_hosts_and_inputs.targets': hostname, 'target_hosts_and_inputs.command': 'ls', - 'schedule.immediate': True, - }, + } ) - assert job_status['overview']['job_status'] == 'Success' - assert job_status['overview']['hosts_table'][0]['Host'] == hostname - assert job_status['overview']['hosts_table'][0]['Status'] == 'success' + job_description = f'{camelize(job_template_name.lower())} with inputs command="ls"' + session.jobinvocation.wait_job_invocation_state( + entity_name=job_description, host_name=hostname + ) + status = session.jobinvocation.read(entity_name=job_description, host_name=hostname) + assert status['overview']['hosts_table'][0]['Status'] == 'success' -@pytest.mark.skip_if_open('BZ:2182353') @pytest.mark.upgrade @pytest.mark.tier3 @pytest.mark.rhel_ver_list([8]) -def test_positive_run_job_template_multiple_hosts_by_ip( +def test_positive_run_job_template_multiple_hosts( session, module_org, target_sat, rex_contenthosts ): - """Run a job template against multiple hosts by ip + """Run a job template against multiple hosts :id: c4439ec0-bb80-47f6-bc31-fa7193bfbeeb @@ -187,7 +232,6 @@ def test_positive_run_job_template_multiple_hosts_by_ip( ) -@pytest.mark.skip_if_open('BZ:2182353') @pytest.mark.rhel_ver_match('8') @pytest.mark.tier3 def test_positive_run_scheduled_job_template_by_ip(session, module_org, rex_contenthost): @@ -536,3 +580,55 @@ def test_positive_matcher_field_highlight(session): :Team: Rocket """ + + +@pytest.mark.stubbed +@pytest.mark.tier2 +def test_positive_schedule_recurring_host_job(self): + """Using the new Host UI, schedule a recurring job on a Host + + :id: 5052be04-28ab-4349-8bee-851ef76e4ffa + + :caseComponent: Ansible-RemoteExecution + + :Team: Rocket + + :steps: + 1. Register a RHEL host to Satellite. + 2. Import all roles available by default. + 3. Assign a role to host. + 4. Navigate to the new UI for the given Host. + 5. Select the Jobs subtab. + 6. Click the Schedule Recurring Job button, and using the popup, schedule a + recurring Job. + 7. Navigate to Job Invocations. + + :expectedresults: The scheduled Job appears in the Job Invocation list at the appointed + time + """ + + +@pytest.mark.stubbed +@pytest.mark.tier2 +def test_positive_schedule_recurring_hostgroup_job(self): + """Using the new recurring job scheduler, schedule a recurring job on a Hostgroup + + :id: c65db99b-11fe-4a32-89d0-0a4692b07efe + + :caseComponent: Ansible-RemoteExecution + + :Team: Rocket + + :steps: + 1. Register a RHEL host to Satellite. + 2. Import all roles available by default. + 3. Assign a role to host. + 4. Navigate to the Host Group page. + 5. Select the "Configure Ansible Job" action. + 6. Click the Schedule Recurring Job button, and using the popup, schedule a + recurring Job. + 7. Navigate to Job Invocations. + + :expectedresults: The scheduled Job appears in the Job Invocation list at the appointed + time + """ From 56f49750086ca126266848562b609f0573037a38 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 29 Mar 2024 09:15:58 -0400 Subject: [PATCH 565/586] [6.14.z] extend the timeout for poor network (#14575) extend the timeout for poor network (#14573) (cherry picked from commit 64815689ecffae197b099099338324bc56931fc5) Co-authored-by: yanpliu --- robottelo/utils/virtwho.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/robottelo/utils/virtwho.py b/robottelo/utils/virtwho.py index e159a487394..b1cac0012ca 100644 --- a/robottelo/utils/virtwho.py +++ b/robottelo/utils/virtwho.py @@ -210,7 +210,7 @@ def check_message_in_rhsm_log(message): """Check the message exist in /var/log/rhsm/rhsm.log""" wait_for( lambda: 'Host-to-guest mapping being sent to' in get_rhsm_log(), - timeout=10, + timeout=20, delay=2, ) logs = get_rhsm_log() @@ -230,7 +230,7 @@ def _get_hypervisor_mapping(hypervisor_type): """ wait_for( lambda: 'Host-to-guest mapping being sent to' in get_rhsm_log(), - timeout=10, + timeout=20, delay=2, ) logs = get_rhsm_log() From d8de753eceddee9b0a6e3c2b6e97adb34e628ef8 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 1 Apr 2024 09:36:24 -0400 Subject: [PATCH 566/586] [6.14.z] Move RHCloud tests to team Phoenix-subscriptions (#14579) Move RHCloud tests to team Phoenix-subscriptions (#14567) (cherry picked from commit da06fea37381e8cbdfa950c0927a03e0946737ce) Co-authored-by: Jameer Pathan <21165044+jameerpathan111@users.noreply.github.com> --- tests/foreman/api/test_rhc.py | 2 +- tests/foreman/api/test_rhcloud_inventory.py | 2 +- tests/foreman/cli/test_rhcloud_inventory.py | 2 +- tests/foreman/ui/test_rhc.py | 2 +- tests/foreman/ui/test_rhcloud_insights.py | 2 +- tests/foreman/ui/test_rhcloud_inventory.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/foreman/api/test_rhc.py b/tests/foreman/api/test_rhc.py index 30559fe406e..391277bfb38 100644 --- a/tests/foreman/api/test_rhc.py +++ b/tests/foreman/api/test_rhc.py @@ -6,7 +6,7 @@ :CaseComponent: RHCloud -:Team: Platform +:Team: Phoenix-subscriptions :CaseImportance: High diff --git a/tests/foreman/api/test_rhcloud_inventory.py b/tests/foreman/api/test_rhcloud_inventory.py index 1f7af05b92a..40b36f4fcab 100644 --- a/tests/foreman/api/test_rhcloud_inventory.py +++ b/tests/foreman/api/test_rhcloud_inventory.py @@ -6,7 +6,7 @@ :CaseComponent: RHCloud -:Team: Platform +:Team: Phoenix-subscriptions :CaseImportance: High diff --git a/tests/foreman/cli/test_rhcloud_inventory.py b/tests/foreman/cli/test_rhcloud_inventory.py index 31847984e03..ed84fd465d7 100644 --- a/tests/foreman/cli/test_rhcloud_inventory.py +++ b/tests/foreman/cli/test_rhcloud_inventory.py @@ -6,7 +6,7 @@ :CaseComponent: RHCloud -:Team: Platform +:Team: Phoenix-subscriptions :CaseImportance: High diff --git a/tests/foreman/ui/test_rhc.py b/tests/foreman/ui/test_rhc.py index f3ab5eae14f..df90a5768ca 100644 --- a/tests/foreman/ui/test_rhc.py +++ b/tests/foreman/ui/test_rhc.py @@ -6,7 +6,7 @@ :CaseComponent: RHCloud -:Team: Platform +:Team: Phoenix-subscriptions :CaseImportance: High diff --git a/tests/foreman/ui/test_rhcloud_insights.py b/tests/foreman/ui/test_rhcloud_insights.py index 2a3cabf789f..3646658c2b0 100644 --- a/tests/foreman/ui/test_rhcloud_insights.py +++ b/tests/foreman/ui/test_rhcloud_insights.py @@ -6,7 +6,7 @@ :CaseComponent: RHCloud -:Team: Platform +:Team: Phoenix-subscriptions :CaseImportance: High diff --git a/tests/foreman/ui/test_rhcloud_inventory.py b/tests/foreman/ui/test_rhcloud_inventory.py index 642bef3b84b..9c98ade0205 100644 --- a/tests/foreman/ui/test_rhcloud_inventory.py +++ b/tests/foreman/ui/test_rhcloud_inventory.py @@ -6,7 +6,7 @@ :CaseComponent: RHCloud -:Team: Platform +:Team: Phoenix-subscriptions :CaseImportance: High From dede6964fec8bf6644ae8af202b5023d422ec942 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 2 Apr 2024 06:44:08 -0400 Subject: [PATCH 567/586] [6.14.z] host name & value updated with subnet (#14586) --- tests/foreman/api/test_subnet.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/tests/foreman/api/test_subnet.py b/tests/foreman/api/test_subnet.py index b4882116f8e..99873b537f5 100644 --- a/tests/foreman/api/test_subnet.py +++ b/tests/foreman/api/test_subnet.py @@ -336,9 +336,8 @@ def test_negative_update_parameter(new_name, target_sat): sub_param.update(['name']) -@pytest.mark.stubbed @pytest.mark.tier2 -def test_positive_update_subnet_parameter_host_impact(): +def test_positive_update_subnet_parameter_host_impact(target_sat): """Update in parameter name and value from subnet component updates the parameter in host inheriting that subnet @@ -353,12 +352,29 @@ def test_positive_update_subnet_parameter_host_impact(): :expectedresults: 1. The inherited subnet parameter in host should have - updated name and value - 2. The inherited subnet parameter in host enc should have - updated name and value + updated name and value. :BZ: 1470014 """ + parameter = [{'name': gen_string('alpha'), 'value': gen_string('alpha')}] + org = target_sat.api.Organization().create() + loc = target_sat.api.Location(organization=[org]).create() + org_subnet = target_sat.api.Subnet( + location=[loc], organization=[org], subnet_parameters_attributes=parameter + ).create() + assert parameter[0]['name'] == org_subnet.subnet_parameters_attributes[0]['name'] + assert parameter[0]['value'] == org_subnet.subnet_parameters_attributes[0]['value'] + host = target_sat.api.Host(location=loc, organization=org, subnet=org_subnet).create() + parameter_new_value = [{'name': gen_string('alpha'), 'value': gen_string('alpha')}] + org_subnet.subnet_parameters_attributes = parameter_new_value + org_subnet.update(['subnet_parameters_attributes']) + assert ( + host.subnet.read().subnet_parameters_attributes[0]['name'] == parameter_new_value[0]['name'] + ) + assert ( + host.subnet.read().subnet_parameters_attributes[0]['value'] + == parameter_new_value[0]['value'] + ) @pytest.mark.tier1 From ee71a72a36bc9b25991faf16f74009b12e5b24d9 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:55:16 -0400 Subject: [PATCH 568/586] [6.14.z] cu-case-katello-reimport-scenario (#14590) --- robottelo/constants/__init__.py | 3 ++ tests/foreman/cli/test_subscription.py | 45 +++++++++++++++++++++++- tests/foreman/data/expired-manifest.zip | Bin 0 -> 1255696 bytes 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/foreman/data/expired-manifest.zip diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index 2adda82a9bc..1efd6f37d24 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -2145,6 +2145,8 @@ class Colored(Box): 'option is not present in the /etc/dnf/dnf.conf' ) +EXPIRED_MANIFEST = 'expired-manifest.zip' + # Data File Paths class DataFile(Box): """The boxed Data directory class with its attributes pointing to the Data directory files""" @@ -2164,3 +2166,4 @@ class DataFile(Box): PARTITION_SCRIPT_DATA_FILE = DATA_DIR.joinpath(PARTITION_SCRIPT_DATA_FILE) OS_TEMPLATE_DATA_FILE = DATA_DIR.joinpath(OS_TEMPLATE_DATA_FILE) FAKE_3_YUM_REPO_RPMS_ANT = DATA_DIR.joinpath(FAKE_3_YUM_REPO_RPMS[0]) + EXPIRED_MANIFEST_FILE = DATA_DIR.joinpath(EXPIRED_MANIFEST) diff --git a/tests/foreman/cli/test_subscription.py b/tests/foreman/cli/test_subscription.py index a7a74f4360c..c28ba08e61a 100644 --- a/tests/foreman/cli/test_subscription.py +++ b/tests/foreman/cli/test_subscription.py @@ -12,10 +12,12 @@ """ from fauxfactory import gen_string +from manifester import Manifester from nailgun import entities import pytest -from robottelo.constants import PRDS, REPOS, REPOSET +from robottelo.config import settings +from robottelo.constants import EXPIRED_MANIFEST, PRDS, REPOS, REPOSET, DataFile from robottelo.exceptions import CLIReturnCodeError pytestmark = [pytest.mark.run_in_one_thread] @@ -276,3 +278,44 @@ def test_positive_auto_attach_disabled_golden_ticket( with pytest.raises(CLIReturnCodeError) as context: target_sat.cli.Host.subscription_auto_attach({'host-id': host_id}) assert "This host's organization is in Simple Content Access mode" in str(context.value) + + +def test_negative_check_katello_reimport(target_sat, function_org): + """Verify katello:reimport trace should not fail with an TypeError + + :id: b7508a1c-7798-4649-83a3-cf94c7409c96 + + :steps: + 1. Import expired manifest & refresh + 2. Delete expired manifest + 3. Re-import new valid manifest & refresh + + :expectedresults: There should not be an error after reimport manifest + + :customerscenario: true + + :BZ: 2225534, 2253621 + """ + remote_path = f'/tmp/{EXPIRED_MANIFEST}' + target_sat.put(DataFile.EXPIRED_MANIFEST_FILE, remote_path) + # Import expired manifest & refresh + target_sat.cli.Subscription.upload({'organization-id': function_org.id, 'file': remote_path}) + with pytest.raises(CLIReturnCodeError): + target_sat.cli.Subscription.refresh_manifest({'organization-id': function_org.id}) + exec_val = target_sat.execute( + 'grep -i "Katello::HttpErrors::BadRequest: This Organization\'s subscription ' + 'manifest has expired. Please import a new manifest" /var/log/foreman/production.log' + ) + assert exec_val.status + # Delete expired manifest + target_sat.cli.Subscription.delete_manifest({'organization-id': function_org.id}) + # Re-import new manifest & refresh + manifester = Manifester(manifest_category=settings.manifest.golden_ticket) + manifest = manifester.get_manifest() + target_sat.upload_manifest(function_org.id, manifest.content) + ret_val = target_sat.cli.Subscription.refresh_manifest({'organization-id': function_org.id}) + assert 'Candlepin job status: SUCCESS' in ret_val + # Additional check, katello:reimport trace should not fail with TypeError + trace_output = target_sat.execute("foreman-rake katello:reimport --trace") + assert 'TypeError: no implicit conversion of String into Integer' not in trace_output.stdout + assert trace_output.status == 0 diff --git a/tests/foreman/data/expired-manifest.zip b/tests/foreman/data/expired-manifest.zip new file mode 100644 index 0000000000000000000000000000000000000000..8737ba42db2e0cb65d4d9331057d690fed1c71a9 GIT binary patch literal 1255696 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
o^q2n69d<~e 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%0A}qzaP}M`bgQ`2VXKNTid?6{5-tk0CO8|uC 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�Zc?=p) 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 H@@0eXcw<} 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

bWgZYImu_5^*Q0?3l44K>7`JEE08OoOcZ&z#K7g z0zD&UZc69V!zq=>vnJdgxmhWh(lgwS_chMfqOVtUK|p;C9QkXGrp(hY+@c+<`OWhw z%&=sC)@wXW^Yi+&sqb1F`F+6-;-m2Ev5Di<HlEnlgh38GwF~cJ~Oe=64jL3snsm}#B zOoG#gU9WH58K-h+%J)cS_DCi%N2<89fx%&aF5_wZx1nkDVOT82hSSMv6upxIav+G> zcUmi^z^TI`N7pDRnrE&cP@*Be*@1zrspD)~p0u6)C5~*!7usEOiydlfY z%DfCI#x0+qV%kr_*W8{15Km$pzWrKt{5s29BPPhBP!BFR3?Z$*o4DFS0y!5NbXIOU z4;(OeE-(h#cbS}0AHX?YYC>qmu{mFgMoE_=*~D3PDX7U+|M(GfJ&Fs-N3dWuCKZiu z9ty8xKCpi~o&p!6I=9Pg$9fh_L$`nX@&-Sfpb+Je{!kvBpC51FZg!Sj$(3|wQvL1V zxj%^)`5rrfQbJX8hgXvGOM4SL`CAY>GT#Mi&SPZ2Hp z(QkbFWaI3o?L3>A)DqB=1aA6^E&c2zd}Njhzj(yl8|ULMY6DvBdAtS}$;}82r6(Pp zmIc6`DiQVfbij)9rf@Jw4EE&TDK)wW-LKwgJ^8!)0`54|EJaZfmG=1AiNzPoijz%X ze^9+NoFBMmf$CZ1-m3T4F!|zqkI6wC93w?95$5U(g(16pJZ{kDf0QYhmB79s(6+a(P z_`yrrY8!VBbytJ(6SJoaniYaqZ!)k`oBNPjm1)(MIJWwg))7d~xv3#Vx=?fVvEfDace4)GdreTUxs{D$d-m%GXD;7 zpeH9EH6cOy?+Ez~g&$ZcU8mOI?xIOGeNqIhsyU$bvT+bye~!v$%XjE$3Am_#;VW)s zAEr9=n#Q?)k<-Tso^CS9M_}{UULw-54u*gLIg;)D&I@DTyac-rc-I`5#%Dgq9$VEG zA+I2;npjs}kM${RsJ+j1EIqBV%53n}2_TWNPw7{9V(In!{hIx4Awje^_p{O&_G4U{ zW`bjfk2|((Kj~||xE6Fj?n)F4r_4Km;`>A}G7CSe&zySdXp{r?*enl>wH3IP$!SXXRwK)S3VsnInCb`L3k3WEfiR1XD!I*;U#OE2pZ~nw>=6pjPszD@r5Mv zb^7L$2uEXo1?{{HK=6`De{J(>`=Rhx^qZo1fwEiM(Qi4vW~ZE?8W1ZCd)TLlMsp*oXOWQI_9ubRTE0Lr;>Y_!R`?u7`IM`E{k8gn+$%V16?M z=pEeO)@`By$c&Z$X6P;yVm09G{N_e*-bIRM867|(I|uy7cab9R-_|#h^vm9iMuokr z*>GoTO@RF|ESV14_cG+I6=sw2Hu8nsfq9509o5wPtc>z75zbY>CHa`!UQH58fnBV} z*W|eNlS#H60!|h~|MEkjh##~oC>e%_sWXKpa7)i02C%M& zTo#rrpK1O01jatp;0RyGe~HAi^m9#qBllMHCb50U9i`werb!A3Mr^C2!;1!Y-hf`Wv^n^rbClqTj<3Ws0t)d+s^0!$_sh1-Mt#W$P_@2oqI|6; zggBtsVjX#HG5hXQb15j$bF~!Z^>kXcj%{854EjJ0xmH;AK7h{rqVVmvu5vc>a7b!o=HwM91uH#P{ zt5#n|=M*P}fax{-O4M?_PPP!8o&EgS{(UqL(*~uzoSG~ERV^HPR#3=8G*IE@8C-u6 zO3+?jQl2&~*6-geaNflJP#RmA5g!|$n6&GI~D(mm*t(wxX`TA6iAfoR5gY?7*|IFl( zWB&c4oH4TTm2Mf{>Q*-a0^aHWelB>YMQBx=m|tY_``C|>5I_I=fqy$bER;?rs-GY2 z0dH-Jr^ReoF#$s{5o7-py8;pPes)W$704IiE=xJyF&eZ;EWXNc<@o zSf9hEV+9ZS>R|_vk80A4mmV**F(7k_JSZr2Prjc@h48*iWam)(p#!Eq}aJW#p5<$Ve`DKI;B`1bz!c*Q%00vau8(Wtl?M4IK1oijXoK=KHt>{-G{TSTt zvJ1aXKWcW{ zQ-IWW^vaHXHY&?NJ;DX>4Bvx4OJcu;)Vh64rhw--}eM^T)MJVBXVLo2tAo>K>asa6Nyyr&WBql9cv@{oX+k z{@5f}*>c0YA5t@;Q9<*d*wDE+(xd-G5~panH_}3e*ca?vo;USKd%b>)3KyfS$GMGF zT-^!+GQNH^O8Y`MTdHfjEEcCgH1|%$8L@52X!^>jHy5Kq247=NNP`_(XB)Na{FqeF8F#+IzAbGOiY@grM}mO=$mPyP(ISUT8Pn*EP8pCn5zRz z0_{rpO>i=X70FtA1m{}oEd?Iwjg5;4QmOY<>rvcaX1|Kc{RkZ#XY@#};xmq4%LFvL zR=lA3JAuf)5qWs5>E+_ZH?p*5Y|!mQK_&NnA|aEXAtT*d-YepLM0LWmVDUJB=8$mh z>-X1Yn9?>f`maQL+z;&BEE2E|ACWi?u#nNmJT+BgKzC@aEYzG&1=4 zQw-8;^lr1h!j=hAmFXTXSxZJSaWZ=P5!y|wdRRA!QGX%On86bC|E5$eF$Z!Q6=i#v z(G93@#!Y2=RUSv|)FUZdhR#iiW6`#hs-m1OD}G{8EfBn;{#?MU4FzZYbJxE6b#hd`Qper$zlO_vVm%^o>n^w&3WC#ij{sZ7t}yl z)9iPOCf`WmhD?OZd4*a;JxUwMUwrW1!QY`;IrW|@_nKAkTQhE`kxrm>c?6N1B{~bUGrJ?)Omfj$@M5 z9?)iJA=aYW(`NvmZq}A$wr8GX9M?yG66}3!Mq> zq*97zRR&Una0c5I-qoXq@dpy9yG?JW;{60tF%1#XgsS`tWA!EP%hp}06yt=MV$@(c zjCcfa?L1%ixE*L1O5(@vnZXd&%V4_76~ygp$7!EROP{?>8C)8(`u3b%TLGUf^1Vqo z;5pQ=v>o^Ro6z|#^>u5XPk3Pn$hzb9F4*x&Iii0wj?8M$C|$FC!49)YsJiDd7XT~d zS~-8>{bg<|pNt&V_*2``u=?pJmDzAH)eFm;HTu_CUK5gVUN}#^Dfxp=#!~}4TwMkX zX}M()pT_ger}X(o^&G`$uH^@a<%%6yn65)`p1rpY@5pOMf^Vp973!n_9rfPN|2-X0 z9_1V=LxX&j_JpTUF}8#oyzNp=oCv0^!s3WNDc23#8g1kzmW(PlfE?4B^>2|IltN8R zc|%D7^*!!+TDtiphfz`lS)AHl_IgOUD2OgRxL<$E0)}@TjsOJX>y1wW_tl7{mHZ$+0JC=lCfO3E7rdSWRxTAj4a(#B(jfoLpe4bYp5Kj{(Yg7}m;e9o2 zjv~I{9q;X^IJzh;=J~ge&4;La(PZD5#nD_hNLUe>K*9z>kq)JtGn(xxRZP6GRI^a=K%KI z1?kT4Yte3eL8P$9$vCuFirSarXT`V3fLThR$Ce3YSidVBkZ$KhGu{?6>ZuTdV(A-2 znW|fLJ~Fj-WY8%kZf403W3U;pErDWlkcMWln{YE2wm|EH4)$+C`zu%sby#>z%caTT zOmQZHlH?Hx@l2iIjOG!j+A+Ll3KZr|j}_wHDvW! z$u&UZ+wWXqCB3mYRi}AB`A)A-W}#wGA8jNg!F%evwzN>;w!|?`{_-|`@*_;RhxqkfxNB-u%UX51hB9q-b%+vFg0L7tX zH@v;}Q;6COPcD~#EL(0d=JVBuYeuQp^4nY_!QW~#`c`P7;DhSiD?a-&qgw!f9+Z75 z{UMw0O8rRnE#*hKKQT8bBaQU#cSD!$b-ir}G~Gf3iyMw|O0m(ysEAMoX3I>ltqfVq zlZ4npnKN+8TTq3+%Z((zw1r7pjpFTdEh3*8Cx=?|Rmp8PG}}Oy1NvLkQBnH{cb#Zz zEQzpv#?H#~!IGA|sy}jPm*cPYOS2S2D;WftxS?nVhXV<(K>`qjtErYDxax0(E#?Ke zDv6QE(vGxcj^-11>ZBI}^Uaa$oAEGY_hfo6uc`mSo_?`1&FwDR+sLG?`F4jWzH>LL|9pSnJIkX#1 z->Gmd#uYN&#Ax`4!I#8}SZpXF8G&saII}P(#<*pyxYf>_Vt)r%I`DVyBm`d!@Jg4WkN0W6 z7QuyXP0XNlxR5nd}N zC!aLvG|VT9xGd=4v(P~BHz5icd4n`47JhB%@*@We4U`5w)vZn8JAE;Bqm}Cq?!q%a zem$2dTDJ|tCTh$Ipo*JMXNwa#|9SNdm>ma4NcN(9=4*V#ATcS1uWGFt+&0Z z-)O5|6087DZ!hLu<{p+~(*rGY@%#(27tnxu&$~lmXS33L?vHF4l;Cr>%6mII_srfUM7m&wRea+^467fXbidD!5Uo)htidsF@w}R}*IH+ZR|4Y)8mMvgUkJ`XQY9 zKA`lYf~li!)v=xA=HEAm6x>g6(XFIP*TtS)D-Y)V7EOF6sL^j<7jsQRXn^!$w7h^h zqP_+N@oiDVewaDjO@-A#+`q)(Xvdm@Hi6-m5BKdeB*CPdyC*SkG5&->)H6ZyAdY@d zi6k5C+Vr!U-!}>F>$0rZubiPLxpndx}i(jv{<`nS%3)oNu%cD zpskBKrjgrrdMA9ZvUJRO8)7&Zp-Savat-$|(x#cH-?AkuJtIBMl?MQZnVBQT(F`VA z*YXQdwZUY9){)!_o?^bHxZkYK zHBg2u(P(zB2`;NEPk&APxV^-W!&rXayZ@lW`<{@OmukR1AyXrae7AT56RDRdQo>NZ z5OeCCnB7*EYX{YunoQQ!)_CEK`!)K3P1A*1tU%TPCS&11vfq%1(XjPO?}#+%FN8dz z{&rS$kL02OLCLWNrs=ZP5lYF_;_Z`N`a->T4G1o;-DKO56cqeoEB*BkT6Or%z@Xm) zX{2>YVN7FmJF%{`nJUpK4E8ENvKNm`1*zjU^tzp_NJlO&5R{($;bXc0s^UZw93Me|iiQ@+Ud~(@J4+FymB{#+-ln3o zD<_s~#V#0DgV7G%u-~c|9IH)LfaH_eXxA;c{G<&>wE9=4b>fC%|OYQT>~9~vzz(BuPu@CyhJM2vVCtGVJ`%&C0omo(*%4}tqdH^>|ElGB^pI7P@$ zpAEFicEL?!vlKtd?PeI@!desdYo8(AyM!iK%4)On%$`HKh}aNVK8h428ajYslg5jp z4nWZl25f8fm+;y;VD!T3aYOJcV$cDp&Q}U6;#+Z1ll5*39d~|Zp2U8P`~4D#K$xiN zbjr1T=jq$iaSf0l>vj>Q!i4VtGF4o^9Qnms^4f=!)Gtw2^`XwWWm%r$?}$yzX7VIX zSeI)|1j23rr}wWaE>varWV1O z_!B@(0#L(fZ;RPV?WOG_6i21_a82G-U`6(4cT|S8uc;Slq1>`JlO%jNbQb|?o3b3@ zs{ks!c8`uGw9ee)TWM$X@pQ~JD2E_4Y*NTs8tuy=ySoj*%osI;+_scKkNvg?^OFNV zzQ2P<+2J2I?eq{OSyH7>)p^M%J63q~YDCY2zq%mj&fHt@3mVr!ZhH}}L%UUUU?5AQ z7B}1wFx$l{uhKg1ZDX4m=D+F(u}|&u97sqJ3X4;!L+5fI!%N#&>IfbH{<|3Gc*13^ z0s2yKGR6f?RpWtX$Fpt91MQ-7kHnl!Fx@*s;q~i4i^-0eW zm$HS$)#Q<5@Y%Xz;S~jW_y_V^&n!sfCg-pK-H$+X*98h2W20FGq$Y#s+}(IsL=l!q z=%rsZQtwAmeYT*Fu1EtyPmItZPD8(go@$6z3?>jioz`BY3~1^ezYy->?5eL~@$l%8 z30tE@elIoXx!FdJLJ?3qd7Yh>u{XC~2-CiMyo%!M_qRqA&P-MkB0Wz!DKm9zTP?yf zJnrslVOyEe_R}0vF{dX zy(}^ykk%wR^ugD^sYNRDX}KRrt`x!;>$U)izmX^e?KBdh4ld3Qzg@%FQBZ-sQ369& zf?O?{XzU+c$HgoW*rFi2Mc3qx5Jmt}Aw)7``ZOxa96xe>#tiy-l&s0Iz-~@96hq(2Gtay=w!esXEeD5Nb z2vuP)1op>(ljT`oS91j=Vw$O24=@rZdAK*PH7%ojC2-lE_HsPN3zeVaw?`N`dREJoO=R@%h zjz&LtA?Zq45fd^!fK24{wnnkvb_EXl5)9;G2J~}>kWF9pOqPC>*S6?-F+Al03zx&w zi;=j$lw%Q}3IgzddzL1jA|^xZu7kjn*RgvD7ZwULb5L87|sL;xEualS1-yk)?jydlJpQ;rJ>B$%o%Kh=8sFp%&!< z?722s`*_LG=ZMhJ9EsKDu;nlTBeJK?y&y~QfW{07 z7~cxHY$rP(ns2DymMzm{6TczKySOt+Iv4NUr!oo7>&s_m~Wn?Ubm0v4* z`$BSC>7W|lINMWWA=MLPnmzUfSa@@n7yB8=wTeRE@}2lE`T7H93Qi(A1i|vVZe#-ZRt~>pF2h}u^FO)~ z#eZ9pFIbD8swAAj#0QT4s{7AD$Ye%^AjS|qc>WId%4o1V07ZTBS&)|Avm+jNWGxe` z17zrsRC$S?u8fK2&AVc5{@dDYG3QSopl@8t(Cd_J0tKESUov&eEX$sbKR#K~b^dwb z>*vx?@1gsZK}94D$6#j!m=tN>yxArqn32A!)um5>Lj2^mA7h?E!4jcI0!JI0V|EBW z=qpY7bbhjlNed`OzD(p@cUJ1(IwrpuBtBKb0Ic6uE`Y+bSf^a9P zJX}hNhx#uRXVx!G)*qB=*=)F&^|*yBZxjHf&*Q523sVbI_}c{?zqGr*Uw-6$6PQun{M;y7iDdNHv%S9=Y|(Z7H5`6V-sa^5-~8&y zw=t2v7P+d8-e4YPW$QNFzPbSKo$Hy^p14tNsdHY}d{KCGvM@@j7rY)h49fG}Mj>y$ z-Dn=>((hGz<-*giR~H7Kvv8{n-);)oM#J)sopCi@Kj$m(WW2d6hvxa>GUm=4nIv5eMO$WDJ^>l^AcUu@BQ4&~?uwslgQlY$8vsZ)$udN6{+ZPKx z4Eps_|EYL*WEvcZE+O4sKm63?cp*64^x?yeom}`1 zKQ!>BlSO5HcTiluB$P}0zDm>5^d%=5Rwqo}ey!y%)|~ng>-Sl{Bl0VR$a`0zZ(r~O zFqR{o*AS7WaZJ4ZGavUhnNXheKlWnOgY+Fg*=y%=EJTh52er2HNQkjCQo<4_K!IyF z-?N*XH5aBn6=`4aEyd|YT}s>Vz1&q!w2dy(2q@%8qn0e@sO5a54fT7BmP`V6{s>Ld zvmNI9HTF+EMb;rycFlnNDja@{S@os|#u8eIcuM7~@2Z<&6vJI{SCZ5faI^8#1ljWj z&{cgyCO)Bc|1Qu@ZO!1{?5BBcm9#+oW|(nbR@Mc^UA8gPU^@cB1ER?+xD$=shL^@# zdc|MOYxaa5ZuF{H5Uf5b<;7-Fnn$oQb^MieQys`ncZ|xQBM68W(cD{zWlA!cF>XN* z>&;S7-gij9nSOyRE5AW1H15=8NAotR`uK5Nn3?3*6~+HWI57Fktwc-;n&063XNyrh$n1 z?rqmizr)P7NIf9dnQsPjQ$F|+WE2Nm$|E>e=NN`awvP_fOKo3F7l`;yq(S|>jrLYtgfusgKXFtr{+9i#cj#G1)eIS=7T+yRvoB68bNPN9-BdSjOGFpQ2g@g z(u~WJM^+ZJl!Rr9m5gsv)Kxfq4i>$BGe;sf4Dik(gc+50qbwhqXKdm_l_2wL`Ywj| zE`S+(9`>qhw+>QaZQW~qOAub~N8%-x_zMOo_1I1Nr0Z+`;Q0aLTHM&27F0zP%oiLH z@%d7IIK|@n=h5}6C7b-*e|pccU80L8pjR&zZ#N%?cE*mB5Ts@VV@a3aGwm6keCV0v zhiEgxwQrmu`SkjX=WFzLke3P6mCAq{Y6kv*3NfLHwT(=TrJsvgY%4 z5<;{sPWv1srf-z<1WJ5r0Llco(fzd7j^BRq`k!Rh79i8;nvTLN*lvyW>Y_EXU{)a>h+A93^M{pdwuJl zGiYd$0+mq4fJJ9x^L1_w$(~U$e`k0&a^!DXcM(Omf*Es7iHIy^hH!R2_P0q&z<4|R zux%zOoDM|EA`4GcATz#Q;>CWqeSSsia{4XOl6b*UzZZ2>uReh!kQ7Uzg6ce>RSmk* z!jfWO^gV*npL9z;pr`HNF;lG}5a({$>USHBt1rb`omMxNGO~Z!MyDc#A%R|AtOfc@TL-D15Q($pUw=Nixq+R>||z zUK5N1HhwU_*pOh!N_(Kbmgo5;Zpmu3U@XCO(8vBgbZ5-if2UD{b+>a+GkRl=C>O$o z1VOM}hwWlY6zQ*A4o*Fj;Ds;;8S8#XUxuQ|*F_+`_yjwF*W}`@g3QCaxk==9gPRNc zU`5|&dfl~`r~XYK>#fjAqetJw@Wjvht|dP>s$2`=8aT@>aPm+dR<9$GNGW9Sa^eRP zcDs{k5Z(K#Gk!U><`{(weT~(X`l2U^TzB%vemkK1W;|BAL|9>icsU}7TlUj$Id3e8 zi2Uhu0u`5$pvEw6pf9WsDhXtdr;MThI|7bU8ChW7h~oa<>~yxBaPm>5GL4Z6?H#g@ z6bDKmypWsz_}vjK>MYID;H2J9!I~%D;rDp!Gwhm7cFJ&DBIy%K6!p^%_2)R z6a4wWQC`!V3bbEx`3^i&X#rvv@Q}4_wTZb}9^xSYMj14^WR^ zXhcIgrd+5@*W2cvS!n}qYTI-%Xx25q<~P`AI!kXfqEnH-}uwGpPHj zaS<$vXQ+W~`iKbxw92g{KT#D^N{%mPse9BjhW>l~s)t)iX!TOLFGf*i18jvqw@1Sw zpSC^xiyY2bOTd%Wh7nJjP)@Vl%CE!38dJo@GTs!*=Z=8!J6Um8vu+wnuH)Yw$dJ>a zov#@oi%}pP62#V1_JZHS(lbxx-i#hM?nX4DZrL*X#!?9Ox?GQCL&=?z7-x90K{_qr zL#$YU@yy$DzZz6DCt9}&tPW?NSiU&>c9V(|TR3G-C4BZrdtK1+~WEKzV`TlI3-Z293eMieo zu?58OQ<8lR0e7?ST*n3Ocvq+CXMIA#%Z9Da?0mT>(0ujICnr(GYd_Z30=^@?l1iPc z`8SnUE^y$(>{1=IvRtsn=DnMhOniqct!Vnyz891z(=CBwHY&lZf&&OKp~^4&(V&(5 zV9hLhQ1wfFdc=nVKj1pRdg$zD;!g79TE|^GSGOelKCF`=eR?6lc~wA034&oUhfUsY z8Ox);Z`Qz}?iVpI?l6aPY*x@tSI1VhUhWKQBzP)xjVG)wRb!719BhzGz#JU zg5T$lI3~Rm`YMZG$p}*l#6*~z$lL%pJon3#A89hA5}UICC$89OoawOiNa&Z^>I`e}?;GH5T~?^WyO+r{LwNn1u8 zE`Pesxzg24SK%pAbR+`YVxop`MhSsdW}Kjy%t=+GP83v=7Tu-`AN=djQ}i1>bA2+S zELGa061Axo@L2w~tPh1HwcloTXo@{ug6g+Ayy)I{l)OL=HNCe|aL*wNRbl7IVz}i4 z_=dl~LWx|lNC4Me!4rq~ky~1=PC?DKprc9FMm(fpb9FC8BV-o3kHj!%6ELe4MGBL<=3;LPZ+b zj%GX6axLQFb&H0qeO5(j%b@vv=7fm1>rz_}dVU9MLEs9P*F%Q9@EdKC*0&z{c7>$P zTCax#&-euP;!mqc%ai6uNJe{kltmhUCco{|?o>gwrRjlw%g3Z+0in#yi{Op;!*y3^G#|I z&)SK(s0YKeLLf%>A((y3{@9+1uN#d#K0CXBi712dyjwz|J$%IgR?vm9>{k1lWi+GU zOAX-eqO4bMdBt51N8bxJWuo}R6Z9$8r*0a_P_o%`T%v+QIm}=C7mmR8&e1Ecdy?i2JpYI%`P+2j zjnm-lQ*;7v@quMXdpvnGk!G}fB3Lf|F@+GiQCRC9U20JNM#qsx{-7IZO7fmk|He04b;r<8e=){ z#_^ht!g_-G&mjuwSbbBqLH>pLZ1E{G;O46M{gJciUMhq9{wXK4(^C;D}_a`~+fr6fda%@dN)?B`F z@w=c}5vXP5(N7`u!W?)vV1e!NJ85_p-3rKps^$B6HNE)8j=-UM*_{e5esMw|jkU#I zrQ8$&;_z8SzEb-DJU?20gUeu+mi(z`yHBQnLx`;c~z z+CMr^-u)2t4Y0=OZ`?awpz7ip6)tLnqUdA1f!X3UOG8QqZ{t=^!-0v)z1zF-7u97-{i+eb_S=6+pLv_y7Z#rCsss+#)3q}mTrca-M%NErzkJ~zv+1FFyGN?23DB;|V|jbr(a z?OW~kYm?))a1zr){q&~GVitH{lrQxhH8zZy-(8XZ+5^b~8eiye5lL7fbtSLOvXemZ zrM6gkKM7^i;3qpY=%-p0|D8qx7_rlCeu;&?lkP{1Avit7d+g~qgXaqo z$r3mBT61ychu|N$0lAb-N7Lwdl}`p|0V1oi?m*1elx#oISX7x2i-dKK?QBzcuO_iE zD=De0_UyXR8AfcIQwn&k{rH@IMF#dS{yPJ5qz~gxg!8AdC6! zgv&f;jNuN4eWBp}euptO+_}X9Boqw^=n-9_U6M(jp0ldoC0n}BZ1vAE3fyWLVxMr| za{TdM-y-=@RWA;f@@upF0@yS?o|-NNaM*3>AWev}pii-3zAq$jU-K7>-mm40t~W^; zZ~Bppp<*KbVInUUexa+4MerfLQXtxx^}P}9pZHUwx{Fz@mT`#VNzsf@BFRqJrpwf1Q7)hdBI!L6@o>@W6p&sU?JE#H$9&jJa= z?#p>Sg_dG{o$Jv)t??(TVz+c)dg6`8G|a>j#?!G&qG?R-^ZFIfXF9uUiT-BL0*`4G z{qz`*nQD+IK|fq(K&v5tb08UO8G(!OL(S@ad9vSP(^$!LHc_$kmF;{MUOezd)!kWt z+ch7x3<{$irO1QMM`$>Tu3vZ}gg?;e;p)5y9P+dY`F+HPJNwkk1rr(oK34kUZI-bU z!M4G~jPrX?S7Y)36g5v5N|227)=|@CN?JQ52+mvFb!rH$*b)f50kaR+<^)m3e`VVO zX~rFq`1dP)Ogi;B4co7|75E}{3ZkZM(J8@6O25@X>UtBfwgKpjDnB*B)41BpHT=sR zZlLmQG+f-HOd3Z>r%B>@%r2MV%8zNk%v!DgYD7xpnhF77`9?ua^}^=Kv}&?qKN+x+ zXzBT8gl3CbM^!XYdjmM?`vK5hC&5rG__ zbCIAZQ0e_G^xPgTLd}v@LQ4~{aA(ma*te3&oZppZtwsGMR`tWx=Q}P2Ol9tzf!CIH}65#Q?5ljuxld3cBW$5*+4F3ANwpC|4EMzYO_YwJ_b!tUCQ zd^n@Rp~1^K2;2mG%BhT~SB}N&kWp6sVds3W=pwzTK44F#=5Pi#UQ=kPzdPz z;(VlajSkvHa43Jh$j^g`^ lEMKKLz6ONS+H4>@6Wo1pA!Bes!QuU9+UESH&R(K%|r!UQ#&)aCse(Y=LD&R)GnnJ~C4wvVr!rS?8OL6xo%FLQNZWMY+5&-QbBMqog1NjO zsxM&m9Ncrf43+%O1qJqqci-I;X77=*nnioqg)AbLwZimmD8vsauTtcSi7yaba;&=} z=he=#JAYxU8Wlvro;;yJnytW#oCr~Ww@F0lS-v`zzSC;77bXtF7cE(gPc?5Cqbn{wj@WIDS)cQN%|SiIOQ1cTxpkuEzn@m6E%k zQq>81KrFrAj&{+>>E$JgGv6$L;&xoa7h5t=$P8SA{=&2{CpImVJN+OX_{N*Jn3}co zF=jYXm3Ep@(H8a{ym%ev0CuJcTdV5#JS}s^TIg!o?`|oXx#m}e`*lPDFDBn=%(%qb z(fr=SMXwz5Lr9LBar#kTBYRyh7w6Hfc5=;8giO4^zt!q=G9-5@xyC2?L#zmeFJHh^ zpzf3j%Es_=)#1(n@)6xg{-mJcm5TeNP%4{!VTk2mxu7?^Kdd6}EhxFE$_ANkr|aqv zy3hx2?utxm0T7LmsFLKQ9%gH3=mOa-T$u0qZ*-+7uJB$m9x4yRHK*MY{FLip_U12> z1?115`4fQN80Gx%372n%IS!nb{L_0+SGr^f`~ed(EnS4uyCnZx$R0;dH zpi_YY3e?Dy@0~Qb{Uz&Eu54sD9Tj|m1zF$yl}XId%ZW zp&d_?8a5?0`V~PzMV=12L2-6Aq9dijvdH9t7XcNEa4n;pSdKH1D2J;(u?+9PtfRdv zlyO3lZlp;?WaONRaoY4YR|KV|r;?3l%kdl^eUOLUm5rr^4i(t2#j4Ex08LvTUKm+n z>!a0*D|rLL$R`T6SH@|6U!9+#!fX@!D81ezmDN66&3j6ynS$U>84mo;@yGIZU^^ps ze*;zxq}<=LU`}~H&J8#(d=&g=byTDd`M ze$rHtyzubM7;*{2JS8t0)st<#%GVO1tFd*Ly-~&0b_tMRvJo?vM*rZ03b1fT4;>X_frg>The6H|fG3`Gum4CoZ!KqaD2(>J z3I&)Mg>td_NxDf4K-=<+@ zYZjsvN+m)w`^~*DWkE9L z7ejhI=R|+#vrRBJdPRYa)U|NdxjnJ8SZhDdUe=EyHqiy~Y2CW~`w@)K>56?rIyi)m zHcDIqgt2;${&Il>#+w(cEa!2KpL3<6mj{Jjbt^RAaWOCzBP9FeaCLTm&W9bJim`>L zGnX_sr-^Sa1MukgyFI-XUf6_E8IdQ4+WFOo#~p)b$@rT+0)m;;I{rFvjHoL|tmzXt zZV9WO(wl%U5goRX<``d<&fN;9JBQaWGd6$lMqB!;sGybDZBhbw z2jlZy>T8zSGJU{G?e|vT^}U_~qok$l;hbn5`oTs+!ZR#6XW9D9lJvo!l#`Slh{r(I z)M8h_BCL;T`Y<^9CPtiUtM^I@#;;JQi!P-`a#{8yduq5`A8M5Gl;Z8lKH%YSIWIvy zDyI(?T6+=86@!z7kut3@V+kaG>U_PA;`@P7jhS=iI@~W>az*hQ8ZiZk$G85 z$W0ZUilQrVbOtIhS|&xI`X z6(uAc?FK~cA~&qBmL?ba+~|d};$Kh<0oJ{o)G9&LU^APR8(ebRS*HcMLOK7Yx=r&3nCG!9N{nD)_J zB!Tso=Y*g0`ZyBYzY*!0g5YjzIR`eEJ9xB4ekb~OvoBg;&~QEyL-d%H9=UovgA$e@ zDfK0OVTU9#u|r{?!cb$Yc=HE11cE1OSU{hzDd+_X$wh06_OON+W1(jbC5QC+ayJC8 zh%nuSQH`~ts)f!9{dVOGkm>BV*SU5vSM?~E+Jkp$Nbg1+yRGlvtN5%wEwsPOL!;Na zUJhF=DP2-4*BOM#>_Df!qzZH2DyOpBpf|Erb>nIrh`On_R$?sKAt|F-S@Ai1|0`C= zkp95D?|<^vjZd@{tGiOQj6;nlv$~vx?FXew<$J%kG>z*|gAbfvr&hytn~29&|5ewR z5AHJyNaWRez;91cq0HA&I9!%`A~+s}IN4!V6zoy@dahkFM6bPXxaU@^OX$!dZfMW#-~1G#B1A3MkSZ5y*EYeQL$^pplNtc1wWKeF_^re#KWas!#O0uX2B z6{YZxsLo3r-4ZiOb<~z9S5>4ieKy}jmPyke>mZB`uIyVK7DH)*c7c9re7u2UK=Uf% zvbX825i1Q(4LlkQYciFDJW0b+xww(>fv)A3cwyVMof<6KsaIOTc>_;b(&0Zgs_TDs4CYOnZ7 zJ>R_zDoKt|{dmQUhC4zB^cy!wks->0zd_B`co^OYll6|X=y^a$;i#IaSEuv)8U1=z zY4x^ol}k{3=_?qo!LG{hRZ+s~P~Wexap$M2K_ha=F!K%mA=QtcgugtNP2w9);>yeW z;#HY$``k(QB^aqY&4h0qUTaLHzGe~z#%s){GP0_b!jA?*ZUG|}AX7juu$mpJ?UJ_a_qDf_l zYF`XS_j+tt8KSF<@h#3owS0T0;KW=Tl-algW&(fnYIY1GZeyveH@*EX4V%Bu+YJ?l zJBeBsbdeo7MRjnNs3nnG=P3VS#LCgi8<69U8eQEO++9K+wUB!}T**3r)R6ZQA{7dA z{6^t%%GlRT(6*zEcc&itEsKMI8#M4<$1)hLl6&e#xYS-OhkHsSlQ@)3?0nVak3h6!DUJgrvj=9(N(g9=|eQ#*>0XlKCPzH}PUvB8%*`CTL#x>-qvO z2i8AKk?)%a61mZhSnok=4R9t{;vk&PRcp#pfLkg}cGI#8LF8~2r>apg3UGy(N%jd< zBF;j=uZgs6#k88(y#i9&TdP?iHY;ISk$&wF7p3bgd(3 zoiHLdMF0eLsPU1bB#TIpg#{@g2di2@8h^PuIK{it$_1!Mmhp%m34zTX!^lN4`c2Ns zT9g3qK;=~5$AOyu&5(;b9CB8Dm*W0**cRu;lvDgf#qQgqf?qpE<{IS#EbUA-mm5SYiB zC4X0_>QNhiR*V1UAKo~RH}X=E+JM8~{pKE)zXMVaBWlOmh@+s6;~zmP?PLIq=@Fza z%Xs!zyURByxFOTi2d&>&8MTN^zZ!Nyn9=>o?H)8$yOv)yv-Kyb){p+<0dt}6!`a5k zor+Hu|7}jSf{I@ldk&GJokD`bVsF}f;}`{PxU91{NFzvN7Ok7)tPE5H@gg(yp2jVi ze!A3KUz=YCJnQh0^toSH)xW0i(sv&i6%hySEo--lkw|}BDOr*LvfEdR8GC_#5=LcL z5TbaaSkUMzE_Ka)6Sx*7s&~DJp|v`!GiNG>OuH4_D8Jdy0o>wNkx5j?+*zi-?NGs+ z_hyh=RShcVN0fr)Xl;(3q#C~McvQ)jpoYFvr6{o9O$!u(xB0`)qi5*UgLEQQLK0ag z<3?S)csN!(@T;xe57xoXP4iQoqtU~fPa3OQdYcrmHPmBoaVZa-cEuqZaA7j6M2(yA+j$jsxnRB9`0d#&n*bZ#=rFkfFcU5=*rw47d3D z?F`EMlj}ap#|HUc!rRGXLaJa6$kX=&x+-53kJ+*>yv?DM} zVcJYBT$b@(Ee_Q`pwS*Ir*OIoJm?&Jud+lH^Oh_1E29QmHng%`jMH@M>CFy#VmqP< z3g5l4j2kw&L(T6C?x9D0B;ama6vH(q#V7h9VTt<997>X2@TKF2_l@)GMRy*=_Uolo zGm7Z^p^ZL*b)Pr$`h_6SdUdc5l{bH*p);1(j2Kdx`DGmjY&MXh%pC#mkMqzo@y1*D z&1+F2+wjbi>o@6qPq-4ys{(|-jHmEcDw!I%CZ7mIPTp92?phncr@_Kua!(P3n zN)9&$&ao9*&c5to>_d#x=NeB=Ple?LgxqvRnFb`|KJ>zw`$>tHIkg#^PKG2;Vl>J( z1E$$F5dpJ^hR=Bu1bu@=_}33}kB@PXHT|j+J+q(@Ypgqyp+NOIlg5`VJSu!!DH{+h zJ7ybUu)tX`RGK=1;+mE+DJ+de;xFt*FBwMJfEOURyXDsL^(-2|vJe`!Uc(e1-)v1q zHb?Da0C`@fCaI2oAjVi1wy3dn?JSJV6yE;^BdQk@*rSe1=KJJAOsl0XkPVTEhN0#N z=fZKrhY~9YoWC@SBI&Mr>Da=PX8b;S*dl9!yk%yQ^T4pR1jB#Ir2}vAYi9I|SPAv@ zx=h&~3Rmqit~cx6>=RFjHQoo`uUgOorNhOWSS$OzEtEeutICQ%J2UMWFJ%#m`4&$X z{t6qE`JT&KaKY5231QoN|DyW#T~0CPy7<0S&$9OWz7_|Z1YpiZLyJuFz>F$?VRJcy zJsMXcyO4e$amM~NJ1r~+O$l9ARCYWDizt&4^+7NaIReo@5#_7K!^}Bh%UD16Xd2Sb z`I6~pla|}c8Z##OlJwmw(BK|Gf?@E?x^mQ4B2ly426i=KwvV;b zgAzkJW<2{Q^2`}?p}y?e{)}SP#qUfUs`nikK)2qpmnff`f7WeO+k|N7r^8Y{UBWh4 zWf$gCuSe-7+SvNg+TU|}RP+T-P}!gfH~?SP{rzqMrZit<=4?gp*YeWZ#qA@N3`l6* z#z^R_4?lrr%!XK#9_113knOS#%t`{Fy6*tS6m~`0Qa6|{XiQ%tb@O?{I z4~?q*?o!>ES$%GtzYWp$%Y@!i4piBRBIVv7R-Q?N0YY!mi@6o{b7|{29U1(Q5#3S@ zy*%TV1>1ut=*F;r;8YOaWCVQikz3JE?>B5Zj7&Bq(P0yqS9+%=C$gV{ge-6BJ4H-W zz?>@Q**+0wbY2w46siGGMONnY#q!;NM}G+`WyaFKZYN=g`|M8sJ*B!1E=tdhc(0tO zMty`Q^;a+H+OPB>fs*Of=C%(ZZ)tACH1Z=Yxl!#h@+1uZDE9n&zxD54IX?^3#W=Ko zr$05d`6KAxZ`1)4*Afo-mRoR0{a9=t_+G<1;3Ky#A2MAduaOvLU1X%gd|j|g;q2WeTGkl;&o~%`;nPpk*-#vw4AX4Rzj9elJ@%ynZfPC1M26bhi!9&F$xj>y!Us3#+Fn%6-7|qk z>PD95)C>Jp1`+eE&gMfcAqP*o$Zh_#MIXQ?5IJ;sx=vbT%a3=`?9-)AL3&#Y@L!xZ zcIZoT`K`Gu z$Pr8B1by9BYv$z9DvRBm8O2)7V)tVxDShW zxJj4I{;uFUn79MQVCc7fV1q0f@Yf5clD-)x;2Y0QhXv~5yi5+dxTg>2l1 zMHxeCGa&76PgP+i6dqloIe85vTM-LBXH6%pfMilH?}auGJUjM;_tj%ddXu5*s-W2w zIrO~D5*VZ(DKLIQEM3jteV1JYlE)c*{U=;U8iiIqlJhWWG^9Gzj~`n;TL$Vz@R+=z zCf_V+RH(P|ik!@cQhA$B*Yr>-SgQ$`;OxpSBA#{BJL5*y8+Q%ooP5x5W0NU)*rute z7S;PZv7l!}ID5&8mqaly>g=xo80{sRO+4N>pZ)zdTWilx*wtpH3GN`c9L@CCLbbN2 z#R9$-Q?CfA5YP6J4CwX9G~m2X!*+VYYjg-EFlW?ndHXwS*6jN3;C;z6VZznX<~5pq zXTqC~zs4)XMUu{osW}0v3DEQvTq?Aa^AS0Fviz3nf?&_}`YcdRDSa(V$))KBrG?L! z&A8LMiM+&%avrb^ywUZD-)QwO$by`@cmTR&?*U-&|#`fBa^yf~Y6!+D=tg@CRg2{rJGnAi$3Jh0S= zvi>5e7k_~*aCtRd;t=hfi)EiKMsmWd*uy@1H*$b)^^m=vmFkl#fq&hVV;k94trqCd_qAW6&RV(ls^+8VBNG&0YN4Q;v)MQ19}Chhj-k z&@+vaXy+cK5MvqXOKL&cZtY=V{b(>4b+ec)$c5s@dSdd)o*w6e+HUAjY{@6xJ~u#; ze40m2s{`%-BHDw0=h5&dBb*2Axu#qL1U)F22wP0sSj zd`2*k0E>N_9txz)v>jXIjXKZ+C=1raBPWDd0oa{TpC zKEBnwe-~@FAHl8ayyoAc3t@$%HU(}W7I1DH<2GnaLma>7f{Ov0xIwXDx6DAS<} z)4w1laeg!NxpJQP?8oXk^))V~Y$2MAdK@5~h1l`3Kw}0!bE4EB9DYq%%OB>@v%X!^UPR*m8}ug=52b2tUS^Kk*>-EQ@1>Q-MGYT$mbotYTt^mXjF=txH} zDA<9rESm(LtRIK38Mv-6-F03NFjUI+hT-j9(LZ;qY5bv*I3~%8x#N!{fgV<8N?To= zq14KfH>M)y6;=R=S{ED@;U+fjk6ZTn4IkmFd6}IKk zM@rxETgT3F?Nbatr^y69uMvG@$Q9i5+;*jMy4+!tkEYG1%Z`%9tIA=qg6PBkcY^}0 zUKwSFIjIy)%tSmG_!z!eRawb3Z7e>NBdW5)y=h;Dl|~;W;pgaqu{Yg)vde?a@1tiJ zE^fyvU0NdsRlrVRAA6MRoe`ZW@gO@m;eq!3t`YQ-WFq%;3^dVOkhxTr(eO+xcf@zI%6J!dNMIG}2h88=@a@ps!LAY<-cM5?8GjymzvY3* zdMA*HWQ>CzV|~!#JKr}DUH^k21fv5sRcS*)E%Jo$tq}`_xu1L=@yS3}$z%WuOZ``I z@+F6NOwD1EHj+G3Z*=(#_oE_5qlc#ZVO+wYcgqN+#hEL9`$p4mP?8D*IMHca#j9$! z`p)B`@2~Pbjy+!apx8m&T=N!nZ7`vaFDsd9{3Z$(VLx5eebrY2U&n-9O6<$)av{;tY|j1gU|~<+%q<)%MLI*yCy; zsb0LAlO@@6TI-=2XMDSfZ0`QORY~*Z4t7j9>-N;D66|0GxN4=w(-tJMU>cG`dd2x* z@^N{e*N{xscJpf_YjLmSOT$b^Y;3Rg%8njs{jYQnI}j;yB}!8pX7e5u4@c)*sGknzKfh@fBm6*D)YxpOu}a)ApSxRm_Y?@U(QdF2|U& zY$2KvB>7jDVMgq7*9Xa8Dcd%gDj#&nQNQy97sQw33R_(M&gdd1->1Z}g~?8jSmxvl7XfzleA!lsImSRa{drm7hoS)4uWy#Vk(gG) z#e1uEdb;y+{FwJ3OVZ~J>2R)4(Vijz&Jocka=2ZfBxx$kSHv_cZfJX<*m-$SESvv8~XDdTW0xQoc^;7lt5{6V$WLDO5 z{#`R+n@_mhpwJ~Z4*6;=aRaiKCf!Xn9SAuJ;>TadJ4$xvDNxbyVW&-c)KeeC2tid( zGK;E8vM?MZ96yVDZAuK(0>AsavG0aQ^)%00gyAOOjYLN3q+X7vcCcmj=L{43JrM(uERY79Y(1BUAk*MKiv*7WVpF`^W4^mGIBbBYxa*e5l4w z*z;M9(fdtJtrVx%&*S-yv5i+W&r2DxXFgJfO+ckmCC}6At z2+hqG$AuJD;*(6lC@Vf<;A}d{>lALA6xAO2ld7C@hZy$&qQpPk-K^F`>Gm}rz~2M< zwe))A&z}jB4N*2k-gQ)2i&3~^DzlF=kzfP9Q(jQT@b@(s)aVrg3uvlXTMDpZEdQ|b z3x-7T)5|bXAUyx~lO3If37x0=yh^Gz-TQ>SvGL~i2!E5<=i^Ic!1JOEWH$b=i(J-j z|8ws*Og6vxO7+OR+grSB?7>IjL9Gdso=)o-(x;X)DIUl0UOTejQ9yvXW$?Egmx)PQ zZiEeq2+MzIqO>N&b0qm?1qpjAPUAJSPdI{(j@!yV6Fum&qey^1eg@yQu1J|mLKPNi z5szEd8eip0NjnQ7@FD#xbrZyFH(npt6pcFfPVcmrwArdZR=mssPBD1o95b4hEXBMQQAnzA~l0=_3(EBj52#O)lLAVSu zz2uRWW|$yPlDLU_IhfvgXg&zfum>%{*r^HFu*vJvZi{&$uHf(K6L7#*?wZa*bgDZ_ z43=HZ9Su8;Q`#CEMtNVBvbO$|T=;T#S|WK^nNIH<$2;D?_#uT9Ih zV5BT!>@l)+Av_7dckQaI`|v~9HC-BsYx{kB*G4!lPguB&rIZwBL<)TQy2C}vQ7tiJ ztl;_bDy~FQ;D^jr8prp_FfZ$qzkkpdPD5}`QoB0W00zCG$zz>NKD?a7@EK!92Xc{$ z{95@sQV+2DFYbu6meQ=1oDo!@FEE&?r5I)~Pj+Hq z|9ZzjFPAYa?cKT z3DDxJh7Qo$DAXEWI8vkJ4DO;Lf=GnaH~*BnK_#z^G7ZmVXbAi{$;F&c0%PDEzj9#c zj=!H%3|w2g^!wH88bfABl{%zLe!2NxP2N{N7d&>^)`W$!E23;w&o@$AOu~v9&{4MY z=0iftesrsFY@x$teCDqf+qXJv?@De#d6J;TtGyu9(Vao(W``FQgADPs0hO-gfQ{Dn zMxX$Eb7YR*leak3?vpe{&9@HEb_tUtbBzU|zYf7Si!6$wv!djU%T=Cl)&C1(N2# zJd)@!*uR;&AI(MkAt`l`?j&GBmxh~q$6@Z2&zkm67b@-?V5lDNq3#B+>#|>Wqt6y= zkgd|lDz}o1N7?uZIi}v81XF?$es#Ci*@MGc2YS z!Ot5va@HSDP(_NP$v3^l4J=H1Ywsi}heXXydn$D;UNLt&@KFE>K=!{` zs*8AC$e{`Mxxmr*v1A9Y@K3JSDjR*=sC&ymQ^)2or{V8j@wjBB)lZBBv_s~-V)4^( zX@y@T<rbD|+l_-kOWzr#)ft zqY>|FBr6Z(?oBj>I=bdfGd)|!s{_EbI?O-H>{-S*FIKhEv}N+*$HS%&++o>Gnz!D5 z64zYdAH~(dIItO||KMC!h}jJ7yeM4CY7E-XP#C*UY!f$Fc5rwmAr5@7w2)h>NGS`1e*WawhXcHD6M?N7oVC z6&FvdM-ujN>Bvz6xyy~NepKCUwIuTA1}LaBy2d16A& zmG2?u+0YLL4iH#%|0JK-9?H{}nD16Ns@0Q^3MlG4iRJZiHG07pY7*=LY(HD6-#5+& z32(JbsWU(!T9P8d4b2S==Xg=H1md%bQWNX#nu+Ew28$Gzh~;b6x=zkkcd{kV|19Yg zQeWH!cJ?FM;1@1N%is@<<(?ew*KB>6)hr2Z{}P5idVzh}xb^t7cR!g~o~SwZEC52}%`HtzJik`&=GbEd2P!uXQA5YbhWkNk6V95N^upU}F=x=#_|F<5$2y zz1&=s3wYJd9`P4rde?V37LIBBnlh-wfxSuAb2w%>kB@U7AJVxWQmzNb8OO{5u1y}d z9HqbeltJ&WZdB+~KKpW4jrd|x@q<&kXX?!;C?*=|s)G?)@8&QzPL3#g7{EB#;S0o9 z-~FFv7$f<0kyRiq?EBj~3XbVcaIh1ugPP>1nfgXKPR^$ne!6t=0;d4HpS3US zGGg$9DtcaM<+-9aD$74A{6<@&Y|FW14-bIuOwn$F-p>4dBl*%;ppA_hFZ*@V-jUfq zx_8`sh5s^lzQor9NkVYICL#<}U`^pW#9zagehnsH(Huo3>j@xGxlZ_t=sskVqJ9)Q zNPouY`-ok)3+(t?z?J^sQH(}C8P~(w$4x(LI6}&NBXT`ONZ*;{9%Qu2S-e~~`aN~f39?UJ@JEyH!(w5+@2EKl{5N45 z(|7;1SR%@-$Qa`pcd3MkNKKpy+w%py`6L72Z|+wQ`6$j?iDH=eB?NSm0wJZ+Dfh`3 zY;)*))6jRlXiItp7EbH2rSu4*Lv1)doS4P6;SV-)TieSmvY(ZLeaB5knvhQ?4R6;a zs8Q!`A^PwOU)t%S7;E>g?4)#@`~-aVyW3?9ao^cJYW22WR-`FS+MvkTlJAwL&&C6h zji#MI#$T^h?|Rt?oh$$|K+L~DgX39!07|sSqClrwezU*lD?Tl+u&-k`#$ynD9^o5E z;O7YSn)da@QXaNu(LJF&dS0(Q%>z{FJ$pwxRiJ4aFITA*3QgCJmO-4%V1{;Lc|&e# zhjMcvZ1!ieY68LMvh8E-MezVQdjnz!Eypb^Rh3bW)vuOc-5s4~iBq8ZJ~Gc(?ueFrj#~ZlA{x&zk=ah4@+Sa3iJdK@1W-jx0hn}sLuy1_r!}J z*ctaim&6itKnMf+H1ar;Vnd8RA>$|~>RdV70JB03M}XX z@$MmE7O*9R4)PfCdb;2Js|QJ8`J7N@U`K8a26xiK#9(U>>E`E$;U-*;bl3kx4ym|3 zaZvzj1wHDfm)Lj5#pA#&t9s9w?NLw z;zUiEJM}WNd?f8=Lg1?aIR|+N||1Kkq zp6=i|KLu&@B)beni=c8&Z&lU zVPa~Gvn5oF&w{*>-j%l7eLZ|c=S-|0`Wbzfsl)`Z1NsHOydE5cY7&z2y^W!G@uEA@ zr|QRA{t2ISMK_lj<*F$B)6?rHA#?NZ4tW2!0BmH<{O}-xCzoxoH^I0NX|jdiAG^X9 zY^E+7r%ora&KJB&P`ZADcB8RuvK;nARtu!@t9=41kOkwh#i|=22fyGjnkN{pMx@)^ z??+IIlzA@(OIE7Ec3;76le3J+xNm0wY%$bUhO_P$fz{o)6f&k!#U`p?M|MT@Rw1#3 z(>)^%lzRXDShI5wAv@K+K)Ke7lQVRP1jx^e~gCt>YZ9&uN z`+#V5&d!uBA7lwo%Uyx4d~=hvcZ--JD$GfWltTPZB1!Q`cB z+|~Tm0mzis0bK3wXE!`;h$OLG?7r;B9Wmxzd`NV{{hWlMySpisQ@D&#&xv%S1-qgP zaTC)-y9n61p-f@x9 zr5Jw3APTh5HQvFl_X{1IXbf#fMyJL5N1ilDpSgsZ!VA;7J%e@8GyNXAwf)6GP)&p_ z1#kpamW94dAeSW(Z0nC~%BfOubTI5RxXG(u$SF@Q1^TT}`gSZn!@*phXec14AOi{r zgf_MY)@!f}Z za4K#RJZUPOmTYo0Gq zoEgxHkER;SW+H`oiAo}|&J{dLF-jsdmV(9lXFhu|blWA+u9 zUq{%##6)0nsCgBibG_p7o`zzc6NR+l69rM|U2a1)`;!jvC-ge0H%yru*_)x!#A$G3 z8?rEGRiq2OJ*p8%8}G;W7yL5x)ia&N;Fyc+>s>7|kbpw?g^^PsRoVU9*OWECpgAA1 zI+Qcl5>jR4gN0TINo~+h!cHzz9hx&6&c{FN!U3o-6PBK6_hD@hq`8gRCgBbx^vizG z@16f`1Lxz87Lw{1~1pTiCaV`A2{cQyN}6M@|Rj7 z*>}t6^zJ%KMT{?p%UP4;U^0_neK9cy;DrSl@*tY~TIFV13szW!d5|W@l`wq<$L@$) z`@9Z)V`8kUg)-jw&ys0Bhmmg~Jpr{Nya)Qc5BGB!$FLiD4yp(-Gcx)0gh{wdc@X?K z8>@tQOW^nNqrFq~jYy8|!|Vp~)OAbom=|_#`lD*xU)53-kMXaO)u~ZjMdek_J98T* z&P$PMYn1nu;9tL%F)ZFUAl}aFn@(jxdip9r(Sx((2G^1kX~n+p)(+C>anoyXPqfEm zT45pnY!^MeUk&liRA9~uhvsP-qWVFz*7;zQo6eu*aFe@xyL4-)_-8-TCf`W0NQ)c| zJS_V$e|Nt?rOE)co|*jHo4?wxs;D2aG9mLyZ5zFuk`{eOG3qU9kdNF9v-)=+{Pu~= z@KP42Ni6jh9-!*Pl6j@|Um{}#; zoXLZM6ai!&jYC_h%wN}pb?vYV5~0DeTn+4EFvf0hIE;6(bH58PXBnljx zRg(uei_rIFwK_!c_K}_S2c~+}|Hc&R%&_M~etzk~TahsoN>PFi?k4z&9;2MV+%y!4 z?yoY*e8kGfM!)!DaoUEILf;4v=g73vDSH21_$>F#)ZP1Hb$MhEGA#Dh0Es;-vtwvZ zi%awngw6Q=5bgdN9M67h*ic*lw@aZ>w>_r_N!^^4YUz%xysKC};0JNmygK0bH*@WC z8?i^gNW45gvfMA--ka+wTkCLrcyF5BM<@9)?JbzMnPanqkNQi10}*VlFb{A*)?yR# zAx{uWj0R0s>)`>lu;8@YY!`N^!5SMkIK@++MCFg}{w%?FzCX&RSr*t@P4#>A^1rs^e&i~i82H06ZP3!2bU`Ou z8qRVZL}GS(j%J6nJk1I>%#u_Xd9z`FSz%_z9tnkiXx%wDT}@p<^;ze!-3$MIhQLSp z>5g_xSa)4%$tM&$NW1R%2`_)I0Af^qBULn}Q&II2GC9+Gs9XrKr*8i77_leKk0#_$w{JQf zqb;sm1Xg>e2Rx96VRvA2Dh4^fJM}eYnz?(s7|vh$jWd{DU`5g0^wIjNo#|7(gTU3H zZIz(W`UE_r{xa5^$Z3kKYD`QFPI{%SUC(v)hR8zY)J)%y8Zyhh#~C`_A=FI-g?grLaAg=a_D}Q-RUImRO7d>0acY&8 zK#?>b>E>9l{*=!paoJHzwyOsEhvk)~B&f!^;FxQ$$9>ut`u?dS%P4k^8S+1x&SS?} zB?`iOUWEnGge5uW%#L892_~DT{~2A~)kqc|Lvv2mSEA__0az(-oUbjTB||+#_ej3z1;BudU@nNqGq*YfsaTkjHmp67T$UUI??_BaDc` zyCL?R0g;P09u<0Br*-B4igpB$-9JOuW zMbewW!n|GzNX`z;C|s8Mg>QGLD^P|-OisST(yX8$Ln%P4X>V@wXCsbWOWVgVl)#{#O zyKmhZQJn#`uKjSe=UCd84l$ST@1Sc=U9fHoC*2^&&Co^Pp~l_M!g^b0fK3}@pZ50^ zaP!kKKoeP2Rh1#E4H~i-Bu4!RSL$$&pzA~YNSsnFcRXd5bbmiJ9#Rpj*RHwkT38n! z_ybIntWJBi1gdU@uB%W{uz0CNq~0oidR^oh=Zk+s8_C%+ouae&=)P;EoCh_V7tbfK zOC(zhjzF^ZJHR+$2=1>FGuupYWb7Sa{;BTjJ3CIvB72vvA$hPzemhTX&tdK5fMtEv z2OQH5#1fb``BM4hr`kPMsAeCy#H9T(@*K8o`NEc3l-a`E8&eQYyq+Tdap%taJq^R* zYvwcJ6zNj3aBJjzz+LMSB;h);p^y((4}+6j>$H((c(Q_EIbkN`+6@D-%otTESXGj? z;zHKa$;xZ1kNaXGmIRupz0p$sj*f9{X#;zKUvWu`vEWp{Gs%1aR`goKl*+o)J2uR- zyn02)%B>_ggJmUbaQ4FdOo}VHc|xD;*ERk5nZGvq!i|IUCyYt@`YmyK{ zm5;yamS%XfoDT+($loG3{r#3plyP>yFSgtHO@g?y3A+H&xe8`%IjX9VmMuYN)bB^$`Ku$Q`s`oGKbu=jy( zvN+J_u%bE4onrhWYPF`Wm5`+dJWoXimZpezl1F?!&*a_I=UM|6i0qH^3ips>7X>2RU%Jr!;6Jp2cm!6DBJ{Kq^DSA9^ zbSIxupAP8$Qr3#<;6Z-q_XZrj3fqE?j1-P+%)K7ux9X((1>P$Eqrr#C4^ZU!`e)!0 z&nT|r)vbmmU&df8?_3!mx5?#qp54048FY%v~&QGlzfn&dRNFxYn(a0{^&bj{?#>^_Ssy>+D^~=d_Tm|j7ufLkfEq?D& zn{^U{pC>aAc6yQ<()X9*f)J)g|2@|k-7?v6Eu<<3aIZ4Z+4DbPT?Mx=oyFVo&QB^xDs zp#Z8wUrCx8WkPRkPMhInS*WT95Ad{O=AlZ^^l-$55l32NdcIt?aafAjxuHg5qV0gL zPi#V*>_Sy6^qptsPi=FzX3@(mL$P8>a*R>ds{M_+wbu&*NXjp0lE;aNQ^50^5)6O!=6=GVJ(aEPi+to|+{dV|KqBHfwbI1kf{%Up4AW zd3(Lp$>F#}30k&)6D~w)Z&J&W3SxTE{mxn@FYrVLbXyC7>~}2NFDk$*qNj5&7AN}9 zptv?wB(HJSCJNii*tt{)^j|M}5m}-cOqR+yMU=|kVB{XKx9~6ko53;#(`SUAp**xD zC!c!?o}$Mn`^AN|Ydguw447CeE8aNIa5kfKG^;Z=_EO*;AhH9##R(zwlAtJ0#-!S7 z7w|k#l0;T6jQ6FLZx=7TUZNbjoqcGZ)W_OVqx8k<4#2_sMyyc&7b8K>ppxU7y+fEw zpEHE7n>(RV3`<+emo|*l0be1PQXXL*`l*k7#gqX3jI)BhRX)QzldbuH_DqlL`O+Mi zDNnX{COFI5?-K9*)}A*twaMj1HoHmPx=q4?+MLmuJR2DghiN=tncB$TsjnYP=lnxt z$BG~rAsw8EPrRIF#rgN19~y!(-WT`ziG6mfnC_QZ8+Ss>-K)XLU`NA}7V^yYre%4^ zq~z`9Bk46Ty}HX`Amapo`+1MGG!G0Uom3c@KpznbZdde8UwH34sNdl74LUkL1*ZRa z(bAWvRy)!q9?PTQ<~zw}#8x=qszgORAo~QL4t@CIjg8qdzTP9vPT{qHpxoVuh(OqW zLJ(3Zhc@rQ60MziD1J#b0D4n#e$y1zojZ0AE4E^|CY=ygUDR8be%%L(NR*DqWZT0X z)el=mKyV!6Y89=ShMr5n*zFexr%6#h3U|w3GtiOKvh(XNoFXBLDQq~T%hjs~%?*uT zgWV^1bR@(S=C}b6mY^AfSMqNqeruM}Tqbm-Eu&BVQ1A4Yk?LtU@rP?mUzvR7=L$_n zH-bPp3xy{(AWTH_n?Vo)h2v37qgGBflfic4uzZB>*#H&1lFWrR{ARfcDE?s}Vr1&c z|Gp$jX21tKP;-CV7)IlZjt^0mmspl>ADDAq$rT@dN6ki5RZB+}xkx}dkCVGR(}mqO zSmFXRJ78;}p{NhPJanb$?e{Iu)8!BZb5WvL7fJYy+b=eo+aG5($?d*UggvfFzl*vr zP}P&&gUxVehX1~YPWfaG5($28x1WY8U~s||{UQC{wbZ0P(bBI&Kao*G0=)v}2Eb+( zrhZ+ty=i=4XqO|Rn%&f$Yv@CwoIz9d+CizXF3-W!T)rwg%s0nX>2D%4ZaTnfy+rXE z;=gB70^FAHYF6g-w~aZbnc&!xBZK$6SBm(+i-pTj^k0(&m%BA`Oh*+bW z@4Nz*R&#k54Vwy$#w|{cjhb@z_jT)HVS@cnemexv8LJWMx?jJz&Utsr?5-U@TfeWr z7J(hWx!}RY{1-k)tL;DG{h>(rvHkOQWpK1*0D;X-9;+WM#rfz@32SX6?LS09@Kyfk z0wjLu?-VfPT|gM=R(H4hXix9cH3(6QlkT#&BdS{+^fMc{A_rjm!mOJGAQw zzO3N(2yNxI;hW3zw?UYG#^Q&G3vUG2tY7)>B#tin!s2Dq`*fKHpGcON+rkf#Y;DF+ zys)ydaYqMOS&waRl-a#o4*W?=30!yPt>t|(d2 zl%Hh>{uL%*em?D?e~|rypwafSb7lVJpZq0&WOR?FPR4=35U$DKNMpYBtTCMPxtXtw z@i$L`T>zi;5n1{y!zp+D-becW7@}pR`-EU^CBJjH^3&!p3~@y#M>2m(If$}Zd=JVY z`(FS&cC+UD5<#HR3xw3Ot>8C70&=W7yfq{|WqX+#(@e>WWy&{lF6)FF{loJO5!ZbP z7&e>?eB{yY!#o52GKF>`84yxQL1yk#6k!ll;GN|1)p?rbQA$oUKwJ@Ozm&dy07T=7 zGqL`DjKH-bpHz}G=TGHRw*=*DSO}Za3#YLpoNRF+rT#t1%U*F4D)swQ1fkh zuZ7`+dW|$)ece_h=SJ~;Qfu!$(#=&-m{*sS$;Mu%pQZ;N3{}q>z-7M3Z)p1os7_YM zwkgywi0XP;;AN6eK$UBSD&5s_R(_C-=xSyop0Yx_GfD=zjxcTa!})G>qhIr5|{4Of(C0}Do^ z+TzR~W*c@JgWO+U>6BCHkAYTtl&Awv$ieTf<5fffYj+mZkfYPCuu-8hNb__XG@JK` zmhVr4@z)o$gB+W!b+d{(FeoXvi3k!)V^>6;*+@LZ(&H zKcz+!5+7Q_`>0u(V7tZg?+ntdTzpv;q&2{uX)|RX3HEiS1bRs&B+V>lz1_io5{qRI zt|<Fxge*aj2a%NG4zc@uM_iIl*L7@vkD-`~UDEbZ_0n*W!1+Y#{aWwN*Us$VjhCTP^x zX4pI$#&4$RH09TiMgWRu!F%)zf_oXKEZVdQ+Wd^}Vjx-ae$i<+(woFQ*K=Rt{16^a zL6v>PR|!_f;mAPvI|ogtQ#qN^8-(>Zxm~D6c8WeIJ)!8VM+vd{NSQfT82*+Jm!77_ zu2P0ml}Dcd;SPA_Dc=0zDyL?D>-(rd9x*1qCJBf8F2QFY9g|hU7=9-TvsfG)p8=UF zl|!>X0eJLt+s%Q3uoE9F!$DYM_^7V`xbL;R-)2^1WP$^*3UN`+U^ zEYEe$DHrfXJX{HdlzNc#l!|-oSJz>* z`1b+fmhfV&ffg`KvQWpSI!bY<%^bKDdk7+deJaxIO#C=Pa)?M^KR{8C!p9u(;KoN^ zqb%JVpDcqdx5VNI^Z9v}5Ol<^fnjZ5DXXa!+_~MYo|uFVfKXNWEZ~VTHZ6Yi zj4Xg>&|3@!RtCIUgJs)~RzN$2_1%m1<9AzC>$HIuuL0@nLxRD{;u=C-)5uTa!S6HZ z)wNxNSrIeeQdyZMvgmVUX_>#2KZxjAlVy7$GCEq!i$%^@@oHj z{<$=K0)oBJmVLi=Y`1@&emKhPcl!S6>+kyK3-OV{LMY89=a z;`MeMt^n;9$9TrJ>bXNP^|t;vugFUvz4I>tbIFoN0vc(VY+J%Tei?qW)eLv6n%kCe zu|K9jq3qeD=ljV>t#EG)8yQQL0`=b{+`V2XksRq7- z*ZR{oDqBwJAPUZS0J!+IqS5j!f_f!IquYEj^Ho+L#;t!Uo-5X5*7YMY57=;OIJ<&* zC|nT3xd&D;UIbLcwtOayAcAQaA|*QvQ38Ri#owhP>y@uo^LfJN#Phuvg9y%!oY%?k z;hmo;1Gh0ij<|u`$xW$=5$Ri+DC=*XG(?dtH)J{*BtiqiUaz>E`@xHiC->meHc#mX zfujypfWLRou0NaortuJz2_@GJ<$Z63Zz;W?Qm5=RO9?gvN^kq>;Pq&TA>(OpLsB`r z3!phOkMn7dgdy^+jWMP0lM(P`0g*CDlC9+EK4!O}!M;WJ7P6(@Yl#?MW~~Vab2vbF z?To!%era8>`yLOD<4-DilQJn9nDu+7e4P+N!0yv_y`9*qyMzr6)~b1S@bZ}kC`H(GgjB_E%1p)^+km&yIs8ooGtZu9#c`ztr^HI01Y$KR{dlW(Ze; zfFEFwy6KdbsL8v8Y2I+gX@g5V0`uvXcVSM<^%+5>J|_X$;G~EbDSzU>_yDk6$u+Mk zNQ}KR!FpL{QhMwZH}uw>9)-1C<$1weW-`A{Z!V}hr4&m2s+mXvrZ?rlXkILQ+)WNB zWC%(LKZx?ScL*ApRQ|@4jX-#}6e?wRy zOj9lH9TYUFWDsUal1;zWn;*$LoW7G{64n(BkZHgo*j@muoW@iX9yQ=#uL5S z>LfR2M{N@3h5&@F4{G*)f%E00z^$d-j?{+^5DCKBe%ptRx$evK-;XHviyJSUr?XQ3 z&CcQhA7st&$j~huRv=dB{G&C96ifS0@7k?pqV@ z$h_cw?~4^btep7)m4eBngNrs6aMl7#iBsY*ElFNix=qRFVccldix}G()YBWbAbS+9 z9;2oPHZNZv%SV@{8S{LRS)g6Yp1Z1t;45xNvu|^b)~fdxTW|VTIa9PV2N7~2(Rr^w zSzwTf6UFDG%`XMt`U(O3^I_HXZ^8X}_(^vv^{T6O`%CaL=Hb)#VA_+)%6FcGg;Tcd z`f?EXQD%qfq1w9vSE{BYZ81fsK9}Sy*Q+)N6y;iKB>Ylr(Q`MgI&1h{iIxLt!e$+k zX5-W+Pfzcf1&*x_VqO01msKb6gY)8O5Hw^&MA~Z5VbQO_{b-^Nk$cX)u`>6P^JkrO z%KACu9|FK#r1gm7k9n?IHWf%`6t8*k4c#NgA2Np#C8s=t!Fxkq@Fl$IM&7FF^cIrX z@r+Y|5@(<6)c7pxS`&Q(8bz)n7^3nwBvzm$%tZc z0T6GWT|Tv+^X!9XWs;$+@#)4Jt4|AQ0&3BZA3f~i7;^Gg-laHZ2UG@yH9KM4g>*pq z%wA!&stdVa76~)P9_03KdoSg2Fv)GM7M3Q)C)VHFQNAe_jGuC12bS>`m{uko2zF-< zn_?{Sh+o8hsLa9*-gm2iuABS3?vj1nr1(PSl`kuJX`D;+Ut}jfoq$Q~qyTOX)m?__ z(U*DHz(QbKpz14CQ?^d`Y%7!5c*5WMV`q+HD|Zy5a_8L)t0#mhwK)Q=FY@Kf3^AfM zU}M9zsLRCk--4!Y=z4{qf3omk^r}h6cxR$2B3c2yWYfWV?!9#}a0@HhxX>*%30_TJ zI52m~E5de3o>aX0JBAlF(x)X7HmrKJh(Jv@{s~QyE)`P#IzToSN@zywmfJAvaQSin zHoN_Sv5&6u4#nt6rrpOUe_aCG==qTgOirIXFlPDjx7;DHW%yN%?Lid!XLo%bQ7UrM z$tPJ@F}+1Rfv)8P@uNvu2afw~+q;3(uN7YjH#2_*0MxdH6dI1k(m8SC0;NOV+-CLT z{GjDxHM?^YU*2se^kI%}sabF_?X4xUajiXvR&E-ogp)sGx zVV+|6^+GaOx>TDC!^mhqyz*CKz&vY0ThF3o?Ok{95qo_PD-v!6Qw%lpi zwZ&(bn^9n1Yp&e_hAigamUF|!_s|LRHH%+T{1(Mo`fL{q3yC6Hg667u{RtBS-Rfjz z5+z>1HATRLMs_P07@*#ri`xcts%yGwiC^|T^Zt?@h8JA6ea?1o`RG2$QSX}GT`u8k zou8v(zXo3mM$iCekHbiVwVZAUh+t9bn`eoiB*v|%Uu{Q$`UM{HmSPpfAq58`&^z$6 z%AF3({!&;6yqDv3*mDZgGXbrQ*zd4w!)mElB#ga>PeSl^jriX_9*fNbAurc*3N2mj zrH6SO;{eEz-U|kCKi6}tEd+$Lz60_88ps+_lgiLTqE6+L?E6@3($7;f{^ut{@BAQX z5`Qy5iY71>%dV=Y(nRep1R)KbX>V`^b(O`kQ#t7GFLmKqXb!(mHTfN={pW(_J7*<9 z2ACfwIqMKPpz2OL-?3?bvS5y^4@m3xxdxa7vo40|O@VuwU`Nl0Jo4dn3~>-W@&lag z$=bPQfTqp5`jdC-ljftcF5*uD<*&#FSA2}M!n1n(Z3j&q>Y|};DxNG2X$NoMnkMF# zBTT_`(Ga!uS5%ibj$3Vh(;Iz+?fJdKlQPP7iy@Gga~6hPcdkyPoBb&53*galW-HxI z5dUP1X&}07tq__zkl+9h4GxQ5Z$gdy;1o7?U07}D7HjS|@Du?a+5n*JMp0NRL`iWc z;2a;{P$+?H;UBw;ndCsLFIUkx#ff<;lavHgz*4^bgCSIvFi8n;*d{-oZqSi5;I*I2 zSTQp?_4UhEL7(Gs$#qfI-3he3IA_78#lKSCfK4W z(u>1tA8>|6tPg&HfaK!468WP#z*dpPAN=4EK)Dh%cKBi}?~T$Wq+aDq@%WB=nbWBB z9IL)8lwnzv6mGr2ji`CQEe^?3s$e5YUjgg&byumC*zY>^i=c!<9@HZDX)TY8XtyO& zUOD1adD%i@IDvjgJv6Ta8cptt8;5tCzD6R0OvxGR_z5 zN+FDRL0I;KeEZGI$n+Uc8qyY72gp?C7IQmZ#oI{y4Bv84aPVer95zEkX=H^nlKmj7 zg+mQeh-R-w*2wmNy`!a=2JyDi?_7QmMEp;oH>d; zFa^vc7eU8;yIn8XV}1LHeP?uR&THgIQgiwNJea6e3oke z(q>AAPI@G_R|4UQY>DQ;+|R11RQ@SbsL+u4p4Gp7a9@*pbzgt~X_C2^cVLk%34-== zt>}l+1!4t(5h9A>#bc0`mapBv_)0-HNvH$~T=hWmNmWY1z4>Wkmxd@*Y9{UQ{07&j-3I4ZB^@rbimEMl-f`%9&(EOts^k~AMT)u<~xo65>4px!9V6)8ZhpS#& zdL7^@yv+Vyjax6-xStv}#({_Or)bG40%BWy%RPZj6K)~6^Q%uusjI5LY@5DW9;XT* zM8k0iT1Jk+Q%Mt4uyBIG>M~Fd%cldjOnQn2n?{Q8@<~CZSvMqnW3O*1^8G;V08LtQ zGQM6mx+d9??Ktf5l+g!?&NZYXR7B3B++%U$XHAV@c~5!RcpU1*iDUG?Fax^m_=f6? zn;em>ZA1^;OR}?#Wkn_E@o>%jE72`+zrHrxwqx=8?m@u+Eg_^QSKU^)<8nA;M3haAzuTi2`a|eD@3yXN*+K+!Eg3Dhg zwZnnKagB$sr^M8qdsWdz4SIjOyqN92f@xi_sq8l{m8TARJZforpAZ6nsO&|Pdi|`%TrOiZ?JexPf~+!!s!RVIhoqaY7UR6tp@Ey8z8*J>pr5mdfur#|Eiu zzM+4yWncXo99e{3f-M7C`%_ES1UXoq!oIfMZ#gsF1Gy$!JVwFZv6%}>XT-!in10~N zAV0A=rCr&69Rf9PpsbuH)xAh+Hm<(v63>+;tSm&^*bPIyLraT2d7HAN-%A751Jq^w z^eyo`WV^_VB7o;!t(fUvG8x~R9-dEzoCmj|?TTKSIs)8&T9MIFzh>B9CbdkjD9 zD-z)h11EWi@RkjffR{fzeeLwHi5QgHItk9bHN6+Rmy^xU?@mv|Gfb(yTL(0t-B*U5 z*$iZMJ*R@39ccHRcXJZ#U8c?V%SZ7V{*e8>5IiAZ4EU9!-{rwrAIr^$zFaT*(c9|_ zL~&Gbgtn-l{Uz3(Dvz+I;uukd;Q_*>`Us&yWyzP0v@iT^j4;(dqS;^H{0lN;;sZWi zhOLQ-)GrT;@3EWB3!!I^!eSmp=F91D_6~=RQ^Llc5|fiW-CNr?E$8atnnDIBuc6e8 zsb!sAyjx|~gob?Cp)(1BW7+trr#dTP{?9U;@WCG6l!pg>lWYy?PLlO6knpT9Gw1+* zi)vNH$~(1&K$$@N>Bfv~QDA>tUh~d`(8lH&K6TQBZW<(&3Vtfh3ji{-*0TGS;)+sK zeoSrkO56mO5gOqk=8>0C(cFz}DOy9?aLlw>I^=WaRc#cNDq z%dgqSv(R1naT#}Wge>0Q7;@)?Vf3Cax^m)d*UL^#WL35wG-`C5z`=^<5GJZmhybdf!MB0|C!J`^fPmO&^n$!r)R!@?&x zD2nNrBF38DN-5sHtA3~o{8bsiYWL?l2z=xEAGd=a%Jhs%6a4vexGDaIAD!!*gAhtj zfK2{vv^^WT!^l28uGSM31F5Do{ws(?2L8>I#B?L&?t}40GRZe*{&NXTf?=&`9q%Oh z&VD=Pg(Nv}N9IHjG{up4Gc?`d1j|57J5@{~5zt6EyA1d|PQ_ zgZUK%UwXRy^HA3}pax$lhb^^MsJ~T2uELojubYqh401`l9jZ(T5h9FFo$aMKvU!Ms z=`?_#Z;o2Q=fF-J* zX3>Xq`$GW@HcsGFghxnu^V_$!-j)tK>%U9t1k@=aG!2vMb_;G5_df>ngs#~K8;_gQl>_>LV$1EAne z&6Wq8Y1CdD;5hnYZYiB=<<_B3IQwn?dJQ-&nhNsK8$x|5PE!qg>uYwTN&?WKLmIX! z(+s=&w2uv^>(aKpH6tya`JnKk6CjXvfuWqLguajmFh5S2cF$M|(Ok_JIH zn&s6pECcqp^7sJlw4U#Ev6wjhdXtd**-LPOzGg|6Rf7!IJM8mo2i`aI8=w^0cnEoG z_t(Y($mR`-eNm{49Vk6ja!3C?ppD7>pcq1_o`FaLsf|w3w|xLZ`71lsO#K{h+rlFA zb)cJH6bNnPPdXoc@?$D9N1Eo}+SQwapB2NbB`+)W$tU6t^eC9t`|o~LEfkgzzilH{ z+V|&<4+J@D1MMI=*%`TxC`U&>5bKm}mZA(itxd8IIgl!Cw!|M@&PoDt1vDs#{><<}3E`S&PmGL2WKp%d&$( zR$B?e5t?973{LwUNAgy>M}Ios=I-2nh~p&y%q0uafjMz^s@vy1X20dbn@@Nolq(to zPJ2SU)4p*yc+}#HGV~o!>q$F-GXzz@?SOxEy&^V>rw)n^^MfZOL9MyH%d9n_5N{?? zrvtV)ShwTBiB+`?#FFXZ*~05VJ)hLZ_1hx?L9kW9ChP0yGHz@s50Y)rLFDh>EVpcX z9+1qma-U2C>}y%$qYDDwop8K)Ao_J2o8Rfwh1L-w82IL#8=O*`Knnp1PMK-y(hh=#ic_ zkn4?oYHrNo^F1E*F9hg`$@bBmZ-^xwZqGV!KIW1=u)mk_hOF%6iJ_UMer%WKziMG? zhRa+5893epF~$khvMk7TH5-y*yQ%9ooT7MH9to%Sx!=DhC$Rcr%!Wf-6$+mU!Jm>> za<2AOiZ&P!AY@u+;{}=-x$rf${9#*)PuQx*^|t6-kRv!^P0dI9$iDR<>Y}G1yS1ZR zHIoz;GeGCz(_Zy*=YA10`N^jiQGA@HS&)=pGWeP%VtC!CXk@oS_P&CRC>rBb#ITj2 zMzDdOlhY3aL+J6M8n;s^cXgaIY^WYF$JV+cCs~Eziyl9Zn25kGFV}sOG*|OoJM{(w zzzDhPfu?`SPl5Wbr{Fm|?_%ING+iC0(%(jEqT0G-MwEScCPt?0m9`Atw6mgoW59?J zWv~QtKuI*fZ~MKlP-3G$&@^9cAd@ym;`A5P4+z2+tb#y$S|Uik!RiA>k3s^~*v-@~ zZ-ML+FF{-58u`gH>?ViP3a|egfTBz3J7n=C_^7DsIPm@Q#GFUA`w7CvSJe7LvlNefSz0(r@*A%$u7x=O9+nFmY?)x!K=}Fr zzccY)HH{-o$zN$0<1QCr&+6q%Tyha*#Rv|XJkgUMu2{GS;KNJ>np1bT+&Z)?*u7`| zI5zZxL2KdUW%BMzT#0PDKX@94${y%(6m*jb_TVl&3NUu(&|6c8joMbrzz5Z z>0dazY@wD1qVR>^>=Xu(G!=hO{GHF#`v5R)c@UVTKo+*r#u@bLKHgBA(eLG@Hj?SU zc!k6CJV?VK!M&HQmMbJ}a@(0PjV=M8lyGUoj0v1(V&LjJz1}e|bCN~cgTvH)2JCDD zhI$cT!Dk;hkrNabiE}DI7YSVwSoXqJGR&io`|GYyvkkn0Ae!66M5YyC39}rcb&+l_MYIYq+pOFBz zjQ>6w0IdQukfr-wL9dEa=|MV87sgMg5?qE#xq;&ApPEPB*U;}1b(Vt6juNUMsli{aA@jdk(XZLkKU{JkL*FOBxAUL!obx1k z4f`+6*6|zt$iV+mvdba$LY3d|wM;Wj8EWEuv>aXr+DKXNwT_3^j{y8|F6iJkd%KB7 z4*ceQ3Z{l4?_-_mutUXU$c?fZiXcl&pCmbh_O8)ah^jNfxt~DKSyb@%rc|^HEm>mX z$NxPAVu`67l3fPfCG@`1-)plfhs9NY>rT!{UtXKr;p#Si2JZ^0qhvxyp&_e8Cmm7MM}@j*&RN}g2S>}L|(B= z7_!$pvff3}bTgpS?$4ir>eDRlYxL4o<;zvy|F-3Lrv}=t`sN^^U3KsVr!=}_ z*%*<=2touy%twb%&JP-71OSn#q#^wJqM)5)8VTVIS|cm3wM^l!#ez{3v`gFX1Ba}0 z8d?*-BxuGUO927h$`LKNdxPx5;hX&jcjpnJpN@z98_O%6O8KU_CC%}p0&2-en~#m?b5p1?y3^-b8$+L`I%n` zNonzb7r0~^ORl8@`SIL1bcjLF@L8-5?H!GosaoUj34UdgiyW8M0Lycl*Z+#%CD~&Y zFUiNP;FO~%oZK<%JMZl~Dch7H+YX6Mko4}aaRP@dDUa(|w!kc+*I0l00^Su(eoZFb zvxb$v*qBCTyx5UA8*r$EBi?|TpGfL|`}tB|fsf^(EFBGqORntSDa`Vpn_E^6u4CKT zV>KO_T$I_-sss!6oUf|)NLRV_>_;`Y9A)|Rs1_g+V31un?`}Prros;%(SX6%U%IOBs?$t^je4W zW}C}>X4ttq+{P#4Y5hpHGSW$#hX5iFpWs(WZ56-!(E)2`OoUQQ<3VUjZbccrC<2iw zzU!M~AYZ_uuaiTGKiCPTgUbZG5yIZ_rD3GklGF)Ld}ZoGVvJZAcINxG?0=3X@p#2K zRyFq7nOFIej6whDt6KA5AQ3Cc>)vha8l~0t{Bk6vx{&H&}wesTZ zO?1y~@V#YwVjrE^ueK+7(+`4!A3th@a1C;37i2rv-MwND`Q0$fY9GQ4e#l7#3tuB% zM7b;@Tq009leMsUS&p8d#KmDJvMDywk8{h8(ReNqNbYah0hX&Io|rM#VtG3r8`*rO ze>*s!)TP+@T_o$>oZnCST=R~eVgw!3krV}ET}K}eJG-0|-Y;1x9ryllHqT+D+K_GF z0ML86S>}Pml_%FU@om>ly$(Dcle>?Wm$WY60#V_Qm=I`Q+PB~7jf+(C3H*Ev@eRys zb{5T zbUlugbti4G90QIX7L}rkG z)9D{Kx{R3r*=z5MGM|iewTvL##fn%N#rRsqhqO9t*0s;S8SJyvE7A0yvsC=HV zDWPD=&aqmMS_C8U%tEZ>1R7e({EyWry-qZKsW7y}fLV$hLXp_TQ7NSEHNMwd^EiV< zI=E5Q#qvaz_=PAL5+B3894~UiqUeU0*F0habOQ`uVHe7OY1+Q;?Qv|Rn0q5PHXI%|g|LPs%qQGZ6)0~I(Y1H>VyNHW%CiQ5!q5ty;D#Hpm4SB6Y+r1Ze=~}LToE*= zC^!Od5Yd-%zVR+p@tF(6cyb`w1)?@?0RM#6`SI!fz2dCkB&_6tkI+_rYReWGS^~&` z+qRL02O|u-`A;ySkl%wQEm))4_5(`vBsP6|-y@#|h<(VRomLzjKXE|89V|Y$jn%U% zu6d8KQJwXtLkyburA(0jb*UUc%`V>JrY=2i5_3wGS-kWt^Tq#M^>@&g+P{q^WDS{9 zO=&(#*9>KGP+lg3IDWBGz>jJ@8k#EX=4zE~Q{CsZ+vMZfRE_I{AFU_5ofFV@84sPz zzTk!(C#!qa<@~j>Qj6v}YoggHMAS^42<)shXR16S<|Cj~Z83<+I zd*2Bu6lL@WNcWH3x3{D|IY>>5s79gp_&60^z}h#;qYr7~+m3!2aM{I_Q9!Vu2+%-f zAr4ju8h~1ADE~Rwi?N_wCHuBekqrg@TfT}rlw87}|%?U#^1b2R}nPF*AetX>Gj#?^q$;2sptqgKk?tC3e5wpHbq7=-S0#U2- z#H&h(u(&hA9z*+{hVGLHT*V~{a}LLaI%>vq1Lk)Vu1|95->ggFLfdeeVFGA=|M8 z5Dg()BPy_-UH;r(Q+?lbZXEccF~!9#3sOX4!W}w%xAp}6y6 zSZ*D<1|=V7Vxwj7DZQ28^2tCl>vUj%C8I_h3r7$ZfZAV%pkFSd!*PPT=qjtn*47^* zFWP``=EeNXJ{JV;eK{=3Z^!(|+^edysM4{PV%*~Vf?}~kjAx-cCBNW@t1kPOC9{)2XUPG z!s**)dcTK12$yYK2ETc*iLV8^MS%1z|H3HYm53W0?zH20ZUl{%!xGg;PhYj(QWs(= zu+FYC>V-}F9S?a@8X5Wc-RA_fijNPdb$PA4qAJz%seExicjN4W)pU49sv2@SkJ9na zoINE;70D{T!-SbAJ^uv>Ah}{`Uo-lt#KYd6yuw%cQ#V`2oK z>#p9RJ+bQDZ-`7yD~;oX=zJSw~!w{0|r$%3y!8BLhOHO9=$3F1NmUpqZGt}_)^O(FU2e;T+ z>!JaFJR(E--pC~0@P|^1P!)|QyOL_zk5&#FTUDc&izN?q#buF1WBL5BQmJ zh-a~3rdw3J{x&P*^uHx`r^D_r&7-dto_^A=tOg|SN7(&*KhG=pB1I^={mN88|Hc~i zQEK9EVS`()Ucr}@w%SYXwC<%8v^K@`^!XP%=vLu;{YKQ&2m*;StSime?e zu*vsbONiY)8sNbOFaNCRF?Iyr?WNu{s|}43G54*~zM!+lE?^JQ&^_?mIOI>gSO|N; z__HGxYE2l!mKesCE{& zG>4x3Duu63tdXi8PHuSYgmu@<#mq8M-?avFN4rr1XqX#THbz2*oIv)Be>9vXOR{zG z^l9$jTUIQ^sEFeUiSn0**g@VrBznCfVTCGUT7b~lO)Fq=(VEhOVC99?TA@4d_TGM5 zBxR%^3@}OX3gi+cWy((H>MP8T3$MgP`>3*`c_Rd_u_kjk+SFY(B2KbqsS_>{$t(Pa zVbGdw;#w6w;heg&!8E@DSEY$++O7fspI`5?B*7=DeU8zMw#dTIuaL#r*M`-TVvh-- z=|q*i>)O`47ko)Z%pZ?=%qY;J3j97<40BPRYh#6oIwdT>FM>Y*nZ{aZ3TduYu`H|e z%5vR_8ob@YjnjOdPP%v`{aFC4^9(PPl^Sw_Ku!)H^hcPO@<@K#z1|c{emk^?e>i0p zaOaUtEN!-4H>ntUk#tT0KC!(F{0Gf{K=p!Pa4x~m{(^If5Fi0Gkla=tR)?Ss%ajpX z{T=1<$=-!6hx7MP06TSV0-Yllv2EOf-3eRy61G- z{TBz?1k`i@XtdErUgb{>Jw@W+Uq{Dk<5L35%3rM#O0UY84S)P^m0MVIdx zvhilG01bkueQmasm-$CnZn1LXv&7>2I~z|18fDG+u)|?j)19dVTTV9X9V|#G$0f;P z7Xs4q-ONVaZ$%idTr^LDt;Sq0u27 z(Tv%lc*`yC!OPTq87-WaY!N=ykyI!Gh`6^nuK4oCzrAbwBB%7 zy7dw>99EITpZpz%n=wCssp=7*N*zDJGAp?FEj9R7CDyGyJ>&~@Ql)d_e1KuEOHVRd z&=%Wi4^jM0lB6(v6C22>Dow>Zw#99($CAR`#+$9UFr|8J^vY%-XFwnTi_7_9_=Fb7 zCrgZtDF%Wp)*6Z6mvpXEXY2Nv=Z)jLW{<|n8 zRJFpT@ap?GPrfCQO7j)cZ*~2!0v5Iw=A2sM(6QUjymJF@HQu|$?f2eZVtA;EA1aW} zx$Lr0-b0Cb?pZtEg9@pQ{K5N>F!%N~*nV{rusl{9kYLoL>K`e9)-?vwIF#$}+a{G0 zIIKr_MB5yln1D{*wosC4e$Yx&R*Z{V9fD`}cUadAnGqYw|Bb ze+cSOyYjT}@PX1JpJZQCWu#0cP2AR}?pGZc3H7$U>C=)Xbo4X!el@x=8KEYtK9_of z9a3@R9_jpPDT{s7$#jf<@7;eZ7b< zmzpv3(YUM0@Azim`@qu{ZT?AdWqzzJwo^Q`Ld9@5IPwmjH&` zvkUtm5WT@=&�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

dJ*4fHol^1XN#?bpsz~gUqtlz7 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 z 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

-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 Date: Tue, 2 Apr 2024 15:10:39 -0400 Subject: [PATCH 569/586] [6.14.z] Add case for special chars in HTTP proxy password (#14571) --- pytest_fixtures/component/http_proxy.py | 7 ++ robottelo/host_helpers/api_factory.py | 2 +- robottelo/hosts.py | 43 ++++++++++ tests/foreman/ui/test_http_proxy.py | 100 +++++++++++++++++++----- 4 files changed, 133 insertions(+), 19 deletions(-) diff --git a/pytest_fixtures/component/http_proxy.py b/pytest_fixtures/component/http_proxy.py index 8c6095092dd..a98e7409699 100644 --- a/pytest_fixtures/component/http_proxy.py +++ b/pytest_fixtures/component/http_proxy.py @@ -1,6 +1,13 @@ import pytest from robottelo.config import settings +from robottelo.hosts import ProxyHost + + +@pytest.fixture(scope='session') +def session_auth_proxy(session_target_sat): + """Instantiates authenticated HTTP proxy as a session-scoped fixture""" + return ProxyHost(settings.http_proxy.auth_proxy_url) @pytest.fixture diff --git a/robottelo/host_helpers/api_factory.py b/robottelo/host_helpers/api_factory.py index bae286dc8ac..1316bca9078 100644 --- a/robottelo/host_helpers/api_factory.py +++ b/robottelo/host_helpers/api_factory.py @@ -145,7 +145,7 @@ def enable_sync_redhat_repo(self, rh_repo, org_id, timeout=1500): """Enable the RedHat repo, sync it and returns repo_id""" # Enable RH repo and fetch repository_id repo_id = self.enable_rhrepo_and_fetchid( - basearch=rh_repo['basearch'], + basearch=rh_repo.get('basearch', rh_repo.get('arch', DEFAULT_ARCHITECTURE)), org_id=org_id, product=rh_repo['product'], repo=rh_repo['name'], diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 12cc3dfd0e9..b51eecc15b4 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -169,6 +169,10 @@ class IPAHostError(Exception): pass +class ProxyHostError(Exception): + pass + + class ContentHost(Host, ContentHostMixins): run = Host.execute default_timeout = settings.server.ssh_client.command_timeout @@ -2558,3 +2562,42 @@ def remove_user_from_usergroup(self, member_username, member_group): ) if result.status != 0: raise IPAHostError('Failed to remove the user from user group') + + +class ProxyHost(Host): + """Class representing HTTP Proxy host""" + + def __init__(self, url, **kwargs): + self._conf_dir = '/etc/squid/' + self._access_log = '/var/log/squid/access.log' + kwargs['hostname'] = urlparse(url).hostname + super().__init__(**kwargs) + + def add_user(self, name, passwd): + """Adds new user to the HTTP Proxy""" + res = self.execute(f"htpasswd -b {self._conf_dir}passwd {name} '{passwd}'") + assert res.status == 0, f'User addition failed on the proxy side: {res.stderr}' + return res + + def remove_user(self, name): + """Removes a user from HTTP Proxy""" + res = self.execute(f'htpasswd -D {self._conf_dir}passwd {name}') + assert res.status == 0, f'User deletion failed on the proxy side: {res.stderr}' + return res + + def get_log(self, which=None, tail=None, grep=None): + """Returns log content from the HTTP Proxy instance + + :param which: Which log file should be read. Defaults to access.log. + :param tail: Use when only the tail of a long log file is needed. + :param grep: Grep for some expression. + :return: Log content found or None + """ + log_file = which or self._access_log + cmd = f'tail -n {tail} {log_file}' if tail else f'cat {log_file}' + if grep: + cmd = f'{cmd} | grep "{grep}"' + res = self.execute(cmd) + if res.status != 0: + raise ProxyHostError(f'Proxy log read failed: {res.stderr}') + return None if res.stdout == '' else res.stdout diff --git a/tests/foreman/ui/test_http_proxy.py b/tests/foreman/ui/test_http_proxy.py index 9e3f1e5b10b..a247058f8b6 100644 --- a/tests/foreman/ui/test_http_proxy.py +++ b/tests/foreman/ui/test_http_proxy.py @@ -11,11 +11,23 @@ :CaseAutomation: Automated """ +from box import Box from fauxfactory import gen_integer, gen_string, gen_url import pytest from robottelo.config import settings -from robottelo.constants import DOCKER_REPO_UPSTREAM_NAME, REPO_TYPE +from robottelo.constants import DOCKER_REPO_UPSTREAM_NAME, REPO_TYPE, REPOS +from robottelo.hosts import ProxyHostError + + +@pytest.fixture +def function_spec_char_user(target_sat, session_auth_proxy): + """Creates a user with special character password on the auth HTTP proxy""" + name = gen_string('alpha').lower() # lower! + passwd = gen_string('punctuation').replace("'", '') + session_auth_proxy.add_user(name, passwd) + yield Box(name=name, passwd=passwd) + session_auth_proxy.remove_user(name) @pytest.mark.tier2 @@ -295,35 +307,87 @@ def test_check_http_proxy_value_repository_details( @pytest.mark.tier3 @pytest.mark.run_in_one_thread -@pytest.mark.stubbed -def test_http_proxy_containing_special_characters(): +def test_http_proxy_containing_special_characters( + request, + target_sat, + session_auth_proxy, + function_spec_char_user, + module_sca_manifest_org, + default_location, +): """Test Manifest refresh and redhat repository sync with http proxy special characters in password. :id: 16082c6a-9320-4a9a-bd6c-5687b099c940 - :customerscenario: true + :setup: + 1. Have an authenticated HTTP proxy. + 2. At the Proxy side create a user with special characters in password + (via function_spec_user fixture), let's call him the spec-char user. :steps: - 1. Navigate to Infrastructure > Http Proxies - 2. Create HTTP Proxy with special characters in password. - 3. Go To to Administer > Settings > content tab - 4. Fill the details related to HTTP Proxy and click on "Test connection" button. - 5. Update the "Default HTTP Proxy" with created above. - 6. Refresh manifest. - 7. Enable and sync any redhat repositories. - - :BZ: 1844840 + 1. Check that no logs exist for the spec-char user at the proxy side yet. + 2. Create a proxy via UI using the spec-char user. + 3. Update settings to use the proxy for the content ops. + 4. Refresh the manifest, check it went through the proxy. + 5. Enable and sync some RH repository, check it went through the proxy. :expectedresults: - 1. "Test connection" button workes as expected. - 2. Manifest refresh, repository enable/disable and repository sync operation - finished successfully. + 1. HTTP proxy can be created via UI using the spec-char user. + 2. Manifest refresh, repository enable and sync succeed and are performed + through the HTTP proxy. - :CaseAutomation: NotAutomated + :BZ: 1844840 - :CaseImportance: High + :customerscenario: true """ + # Check that no logs exist for the spec-char user at the proxy side yet. + with pytest.raises(ProxyHostError): + session_auth_proxy.get_log(tail=100, grep=function_spec_char_user.name) + + # Create a proxy via UI using the spec-char user. + proxy_name = gen_string('alpha') + with target_sat.ui_session() as session: + session.organization.select(org_name=module_sca_manifest_org.name) + session.http_proxy.create( + { + 'http_proxy.name': proxy_name, + 'http_proxy.url': settings.http_proxy.auth_proxy_url, + 'http_proxy.username': function_spec_char_user.name, + 'http_proxy.password': function_spec_char_user.passwd, + 'locations.resources.assigned': [default_location.name], + 'organizations.resources.assigned': [module_sca_manifest_org.name], + } + ) + request.addfinalizer( + lambda: target_sat.api.HTTPProxy() + .search(query={'search': f'name={proxy_name}'})[0] + .delete() + ) + + # Update settings to use the proxy for the content ops. + session.settings.update( + 'name = content_default_http_proxy', + f'{proxy_name} ({settings.http_proxy.auth_proxy_url})', + ) + + # Refresh the manifest, check it went through the proxy. + target_sat.cli.Subscription.refresh_manifest( + {'organization-id': module_sca_manifest_org.id} + ) + assert session_auth_proxy.get_log( + tail=100, grep=f'CONNECT subscription.rhsm.redhat.com.*{function_spec_char_user.name}' + ), 'RHSM connection not found in proxy log' + + # Enable and sync some RH repository, check it went through the proxy. + repo_id = target_sat.api_factory.enable_sync_redhat_repo( + REPOS['rhae2'], module_sca_manifest_org.id + ) + repo = target_sat.api.Repository(id=repo_id).read() + assert session_auth_proxy.get_log( + tail=100, grep=f'CONNECT cdn.redhat.com.*{function_spec_char_user.name}' + ), 'CDN connection not found in proxy log' + assert repo.content_counts['rpm'] > 0, 'Where is my content?!' @pytest.mark.tier2 From ea52519aa6e8590814638d23b1744e9b96da812a Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 2 Apr 2024 23:55:15 -0400 Subject: [PATCH 570/586] [6.14.z] Bump pytest-order from 1.2.0 to 1.2.1 (#14595) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e6a79f733cd..abe79282b19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ productmd==1.38 pyotp==2.9.0 python-box==7.1.1 pytest==8.1.1 -pytest-order==1.2.0 +pytest-order==1.2.1 pytest-services==2.2.1 pytest-mock==3.14.0 pytest-reportportal==5.4.1 From 4030d3456651058c86506816a8196cafb5c270df Mon Sep 17 00:00:00 2001 From: Jitendra Yejare Date: Wed, 3 Apr 2024 13:58:10 +0530 Subject: [PATCH 571/586] [6.14.z] zStream Checks for Python 3.12 (#14558) (#14601) zStream Checks for Python 3.12 (#14558) Add Python 3.12 to supported versions (#12793) * Add Python 3.12 to supported versions Python 3.12 is out and should be supported by Robottelo. Also switched the weekly workflow to use Python 3.12 instead of 3.11. * update unittest deprecations * Adjust unit tests for python 3.12 This commit introduces a few changes relating to unittest changes and differences post-removal of old behavior. Co-authored-by: Jake Callahan --- .github/workflows/pull_request.yml | 2 +- .github/workflows/weekly.yml | 2 +- robottelo/cli/base.py | 8 +- robottelo/cli/subscription.py | 4 +- tests/robottelo/test_cli.py | 134 ++++++++++++++++------- tests/robottelo/test_cli_method_calls.py | 18 +-- 6 files changed, 114 insertions(+), 54 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 47963adca8a..3580cf23a3f 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.10', '3.11'] + python-version: ['3.10', '3.11', '3.12'] steps: - name: Checkout Robottelo uses: actions/checkout@v4 diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index e767f188ab5..a08485c8f49 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.9] + python-version: [3.12] steps: - name: Checkout Robottelo uses: actions/checkout@v4 diff --git a/robottelo/cli/base.py b/robottelo/cli/base.py index 2a3ad35ff4f..8d258500b27 100644 --- a/robottelo/cli/base.py +++ b/robottelo/cli/base.py @@ -132,24 +132,24 @@ def delete(cls, options=None, timeout=None): return cls.execute(cls._construct_command(options), ignore_stderr=True, timeout=timeout) @classmethod - def delete_parameter(cls, options=None): + def delete_parameter(cls, options=None, timeout=None): """ Deletes parameter from record. """ cls.command_sub = 'delete-parameter' - return cls.execute(cls._construct_command(options)) + return cls.execute(cls._construct_command(options), ignore_stderr=False, timeout=timeout) @classmethod - def dump(cls, options=None): + def dump(cls, options=None, timeout=None): """ Displays the content for existing partition table. """ cls.command_sub = 'dump' - return cls.execute(cls._construct_command(options)) + return cls.execute(cls._construct_command(options), ignore_stderr=False, timeout=timeout) @classmethod def _get_username_password(cls, username=None, password=None): diff --git a/robottelo/cli/subscription.py b/robottelo/cli/subscription.py index 86b57d51d41..0e22eaeaccd 100644 --- a/robottelo/cli/subscription.py +++ b/robottelo/cli/subscription.py @@ -47,7 +47,7 @@ def refresh_manifest(cls, options=None, timeout=None): return cls.execute(cls._construct_command(options), ignore_stderr=True, timeout=timeout) @classmethod - def manifest_history(cls, options=None): + def manifest_history(cls, options=None, timeout=None): """Provided history for subscription manifest""" cls.command_sub = 'manifest-history' - return cls.execute(cls._construct_command(options)) + return cls.execute(cls._construct_command(options), ignore_stderr=True, timeout=timeout) diff --git a/tests/robottelo/test_cli.py b/tests/robottelo/test_cli.py index debbc8c6e42..a2568f5a584 100644 --- a/tests/robottelo/test_cli.py +++ b/tests/robottelo/test_cli.py @@ -149,8 +149,8 @@ def test_add_operating_system(self, construct, execute): assert Base.command_sub != 'add-operatingsystem' assert execute.return_value == Base.add_operating_system(options) assert Base.command_sub == 'add-operatingsystem' - construct.called_once_with(options) - execute.called_once_with(construct.return_value) + construct.assert_called_once_with(options) + execute.assert_called_once_with(construct.return_value) @mock.patch('robottelo.cli.base.Base.execute') @mock.patch('robottelo.cli.base.Base._construct_command') @@ -159,8 +159,8 @@ def test_add_create_with_empty_result(self, construct, execute): execute.return_value = [] assert execute.return_value == Base.create() assert Base.command_sub == 'create' - construct.called_once_with({}) - execute.called_once_with(construct.return_value, output_format='csv') + construct.assert_called_once_with({}) + execute.assert_called_once_with(construct.return_value, output_format='csv', timeout=None) @mock.patch('robottelo.cli.base.Base.info') @mock.patch('robottelo.cli.base.Base.execute') @@ -170,8 +170,8 @@ def test_add_create_with_result_dct_without_id(self, construct, execute, info): execute.return_value = [{'not_id': 'foo'}] assert execute.return_value == Base.create() assert Base.command_sub == 'create' - construct.called_once_with({}) - execute.called_once_with(construct.return_value, output_format='csv') + construct.assert_called_once_with({}) + execute.assert_called_once_with(construct.return_value, output_format='csv', timeout=None) assert not info.called @mock.patch('robottelo.cli.base.Base.info') @@ -185,9 +185,9 @@ def test_add_create_with_result_dct_with_id_not_required_org(self, construct, ex Base.command_requires_org = False assert execute.return_value == Base.create() assert Base.command_sub == 'create' - construct.called_once_with({}) - execute.called_once_with(construct.return_value, output_format='csv') - info.called_once_with({'id': 'foo'}) + construct.assert_called_once_with({}) + execute.assert_called_once_with(construct.return_value, output_format='csv', timeout=None) + info.assert_called_once_with({'id': 'foo'}) @mock.patch('robottelo.cli.base.Base.info') @mock.patch('robottelo.cli.base.Base.execute') @@ -200,9 +200,9 @@ def test_add_create_with_result_dct_with_id_required_org(self, construct, execut Base.command_requires_org = True assert execute.return_value == Base.create({'organization-id': 'org-id'}) assert Base.command_sub == 'create' - construct.called_once_with({}) - execute.called_once_with(construct.return_value, output_format='csv') - info.called_once_with({'id': 'foo', 'organization-id': 'org-id'}) + construct.assert_called_once_with({'organization-id': 'org-id'}) + execute.assert_called_once_with(construct.return_value, output_format='csv', timeout=None) + info.assert_called_once_with({'id': 'foo', 'organization-id': 'org-id'}) @mock.patch('robottelo.cli.base.Base.execute') @mock.patch('robottelo.cli.base.Base._construct_command') @@ -215,8 +215,8 @@ def test_add_create_with_result_dct_id_required_org_error(self, construct, execu with pytest.raises(CLIError): Base.create() assert Base.command_sub == 'create' - construct.called_once_with({}) - execute.called_once_with(construct.return_value, output_format='csv') + construct.assert_called_once_with({}) + execute.assert_called_once_with(construct.return_value, output_format='csv', timeout=None) def assert_cmd_execution( self, construct, execute, base_method, cmd_sub, ignore_stderr=False, **base_method_kwargs @@ -224,8 +224,10 @@ def assert_cmd_execution( """Asssert Base class method successfully executed""" assert execute.return_value == base_method(**base_method_kwargs) assert cmd_sub == Base.command_sub - construct.called_once_with({}) - execute.called_once_with(construct.return_value, ignore_stderr=ignore_stderr) + construct.assert_called_once_with(base_method_kwargs.get('options')) + execute.assert_called_once_with( + construct.return_value, ignore_stderr=ignore_stderr, timeout=None + ) @mock.patch('robottelo.cli.base.Base.execute') @mock.patch('robottelo.cli.base.Base._construct_command') @@ -308,16 +310,36 @@ def test_info_requires_organization_id(self, _): # noqa: PT019 - not a fixture with pytest.raises(CLIError): Base.info() + def assert_alt_cmd_execution( + self, + construct, + execute, + base_method, + cmd_sub, + call_kwargs, + command_kwarg=True, + **base_method_kwargs, + ): + """Asssert Base class method successfully executed""" + assert execute.return_value == base_method(**base_method_kwargs) + assert cmd_sub == Base.command_sub + construct.assert_called_once_with(base_method_kwargs.get('options')) + if command_kwarg: + execute.assert_called_once_with(command=construct.return_value, **call_kwargs) + else: + execute.assert_called_once_with(construct.return_value, **call_kwargs) + @mock.patch('robottelo.cli.base.hammer.parse_info') @mock.patch('robottelo.cli.base.Base.execute') @mock.patch('robottelo.cli.base.Base._construct_command') def test_info_without_parsing_response(self, construct, execute, parse): """Check info method execution without parsing response""" - self.assert_cmd_execution( + self.assert_alt_cmd_execution( construct, execute, Base.info, 'info', + call_kwargs={'output_format': 'json', 'return_raw_response': None}, output_format='json', options={'organization-id': 1}, ) @@ -329,18 +351,15 @@ def test_info_without_parsing_response(self, construct, execute, parse): def test_info_parsing_response(self, construct, execute, parse): """Check info method execution parsing response""" parse.return_value = execute.return_value = 'some_response' - self.assert_cmd_execution( - construct, execute, Base.info, 'info', options={'organization-id': 1} + self.assert_alt_cmd_execution( + construct, + execute, + Base.info, + 'info', + call_kwargs={'output_format': None, 'return_raw_response': None}, + options={'organization-id': 1}, ) - parse.called_once_with('some_response') - - # @mock.patch('robottelo.cli.base.Base.command_requires_org') - # def test_list_requires_organization_id(self, _): - # """Check list raises CLIError with organization-id is not present in - # options - # """ - # with pytest.raises(CLIError): - # Base.list() + parse.assert_called_once_with('some_response') @mock.patch('robottelo.cli.base.Base.execute') @mock.patch('robottelo.cli.base.Base._construct_command') @@ -348,8 +367,8 @@ def test_list_with_default_per_page(self, construct, execute): """Check list method set per_page as 1000 by default""" assert execute.return_value == Base.list(options={'organization-id': 1}) assert Base.command_sub == 'list' - construct.called_once_with({'per-page': 1000}) - execute.called_once_with(construct.return_value, output_format='csv') + construct.assert_called_once_with({'organization-id': 1, 'per-page': 10000}) + execute.assert_called_once_with(construct.return_value, output_format='csv') @mock.patch('robottelo.cli.base.Base.execute') @mock.patch('robottelo.cli.base.Base._construct_command') @@ -358,39 +377,80 @@ def test_list_without_per_page(self, construct, execute): list_with_per_page_false = partial( Base.list, per_page=False, options={'organization-id': 1} ) - self.assert_cmd_execution(construct, execute, list_with_per_page_false, 'list') + self.assert_alt_cmd_execution( + construct, + execute, + list_with_per_page_false, + 'list', + call_kwargs={'output_format': 'csv'}, + command_kwarg=False, + options={'organization-id': 1}, + ) @mock.patch('robottelo.cli.base.Base.execute') @mock.patch('robottelo.cli.base.Base._construct_command') def test_puppet_classes(self, construct, execute): """Check puppet_classes method execution""" - self.assert_cmd_execution(construct, execute, Base.puppetclasses, 'puppet-classes') + self.assert_alt_cmd_execution( + construct, + execute, + Base.puppetclasses, + 'puppet-classes', + call_kwargs={'output_format': 'csv'}, + command_kwarg=False, + ) @mock.patch('robottelo.cli.base.Base.execute') @mock.patch('robottelo.cli.base.Base._construct_command') def test_remove_operating_system(self, construct, execute): """Check remove_operating_system method execution""" - self.assert_cmd_execution( - construct, execute, Base.remove_operating_system, 'remove-operatingsystem' + self.assert_alt_cmd_execution( + construct, + execute, + Base.remove_operating_system, + 'remove-operatingsystem', + call_kwargs={}, + command_kwarg=False, ) @mock.patch('robottelo.cli.base.Base.execute') @mock.patch('robottelo.cli.base.Base._construct_command') def test_sc_params(self, construct, execute): """Check sc_params method execution""" - self.assert_cmd_execution(construct, execute, Base.sc_params, 'sc-params') + self.assert_alt_cmd_execution( + construct, + execute, + Base.sc_params, + 'sc-params', + call_kwargs={'output_format': 'csv'}, + command_kwarg=False, + ) @mock.patch('robottelo.cli.base.Base.execute') @mock.patch('robottelo.cli.base.Base._construct_command') def test_set_parameter(self, construct, execute): """Check set_parameter method execution""" - self.assert_cmd_execution(construct, execute, Base.set_parameter, 'set-parameter') + self.assert_alt_cmd_execution( + construct, + execute, + Base.set_parameter, + 'set-parameter', + call_kwargs={}, + command_kwarg=False, + ) @mock.patch('robottelo.cli.base.Base.execute') @mock.patch('robottelo.cli.base.Base._construct_command') def test_update(self, construct, execute): """Check update method execution""" - self.assert_cmd_execution(construct, execute, Base.update, 'update') + self.assert_alt_cmd_execution( + construct, + execute, + Base.update, + 'update', + call_kwargs={'output_format': 'csv', 'return_raw_response': None}, + command_kwarg=False, + ) class CLIErrorTests(unittest.TestCase): diff --git a/tests/robottelo/test_cli_method_calls.py b/tests/robottelo/test_cli_method_calls.py index 04d4c65e8cd..765a24d2044 100644 --- a/tests/robottelo/test_cli_method_calls.py +++ b/tests/robottelo/test_cli_method_calls.py @@ -40,8 +40,8 @@ def test_cli_org_method_called(mocker, command_sub): options = {'foo': 'bar'} assert execute.return_value == getattr(Org, command_sub.replace('-', '_'))(options) assert command_sub == Org.command_sub - assert construct.called_once_with(options) - assert execute.called_once_with(construct.return_value) + construct.assert_called_once_with(options) + execute.assert_called_once_with(construct.return_value) @pytest.mark.parametrize('command_sub', ['import-classes', 'refresh-features']) @@ -54,11 +54,11 @@ def test_cli_proxy_method_called(mocker, command_sub): options = {'foo': 'bar'} assert execute.return_value == getattr(Proxy, command_sub.replace('-', '_'))(options) assert command_sub == Proxy.command_sub - assert construct.called_once_with(options) - assert execute.called_once_with(construct.return_value) + construct.assert_called_once_with(options) + execute.assert_called_once_with(construct.return_value) -@pytest.mark.parametrize('command_sub', ['synchronize', 'remove-content', 'upload-content']) +@pytest.mark.parametrize('command_sub', ['remove-content', 'upload-content']) def test_cli_repository_method_called(mocker, command_sub): """Check Repository methods are called and command_sub edited This is a parametrized test called by Pytest for each of Repository methods @@ -68,8 +68,8 @@ def test_cli_repository_method_called(mocker, command_sub): options = {'foo': 'bar'} assert execute.return_value == getattr(Repository, command_sub.replace('-', '_'))(options) assert command_sub == Repository.command_sub - assert construct.called_once_with(options) - assert execute.called_once_with(construct.return_value) + construct.assert_called_once_with(options) + execute.assert_called_once_with(construct.return_value, output_format='csv', ignore_stderr=True) @pytest.mark.parametrize('command_sub', ['info', 'create']) @@ -94,5 +94,5 @@ def test_cli_subscription_method_called(mocker, command_sub): options = {'foo': 'bar'} assert execute.return_value == getattr(Subscription, command_sub.replace('-', '_'))(options) assert command_sub == Subscription.command_sub - assert construct.called_once_with(options) - assert execute.called_once_with(construct.return_value) + construct.assert_called_once_with(options) + execute.assert_called_once_with(construct.return_value, ignore_stderr=True, timeout=None) From b240f8690d379d55202b3df55fe852bcc4665b20 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 3 Apr 2024 04:31:21 -0400 Subject: [PATCH 572/586] [6.14.z] Fix foreman service test (#14600) --- tests/foreman/maintain/test_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/foreman/maintain/test_service.py b/tests/foreman/maintain/test_service.py index 9b2970a8a64..5fece7c6aaf 100644 --- a/tests/foreman/maintain/test_service.py +++ b/tests/foreman/maintain/test_service.py @@ -226,7 +226,9 @@ def test_positive_foreman_service(sat_maintain): assert 'foreman' in result.stdout result = sat_maintain.cli.Service.status(options={'only': 'httpd'}) assert result.status == 0 - result = sat_maintain.cli.Health.check(options={'assumeyes': True}) + result = sat_maintain.cli.Health.check( + options={'assumeyes': True, 'whitelist': 'check-tftp-storage'} + ) assert result.status == 0 assert 'foreman' in result.stdout assert sat_maintain.cli.Service.start(options={'only': 'foreman'}).status == 0 From b61e4cc5186738ce18dbdb212c3841494c10ee4e Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:57:03 -0400 Subject: [PATCH 573/586] [6.14.z] Update Platform tests that have hard coded content host versions (#14612) --- pytest_fixtures/core/contenthosts.py | 15 ++++- pytest_plugins/fixture_markers.py | 1 + .../destructive/test_capsule_loadbalancer.py | 66 ++++++++++++------- tests/foreman/maintain/test_upgrade.py | 17 +++-- 4 files changed, 69 insertions(+), 30 deletions(-) diff --git a/pytest_fixtures/core/contenthosts.py b/pytest_fixtures/core/contenthosts.py index 72f39b50796..d2bbb4cce39 100644 --- a/pytest_fixtures/core/contenthosts.py +++ b/pytest_fixtures/core/contenthosts.py @@ -45,6 +45,15 @@ def rhel_contenthost(request): yield host +@pytest.fixture(scope='module') +def module_rhel_contenthost(request): + """A module-level fixture that provides a content host object parametrized""" + # Request should be parametrized through pytest_fixtures.fixture_markers + # unpack params dict + with Broker(**host_conf(request), host_class=ContentHost) as host: + yield host + + @pytest.fixture(params=[{'rhel_version': '7'}]) def rhel7_contenthost(request): """A function-level fixture that provides a rhel7 content host object""" @@ -278,8 +287,10 @@ def sat_upgrade_chost(): def custom_host(request): """A rhel content host that passes custom host config through request.param""" deploy_args = request.param - # if 'deploy_rhel_version' is not set, let's default to RHEL 8 - deploy_args['deploy_rhel_version'] = deploy_args.get('deploy_rhel_version', '8') + # if 'deploy_rhel_version' is not set, let's default to what's in content_host.yaml + deploy_args['deploy_rhel_version'] = deploy_args.get( + 'deploy_rhel_version', settings.content_host.default_rhel_version + ) deploy_args['workflow'] = 'deploy-rhel' with Broker(**deploy_args, host_class=Satellite) as host: yield host diff --git a/pytest_plugins/fixture_markers.py b/pytest_plugins/fixture_markers.py index 5905114132d..795397bec5d 100644 --- a/pytest_plugins/fixture_markers.py +++ b/pytest_plugins/fixture_markers.py @@ -5,6 +5,7 @@ TARGET_FIXTURES = [ 'rhel_contenthost', + 'module_rhel_contenthost', 'content_hosts', 'module_provisioning_rhel_content', 'capsule_provisioning_rhel_content', diff --git a/tests/foreman/destructive/test_capsule_loadbalancer.py b/tests/foreman/destructive/test_capsule_loadbalancer.py index 9f94f13402c..07ad5cffb27 100644 --- a/tests/foreman/destructive/test_capsule_loadbalancer.py +++ b/tests/foreman/destructive/test_capsule_loadbalancer.py @@ -14,6 +14,7 @@ import pytest from wrapanapi import VmState +from robottelo import constants from robottelo.config import settings from robottelo.constants import CLIENT_PORT, DataFile from robottelo.utils.installer import InstallerCommand @@ -22,34 +23,50 @@ @pytest.fixture(scope='module') -def content_for_client(module_target_sat, module_org, module_lce, module_cv, module_ak): +def content_for_client(module_target_sat, module_sca_manifest_org, module_lce, module_cv): """Setup content to be used by haproxy and client :return: Activation key, client lifecycle environment(used by setup_capsules()) """ - module_target_sat.cli_factory.setup_org_for_a_custom_repo( - { - 'url': settings.repos.RHEL7_OS, - 'organization-id': module_org.id, - 'content-view-id': module_cv.id, - 'lifecycle-environment-id': module_lce.id, - 'activationkey-id': module_ak.id, - } - ) - return {'client_ak': module_ak, 'client_lce': module_lce} + rhel_ver = settings.content_host.default_rhel_version + baseos = f'rhel{rhel_ver}_bos' + appstream = f'rhel{rhel_ver}_aps' + + rh_repos = [] + for repo in [baseos, appstream]: + synced_repo_id = module_target_sat.api_factory.enable_sync_redhat_repo( + constants.REPOS[repo], module_sca_manifest_org.id + ) + repo = module_target_sat.api.Repository(id=synced_repo_id).read() + rh_repos.append(repo) + + module_cv.repository = rh_repos + module_cv.update(['repository']) + module_cv.publish() + module_cv = module_cv.read() + cvv = module_cv.version[0] + cvv.promote(data={'environment_ids': module_lce.id}) + module_cv = module_cv.read() + ak = module_target_sat.api.ActivationKey( + content_view=module_cv, + environment=module_lce, + organization=module_sca_manifest_org, + ).create() + + return {'client_ak': ak, 'client_lce': module_lce} @pytest.fixture(scope='module') def setup_capsules( module_org, - rhel7_contenthost_module, + module_rhel_contenthost, module_lb_capsule, module_target_sat, content_for_client, ): """Install capsules with loadbalancer options""" - extra_cert_var = {'foreman-proxy-cname': rhel7_contenthost_module.hostname} - extra_installer_var = {'certs-cname': rhel7_contenthost_module.hostname} + extra_cert_var = {'foreman-proxy-cname': module_rhel_contenthost.hostname} + extra_installer_var = {'certs-cname': module_rhel_contenthost.hostname} for capsule in module_lb_capsule: capsule.register_to_cdn() @@ -92,20 +109,20 @@ def setup_capsules( @pytest.fixture(scope='module') def setup_haproxy( module_org, - rhel7_contenthost_module, + module_rhel_contenthost, content_for_client, module_target_sat, setup_capsules, ): """Install and configure haproxy and setup logging""" - haproxy = rhel7_contenthost_module + haproxy = module_rhel_contenthost # Using same AK for haproxy just for packages haproxy_ak = content_for_client['client_ak'] haproxy.execute('firewall-cmd --add-service RH-Satellite-6-capsule') haproxy.execute('firewall-cmd --runtime-to-permanent') haproxy.install_katello_ca(module_target_sat) haproxy.register_contenthost(module_org.label, haproxy_ak.name) - result = haproxy.execute('yum install haproxy policycoreutils-python -y') + result = haproxy.execute('yum install haproxy policycoreutils-python-utils -y') assert result.status == 0 haproxy.execute('rm -f /etc/haproxy/haproxy.cfg') haproxy.session.sftp_write( @@ -171,8 +188,9 @@ def loadbalancer_setup( @pytest.mark.e2e @pytest.mark.tier1 +@pytest.mark.rhel_ver_list([settings.content_host.default_rhel_version]) def test_loadbalancer_install_package( - loadbalancer_setup, setup_capsules, rhel7_contenthost, module_org, module_location, request + loadbalancer_setup, setup_capsules, rhel_contenthost, module_org, module_location, request ): r"""Install packages on a content host regardless of the registered capsule being available @@ -193,7 +211,7 @@ def test_loadbalancer_install_package( """ # Register content host - result = rhel7_contenthost.register( + result = rhel_contenthost.register( org=module_org, loc=module_location, activation_keys=loadbalancer_setup['content_for_client']['client_ak'].name, @@ -203,15 +221,15 @@ def test_loadbalancer_install_package( assert result.status == 0, f'Failed to register host: {result.stderr}' # Try package installation - result = rhel7_contenthost.execute('yum install -y tree') + result = rhel_contenthost.execute('yum install -y tree') assert result.status == 0 hosts = loadbalancer_setup['module_target_sat'].cli.Host.list( {'organization-id': loadbalancer_setup['module_org'].id} ) - assert rhel7_contenthost.hostname in [host['name'] for host in hosts] + assert rhel_contenthost.hostname in [host['name'] for host in hosts] - result = rhel7_contenthost.execute('rpm -qa | grep katello-ca-consumer') + result = rhel_contenthost.execute('rpm -qa | grep katello-ca-consumer') # Find which capsule the host is registered to since it's RoundRobin # The following also asserts the above result @@ -225,14 +243,14 @@ def test_loadbalancer_install_package( ) # Remove the packages from the client - result = rhel7_contenthost.execute('yum remove -y tree') + result = rhel_contenthost.execute('yum remove -y tree') assert result.status == 0 # Power off the capsule that the client is registered to registered_to_capsule.power_control(state=VmState.STOPPED, ensure=True) # Try package installation again - result = rhel7_contenthost.execute('yum install -y tree') + result = rhel_contenthost.execute('yum install -y tree') assert result.status == 0 diff --git a/tests/foreman/maintain/test_upgrade.py b/tests/foreman/maintain/test_upgrade.py index 686f036cd80..1e544ee6751 100644 --- a/tests/foreman/maintain/test_upgrade.py +++ b/tests/foreman/maintain/test_upgrade.py @@ -98,8 +98,14 @@ def test_positive_repositories_validate(sat_maintain): @pytest.mark.parametrize( 'custom_host', [ - {'deploy_rhel_version': '8', 'deploy_flavor': 'satqe-ssd.disk.xxxl'}, - {'deploy_rhel_version': '8', 'deploy_flavor': 'satqe-ssd.standard.std'}, + { + 'deploy_rhel_version': settings.server.version.rhel_version, + 'deploy_flavor': 'satqe-ssd.disk.xxxl', + }, + { + 'deploy_rhel_version': settings.server.version.rhel_version, + 'deploy_flavor': 'satqe-ssd.standard.std', + }, ], ids=['default', 'medium'], indirect=True, @@ -122,15 +128,18 @@ def test_negative_pre_upgrade_tuning_profile_check(request, custom_host): :expectedresults: Pre-upgrade check fails. """ profile = request.node.callspec.id + rhel_major = custom_host.os_version.major sat_version = ".".join(settings.server.version.release.split('.')[0:2]) - # Register to CDN for RHEL8 repos, download and enable last y stream's ohsnap repos, + # Register to CDN for RHEL repos, download and enable last y stream's ohsnap repos, # and enable the satellite module and install it on the host custom_host.register_to_cdn() last_y_stream = last_y_stream_version( SATELLITE_VERSION if sat_version == 'stream' else sat_version ) custom_host.download_repofile(product='satellite', release=last_y_stream) - custom_host.execute('dnf -y module enable satellite:el8 && dnf -y install satellite') + custom_host.execute( + f'dnf -y module enable satellite:el{rhel_major} && dnf -y install satellite' + ) # Install with development tuning profile to get around installer checks custom_host.execute( 'satellite-installer --scenario satellite --tuning development', From ba752329f9adde42bffb452abe5d574fe8cdb7b4 Mon Sep 17 00:00:00 2001 From: Gaurav Talreja Date: Thu, 4 Apr 2024 12:44:18 +0530 Subject: [PATCH 574/586] [6.14.z] Move TestAnsibleREX CLI tests to Ansible module (#14611) Move TestAnsibleREX CLI tests to Ansible module (#14577) Signed-off-by: Gaurav Talreja (cherry picked from commit 9687508361e085b41c512128b3f3183fc26f1249) --- tests/foreman/cli/test_ansible.py | 604 +++++++++++++++++----- tests/foreman/cli/test_remoteexecution.py | 412 +-------------- 2 files changed, 486 insertions(+), 530 deletions(-) diff --git a/tests/foreman/cli/test_ansible.py b/tests/foreman/cli/test_ansible.py index 8cffe15b20d..707acbcfec9 100644 --- a/tests/foreman/cli/test_ansible.py +++ b/tests/foreman/cli/test_ansible.py @@ -4,156 +4,520 @@ :CaseAutomation: Automated -:CaseComponent: Ansible-ConfigurationManagement - :Team: Rocket :CaseImportance: High - """ +from time import sleep + from fauxfactory import gen_string import pytest from robottelo.config import settings -@pytest.mark.e2e -@pytest.mark.no_containers -@pytest.mark.rhel_ver_match('[^6].*') -def test_positive_ansible_e2e(target_sat, module_org, rhel_contenthost): +def assert_job_invocation_result( + sat, invocation_command_id, client_hostname, expected_result='success' +): + """Asserts the job invocation finished with the expected result and fetches job output + when error occurs. Result is one of: success, pending, error, warning""" + result = sat.cli.JobInvocation.info({'id': invocation_command_id}) + try: + assert result[expected_result] == '1' + except AssertionError as err: + raise AssertionError( + 'host output: {}'.format( + ' '.join( + sat.cli.JobInvocation.get_output( + {'id': invocation_command_id, 'host': client_hostname} + ) + ) + ) + ) from err + + +@pytest.mark.upgrade +class TestAnsibleCfgMgmt: + """Test class for Configuration Management with Ansible + + :CaseComponent: Ansible-ConfigurationManagement """ - Test successful execution of Ansible Job on host. - :id: 0c52bc63-a41a-4f48-a980-fe49b4ecdbdc + @pytest.mark.e2e + @pytest.mark.no_containers + @pytest.mark.rhel_ver_match('[^6].*') + def test_positive_ansible_e2e( + self, target_sat, module_sca_manifest_org, module_ak_with_cv, rhel_contenthost + ): + """ + Test successful execution of Ansible Job on host. - :steps: - 1. Register a content host with satellite - 2. Import a role into satellite - 3. Assign that role to a host - 4. Assert that the role and variable were assigned to the host successfully - 5. Run the Ansible playbook associated with that role - 6. Check if the job is executed successfully. - 7. Disassociate the Role from the host. - 8. Delete the assigned ansible role + :id: 0c52bc63-a41a-4f48-a980-fe49b4ecdbdc - :expectedresults: - 1. Host should be assigned the proper role. - 2. Job execution must be successful. - 3. Operations performed with hammer must be successful. + :steps: + 1. Register a content host with satellite + 2. Import a role into satellite + 3. Assign that role to a host + 4. Assert that the role and variable were assigned to the host successfully + 5. Run the Ansible playbook associated with that role + 6. Check if the job is executed successfully. + 7. Disassociate the Role from the host. + 8. Delete the assigned ansible role - :BZ: 2154184 + :expectedresults: + 1. Host should be assigned the proper role. + 2. Job execution must be successful. + 3. Operations performed with hammer must be successful. - :customerscenario: true + :BZ: 2154184 - :CaseImportance: Critical - """ - SELECTED_ROLE = 'RedHatInsights.insights-client' - SELECTED_ROLE_1 = 'theforeman.foreman_scap_client' - SELECTED_VAR = gen_string('alpha') - # disable batch tasks to test BZ#2154184 - target_sat.cli.Settings.set({'name': 'foreman_tasks_proxy_batch_trigger', 'value': 'false'}) - if rhel_contenthost.os_version.major <= 7: - rhel_contenthost.create_custom_repos(rhel7=settings.repos.rhel7_os) - assert rhel_contenthost.execute('yum install -y insights-client').status == 0 - rhel_contenthost.install_katello_ca(target_sat) - rhel_contenthost.register_contenthost(module_org.label, force=True) - assert rhel_contenthost.subscribed - rhel_contenthost.add_rex_key(satellite=target_sat) - proxy_id = target_sat.nailgun_smart_proxy.id - target_host = rhel_contenthost.nailgun_host - - target_sat.cli.Ansible.roles_sync( - {'role-names': f'{SELECTED_ROLE},{SELECTED_ROLE_1}', 'proxy-id': proxy_id} - ) + :customerscenario: true + """ + SELECTED_ROLE = 'RedHatInsights.insights-client' + SELECTED_ROLE_1 = 'theforeman.foreman_scap_client' + SELECTED_VAR = gen_string('alpha') + proxy_id = target_sat.nailgun_smart_proxy.id + # disable batch tasks to test BZ#2154184 + target_sat.cli.Settings.set({'name': 'foreman_tasks_proxy_batch_trigger', 'value': 'false'}) + result = rhel_contenthost.register( + module_sca_manifest_org, None, module_ak_with_cv.name, target_sat + ) + assert result.status == 0, f'Failed to register host: {result.stderr}' + if rhel_contenthost.os_version.major <= 7: + rhel_contenthost.create_custom_repos(rhel7=settings.repos.rhel7_os) + assert rhel_contenthost.execute('yum install -y insights-client').status == 0 + target_host = rhel_contenthost.nailgun_host - result = target_sat.cli.Host.ansible_roles_add( - {'id': target_host.id, 'ansible-role': SELECTED_ROLE} - ) - assert 'Ansible role has been associated.' in result[0]['message'] + target_sat.cli.Ansible.roles_sync( + {'role-names': f'{SELECTED_ROLE},{SELECTED_ROLE_1}', 'proxy-id': proxy_id} + ) + result = target_sat.cli.Host.ansible_roles_add( + {'id': target_host.id, 'ansible-role': SELECTED_ROLE} + ) + assert 'Ansible role has been associated.' in result[0]['message'] - target_sat.cli.Ansible.variables_create( - {'variable': SELECTED_VAR, 'ansible-role': SELECTED_ROLE} - ) + target_sat.cli.Ansible.variables_create( + {'variable': SELECTED_VAR, 'ansible-role': SELECTED_ROLE} + ) - assert SELECTED_ROLE, ( - SELECTED_VAR in target_sat.cli.Ansible.variables_info({'name': SELECTED_VAR}).stdout - ) - template_id = ( - target_sat.api.JobTemplate() - .search(query={'search': 'name="Ansible Roles - Ansible Default"'})[0] - .id - ) - job = target_sat.api.JobInvocation().run( - synchronous=False, - data={ - 'job_template_id': template_id, - 'targeting_type': 'static_query', - 'search_query': f'name = {rhel_contenthost.hostname}', - }, - ) - target_sat.wait_for_tasks( - f'resource_type = JobInvocation and resource_id = {job["id"]}', poll_timeout=1000 - ) - result = target_sat.api.JobInvocation(id=job['id']).read() - assert result.succeeded == 1 + assert SELECTED_ROLE, ( + SELECTED_VAR in target_sat.cli.Ansible.variables_info({'name': SELECTED_VAR}).stdout + ) + template_id = ( + target_sat.api.JobTemplate() + .search(query={'search': 'name="Ansible Roles - Ansible Default"'})[0] + .id + ) + job = target_sat.api.JobInvocation().run( + synchronous=False, + data={ + 'job_template_id': template_id, + 'targeting_type': 'static_query', + 'search_query': f'name = {rhel_contenthost.hostname}', + }, + ) + target_sat.wait_for_tasks( + f'resource_type = JobInvocation and resource_id = {job["id"]}', poll_timeout=1000 + ) + result = target_sat.api.JobInvocation(id=job['id']).read() + assert result.succeeded == 1 - result = target_sat.cli.Host.ansible_roles_assign( - {'id': target_host.id, 'ansible-roles': f'{SELECTED_ROLE},{SELECTED_ROLE_1}'} - ) - assert 'Ansible roles were assigned to the host' in result[0]['message'] + result = target_sat.cli.Host.ansible_roles_assign( + {'id': target_host.id, 'ansible-roles': f'{SELECTED_ROLE},{SELECTED_ROLE_1}'} + ) + assert 'Ansible roles were assigned to the host' in result[0]['message'] - result = target_sat.cli.Host.ansible_roles_remove( - {'id': target_host.id, 'ansible-role': SELECTED_ROLE} - ) - assert 'Ansible role has been disassociated.' in result[0]['message'] + result = target_sat.cli.Host.ansible_roles_remove( + {'id': target_host.id, 'ansible-role': SELECTED_ROLE} + ) + assert 'Ansible role has been disassociated.' in result[0]['message'] - result = target_sat.cli.Ansible.roles_delete({'name': SELECTED_ROLE}) - assert f'Ansible role [{SELECTED_ROLE}] was deleted.' in result[0]['message'] + result = target_sat.cli.Ansible.roles_delete({'name': SELECTED_ROLE}) + assert f'Ansible role [{SELECTED_ROLE}] was deleted.' in result[0]['message'] - assert SELECTED_ROLE, ( - SELECTED_VAR not in target_sat.cli.Ansible.variables_info({'name': SELECTED_VAR}).stdout - ) + assert SELECTED_ROLE, ( + SELECTED_VAR not in target_sat.cli.Ansible.variables_info({'name': SELECTED_VAR}).stdout + ) + @pytest.mark.e2e + @pytest.mark.tier2 + def test_add_and_remove_ansible_role_hostgroup(self, target_sat): + """ + Test add and remove functionality for ansible roles in hostgroup via CLI -@pytest.mark.e2e -@pytest.mark.tier2 -def test_add_and_remove_ansible_role_hostgroup(target_sat): - """ - Test add and remove functionality for ansible roles in hostgroup via CLI + :id: 2c6fda14-4cd2-490a-b7ef-7a08f8164fad + + :customerscenario: true + + :steps: + 1. Create a hostgroup + 2. Sync few ansible roles + 3. Assign a few ansible roles with the host group + 4. Add some ansible role with the host group + 5. Remove the added ansible roles from the host group - :id: 2c6fda14-4cd2-490a-b7ef-7a08f8164fad + :expectedresults: + 1. Ansible role assign/add/remove functionality should work as expected in CLI - :customerscenario: true + :BZ: 2029402 + """ + ROLES = [ + 'theforeman.foreman_scap_client', + 'redhat.satellite.hostgroups', + 'RedHatInsights.insights-client', + ] + proxy_id = target_sat.nailgun_smart_proxy.id + hg_name = gen_string('alpha') + result = target_sat.cli.HostGroup.create({'name': hg_name}) + assert result['name'] == hg_name + target_sat.cli.Ansible.roles_sync({'role-names': ROLES, 'proxy-id': proxy_id}) + result = target_sat.cli.HostGroup.ansible_roles_assign( + {'name': hg_name, 'ansible-roles': f'{ROLES[1]},{ROLES[2]}'} + ) + assert 'Ansible roles were assigned to the hostgroup' in result[0]['message'] + result = target_sat.cli.HostGroup.ansible_roles_add( + {'name': hg_name, 'ansible-role': ROLES[0]} + ) + assert 'Ansible role has been associated.' in result[0]['message'] + result = target_sat.cli.HostGroup.ansible_roles_remove( + {'name': hg_name, 'ansible-role': ROLES[0]} + ) + assert 'Ansible role has been disassociated.' in result[0]['message'] - :steps: - 1. Create a hostgroup - 2. Sync few ansible roles - 3. Assign a few ansible roles with the host group - 4. Add some ansible role with the host group - 5. Remove the added ansible roles from the host group - :expectedresults: - 1. Ansible role assign/add/remove functionality should work as expected in CLI +@pytest.mark.tier3 +@pytest.mark.upgrade +class TestAnsibleREX: + """Test class for remote execution via Ansible - :BZ: 2029402 + :CaseComponent: Ansible-RemoteExecution """ - ROLES = [ - 'theforeman.foreman_scap_client', - 'redhat.satellite.hostgroups', - 'RedHatInsights.insights-client', - ] - proxy_id = target_sat.nailgun_smart_proxy.id - hg_name = gen_string('alpha') - result = target_sat.cli.HostGroup.create({'name': hg_name}) - assert result['name'] == hg_name - target_sat.cli.Ansible.roles_sync({'role-names': ROLES, 'proxy-id': proxy_id}) - result = target_sat.cli.HostGroup.ansible_roles_assign( - {'name': hg_name, 'ansible-roles': f'{ROLES[1]},{ROLES[2]}'} - ) - assert 'Ansible roles were assigned to the hostgroup' in result[0]['message'] - result = target_sat.cli.HostGroup.ansible_roles_add({'name': hg_name, 'ansible-role': ROLES[0]}) - assert 'Ansible role has been associated.' in result[0]['message'] - result = target_sat.cli.HostGroup.ansible_roles_remove( - {'name': hg_name, 'ansible-role': ROLES[0]} + + @pytest.mark.pit_client + @pytest.mark.pit_server + @pytest.mark.rhel_ver_match('[^6]') + def test_positive_run_effective_user_job(self, rex_contenthost, target_sat): + """Tests Ansible REX job having effective user runs successfully + + :id: a5fa20d8-c2bd-4bbf-a6dc-bf307b59dd8c + + :steps: + 0. Create a VM and register to SAT and prepare for REX (ssh key) + 1. Run Ansible Command job for the host to create a user + 2. Run Ansible Command job using effective user + 3. Check the job result at the host is done under that user + + :expectedresults: multiple asserts along the code + + :parametrized: yes + """ + client = rex_contenthost + # create a user on client via remote job + username = gen_string('alpha') + filename = gen_string('alpha') + make_user_job = target_sat.cli_factory.job_invocation( + { + 'job-template': 'Run Command - Ansible Default', + 'inputs': f'command=useradd -m {username}', + 'search-query': f'name ~ {client.hostname}', + } + ) + assert_job_invocation_result(target_sat, make_user_job['id'], client.hostname) + # create a file as new user + invocation_command = target_sat.cli_factory.job_invocation( + { + 'job-template': 'Run Command - Ansible Default', + 'inputs': f'command=touch /home/{username}/{filename}', + 'search-query': f'name ~ {client.hostname}', + 'effective-user': username, + } + ) + assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) + + # check the file owner + result = client.execute( + f'''stat -c '%U' /home/{username}/{filename}''', + ) + # assert the file is owned by the effective user + assert username == result.stdout.strip('\n'), 'file ownership mismatch' + + @pytest.mark.rhel_ver_list([8]) + def test_positive_run_reccuring_job(self, rex_contenthost, target_sat): + """Tests Ansible REX reccuring job runs successfully multiple times + + :id: 49b0d31d-58f9-47f1-aa5d-561a1dcb0d66 + + :setup: + 1. Create a VM, register to SAT and configure REX (ssh-key) + + :steps: + 1. Run recurring Ansible Command job for the host + 2. Check the multiple job results at the host + + :expectedresults: multiple asserts along the code + + :bz: 2129432 + + :customerscenario: true + + :parametrized: yes + """ + client = rex_contenthost + invocation_command = target_sat.cli_factory.job_invocation( + { + 'job-template': 'Run Command - Ansible Default', + 'inputs': 'command=ls', + 'search-query': f'name ~ {client.hostname}', + 'cron-line': '* * * * *', # every minute + 'max-iteration': 2, # just two runs + } + ) + result = target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) + sleep(150) + rec_logic = target_sat.cli.RecurringLogic.info({'id': result['recurring-logic-id']}) + assert rec_logic['state'] == 'finished' + assert rec_logic['iteration'] == '2' + # 2129432 + rec_logic_keys = rec_logic.keys() + assert 'action' in rec_logic_keys + assert 'last-occurrence' in rec_logic_keys + assert 'next-occurrence' in rec_logic_keys + assert 'state' in rec_logic_keys + assert 'purpose' in rec_logic_keys + assert 'iteration' in rec_logic_keys + assert 'iteration-limit' in rec_logic_keys + + @pytest.mark.rhel_ver_list([8]) + def test_positive_run_concurrent_jobs(self, rex_contenthosts, target_sat): + """Tests Ansible REX concurent jobs without batch trigger + + :id: ad0f108c-03f2-49c7-8732-b1056570567b + + :steps: + 1. Create 2 hosts, disable foreman_tasks_proxy_batch_trigger + 2. Run Ansible Command job with concurrency-setting + + :expectedresults: multiple asserts along the code + + :BZ: 1817320 + + :customerscenario: true + + :parametrized: yes + """ + clients = rex_contenthosts + param_name = 'foreman_tasks_proxy_batch_trigger' + target_sat.cli.GlobalParameter().set({'name': param_name, 'value': 'false'}) + output_msgs = [] + invocation_command = target_sat.cli_factory.job_invocation( + { + 'job-template': 'Run Command - Ansible Default', + 'inputs': 'command=ls', + 'search-query': f'name ~ {clients[0].hostname} or name ~ {clients[1].hostname}', + 'concurrency-level': 2, + } + ) + for vm in clients: + output_msgs.append( + 'host output from {}: {}'.format( + vm.hostname, + ' '.join( + target_sat.cli.JobInvocation.get_output( + {'id': invocation_command['id'], 'host': vm.hostname} + ) + ), + ) + ) + result = target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) + assert result['success'] == '2', output_msgs + target_sat.cli.GlobalParameter().delete({'name': param_name}) + assert len(target_sat.cli.GlobalParameter().list({'search': param_name})) == 0 + + @pytest.mark.rhel_ver_list([8]) + def test_positive_run_serial(self, rex_contenthosts, target_sat): + """Tests subtasks in a job run one by one when concurrency level set to 1 + + :id: 5ce39447-82d0-42df-81be-16ed3d67a2a4 + + :setup: + 1. Create 2 hosts, register to SAT and configure REX (ssh-key) + + :steps: + 1. Run a bash command job with concurrency level 1 + + :expectedresults: First subtask should run immediately, second one after the first one finishes + + :parametrized: yes + """ + hosts = rex_contenthosts + output_msgs = [] + template_file = f'/root/{gen_string("alpha")}.template' + target_sat.execute( + f"echo 'rm /root/test-<%= @host %>; echo $(date +%s) >> /root/test-<%= @host %>; sleep 120; echo $(date +%s) >> /root/test-<%= @host %>' > {template_file}" + ) + template = target_sat.cli.JobTemplate.create( + { + 'name': gen_string('alpha'), + 'file': template_file, + 'job-category': 'Commands', + 'provider-type': 'script', + } + ) + invocation = target_sat.cli_factory.job_invocation( + { + 'job-template': template['name'], + 'search-query': f'name ~ {hosts[0].hostname} or name ~ {hosts[1].hostname}', + 'concurrency-level': 1, + } + ) + for vm in hosts: + output_msgs.append( + 'host output from {}: {}'.format( + vm.hostname, + ' '.join( + target_sat.cli.JobInvocation.get_output( + {'id': invocation['id'], 'host': vm.hostname} + ) + ), + ) + ) + result = target_sat.cli.JobInvocation.info({'id': invocation['id']}) + assert result['success'] == '2', output_msgs + # assert for time diffs + file1 = hosts[0].execute('cat /root/test-$(hostname)').stdout + file2 = hosts[1].execute('cat /root/test-$(hostname)').stdout + file1_start, file1_end = map(int, file1.rstrip().split('\n')) + file2_start, file2_end = map(int, file2.rstrip().split('\n')) + if file1_start > file2_start: + file1_start, file1_end, file2_start, file2_end = ( + file2_start, + file2_end, + file1_start, + file1_end, + ) + assert file1_end - file1_start >= 120 + assert file2_end - file2_start >= 120 + assert file2_start >= file1_end # the jobs did NOT run concurrently + + @pytest.mark.e2e + @pytest.mark.no_containers + @pytest.mark.pit_server + @pytest.mark.rhel_ver_match('[^6].*') + @pytest.mark.skipif( + (not settings.robottelo.repos_hosting_url), reason='Missing repos_hosting_url' ) - assert 'Ansible role has been disassociated.' in result[0]['message'] + def test_positive_run_packages_and_services_job( + self, rhel_contenthost, module_sca_manifest_org, module_ak_with_cv, target_sat + ): + """Tests Ansible REX job can install packages and start services + + :id: 47ed82fb-77ca-43d6-a52e-f62bae5d3a42 + + :setup: + 1. Create a VM, register to SAT and configure REX (ssh-key) + + :steps: + 1. Run Ansible Package job for the host to install a package + 2. Check the package is present at the host + 3. Run Ansible Service job for the host to start a service + 4. Check the service is started on the host + + :expectedresults: multiple asserts along the code + + :bz: 1872688, 1811166 + + :customerscenario: true + + :parametrized: yes + """ + client = rhel_contenthost + packages = ['tapir'] + result = client.register( + module_sca_manifest_org, + None, + module_ak_with_cv.name, + target_sat, + repo=settings.repos.yum_3.url, + ) + assert result.status == 0, f'Failed to register host: {result.stderr}' + # install package + invocation_command = target_sat.cli_factory.job_invocation( + { + 'job-template': 'Package Action - Ansible Default', + 'inputs': 'state=latest, name={}'.format(*packages), + 'search-query': f'name ~ {client.hostname}', + } + ) + assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) + result = client.run(f'rpm -q {" ".join(packages)}') + assert result.status == 0 + + # stop a service + service = 'rsyslog' + invocation_command = target_sat.cli_factory.job_invocation( + { + 'job-template': 'Service Action - Ansible Default', + 'inputs': f'state=stopped, name={service}', + 'search-query': f"name ~ {client.hostname}", + } + ) + assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) + result = client.execute(f'systemctl status {service}') + assert result.status == 3 + + # start it again + invocation_command = target_sat.cli_factory.job_invocation( + { + 'job-template': 'Service Action - Ansible Default', + 'inputs': f'state=started, name={service}', + 'search-query': f'name ~ {client.hostname}', + } + ) + assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) + result = client.execute(f'systemctl status {service}') + assert result.status == 0 + + @pytest.mark.rhel_ver_list([8]) + def test_positive_install_ansible_collection(self, rex_contenthost, target_sat): + """Test whether Ansible collection can be installed via Ansible REX + + :id: ad25aee5-4ea3-4743-a301-1c6271856f79 + + :steps: + 1. Upload a manifest. + 2. Register content host to Satellite with REX setup + 3. Enable Ansible repo on content host. + 4. Install ansible or ansible-core package + 5. Run REX job to install Ansible collection on content host. + + :expectedresults: Ansible collection can be installed on content host via REX. + """ + client = rex_contenthost + # Enable Ansible repository and Install ansible or ansible-core package + client.create_custom_repos(rhel8_aps=settings.repos.rhel8_os.appstream) + assert client.execute('dnf -y install ansible-core').status == 0 + + collection_job = target_sat.cli_factory.job_invocation( + { + 'job-template': 'Ansible Collection - Install from Galaxy', + 'inputs': 'ansible_collections_list="oasis_roles.system"', + 'search-query': f'name ~ {client.hostname}', + } + ) + result = target_sat.cli.JobInvocation.info({'id': collection_job['id']}) + assert result['success'] == '1' + collection_path = client.execute('ls /etc/ansible/collections/ansible_collections').stdout + assert 'oasis_roles' in collection_path + + # Extend test with custom collections_path advanced input field + collection_job = target_sat.cli_factory.job_invocation( + { + 'job-template': 'Ansible Collection - Install from Galaxy', + 'inputs': 'ansible_collections_list="oasis_roles.system", collections_path="~/"', + 'search-query': f'name ~ {client.hostname}', + } + ) + result = target_sat.cli.JobInvocation.info({'id': collection_job['id']}) + assert result['success'] == '1' + collection_path = client.execute('ls ~/ansible_collections').stdout + assert 'oasis_roles' in collection_path diff --git a/tests/foreman/cli/test_remoteexecution.py b/tests/foreman/cli/test_remoteexecution.py index 1b3bca14dc0..6c2e2bab0a5 100644 --- a/tests/foreman/cli/test_remoteexecution.py +++ b/tests/foreman/cli/test_remoteexecution.py @@ -15,7 +15,6 @@ from datetime import datetime, timedelta from time import sleep -from broker import Broker from dateutil.relativedelta import FR, relativedelta from fauxfactory import gen_string import pytest @@ -23,29 +22,9 @@ from robottelo.cli.host import Host from robottelo.config import settings -from robottelo.constants import PRDS, REPOS, REPOSET -from robottelo.hosts import ContentHost from robottelo.utils import ohsnap -@pytest.fixture -def fixture_sca_vmsetup(request, module_sca_manifest_org, target_sat): - """Create VM and register content host to Simple Content Access organization""" - if '_count' in request.param: - with Broker( - nick=request.param['nick'], - host_class=ContentHost, - _count=request.param['_count'], - ) as clients: - for client in clients: - client.configure_rex(satellite=target_sat, org=module_sca_manifest_org) - yield clients - else: - with Broker(nick=request.param['nick'], host_class=ContentHost) as client: - client.configure_rex(satellite=target_sat, org=module_sca_manifest_org) - yield client - - @pytest.fixture def infra_host(request, target_sat, module_capsule_configured): infra_hosts = {'target_sat': target_sat, 'module_capsule_configured': module_capsule_configured} @@ -214,10 +193,9 @@ def test_positive_run_custom_job_template_by_ip(self, rex_contenthost, module_or @pytest.mark.tier3 @pytest.mark.upgrade - @pytest.mark.no_containers @pytest.mark.rhel_ver_list([8]) def test_positive_run_default_job_template_multiple_hosts_by_ip( - self, registered_hosts, module_target_sat + self, rex_contenthosts, module_target_sat ): """Run default job template against multiple hosts by ip @@ -227,7 +205,7 @@ def test_positive_run_default_job_template_multiple_hosts_by_ip( :parametrized: yes """ - clients = registered_hosts + clients = rex_contenthosts invocation_command = module_target_sat.cli_factory.job_invocation( { 'job-template': 'Run Command - Script Default', @@ -519,392 +497,6 @@ def test_recurring_with_unreachable_host(self, module_target_sat, rhel_contentho assert cli.JobInvocation.info({'id': invocation.id})['failed'] != '0' -class TestAnsibleREX: - """Test class for remote execution via Ansible""" - - @pytest.mark.tier3 - @pytest.mark.upgrade - @pytest.mark.pit_client - @pytest.mark.pit_server - @pytest.mark.rhel_ver_list([7, 8, 9]) - def test_positive_run_effective_user_job(self, rex_contenthost, target_sat): - """Tests Ansible REX job having effective user runs successfully - - :id: a5fa20d8-c2bd-4bbf-a6dc-bf307b59dd8c - - :steps: - - 0. Create a VM and register to SAT and prepare for REX (ssh key) - - 1. Run Ansible Command job for the host to create a user - - 2. Run Ansible Command job using effective user - - 3. Check the job result at the host is done under that user - - :expectedresults: multiple asserts along the code - - :CaseAutomation: Automated - - :parametrized: yes - """ - client = rex_contenthost - # create a user on client via remote job - username = gen_string('alpha') - filename = gen_string('alpha') - make_user_job = target_sat.cli_factory.job_invocation( - { - 'job-template': 'Run Command - Ansible Default', - 'inputs': f"command=useradd -m {username}", - 'search-query': f"name ~ {client.hostname}", - } - ) - assert_job_invocation_result(target_sat, make_user_job['id'], client.hostname) - # create a file as new user - invocation_command = target_sat.cli_factory.job_invocation( - { - 'job-template': 'Run Command - Ansible Default', - 'inputs': f"command=touch /home/{username}/{filename}", - 'search-query': f"name ~ {client.hostname}", - 'effective-user': f'{username}', - } - ) - assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) - # check the file owner - result = client.execute( - f'''stat -c '%U' /home/{username}/{filename}''', - ) - # assert the file is owned by the effective user - assert username == result.stdout.strip('\n'), "file ownership mismatch" - - @pytest.mark.tier3 - @pytest.mark.upgrade - @pytest.mark.rhel_ver_list([8]) - def test_positive_run_reccuring_job(self, rex_contenthost, target_sat): - """Tests Ansible REX reccuring job runs successfully multiple times - - :id: 49b0d31d-58f9-47f1-aa5d-561a1dcb0d66 - - :steps: - - 0. Create a VM and register to SAT and prepare for REX (ssh key) - - 1. Run recurring Ansible Command job for the host - - 2. Check the multiple job results at the host - - :expectedresults: multiple asserts along the code - - :CaseAutomation: Automated - - :customerscenario: true - - :bz: 2129432 - - :parametrized: yes - """ - client = rex_contenthost - invocation_command = target_sat.cli_factory.job_invocation( - { - 'job-template': 'Run Command - Ansible Default', - 'inputs': 'command=ls', - 'search-query': f"name ~ {client.hostname}", - 'cron-line': '* * * * *', # every minute - 'max-iteration': 2, # just two runs - } - ) - result = target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) - sleep(150) - rec_logic = target_sat.cli.RecurringLogic.info({'id': result['recurring-logic-id']}) - assert rec_logic['state'] == 'finished' - assert rec_logic['iteration'] == '2' - # 2129432 - rec_logic_keys = rec_logic.keys() - assert 'action' in rec_logic_keys - assert 'last-occurrence' in rec_logic_keys - assert 'next-occurrence' in rec_logic_keys - assert 'state' in rec_logic_keys - assert 'purpose' in rec_logic_keys - assert 'iteration' in rec_logic_keys - assert 'iteration-limit' in rec_logic_keys - - @pytest.mark.tier3 - @pytest.mark.no_containers - def test_positive_run_concurrent_jobs(self, registered_hosts, target_sat): - """Tests Ansible REX concurent jobs without batch trigger - - :id: ad0f108c-03f2-49c7-8732-b1056570567b - - :steps: - - 0. Create 2 hosts, disable foreman_tasks_proxy_batch_trigger - - 1. Run Ansible Command job with concurrency-setting - - :expectedresults: multiple asserts along the code - - :CaseAutomation: Automated - - :customerscenario: true - - :BZ: 1817320 - - :parametrized: yes - """ - param_name = 'foreman_tasks_proxy_batch_trigger' - target_sat.cli.GlobalParameter().set({'name': param_name, 'value': 'false'}) - clients = registered_hosts - output_msgs = [] - invocation_command = target_sat.cli_factory.job_invocation( - { - 'job-template': 'Run Command - Ansible Default', - 'inputs': 'command=ls', - 'search-query': f'name ~ {clients[0].hostname} or name ~ {clients[1].hostname}', - 'concurrency-level': 2, - } - ) - for vm in clients: - output_msgs.append( - 'host output from {}: {}'.format( - vm.hostname, - ' '.join( - target_sat.cli.JobInvocation.get_output( - {'id': invocation_command['id'], 'host': vm.hostname} - ) - ), - ) - ) - result = target_sat.cli.JobInvocation.info({'id': invocation_command['id']}) - assert result['success'] == '2', output_msgs - target_sat.cli.GlobalParameter().delete({'name': param_name}) - assert len(target_sat.cli.GlobalParameter().list({'search': param_name})) == 0 - - @pytest.mark.tier3 - @pytest.mark.no_containers - def test_positive_run_serial(self, registered_hosts, target_sat): - """Tests subtasks in a job run one by one when concurrency level set to 1 - - :id: 5ce39447-82d0-42df-81be-16ed3d67a2a4 - - :Setup: - 0. Create 2 hosts - - :steps: - - 0. Run a bash command job with concurrency level 1 - - :expectedresults: First subtask should run immediately, second one after the first one finishes - - :CaseAutomation: Automated - - :parametrized: yes - """ - hosts = registered_hosts - output_msgs = [] - template_file = f"/root/{gen_string('alpha')}.template" - target_sat.execute( - f"echo 'rm /root/test-<%= @host %>; echo $(date +%s) >> /root/test-<%= @host %>; sleep 120; echo $(date +%s) >> /root/test-<%= @host %>' > {template_file}" - ) - template = target_sat.cli.JobTemplate.create( - { - 'name': gen_string('alpha'), - 'file': template_file, - 'job-category': 'Commands', - 'provider-type': 'script', - } - ) - invocation = target_sat.cli_factory.job_invocation( - { - 'job-template': template['name'], - 'search-query': f'name ~ {hosts[0].hostname} or name ~ {hosts[1].hostname}', - 'concurrency-level': 1, - } - ) - for vm in hosts: - output_msgs.append( - 'host output from {}: {}'.format( - vm.hostname, - ' '.join( - target_sat.cli.JobInvocation.get_output( - {'id': invocation['id'], 'host': vm.hostname} - ) - ), - ) - ) - result = target_sat.cli.JobInvocation.info({'id': invocation['id']}) - assert result['success'] == '2', output_msgs - # assert for time diffs - file1 = hosts[0].execute('cat /root/test-$(hostname)').stdout - file2 = hosts[1].execute('cat /root/test-$(hostname)').stdout - file1_start, file1_end = map(int, file1.rstrip().split('\n')) - file2_start, file2_end = map(int, file2.rstrip().split('\n')) - if file1_start > file2_start: - file1_start, file1_end, file2_start, file2_end = ( - file2_start, - file2_end, - file1_start, - file1_end, - ) - assert file1_end - file1_start >= 120 - assert file2_end - file2_start >= 120 - assert file2_start >= file1_end # the jobs did NOT run concurrently - - @pytest.mark.tier3 - @pytest.mark.upgrade - @pytest.mark.e2e - @pytest.mark.no_containers - @pytest.mark.pit_server - @pytest.mark.rhel_ver_match('[^6].*') - @pytest.mark.skipif( - (not settings.robottelo.repos_hosting_url), reason='Missing repos_hosting_url' - ) - def test_positive_run_packages_and_services_job( - self, rhel_contenthost, module_org, module_ak_with_cv, target_sat - ): - """Tests Ansible REX job can install packages and start services - - :id: 47ed82fb-77ca-43d6-a52e-f62bae5d3a42 - - :steps: - - 0. Create a VM and register to SAT and prepare for REX (ssh key) - - 1. Run Ansible Package job for the host to install a package - - 2. Check the package is present at the host - - 3. Run Ansible Service job for the host to start a service - - 4. Check the service is started on the host - - :expectedresults: multiple asserts along the code - - :CaseAutomation: Automated - - :bz: 1872688, 1811166 - - :CaseImportance: Critical - - :customerscenario: true - - :parametrized: yes - """ - client = rhel_contenthost - packages = ['tapir'] - client.register( - module_org, - None, - module_ak_with_cv.name, - target_sat, - repo=settings.repos.yum_3.url, - ) - # install package - invocation_command = target_sat.cli_factory.job_invocation( - { - 'job-template': 'Package Action - Ansible Default', - 'inputs': 'state=latest, name={}'.format(*packages), - 'search-query': f'name ~ {client.hostname}', - } - ) - assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) - result = client.run(f'rpm -q {" ".join(packages)}') - assert result.status == 0 - - # stop a service - service = "rsyslog" - invocation_command = target_sat.cli_factory.job_invocation( - { - 'job-template': 'Service Action - Ansible Default', - 'inputs': f'state=stopped, name={service}', - 'search-query': f"name ~ {client.hostname}", - } - ) - assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) - result = client.execute(f'systemctl status {service}') - assert result.status == 3 - - # start it again - invocation_command = target_sat.cli_factory.job_invocation( - { - 'job-template': 'Service Action - Ansible Default', - 'inputs': f'state=started, name={service}', - 'search-query': f'name ~ {client.hostname}', - } - ) - assert_job_invocation_result(target_sat, invocation_command['id'], client.hostname) - result = client.execute(f'systemctl status {service}') - assert result.status == 0 - - @pytest.mark.tier3 - @pytest.mark.parametrize( - 'fixture_sca_vmsetup', [{'nick': 'rhel8'}], ids=['rhel8'], indirect=True - ) - def test_positive_install_ansible_collection( - self, fixture_sca_vmsetup, module_sca_manifest_org, target_sat - ): - """Test whether Ansible collection can be installed via REX - - :steps: - 1. Upload a manifest. - 2. Enable and sync Ansible repository. - 3. Register content host to Satellite. - 4. Enable Ansible repo on content host. - 5. Install ansible package. - 6. Run REX job to install Ansible collection on content host. - - :id: ad25aee5-4ea3-4743-a301-1c6271856f79 - - :CaseComponent: Ansible-RemoteExecution - - :Team: Rocket - """ - # Configure repository to prepare for installing ansible on host - target_sat.cli.RepositorySet.enable( - { - 'basearch': 'x86_64', - 'name': REPOSET['rhae2.9_el8'], - 'organization-id': module_sca_manifest_org.id, - 'product': PRDS['rhae'], - 'releasever': '8', - } - ) - target_sat.cli.Repository.synchronize( - { - 'name': REPOS['rhae2.9_el8']['name'], - 'organization-id': module_sca_manifest_org.id, - 'product': PRDS['rhae'], - } - ) - client = fixture_sca_vmsetup - client.execute('subscription-manager refresh') - client.execute(f'subscription-manager repos --enable {REPOS["rhae2.9_el8"]["id"]}') - client.execute('dnf -y install ansible') - collection_job = target_sat.cli_factory.job_invocation( - { - 'job-template': 'Ansible Collection - Install from Galaxy', - 'inputs': 'ansible_collections_list="oasis_roles.system"', - 'search-query': f'name ~ {client.hostname}', - } - ) - result = target_sat.cli.JobInvocation.info({'id': collection_job['id']}) - assert result['success'] == '1' - collection_path = client.execute('ls /etc/ansible/collections/ansible_collections').stdout - assert 'oasis_roles' in collection_path - - # Extend test with custom collections_path advanced input field - collection_job = target_sat.cli_factory.job_invocation( - { - 'job-template': 'Ansible Collection - Install from Galaxy', - 'inputs': 'ansible_collections_list="oasis_roles.system", collections_path="~/"', - 'search-query': f'name ~ {client.hostname}', - } - ) - result = target_sat.cli.JobInvocation.info({'id': collection_job['id']}) - assert result['success'] == '1' - collection_path = client.execute('ls ~/ansible_collections').stdout - assert 'oasis_roles' in collection_path - - class TestRexUsers: """Tests related to remote execution users""" From 64bc5a61e09661d81fa1bae9c402ceeec29bed25 Mon Sep 17 00:00:00 2001 From: David Moore <109112035+damoore044@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:07:19 -0400 Subject: [PATCH 575/586] [6.14.z] Repair setup for package w/ swidtag, install and modular update (#14625) Repair setup for package w/ swidtag, install and modular update --- tests/foreman/api/test_errata.py | 164 +++++++++++++++++-------------- 1 file changed, 89 insertions(+), 75 deletions(-) diff --git a/tests/foreman/api/test_errata.py b/tests/foreman/api/test_errata.py index 45ad3774a11..ab4e5dd0c1c 100644 --- a/tests/foreman/api/test_errata.py +++ b/tests/foreman/api/test_errata.py @@ -1328,15 +1328,21 @@ def _validate_swid_tags_installed(vm, module_name): assert module_name in result +@pytest.fixture +def errata_host_lce(module_sca_manifest_org, target_sat): + """Create and return a new lce in module SCA org.""" + return target_sat.api.LifecycleEnvironment(organization=module_sca_manifest_org).create() + + @pytest.mark.tier3 @pytest.mark.upgrade @pytest.mark.pit_client @pytest.mark.no_containers +@pytest.mark.rhel_ver_match('8') def test_errata_installation_with_swidtags( module_sca_manifest_org, - module_lce, - module_cv, - rhel8_contenthost, + rhel_contenthost, + errata_host_lce, target_sat, ): """Verify errata installation with swid_tags and swid tags get updated after @@ -1344,23 +1350,25 @@ def test_errata_installation_with_swidtags( :id: 43a59b9a-eb9b-4174-8b8e-73d923b1e51e + :setup: + + 1. rhel8 contenthost checked out, using org with simple content access. + 2. create satellite repositories having rhel8 baseOS, prereqs, custom content w/ swid tags. + 3. associate repositories to org, lifecycle environment, and cv. Sync all content. + 4. publish & promote to environment, content view version with all content. + 5. create activation key, for registering host to cv. + :steps: - 1. promote empty content view and create activation key - 2. create product and repository having swid tags - 3. create rhel8, swid repos on content host - 4. create custom repo with applicable module stream packages - 5. associate repositories and cv / ak to contenthost, sync all content - 6. publish & promote content view version with all content - 7. register host using cv's activation key - 8. install swid-tools, dnf-plugin-swidtags packages on content host - 9. install older module stream and generate errata, swid tag - 10. assert errata count, swid tags are generated - 11. install errata via updating module stream - 12. assert errata count and swid tag changed after module update - - :expectedresults: swid tags should get updated after errata installation via - module stream update + 1. register host using cv's activation key, assert succeeded. + 2. install swid-tools, dnf-plugin-swidtags packages on content host. + 3. install older module stream and generate errata, swid tag. + 4. assert errata count, swid tags are generated. + 5. install errata via updating module stream. + 6. assert errata count and swid tag changed after module update. + + :expectedresults: + swid tags should get updated after errata installation via module stream update :CaseAutomation: Automated @@ -1371,94 +1379,100 @@ def test_errata_installation_with_swidtags( """ module_name = 'kangaroo' version = '20180704111719' - # new cv version for ak - cv_publish_promote( - target_sat, - module_sca_manifest_org, - module_cv, - module_lce, - ) - _ak = target_sat.api.ActivationKey( - organization=module_sca_manifest_org, - environment=module_lce, - content_view=module_cv, + org = module_sca_manifest_org + lce = errata_host_lce + cv = target_sat.api.ContentView( + organization=org, + environment=[lce], ).create() - # needed repos for module stream, swid tags, prereqs - _repos = { - 'base_os': settings.repos.rhel8_os.baseos, - 'sat_tools': settings.repos.rhel8_os.appstream, - 'swid_tags': settings.repos.swid_tag.url, + + repos = { + 'base_os': settings.repos.rhel8_os.baseos, # base rhel8 + 'sat_tools': settings.repos.rhel8_os.appstream, # swid prereqs + 'swid_tags': settings.repos.swid_tag.url, # module stream pkgs and errata } - # associate repos with host, org, lce, and sync - rhel8_contenthost.create_custom_repos(**_repos) - for _key in _repos: + # associate repos with sat, org, lce, cv, and sync + for r in repos: target_sat.cli_factory.setup_org_for_a_custom_repo( { - 'url': _repos[_key], - 'name': _key, - 'organization-id': module_sca_manifest_org.id, - 'lifecycle-environment-id': module_lce.id, - 'activationkey-id': _ak.id, + 'url': repos[r], + 'organization-id': org.id, + 'lifecycle-environment-id': lce.id, + 'content-view-id': cv.id, }, ) - # promote new version with all repos/content - cv_publish_promote( - target_sat, - module_sca_manifest_org, - module_cv, - module_lce, - ) + # promote newest cv version with all repos/content + cv = cv_publish_promote( + sat=target_sat, + org=org, + cv=cv, + lce=lce, + )['content-view'] + # ak in env, tied to content-view + ak = target_sat.api.ActivationKey( + organization=org, + environment=lce, + content_view=cv, + ).create() # register host with ak, succeeds - result = rhel8_contenthost.register( - activation_keys=_ak.name, + result = rhel_contenthost.register( + activation_keys=ak.name, target=target_sat, - org=module_sca_manifest_org, + org=org, loc=None, ) assert result.status == 0, f'Failed to register the host {target_sat.hostname},\n{result}' - rhel8_contenthost.add_rex_key(satellite=target_sat) assert ( - rhel8_contenthost.subscribed + rhel_contenthost.subscribed ), f'Failed to subscribe the host {target_sat.hostname}, to content.' - rhel8_contenthost.install_katello_ca(target_sat) - result = rhel8_contenthost.execute(r'subscription-manager repos --enable \*') + result = rhel_contenthost.execute(r'subscription-manager repos --enable \*') assert result.status == 0, f'Failed to enable repositories with subscription-manager,\n{result}' # install outdated module stream package - _set_prerequisites_for_swid_repos(rhel8_contenthost) - result = rhel8_contenthost.execute(f'dnf -y module install {module_name}:0:{version}') + _set_prerequisites_for_swid_repos(rhel_contenthost) + result = rhel_contenthost.execute(f'dnf -y module install {module_name}:0:{version}') assert ( result.status == 0 ), f'Failed to install module stream package: {module_name}:0:{version}.\n{result.stdout}' # recalculate errata after install of old module stream - rhel8_contenthost.execute('subscription-manager repos') + rhel_contenthost.execute('subscription-manager repos') # validate swid tags Installed - before_errata_apply_result = _run_remote_command_on_content_host( - f"swidq -i -n {module_name} | grep 'File' | grep -o 'rpm-.*.swidtag'", - rhel8_contenthost, - return_result=True, + result = rhel_contenthost.execute( + f'swidq -i -n {module_name} | grep "File" | grep -o "rpm-.*.swidtag"', + ) + assert ( + result.status == 0 + ), f'An error occured trying to fetch swid tags for {module_name}.\n{result}' + before_errata_apply_result = result.stdout + assert before_errata_apply_result != '', f'Found no swid tags contained in {module_name}.' + assert (app_errata_count := rhel_contenthost.applicable_errata_count) == 1, ( + f'Found {rhel_contenthost.applicable_errata_count} applicable errata,' + f' after installing {module_name}:0:{version}, expected 1.' ) - assert before_errata_apply_result != '' - assert (applicable_errata_count := rhel8_contenthost.applicable_errata_count) == 1 # apply modular errata - result = rhel8_contenthost.execute(f'dnf -y module update {module_name}') + result = rhel_contenthost.execute(f'dnf -y module update {module_name}') assert ( result.status == 0 - ), f'Failed to updated module stream package: {module_name}.\n{result.stdout}' - assert rhel8_contenthost.execute('dnf -y upload-profile').status == 0 + ), f'Failed to update module stream package: {module_name}.\n{result.stdout}' + assert rhel_contenthost.execute('dnf -y upload-profile').status == 0 # recalculate and check errata after modular update - rhel8_contenthost.execute('subscription-manager repos') - applicable_errata_count -= 1 - assert rhel8_contenthost.applicable_errata_count == applicable_errata_count + rhel_contenthost.execute('subscription-manager repos') + app_errata_count -= 1 + assert rhel_contenthost.applicable_errata_count == app_errata_count, ( + f'Found {rhel_contenthost.applicable_errata_count} applicable errata, after modular update of {module_name},' + f' expected {app_errata_count}.' + ) # swidtags were updated based on package version - after_errata_apply_result = _run_remote_command_on_content_host( - f"swidq -i -n {module_name} | grep 'File' | grep -o 'rpm-.*.swidtag'", - rhel8_contenthost, - return_result=True, + result = rhel_contenthost.execute( + f'swidq -i -n {module_name} | grep "File" | grep -o "rpm-.*.swidtag"', ) + assert ( + result.status == 0 + ), f'An error occured trying to fetch swid tags for {module_name}.\n{result}' + after_errata_apply_result = result.stdout assert before_errata_apply_result != after_errata_apply_result From e6933b3ebd674e788faa18bc44c4548e3709c8cc Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 5 Apr 2024 05:47:25 -0400 Subject: [PATCH 576/586] [6.14.z] Move ansible API tests to Ansible-Cfgmgmt and REX classes (#14634) --- tests/foreman/api/test_ansible.py | 753 +++++++++++++++--------------- 1 file changed, 382 insertions(+), 371 deletions(-) diff --git a/tests/foreman/api/test_ansible.py b/tests/foreman/api/test_ansible.py index 04b25f2484b..435501d0696 100644 --- a/tests/foreman/api/test_ansible.py +++ b/tests/foreman/api/test_ansible.py @@ -51,163 +51,313 @@ def filtered_user(target_sat, module_org, module_location): return user, password -@pytest.fixture -def rex_host_in_org_and_loc(target_sat, module_org, module_location, rex_contenthost): - host = target_sat.api.Host().search(query={'search': f'name={rex_contenthost.hostname}'})[0] - target_sat.api.Host(id=host.id, organization=[module_org.id]).update(['organization']) - target_sat.api.Host(id=host.id, location=module_location.id).update(['location']) - return host +@pytest.mark.upgrade +class TestAnsibleCfgMgmt: + """Test class for Configuration Management with Ansible + :CaseComponent: Ansible-ConfigurationManagement -@pytest.mark.e2e -def test_fetch_and_sync_ansible_playbooks(target_sat): """ - Test Ansible Playbooks api for fetching and syncing playbooks - - :id: 17b4e767-1494-4960-bc60-f31a0495c09f - :customerscenario: true + @pytest.mark.e2e + def test_fetch_and_sync_ansible_playbooks(self, target_sat): + """ + Test Ansible Playbooks api for fetching and syncing playbooks - :steps: + :id: 17b4e767-1494-4960-bc60-f31a0495c09f - 1. Install ansible collection with playbooks. - 2. Try to fetch the playbooks via api. - 3. Sync the playbooks. - 4. Assert the count of playbooks fetched and synced are equal. + :steps: + 1. Install ansible collection with playbooks. + 2. Try to fetch the playbooks via api. + 3. Sync the playbooks. + 4. Assert the count of playbooks fetched and synced are equal. - :expectedresults: - 1. Playbooks should be fetched and synced successfully. + :expectedresults: + 1. Playbooks should be fetched and synced successfully. - :BZ: 2115686 - """ - target_sat.execute( - "ansible-galaxy collection install -p /usr/share/ansible/collections " - "xprazak2.forklift_collection" - ) - proxy_id = target_sat.nailgun_smart_proxy.id - playbook_fetch = target_sat.api.AnsiblePlaybooks().fetch(data={'proxy_id': proxy_id}) - playbooks_count = len(playbook_fetch['results']['playbooks_names']) - playbook_sync = target_sat.api.AnsiblePlaybooks().sync(data={'proxy_id': proxy_id}) - assert playbook_sync['action'] == "Sync playbooks" - - target_sat.wait_for_tasks( - search_query=(f'id = {playbook_sync["id"]}'), - poll_timeout=100, - ) - task_details = target_sat.api.ForemanTask().search( - query={'search': f'id = {playbook_sync["id"]}'} - ) - assert task_details[0].result == 'success' - assert len(task_details[0].output['result']['created']) == playbooks_count - - -@pytest.mark.e2e -@pytest.mark.no_containers -@pytest.mark.rhel_ver_match('[^6].*') -def test_positive_ansible_job_on_host( - target_sat, module_org, module_location, module_ak_with_synced_repo, rhel_contenthost -): - """ - Test successful execution of Ansible Job on host. + :BZ: 2115686 - :id: c8dcdc54-cb98-4b24-bff9-049a6cc36acb - - :steps: - 1. Register a content host with satellite - 2. Import a role into satellite - 3. Assign that role to a host - 4. Assert that the role was assigned to the host successfully - 5. Run the Ansible playbook associated with that role - 6. Check if the job is executed. - - :expectedresults: - 1. Host should be assigned the proper role. - 2. Job execution must be successful. - - :BZ: 2164400 - - :CaseComponent: Ansible-RemoteExecution - """ - SELECTED_ROLE = 'RedHatInsights.insights-client' - if rhel_contenthost.os_version.major <= 7: - rhel_contenthost.create_custom_repos(rhel7=settings.repos.rhel7_os) - assert rhel_contenthost.execute('yum install -y insights-client').status == 0 - result = rhel_contenthost.register( - module_org, module_location, module_ak_with_synced_repo.name, target_sat - ) - assert result.status == 0, f'Failed to register host: {result.stderr}' - proxy_id = target_sat.nailgun_smart_proxy.id - target_host = rhel_contenthost.nailgun_host - target_sat.api.AnsibleRoles().sync(data={'proxy_id': proxy_id, 'role_names': [SELECTED_ROLE]}) - role_id = target_sat.api.AnsibleRoles().search(query={'search': f'name={SELECTED_ROLE}'})[0].id - target_sat.api.Host(id=target_host.id).add_ansible_role(data={'ansible_role_id': role_id}) - host_roles = target_host.list_ansible_roles() - assert host_roles[0]['name'] == SELECTED_ROLE - assert target_host.name == rhel_contenthost.hostname - - template_id = ( - target_sat.api.JobTemplate() - .search(query={'search': 'name="Ansible Roles - Ansible Default"'})[0] - .id - ) - job = target_sat.api.JobInvocation().run( - synchronous=False, - data={ - 'job_template_id': template_id, - 'targeting_type': 'static_query', - 'search_query': f'name = {rhel_contenthost.hostname}', - }, - ) - target_sat.wait_for_tasks( - f'resource_type = JobInvocation and resource_id = {job["id"]}', poll_timeout=1000 - ) - result = target_sat.api.JobInvocation(id=job['id']).read() - assert result.succeeded == 1 - target_sat.api.Host(id=target_host.id).remove_ansible_role(data={'ansible_role_id': role_id}) - host_roles = target_host.list_ansible_roles() - assert len(host_roles) == 0 + :customerscenario: true + """ + target_sat.execute( + "ansible-galaxy collection install -p /usr/share/ansible/collections " + "xprazak2.forklift_collection" + ) + proxy_id = target_sat.nailgun_smart_proxy.id + playbook_fetch = target_sat.api.AnsiblePlaybooks().fetch(data={'proxy_id': proxy_id}) + playbooks_count = len(playbook_fetch['results']['playbooks_names']) + playbook_sync = target_sat.api.AnsiblePlaybooks().sync(data={'proxy_id': proxy_id}) + assert playbook_sync['action'] == "Sync playbooks" + + target_sat.wait_for_tasks( + search_query=(f'id = {playbook_sync["id"]}'), + poll_timeout=100, + ) + task_details = target_sat.api.ForemanTask().search( + query={'search': f'id = {playbook_sync["id"]}'} + ) + assert task_details[0].result == 'success' + assert len(task_details[0].output['result']['created']) == playbooks_count + + @pytest.mark.e2e + @pytest.mark.tier2 + def test_add_and_remove_ansible_role_hostgroup(self, target_sat): + """ + Test add and remove functionality for ansible roles in hostgroup via API + + :id: 7672cf86-fa31-11ed-855a-0fd307d2d66b + + :steps: + 1. Create a hostgroup and a nested hostgroup + 2. Sync a few ansible roles + 3. Assign a few ansible roles with the host group + 4. Add some ansible role with the host group + 5. Add some ansible roles to the nested hostgroup + 6. Remove the added ansible roles from the parent and nested hostgroup + + :expectedresults: + 1. Ansible role assign/add/remove functionality should work as expected in API + + :BZ: 2164400 + """ + ROLE_NAMES = [ + 'theforeman.foreman_scap_client', + 'redhat.satellite.hostgroups', + 'RedHatInsights.insights-client', + 'redhat.satellite.compute_resources', + ] + hg = target_sat.api.HostGroup(name=gen_string('alpha')).create() + hg_nested = target_sat.api.HostGroup(name=gen_string('alpha'), parent=hg).create() + proxy_id = target_sat.nailgun_smart_proxy.id + target_sat.api.AnsibleRoles().sync(data={'proxy_id': proxy_id, 'role_names': ROLE_NAMES}) + ROLES = [ + target_sat.api.AnsibleRoles().search(query={'search': f'name={role}'})[0].id + for role in ROLE_NAMES + ] + # Assign first 2 roles to HG and verify it + target_sat.api.HostGroup(id=hg.id).assign_ansible_roles( + data={'ansible_role_ids': ROLES[:2]} + ) + for r1, r2 in zip( + target_sat.api.HostGroup(id=hg.id).list_ansible_roles(), ROLE_NAMES[:2], strict=True + ): + assert r1['name'] == r2 + + # Add next role from list to HG and verify it + target_sat.api.HostGroup(id=hg.id).add_ansible_role(data={'ansible_role_id': ROLES[2]}) + for r1, r2 in zip( + target_sat.api.HostGroup(id=hg.id).list_ansible_roles(), ROLE_NAMES[:3], strict=True + ): + assert r1['name'] == r2 + + # Add next role to nested HG, and verify roles are also nested to HG along with assigned role + # Also, ensure the parent HG does not contain the roles assigned to nested HGs + target_sat.api.HostGroup(id=hg_nested.id).add_ansible_role( + data={'ansible_role_id': ROLES[3]} + ) + for r1, r2 in zip( + target_sat.api.HostGroup(id=hg_nested.id).list_ansible_roles(), + [ROLE_NAMES[-1]] + ROLE_NAMES[:-1], + strict=True, + ): + assert r1['name'] == r2 + + for r1, r2 in zip( + target_sat.api.HostGroup(id=hg.id).list_ansible_roles(), ROLE_NAMES[:3], strict=True + ): + assert r1['name'] == r2 + + # Remove roles assigned one by one from HG and nested HG + for role in ROLES[:3]: + target_sat.api.HostGroup(id=hg.id).remove_ansible_role(data={'ansible_role_id': role}) + hg_roles = target_sat.api.HostGroup(id=hg.id).list_ansible_roles() + assert len(hg_roles) == 0 + + for role in ROLES: + target_sat.api.HostGroup(id=hg_nested.id).remove_ansible_role( + data={'ansible_role_id': role} + ) + hg_nested_roles = target_sat.api.HostGroup(id=hg_nested.id).list_ansible_roles() + assert len(hg_nested_roles) == 0 + + @pytest.mark.e2e + @pytest.mark.tier2 + def test_positive_ansible_roles_inherited_from_hostgroup( + self, request, target_sat, module_org, module_location + ): + """Verify ansible roles inheritance functionality for host with parent/nested hostgroup via API + :id: 7672cf86-fa31-11ed-855a-0fd307d2d66g -@pytest.mark.no_containers -def test_positive_ansible_job_on_multiple_host( - target_sat, - module_org, - rhel9_contenthost, - rhel8_contenthost, - rhel7_contenthost, - module_location, - module_ak_with_synced_repo, -): - """Test execution of Ansible job on multiple hosts simultaneously. + :steps: + 1. Create a host, hostgroup and nested hostgroup + 2. Sync a few ansible roles + 3. Assign a few ansible roles to the host, hostgroup, nested hostgroup and verify it. + 4. Update host to be in parent/nested hostgroup and verify roles assigned - :id: 9369feef-466c-40d3-9d0d-65520d7f21ef + :expectedresults: + 1. Hosts in parent/nested hostgroups must have direct and indirect roles correctly assigned. - :customerscenario: true + :BZ: 2187967 - :steps: - 1. Register multiple content hosts with satellite - 2. Import a role into satellite - 3. Assign that role to all host - 4. Trigger ansible job keeping all host in a single query - 5. Check the passing and failing of individual hosts - 6. Check if one of the job on a host is failed resulting into whole job is marked as failed. + :customerscenario: true + """ + ROLE_NAMES = [ + 'theforeman.foreman_scap_client', + 'RedHatInsights.insights-client', + 'redhat.satellite.compute_resources', + ] + proxy_id = target_sat.nailgun_smart_proxy.id + host = target_sat.api.Host(organization=module_org, location=module_location).create() + hg = target_sat.api.HostGroup(name=gen_string('alpha'), organization=[module_org]).create() + hg_nested = target_sat.api.HostGroup( + name=gen_string('alpha'), parent=hg, organization=[module_org] + ).create() + + @request.addfinalizer + def _finalize(): + host.delete() + hg_nested.delete() + hg.delete() + + target_sat.api.AnsibleRoles().sync(data={'proxy_id': proxy_id, 'role_names': ROLE_NAMES}) + ROLES = [ + target_sat.api.AnsibleRoles().search(query={'search': f'name={role}'})[0].id + for role in ROLE_NAMES + ] + + # Assign roles to Host/Hostgroup/Nested Hostgroup and verify it + target_sat.api.Host(id=host.id).add_ansible_role(data={'ansible_role_id': ROLES[0]}) + assert ROLE_NAMES[0] == target_sat.api.Host(id=host.id).list_ansible_roles()[0]['name'] + + target_sat.api.HostGroup(id=hg.id).add_ansible_role(data={'ansible_role_id': ROLES[1]}) + assert ROLE_NAMES[1] == target_sat.api.HostGroup(id=hg.id).list_ansible_roles()[0]['name'] + + target_sat.api.HostGroup(id=hg_nested.id).add_ansible_role( + data={'ansible_role_id': ROLES[2]} + ) + listroles = target_sat.api.HostGroup(id=hg_nested.id).list_ansible_roles() + assert ROLE_NAMES[2] == listroles[0]['name'] + assert listroles[0]['directly_assigned'] + assert ROLE_NAMES[1] == listroles[1]['name'] + assert not listroles[1]['directly_assigned'] + + # Update host to be in nested hostgroup and verify roles assigned + host.hostgroup = hg_nested + host = host.update(['hostgroup']) + listroles_host = target_sat.api.Host(id=host.id).list_ansible_roles() + assert ROLE_NAMES[0] == listroles_host[0]['name'] + assert listroles_host[0]['directly_assigned'] + assert ROLE_NAMES[1] == listroles_host[1]['name'] + assert not listroles_host[1]['directly_assigned'] + assert ROLE_NAMES[2] == listroles_host[2]['name'] + assert not listroles_host[1]['directly_assigned'] + # Verify nested hostgroup doesn't contains the roles assigned to host + listroles_nested_hg = target_sat.api.HostGroup(id=hg_nested.id).list_ansible_roles() + assert ROLE_NAMES[0] not in [role['name'] for role in listroles_nested_hg] + assert ROLE_NAMES[2] == listroles_nested_hg[0]['name'] + assert ROLE_NAMES[1] == listroles_nested_hg[1]['name'] + + # Update host to be in parent hostgroup and verify roles assigned + host.hostgroup = hg + host = host.update(['hostgroup']) + listroles = target_sat.api.Host(id=host.id).list_ansible_roles() + assert ROLE_NAMES[0] == listroles[0]['name'] + assert listroles[0]['directly_assigned'] + assert ROLE_NAMES[1] == listroles[1]['name'] + assert not listroles[1]['directly_assigned'] + # Verify parent hostgroup doesn't contains the roles assigned to host + listroles_hg = target_sat.api.HostGroup(id=hg.id).list_ansible_roles() + assert ROLE_NAMES[0] not in [role['name'] for role in listroles_hg] + assert ROLE_NAMES[1] == listroles_hg[0]['name'] + + @pytest.mark.rhel_ver_match('[78]') + @pytest.mark.tier2 + def test_positive_read_facts_with_filter( + self, target_sat, rex_contenthost, filtered_user, module_org, module_location + ): + """Read host's Ansible facts as a user with a role that has host filter + + :id: 483d5faf-7a4c-4cb7-b14f-369768ad99b0 + + :steps: + 1. Run Ansible roles on a host + 2. Using API, read Ansible facts of that host + + :expectedresults: Ansible facts returned + + :BZ: 1699188 + + :customerscenario: true + """ + user, password = filtered_user + host = rex_contenthost.nailgun_host + host.organization = module_org + host.location = module_location + host.update(['organization', 'location']) + + # gather ansible facts by running ansible roles on the host + host.play_ansible_roles() + if is_open('BZ:2216471'): + wait_for( + lambda: len(rex_contenthost.nailgun_host.get_facts()) > 0, + timeout=30, + delay=2, + ) + user_cfg = user_nailgun_config(user.login, password) + # get facts through API + user_facts = ( + target_sat.api.Host(server_config=user_cfg) + .search(query={'search': f'name={rex_contenthost.hostname}'})[0] + .get_facts() + ) + assert 'subtotal' in user_facts + assert user_facts['subtotal'] == 1 + assert 'results' in user_facts + assert rex_contenthost.hostname in user_facts['results'] + assert len(user_facts['results'][rex_contenthost.hostname]) > 0 - :expectedresults: - 1. One of the jobs failing on a single host must impact the overall result as failed. - :BZ: 2167396, 2190464, 2184117 +class TestAnsibleREX: + """Test class for remote execution via Ansible :CaseComponent: Ansible-RemoteExecution """ - hosts = [rhel9_contenthost, rhel8_contenthost, rhel7_contenthost] - SELECTED_ROLE = 'RedHatInsights.insights-client' - for host in hosts: - result = host.register( + + @pytest.mark.e2e + @pytest.mark.no_containers + @pytest.mark.rhel_ver_match('[^6].*') + def test_positive_ansible_job_on_host( + self, target_sat, module_org, module_location, module_ak_with_synced_repo, rhel_contenthost + ): + """Test successful execution of Ansible Job on host. + + :id: c8dcdc54-cb98-4b24-bff9-049a6cc36acb + + :steps: + 1. Register a content host with satellite + 2. Import a role into satellite + 3. Assign that role to a host + 4. Assert that the role was assigned to the host successfully + 5. Run the Ansible playbook associated with that role + 6. Check if the job is executed. + + :expectedresults: + 1. Host should be assigned the proper role. + 2. Job execution must be successful. + + :BZ: 2164400 + """ + SELECTED_ROLE = 'RedHatInsights.insights-client' + if rhel_contenthost.os_version.major <= 7: + rhel_contenthost.create_custom_repos(rhel7=settings.repos.rhel7_os) + assert rhel_contenthost.execute('yum install -y insights-client').status == 0 + result = rhel_contenthost.register( module_org, module_location, module_ak_with_synced_repo.name, target_sat ) assert result.status == 0, f'Failed to register host: {result.stderr}' proxy_id = target_sat.nailgun_smart_proxy.id - target_host = host.nailgun_host + target_host = rhel_contenthost.nailgun_host target_sat.api.AnsibleRoles().sync( data={'proxy_id': proxy_id, 'role_names': [SELECTED_ROLE]} ) @@ -217,244 +367,105 @@ def test_positive_ansible_job_on_multiple_host( target_sat.api.Host(id=target_host.id).add_ansible_role(data={'ansible_role_id': role_id}) host_roles = target_host.list_ansible_roles() assert host_roles[0]['name'] == SELECTED_ROLE + assert target_host.name == rhel_contenthost.hostname - template_id = ( - target_sat.api.JobTemplate() - .search(query={'search': 'name="Ansible Roles - Ansible Default"'})[0] - .id - ) - job = target_sat.api.JobInvocation().run( - synchronous=False, - data={ - 'job_template_id': template_id, - 'targeting_type': 'static_query', - 'search_query': f'name ^ ({hosts[0].hostname} && {hosts[1].hostname} ' - f'&& {hosts[2].hostname})', - }, - ) - target_sat.wait_for_tasks( - f'resource_type = JobInvocation and resource_id = {job["id"]}', - poll_timeout=1000, - must_succeed=False, - ) - result = target_sat.api.JobInvocation(id=job['id']).read() - assert result.succeeded == 2 # SELECTED_ROLE working on rhel8/rhel9 clients - assert result.failed == 1 # SELECTED_ROLE failing on rhel7 client - assert result.status_label == 'failed' - - -@pytest.mark.e2e -@pytest.mark.tier2 -@pytest.mark.upgrade -def test_add_and_remove_ansible_role_hostgroup(target_sat): - """ - Test add and remove functionality for ansible roles in hostgroup via API - - :id: 7672cf86-fa31-11ed-855a-0fd307d2d66b - - :steps: - 1. Create a hostgroup and a nested hostgroup - 2. Sync a few ansible roles - 3. Assign a few ansible roles with the host group - 4. Add some ansible role with the host group - 5. Add some ansible roles to the nested hostgroup - 6. Remove the added ansible roles from the parent and nested hostgroup - - :expectedresults: - 1. Ansible role assign/add/remove functionality should work as expected in API - - :BZ: 2164400 - """ - ROLE_NAMES = [ - 'theforeman.foreman_scap_client', - 'redhat.satellite.hostgroups', - 'RedHatInsights.insights-client', - 'redhat.satellite.compute_resources', - ] - hg = target_sat.api.HostGroup(name=gen_string('alpha')).create() - hg_nested = target_sat.api.HostGroup(name=gen_string('alpha'), parent=hg).create() - proxy_id = target_sat.nailgun_smart_proxy.id - target_sat.api.AnsibleRoles().sync(data={'proxy_id': proxy_id, 'role_names': ROLE_NAMES}) - ROLES = [ - target_sat.api.AnsibleRoles().search(query={'search': f'name={role}'})[0].id - for role in ROLE_NAMES - ] - # Assign first 2 roles to HG and verify it - target_sat.api.HostGroup(id=hg.id).assign_ansible_roles(data={'ansible_role_ids': ROLES[:2]}) - for r1, r2 in zip( - target_sat.api.HostGroup(id=hg.id).list_ansible_roles(), ROLE_NAMES[:2], strict=True - ): - assert r1['name'] == r2 - - # Add next role from list to HG and verify it - target_sat.api.HostGroup(id=hg.id).add_ansible_role(data={'ansible_role_id': ROLES[2]}) - for r1, r2 in zip( - target_sat.api.HostGroup(id=hg.id).list_ansible_roles(), ROLE_NAMES[:3], strict=True - ): - assert r1['name'] == r2 - - # Add next role to nested HG, and verify roles are also nested to HG along with assigned role - # Also, ensure the parent HG does not contain the roles assigned to nested HGs - target_sat.api.HostGroup(id=hg_nested.id).add_ansible_role(data={'ansible_role_id': ROLES[3]}) - for r1, r2 in zip( - target_sat.api.HostGroup(id=hg_nested.id).list_ansible_roles(), - [ROLE_NAMES[-1]] + ROLE_NAMES[:-1], - strict=True, - ): - assert r1['name'] == r2 - - for r1, r2 in zip( - target_sat.api.HostGroup(id=hg.id).list_ansible_roles(), ROLE_NAMES[:3], strict=True + template_id = ( + target_sat.api.JobTemplate() + .search(query={'search': 'name="Ansible Roles - Ansible Default"'})[0] + .id + ) + job = target_sat.api.JobInvocation().run( + synchronous=False, + data={ + 'job_template_id': template_id, + 'targeting_type': 'static_query', + 'search_query': f'name = {rhel_contenthost.hostname}', + }, + ) + target_sat.wait_for_tasks( + f'resource_type = JobInvocation and resource_id = {job["id"]}', poll_timeout=1000 + ) + result = target_sat.api.JobInvocation(id=job['id']).read() + assert result.succeeded == 1 + target_sat.api.Host(id=target_host.id).remove_ansible_role( + data={'ansible_role_id': role_id} + ) + host_roles = target_host.list_ansible_roles() + assert len(host_roles) == 0 + + @pytest.mark.no_containers + def test_positive_ansible_job_on_multiple_host( + self, + target_sat, + module_org, + rhel9_contenthost, + rhel8_contenthost, + rhel7_contenthost, + module_location, + module_ak_with_synced_repo, ): - assert r1['name'] == r2 - - # Remove roles assigned one by one from HG and nested HG - for role in ROLES[:3]: - target_sat.api.HostGroup(id=hg.id).remove_ansible_role(data={'ansible_role_id': role}) - hg_roles = target_sat.api.HostGroup(id=hg.id).list_ansible_roles() - assert len(hg_roles) == 0 - - for role in ROLES: - target_sat.api.HostGroup(id=hg_nested.id).remove_ansible_role( - data={'ansible_role_id': role} + """Test execution of Ansible job on multiple hosts simultaneously. + + :id: 9369feef-466c-40d3-9d0d-65520d7f21ef + + :customerscenario: true + + :steps: + 1. Register multiple content hosts with satellite + 2. Import a role into satellite + 3. Assign that role to all host + 4. Trigger ansible job keeping all host in a single query + 5. Check the passing and failing of individual hosts + 6. Check if one of the job on a host is failed resulting into whole job is marked as failed. + + :expectedresults: + 1. One of the jobs failing on a single host must impact the overall result as failed. + + :BZ: 2167396, 2190464, 2184117 + """ + hosts = [rhel9_contenthost, rhel8_contenthost, rhel7_contenthost] + SELECTED_ROLE = 'RedHatInsights.insights-client' + for host in hosts: + result = host.register( + module_org, module_location, module_ak_with_synced_repo.name, target_sat + ) + assert result.status == 0, f'Failed to register host: {result.stderr}' + proxy_id = target_sat.nailgun_smart_proxy.id + target_host = host.nailgun_host + target_sat.api.AnsibleRoles().sync( + data={'proxy_id': proxy_id, 'role_names': [SELECTED_ROLE]} + ) + role_id = ( + target_sat.api.AnsibleRoles() + .search(query={'search': f'name={SELECTED_ROLE}'})[0] + .id + ) + target_sat.api.Host(id=target_host.id).add_ansible_role( + data={'ansible_role_id': role_id} + ) + host_roles = target_host.list_ansible_roles() + assert host_roles[0]['name'] == SELECTED_ROLE + + template_id = ( + target_sat.api.JobTemplate() + .search(query={'search': 'name="Ansible Roles - Ansible Default"'})[0] + .id ) - hg_nested_roles = target_sat.api.HostGroup(id=hg_nested.id).list_ansible_roles() - assert len(hg_nested_roles) == 0 - - -@pytest.mark.e2e -@pytest.mark.tier2 -@pytest.mark.upgrade -def test_positive_ansible_roles_inherited_from_hostgroup( - request, target_sat, module_org, module_location -): - """Verify ansible roles inheritance functionality for host with parent/nested hostgroup via API - - :id: 7672cf86-fa31-11ed-855a-0fd307d2d66g - - :steps: - 1. Create a host, hostgroup and nested hostgroup - 2. Sync a few ansible roles - 3. Assign a few ansible roles to the host, hostgroup, nested hostgroup and verify it. - 4. Update host to be in parent/nested hostgroup and verify roles assigned - - :expectedresults: - 1. Hosts in parent/nested hostgroups must have direct and indirect roles correctly assigned. - - :BZ: 2187967 - - :customerscenario: true - """ - ROLE_NAMES = [ - 'theforeman.foreman_scap_client', - 'RedHatInsights.insights-client', - 'redhat.satellite.compute_resources', - ] - proxy_id = target_sat.nailgun_smart_proxy.id - host = target_sat.api.Host(organization=module_org, location=module_location).create() - hg = target_sat.api.HostGroup(name=gen_string('alpha'), organization=[module_org]).create() - hg_nested = target_sat.api.HostGroup( - name=gen_string('alpha'), parent=hg, organization=[module_org] - ).create() - - @request.addfinalizer - def _finalize(): - host.delete() - hg_nested.delete() - hg.delete() - - target_sat.api.AnsibleRoles().sync(data={'proxy_id': proxy_id, 'role_names': ROLE_NAMES}) - ROLES = [ - target_sat.api.AnsibleRoles().search(query={'search': f'name={role}'})[0].id - for role in ROLE_NAMES - ] - - # Assign roles to Host/Hostgroup/Nested Hostgroup and verify it - target_sat.api.Host(id=host.id).add_ansible_role(data={'ansible_role_id': ROLES[0]}) - assert ROLE_NAMES[0] == target_sat.api.Host(id=host.id).list_ansible_roles()[0]['name'] - - target_sat.api.HostGroup(id=hg.id).add_ansible_role(data={'ansible_role_id': ROLES[1]}) - assert ROLE_NAMES[1] == target_sat.api.HostGroup(id=hg.id).list_ansible_roles()[0]['name'] - - target_sat.api.HostGroup(id=hg_nested.id).add_ansible_role(data={'ansible_role_id': ROLES[2]}) - listroles = target_sat.api.HostGroup(id=hg_nested.id).list_ansible_roles() - assert ROLE_NAMES[2] == listroles[0]['name'] - assert listroles[0]['directly_assigned'] - assert ROLE_NAMES[1] == listroles[1]['name'] - assert not listroles[1]['directly_assigned'] - - # Update host to be in nested hostgroup and verify roles assigned - host.hostgroup = hg_nested - host = host.update(['hostgroup']) - listroles_host = target_sat.api.Host(id=host.id).list_ansible_roles() - assert ROLE_NAMES[0] == listroles_host[0]['name'] - assert listroles_host[0]['directly_assigned'] - assert ROLE_NAMES[1] == listroles_host[1]['name'] - assert not listroles_host[1]['directly_assigned'] - assert ROLE_NAMES[2] == listroles_host[2]['name'] - assert not listroles_host[1]['directly_assigned'] - # Verify nested hostgroup doesn't contains the roles assigned to host - listroles_nested_hg = target_sat.api.HostGroup(id=hg_nested.id).list_ansible_roles() - assert ROLE_NAMES[0] not in [role['name'] for role in listroles_nested_hg] - assert ROLE_NAMES[2] == listroles_nested_hg[0]['name'] - assert ROLE_NAMES[1] == listroles_nested_hg[1]['name'] - - # Update host to be in parent hostgroup and verify roles assigned - host.hostgroup = hg - host = host.update(['hostgroup']) - listroles = target_sat.api.Host(id=host.id).list_ansible_roles() - assert ROLE_NAMES[0] == listroles[0]['name'] - assert listroles[0]['directly_assigned'] - assert ROLE_NAMES[1] == listroles[1]['name'] - assert not listroles[1]['directly_assigned'] - # Verify parent hostgroup doesn't contains the roles assigned to host - listroles_hg = target_sat.api.HostGroup(id=hg.id).list_ansible_roles() - assert ROLE_NAMES[0] not in [role['name'] for role in listroles_hg] - assert ROLE_NAMES[1] == listroles_hg[0]['name'] - - -@pytest.mark.rhel_ver_match('[78]') -@pytest.mark.tier2 -def test_positive_read_facts_with_filter( - target_sat, rex_contenthost, filtered_user, rex_host_in_org_and_loc -): - """ - Read host's Ansible facts as a user with a role that has host filter - - :id: 483d5faf-7a4c-4cb7-b14f-369768ad99b0 - - 1. Run Ansible roles on a host - 2. Using API, read Ansible facts of that host - - :expectedresults: Ansible facts returned - - :BZ: 1699188 - - :customerscenario: true - """ - user, password = filtered_user - host = rex_host_in_org_and_loc - - # gather ansible facts by running ansible roles on the host - host.play_ansible_roles() - if is_open('BZ:2216471'): - host_wait = target_sat.api.Host().search( - query={'search': f'name={rex_contenthost.hostname}'} - )[0] - wait_for( - lambda: len(host_wait.get_facts()) > 0, - timeout=30, - delay=2, + job = target_sat.api.JobInvocation().run( + synchronous=False, + data={ + 'job_template_id': template_id, + 'targeting_type': 'static_query', + 'search_query': f'name ^ ({hosts[0].hostname} && {hosts[1].hostname} ' + f'&& {hosts[2].hostname})', + }, ) - - user_cfg = user_nailgun_config(user.login, password) - host = target_sat.api.Host(server_config=user_cfg).search( - query={'search': f'name={rex_contenthost.hostname}'} - )[0] - # get facts through API - facts = host.get_facts() - assert 'subtotal' in facts - assert facts['subtotal'] == 1 - assert 'results' in facts - assert rex_contenthost.hostname in facts['results'] - assert len(facts['results'][rex_contenthost.hostname]) > 0 + target_sat.wait_for_tasks( + f'resource_type = JobInvocation and resource_id = {job["id"]}', + poll_timeout=1000, + must_succeed=False, + ) + result = target_sat.api.JobInvocation(id=job['id']).read() + assert result.succeeded == 2 # SELECTED_ROLE working on rhel8/rhel9 clients + assert result.failed == 1 # SELECTED_ROLE failing on rhel7 client + assert result.status_label == 'failed' From 6769882a14ec7f2934ca1338eb571b8ba1507929 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 5 Apr 2024 13:46:23 -0400 Subject: [PATCH 577/586] [6.14.z] Verify a variable created and added to the host (#14624) --- pytest_fixtures/component/activationkey.py | 2 +- tests/foreman/ui/test_ansible.py | 74 ++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/pytest_fixtures/component/activationkey.py b/pytest_fixtures/component/activationkey.py index e2a68959d29..0be96945b99 100644 --- a/pytest_fixtures/component/activationkey.py +++ b/pytest_fixtures/component/activationkey.py @@ -8,7 +8,7 @@ @pytest.fixture(scope='module') def module_activation_key(module_entitlement_manifest_org, module_target_sat): """Create activation key using default CV and library environment.""" - module_target_sat.api.ActivationKey( + return module_target_sat.api.ActivationKey( content_view=module_entitlement_manifest_org.default_content_view.id, environment=module_entitlement_manifest_org.library.id, organization=module_entitlement_manifest_org, diff --git a/tests/foreman/ui/test_ansible.py b/tests/foreman/ui/test_ansible.py index 89eba62ff56..22a1d36f945 100644 --- a/tests/foreman/ui/test_ansible.py +++ b/tests/foreman/ui/test_ansible.py @@ -275,6 +275,80 @@ def test_positive_host_role_information(target_sat, function_host): assert all_assigned_roles_table[0]["Name"] == SELECTED_ROLE +@pytest.mark.rhel_ver_match('8') +def test_positive_assign_ansible_role_variable_on_host( + target_sat, rhel_contenthost, module_activation_key, module_org, module_location, request +): + """Verify ansible variable is added to the role and attached to the host. + + :id: 7ec4fe19-5a08-4b10-bb4e-7327dd68699a + + :BZ: 2170727 + + :customerscenario: true + + :steps: + 1. Create an Ansible variable with array type and set the default value. + 2. Enable both 'Merge Overrides' and 'Merge Default'. + 3. Add the variable to a role and attach the role to the host. + 4. Verify that ansible role and variable is added to the host. + + :expectedresults: The role and variable is successfully added to the host. + """ + + @request.addfinalizer + def _finalize(): + result = target_sat.cli.Ansible.roles_delete({'name': SELECTED_ROLE}) + assert f'Ansible role [{SELECTED_ROLE}] was deleted.' in result[0]['message'] + + key = gen_string('alpha') + SELECTED_ROLE = 'redhat.satellite.activation_keys' + proxy_id = target_sat.nailgun_smart_proxy.id + target_sat.api.AnsibleRoles().sync(data={'proxy_id': proxy_id, 'role_names': [SELECTED_ROLE]}) + command = target_sat.api.RegistrationCommand( + organization=module_org, + location=module_location, + activation_keys=[module_activation_key.name], + ).create() + result = rhel_contenthost.execute(command) + assert result.status == 0, f'Failed to register host: {result.stderr}' + target_host = rhel_contenthost.nailgun_host + default_value = '[\"test\"]' + parameter_type = 'array' + with target_sat.ui_session() as session: + session.organization.select(org_name=module_org.name) + session.location.select(loc_name=module_location.name) + session.ansiblevariables.create_with_overrides( + { + 'key': key, + 'ansible_role': SELECTED_ROLE, + 'override': 'true', + 'parameter_type': parameter_type, + 'default_value': default_value, + 'validator_type': None, + 'attribute_order': 'domain \n fqdn \n hostgroup \n os', + 'merge_default': 'true', + 'merge_overrides': 'true', + 'matcher_section.params': [ + { + 'attribute_type': {'matcher_key': 'os', 'matcher_value': 'fedora'}, + 'value': '[\'13\']', + } + ], + } + ) + result = target_sat.cli.Host.ansible_roles_assign( + {'id': target_host.id, 'ansible-roles': SELECTED_ROLE} + ) + assert 'Ansible roles were assigned' in result[0]['message'] + values = session.host_new.get_details(rhel_contenthost.hostname, 'ansible')['ansible'][ + 'variables' + ]['table'] + assert (key, SELECTED_ROLE, default_value, parameter_type) in [ + (var['Name'], var['Ansible role'], var['Value'], var['Type']) for var in values + ] + + @pytest.mark.stubbed @pytest.mark.tier2 def test_positive_role_variable_information(self): From 6a0596ea7c8448842dc5bd4c3d17cb00f1e0fb4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Gajdu=C5=A1ek?= Date: Fri, 5 Apr 2024 19:58:28 +0200 Subject: [PATCH 578/586] [6.14.z] Make test parametrization more readable (#14569) (#14644) Make test parametrization more readable (#14569) Co-authored-by: dosas --- tests/foreman/api/test_repository.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index e73a4ae35e5..25046222491 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -485,14 +485,13 @@ def test_negative_update_to_invalid_download_policy(self, repo, target_sat): @pytest.mark.tier1 @pytest.mark.parametrize( 'repo_options', - **datafactory.parametrized( - [ - {'content_type': content_type, 'download_policy': 'on_demand'} - for content_type in constants.REPO_TYPE - if content_type != 'yum' - ] - ), + [ + {'content_type': content_type, 'download_policy': 'on_demand'} + for content_type in constants.REPO_TYPE + if content_type != 'yum' + ], indirect=True, + ids=lambda x: x['content_type'], ) def test_negative_create_non_yum_with_download_policy(self, repo_options, target_sat): """Verify that non-YUM repositories cannot be created with From 659f90e73c7e305810e95e2cad1445a515cca432 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:09:54 -0400 Subject: [PATCH 579/586] [6.14.z] Bump testimony from 2.3.0 to 2.4.0 (#14651) Bump testimony from 2.3.0 to 2.4.0 (#14648) (cherry picked from commit dc21432ee139abeef159f5e2502577d401b8716f) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index abe79282b19..c06725073a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ pytest-ibutsu==2.2.4 PyYAML==6.0.1 requests==2.31.0 tenacity==8.2.3 -testimony==2.3.0 +testimony==2.4.0 wait-for==1.2.0 wrapanapi==3.6.0 From d516264e86f41190b158f12d42992c5545813838 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 8 Apr 2024 03:13:29 -0400 Subject: [PATCH 580/586] [6.14.z] bookmark entities reduced parametrization (#14660) --- robottelo/constants/__init__.py | 121 +++------------------------- tests/foreman/api/test_bookmarks.py | 4 +- tests/foreman/ui/test_bookmarks.py | 6 +- tests/upgrades/test_bookmarks.py | 10 +-- 4 files changed, 23 insertions(+), 118 deletions(-) diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index 1efd6f37d24..6664aedc310 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -1650,77 +1650,20 @@ class Colored(Box): 'Viewer', ] -BOOKMARK_ENTITIES = [ +BOOKMARK_ENTITIES_SELECTION = [ { 'name': 'ActivationKey', 'controller': 'katello_activation_keys', 'session_name': 'activationkey', 'old_ui': True, }, - {'name': 'Dashboard', 'controller': 'dashboard', 'session_name': 'dashboard'}, - {'name': 'Audit', 'controller': 'audits', 'session_name': 'audit'}, - { - 'name': 'Report', - 'controller': 'config_reports', - 'setup': entities.Report, - 'session_name': 'configreport', - }, - {'name': 'Task', 'controller': 'foreman_tasks_tasks', 'session_name': 'task'}, - # TODO Load manifest for the test_positive_end_to_end from the ui/test_bookmarks.py - # {'name': 'Subscriptions', 'controller': 'subscriptions','session_name': 'subscription' }, - { - 'name': 'Product', - 'controller': 'katello_products', - 'session_name': 'product', - 'old_ui': True, - }, - {'name': 'Repository', 'controller': 'katello_repositories', 'session_name': 'repository'}, - { - 'name': 'ContentCredential', - 'controller': 'katello_content_credentials', - 'session_name': 'contentcredential', - 'old_ui': True, - }, - { - 'name': 'SyncPlan', - 'controller': 'katello_sync_plans', - 'session_name': 'syncplan', - 'old_ui': True, - }, - {'name': 'ContentView', 'controller': 'katello_content_views', 'session_name': 'contentview'}, {'name': 'Errata', 'controller': 'katello_errata', 'session_name': 'errata', 'old_ui': True}, - {'name': 'Package', 'controller': 'katello_erratum_packages', 'session_name': 'package'}, - { - 'name': 'ContainerImageTag', - 'controller': 'katello_docker_tags', - 'session_name': 'containerimagetag', - 'old_ui': True, - }, {'name': 'Host', 'controller': 'hosts', 'setup': entities.Host, 'session_name': 'host_new'}, - {'name': 'ContentHost', 'controller': 'hosts', 'session_name': 'contenthost', 'old_ui': True}, { - 'name': 'HostCollection', - 'controller': 'katello_host_collections', - 'session_name': 'hostcollection', - 'old_ui': True, - }, - {'name': 'Architecture', 'controller': 'architectures', 'session_name': 'architecture'}, - { - 'name': 'HardwareModel', - 'controller': 'models', - 'setup': entities.Model, - 'session_name': 'hardwaremodel', - }, - { - 'name': 'InstallationMedia', - 'controller': 'media', - 'session_name': 'media', - 'setup': entities.Media, - }, - { - 'name': 'OperatingSystem', - 'controller': 'operatingsystems', - 'session_name': 'operatingsystem', + 'name': 'UserGroup', + 'controller': 'usergroups', + 'setup': entities.UserGroup, + 'session_name': 'usergroup', }, { 'name': 'PartitionTable', @@ -1728,58 +1671,18 @@ class Colored(Box): 'setup': entities.PartitionTable, 'session_name': 'partitiontable', }, + { + 'name': 'Product', + 'controller': 'katello_products', + 'session_name': 'product', + 'old_ui': True, + }, { 'name': 'ProvisioningTemplate', 'controller': 'provisioning_templates', 'session_name': 'provisioningtemplate', }, - { - 'name': 'HostGroup', - 'controller': 'hostgroups', - 'setup': entities.HostGroup, - 'session_name': 'hostgroup', - }, - { - 'name': 'DiscoveryRule', - 'controller': 'discovery_rules', - 'setup': entities.DiscoveryRule, - 'session_name': 'discoveryrule', - }, - { - 'name': 'GlobalParameter', - 'controller': 'common_parameters', - 'setup': entities.CommonParameter, - 'skip_for_ui': True, - }, - {'name': 'Role', 'controller': 'ansible_roles', 'setup': entities.Role, 'session_name': 'role'}, - {'name': 'Variables', 'controller': 'ansible_variables', 'session_name': 'ansiblevariables'}, - {'name': 'Capsules', 'controller': 'smart_proxies', 'session_name': 'capsule'}, - { - 'name': 'ComputeResource', - 'controller': 'compute_resources', - 'setup': entities.LibvirtComputeResource, - 'session_name': 'computeresource', - }, - { - 'name': 'ComputeProfile', - 'controller': 'compute_profiles', - 'setup': entities.ComputeProfile, - 'session_name': 'computeprofile', - }, - {'name': 'Subnet', 'controller': 'subnets', 'setup': entities.Subnet, 'session_name': 'subnet'}, - {'name': 'Domain', 'controller': 'domains', 'setup': entities.Domain, 'session_name': 'domain'}, - {'name': 'Realm', 'controller': 'realms', 'setup': entities.Realm, 'session_name': 'realm'}, - {'name': 'Location', 'controller': 'locations', 'session_name': 'location'}, - {'name': 'Organization', 'controller': 'organizations', 'session_name': 'organization'}, - {'name': 'User', 'controller': 'users', 'session_name': 'user'}, - { - 'name': 'UserGroup', - 'controller': 'usergroups', - 'setup': entities.UserGroup, - 'session_name': 'usergroup', - }, - {'name': 'Role', 'controller': 'roles', 'session_name': 'role'}, - {'name': 'Settings', 'controller': 'settings', 'session_name': 'settings'}, + {'name': 'Repository', 'controller': 'katello_repositories', 'session_name': 'repository'}, ] STRING_TYPES = ['alpha', 'numeric', 'alphanumeric', 'latin1', 'utf8', 'cjk', 'html'] diff --git a/tests/foreman/api/test_bookmarks.py b/tests/foreman/api/test_bookmarks.py index b51ac194cb3..5d5833ef563 100644 --- a/tests/foreman/api/test_bookmarks.py +++ b/tests/foreman/api/test_bookmarks.py @@ -17,11 +17,11 @@ import pytest from requests.exceptions import HTTPError -from robottelo.constants import BOOKMARK_ENTITIES +from robottelo.constants import BOOKMARK_ENTITIES_SELECTION from robottelo.utils.datafactory import invalid_values_list, valid_data_list # List of unique bookmark controller values, preserving order -CONTROLLERS = list(dict.fromkeys(entity['controller'] for entity in BOOKMARK_ENTITIES)) +CONTROLLERS = list(dict.fromkeys(entity['controller'] for entity in BOOKMARK_ENTITIES_SELECTION)) @pytest.mark.tier1 diff --git a/tests/foreman/ui/test_bookmarks.py b/tests/foreman/ui/test_bookmarks.py index 037af909046..096f8fdf7cd 100644 --- a/tests/foreman/ui/test_bookmarks.py +++ b/tests/foreman/ui/test_bookmarks.py @@ -16,11 +16,13 @@ import pytest from robottelo.config import user_nailgun_config -from robottelo.constants import BOOKMARK_ENTITIES +from robottelo.constants import BOOKMARK_ENTITIES_SELECTION @pytest.fixture( - scope='module', params=BOOKMARK_ENTITIES, ids=(i['name'] for i in BOOKMARK_ENTITIES) + scope='module', + params=BOOKMARK_ENTITIES_SELECTION, + ids=(i['name'] for i in BOOKMARK_ENTITIES_SELECTION), ) def ui_entity(module_org, module_location, request): """Collects the list of all applicable UI entities for testing and does all diff --git a/tests/upgrades/test_bookmarks.py b/tests/upgrades/test_bookmarks.py index b0d04cfa985..ff53bcdb30b 100644 --- a/tests/upgrades/test_bookmarks.py +++ b/tests/upgrades/test_bookmarks.py @@ -13,7 +13,7 @@ """ import pytest -from robottelo.constants import BOOKMARK_ENTITIES +from robottelo.constants import BOOKMARK_ENTITIES_SELECTION class TestPublicDisableBookmark: @@ -45,7 +45,7 @@ def test_pre_create_public_disable_bookmark(self, request, target_sat): :CaseImportance: Critical """ - for entity in BOOKMARK_ENTITIES: + for entity in BOOKMARK_ENTITIES_SELECTION: book_mark_name = entity["name"] + request.node.name bm = target_sat.api.Bookmark( controller=entity['controller'], @@ -77,7 +77,7 @@ def test_post_create_public_disable_bookmark(self, dependent_scenario_name, targ :CaseImportance: Critical """ pre_test_name = dependent_scenario_name - for entity in BOOKMARK_ENTITIES: + for entity in BOOKMARK_ENTITIES_SELECTION: book_mark_name = entity["name"] + pre_test_name bm = target_sat.api.Bookmark().search(query={'search': f'name="{book_mark_name}"'})[0] assert bm.controller == entity['controller'] @@ -115,7 +115,7 @@ def test_pre_create_public_enable_bookmark(self, request, target_sat): :customerscenario: true """ - for entity in BOOKMARK_ENTITIES: + for entity in BOOKMARK_ENTITIES_SELECTION: book_mark_name = entity["name"] + request.node.name bm = target_sat.api.Bookmark( controller=entity['controller'], @@ -145,7 +145,7 @@ def test_post_create_public_enable_bookmark(self, dependent_scenario_name, targe :CaseImportance: Critical """ pre_test_name = dependent_scenario_name - for entity in BOOKMARK_ENTITIES: + for entity in BOOKMARK_ENTITIES_SELECTION: book_mark_name = entity["name"] + pre_test_name bm = target_sat.api.Bookmark().search(query={'search': f'name="{book_mark_name}"'})[0] assert bm.controller == entity['controller'] From 69461ed1109225bbd4b90a5e04a6512207c6427f Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Mon, 8 Apr 2024 10:53:28 -0400 Subject: [PATCH 581/586] [6.14.z] Make oscap profile configurable in settings file (#14665) Make oscap profile configurable in settings file (#14535) to enable tests for OS other than rhel (cherry picked from commit ca8ab783ccb7e10240515830a3b344f3a7c6c0f6) Co-authored-by: dosas --- conf/oscap.yaml.template | 2 ++ pytest_fixtures/component/oscap.py | 2 +- robottelo/config/validators.py | 7 ++++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/conf/oscap.yaml.template b/conf/oscap.yaml.template index bfeec7103fb..f23ec46ffb9 100644 --- a/conf/oscap.yaml.template +++ b/conf/oscap.yaml.template @@ -1,2 +1,4 @@ OSCAP: CONTENT_PATH: /usr/share/xml/scap/ssg/content/ssg-rhel7-ds.xml + # see: robottelo/constants/__init__.py OSCAP_PROFILE + PROFILE: security7 diff --git a/pytest_fixtures/component/oscap.py b/pytest_fixtures/component/oscap.py index e8a7d230603..786788914f2 100644 --- a/pytest_fixtures/component/oscap.py +++ b/pytest_fixtures/component/oscap.py @@ -39,7 +39,7 @@ def scap_content(import_ansible_roles, module_target_sat): scap_profile_id = [ profile['id'] for profile in scap_info.scap_content_profiles - if OSCAP_PROFILE['security7'] in profile['title'] + if OSCAP_PROFILE[settings.oscap.profile] in profile['title'] ][0] return { "title": title, diff --git a/robottelo/config/validators.py b/robottelo/config/validators.py index f598821839d..013be110fe0 100644 --- a/robottelo/config/validators.py +++ b/robottelo/config/validators.py @@ -225,7 +225,12 @@ Validator( 'oscap.content_path', must_exist=True, - ) + ), + Validator( + 'oscap.profile', + default='security7', + must_exist=True, + ), ], osp=[ Validator( From 6c55b64b6d8fd25565aab366655ca1bf39041ad7 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 9 Apr 2024 03:58:30 -0400 Subject: [PATCH 582/586] [6.14.z] Add filter for ldap related tests (#14687) Add filter for ldap related tests (#14540) (cherry picked from commit 21580fc40d64e0daaa31bf72d135bdd4d1fc9469) Co-authored-by: dosas --- pytest_plugins/markers.py | 1 + tests/foreman/conftest.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pytest_plugins/markers.py b/pytest_plugins/markers.py index b7e0f8f6346..e5d9855a179 100644 --- a/pytest_plugins/markers.py +++ b/pytest_plugins/markers.py @@ -25,6 +25,7 @@ def pytest_configure(config): "include_capsule: For satellite-maintain tests to run on Satellite and Capsule both", "capsule_only: For satellite-maintain tests to run only on Capsules", "manifester: Tests that require manifester", + "ldap: Tests related to ldap authentication", ] markers.extend(module_markers()) for marker in markers: diff --git a/tests/foreman/conftest.py b/tests/foreman/conftest.py index 339eb6016f9..1be6ba294e6 100644 --- a/tests/foreman/conftest.py +++ b/tests/foreman/conftest.py @@ -32,6 +32,8 @@ def pytest_collection_modifyitems(session, items, config): for item in items: if any("manifest" in f for f in getattr(item, "fixturenames", ())): item.add_marker("manifester") + if any("ldap" in f for f in getattr(item, "fixturenames", ())): + item.add_marker("ldap") # 1. Deselect tests marked with @pytest.mark.deselect # WONTFIX BZs makes test to be dynamically marked as deselect. deselect = item.get_closest_marker('deselect') From 90a0356938c444b92d3bb1f4d9a8f5d74b0ce397 Mon Sep 17 00:00:00 2001 From: Satellite QE <115476073+Satellite-QE@users.noreply.github.com> Date: Tue, 9 Apr 2024 04:27:42 -0400 Subject: [PATCH 583/586] [6.14.z] HTTP Proxy UI fixes (#14668) --- tests/foreman/ui/test_http_proxy.py | 32 ++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/tests/foreman/ui/test_http_proxy.py b/tests/foreman/ui/test_http_proxy.py index a247058f8b6..10d2249be30 100644 --- a/tests/foreman/ui/test_http_proxy.py +++ b/tests/foreman/ui/test_http_proxy.py @@ -38,8 +38,6 @@ def test_positive_create_update_delete(module_org, module_location, target_sat): :id: 0c7cdf3d-778f-427a-9a2f-42ad7c23aa15 :expectedresults: All expected CRUD actions finished successfully - - :CaseImportance: High """ http_proxy_name = gen_string('alpha', 15) updated_proxy_name = gen_string('alpha', 15) @@ -65,8 +63,8 @@ def test_positive_create_update_delete(module_org, module_location, target_sat): assert http_proxy_values['http_proxy']['name'] == http_proxy_name assert http_proxy_values['http_proxy']['url'] == http_proxy_url assert http_proxy_values['http_proxy']['username'] == username - assert http_proxy_values['locations']['resources']['assigned'][0] == module_location.name - assert http_proxy_values['organizations']['resources']['assigned'][0] == module_org.name + assert module_location.name in http_proxy_values['locations']['resources']['assigned'] + assert module_org.name in http_proxy_values['organizations']['resources']['assigned'] # Update http_proxy with new name session.http_proxy.update(http_proxy_name, {'http_proxy.name': updated_proxy_name}) assert session.http_proxy.search(updated_proxy_name)[0]['Name'] == updated_proxy_name @@ -210,7 +208,7 @@ def test_set_default_http_proxy(module_org, module_location, setting_update, tar :steps: 1. Navigate to Infrastructure > Http Proxies 2. Create a Http Proxy - 3. GoTo to Administer > Settings > content tab + 3. Go to Administer > Settings > Content tab 4. Update the "Default HTTP Proxy" with created above. 5. Update "Default HTTP Proxy" to "no global default". @@ -251,29 +249,30 @@ def test_set_default_http_proxy(module_org, module_location, setting_update, tar def test_check_http_proxy_value_repository_details( function_org, function_location, function_product, setting_update, target_sat ): - """Deleted Global Http Proxy is reflected in repository details page". + """Global Http Proxy is reflected in repository details page". :id: 3f64255a-ef6c-4acb-b99b-e5579133b564 :steps: 1. Create Http Proxy (Go to Infrastructure > Http Proxies > New Http Proxy) - 2. GoTo to Administer > Settings > content tab + 2. Go to Administer > Settings > Content tab 3. Update the "Default HTTP Proxy" with created above. - 4. Create repository with Global Default Http Proxy. - 5. Delete the Http Proxy + 4. Create repository, check the Global Default Http Proxy is used. + 5. Delete the Http Proxy. + 6. Check it no longer appears on the Settings and repository page. :BZ: 1820193 :parametrized: yes :expectedresults: - 1. After deletion of "Default Http Proxy" its field on settings page should be - set to no global defult - 2. "HTTP Proxy" field in repository details page should be set to Global Default (None). + 1. Repository is automatically created with relevant Global Default Http Proxy. + 2. After Http Proxy deletion + - its field on Settings page should be set to Empty. + - "HTTP Proxy" field in repository details page should be set to Global Default (None). :CaseImportance: Medium """ - property_name = setting_update.name repo_name = gen_string('alpha') http_proxy_a = target_sat.api.HTTPProxy( @@ -297,10 +296,15 @@ def test_check_http_proxy_value_repository_details( 'repo_content.upstream_url': settings.repos.yum_0.url, }, ) + repo_values = session.repository.read(function_product.name, repo_name) + assert ( + repo_values['repo_content']['http_proxy_policy'] + == f'Global Default ({http_proxy_a.name})' + ) + session.http_proxy.delete(http_proxy_a.name) result = session.settings.read(f'name = {property_name}') assert result['table'][0]['Value'] == "Empty" - session.repository.search(function_product.name, repo_name)[0]['Name'] repo_values = session.repository.read(function_product.name, repo_name) assert repo_values['repo_content']['http_proxy_policy'] == 'Global Default (None)' From bd062d9b76f227a7814d7ef6f94ef66dbe93cc0b Mon Sep 17 00:00:00 2001 From: Gaurav Talreja Date: Tue, 9 Apr 2024 14:02:11 +0530 Subject: [PATCH 584/586] [6.14.z] Remove test_positive_matcher_field_highlight (#14639) (cherry picked from commit 8c2a4c6acecc379e9ddbf196d36f5c86d622c738) Signed-off-by: Gaurav Talreja --- tests/foreman/ui/test_remoteexecution.py | 47 ------------------------ 1 file changed, 47 deletions(-) diff --git a/tests/foreman/ui/test_remoteexecution.py b/tests/foreman/ui/test_remoteexecution.py index 9eb06939376..021374ffc16 100644 --- a/tests/foreman/ui/test_remoteexecution.py +++ b/tests/foreman/ui/test_remoteexecution.py @@ -411,27 +411,6 @@ def test_positive_ansible_variables_imported_with_roles(session): """ -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_positive_roles_import_in_background(session): - """Verify that importing roles does not create a popup that blocks the UI - - :id: 4f1c7b76-9c67-42b2-9a73-980ca1f05abc - - :steps: - - 1. Import Ansible roles - - :expectedresults: Verify that the UI is accessible while roles are importing - - :CaseAutomation: NotAutomated - - :CaseComponent: Ansible-ConfigurationManagement - - :Team: Rocket - """ - - @pytest.mark.stubbed @pytest.mark.tier3 def test_positive_ansible_roles_ignore_list(session): @@ -556,32 +535,6 @@ def test_positive_set_ansible_role_order_per_hostgroup(session): """ -@pytest.mark.stubbed -@pytest.mark.tier3 -def test_positive_matcher_field_highlight(session): - """Verify that Ansible variable matcher fields change color when modified - - :id: 67b45cfe-31bb-41a8-b88e-27917c68f33e - - :steps: - - 1. Navigate to Configure > Variables > $variablename - 2. Select the "Override" checkbox in the "Default Behavior" section - 3. Click "+Add Matcher" in the "Specify Matcher" section - 4. Select an option from the "Attribute type" dropdown - 5. Add text to the attribute type input field - 6. Add text to the "Value" input field - - :expectedresults: The background of each field turns yellow when a change is made - - :CaseAutomation: NotAutomated - - :CaseComponent: Ansible-ConfigurationManagement - - :Team: Rocket - """ - - @pytest.mark.stubbed @pytest.mark.tier2 def test_positive_schedule_recurring_host_job(self): From 2ae28f7d5d8acd53f3026c0e454bde3126ffbf9d Mon Sep 17 00:00:00 2001 From: vsedmik <46570670+vsedmik@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:50:47 +0200 Subject: [PATCH 585/586] [6.14.z] DRPM removal, SRPM fix (#14691) * Get rid of deprecated DRPM repo tests * Fix SRPM repo tests --- robottelo/constants/repos.py | 2 +- tests/foreman/api/test_repository.py | 15 ++--- tests/foreman/cli/test_repository.py | 88 ---------------------------- 3 files changed, 5 insertions(+), 100 deletions(-) diff --git a/robottelo/constants/repos.py b/robottelo/constants/repos.py index a0b2a8adeec..9d968a3830b 100644 --- a/robottelo/constants/repos.py +++ b/robottelo/constants/repos.py @@ -12,7 +12,7 @@ CUSTOM_RPM_SHA = 'https://fixtures.pulpproject.org/rpm-with-sha/' CUSTOM_RPM_SHA_512 = 'https://fixtures.pulpproject.org/rpm-with-sha-512/' FAKE_5_YUM_REPO = 'https://rplevka.fedorapeople.org/fakerepo01/' -FAKE_YUM_DRPM_REPO = 'https://fixtures.pulpproject.org/drpm-signed/' +FAKE_YUM_MISSING_REPO = 'https://fixtures.pulpproject.org/missing-repo/' FAKE_YUM_SRPM_REPO = 'https://fixtures.pulpproject.org/srpm-signed/' FAKE_YUM_SRPM_DUPLICATE_REPO = 'https://fixtures.pulpproject.org/srpm-duplicate/' FAKE_YUM_MD5_REPO = 'https://fixtures.pulpproject.org/rpm-with-md5/' diff --git a/tests/foreman/api/test_repository.py b/tests/foreman/api/test_repository.py index 25046222491..e3b93fc7089 100644 --- a/tests/foreman/api/test_repository.py +++ b/tests/foreman/api/test_repository.py @@ -50,12 +50,6 @@ def repo_options_custom_product(request, module_org, module_target_sat): return options -@pytest.fixture -def env(module_org, module_target_sat): - """Create a new puppet environment.""" - return module_target_sat.api.Environment(organization=[module_org]).create() - - @pytest.fixture def repo(repo_options, module_target_sat): """Create a new repository.""" @@ -2134,7 +2128,7 @@ class TestSRPMRepository: @pytest.mark.upgrade @pytest.mark.tier2 def test_positive_srpm_upload_publish_promote_cv( - self, module_org, env, repo, module_target_sat + self, module_org, module_lce, repo, module_target_sat ): """Upload SRPM to repository, add repository to content view and publish, promote content view @@ -2168,7 +2162,6 @@ def test_positive_srpm_upload_publish_promote_cv( @pytest.mark.upgrade @pytest.mark.tier2 - @pytest.mark.skip('Uses deprecated SRPM repository') @pytest.mark.skipif( (not settings.robottelo.REPOS_HOSTING_URL), reason='Missing repos_hosting_url' ) @@ -2177,7 +2170,7 @@ def test_positive_srpm_upload_publish_promote_cv( **datafactory.parametrized({'fake_srpm': {'url': repo_constants.FAKE_YUM_SRPM_REPO}}), indirect=True, ) - def test_positive_repo_sync_publish_promote_cv(self, module_org, env, repo, target_sat): + def test_positive_repo_sync_publish_promote_cv(self, module_org, module_lce, repo, target_sat): """Synchronize repository with SRPMs, add repository to content view and publish, promote content view @@ -2201,8 +2194,8 @@ def test_positive_repo_sync_publish_promote_cv(self, module_org, env, repo, targ >= 3 ) - cv.version[0].promote(data={'environment_ids': env.id, 'force': False}) - assert len(target_sat.api.Srpms().search(query={'environment_id': env.id})) == 3 + cv.version[0].promote(data={'environment_ids': module_lce.id, 'force': False}) + assert len(target_sat.api.Srpms().search(query={'environment_id': module_lce.id})) >= 3 class TestSRPMRepositoryIgnoreContent: diff --git a/tests/foreman/cli/test_repository.py b/tests/foreman/cli/test_repository.py index 466ffbb8319..21788a32891 100644 --- a/tests/foreman/cli/test_repository.py +++ b/tests/foreman/cli/test_repository.py @@ -41,7 +41,6 @@ CUSTOM_FILE_REPO, CUSTOM_RPM_SHA, FAKE_5_YUM_REPO, - FAKE_YUM_DRPM_REPO, FAKE_YUM_MD5_REPO, FAKE_YUM_SRPM_REPO, ) @@ -2532,93 +2531,6 @@ def test_positive_sync_publish_promote_cv(self, repo, module_org, target_sat): assert lce['id'] in [lc['id'] for lc in cv['lifecycle-environments']] -@pytest.mark.skip_if_open("BZ:1682951") -class TestDRPMRepository: - """Tests specific to using repositories containing delta RPMs.""" - - @pytest.mark.tier2 - @pytest.mark.skip("Uses deprecated DRPM repository") - @pytest.mark.parametrize( - 'repo_options', **parametrized([{'url': FAKE_YUM_DRPM_REPO}]), indirect=True - ) - def test_positive_sync(self, repo, module_org, module_product, target_sat): - """Synchronize repository with DRPMs - - :id: a645966c-750b-40ef-a264-dc3bb632b9fd - - :parametrized: yes - - :expectedresults: drpms can be listed in repository - """ - target_sat.cli.Repository.synchronize({'id': repo['id']}) - result = target_sat.execute( - f"ls /var/lib/pulp/published/yum/https/repos/{module_org.label}/Library" - f"/custom/{module_product.label}/{repo['label']}/drpms/ | grep .drpm" - ) - assert result.status == 0 - assert result.stdout - - @pytest.mark.tier2 - @pytest.mark.skip("Uses deprecated DRPM repository") - @pytest.mark.parametrize( - 'repo_options', **parametrized([{'url': FAKE_YUM_DRPM_REPO}]), indirect=True - ) - def test_positive_sync_publish_cv(self, repo, module_org, module_product, target_sat): - """Synchronize repository with DRPMs, add repository to content view - and publish content view - - :id: 014bfc80-4622-422e-a0ec-755b1d9f845e - - :parametrized: yes - - :expectedresults: drpms can be listed in content view - """ - target_sat.cli.Repository.synchronize({'id': repo['id']}) - cv = target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) - target_sat.cli.ContentView.add_repository({'id': cv['id'], 'repository-id': repo['id']}) - target_sat.cli.ContentView.publish({'id': cv['id']}) - result = target_sat.execute( - f"ls /var/lib/pulp/published/yum/https/repos/{module_org.label}/content_views/" - f"{cv['label']}/1.0/custom/{module_product.label}/{repo['label']}/drpms/ | grep .drpm" - ) - assert result.status == 0 - assert result.stdout - - @pytest.mark.tier2 - @pytest.mark.upgrade - @pytest.mark.skip("Uses deprecated DRPM repository") - @pytest.mark.parametrize( - 'repo_options', **parametrized([{'url': FAKE_YUM_DRPM_REPO}]), indirect=True - ) - def test_positive_sync_publish_promote_cv(self, repo, module_org, module_product, target_sat): - """Synchronize repository with DRPMs, add repository to content view, - publish and promote content view to lifecycle environment - - :id: a01cb12b-d388-4902-8532-714f4e28ec56 - - :parametrized: yes - - :expectedresults: drpms can be listed in content view in proper - lifecycle environment - """ - lce = target_sat.cli_factory.make_lifecycle_environment({'organization-id': module_org.id}) - target_sat.cli.Repository.synchronize({'id': repo['id']}) - cv = target_sat.cli_factory.make_content_view({'organization-id': module_org.id}) - target_sat.cli.ContentView.add_repository({'id': cv['id'], 'repository-id': repo['id']}) - target_sat.cli.ContentView.publish({'id': cv['id']}) - content_view = target_sat.cli.ContentView.info({'id': cv['id']}) - cvv = content_view['versions'][0] - target_sat.cli.ContentView.version_promote( - {'id': cvv['id'], 'to-lifecycle-environment-id': lce['id']} - ) - result = target_sat.execute( - f"ls /var/lib/pulp/published/yum/https/repos/{module_org.label}/{lce['label']}" - f"/{cv['label']}/custom/{module_product.label}/{repo['label']}/drpms/ | grep .drpm" - ) - assert result.status == 0 - assert result.stdout - - class TestFileRepository: """Specific tests for File Repositories""" From ab3bac956f9b9155ca94087fe8614c0ea2182176 Mon Sep 17 00:00:00 2001 From: Pavel Novotny Date: Mon, 8 Apr 2024 09:07:30 +0200 Subject: [PATCH 586/586] notifications: new test for notification_recipients endpoint (#14592) This new test verifies that endpoint `/notification_recipients` works and returns correct data structure. It covers bug https://bugzilla.redhat.com/2249970 where this broken endpoint caused some web UI pages to fail to load. --- tests/foreman/api/test_notifications.py | 78 ++++++++++++++++++------- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/tests/foreman/api/test_notifications.py b/tests/foreman/api/test_notifications.py index 7954ae9bd0d..c6fd2f3e5dc 100644 --- a/tests/foreman/api/test_notifications.py +++ b/tests/foreman/api/test_notifications.py @@ -21,7 +21,6 @@ from robottelo.config import settings from robottelo.constants import DEFAULT_LOC, DEFAULT_ORG -from robottelo.utils.issue_handlers import is_open @pytest.fixture @@ -54,8 +53,8 @@ def reschedule_long_running_tasks_notification(target_sat): assert ( target_sat.execute( - f"FOREMAN_TASKS_CHECK_LONG_RUNNING_TASKS_CRONLINE='{every_minute_cron_schedule}' " - "foreman-rake foreman_tasks:reschedule_long_running_tasks_checker" + "foreman-rake foreman_tasks:reschedule_long_running_tasks_checker " + f"FOREMAN_TASKS_CHECK_LONG_RUNNING_TASKS_CRONLINE='{every_minute_cron_schedule}'" ).status == 0 ) @@ -64,14 +63,14 @@ def reschedule_long_running_tasks_notification(target_sat): assert ( target_sat.execute( - f"FOREMAN_TASKS_CHECK_LONG_RUNNING_TASKS_CRONLINE='{default_cron_schedule}' " - "foreman-rake foreman_tasks:reschedule_long_running_tasks_checker" + "foreman-rake foreman_tasks:reschedule_long_running_tasks_checker " + f"FOREMAN_TASKS_CHECK_LONG_RUNNING_TASKS_CRONLINE='{default_cron_schedule}'" ).status == 0 ) -@pytest.fixture +@pytest.fixture(autouse=True) def start_postfix_service(target_sat): """Start postfix service (disabled by default).""" assert target_sat.execute('systemctl start postfix').status == 0 @@ -92,33 +91,42 @@ def clean_root_mailbox(target_sat): target_sat.execute(f'mv -f {root_mailbox_backup} {root_mailbox}') -@pytest.fixture -def wait_for_long_running_task_mail(target_sat, clean_root_mailbox, long_running_task): - """Wait until the long-running task ID is found in the Satellite's mbox file.""" - timeout = 300 +def wait_for_mail(sat_obj, mailbox_file, contains_string, timeout=300, delay=5): + """ + Wait until the desired string is found in the Satellite's mbox file. + """ try: wait_for( - func=target_sat.execute, - func_args=[f'grep --quiet {long_running_task["task"]["id"]} {clean_root_mailbox}'], - fail_condition=lambda res: res.status == 0, + func=sat_obj.execute, + func_args=[f"grep --quiet '{contains_string}' {mailbox_file}"], + fail_condition=lambda res: res.status != 0, timeout=timeout, - delay=5, + delay=delay, ) except TimedOutError as err: raise AssertionError( - f'No notification e-mail with long-running task ID {long_running_task["task"]["id"]} ' - f'has arrived to {clean_root_mailbox} after {timeout} seconds.' + f'No e-mail with text "{contains_string}" has arrived to mailbox {mailbox_file} ' + f'after {timeout} seconds.' ) from err return True @pytest.fixture -def root_mailbox_copy(target_sat, clean_root_mailbox, wait_for_long_running_task_mail): +def wait_for_long_running_task_mail(target_sat, clean_root_mailbox, long_running_task): + """Wait until the long-running task ID is found in the Satellite's mbox file.""" + return wait_for_mail( + sat_obj=target_sat, + mailbox_file=clean_root_mailbox, + contains_string=long_running_task["task"]["id"], + ) + + +@pytest.fixture +def root_mailbox_copy(target_sat, clean_root_mailbox): """Parsed local system copy of the Satellite's root user mailbox. :returns: :class:`mailbox.mbox` instance """ - assert wait_for_long_running_task_mail result = target_sat.execute(f'cat {clean_root_mailbox}') assert result.status == 0, f'Could not read mailbox {clean_root_mailbox} on Satellite host.' mbox_content = result.stdout @@ -173,7 +181,7 @@ def long_running_task(target_sat): @pytest.mark.usefixtures( 'admin_user_with_localhost_email', 'reschedule_long_running_tasks_notification', - 'start_postfix_service', + 'wait_for_long_running_task_mail', ) def test_positive_notification_for_long_running_tasks(long_running_task, root_mailbox_copy): """Check that a long-running task (i.e., running or paused for more than two days) @@ -218,5 +226,33 @@ def test_positive_notification_for_long_running_tasks(long_running_task, root_ma '/foreman_tasks/tasks?search=state+%5E+%28running%2C+paused' '%29+AND+state_updated_at' in body_text ), 'Link for long-running tasks is missing in the e-mail body.' - if not is_open('BZ:2223996'): - assert findall(r'_\("[\w\s]*"\)', body_text), 'Untranslated strings found.' + assert not findall(r'_\("[\w\s]*"\)', body_text), 'Untranslated strings found.' + + +@pytest.mark.tier1 +def test_positive_notification_recipients(target_sat): + """Check that endpoint `/notification_recipients` works and returns correct data structure. + + :id: 10e0fac2-f11f-11ee-ba60-000c2989e153 + + :steps: + 1. Do a GET request to /notification_recipients endpoint. + 2. Check the returned data structure for expected keys. + + :BZ: 2249970 + + :customerscenario: true + """ + notification_keys = [ + 'id', + 'seen', + 'level', + 'text', + 'created_at', + 'group', + 'actions', + ] + + recipients = target_sat.api.NotificationRecipients().read() + for notification in recipients.notifications: + assert set(notification_keys) == set(notification.keys())

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