diff --git a/.github/workflows/auto_cherry_pick_merged.yaml b/.github/workflows/auto_cherry_pick_merged.yaml new file mode 100644 index 00000000000..c93cc02e7bf --- /dev/null +++ b/.github/workflows/auto_cherry_pick_merged.yaml @@ -0,0 +1,169 @@ +### The workflow for retrying/rerunning the merged PRs AutoCherryPick which was missed/failed due to any circumstances +name: Retry Merged PRs AutoCherryPick + +# Run on workflow dispatch from CI +on: + workflow_dispatch: + inputs: + parent_pr: + type: string + description: | + An identifier for parent PR to retry it's cherrypick + e.g 12314 + + branches: + type: string + description: | + Comma separated list of branches where the master PR to be cherrypicked. + e.g: 6.15.z, 6.16.z + +env: + number: ${{ github.event.inputs.parent_pr }} + is_dependabot_pr: '' + +jobs: + get-parentPR-details: + runs-on: ubuntu-latest + outputs: + labels: ${{ steps.parentPR.outputs.labels }} + state: ${{ steps.parentPR.outputs.state }} + base_ref: ${{ steps.parentPR.outputs.base_ref }} + assignee: ${{ steps.parentPR.outputs.assignee }} + title: ${{ steps.parentPR.outputs.title }} + prt_comment: ${{ steps.fc.outputs.comment-body }} + + steps: + - name: Find parent PR details + id: parentPR + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.CHERRYPICK_PAT }} + script: | + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: ${{ env.number }}, + }); + core.setOutput('labels', pr.labels); + core.setOutput('state', pr.state); + core.setOutput('base_ref', pr.base.ref); + core.setOutput('assignee', pr.assignee.login); + core.setOutput('title', pr.title); + + - name: Find & Save last PRT comment of Parent PR + uses: peter-evans/find-comment@v3 + id: fc + with: + issue-number: ${{ env.number }} + body-includes: "trigger: test-robottelo" + direction: last + + - name: Print PR details + run: | + echo "Labels are ${{ steps.parentPR.outputs.labels }}" + echo "State is ${{ steps.parentPR.outputs.state }}" + echo "Base Ref is ${{ steps.parentPR.outputs.base_ref }}" + echo "Assignee is ${{ steps.parentPR.outputs.assignee }}" + echo "Title is ${{ steps.parentPR.outputs.title }}" + + arrayconversion: + needs: get-parentPR-details + if: ${{ needs.get-parentPR-details.outputs.state }} == closed + runs-on: ubuntu-latest + outputs: + branches: ${{ steps.conversion.outputs.branches }} + steps: + - name: Convert String to List + id: conversion + uses: actions/github-script@v7 + with: + script: | + const branches = "${{ github.event.inputs.branches }}"; + const branchesArray = branches.includes(',') ? branches.split(',').map(item => item.trim()) : [branches.trim()]; + core.setOutput('branches', JSON.stringify(branchesArray)); + + + run-the-branch-matrix: + name: Auto Cherry Pick to labeled branches + runs-on: ubuntu-latest + needs: [arrayconversion, get-parentPR-details] + if: ${{ needs.arrayconversion.outputs.branches != '' }} + strategy: + matrix: + branch: ${{ fromJson(needs.arrayconversion.outputs.branches) }} + steps: + - name: Tell me the branch name + run: | + echo "Branch is: ${{ matrix.branch }}" + + # 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.branch, '6.') && matrix.branch != needs.get-parentPR-details.outputs.base_ref }} + 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(needs.get-parentPR-details.outputs.labels.*.name, 'dependencies') + run: | + echo "is_dependabot_pr=dependencies" >> $GITHUB_ENV + + ## CherryPicking and AutoMerging + - name: Cherrypicking to zStream branch + id: cherrypick + if: ${{ startsWith(matrix.branch, '6.') && matrix.branch != needs.get-parentPR-details.outputs.base_ref }} + uses: jyejare/github-cherry-pick-action@main + with: + token: ${{ secrets.CHERRYPICK_PAT }} + pull_number: ${{ env.number }} + branch: ${{ matrix.branch }} + labels: | + Auto_Cherry_Picked + ${{ matrix.branch }} + No-CherryPick + ${{ env.is_dependabot_pr }} + assignees: ${{ needs.get-parentPR-details.outputs.assignee }} + + - name: Add Parent PR's PRT comment to Auto_Cherry_Picked PR's + id: add-parent-prt-comment + if: ${{ always() && needs.get-parentPR-details.outputs.prt_comment != '' && steps.cherrypick.outcome == 'success' }} + uses: thollander/actions-comment-pull-request@v2 + with: + message: | + ${{ needs.get-parentPR-details.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(needs.get-parentPR-details.outputs.labels.*.name, 'AutoMerge_Cherry_Picked') }} + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.CHERRYPICK_PAT }} + script: | + github.rest.issues.addLabels({ + issue_number: ${{ steps.cherrypick.outputs.number }}, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ["AutoMerge_Cherry_Picked"] + }) + + - name: Check if cherrypick pr is created + id: search_pr + if: always() + run: | + PR_TITLE="[${{ matrix.branch }}] ${{ needs.get-parentPR-details.outputs.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 diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 1e65633a285..276ff267e1a 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -9,7 +9,6 @@ env: PYCURL_SSL_LIBRARY: openssl ROBOTTELO_BUGZILLA__API_KEY: ${{ secrets.BUGZILLA_KEY }} ROBOTTELO_JIRA__API_KEY: ${{ secrets.JIRA_KEY }} - ROBOTTELO_ROBOTTELO__SETTINGS__IGNORE_VALIDATION_ERRORS: true jobs: codechecks: diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index 92fa9f329a9..b07e6f1d85e 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -48,7 +48,7 @@ jobs: echo "assignees: pondrejk " >> .env.md echo "labels: Documentation" >> .env.md echo "---" >> .env.md - echo CS_TAGS="$(make customer-scenario-check)" >> .env.md + echo CS_TAGS="$(make customer-scenario-check-jira)" >> .env.md if grep 'The following tests need customerscenario tags' .env.md; then echo "::set-output name=result::0" fi diff --git a/.gitignore b/.gitignore index a8d2ee5b44c..b991e4a5107 100644 --- a/.gitignore +++ b/.gitignore @@ -81,11 +81,6 @@ tests/foreman/pytest.ini /conf/*.conf !/conf/supportability.yaml -# I don't know where those 2 files come from -# but they are always there. -full_upgrade -upgrade_highlights - #Robottelo artifact screenshots/ tmp/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0df5dd8a857..f42209f5e61 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: check-yaml - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.6 + rev: v0.6.1 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/BEST_PRACTICES.md b/BEST_PRACTICES.md deleted file mode 100644 index a0b41a4dfd2..00000000000 --- a/BEST_PRACTICES.md +++ /dev/null @@ -1,149 +0,0 @@ -# Helpers Best Practices - Guide -Welcome to the **Helpers Best Practices Guide**! This comprehensive guide is designed to assist contributors in locating existing helpers and adding new ones to the right place, making them accessible through appropriate objects, fixtures, and importables. By following these best practices, we aim to improve the organization of helpers within the framework, enhance the discovery of helpful tools, and minimize the duplication of helper definitions and maintenance efforts. - -## Introduction -The scattered and misaligned placement of helpers within the framework can lead to challenges in discovering and managing helpers, resulting in duplicated definitions and increased maintenance overhead. To address these issues, we have outlined best practices for defining helper functions, modules, classes and fixtures. These guidelines will help you make informed decisions about where to place different types of helpers within the framework. - - -## General Practices -Here are some general best practices for helper management: -- **Prefer API Calls**: When creating test setups, prioritize using API calls (nailgun methods) over UI and CLI interactions, as they tend to be faster and more efficient, unless there is a specific need for specific endpoint based setups. UI setups should be the last resort. -- **Utilize `target_sat` Fixtures**: Ensure that all tests make use of target satellite fixtures to access a wide range of helper methods provided by the satellite object. -- **Non-Reusable helpers**: If helper/fixture is not reusable, keep them closer to test in test module itself. -- **No one liner functions**: If the helper function is just a one-liner, It's recommended to directly replace the function code where it's used. - -## Python Packages and Modules: - -### pytest_fixtures - -In the `pytest_fixtures` Python package, we provide globally accessible fixtures for use in all tests and fixtures. These fixtures are cached by scopes, making them highly reusable without the need for reimplementation. There are two types of fixtures within this package: - -#### Components -- Component fixtures serve as setup and teardown utilities for component tests. -- Each individual fixture module in this package contains and represents fixtures for specific components. - -#### Core -- Core fixtures are framework-level fixtures that serve as setup and teardown for component tests. -- Several special fixture modules deserve mention here: - - `broker.py`: Contains all the `target_sat` fixtures, scoped by usage, which should be used in every functional test in `robottelo`. These fixtures return the same satellite instance for all scoped fixtures, provided that the calling test does not have a destructive marker. For tests with destructive markers, they return a new satellite instance from an image. - - `contenthosts.py`: Houses all content host creation fixtures. All fixture markers for content hosts should be added here since the `fixture_markers` pytest plugin operates on these fixtures, adding markers and user properties to tests that use them. - - `sat_cap_factory.py`: Contains fixtures that create satellites/capsules from scratch without using the xDist satellite. These fixtures should be placed here, as the `factory_collection` pytest plugin operates on them, adding markers to tests that use these fixtures. - - `xdist.py`: Includes the xDist distribution session-scoped fixture, which distributes and generates satellites for running Robottelo tests. The behavior of xDist is based on the `xdist_behavior` set in the `server.yaml` configuration file. - -### Robottelo - -#### host_helpers -- `api_factory.py` - - This module contains the `APIFactory` class. Helpers within this class facilitate integrated operations between components of the satellite object using satellite API calls. - - Since we favor API test setups, this class is the preferred location for adding generic helpers that can be used across UI, CLI, and API tests. - - Helpers added to this class or inherited by this class can be accessed as `sat_obj.api_factory.helper_func_name()`. - -- `cli_factory.py` - - This module contains the `CLIFactory` class. Helpers within this class enable integrated operations between components of the satellite object using satellite CLI hammer commands. - - While it is not our primary choice to add more CLI-based helpers, this class is the appropriate place to add them if the test demands it. - - Helpers added to this class or inherited by this class can be accessed as `sat_obj.cli_factory.helper_func_name()`. - -- `ui_factory.py` - - This module contains a `UIFactory` class that accepts a session object as a parameter. Helpers within this class enable integrated operations between components of the satellite object using the satellite UI. - - Although adding more UI-based helpers is not our preference, this class is suitable for adding them if the test demands it. - - Helpers added to this class or inherited by this class can be accessed as `sat_obj.ui_factory(session).helper_func_name()`. - - The session object provided here is the existing session object from the test case where the helper is being accessed. - - One can use different session object when session is created for different user parameters if needed. - -- `*_mixins.py` - - Classes in mixin modules are inherited by the `Satellite`, `Capsule`, and `Host` classes in the `robottelo/hosts.py` module. Refer to the `hosts.py` module section for more details on that class. - - Mixin classes in these mixin modules and their methods extend functionalities for the host classes in `robottelo/hosts.py` mentioned on above line. - - Unlike methods in `robottelo/hosts.py`, these methods do not pertain to the hosts themselves but perform operations on those hosts and integration between host components. - -#### utils - - Helpers/Utilities which are not directly related to any host object (that does not need operations on hosts) should be added to utils package. - - We have special utility modules for a specific subject. One can add utility helpers in those modules if the helper relates to any of the utlity modules or create new module for new specific subject. - - If the helper does not fit into any of the existing utils modules or subject of helper(s) is not big enough to create new , then add in `robottelo/utils/__init__.py` as a standalone helper. - -#### hosts.py -The `hosts.py` module primarily contains three main classes that represent RedHat Satellite's hosts. Each higher ordered class here inherits the behavior of the previous host class.: - -- **ContentHost**: - - This class represents the ContentHost or Client, including crucial properties and methods that describe these hosts. - - Properties like `hostname` specify details about specific ContentHosts. - - The `nailgun_host` property returns the API/nailgun object for the ContentHost. - - Methods within this class manage operations on ContentHosts, particularly for reconfiguring them. Other operational methods are part of the ContentHost mixins. - - This class inherits methods from `Brokers` modules `Host` class, hence methods from that class are available with this class objects. -- **Capsule**: - - This class represents the Capsule host and includes essential properties and methods that describe the Capsule host. - - Properties like `url` and `is_stream` specify details about the specific Capsule host. - - The `satellite` property returns the satellite object to which the Capsule is connected. - - Methods within this class manage operations related to the Capsule, especially for reconfiguring it via installation processes. Other operational methods are part of the Capsule mixins. e.g `sat.restart_services()` - - This class inherits methods from ContentHost class, hence methods from these classes are available with this class objects. -- **Satellite**: - - This class serves as the representation of the Satellite host and includes essential properties and methods that describe the Satellite host. - - Properties like `hostname` and `port` specify specific details about the Satellite host. - - Properties like `api`, `cli`, and `ui_session` provide access to interface objects that expose satellite components through various endpoints. For example, you can access these components as `sat.api.ActivationKey`, `sat.cli.ActivationKey`, or `sat.ui_session.ActivationKey`. - - Methods within this class handle satellite operations, especially for reconfiguring the satellite via installation processes etc. Other operational methods are part of the Satellite mixins, including factories. e.g `sat.capsule_certs_generate()` - - This class inherits methods from ContentHost and Capsule classes, hence methods from those classes are available with this class objects. - -By adhering to these best practices, you'll contribute to the efficient organization and accessibility of helpers within the framework, enhancing the overall development experience and maintainability - of the project. Thank you for your commitment to best practices! - - -## FAQs: - -_**Question. When should I prefer/not prefer fixtures over to helper function when implementing a new helper ?**_ - -_Answer:_ The answers could be multiple. Fixture over functions are preferred when: - - A new helper function should be cached based on scope of the function usage. - - A new helper has dependency on other fixtures. - - A new helper provides the facility of setup and teardown in the helper function itself. - - A new helper as a fixture accept parameters, allowing you to create dynamic test scenarios. - -_**Question. Where should I implement the fixture that could be used by all tests from all endpoints?**_ - - _Answer:_ The global package `pytest_fixtures` is the recommended place. Choose the right subpackage `core` if framework fixture `component` if component fixture. - -_**Question. Where should we implement the helper functions needed by fixtures ?**_ - - _Answer:_ It is preferred to use reusable helper function in utils modules but if the function is not reusable, it should live in the fixture own module. - -_**Question. Whats the most preferred user interface for writing helper that could be used across all three interfaces tests ?**_ - - _Answer:_ API helpers are preferred but if test demands setup from UI/CLI interfaces then the helper should be added in those interfaces. - -_**Question. I want to create a property / method to describe satellite/capsule/host, where should I add that property ?**_ - - _Answer:_ Satellite/Caspule/ContentHost classes (but not in their mixins) are the best to add properties/methods that describe those. - -_**Question. I want to perform operations on Satellite/Capsule/ContentHost like execute CLI commands, read conf files etc, where should I add a method for that?**_ - - _Answer:_ Add into existing mixin classes in mixin modules in `robottelo/host_helpers/*_mixins.py` or create a new targeted mixin class and inherit than in host classes of `robottelo/hosts.py` module - -_**Question. I have new CLI or UI or API entity or operation to implement, should it be in factory object or endpoint object.**_ - - _Answer:_ New entity/subcommand/operations added in satellite product should be added to be accessed by endpoint onjects. e.g `target_sat.api.ComponentNew()`. The factory objects are just for adding helpers. - -_**Question. When should I prefer helper function as a host object method over utils function ?**_ - - _Answer:_ When the new function is dependent on existing host methods or attributes then its good implement such helper function in host classes as it makes to access those host methods and attributes easier. - -_**Question. What helper functions goes into `robottelo/utils/__init__.py` module?**_ - - _Answer:_ If the helper does not fit into any of the existing utils modules or subject of helper(s) is not big enough to create new , then add in `robottelo/utils/__init__.py` as a standalone helper. - -_**Question. I have a helper function that applies to all Satellite, Capsule, ContentHost classes. Which class should I choose to add it?**_ - - _Answer:_ The recommeded class in such cases is always the highest parent class. Here ContentHost class is the highest parent class where this function should be added as a class method or a mixin method. - -_**Question. I have some upgrade scenario helpers to implement, where should I add them?**_ - - _Answer:_ Upgrade scenarios are not special in this case. All helpers/function except framework should be same as being used in foreman tests and hence all rules are applied. - -_**Question. Where should a non-reusable helper function reside?**_ - - _Answer:_ The preffered place is the test module where its being used. - -_**Question. I need to extend the functionality of third party library methods/objects, should I do it in robottelo?**_ - - _Answer:_ Its recommended to extend the functionality of third library methods in that library itself if its being maintained by SatelliteQE like airgun,nailgun, manifester or is active community like widgetastic, wrapanapi. Else extend that appropriately in utils, host methods or fixtures. - -_**Question. What if I see two helper methods/functions almost doing equal operations with a little diff ?**_ - - _Answer:_ See if this could be merged in one function with optional parameters else leave them separated. E.g provisioning functions using API calls with minimum difference and being used in API, UI and CLI tests then merge them. But if two provisioning helpers one for CLI and API tests and tests demands it then good to keep it separate. diff --git a/Makefile b/Makefile index ff0920c70ee..ac14dc3a0eb 100644 --- a/Makefile +++ b/Makefile @@ -169,8 +169,11 @@ clean-cache: clean-all: docs-clean logs-clean pyc-clean clean-cache clean-shared -customer-scenario-check: - @scripts/customer_scenarios.py +customer-scenario-check-bz: + @scripts/customer_scenarios.py --bz + +customer-scenario-check-jira: + @scripts/customer_scenarios.py --jira vault-login: @scripts/vault_login.py --login diff --git a/conf/capsule.yaml.template b/conf/capsule.yaml.template index 4f50d26bfaf..b101f432a93 100644 --- a/conf/capsule.yaml.template +++ b/conf/capsule.yaml.template @@ -17,4 +17,4 @@ CAPSULE: 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: - # deploy_network_type: '@jinja {{"ipv6" if this.server.is_ipv6 else "ipv4"}}' + deploy_network_type: '@jinja {{"ipv6" if this.server.is_ipv6 else "ipv4"}}' diff --git a/conf/dynaconf_hooks.py b/conf/dynaconf_hooks.py index b3d7a8e219c..0fb3734eeee 100644 --- a/conf/dynaconf_hooks.py +++ b/conf/dynaconf_hooks.py @@ -53,7 +53,7 @@ def config_migrations(settings, data): :type data: dict """ logger.info('Running config migration hooks') - sys.path.append(str(Path(__file__).parent)) + sys.path.append(str(Path(__file__).parent.parent)) from conf import migrations migration_functions = [ diff --git a/conf/gce.yaml.template b/conf/gce.yaml.template index 23f92263c61..18b53233ccb 100644 --- a/conf/gce.yaml.template +++ b/conf/gce.yaml.template @@ -1,8 +1,8 @@ GCE: # Google Provider as Compute Resource # client json Certificate path which is local path on satellite - CERT_PATH: /path/to/certificate.json + CERT_PATH: /usr/share/foreman/path/to/certificate.json # Zones - ZONE: example-zone + ZONE: northamerica-northeast1-a # client certificate CERT: "{}" # client json Certificate diff --git a/conf/rh_cloud.yaml.template b/conf/rh_cloud.yaml.template index 752585b210e..acaa5fc7b9b 100644 --- a/conf/rh_cloud.yaml.template +++ b/conf/rh_cloud.yaml.template @@ -1,4 +1,5 @@ RH_CLOUD: + TOKEN: this-isnt-the-token INSTALL_RHC: false ORGANIZATION: org_name ACTIVATION_KEY: ak_name diff --git a/docs/code_standards.rst b/docs/code_standards.rst index 34af971c26f..72f9ffe5c50 100644 --- a/docs/code_standards.rst +++ b/docs/code_standards.rst @@ -23,9 +23,9 @@ Black Linting -* All code will be linted to black-compatible `PEP8`_ standards using `flake8`_. -* In the root of the **Robottelo** directory, run :code:`flake8 .` -* If flake8 returns errors, make corrections before submitting a pull request. +* All code will be linted to black-compatible `PEP8`_ standards using `ruff linter`_. +* In the root of the **Robottelo** directory, run :code:`ruff check .` +* If ruff linter returns errors, make corrections before submitting a pull request. * pre-commit configuration is available, and its use is strongly encouraged in local development. Docstrings @@ -146,7 +146,7 @@ Categorize each standard into how strictly they are enforced .. _PEP8: http://legacy.python.org/dev/peps/pep-0008/ -.. _flake8: http://flake8.readthedocs.org/ +.. _ruff linter: https://docs.astral.sh/ruff/linter/ .. _testimony: https://github.com/SatelliteQE/testimony .. _sphinx: http://sphinx-doc.org/markup/para.html .. _properly format strings: https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting diff --git a/docs/committing.rst b/docs/committing.rst index c610c3cdcf8..01715163267 100644 --- a/docs/committing.rst +++ b/docs/committing.rst @@ -41,11 +41,11 @@ documented, it doesn’t exist. In order to ensure you are able to pass the Travis CI build, it is recommended that you run the following commands in the base of your Robottelo directory.:: - $ flake8 . + $ ruff check . $ make test-docstrings $ make test-robottelo -:code:`flake8` will ensure that the changes you made are not in violation of +:code:`ruff linter` will ensure that the changes you made are not in violation of PEP8 standards. If the command gives no output, then you have passed. If not, then address any corrections recommended. diff --git a/pytest_plugins/metadata_markers.py b/pytest_plugins/metadata_markers.py index e141d45a1ad..364c250a84e 100644 --- a/pytest_plugins/metadata_markers.py +++ b/pytest_plugins/metadata_markers.py @@ -152,6 +152,9 @@ def pytest_collection_modifyitems(items, config): sat_version = settings.server.version.get('release') snap_version = settings.server.version.get('snap', '') + # Satellite Network Type on which tests are running on + satellite_network_type = 'ipv6' if settings.server.is_ipv6 else 'ipv4' + # split the option string and handle no option, single option, multiple # config.getoption(default) doesn't work like you think it does, hence or '' importance = [i.lower() for i in (config.getoption('importance') or '').split(',') if i != ''] @@ -224,6 +227,9 @@ def pytest_collection_modifyitems(items, config): item.user_properties.append(("SatelliteVersion", sat_version)) item.user_properties.append(("SnapVersion", snap_version)) + # Network Type user property + item.user_properties.append(("SatelliteNetworkType", satellite_network_type)) + # exit early if no filters were passed if importance or component or team: # Filter test collection based on CLI options for filtering diff --git a/pytest_plugins/video_cleanup.py b/pytest_plugins/video_cleanup.py index 320864b7060..2832d249a52 100644 --- a/pytest_plugins/video_cleanup.py +++ b/pytest_plugins/video_cleanup.py @@ -22,7 +22,7 @@ def _clean_video(session_id, test): if settings.ui.grid_url and session_id: grid = urlparse(url=settings.ui.grid_url) - infra_grid = Host(hostname=grid.hostname) + infra_grid = Host(hostname=grid.hostname, ipv6=settings.server.is_ipv6) 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: diff --git a/requirements-optional.txt b/requirements-optional.txt index d6cfa4e3599..eecc4ec83b4 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -1,8 +1,8 @@ # For running tests and checking code quality using these modules. -flake8==7.1.0 pytest-cov==5.0.0 redis==5.0.8 pre-commit==3.8.0 +ruff==0.6.2 # For generating documentation. sphinx==8.0.2 diff --git a/requirements.txt b/requirements.txt index 74c64eeabac..20f2fa8155b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ apypie==0.4.0 betelgeuse==1.11.0 -broker[docker,podman,hussh]==0.5.2 +broker[docker,podman,hussh]==0.5.3 cryptography==43.0.0 deepdiff==7.0.1 dynaconf[vault]==3.2.6 @@ -14,14 +14,14 @@ productmd==1.38 pyotp==2.9.0 python-box==7.2.0 pytest==8.3.2 -pytest-order==1.2.1 +pytest-order==1.3.0 pytest-services==2.2.1 pytest-mock==3.14.0 pytest-reportportal==5.4.1 pytest-xdist==3.6.1 pytest-fixturecollection==0.1.2 pytest-ibutsu==2.2.4 -PyYAML==6.0.1 +PyYAML==6.0.2 requests==2.32.3 tenacity==9.0.0 testimony==2.4.0 diff --git a/robottelo/config/__init__.py b/robottelo/config/__init__.py index 16af2133b31..13769cdef2a 100644 --- a/robottelo/config/__init__.py +++ b/robottelo/config/__init__.py @@ -28,6 +28,7 @@ def get_settings(): settings = LazySettings( envvar_prefix="ROBOTTELO", core_loaders=["YAML"], + root_path=str(robottelo_root_dir), settings_file="settings.yaml", preload=["conf/*.yaml"], includes=["settings.local.yaml", ".secrets.yaml", ".secrets_*.yaml"], diff --git a/robottelo/constants/__init__.py b/robottelo/constants/__init__.py index cf82950ad7d..2f676bc6d20 100644 --- a/robottelo/constants/__init__.py +++ b/robottelo/constants/__init__.py @@ -1978,6 +1978,12 @@ "webhook", ] +FAM_TEST_LIBVIRT_PLAYBOOKS = [ + "compute_attribute", + "compute_profile", + "hostgroup", +] + FAM_ROOT_DIR = '/usr/share/ansible/collections/ansible_collections/redhat/satellite' FAM_MODULE_PATH = f'{FAM_ROOT_DIR}/plugins/modules' diff --git a/robottelo/host_helpers/contenthost_mixins.py b/robottelo/host_helpers/contenthost_mixins.py index 2fcfd909d3e..8983287a9fe 100644 --- a/robottelo/host_helpers/contenthost_mixins.py +++ b/robottelo/host_helpers/contenthost_mixins.py @@ -179,7 +179,7 @@ def get_facts(self): if result.status == 0: for line in result.stdout.splitlines(): if ': ' in line: - key, val = line.split(': ') + key, val = line.split(': ', 1) else: key = last_key val = f'{fact_dict[key]} {line}' diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 3dc6755dcbf..ea180f7ddac 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -195,6 +195,7 @@ def __init__(self, hostname, auth=None, **kwargs): # key file based authentication kwargs.update({'key_filename': auth}) self._satellite = kwargs.get('satellite') + self.ipv6 = kwargs.get('ipv6', settings.server.is_ipv6) self.blank = kwargs.get('blank', False) super().__init__(hostname=hostname, **kwargs) @@ -1047,7 +1048,7 @@ def configure_puppet(self, proxy_hostname=None, run_puppet_agent=True): # 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(hostname=proxy_hostname) + proxy_host = Host(hostname=proxy_hostname, ipv6=settings.server.is_ipv6) proxy_host.execute(f'puppetserver ca sign --certname {cert_name}') if run_puppet_agent: @@ -2440,6 +2441,7 @@ class SSOHost(Host): def __init__(self, sat_obj, **kwargs): self.satellite = sat_obj kwargs['hostname'] = kwargs.get('hostname', settings.rhsso.host_name) + kwargs['ipv6'] = kwargs.get('ipv6', settings.server.is_ipv6) super().__init__(**kwargs) def get_rhsso_client_id(self): @@ -2611,6 +2613,7 @@ class IPAHost(Host): def __init__(self, sat_obj, **kwargs): self.satellite = sat_obj kwargs['hostname'] = kwargs.get('hostname', settings.ipa.hostname) + kwargs['ipv6'] = kwargs.get('ipv6', settings.server.is_ipv6) # Allow the class to be constructed from kwargs kwargs['from_dict'] = True kwargs.update( @@ -2712,6 +2715,7 @@ def __init__(self, url, **kwargs): self._conf_dir = '/etc/squid/' self._access_log = '/var/log/squid/access.log' kwargs['hostname'] = urlparse(url).hostname + kwargs['ipv6'] = kwargs.get('ipv6', settings.server.is_ipv6) super().__init__(**kwargs) def add_user(self, name, passwd): diff --git a/robottelo/utils/issue_handlers/jira.py b/robottelo/utils/issue_handlers/jira.py index f9ec6e47bfc..e48e32e6b77 100644 --- a/robottelo/utils/issue_handlers/jira.py +++ b/robottelo/utils/issue_handlers/jira.py @@ -20,6 +20,31 @@ # The .version group being a `d.d` string that can be casted to Version() VERSION_RE = re.compile(r'(?:sat-)*?(?P\d\.\d)\.\w*') +common_jira_fields = ['key', 'summary', 'status', 'labels', 'resolution', 'fixVersions'] + +mapped_response_fields = { + 'key': "{obj_name}['key']", + 'summary': "{obj_name}['fields']['summary']", + 'status': "{obj_name}['fields']['status']['name']", + 'labels': "{obj_name}['fields']['labels']", + 'resolution': "{obj_name}['fields']['resolution']['name'] if {obj_name}['fields']['resolution'] else ''", + 'fixVersions': "[ver['name'] for ver in {obj_name}['fields']['fixVersions']] if {obj_name}['fields']['fixVersions'] else []", + # Custom Field - SFDC Cases Counter + 'customfield_12313440': "{obj_name}['fields']['customfield_12313440']", +} + + +def sanitized_issue_data(issue, out_fields): + """fetches the value for all the given fields from a given jira issue + + Arguments: + issue {dict} -- The json data for a jira issue + out_fields {list} -- The list of fields for which data to be retrieved from jira issue + """ + return { + field: eval(mapped_response_fields[field].format(obj_name=issue)) for field in out_fields + } + def is_open_jira(issue_id, data=None): """Check if specific Jira is open consulting a cached `data` dict or @@ -131,8 +156,7 @@ def collect_data_jira(collected_data, cached_data): # pragma: no cover """ jira_data = ( get_data_jira( - [item for item in collected_data if item.startswith('SAT-')], - cached_data=cached_data, + [item for item in collected_data if item.startswith('SAT-')], cached_data=cached_data ) or [] ) @@ -169,16 +193,41 @@ def collect_dupes(jira, collected_data, cached_data=None): # pragma: no cover stop=stop_after_attempt(4), # Retry 3 times before raising wait=wait_fixed(20), # Wait seconds between retries ) -def get_data_jira(issue_ids, cached_data=None): # pragma: no cover +def get_jira(jql, fields=None): + """Accepts the jql to retrieve the data from Jira for the given fields + + Arguments: + jql {str} -- The query for retrieving the issue(s) details from jira + fields {list} -- The custom fields in query to retrieve the data for + + Returns: Jira object of response after status check + """ + params = {"jql": jql} + if fields: + params.update({"fields": ",".join(fields)}) + response = requests.get( + f"{settings.jira.url}/rest/api/latest/search/", + params=params, + headers={"Authorization": f"Bearer {settings.jira.api_key}"}, + ) + response.raise_for_status() + return response + + +def get_data_jira(issue_ids, cached_data=None, jira_fields=None): # pragma: no cover """Get a list of marked Jira data and query Jira REST API. Arguments: issue_ids {list of str} -- ['SAT-12345', ...] cached_data {dict} -- Cached data previous loaded from API + jira_fields {list of str} -- List of fields to be retrieved by a jira issue GET request Returns: [list of dicts] -- [{'id':..., 'status':..., 'resolution': ...}] """ + if not jira_fields: + jira_fields = common_jira_fields + if not issue_ids: return [] @@ -204,48 +253,18 @@ def get_data_jira(issue_ids, cached_data=None): # pragma: no cover # No cached data so Call Jira API logger.debug(f"Calling Jira API for {set(issue_ids)}") - jira_fields = [ - "key", - "summary", - "status", - "labels", - "resolution", - "fixVersions", - ] # Following fields are dynamically calculated/loaded for field in ('is_open', 'version'): assert field not in jira_fields # Generate jql + if isinstance(issue_ids, str): + issue_ids = [issue_id.strip() for issue_id in issue_ids.split(',')] jql = ' OR '.join([f"id = {issue_id}" for issue_id in issue_ids]) - - response = requests.get( - f"{settings.jira.url}/rest/api/latest/search/", - params={ - "jql": jql, - "fields": ",".join(jira_fields), - }, - headers={"Authorization": f"Bearer {settings.jira.api_key}"}, - ) - response.raise_for_status() + response = get_jira(jql, jira_fields) data = response.json().get('issues') # Clean the data, only keep the required info. - data = [ - { - 'key': issue['key'], - 'summary': issue['fields']['summary'], - 'status': issue['fields']['status']['name'], - 'labels': issue['fields']['labels'], - 'resolution': issue['fields']['resolution']['name'] - if issue['fields']['resolution'] - else '', - 'fixVersions': [ver['name'] for ver in issue['fields']['fixVersions']] - if issue['fields']['fixVersions'] - else [], - } - for issue in data - if issue is not None - ] + data = [sanitized_issue_data(issue, jira_fields) for issue in data if issue is not None] CACHED_RESPONSES['get_data'][str(sorted(issue_ids))] = data return data diff --git a/scripts/customer_scenarios.py b/scripts/customer_scenarios.py index b81137042b9..adee83132ff 100755 --- a/scripts/customer_scenarios.py +++ b/scripts/customer_scenarios.py @@ -6,6 +6,7 @@ import testimony from robottelo.config import settings +from robottelo.utils.issue_handlers.jira import get_data_jira @click.group() @@ -30,10 +31,39 @@ 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 and ( - 'customerscenario' not in test_data or test_data['customerscenario'] == 'false' + lowered_test_data = {name.lower(): val for name, val in test_data.items()} + if 'bz' in lowered_test_data and ( + 'customerscenario' not in lowered_test_data + or lowered_test_data['customerscenario'] == 'false' ): - path_result.append([test.name, test_data['bz']]) + path_result.append([test.name, lowered_test_data['bz']]) + if path_result: + result[path] = path_result + return result + + +def get_tests_path_without_customer_tag(paths): + """Returns the path and test name that does not have customerscenario token even + though it has verifies token when necessary + + Arguments: + paths {list} -- List of test modules paths + """ + testcases = testimony.get_testcases(paths) + result = {} + for path, tests in testcases.items(): + path_result = [] + for test in tests: + test_dict = test.to_dict() + test_data = {**test_dict['tokens'], **test_dict['invalid-tokens']} + # 1st level lowering should be good enough as `verifies` and `customerscenario` + # tokens are at 1st level + lowered_test_data = {name.lower(): val for name, val in test_data.items()} + if 'verifies' in lowered_test_data and ( + 'customerscenario' not in lowered_test_data + or lowered_test_data['customerscenario'] == 'false' + ): + path_result.append([test.name, lowered_test_data['verifies']]) if path_result: result[path] = path_result return result @@ -82,11 +112,41 @@ def query_bz(data): return set(output) +def query_jira(data): + """Returns the list of path and test name for missing customerscenario token + + Arguments: + data {dict} -- The list of test modules and tests without customerscenario tags + """ + output = [] + sfdc_counter_field = 'customfield_12313440' + with click.progressbar(data.items()) as bar: + for path, tests in bar: + for test in tests: + jira_data = get_data_jira(test[1], jira_fields=[sfdc_counter_field]) + for data in jira_data: + customer_cases = int(float(data[sfdc_counter_field])) + if customer_cases and customer_cases >= 1: + output.append(f'{path} {test}') + break + return set(output) + + @main.command() -def run(paths=None): - path_list = make_path_list(paths) - values = get_bz_data(path_list) - results = query_bz(values) +@click.option('--jira', is_flag=True, help='Run the customer scripting for Jira') +@click.option('--bz', is_flag=True, help='Run the customer scripting for BZ') +def run(jira, bz, paths=None): + if jira: + path_list = make_path_list(paths) + values = get_tests_path_without_customer_tag(path_list) + results = query_jira(values) + elif bz: + path_list = make_path_list(paths) + values = get_bz_data(path_list) + results = query_bz(values) + else: + raise UserWarning('Choose either `--jira` or `--bz` option') + if len(results) == 0: click.echo('No action needed for customerscenario tags') else: diff --git a/settings.sample.yaml b/settings.sample.yaml index 56ddbcfe94a..55bf41ee33a 100644 --- a/settings.sample.yaml +++ b/settings.sample.yaml @@ -3,6 +3,11 @@ # example: # `export SATQE_SERVER__HOSTNAME=myserver.redhat.com` --- + +# merge settings from this file with the files that were preloaded from the conf/ directory +# see: https://www.dynaconf.com/merging/ +dynaconf_merge: true + server: admin_password: "" admin_username: "" diff --git a/tests/foreman/api/test_ansible.py b/tests/foreman/api/test_ansible.py index 829f225938d..9cea42bab82 100644 --- a/tests/foreman/api/test_ansible.py +++ b/tests/foreman/api/test_ansible.py @@ -535,8 +535,8 @@ def test_positive_ansible_localhost_job_on_host( assert [i['output'] for i in result if i['output'] == 'Exit status: 0'] @pytest.mark.no_containers - @pytest.mark.rhel_ver_list('8') - def test_negative_ansible_job_timeout_to_kill( + @pytest.mark.rhel_ver_list([settings.content_host.default_rhel_version]) + def test_positive_ansible_job_timeout_to_kill( self, target_sat, module_org, module_location, module_ak_with_synced_repo, rhel_contenthost ): """when running ansible-playbook, timeout to kill/execution_timeout_interval setting @@ -602,3 +602,158 @@ def test_negative_ansible_job_timeout_to_kill( assert [i['output'] for i in result if i['output'] == termination_msg] assert [i['output'] for i in result if i['output'] == 'StandardError: Job execution failed'] assert [i['output'] for i in result if i['output'] == 'Exit status: 120'] + + @pytest.mark.tier2 + @pytest.mark.no_containers + @pytest.mark.rhel_ver_list([settings.content_host.default_rhel_version]) + def test_positive_ansible_job_privilege_escalation( + self, + target_sat, + rhel_contenthost, + module_org, + module_location, + module_ak_with_synced_repo, + ): + """Verify privilege escalation defined inside ansible playbook tasks is working + when executing the playbook via Ansible - Remote Execution + + :id: 8c63fd1a-2121-4cce-9ec1-ae12817c9cc4 + + :steps: + 1. Register a RHEL host to Satellite. + 2. Setup a user on that host. + 3. Create a playbook. + 4. Set the SSH user to the created user, and unset the Effective user. + 5. Run the playbook. + + :expectedresults: In the playbook, created user is expected instead root user. + + :BZ: 1955385 + + :customerscenario: true + """ + playbook = ''' + --- + - name: Test Play + hosts: all + gather_facts: false + tasks: + - name: Check current user + command: bash -c "whoami" + register: def_user + - debug: + var: def_user.stdout + - name: Check become user + command: bash -c "whoami" + become: true + become_user: testing + register: bec_user + - debug: + var: bec_user.stdout + ''' + 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}' + assert rhel_contenthost.execute('useradd testing').status == 0 + pwd = rhel_contenthost.execute( + f'echo {settings.server.ssh_password} | passwd testing --stdin' + ) + assert 'passwd: all authentication tokens updated successfully.' in pwd.stdout + template_id = ( + target_sat.api.JobTemplate() + .search(query={'search': 'name="Ansible - Run playbook"'})[0] + .id + ) + job = target_sat.api.JobInvocation().run( + synchronous=False, + data={ + 'job_category': 'Ansible Playbook', + 'job_template_id': template_id, + 'search_query': f'name = {rhel_contenthost.hostname}', + 'targeting_type': 'static_query', + 'inputs': {'playbook': playbook}, + }, + ) + 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.pending == 0 + assert result.succeeded == 1 + assert result.status_label == 'succeeded' + + task = target_sat.wait_for_tasks( + f'resource_type = JobInvocation and resource_id = {job["id"]}', + ) + assert '"def_user.stdout": "root"' in task[0].humanized['output'] + assert '"bec_user.stdout": "testing"' in task[0].humanized['output'] + + @pytest.mark.no_containers + @pytest.mark.rhel_ver_list([settings.content_host.default_rhel_version]) + def test_positive_ansible_job_with_nonexisting_module( + self, target_sat, module_org, module_location, module_ak_with_synced_repo, rhel_contenthost + ): + """Verify running ansible-playbook job with nonexisting_module, as a result the playbook fails, + and the Ansible REX job fails on Satellite as well. + + :id: a082f599-fbf7-4779-aa18-5139e2bce888 + + :steps: + 1. Register a content host with satellite + 2. Run Ansible playbook with nonexisting_module + 3. Verify playbook fails and the Ansible REX job fails on Satellite + + :expectedresults: Satellite job fails with the error and non-zero exit status, + when using a playbook with the nonexisting_module module. + + :BZ: 2107577, 2028112 + + :customerscenario: true + """ + playbook = ''' + --- + - name: Playbook with a failing task + hosts: localhost + gather_facts: no + tasks: + - name: Run a non-existing module + nonexisting_module: "" + ''' + 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}' + + template_id = ( + target_sat.api.JobTemplate() + .search(query={'search': 'name="Ansible - Run playbook"'})[0] + .id + ) + # run ansible-playbook with nonexisting_module + 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}', + 'inputs': {'playbook': playbook}, + 'execution_timeout_interval': '30', + }, + ) + 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.pending == 0 + assert result.failed == 1 + assert result.status_label == 'failed' + result = target_sat.api.JobInvocation(id=job['id']).outputs()['outputs'][0]['output'] + termination_msg = 'ERROR! couldn\'t resolve module/action \'nonexisting_module\'' + assert [i['output'] for i in result if termination_msg in i['output']] + assert [i['output'] for i in result if i['output'] == 'StandardError: Job execution failed'] + assert [i['output'] for i in result if i['output'] == 'Exit status: 4'] diff --git a/tests/foreman/api/test_capsulecontent.py b/tests/foreman/api/test_capsulecontent.py index 7240d19b5a9..4ce5f3f720d 100644 --- a/tests/foreman/api/test_capsulecontent.py +++ b/tests/foreman/api/test_capsulecontent.py @@ -244,6 +244,7 @@ def test_positive_checksum_sync( @pytest.mark.skip_if_open("BZ:2025494") @pytest.mark.e2e @pytest.mark.tier4 + @pytest.mark.pit_client @pytest.mark.skip_if_not_set('capsule') def test_positive_sync_updated_repo( self, @@ -352,6 +353,7 @@ def test_positive_sync_updated_repo( @pytest.mark.e2e @pytest.mark.tier4 + @pytest.mark.pit_client @pytest.mark.skip_if_not_set('capsule', 'fake_manifest') def test_positive_capsule_sync( self, @@ -875,6 +877,7 @@ def test_positive_sync_kickstart_repo( @pytest.mark.tier4 @pytest.mark.e2e + @pytest.mark.pit_client @pytest.mark.skip_if_not_set('capsule') def test_positive_sync_container_repo_end_to_end( self, @@ -1708,7 +1711,7 @@ def test_automatic_content_counts_update_toggle( :BlockedBy: SAT-25503 - :BZ: 2284027 + :verifies: SAT-25503 :customerscenario: true """ diff --git a/tests/foreman/api/test_computeresource_vmware.py b/tests/foreman/api/test_computeresource_vmware.py index f4498ccf89f..2399dddbc5d 100644 --- a/tests/foreman/api/test_computeresource_vmware.py +++ b/tests/foreman/api/test_computeresource_vmware.py @@ -60,7 +60,7 @@ def test_positive_provision_end_to_end( :customerscenario: true - :BZ: 2186114 + :verifies: SAT-18721 """ sat = module_provisioning_sat.sat name = gen_string('alpha').lower() diff --git a/tests/foreman/cli/test_acs.py b/tests/foreman/cli/test_acs.py index 19620afeea5..90f90550f94 100644 --- a/tests/foreman/cli/test_acs.py +++ b/tests/foreman/cli/test_acs.py @@ -30,6 +30,7 @@ @pytest.mark.e2e @pytest.mark.tier2 +@pytest.mark.pit_server @pytest.mark.parametrize('cnt_type', ['yum', 'file']) @pytest.mark.parametrize('acs_type', ['custom', 'simplified', 'rhui']) def test_positive_CRUD_all_types( diff --git a/tests/foreman/cli/test_capsulecontent.py b/tests/foreman/cli/test_capsulecontent.py index a75ffbb1aed..8bb276556ae 100644 --- a/tests/foreman/cli/test_capsulecontent.py +++ b/tests/foreman/cli/test_capsulecontent.py @@ -45,6 +45,7 @@ indirect=True, ) @pytest.mark.stream +@pytest.mark.pit_client def test_positive_content_counts_for_mixed_cv( target_sat, module_capsule_configured, @@ -181,6 +182,7 @@ def test_positive_content_counts_for_mixed_cv( @pytest.mark.stream +@pytest.mark.pit_client def test_positive_update_counts(target_sat, module_capsule_configured): """Verify the update counts functionality diff --git a/tests/foreman/cli/test_discoveredhost.py b/tests/foreman/cli/test_discoveredhost.py index 62e6c73c4b4..2e0de6a1b5f 100644 --- a/tests/foreman/cli/test_discoveredhost.py +++ b/tests/foreman/cli/test_discoveredhost.py @@ -429,8 +429,6 @@ def test_positive_verify_updated_fdi_image(target_sat): Verifies: SAT-24197, SAT-25275 - :BZ: 2271598 - :customerscenario: true :CaseImportance: Critical diff --git a/tests/foreman/cli/test_errata.py b/tests/foreman/cli/test_errata.py index ca81d544b66..f0e647a4006 100644 --- a/tests/foreman/cli/test_errata.py +++ b/tests/foreman/cli/test_errata.py @@ -1498,7 +1498,7 @@ def test_errata_list_by_contentview_filter(module_sca_manifest_org, module_targe :customerscenario: true - :BZ: 1785146 + :verifies: SAT-7987 """ product = module_target_sat.api.Product(organization=module_sca_manifest_org).create() repo = module_target_sat.cli_factory.make_repository( diff --git a/tests/foreman/cli/test_fact.py b/tests/foreman/cli/test_fact.py index 096f4368d9f..a0581a05782 100644 --- a/tests/foreman/cli/test_fact.py +++ b/tests/foreman/cli/test_fact.py @@ -16,6 +16,7 @@ import pytest from robottelo.config import settings +from robottelo.utils.issue_handlers import is_open pytestmark = [pytest.mark.tier1] @@ -65,32 +66,35 @@ def test_negative_list_by_name(module_target_sat): ) +@pytest.mark.e2e @pytest.mark.no_containers @pytest.mark.pit_client @pytest.mark.rhel_ver_list([settings.content_host.default_rhel_version]) -def test_positive_update_client_facts_verify_imported_values( +def test_positive_facts_end_to_end( module_target_sat, rhel_contenthost, module_org, module_location, module_activation_key ): - """Update client facts and verify the facts are updated in Satellite. + """Update client facts and run Ansible roles and verify the facts are updated in Satellite. :id: ea94ccb7-a125-4be3-932a-bfcb035d3604 + :Verifies: SAT-27056 + :steps: 1. Add a new interface to the host. 2. Register the host to Satellite - 3. Update the facts in Satellite. - 4. Read all the facts for the host. - 5. Verify that all the facts(new and existing) are updated in Satellite. + 3. Gather ansible facts by running ansible roles on the host. + 4. Update the facts in Satellite. + 5. Read all the facts for the host. + 6. Verify that all the facts (new and existing) are updated in Satellite. :expectedresults: Facts are successfully updated in the Satellite. """ - mac_address = gen_mac(multicast=False) ip = gen_ipaddr() + mac_address = gen_mac(multicast=False) # Create eth1 interface on the host add_interface_command = ( - f'ip link add eth1 type dummy && ifconfig eth1 hw ether {mac_address} &&' - f'ip addr add {ip}/24 brd + dev eth1 label eth1:1 &&' - 'ip link set dev eth1 up' + f'nmcli connection add type dummy ifname eth1 ipv4.method manual ipv4.addresses {ip} && ' + f'nmcli connection modify id dummy-eth1 ethernet.mac-address {mac_address}' ) assert rhel_contenthost.execute(add_interface_command).status == 0 result = rhel_contenthost.register( @@ -100,17 +104,29 @@ def test_positive_update_client_facts_verify_imported_values( activation_keys=[module_activation_key.name], ) assert result.status == 0, f'Failed to register host: {result.stderr}' - rhel_contenthost.execute('subscription-manager facts --update') + + host = rhel_contenthost.nailgun_host + # gather ansible facts by running ansible roles on the host + task_id = host.play_ansible_roles() + module_target_sat.wait_for_tasks( + search_query=f'id = {task_id}', + poll_timeout=100, + ) + task_details = module_target_sat.api.ForemanTask().search(query={'search': f'id = {task_id}'}) + assert task_details[0].result == 'success' facts = module_target_sat.cli.Fact().list( options={'search': f'host={rhel_contenthost.hostname}'}, output_format='json' ) facts_dict = {fact['fact']: fact['value'] for fact in facts} expected_values = { 'net::interface::eth1::ipv4_address': ip, - 'net::interface::eth1::mac_address': mac_address.lower(), 'network::fqdn': rhel_contenthost.hostname, 'lscpu::architecture': rhel_contenthost.arch, + 'ansible_distribution_major_version': str(rhel_contenthost.os_version.major), + 'ansible_fqdn': rhel_contenthost.hostname, } + if not is_open('SAT-27056'): + expected_values['net::interface::eth1::mac_address'] = mac_address.lower() for fact, expected_value in expected_values.items(): actual_value = facts_dict.get(fact) assert ( diff --git a/tests/foreman/cli/test_registration.py b/tests/foreman/cli/test_registration.py index 22d24ae517e..463e39e4d38 100644 --- a/tests/foreman/cli/test_registration.py +++ b/tests/foreman/cli/test_registration.py @@ -45,7 +45,7 @@ def test_host_registration_end_to_end( :expectedresults: Host registered successfully - :BZ: 2156926 + :verifies: SAT-14716 :customerscenario: true """ diff --git a/tests/foreman/cli/test_report.py b/tests/foreman/cli/test_report.py index 588346f9431..eac89c8ae5f 100644 --- a/tests/foreman/cli/test_report.py +++ b/tests/foreman/cli/test_report.py @@ -78,7 +78,9 @@ def test_positive_install_configure_host( :customerscenario: true - :BZ: 2126891, 2026239 + :BZ: 2026239 + + :verifies: SAT-25418 """ puppet_infra_host = [session_puppet_enabled_sat, session_puppet_enabled_capsule] for client, puppet_proxy in zip(content_hosts, puppet_infra_host, strict=True): diff --git a/tests/foreman/cli/test_satellitesync.py b/tests/foreman/cli/test_satellitesync.py index 36f4d4c2223..43c17ec56b3 100644 --- a/tests/foreman/cli/test_satellitesync.py +++ b/tests/foreman/cli/test_satellitesync.py @@ -2601,10 +2601,13 @@ def test_positive_custom_cdn_with_credential( @pytest.mark.e2e @pytest.mark.tier3 - @pytest.mark.rhel_ver_list([8]) + @pytest.mark.pit_server + @pytest.mark.pit_client + @pytest.mark.no_containers + @pytest.mark.rhel_ver_list([settings.content_host.default_rhel_version]) @pytest.mark.parametrize( 'function_synced_rh_repo', - ['rhsclient8'], + ['rhsclient9'], indirect=True, ) def test_positive_export_import_consume_incremental_yum_repo( @@ -2837,6 +2840,7 @@ class TestNetworkSync: """Implements Network Sync scenarios.""" @pytest.mark.tier2 + @pytest.mark.pit_server @pytest.mark.parametrize( 'function_synced_rh_repo', ['rhae2'], diff --git a/tests/foreman/cli/test_webhook.py b/tests/foreman/cli/test_webhook.py index b981f01f8b1..ec363ecd309 100644 --- a/tests/foreman/cli/test_webhook.py +++ b/tests/foreman/cli/test_webhook.py @@ -57,6 +57,7 @@ def assert_created(options, hook): class TestWebhook: @pytest.mark.tier3 + @pytest.mark.pit_server @pytest.mark.e2e def test_positive_end_to_end(self, webhook_factory, class_target_sat): """Test creation, list, update and removal of webhook diff --git a/tests/foreman/destructive/test_clone.py b/tests/foreman/destructive/test_clone.py index 0a53ff93b01..4e07f9a7406 100644 --- a/tests/foreman/destructive/test_clone.py +++ b/tests/foreman/destructive/test_clone.py @@ -44,7 +44,9 @@ def test_positive_clone_backup( :parametrized: yes - :BZ: 2142514, 2013776 + :BZ: 2142514 + + :Verifies: SAT-10789 :customerscenario: true """ diff --git a/tests/foreman/destructive/test_fm_upgrade.py b/tests/foreman/destructive/test_fm_upgrade.py index 2e29fb7e895..498b687ed94 100644 --- a/tests/foreman/destructive/test_fm_upgrade.py +++ b/tests/foreman/destructive/test_fm_upgrade.py @@ -32,7 +32,7 @@ def test_negative_ipv6_update_check(sat_maintain): :customerscenario: true - :BZ: 2277393 + :verifies: SAT-24811 :expectedresults: Update check fails due to ipv6.disable=1 in boot options """ diff --git a/tests/foreman/endtoend/test_api_endtoend.py b/tests/foreman/endtoend/test_api_endtoend.py index 9351084eb12..4a824fc65ce 100644 --- a/tests/foreman/endtoend/test_api_endtoend.py +++ b/tests/foreman/endtoend/test_api_endtoend.py @@ -33,7 +33,6 @@ from robottelo.utils.issue_handlers import is_open API_PATHS = { - # flake8:noqa (line-too-long) 'activation_keys': ( '/katello/api/activation_keys', '/katello/api/activation_keys', diff --git a/tests/foreman/longrun/test_oscap.py b/tests/foreman/longrun/test_oscap.py index d448f8d541d..dc06d680bb6 100644 --- a/tests/foreman/longrun/test_oscap.py +++ b/tests/foreman/longrun/test_oscap.py @@ -258,7 +258,7 @@ def test_positive_oscap_run_via_ansible_bz_1814988( :expectedresults: REX job should be success and ARF report should be sent to satellite - :BZ: 1814988 + :verifies: SAT-19505 """ hgrp_name = gen_string('alpha') policy_name = gen_string('alpha') diff --git a/tests/foreman/sys/test_fam.py b/tests/foreman/sys/test_fam.py index 2394b736f5f..e4ceb9089da 100644 --- a/tests/foreman/sys/test_fam.py +++ b/tests/foreman/sys/test_fam.py @@ -19,6 +19,7 @@ from robottelo.constants import ( FAM_MODULE_PATH, FAM_ROOT_DIR, + FAM_TEST_LIBVIRT_PLAYBOOKS, FAM_TEST_PLAYBOOKS, FOREMAN_ANSIBLE_MODULES, RH_SAT_ROLES, @@ -58,8 +59,7 @@ def setup_fam(module_target_sat, module_sca_manifest, install_import_ansible_rol # Execute AAP WF for FAM setup Broker().execute(workflow='fam-test-setup', source_vm=module_target_sat.name) - # Setup provisioning resources and copy config files to the Satellite - module_target_sat.configure_libvirt_cr() + # Copy config files to the Satellite module_target_sat.put( settings.fam.server.to_yaml(), f'{FAM_ROOT_DIR}/tests/test_playbooks/vars/server.yml', @@ -135,6 +135,10 @@ def test_positive_run_modules_and_roles(module_target_sat, setup_fam, ansible_mo :expectedresults: All modules and roles run successfully """ + # Setup provisioning resources + if ansible_module in FAM_TEST_LIBVIRT_PLAYBOOKS: + module_target_sat.configure_libvirt_cr() + # Execute test_playbook result = module_target_sat.execute( f'export NO_COLOR=True && . ~/localenv/bin/activate && cd {FAM_ROOT_DIR} && make livetest_{ansible_module}' diff --git a/tests/foreman/ui/test_ansible.py b/tests/foreman/ui/test_ansible.py index f937bb2e729..4316f9aeb23 100644 --- a/tests/foreman/ui/test_ansible.py +++ b/tests/foreman/ui/test_ansible.py @@ -250,78 +250,6 @@ def test_positive_role_variable_information(self): :expectedresults: The variables information for the given Host is visible. """ - @pytest.mark.stubbed - @pytest.mark.tier2 - def test_positive_assign_role_in_new_ui(self): - """Using the new Host UI, assign a role to a Host - - :id: 044f38b4-cff2-4ddc-b93c-7e9f2826d00d - - :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. - 4. Select the 'Ansible' tab - 5. Click the 'Assign Ansible Roles' button. - 6. Using the popup, assign a role to the Host. - - :expectedresults: The Role is successfully assigned to the Host, and visible on the UI - """ - - @pytest.mark.stubbed - @pytest.mark.tier2 - def test_positive_remove_role_in_new_ui(self): - """Using the new Host UI, remove the role(s) of a Host - - :id: d6de5130-45f6-4349-b490-fbde2aed082c - - :steps: - 1. Register a RHEL host to Satellite. - 2. Import all roles available by default. - 3. Assign a role to the host. - 4. Navigate to the new UI for the given Host. - 5. Select the 'Ansible' tab - 6. Click the 'Edit Ansible roles' button. - 7. Using the popup, remove the assigned role from the Host. - - :expectedresults: Role is successfully removed from the Host, and not visible on the UI - """ - - @pytest.mark.stubbed - @pytest.mark.tier3 - def test_positive_ansible_config_report_failed_tasks_errors(self): - """Check that failed Ansible tasks show as errors in the config report - - :id: 1a91e534-143f-4f35-953a-7ad8b7d2ddf3 - - :steps: - 1. Import Ansible roles - 2. Assign Ansible roles to a host - 3. Run Ansible roles on host - - :expectedresults: Verify that any task failures are listed as errors in the config report - - :CaseAutomation: NotAutomated - """ - - @pytest.mark.stubbed - @pytest.mark.tier3 - def test_positive_ansible_config_report_changes_notice(self): - """Check that Ansible tasks that make changes on a host show as notice in the config report - - :id: 8c90f179-8b70-4932-a477-75dc3566c437 - - :steps: - 1. Import Ansible Roles - 2. Assign Ansible roles to a host - 3. Run Ansible Roles on a host - - :expectedresults: Verify that any tasks that make changes on the host - are listed as notice in the config report - - :CaseAutomation: NotAutomated - """ - @pytest.mark.stubbed @pytest.mark.tier3 def test_positive_ansible_variables_imported_with_roles(self): @@ -556,6 +484,87 @@ def test_positive_non_admin_user_access_with_usergroup( ansible_roles_table = session.host_new.get_ansible_roles(target_sat.hostname) assert ansible_roles_table[0]['Name'] == SELECTED_ROLE + @pytest.mark.no_containers + @pytest.mark.rhel_ver_list([settings.content_host.default_rhel_version]) + def test_positive_ansible_config_report_changes_notice_and_failed_tasks_errors( + self, + rhel_contenthost, + module_target_sat, + module_org, + module_location, + module_activation_key, + ): + """Check that Ansible tasks that make changes on a host show as notice in the config report and + failed Ansible tasks show as errors in the config report + + :id: 286048f8-0f4f-4a3c-b5c7-fe9c7af8a780 + + :steps: + 1. Import Ansible Roles + 2. Assign and Run Ansible roles to a host + 3. Run Ansible Roles on a host + 4. Check Config Report + + :expectedresults: + 1. Verify that any tasks that make changes on the host are listed as notice in the config report + 2. Verify that any task failures are listed as errors in the config report + """ + SELECTED_ROLE = 'theforeman.foreman_scap_client' + nc = module_target_sat.nailgun_smart_proxy + nc.location = [module_location] + nc.organization = [module_org] + nc.update(['organization', 'location']) + module_target_sat.api.AnsibleRoles().sync( + data={'proxy_id': nc.id, 'role_names': SELECTED_ROLE} + ) + rhel_ver = rhel_contenthost.os_version.major + rhel_repo_urls = getattr(settings.repos, f'rhel{rhel_ver}_os', None) + rhel_contenthost.create_custom_repos(**rhel_repo_urls) + result = rhel_contenthost.register( + module_org, module_location, module_activation_key.name, module_target_sat + ) + assert result.status == 0, f'Failed to register host: {result.stderr}' + with module_target_sat.ui_session() as session: + session.location.select(module_location.name) + session.organization.select(module_org.name) + session.host_new.add_single_ansible_role(rhel_contenthost.hostname) + ansible_roles_table = session.host_new.get_ansible_roles(rhel_contenthost.hostname) + assert ansible_roles_table[0]['Name'] == SELECTED_ROLE + # Verify error log for config report after ansible role is executed + session.host_new.run_job(rhel_contenthost.hostname) + session.jobinvocation.wait_job_invocation_state( + entity_name='Run ansible roles', + host_name=rhel_contenthost.hostname, + expected_state='failed', + ) + err_log = session.configreport.search(rhel_contenthost.hostname) + package_name = SELECTED_ROLE.split('.')[1] + assert f'err Install the {package_name} package' in err_log['permission_denied'] + assert ( + 'Execution error: Failed to install some of the specified packages' + in err_log['permission_denied'] + ) + + # Verify notice log for config report after ansible role is successfully executed + rhel_contenthost.create_custom_repos( + client_repo=settings.repos.satclient_repo[f'rhel{rhel_ver}'] + ) + result = rhel_contenthost.register( + module_org, + module_location, + module_activation_key.name, + module_target_sat, + force=True, + ) + assert result.status == 0, f'Failed to register host: {result.stderr}' + session.host_new.run_job(rhel_contenthost.hostname) + session.jobinvocation.wait_job_invocation_state( + entity_name='Run ansible roles', host_name=rhel_contenthost.hostname + ) + notice_log = session.configreport.search(rhel_contenthost.hostname) + assert f'notice Install the {package_name} package' in notice_log['permission_denied'] + assert f'Installed: rubygem-{package_name}' in notice_log['permission_denied'] + class TestAnsibleREX: """Test class for remote execution via Ansible diff --git a/tests/foreman/ui/test_capsulecontent.py b/tests/foreman/ui/test_capsulecontent.py index 53d08b68411..8e520b1d970 100644 --- a/tests/foreman/ui/test_capsulecontent.py +++ b/tests/foreman/ui/test_capsulecontent.py @@ -52,6 +52,7 @@ def capsule_default_org(module_target_sat, module_capsule_configured, default_or ], indirect=True, ) +@pytest.mark.pit_client def test_positive_content_counts_for_mixed_cv( target_sat, module_capsule_configured, diff --git a/tests/foreman/ui/test_computeresource_vmware.py b/tests/foreman/ui/test_computeresource_vmware.py index 751202aba30..cec79fe6311 100644 --- a/tests/foreman/ui/test_computeresource_vmware.py +++ b/tests/foreman/ui/test_computeresource_vmware.py @@ -311,7 +311,9 @@ def test_positive_vmware_custom_profile_end_to_end( :expectedresults: Compute profiles are updated successfully with all the values. - :BZ: 1315277, 2266672 + :BZ: 1315277 + + :verifies: SAT-23630 """ cr_name = gen_string('alpha') guest_os_names = [ diff --git a/tests/foreman/ui/test_discoveredhost.py b/tests/foreman/ui/test_discoveredhost.py index 97a55c31c27..b316d6da63d 100644 --- a/tests/foreman/ui/test_discoveredhost.py +++ b/tests/foreman/ui/test_discoveredhost.py @@ -73,7 +73,9 @@ def test_positive_provision_pxe_host( :expectedresults: Host should be provisioned and entry from discovered host should be auto removed. - :BZ: 1728306, 1731112, 2258024 + :BZ: 1728306, 1731112 + + :verifies: SAT-22452 :CaseImportance: High """ @@ -152,7 +154,9 @@ def test_positive_custom_provision_pxe_host( :expectedresults: Host should be provisioned and entry from discovered host should be auto removed. - :BZ: 2238952, 2268544, 2258024, 2025523 + :BZ: 2025523 + + :verifies: SAT-22452, SAT-20098, SAT-23860 :customerscenario: true diff --git a/tests/foreman/ui/test_errata.py b/tests/foreman/ui/test_errata.py index ee90e4e1114..2212bca1d47 100644 --- a/tests/foreman/ui/test_errata.py +++ b/tests/foreman/ui/test_errata.py @@ -316,7 +316,9 @@ def test_end_to_end( :parametrized: yes - :BZ: 2029192, 2265095 + :BZ: 2029192 + + :verifies: SAT-23414 :customerscenario: true """ diff --git a/tests/foreman/ui/test_location.py b/tests/foreman/ui/test_location.py index d846fa31205..56867d95416 100644 --- a/tests/foreman/ui/test_location.py +++ b/tests/foreman/ui/test_location.py @@ -116,7 +116,9 @@ def test_positive_update_with_all_users(session, target_sat): :expectedresults: Location entity is assigned to user after checkbox was enabled and then disabled afterwards - :BZ: 1321543, 1479736, 1479736 + :BZ: 1479736 + + :verifies: SAT-25386 :BlockedBy: SAT-25386 """ diff --git a/tests/foreman/ui/test_organization.py b/tests/foreman/ui/test_organization.py index 510bfa3731b..0e04a473ac2 100644 --- a/tests/foreman/ui/test_organization.py +++ b/tests/foreman/ui/test_organization.py @@ -199,7 +199,7 @@ def test_positive_create_with_all_users(session, module_target_sat): :expectedresults: Organization and user entities assigned to each other - :BZ: 1321543 + :verifies: SAT-25386 :BlockedBy: SAT-25386 """ diff --git a/tests/foreman/ui/test_registration.py b/tests/foreman/ui/test_registration.py index d9d07582eda..9c1bef43dfb 100644 --- a/tests/foreman/ui/test_registration.py +++ b/tests/foreman/ui/test_registration.py @@ -375,7 +375,7 @@ def test_global_registration_with_gpg_repo_and_default_package( :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 + 3. update the 'host_packages' parameter in organization with package name e.g. zsh 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 @@ -388,12 +388,13 @@ def test_global_registration_with_gpg_repo_and_default_package( repo_gpg_url = settings.repos.gr_yum_repo.gpg_url with target_sat.ui_session() as session: session.organization.select(org_name=module_org.name) + cmd = session.host.get_register_command( { 'general.activation_keys': module_activation_key.name, 'general.insecure': True, 'advanced.force': True, - 'advanced.install_packages': 'mlocate vim', + 'advanced.install_packages': 'mlocate zsh', 'advanced.repository': repo_url, 'advanced.repository_gpg_key_url': repo_gpg_url, } @@ -416,8 +417,9 @@ def test_global_registration_with_gpg_repo_and_default_package( 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}') + result = client.execute('yum -v repolist') assert result.status == 0 + assert repo_name in result.stdout assert repo_url in result.stdout diff --git a/tests/foreman/ui/test_repository.py b/tests/foreman/ui/test_repository.py index cf6f3aa238c..b2ff2bc7c38 100644 --- a/tests/foreman/ui/test_repository.py +++ b/tests/foreman/ui/test_repository.py @@ -317,25 +317,6 @@ def test_positive_sync_custom_repo_yum(session, module_org, module_target_sat): assert 'ago' in sync_values[0]['Finished'] -@pytest.mark.tier2 -@pytest.mark.upgrade -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 - - :expectedresults: Sync procedure for specific docker repository is - successful - """ - 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: - result = session.repository.synchronize(product.name, repo.name) - assert result['result'] == 'success' - - @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, module_target_sat):