Compare commits

...

75 Commits

Author SHA1 Message Date
Rafael Guterres Jeffman
e37f190ee1 Merge pull request #1430 from t-woerner/ipaclient_test_ca_cert_files_test_fix
ipaclient_test: Drop extra ca_cert_files list test
2026-07-22 15:19:02 -03:00
Thomas Woerner
817ac954ca ipaclient_test: Drop extra ca_cert_files list test
ca_cert_files is always a list of strings or None, therefore no
isinstance(options.ca_cert_files, list) test needed.

Fixes: #1425 (Fix ca_cert_files spec according to implementation)
2026-07-22 15:54:07 +02:00
Rafael Guterres Jeffman
7ba279c09f Merge pull request #1429 from t-woerner/ansible_test_sanity_fixes
Ansible test sanity fixes
2026-07-22 08:35:34 -03:00
Thomas Woerner
e0d70eeeea Fix latest ansible-test sanity findings
Some argument_spec and also DOCUMENTATION issues were fixed.

But also some recurring invalid-name findings like for example for the
__metaclass__ line needed to be "fixed".
2026-07-21 14:05:16 +02:00
Varun Mylaraiah
268d4822f1 Merge pull request #1427 from t-woerner/ipagroup_query
ipagroup: Use PARAM_MAPPING and query state support
2026-07-20 12:17:21 +05:30
Thomas Woerner
0e2e6a6653 ipagroup: Use PARAM_MAPPING and query state support
The ipagroup module has been reworked to use the new PARAM_MAPPING
added to ansible_freeipa_module.

The member handling for user, group, service and membermanager has
been simplified by using gen_member_add_del_lists. The member entries
in PARAM_MAPPING are now marked with "member": True. This replaces the
manual calls to gen_add_del_lists, gen_add_list and
gen_intersection_list across separate action/state branches with a
single unified call. externalmember and idoverrideuser are still
handled manually since they need SID-based comparison.

The new query state allows to retrieve group information from IPA.
The query_param option controls which fields are returned: BASE for
essential fields, ALL for all fields, PKEY_ONLY for group names only,
or a list of specific field names.

Here is the updated documentation of the module:

    README-group.md

New tests for the query state can be found at:

    tests/group/test_group_query.yml
2026-07-16 17:09:37 +02:00
Rafael Guterres Jeffman
a45b8f0715 Merge pull request #1411 from t-woerner/build_collection_update_for_aah
build-collection.sh: Rename base README.md to make it invisible for AAH
2026-07-15 14:50:11 -03:00
Rafael Guterres Jeffman
79aed9ec84 Merge pull request #1428 from t-woerner/ipauser_query_fix
ipauser: Fix query state error handling and per-field conversion errors
2026-07-15 11:22:03 -03:00
Thomas Woerner
21258df45b ipauser: Fix query state error handling and per-field conversion errors
query_convert_result now reports the failing field name per-key instead
of aborting the whole loop with an unrelated try/except scope. main()
also rejects state=query with action=member and query is excluded from
rename's generated args and PARAM_MAPPING query fields.
2026-07-13 13:46:04 +02:00
Rafael Guterres Jeffman
9f7bfb3ec4 Merge pull request #1422 from t-woerner/ipauser_query
Ipauser query
2026-07-10 00:12:10 -03:00
Thomas Woerner
5e057cfbcd test_group_case_insensitive.yml: Fix wrong user absent task
The task used a users dict with name, first and last name given
and state:absent. This combination does not make sense and was
silenty ignored before by ipauser module.
2026-07-09 13:53:24 +02:00
Thomas Woerner
fcf4a60de6 ipauser: Use PARAM_MAPPING and query state support
The ipauser module has been reworked to use the new PARAM_MAPPING added
to ansible_freeipa_module.

The member handling for manager, principal, certificate and certmapdata
has been simplified by using gen_member_add_del_lists. The member
entries in PARAM_MAPPING are now marked with "member": True. This
replaces the manual calls to gen_add_del_lists, gen_add_list and
gen_intersection_list across three separate action/state branches with
a single unified call.

The new query state allows to retrieve user information from IPA.
The query_param option controls which fields are returned: BASE for
essential fields, ALL for all fields, PKEY_ONLY for user names only,
or a list of specific field names.

Here is the updated documentation of the module:

    README-user.md

New tests for the query state can be found at:

    tests/user/test_user_query.yml
2026-07-09 13:53:16 +02:00
Thomas Woerner
964345aed8 ansible_freeipa_module.py: New PARAM_MAPPING and query support
New functions and methods have been added to reduce code complexity
and duplication in several modules, as well as to enable query mode
in the modules.

New functions:

    gen_member_add_del_lists
        Compute member add/del lists for all member params marked with
        "member": True in PARAM_MAPPING. Handles sync, add-only and
        remove-only modes based on action and state.

    gen_member_args_from_mapping
        Build a member args dict for compare_args_ipa() from
        PARAM_MAPPING. Replaces per-module gen_member_args() functions.

New static methods:

    gen_args_from_mapping
        Generate IPA command args dict from PARAM_MAPPING. Replaces
        per-module gen_args() functions.

    extract_params
        Extract parameter values from module params using PARAM_MAPPING.

    extract_params_from_entry
        Extract parameter values from a users/hosts dict entry using
        PARAM_MAPPING.

    build_query_param_settings
        Build query parameter settings from PARAM_MAPPING and
        QUERY_FIELDS for use with execute_query.

New methods:

    execute_query
        Execute query state for modules supporting state: query.

    _extract_query_fields
        Extract requested fields from a query result using the mapping.

The params_fail_used_invalid method has been extended to accept optional
params and param_mapping arguments for use with extracted params dicts.

PARAM_MAPPING is an ordered dict that maps Ansible parameter names to
their metadata. Each entry is a dict with optional keys:

    "api_name"      IPA attribute name if different from the Ansible
                    parameter name.
    "gen_args"      Set to False to exclude the parameter from
                    gen_args_from_mapping (used for members and
                    parameters handled separately).
    "member"        Set to True to include the parameter in
                    gen_member_add_del_lists and
                    gen_member_args_from_mapping.
    "module_param"  Set to True for top-level module parameters that
                    are not part of the entity (e.g. action, state,
                    query_param, users).
    "return_only"   Set to True for parameters that are only returned
                    by IPA and never sent (excluded from
                    gen_args_from_mapping).
    "nonempty_list" Set to True if an empty list value should not be
                    sent to IPA.
    "lowercase"     Set to True to lowercase values before comparison.
    "convert_to"    Conversion type for the parameter value
                    (e.g. "str", "int", "bool").

An empty dict {} means the Ansible name equals the IPA attribute name
and the parameter is included in gen_args_from_mapping with no special
handling.

For query support a QUERY_FIELDS dict is needed with "prefix" for the
grouping key in PKEY_ONLY results, "primary_key" for the IPA name
attribute and "base" for the list of essential field names returned by
default.
2026-07-09 13:52:21 +02:00
Thomas Woerner
12bbf74d52 setup.cfg: Set max-public-methods to 25
With the updates for PARAM_MAPPING in IPAAnsibleModule it is needed to
increase max public methods.
2026-06-23 12:57:15 +02:00
Thomas Woerner
432c0b75fc Update pre-commit repo versions, deactivate parseable for ansible-lint
ansible-lint was updated to v26.4.0, yamllint to v1.38.0 and pylint to
v4.0.6.

parseable was deactivated in .ansible-lint as this option is not
supported any more.
2026-06-23 12:57:15 +02:00
Rafael Guterres Jeffman
4f6575b30c Merge pull request #1418 from t-woerner/fix_ssh_public_key_tests_without_key_type_name
Fix SSH public key tests without key type name
2026-04-28 09:43:30 -03:00
Thomas Woerner
ad4b343c7e Fix SSH public key tests without key type name
The tests using SSH public tests without using the "ssh-" key type name
are failing with latest Fedora versions.

The missing "ssh-rsa" key type name for the ssh public keys has been
added.
2026-04-27 09:44:05 +02:00
Rafael Guterres Jeffman
62fd1551eb Merge pull request #1410 from t-woerner/infra_container_load_save
infra/image/shcontainer: New container_save and container_load
2026-02-12 09:36:23 -03:00
Thomas Woerner
fe2e65bd35 build-collection.sh: Rename base README.md to make it invisible for AAH
AAH and also Galaxy are automatically trying to render the base README.md
file in the collection even if "readme" is set to another file in galaxy.yml.

Fixes:
1) README.md is renamed to README-COLLECTION.md
2) New README.md is generated with information about the rename and also
   pointing to the README-COLLECTION.md file in the collection and also
   the upstream documentation (versioned and latest).
3) Upstream "documentation" in galaxy.yml set to new README.md file.
2026-02-10 12:00:37 +01:00
Thomas Woerner
a24e90ad0c infra/image/shcontainer: New container_save and container_load
The new container_save and container_load functions can be used to
save and load container images.

container_save
    Save a container image to a local file.
    Example: container_save "${name}"

container_load
    Load a container image from an tar archive.
    Example: local_image=$(container_load "${archive}")
2026-02-09 15:37:26 +01:00
Rafael Guterres Jeffman
0b9718b3ec Merge pull request #1409 from t-woerner/utils_build_collection_command
Reworked and renamed script to generate Ansible collections
2026-01-22 08:54:35 -03:00
Thomas Woerner
226b8c4d75 Reworked and renamed script to generate Ansible collections
The script utils/build-galaxy-release.sh has been renamed to
utils/build-collection.sh, the script provides the same options, but
requires an extra argument now:

    build-collection.sh [options] rpm|aah|galaxy

The namespace and name are defined according to the argument:

    rpm     freeipa.ansible_freeipa   - General use and RPMs
    galaxy  freeipa.ansible_freeipa   - Ansible Galaxy
    aah     redhat.rhel_idm           - Ansible AutomationHub

The generated file README-COLLECTION.md is set in galaxy.yml as the
documentation entry point for the collections generated with aah and galaxy
as Ansible AutomationHub and also Ansible Galaxy are not able to render the
documentation README files in the collection properly.

The commit also changes the calls of utils/build-galaxy-release.sh to
utils/build-collection.sh.
2026-01-20 13:07:24 +01:00
Thomas Woerner
2f34e1ac6a Merge pull request #1407 from rjeffman/ipaserver_firewalld_warning
Fix Ansible warnings in Firewall zone testing tasks
2026-01-09 17:44:55 +01:00
Thomas Woerner
e4ea7c8983 Merge pull request #1382 from rjeffman/ipadnsrecord_a_rec_create_reverse
ipadnsrecord: Allow setting any IP address if create_reverse is false
2026-01-09 13:43:44 +01:00
Rafael Guterres Jeffman
b3f024869c Fix Ansible warnings in Firewalld zone testing tasks
The firewalld zone verification tasks in ipaserver, ipareplica, and
ipabackup roles were triggering Ansible warnings due to variable
ipareplica_firewalld_zone not being defined when evaluating the task
name.

This fix remove the Jinja template from the task names and wrap the
tasks in a single block so the variable verification is done only once.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2026-01-08 15:24:07 -03:00
Rafael Guterres Jeffman
355438cea9 ipadnsrecord: Allow setting any IP address if create_reverse is false
Adding an A/AAAA record to a host fails if there's not a reverse zone
set that the resulting PTR record can be added to, even if
create_reverse is false.

Changing the rule to create the reverse record fixes the issue.

Fixes: #1381

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2026-01-08 10:18:46 -03:00
Thomas Woerner
30b72422d9 Merge pull request #1372 from rjeffman/passkey_support
Add support for passkey
2026-01-07 20:22:46 +01:00
Thomas Woerner
10a84429e2 Merge pull request #1394 from rjeffman/pre-commit-update
pre-commit: Update pre-commit repo versions
2026-01-07 18:12:09 +01:00
Rafael Guterres Jeffman
bf384ab1aa New passkeyconfig management module
There is a new paskeyconfig management module placed in the plugins
folder:

    plugins/modules/ipapasskeyconfig.py

The paskeyconfig module allows to retrieve and modify global passkey
configuration attributes.

Here is the documentation of the module:

    README-passkeyconfig.md

New example playbooks have been added:

    playbooks/passkeyconfig/passkeyconfig-retrieve.yml
    playbooks/passkeyconfig/passkeyconfig-present.yml

New tests for the module can be found at:

    tests/passkeyconfig/test_passkeyconfig.yml
    tests/passkeyconfig/test_passkeyconfig_client_context.yml

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2026-01-07 11:13:32 -03:00
Rafael Guterres Jeffman
536b7cb5f3 ipauser: Add support for 'passkey' in 'user_auth_type'
The value 'passkey' was missing as a valid value for user_auth_type
attribute.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2026-01-07 11:13:16 -03:00
Rafael Guterres Jeffman
17b100baec ipaservice: Add support for 'passkey' in 'auth_ind'
The value 'passkey' was missing as a valid value for auth_ind attribute.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2026-01-07 11:13:16 -03:00
Rafael Guterres Jeffman
1488fb7b5e ipahost: Add support for 'passkey' in 'auth_ind'
The value 'passkey' was missing as a valid value for auth_ind attribute.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2026-01-07 11:13:16 -03:00
Rafael Guterres Jeffman
a733c031b0 ipaconfig: Add support for 'passkey' in 'user_auth_type'
The value 'passkey' was missing as a valid value for user_auth_type
attribute.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2026-01-07 11:13:16 -03:00
Rafael Guterres Jeffman
ff1a026ef4 tests: Add fact for passkey support
When testing passkey attributes some version of IPA do not support it,
se we need a fact that states that the support is available for proper
testing.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2026-01-07 11:13:16 -03:00
Rafael Guterres Jeffman
fa5d056e72 Merge pull request #1398 from t-woerner/sysaccount
Sysaccount management
2026-01-05 14:45:44 -03:00
Rafael Guterres Jeffman
e0e3cb041e Merge pull request #1405 from t-woerner/cert_new_invalid_profile_message
Cert tests: Do not fail on new dogtag profile not found error message
2026-01-05 14:38:24 -03:00
Thomas Woerner
b54aaf127d README-role.md: Fix typo in action description
There was a typo in the description for action.
2026-01-05 18:22:44 +01:00
Thomas Woerner
4e16126b29 iparole: Add sysaccount member support
sysaccounts can now be used as a member for roles.

Example:

  - name: Ensure role my-app role has sysaccount member my-app
    iparole:
      name: my-app role
      sysaccount: my-app
      action: member

New tests for the module:

    tests/role/test_role_sysaccount_member.yml
2026-01-05 18:22:37 +01:00
Thomas Woerner
ed62c2f1bf Cert tests: Do not fail on new dogtag profile not found error message
The error message for an invalid profile has changes in dogtag. The new
message is "Certificate operation cannot be completed: Unable to get
enrollment template for <profile name>: Profile not found"

Therefore the test is additionally checking for "Profile not found" now.
2026-01-05 16:39:07 +01:00
Thomas Woerner
dc9b0ce4e8 New sysaccount management module
There is a new sysaccount management module placed in the plugins folder:

    plugins/modules/ipasysaccount.py

The sysaccount module allows to ensure presence or absence of system
accounts.

Here is the documentation for the module:

    README-sysaccount.md

New sysaccount example playbooks:

    playbooks/sysaccount/sysaccount-absent.yml
    playbooks/sysaccount/sysaccount-disabled.yml
    playbooks/sysaccount/sysaccount-enabled.yml
    playbooks/sysaccount/sysaccount-present.yml
    playbooks/sysaccount/sysaccount-privileged.yml
    playbooks/sysaccount/sysaccount-unprivileged.yml

New tests for the module:

    tests/sysaccount/test_sysaccount.yml
    tests/sysaccount/test_sysaccount_client_context.yml
2026-01-05 16:36:26 +01:00
Thomas Woerner
aa3bf1f015 Merge pull request #1406 from rjeffman/fix_checkpr_version_name
upstream ci: Use version_name for CheckPR labels
2026-01-05 16:35:23 +01:00
Rafael Guterres Jeffman
f0aa531b28 upstream ci: Use version_name for CheckPR labels
As the ansible_version variable may contain a version specification, we
need a version_name to correctly report the job label in Azure.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2026-01-05 12:01:15 -03:00
Thomas Woerner
6407fd8b2e Merge pull request #1404 from rjeffman/fix_pipeline_names
upstream ci: Fix Azure pipelines invalid names
2025-11-27 10:43:01 +01:00
Rafael Guterres Jeffman
2a1be13d3e upstream ci: Fix Azure pipelines invalid names
Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-11-24 13:25:26 -03:00
Thomas Woerner
2afca1fa5e Merge pull request #1403 from rjeffman/checkpr_ansible_version
upstream CI: Fix CheckPR ansible-core version definition
2025-11-24 11:23:44 +01:00
Thomas Woerner
2a40e42b0c Merge pull request #1402 from rjeffman/fix_nightlies
upstream CI: Fix nightly and azure-pipelines to use version map
2025-11-24 11:23:11 +01:00
Rafael Guterres Jeffman
8a33941188 upstream CI: Fix CheckPR ansible-core version definition
By using the 'ansible-version' variable as '<2.17' allows 'pip' to
install the latest version of the 2.16 series, instead of version 2.16.0
in the case '==2.16'. This ensures we run the tests with the latest
supported version for the specific distro.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-11-21 09:33:47 -03:00
Rafael Guterres Jeffman
0aef995bbe upstream CI: Fix Azure nightly pipelines to use version map
Modify nigtly pipelines to use the same distro-to-Ansible version map
so that the Ansible version matches the required version for the
specific distro. Nightly pipelines are the same used for Weekly tests.

This was required due to recent updates for Python 3.14 and Ansible
upstream versions.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-11-21 09:25:08 -03:00
Rafael Guterres Jeffman
e75efb7a13 pre-commit: Update pre-commit repo versions
ansible-lint version series 24.y is not working with ansible-core 2.19 and
requires versions in series 25.y. Also, other tools were update to more
recent versions.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-11-11 15:11:20 -03:00
Thomas Woerner
ed44344519 Merge pull request #1380 from rjeffman/python_requirements
Fixes several linter issues for recent tool versions.
2025-11-11 13:04:51 +01:00
Rafael Guterres Jeffman
b186a1f28f upstream CI: Update Ansible version for c9s
Although the available ansible-core package version for c9s is 2.14, the
upstream "pip" version of this package has a broken certificate and is
unusable against Galaxy.

This patch fixes the version to 2.16, as it is the same version for c8s
and c10s, and the oldest one available as packages for the CentOS
Streams.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-11-10 15:48:26 -03:00
Rafael Guterres Jeffman
d307635c38 pytest: update to work with recent Python
With Python 3.14, the required pytest version stopped working due to
breaking changes in AST.

This patch changes the test tool versions to the most recent one, by
requiring only the minimal version, but not setting a specific one.
Recent pytest version also requires that the search path for Python
modules is defined.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-11-10 15:39:00 -03:00
Thomas Woerner
74f3817531 Merge pull request #1377 from rjeffman/ansible_2_18
Update Ansible version in Upstream CI
2025-11-10 15:39:41 +01:00
Rafael Guterres Jeffman
97378c38cf pylint: Add list of upper case constants to setup.cfg
Pylint 4.0.1 seems to not understand that some of the constants used by
ansible-freeipa roles and modules are constants and not variables, and
complain about the naming style.

By adding these constant names to the "good-names" list avoid pylint
errors and don't require future unnecessary changes.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-11-10 11:38:52 -03:00
Rafael Guterres Jeffman
6f15cd093a ansible-lint: Fix Jinja error
A task in 'roles/ipaclient/tasks/install.yml' uses logic that
ansible-lint and jinja are unable to evaluate due to missing type. By
refactoring the task the tools are able to evaluate the task.
2025-11-10 11:38:52 -03:00
Rafael Guterres Jeffman
52f7f7848e ansible-lint: Fix deprecation warning with bool and omit
The application of the 'bool' filter to an OmitType value is deprecated
and will be removed on ansible-core 2.23.
2025-11-10 11:38:52 -03:00
Rafael Guterres Jeffman
fdd45cc475 pylint: Fix pylint 3.3.8 issues
With the latest pylint version, an issue is raised by inheriting from
BaseInventoryPlugin, as the class has too many ancestors
(too-many-ancestors). This is caused by a class hierarchy that is too
deep, and is not under ansible-freeipa's control.
2025-11-10 11:38:52 -03:00
Rafael Guterres Jeffman
a1cad32a46 requirements.txt: Add setuptools
In recent Python setups, 'setuptools' may not be readily available, and,
as we depend on it, it should be a requirement.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-11-10 11:38:52 -03:00
Thomas Woerner
7036fa3e1b Merge pull request #1392 from rjeffman/fix_cert_msg_change
ipacert: Fix tests for inexistent certificate
2025-11-10 15:20:07 +01:00
Rafael Guterres Jeffman
95d935f185 ansible-docs: Update versions for ansible-doc-test checks
Older versions of ansible-doc-test are failing due to code errors in the
parsing module. This is fixed by using newer versions.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-11-07 14:32:35 -03:00
Rafael Guterres Jeffman
dd3bc4fcdd linter: Pin Python version for ansible-lint
ansible-lint is complaining that Python 3.14 requries ansible-core 2.20,
even if other versions work on that Python version.

Woraround implemented is to pin the ansible-lint Python version to 3.13.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-11-07 14:28:33 -03:00
Rafael Guterres Jeffman
c405229553 ipacert: Fix tests for inexistent certificate
After a PKI update the message returned for 'cert_show' in the case of
an inexistent certificate has changed, causing tests to fail.

The fix is only required for the tests, as the behavior has not changed.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-10-21 15:27:31 -03:00
Rafael Guterres Jeffman
3fa3bf0822 ci: Update ansible-core to 2.18 in CI
The ansible-core version used in the CI pipelines has been updated
from 2.16 to 2.18 to keep the testing environment current.

Additionally, the pull request pipeline has been enhanced to test
against the specific ansible-core versions that are packaged with the
latest stable distributions. This will help ensure that the roles remain
compatible with the versions users are likely to have installed.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-10-17 16:11:49 -03:00
Rafael Guterres Jeffman
7cef44c01d Merge pull request #1387 from t-woerner/Add_capability_sys_admin_to_fix_dbus_broker_in_systemd_258
Add capability sys admin to fix dbus broker in systemd 258
2025-09-18 09:56:15 -03:00
Thomas Woerner
cd7d19bfeb Dockerfiles c8s,c9s,fedora-latest and fedora-rawhide: Install hostname
The hostname command is needed to be able to execute fixnet.service. It
has been missing from some docker files and therefore the script failed
in the -base images. The server images have not been affected as
ipa-client has a requirement for hostname.
2025-09-18 14:54:40 +02:00
Thomas Woerner
0e748d372a infra/image/shdefaults: Add capability SYS_ADMIN for systemd 258
Fedora 43 and 44 switched to systemd 258 rc4. The dbus-broker service
of systemd 258 does not start any more without enabling the capability
SYS_ADMIN.

The capabilities AUDIT_WRITE, SETUID and SETGID should be enough, but
they are not sufficient to be able to start the service.

With final systemd 258 it should be tested if the capability can be
removed again.
2025-09-18 14:54:18 +02:00
Rafael Guterres Jeffman
e24340447d Merge pull request #1379 from t-woerner/backup_test_ansible_2_19_v2
test_backup.yml: Fix evaluation of 'list = False' and 'list = True' v2
2025-08-15 16:29:20 -03:00
Thomas Woerner
092ad81d03 test_backup.yml: Fix evaluation of 'list = False' and 'list = True' v2
ansible-core 2.19 is not automatically converting empty and non empty lists
to bool values. Conditionals must have a boolean result.

The solution is to evaluate the length of the lists instead.
2025-08-15 12:19:21 +02:00
Rafael Guterres Jeffman
4d22e917df Merge pull request #1376 from t-woerner/backup_test_ansible_2_19
test_backup.yml: Fix evaluation of 'list = False' and 'list = True'
2025-08-13 09:54:35 -03:00
Thomas Woerner
a04a357b6a test_backup.yml: Fix evaluation of 'list = False' and 'list = True'
ansible-core 2.19 is not automatically converting empty and non empty lists
to bool values. Conditionals must have a boolean result.

The solution is to evaluate the length of the lists instead.
2025-08-13 14:17:32 +02:00
Thomas Woerner
2081a1a8dd Merge pull request #1369 from rjeffman/prepare_ansible_2_19
Prepare playbooks for ansible core 2.19
2025-08-12 14:30:47 +02:00
Rafael Guterres Jeffman
d1dfdc38c9 tests service: Fixes evaluation of 'Keytab = True'
In ansible-core 2.19 there's no automatic coercion from None or empty
strings to the boolean value "false", so we need to compare the result
of the filter 'regex_search' to 'None' and the empty string to evaluate
if any match occurred.

In fixing this issue, it was found that the tests were incorrectly
evaluating the results, and the comparisons were fixed.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-08-12 08:25:17 -03:00
Rafael Guterres Jeffman
668830fc94 ansible-core 2.19: 'upper' and 'lower' make lists into strings
As ansible-core 2.19 'upper' and 'lower' filters make lists into strings
and these strings are not interpreted as lists when running the plugins,
it is needed to use 'map(<filter>)' to apply the filter to all entries
of a list.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-06-14 11:45:30 -03:00
Rafael Guterres Jeffman
5ae39ec9de ansible-core 2.19: Templates and expressions must use trusted sources
In ansible-core, templates and expressions must use trusted sources,
such as playbooks or roles, and module results are considered untrusted
sources.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-06-14 11:45:26 -03:00
Rafael Guterres Jeffman
3f59332d99 ansible-core 2.19: when clause don't automatically convert to bool
In ansible-core 2.19, when clauses (when, failed_when, etc) do not
convert values to bool automatically, also, templating with "|bool" does
not work too, so an actual value comparison is required.

Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
2025-06-14 11:45:21 -03:00
93 changed files with 3719 additions and 1105 deletions

View File

@@ -23,7 +23,7 @@ kinds:
- tasks: '**/tasks_*.yml'
- tasks: '**/env_*.yml'
parseable: true
# parseable: true
quiet: false

View File

@@ -5,7 +5,7 @@ on:
- pull_request
jobs:
check_docs_oldest_supported:
name: Check Ansible Documentation with ansible-core 2.13.
name: Check Ansible Documentation with ansible-core 2.16.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.1
@@ -14,15 +14,15 @@ jobs:
- uses: actions/setup-python@v5.1.0
with:
python-version: '3.x'
- name: Install Ansible 2.13
- name: Install Ansible 2.16
run: |
python -m pip install "ansible-core >=2.13,<2.14"
python -m pip install "ansible-core >=2.16,<2.17"
- name: Run ansible-doc-test
run: |
ANSIBLE_LIBRARY="." ANSIBLE_DOC_FRAGMENT_PLUGINS="." python utils/ansible-doc-test -v roles plugins
check_docs_previous:
name: Check Ansible Documentation with ansible-core 2.14.
name: Check Ansible Documentation with ansible-core 2.18.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.1
@@ -31,15 +31,15 @@ jobs:
- uses: actions/setup-python@v5.1.0
with:
python-version: '3.x'
- name: Install Ansible 2.14
- name: Install Ansible 2.18
run: |
python -m pip install "ansible-core >=2.14,<2.15"
python -m pip install "ansible-core >=2.18,<2.19"
- name: Run ansible-doc-test
run: |
ANSIBLE_LIBRARY="." ANSIBLE_DOC_FRAGMENT_PLUGINS="." python utils/ansible-doc-test -v roles plugins
check_docs_current:
name: Check Ansible Documentation with ansible-core 2.15.
name: Check Ansible Documentation with ansible-core 2.19.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.1
@@ -48,9 +48,9 @@ jobs:
- uses: actions/setup-python@v5.1.0
with:
python-version: '3.x'
- name: Install Ansible 2.15
- name: Install Ansible 2.20
run: |
python -m pip install "ansible-core >=2.15,<2.16"
python -m pip install "ansible-core <2.20"
- name: Run ansible-doc-test
run: |
ANSIBLE_LIBRARY="." ANSIBLE_DOC_FRAGMENT_PLUGINS="." python utils/ansible-doc-test -v roles plugins

View File

@@ -13,12 +13,12 @@ jobs:
fetch-depth: 0
- uses: actions/setup-python@v5.1.0
with:
python-version: "3.x"
python-version: "3.13"
- name: Run ansible-lint
run: |
pip install "ansible-core>=2.16,<2.17" 'ansible-lint==6.22'
utils/build-galaxy-release.sh -ki
cd .galaxy-build
utils/build-collection.sh -ki rpm
cd .collection-build
ansible-lint --profile production --exclude tests/integration/ --exclude tests/unit/ --parseable --nocolor
yamllint:

View File

@@ -1,7 +1,7 @@
---
repos:
- repo: https://github.com/ansible/ansible-lint.git
rev: v24.5.0
rev: v26.4.0
hooks:
- id: ansible-lint
always_run: false
@@ -18,19 +18,18 @@ repos:
--profile production
--exclude tests/integration/
--exclude tests/unit/
--parseable
--nocolor
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.35.1
rev: v1.38.0
hooks:
- id: yamllint
files: \.(yaml|yml)$
- repo: https://github.com/pycqa/flake8
rev: 7.2.0
rev: 7.3.0
hooks:
- id: flake8
- repo: https://github.com/pycqa/pylint
rev: v3.2.2
rev: v4.0.6
hooks:
- id: pylint
args:

View File

@@ -145,7 +145,7 @@ Variable | Description | Required
`selinuxusermaporder` \| `ipaselinuxusermaporder`| Set ordered list in increasing priority of SELinux users | no
`selinuxusermapdefault`\| `ipaselinuxusermapdefault` | Set default SELinux user when no match is found in SELinux map rule | no
`pac_type` \| `ipakrbauthzdata` | set default types of PAC supported for services (choices: `MS-PAC`, `PAD`, `nfs:NONE`). Use `""` to clear this variable. | no
`user_auth_type` \| `ipauserauthtype` | set default types of supported user authentication (choices: `password`, `radius`, `otp`, `pkinit`, `hardened`, `idp`, `disabled`, `""`). An additional check ensures that only types can be used that are supported by the IPA version. Use `""` to clear this variable. | no
`user_auth_type` \| `ipauserauthtype` | set default types of supported user authentication (choices: `password`, `radius`, `otp`, `pkinit`, `hardened`, `idp`, `passkey`, `disabled`, `""`). An additional check ensures that only types can be used that are supported by the IPA version. Use `""` to clear this variable. | no
`domain_resolution_order` \| `ipadomainresolutionorder` | Set list of domains used for short name qualification | no
`ca_renewal_master_server` \| `ipacarenewalmasterserver`| Renewal master for IPA certificate authority. | no
`enable_sid` | New users and groups automatically get a SID assigned. Cannot be deactivated once activated. Requires IPA 4.9.8+. (bool) | no

View File

@@ -281,6 +281,99 @@ Example playbook to ensure groups are absent:
```
Example playbook to query a group and print the base fields:
```yaml
---
- name: Playbook to query groups
hosts: ipaserver
become: true
tasks:
- name: Query group ops
ipagroup:
ipaadmin_password: SomeADMINpassword
name: ops
state: query
register: result
- name: Print group info
debug:
var: result.group
```
Example playbook to query specific fields of a group:
```yaml
---
- name: Playbook to query groups
hosts: ipaserver
become: true
tasks:
- name: Query description and members of group ops
ipagroup:
ipaadmin_password: SomeADMINpassword
name: ops
query_param:
- description
- gid
- user
state: query
register: result
- name: Print group info
debug:
var: result.group
```
Example playbook to query all fields of a group:
```yaml
---
- name: Playbook to query groups
hosts: ipaserver
become: true
tasks:
- name: Query all fields of group ops
ipagroup:
ipaadmin_password: SomeADMINpassword
name: ops
query_param: ALL
state: query
register: result
- name: Print group info
debug:
var: result.group
```
Example playbook to query only the names of all groups:
```yaml
---
- name: Playbook to query groups
hosts: ipaserver
become: true
tasks:
- name: Query all group names
ipagroup:
ipaadmin_password: SomeADMINpassword
query_param: PKEY_ONLY
state: query
register: result
- name: Print group names
debug:
var: result.group.groups
```
Variables
=========
@@ -306,9 +399,10 @@ Variable | Description | Required
`membermanager_group` | List of member manager groups assigned to this group. Only usable with IPA versions 4.8.4 and up. | no
`externalmember` \| `ipaexternalmember` \| `external_member`| List of members of a trusted domain in DOM\\name or name@domain form. Requires "server" context. | no
`idoverrideuser` | List of user ID overrides to manage. Only usable with IPA versions 4.8.7 and up. Requires "server" context. | no
`rename` \| `new_name` | Rename the user object to the new name string. Only usable with `state: renamed`. | no
`action` | Work on group or member level. It can be on of `member` or `group` and defaults to `group`. | no
`state` | The state to ensure. It can be one of `present`, `absent` or `renamed`, default: `present`. | yes
`rename` \| `new_name` | Rename the group object to the new name string. Only usable with `state: renamed`. | no
`action` | Work on group or member level. It can be one of `member` or `group` and defaults to `group`. | no
`query_param` | The fields to query with `state: query`. Can be `ALL`, `BASE`, `PKEY_ONLY` or a list of specific field names. Only usable with `state: query`. | no
`state` | The state to ensure. It can be one of `present`, `absent`, `renamed` or `query`, default: `present`. | yes
Authors

View File

@@ -354,7 +354,7 @@ Variable | Description | Required
`mac_address` \| `macaddress` | List of hardware MAC addresses. | no
`sshpubkey` \| `ipasshpubkey` | List of SSH public keys | no
`userclass` \| `class` | Host category (semantics placed on this attribute are for local interpretation) | no
`auth_ind` \| `krbprincipalauthind` | Defines an allow list for Authentication Indicators. Use 'otp' to allow OTP-based 2FA authentications. Use 'radius' to allow RADIUS-based 2FA authentications. Use empty string to reset auth_ind to the initial value. Other values may be used for custom configurations. An additional check ensures that only types can be used that are supported by the IPA version. Choices: ["radius", "otp", "pkinit", "hardened", "idp", ""] | no
`auth_ind` \| `krbprincipalauthind` | Defines an allow list for Authentication Indicators. Use 'otp' to allow OTP-based 2FA authentications. Use 'radius' to allow RADIUS-based 2FA authentications. Use empty string to reset auth_ind to the initial value. Other values may be used for custom configurations. An additional check ensures that only types can be used that are supported by the IPA version. Choices: ["radius", "otp", "pkinit", "hardened", "idp", "passkey", ""] | no
`requires_pre_auth` \| `ipakrbrequirespreauth` | Pre-authentication is required for the service (bool) | no
`ok_as_delegate` \| `ipakrbokasdelegate` | Client credentials may be delegated to the service (bool) | no
`ok_to_auth_as_delegate` \| `ipakrboktoauthasdelegate` | The service is allowed to authenticate on behalf of a client (bool) | no

88
README-passkeyconfig.md Normal file
View File

@@ -0,0 +1,88 @@
Passkeyconfig module
============
Description
-----------
The passkeyconfig module allows to manage FreeIPA passkey configuration settings.
Features
--------
* Passkeyconfig management
Supported FreeIPA Versions
--------------------------
FreeIPA versions 4.4.0 and up are supported by the ipapasskeyconfig module.
Requirements
------------
**Controller**
* Ansible version: 2.15+
**Node**
* Supported FreeIPA version (see above)
Usage
=====
Example inventory file
```ini
[ipaserver]
ipaserver.test.local
```
By default, user verification for passkey authentication is turned on (`true`). Example playbook to ensure that the requirement for user verification for passkey authentication is turned off:
```yaml
---
- name: Playbook to manage IPA passkeyconfig.
hosts: ipaserver
become: false
tasks:
- name: Ensure require_user_verification is false
ipapasskeyconfig:
ipaadmin_password: SomeADMINpassword
require_user_verification: false
```
Example playbook to get current passkeyconfig:
```yaml
---
- name: Playbook to get IPA passkeyconfig.
hosts: ipaserver
become: false
tasks:
- name: Retrieve current passkey configuration
ipapasskeyconfig:
ipaadmin_password: SomeADMINpassword
```
Variables
---------
Variable | Description | Required
-------- | ----------- | --------
`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no
`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no
`ipaapi_context` | The context in which the module will execute. Executing in a server context is preferred. If not provided context will be determined by the execution environment. Valid values are `server` and `client`. | no
`ipaapi_ldap_cache` | Use LDAP cache for IPA connection. The bool setting defaults to true. (bool) | no
`require_user_verification` \| `iparequireuserverification` | Require user verification for passkey authentication. (bool) | no
Authors
=======
Rafael Guterres Jeffman

View File

@@ -230,6 +230,8 @@ Example playbook to ensure that different members are not associated with a role
- User Administrators
service:
- service01
sysaccount:
- my-app
action: member
state: absent
```
@@ -253,7 +255,8 @@ Variable | Description | Required
`host` | List of hosts to be assigned or not assigned to the role. | no
`hostgroup` | List of hostgroups to be assigned or not assigned to the role. | no
`service` | List of services to be assigned or not assigned to the role. | no
`action` | Work on role or member level. It can be on of `member` or `role` and defaults to `role`. | no
`sysaccount` | List of sysaccounts to be assigned or not assigned to the role. | no
`action` | Work on role or member level. It can be one of `member` or `role` and defaults to `role`. | no
`state` | The state to ensure. It can be one of `present`, `absent`, default: `present`. | no
@@ -261,3 +264,4 @@ Authors
=======
Rafael Jeffman
Thomas Woerner

View File

@@ -361,7 +361,7 @@ Variable | Description | Required
-------- | ----------- | --------
`certificate` \| `usercertificate` | Base-64 encoded service certificate. | no
`pac_type` \| `ipakrbauthzdata` | Supported PAC type. It can be one of `MS-PAC`, `PAD`, or `NONE`. Use empty string to reset pac_type to the initial value. | no
`auth_ind` \| `krbprincipalauthind` | Defines an allow list for Authentication Indicators. It can be any of `otp`, `radius`, `pkinit`, `hardened`, `idp` or `""`. An additional check ensures that only types can be used that are supported by the IPA version. Use empty string to reset auth_ind to the initial value. | no
`auth_ind` \| `krbprincipalauthind` | Defines an allow list for Authentication Indicators. It can be any of `otp`, `radius`, `pkinit`, `hardened`, `idp`, `passkey` or `""`. An additional check ensures that only types can be used that are supported by the IPA version. Use empty string to reset auth_ind to the initial value. | no
`requires_pre_auth` \| `ipakrbrequirespreauth` | Pre-authentication is required for the service. Default to true. (bool) | no
`ok_as_delegate` \| `ipakrbokasdelegate` | Client credentials may be delegated to the service. Default to false. (bool) | no
`ok_to_auth_as_delegate` \| `ipakrboktoauthasdelegate` | The service is allowed to authenticate on behalf of a client. Default to false. (bool) | no

196
README-sysaccount.md Normal file
View File

@@ -0,0 +1,196 @@
Sysaccount module
============
Description
-----------
The sysaccount module allows to ensure presence and absence of system accounts.
Features
--------
* Sysaccount management
Supported FreeIPA Versions
--------------------------
FreeIPA versions 4.4.0 and up are supported by the ipasysaccount module.
Requirements
------------
**Controller**
* Ansible version: 2.15+
**Node**
* Supported FreeIPA version (see above)
Usage
=====
Example inventory file
```ini
[ipaserver]
ipaserver.test.local
```
Example playbook to make sure sysaccount "my-app" is present with random password:
```yaml
---
- name: Playbook to manage IPA sysaccount.
hosts: ipaserver
become: false
tasks:
- name: Ensure sysaccount "my-app" is present with random password
ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
random: true
register: result
- name: Print generated random password
debug:
var: result.sysaccount.randompassword
```
Example playbook to make sure sysaccount "my-app" is present with given password:
```yaml
---
- name: Playbook to manage IPA sysaccount.
hosts: ipaserver
become: false
tasks:
- name: Ensure sysaccount "my-app" is present with given password
ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
password: SomeAPPpassword
```
Example playbook to make sure sysaccount "my-app" is absent:
```yaml
---
- name: Playbook to manage IPA sysaccount.
hosts: ipaserver
become: false
tasks:
- name: Ensure sysaccount "my-app" is absent
ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
state: absent
```
Example playbook to ensure existing sysaccount my-app is privileged
```yaml
---
- name: Playbook to manage IPA sysaccount.
hosts: ipaserver
become: false
tasks:
- name: Ensure existing sysaccount my-app is privileged
ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
privileged: true
```
Example playbook to ensure existing sysaccount my-app is not privileged
```yaml
---
- name: Playbook to manage IPA sysaccount.
hosts: ipaserver
become: false
tasks:
- name: Ensure existing sysaccount my-app is not privileged
ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
privileged: false
```
Example playbook to ensure existing sysaccount my-app is disabled
```yaml
---
- name: Playbook to manage IPA sysaccount.
hosts: ipaserver
become: false
tasks:
- name: Ensure existing sysaccount my-app is disabled
ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
state: disabled
```
Example playbook to ensure existing sysaccount my-app is enabled
```yaml
---
- name: Playbook to manage IPA sysaccount.
hosts: ipaserver
become: false
tasks:
- name: Ensure existing sysaccount my-app is enabled
ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
state: enabled
```
Variables
---------
Variable | Description | Required
-------- | ----------- | --------
`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no
`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no
`ipaapi_context` | The context in which the module will execute. Executing in a server context is preferred. If not provided context will be determined by the execution environment. Valid values are `server` and `client`. | no
`ipaapi_ldap_cache` | Use LDAP cache for IPA connection. The bool setting defaults to true. (bool) | no
`name` \| `login` | The list of sysaccount name strings - internally uid. (list of strings) | yes
`description` | A description for the sysaccount. (string) | no
`privileged` | Allow password updates without reset. This flag is not replicated. It is needed to set privileged on all servers, where it is needed. (bool) | no
`random` | Generate a random user password. (bool) | no
`password` \| `userpassword` | Set the password. (string) | no
`update_password` | Set password for a sysaccount in present state only on creation or always. It can be one of `always` or `on_create` and defaults to `always`. | no
`state` | The state to ensure. It can be one of `present`, `absent`, 'enabled', 'disabled', default: `present`. | no
Return Values
=============
There are only return values if a random passwords has been generated.
Variable | Description | Returned When
-------- | ----------- | -------------
`sysaccount` | Sysaccount dict (dict) <br>Options: | Always
&nbsp; | `randompassword` - The generated random password | If random is yes and sysaccount did not exist or update_password is yes
Authors
=======
Thomas Woerner

View File

@@ -368,6 +368,100 @@ Example playbook to ensure users are absent:
state: absent
```
Example playbook to query a user and print the base fields:
```yaml
---
- name: Playbook to query users
hosts: ipaserver
become: true
tasks:
- name: Query user pinky
ipauser:
ipaadmin_password: SomeADMINpassword
name: pinky
state: query
register: result
- name: Print user info
debug:
var: result.user
```
Example playbook to query specific fields of a user:
```yaml
---
- name: Playbook to query users
hosts: ipaserver
become: true
tasks:
- name: Query first and last name of user pinky
ipauser:
ipaadmin_password: SomeADMINpassword
name: pinky
query_param:
- first
- last
- email
state: query
register: result
- name: Print user info
debug:
var: result.user
```
Example playbook to query all fields of a user:
```yaml
---
- name: Playbook to query users
hosts: ipaserver
become: true
tasks:
- name: Query all fields of user pinky
ipauser:
ipaadmin_password: SomeADMINpassword
name: pinky
query_param: ALL
state: query
register: result
- name: Print user info
debug:
var: result.user
```
Example playbook to query only the names of all users:
```yaml
---
- name: Playbook to query users
hosts: ipaserver
become: true
tasks:
- name: Query all user names
ipauser:
ipaadmin_password: SomeADMINpassword
query_param: PKEY_ONLY
state: query
register: result
- name: Print user names
debug:
var: result.user.users
```
When using FreeIPA 4.8.0+, SMB logon script, profile, home directory and home drive can be set for users.
In the example playbook to set SMB attributes note that `smb_profile_path` and `smb_home_dir` use paths in UNC format, which includes backslashes ('\\`). If the paths are quoted, the backslash needs to be escaped becoming "\\", so the path `\\server\dir` becomes `"\\\\server\\dir"`. If the paths are unquoted the slashes do not have to be escaped.
@@ -416,7 +510,7 @@ Variable | Description | Required
`update_password` | Set password for a user in present state only on creation or always. It can be one of `always` or `on_create` and defaults to `always`. | no
`preserve` | Delete a user, keeping the entry available for future use. (bool) | no
`action` | Work on user or member level. It can be on of `member` or `user` and defaults to `user`. | no
`state` | The state to ensure. It can be one of `present`, `absent`, `enabled`, `disabled`, `renamed`, `unlocked` or `undeleted`, default: `present`. Only `names` or `users` with only `name` set are allowed if state is not `present`. | yes
`state` | The state to ensure. It can be one of `present`, `absent`, `enabled`, `disabled`, `renamed`, `unlocked`, `undeleted` or `query`, default: `present`. Only `names` or `users` with only `name` set are allowed if state is not `present`. | yes
@@ -452,7 +546,7 @@ Variable | Description | Required
`manager` | List of manager user names. | no
`carlicense` | List of car licenses. | no
`sshpubkey` \| `ipasshpubkey` | List of SSH public keys. | no
`userauthtype` \| `ipauserauthtype` | List of supported user authentication types. Choices: `password`, `radius`, `otp`, `pkinit`, `hardened`, `idp` and `""`. An additional check ensures that only types can be used that are supported by the IPA version. Use empty string to reset userauthtype to the initial value. | no
`userauthtype` \| `ipauserauthtype` | List of supported user authentication types. Choices: `password`, `radius`, `otp`, `pkinit`, `hardened`, `idp`, `passkey` and `""`. An additional check ensures that only types can be used that are supported by the IPA version. Use empty string to reset userauthtype to the initial value. | no
`userclass` | User category. (semantics placed on this attribute are for local interpretation). | no
`radius` | RADIUS proxy configuration | no
`radiususer` | RADIUS proxy username | no

View File

@@ -38,6 +38,7 @@ Features
* Modules for idview management
* Modules for location management
* Modules for netgroup management
* Modules for passkeyconfig management
* Modules for permission management
* Modules for privilege management
* Modules for pwpolicy management
@@ -50,6 +51,7 @@ Features
* Modules for sudocmd management
* Modules for sudocmdgroup management
* Modules for sudorule management
* Modules for sysaccount management
* Modules for topology management
* Modules for trust management
* Modules for user management
@@ -453,6 +455,7 @@ Modules in plugin/modules
* [idview](README-idview.md)
* [ipalocation](README-location.md)
* [ipanetgroup](README-netgroup.md)
* [ipapasskeyconfig](README-passkeyconfig.md)
* [ipapermission](README-permission.md)
* [ipaprivilege](README-privilege.md)
* [ipapwpolicy](README-pwpolicy.md)
@@ -465,6 +468,7 @@ Modules in plugin/modules
* [ipasudocmd](README-sudocmd.md)
* [ipasudocmdgroup](README-sudocmdgroup.md)
* [ipasudorule](README-sudorule.md)
* [ipasysaccount](README-sysaccount.md)
* [ipatopologysegment](README-topology.md)
* [ipatopologysuffix](README-topology.md)
* [ipatrust](README-trust.md)

View File

@@ -6,7 +6,7 @@ pool:
vmImage: 'ubuntu-24.04'
variables:
ansible_version: "-core >=2.16,<2.17"
ansible_version: "-core >=2.18,<2.19"
ansible_latest: "-core"
ansible_minimum: "-core <2.16"
distros: "fedora-latest,c9s,c10s,fedora-rawhide"
@@ -36,7 +36,7 @@ stages:
# Supported distros
- ${{ each distro in split(variables.distros, ',') }}:
- stage: ${{ replace(distro, '-', '_') }}_ansible_2_16
- stage: ${{ replace(distro, '-', '_') }}_ansible_2_18
dependsOn: []
jobs:
- template: templates/group_tests.yml
@@ -49,7 +49,7 @@ stages:
# Galaxy on Fedora
- stage: galaxy_fedora_latest_ansible_2_16
- stage: galaxy_fedora_latest_ansible_2_18
dependsOn: []
jobs:
- template: templates/group_tests.yml

View File

@@ -12,13 +12,24 @@ trigger: none
pool:
vmImage: 'ubuntu-24.04'
parameters:
# Not really a parameter, but variables cannot be arrays or dicts
# This maps the distro LATEST version to the avaiable ansible-core
# version of the latest released compose.
- name: "distro_ansible_map"
type: object
default:
- { distro: "c8s", ansible_version: "<2.17", version_name: "2.16" }
# c9s should use 2.14, but this version has an invalid certificate
# and so is unsuable against ansible-galaxy.
- { distro: "c9s", ansible_version: "<2.17", version_name: "2.16" }
- { distro: "c10s", ansible_version: "<2.17", version_name: "2.16" }
variables:
# We need to have two sets, as c8s is not supported by all ansible versions
recent_distros: "fedora-latest,fedora-rawhide,c10s,c9s"
distros: "fedora-latest,fedora-rawhide,c10s,c9s,c8s"
distros: "fedora-latest,c10s,c9s,fedora-rawhide"
ansible_version: "-core >=2.18,<2.19"
ansible_latest: "-core"
ansible_minimum: "-core <2.16"
ansible_version: "-core >=2.16,<2.17"
stages:
@@ -38,7 +49,7 @@ stages:
# Latest ansible
- ${{ each distro in split(variables.recent_distros, ',') }}:
- ${{ each distro in split(variables.distros, ',') }}:
- stage: ${{ replace(distro, '-', '_') }}_ansible_latest
dependsOn: []
jobs:
@@ -50,30 +61,44 @@ stages:
skip_git_test: true
test_galaxy: false
# Selected ansible-core version
# Galaxy with Latest ansible
- ${{ each distro in split(variables.distros, ',') }}:
- stage: ${{ replace(distro, '-', '_') }}_ansible_2_16
- stage: galaxy_${{ replace(distro, '-', '_') }}_ansible_latest
dependsOn: []
jobs:
- template: templates/group_tests.yml
parameters:
build_number: $(Build.BuildNumber)
distro: ${{ distro }}
ansible_version: ${{ variables.ansible_version }}
ansible_version: ${{ variables.ansible_latest }}
skip_git_test: true
test_galaxy: true
# Test with pinned ansible version for the distro
- ${{ each config in parameters.distro_ansible_map }}:
- stage: ${{ config.distro }}_distro_ansible_${{ replace(config.version_name, '.', '_') }}
dependsOn: []
jobs:
- template: templates/group_tests.yml
parameters:
build_number: $(Build.BuildNumber)
distro: ${{ config.distro }}
ansible_version: -core${{ config.ansible_version }}
skip_git_test: true
test_galaxy: false
# Galaxy collection with selected ansible-core version
# Test Galaxy collection with pinned ansible version for the distro
- ${{ each distro in split(variables.distros, ',') }}:
- stage: galaxy_${{ replace(distro, '-', '_') }}_asible_2_16
- ${{ each config in parameters.distro_ansible_map }}:
- stage: galaxy_${{ config.distro }}_distro_ansible_${{ replace(config.version_name, '.', '_') }}
dependsOn: []
jobs:
- template: templates/group_tests.yml
parameters:
build_number: $(Build.BuildNumber)
distro: ${{ distro }}
ansible_version: ${{ variables.ansible_version }}
distro: ${{ config.distro }}
ansible_version: -core${{ config.ansible_version }}
skip_git_test: true
test_galaxy: true

View File

@@ -5,16 +5,29 @@ trigger:
pool:
vmImage: 'ubuntu-24.04'
parameters:
# Not really a parameter, but variables cannot be arrays or dicts
# This maps the distro LATEST version to the avaiable ansible-core
# version of the latest released compose.
- name: "distro_ansible_map"
type: object
default:
- { distro: "c8s", ansible_version: "<2.17", version_name: "2.16" }
# c9s should use 2.14, but this version has an invalid certificate
# and so is unsuable against ansible-galaxy.
- { distro: "c9s", ansible_version: "<2.17", version_name: "2.16" }
- { distro: "c10s", ansible_version: "<2.17", version_name: "2.16" }
variables:
distros: "fedora-latest,c10s,c9s,c8s,fedora-rawhide"
ansible_version: "-core >=2.15,<2.16"
distros: "fedora-latest,c10s,c9s,fedora-rawhide"
ansible_version: "-core >=2.18,<2.19"
stages:
# Test with repository in all distros
# Test with repository in all "current" distros
- ${{ each distro in split(variables.distros, ',') }}:
- stage: ${{ replace(distro, '-', '_') }}_ansible_2_16
- stage: ${{ replace(distro, '-', '_') }}_ansible_2_18
dependsOn: []
jobs:
- template: templates/run_tests.yml
@@ -27,7 +40,7 @@ stages:
# Galaxy on Fedora
- stage: galaxy_fedora_latest_ansible_2_16
- stage: galaxy_fedora_latest_ansible_2_18
dependsOn: []
jobs:
- template: templates/run_tests.yml
@@ -37,3 +50,18 @@ stages:
ansible_version: ${{ variables.ansible_version }}
skip_git_test: false
test_galaxy: true
# Test with pinned ansible version for the distro
- ${{ each config in parameters.distro_ansible_map }}:
- stage: ${{ config.distro }}_distro_ansible_${{ replace(config.version_name, '.', '_') }}
dependsOn: []
jobs:
- template: templates/run_tests.yml
parameters:
build_number: $(Build.BuildNumber)
distro: ${{ config.distro }}
ansible_version: -core${{ config.ansible_version }}
skip_git_test: false
test_galaxy: false

View File

@@ -54,7 +54,7 @@ jobs:
- script: |
git fetch --unshallow
utils/build-galaxy-release.sh -i
utils/build-collection.sh -i rpm
retryCountOnTaskFailure: 5
displayName: Build Galaxy release
condition: ${{ parameters.test_galaxy }}

View File

@@ -12,6 +12,7 @@ dnf --assumeyes install \
bash \
systemd \
procps-ng \
hostname \
iproute; \
dnf clean all; \
rm -rf /var/cache/dnf/;

View File

@@ -9,6 +9,7 @@ dnf --assumeyes install \
bash \
systemd \
procps-ng \
hostname \
iproute; \
rm -rf /var/cache/dnf/;

View File

@@ -11,6 +11,7 @@ dnf --assumeyes install \
bash \
systemd \
procps-ng \
hostname \
iproute; \
dnf clean all; \
rm -rf /var/cache/dnf/;

View File

@@ -11,6 +11,7 @@ dnf --assumeyes install \
bash \
systemd \
procps-ng \
hostname \
iproute; \
dnf clean all; \
rm -rf /var/cache/dnf/;

View File

@@ -228,3 +228,22 @@ container_tee() {
rm "${tmpfile}"
echo
}
container_save() {
local name=${1}
archive="${name}.tar"
log info "= Saving ${name} to ${archive} ="
# podman is not able to overwrite the archive
[ -f "${archive}" ] && rm "${archive}"
podman save -o "${archive}" "${name}"
echo
}
container_load() {
local name=${1}
image_name=$(podman load -q -i "${name}" | sed -e "s/^Loaded image: //")
image=$(podman image list -q "${image_name}")
echo "$image"
}

View File

@@ -6,4 +6,6 @@
CAP_DEFAULTS=(
"+DAC_READ_SEARCH" # Required for SSSD
"+SYS_PTRACE" # Required for debugging
"+SYS_ADMIN" # Required to make dbus-brokder for systemd 258 work
# Should be "+AUDIT_WRITE", "+SETUID", "+SETGID"
)

View File

@@ -0,0 +1,10 @@
---
- name: Passkeyconfig example
hosts: ipaserver
become: no
tasks:
- name: Set passkeyconfig require_user_verification to false
ipapasskeyconfig:
ipaadmin_password: SomeADMINpassword
require_user_verification: false

View File

@@ -0,0 +1,14 @@
---
- name: Passkeyconfig get current configuration example
hosts: ipaserver
become: true
tasks:
- name: Get current passkey configuration
ipapasskeyconfig:
ipaadmin_password: SomeADMINpassword
register: result
- name: Display current passkey configuration
ansible.builtin.debug:
var: result.passkeyconfig

View File

@@ -0,0 +1,11 @@
---
- name: Sysaccount example
hosts: ipaserver
become: false
tasks:
- name: Ensure sysaccount my-app is absent
ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
state: absent

View File

@@ -0,0 +1,11 @@
---
- name: Sysaccount example
hosts: ipaserver
become: false
tasks:
- name: Ensure sysaccount my-app is disabled
ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
state: disabled

View File

@@ -0,0 +1,11 @@
---
- name: Sysaccount example
hosts: ipaserver
become: false
tasks:
- name: Ensure sysaccount my-app is enabled
ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
state: enabled

View File

@@ -0,0 +1,11 @@
---
- name: Sysaccount example
hosts: ipaserver
become: false
tasks:
- name: Ensure sysaccount my-app is present with random password
ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
random: true

View File

@@ -0,0 +1,11 @@
---
- name: Sysaccount example
hosts: ipaserver
become: false
tasks:
- name: Ensure sysaccount my-app is privileged
ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
privileged: true

View File

@@ -0,0 +1,11 @@
---
- name: Sysaccount example
hosts: ipaserver
become: false
tasks:
- name: Ensure sysaccount my-app is not privileged
ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
privileged: false

View File

@@ -107,7 +107,7 @@ from ansible.plugins.inventory import BaseInventoryPlugin
from ansible.module_utils.six.moves.urllib.parse import quote
class InventoryModule(BaseInventoryPlugin):
class InventoryModule(BaseInventoryPlugin): # pylint: disable=R0901
NAME = 'freeipa'

View File

@@ -666,6 +666,108 @@ def gen_intersection_list(user_list, res_list):
return list(set(res_list or []).intersection(set(user_list or [])))
def gen_member_add_del_lists(param_mapping, params, res_find,
action, state):
"""
Compute member add/del lists for all member params in param_mapping.
Scans param_mapping for entries with "member": True and computes
add and del lists based on action and state:
- action != "member", state == "present": sync mode using
gen_add_del_lists (both additions and removals)
- action == "member", state == "present": add-only mode using
gen_add_list
- action == "member", state == "absent": remove-only mode using
gen_intersection_list
For any other action/state combination an empty dict is returned.
Parameters
----------
param_mapping: dict
The module's PARAM_MAPPING dict. Entries with "member": True
are processed.
params: dict
Extracted parameter values keyed by ansible parameter name.
res_find: dict
The IPA find result for the entity. May be an empty dict for
newly created entities.
action: str
The module's action value ("member" or the entity action name).
state: str
The module's state value ("present" or "absent").
Returns
-------
dict
Mapping of ansible parameter names to (add_list, del_list)
tuples.
"""
if state == "present" and action != "member":
mode = "sync"
elif state == "present" and action == "member":
mode = "add"
elif state == "absent" and action == "member":
mode = "remove"
else:
return {}
result = {}
for ansible_name, entry in param_mapping.items():
if not entry.get("member"):
continue
value = params.get(ansible_name)
find_key = entry.get("api_name", ansible_name)
existing = res_find.get(find_key)
if mode == "sync":
result[ansible_name] = gen_add_del_lists(value, existing)
elif mode == "add":
result[ansible_name] = (
gen_add_list(value, existing), [])
else:
result[ansible_name] = (
[], gen_intersection_list(value, existing))
return result
def gen_member_args_from_mapping(param_mapping, params):
"""
Build a member args dict for compare_args_ipa() from param_mapping.
Scans param_mapping for entries with "member": True and builds a
dict mapping the IPA attribute name (api_name) to the parameter
value for all member params where the value is not None.
This replaces per-module gen_member_args() functions.
Parameters
----------
param_mapping: dict
The module's PARAM_MAPPING dict.
params: dict
Extracted parameter values keyed by ansible parameter name.
Returns
-------
dict
IPA attribute names as keys, parameter values as values.
Suitable for passing to compare_args_ipa().
"""
_args = {}
for ansible_name, entry in param_mapping.items():
if not entry.get("member"):
continue
value = params.get(ansible_name)
if value is not None:
find_key = entry.get("api_name", ansible_name)
_args[find_key] = value
return _args
def encode_certificate(cert):
"""
Encode a certificate using base64.
@@ -1226,7 +1328,8 @@ class IPAAnsibleModule(AnsibleModule):
return module_params_get_with_type_cast(
self, name, datatype, allow_empty)
def params_fail_used_invalid(self, invalid_params, state, action=None):
def params_fail_used_invalid(self, invalid_params, state, action=None,
params=None, param_mapping=None):
"""
Fail module execution if one of the invalid parameters is not None.
@@ -1238,6 +1341,12 @@ class IPAAnsibleModule(AnsibleModule):
State being tested.
action:
Action being tested (optional).
params:
Extracted params dict to check (optional, defaults to
self.params).
param_mapping:
Parameter mapping dict (optional). When provided, params
marked with "module_param" are checked via self.params.
"""
if action is None:
@@ -1246,8 +1355,15 @@ class IPAAnsibleModule(AnsibleModule):
msg = "Argument '{0}' can not be used with action "\
"'{2}' and state '{1}'"
_params = params if params is not None else self.params
for param in invalid_params:
if self.params.get(param) is not None:
if param_mapping is not None \
and param in param_mapping \
and param_mapping[param].get("module_param"):
value = self.params.get(param)
else:
value = _params.get(param)
if value is not None:
self.fail_json(msg=msg.format(param, state, action))
def ipa_command(self, command, name, args):
@@ -1605,6 +1721,301 @@ class IPAAnsibleModule(AnsibleModule):
return changed
@staticmethod
def _parse_mapping_entry(ansible_name, entry):
"""
Return (ansible_name, ipa_name) from a dict-of-dicts mapping entry.
If entry has "api_name", that is the IPA attribute name.
Otherwise, the IPA name equals the ansible name.
"""
return ansible_name, entry.get("api_name", ansible_name)
@staticmethod
def gen_args_from_mapping(param_mapping, params):
"""
Generate IPA command args dict from a parameter mapping.
Parameters
----------
param_mapping: dict
The module's parameter mapping dict. Keys are ansible param
names, values are dicts with optional keys: "api_name",
"nonempty_list", "convert_to", "gen_args", "return_only".
params: dict
Parameter values keyed by ansible name.
Returns
-------
dict
IPA attribute names as keys, suitable for IPA API commands
and compare_args_ipa().
"""
_args = {}
for ansible_name, entry in param_mapping.items():
if entry.get("gen_args") is False \
or entry.get("return_only") \
or entry.get("module_param"):
continue
ipa_name = entry.get("api_name", ansible_name)
value = params.get(ansible_name)
if value is None:
continue
if entry.get("nonempty_list") and len(value) == 0:
continue
convert = entry.get("convert_to")
if convert == "text":
value = to_text(str(value))
_args[ipa_name] = value
return _args
@staticmethod
def extract_params(module, param_mapping):
"""Extract parameter values from module params using mapping."""
params = {}
for ansible_name, entry in param_mapping.items():
if entry.get("return_only") or entry.get("module_param"):
continue
type_cast = entry.get("type_cast")
if type_cast is not None:
params[ansible_name] = module.params_get_with_type_cast(
ansible_name, type_cast,
allow_empty=entry.get("allow_empty", False))
elif entry.get("lowercase"):
allow_empty = entry.get("allow_empty_list_item")
params[ansible_name] = module.params_get_lowercase(
ansible_name, allow_empty_list_item=bool(allow_empty))
elif entry.get("allow_empty_list_item"):
params[ansible_name] = module.params_get(
ansible_name, allow_empty_list_item=True)
else:
params[ansible_name] = module.params_get(ansible_name)
return params
@staticmethod
def extract_params_from_entry(entry_dict, param_mapping):
"""Extract parameter values from a dict using mapping."""
params = {}
for ansible_name, entry in param_mapping.items():
if entry.get("return_only") or entry.get("module_param"):
continue
value = entry_dict.get(ansible_name)
if value is not None and entry.get("lowercase"):
value = convert_param_value_to_lowercase(value)
params[ansible_name] = value
return params
@staticmethod
def build_query_param_settings(param_mapping, query_fields):
"""
Build query_param_settings from a parameter mapping.
Parameters
----------
param_mapping: dict
The module's parameter mapping dict.
query_fields: list of str
Ansible-name fields for the BASE query set.
Returns
-------
dict
Dict with "ALL", "BASE", "mapping", and "param_mapping"
keys, compatible with execute_query().
"""
all_fields = []
mapping = {}
for ansible_name, entry in param_mapping.items():
if entry.get("query") is False \
or entry.get("module_param"):
continue
all_fields.append(ansible_name)
ipa_name = entry.get("api_name", ansible_name)
if ansible_name != ipa_name:
mapping[ansible_name] = ipa_name
return {
"ALL": all_fields,
"BASE": query_fields.get("base", []),
"PRIMARY_KEY": query_fields.get("primary_key", []),
"PREFIX": query_fields.get("prefix"),
"mapping": mapping,
"param_mapping": param_mapping,
}
def _extract_query_fields(self, result, fields, mapping,
param_mapping=None):
"""
Extract query fields from an IPA result using name mapping.
Parameters
----------
result: dict
The IPA result dict (after convert_result if provided).
fields: list of str
The Ansible-friendly field names to extract.
mapping: dict
Mapping of ansible_name -> ipa_attribute_name.
param_mapping: dict or None
The full parameter mapping dict. When provided, entry
metadata such as "type" is used for value conversion.
Returns
-------
dict
Extracted fields with Ansible-friendly names as keys.
"""
output = {}
for field in fields:
ipa_field = mapping.get(field, field)
if ipa_field in result:
value = result[ipa_field]
if param_mapping is not None:
entry = param_mapping.get(field, {})
field_type = entry.get("type")
try:
if field_type == "bool":
if isinstance(
value,
(str, unicode) # pylint: disable=W0012,E0606
):
value = value.lower() in ("true", "1", "yes")
else:
value = bool(value)
elif field_type == "int":
value = int(value)
except (ValueError, TypeError) as e:
self.fail_json(
msg="Parameter '%s' could not be converted "
"to %s: %s" % (field, field_type, str(e))
)
output[field] = value
return output
def execute_query(self, names, query_param, find_command,
query_param_settings, convert_result=None):
"""
Execute query state.
Parameters
----------
names: list of str or None
The item names to query. If None or empty, all items are
queried using find_command(module, None).
prefix: str
The grouping key for name-only results (e.g., "users").
name_ipa_param: str
The IPA attribute name for the item name (e.g., "uid").
query_param: list of str or None
The fields to return. ["ALL"] for all fields, ["BASE"] for
base fields, a custom list for specific fields, or None to
return only item names.
find_command: callable
Module function: find_command(module, name) returning a dict
for a single item or a list of dicts for all items (when
name is None). Returns None if not found.
query_param_settings: dict
Module-defined dict with "ALL" (list of all Ansible field
names), "BASE" (list of essential field names), and
"mapping" (dict of ansible_name -> ipa_attribute_name for
names that differ).
convert_result: callable or None
Optional function to convert a raw IPA result dict to
Ansible-friendly values (e.g., unwrap single-element lists,
encode certificates). Applied to each result before field
extraction. Default: None.
Returns
-------
dict
The query results structured for exit_json.
"""
if query_param is not None and \
set(query_param).intersection(["PKEY_ONLY", "BASE", "ALL"]):
if len(query_param) > 1:
self.fail_json(
msg="query_params PKEY_ONLY, BASE or ALL can "
"only be used alone"
)
exit_args = {}
mapping = query_param_settings.get("mapping", {})
param_mapping = query_param_settings.get("param_mapping")
prefix = query_param_settings.get("PREFIX")
name_ipa_param = query_param_settings.get("PRIMARY_KEY")
# Resolve query_param to a concrete field list
if query_param == ["PKEY_ONLY"]:
resolved_fields = None
elif query_param == ["BASE"]:
# explicit BASE
resolved_fields = query_param_settings["BASE"]
elif query_param == ["ALL"]:
resolved_fields = query_param_settings["ALL"]
elif query_param is not None:
resolved_fields = query_param
else:
# Use BASE always if query_param is not set
resolved_fields = query_param_settings["BASE"]
# Validate requested fields
if resolved_fields is not None:
all_fields = query_param_settings["ALL"]
for field in resolved_fields:
if field not in all_fields:
self.fail_json(
msg="query_param '%s' is not supported" % field)
# If no names have been given, use [None] to get all items with the
# find command
_names = names
if _names is None or not isinstance(_names, list):
_names = [None]
single_item = len(_names) == 1
# For all names in _names convert results and add the requested
# fields to exit_args
for name in _names:
results = find_command(self, name)
if not isinstance(results, list):
results = [results]
for result in results:
if result is None:
continue
if convert_result is not None:
result = convert_result(result)
if name_ipa_param not in result:
self.fail_json(
msg="execute_query: primary key '%s' missing "
"from result for '%s'" % (name_ipa_param, name)
)
item_name = result[name_ipa_param]
if query_param == ["PKEY_ONLY"]:
exit_args.setdefault(prefix, []).append(item_name)
else:
fields = self._extract_query_fields(
result, resolved_fields, mapping,
param_mapping)
if name is not None:
if single_item:
exit_args = fields
else:
exit_args[name] = fields
else:
exit_args[item_name] = fields
return exit_args
def tm_warn(self, warning):
ts = time.time()
# pylint: disable=super-with-arguments

View File

@@ -161,7 +161,7 @@ options:
type: list
elements: str
choices: ["password", "radius", "otp", "pkinit", "hardened", "idp",
"disabled", ""]
"passkey", "disabled", ""]
aliases: ["ipauserauthtype"]
ca_renewal_master_server:
description: Renewal master for IPA certificate authority.
@@ -426,7 +426,7 @@ def main():
user_auth_type=dict(type="list", elements="str", required=False,
choices=["password", "radius", "otp",
"pkinit", "hardened", "idp",
"disabled", ""],
"passkey", "disabled", ""],
aliases=["ipauserauthtype"]),
ca_renewal_master_server=dict(type="str", required=False),
domain_resolution_order=dict(type="list", elements="str",

View File

@@ -1454,11 +1454,13 @@ def define_commands_for_present_state(module, zone_name, entry, res_find):
# Create reverse records for existing records
for ipv in ['a', 'aaaa']:
record = '%srecord' % ipv
if record in args and ('%s_extra_create_reverse' % ipv) in args:
if (
record in args
and args.pop('%s_extra_create_reverse' % ipv, False)
):
cmds = create_reverse_ip_record(
module, zone_name, name, args[record])
_commands.extend(cmds)
del args['%s_extra_create_reverse' % ipv]
for record, fields in _RECORD_PARTS.items():
part_fields = [f for f in fields if f in args]
if part_fields:
@@ -1620,7 +1622,6 @@ def main():
commands.extend(cmds)
# Execute commands
changed = ansible_module.execute_ipa_commands(
commands, exception_handler=exception_handler)

View File

@@ -200,6 +200,17 @@ options:
required: false
type: list
elements: str
query_param:
description:
- The fields to query with state=query.
- Can be `ALL`, `BASE`, `PKEY_ONLY` or a list of specific field names.
required: false
type: list
elements: str
choices: ["ALL", "BASE", "PKEY_ONLY", "dn", "objectclass", "ipauniqueid",
"ipantsecurityidentifier", "name", "description", "gid", "user",
"group", "service", "externalmember", "idoverrideuser",
"membermanager_user", "membermanager_group"]
action:
description: Work on group or member level
type: str
@@ -214,7 +225,8 @@ options:
description: State to ensure
type: str
default: present
choices: ["present", "absent", "renamed"]
choices: ["present", "absent", "renamed",
"query"]
author:
- Thomas Woerner (@t-woerner)
"""
@@ -322,6 +334,39 @@ EXAMPLES = """
ipaadmin_password: SomeADMINpassword
name: sysops,appops,ops, nongroup
state: absent
# Query base fields of a group
- ipagroup:
ipaadmin_password: SomeADMINpassword
name: ops
state: query
register: result
# Query specific fields of a group
- ipagroup:
ipaadmin_password: SomeADMINpassword
name: ops
query_param:
- description
- gid
- user
state: query
register: result
# Query all fields of a group
- ipagroup:
ipaadmin_password: SomeADMINpassword
name: ops
query_param: ALL
state: query
register: result
# Query only the names of all groups
- ipagroup:
ipaadmin_password: SomeADMINpassword
query_param: PKEY_ONLY
state: query
register: result
"""
RETURN = """
@@ -330,8 +375,8 @@ RETURN = """
from ansible.module_utils._text import to_text
from ansible.module_utils.ansible_freeipa_module import \
IPAAnsibleModule, compare_args_ipa, gen_add_del_lists, \
gen_add_list, gen_intersection_list, api_check_param, \
convert_to_sid
gen_add_list, gen_intersection_list, gen_member_add_del_lists, \
api_check_param, convert_to_sid, ipalib_errors
from ansible.module_utils import six
if six.PY3:
unicode = str
@@ -348,59 +393,67 @@ else:
"deepcopy" in baseldap.LDAPObject.__json__.__code__.co_names
def find_group(module, name):
_args = {
"all": True,
"cn": name,
}
def group_show(module, name):
_args = {"all": True}
_result = module.ipa_command("group_find", name, _args)
try:
_result = module.ipa_command("group_show", name, _args).get("result")
except ipalib_errors.NotFound:
return None
if len(_result["result"]) > 1:
module.fail_json(
msg="There is more than one group '%s'" % (name))
elif len(_result["result"]) == 1:
_res = _result["result"][0]
# The returned services are of type ipapython.kerberos.Principal,
# also services are not case sensitive. Therefore services are
# converted to lowercase strings to be able to do the comparison.
if "member_service" in _res:
_res["member_service"] = \
[to_text(svc).lower() for svc in _res["member_service"]]
return _res
return None
# The returned services are of type ipapython.kerberos.Principal,
# also services are not case sensitive. Therefore services are
# converted to lowercase strings to be able to do the comparison.
if "member_service" in _result:
_result["member_service"] = \
[to_text(svc).lower() for svc in _result["member_service"]]
# user_find is returning SIDs, but user_show is not. Therefore convert
# external users to SIDs.
if "ipaexternalmember" in _result:
_result["ipaexternalmember"] = \
convert_to_sid(_result["ipaexternalmember"])
return _result
def gen_args(description, gid, nomembers):
_args = {}
if description is not None:
_args["description"] = description
if gid is not None:
_args["gidnumber"] = gid
if nomembers is not None:
_args["nomembers"] = nomembers
return _args
def query_convert_result(module, res):
_res = {}
for key in res:
try:
if key.startswith("member_") or key.startswith("membermanager_"):
_res[key] = [to_text(svc) for svc in res[key]]
elif isinstance(res[key], (list, tuple)):
if len(res[key]) == 1:
_res[key] = to_text(res[key][0])
else:
_res[key] = [to_text(item) for item in res[key]]
elif key in ["gidnumber"]:
_res[key] = int(res[key])
else:
_res[key] = to_text(res[key])
except (TypeError, ValueError) as e:
module.fail_json(
msg="Failed to convert query result for '%s': %s"
% (key, str(e)))
return _res
def gen_member_args(user, group, service, externalmember, idoverrideuser):
_args = {}
if user is not None:
_args["member_user"] = user
if group is not None:
_args["member_group"] = group
if service is not None:
_args["member_service"] = service
if externalmember is not None:
_args["member_external"] = externalmember
if idoverrideuser is not None:
_args["member_idoverrideuser"] = idoverrideuser
def group_find(module, name):
_args = {"all": True}
return _args
try:
if name:
_args["cn"] = name
_result = module.ipa_command_no_name(
"group_find", _args).get("result")
if _result and name:
_result = _result[0]
except ipalib_errors.NotFound:
return None
return _result
def check_parameters(module, state, action):
def check_parameters(module, state, action, group_params):
invalid = ["description", "gid", "posix", "nonposix", "external",
"nomembers"]
if action == "group":
@@ -415,7 +468,14 @@ def check_parameters(module, state, action):
invalid.extend(["user", "group", "service", "externalmember"])
else:
invalid.append("rename")
module.params_fail_used_invalid(invalid, state, action)
if state == "query":
module.fail_json(
msg="check_parameters can not be used with action query.")
invalid.append("query_param")
module.params_fail_used_invalid(invalid, state, action, group_params,
PARAM_MAPPING)
def is_external_group(res_find):
@@ -445,6 +505,75 @@ def check_objectclass_args(module, res_find, posix, external):
"`non-posix`.")
def convert_params(module, group_params):
"""Convert parameter values in group_params in-place."""
nonposix = group_params.get("nonposix")
external = group_params.get("external")
posix = group_params.get("posix")
if all((posix, nonposix)) or \
all((posix, external)) or \
all((nonposix, external)):
module.fail_json(
msg="parameters are mutually exclusive for group "
"`{0}`: posix|nonposix|external".format(
group_params.get("name")))
if external is False:
module.fail_json(msg="group can not be non-external")
if nonposix is not None:
group_params["posix"] = not nonposix
PARAM_MAPPING = {
# Read-only system fields
"dn": {"return_only": True},
"objectclass": {"return_only": True},
"ipauniqueid": {"return_only": True},
"ipantsecurityidentifier": {"return_only": True},
# Query-only: name is the primary key
"name": {"api_name": "cn", "gen_args": False},
# Writable params (used in gen_args)
"description": {},
"gid": {"api_name": "gidnumber", "type": "int"},
# Query-only: members handled via separate member commands
"user": {"api_name": "member_user", "gen_args": False,
"lowercase": True, "member": True},
"group": {"api_name": "member_group", "gen_args": False,
"lowercase": True, "member": True},
"service": {"api_name": "member_service", "gen_args": False,
"lowercase": True, "member": True},
"externalmember": {"api_name": "ipaexternalmember", "gen_args": False},
"idoverrideuser": {"api_name": "member_idoverrideuser",
"gen_args": False},
"membermanager_user": {"gen_args": False, "lowercase": True,
"member": True},
"membermanager_group": {"gen_args": False, "lowercase": True,
"member": True},
# Writable params not queryable by name
"rename": {"gen_args": False, "query": False},
"nonposix": {"gen_args": False, "query": False},
"external": {"gen_args": False, "query": False},
"posix": {"gen_args": False, "query": False},
"nomembers": {"query": False},
# Module-level params (not per-item, checked via self.params)
"query_param": {"module_param": True},
}
QUERY_FIELDS = {
"prefix": "groups",
"primary_key": "cn",
"base": ["name", "description", "gid"]
}
def main():
group_spec = dict(
# present
@@ -475,6 +604,11 @@ def main():
rename=dict(type="str", required=False, default=None,
aliases=["new_name"]),
)
query_param_settings = IPAAnsibleModule.build_query_param_settings(
PARAM_MAPPING, QUERY_FIELDS
)
ansible_module = IPAAnsibleModule(
argument_spec=dict(
# general
@@ -491,11 +625,16 @@ def main():
),
elements='dict',
required=False),
# query
query_param=dict(type="list", elements="str", default=None,
choices=["ALL", "BASE", "PKEY_ONLY"]
+ query_param_settings["ALL"],
required=False),
# general
action=dict(type="str", default="group",
choices=["member", "group"]),
state=dict(type="str", default="present",
choices=["present", "absent", "renamed"]),
choices=["present", "absent", "renamed", "query"]),
# Add group specific parameters for simple use case
**group_spec
@@ -504,7 +643,6 @@ def main():
# same time
mutually_exclusive=[['posix', 'nonposix', 'external'],
["name", "groups"]],
required_one_of=[["name", "groups"]],
supports_check_mode=True,
)
@@ -516,34 +654,27 @@ def main():
names = ansible_module.params_get("name")
groups = ansible_module.params_get("groups")
# present
description = ansible_module.params_get("description")
gid = ansible_module.params_get("gid")
nonposix = ansible_module.params_get("nonposix")
external = ansible_module.params_get("external")
idoverrideuser = ansible_module.params_get("idoverrideuser")
posix = ansible_module.params_get("posix")
nomembers = ansible_module.params_get("nomembers")
user = ansible_module.params_get_lowercase("user")
group = ansible_module.params_get_lowercase("group")
# Services are not case sensitive
service = ansible_module.params_get_lowercase("service")
membermanager_user = (
ansible_module.params_get_lowercase("membermanager_user"))
membermanager_group = (
ansible_module.params_get_lowercase("membermanager_group"))
externalmember = ansible_module.params_get("externalmember")
# rename
rename = ansible_module.params_get("rename")
# query
query_param = ansible_module.params_get("query_param")
# state and action
action = ansible_module.params_get("action")
state = ansible_module.params_get("state")
# Check parameters
if (names is None or len(names) < 1) and \
(groups is None or len(groups) < 1):
ansible_module.fail_json(msg="At least one name or groups is required")
if state != "query":
if (names is None or len(names) < 1) and \
(groups is None or len(groups) < 1):
ansible_module.fail_json(
msg="At least one name or groups is required")
else:
if action == "member":
ansible_module.fail_json(
msg="Query is not possible with action=member")
if groups is not None:
ansible_module.fail_json(
msg="groups can not be used with state=query, "
"use name instead")
if state in ["present", "renamed"]:
if names is not None and len(names) != 1:
@@ -551,12 +682,6 @@ def main():
ansible_module.fail_json(
msg="Only one group can be %s at a time using 'name'." % what)
check_parameters(ansible_module, state, action)
if external is False:
ansible_module.fail_json(
msg="group can not be non-external")
# Ensuring (adding) several groups with mixed types external, nonposix
# and posix require to have a fix in IPA:
#
@@ -580,15 +705,6 @@ def main():
"supported by your IPA version: "
"https://pagure.io/freeipa/issue/9349")
if (
(externalmember is not None
or idoverrideuser is not None)
and context == "client"
):
ansible_module.fail_json(
msg="Cannot use externalmember in client context."
)
# Use groups if names is None
if groups is not None:
names = groups
@@ -598,35 +714,24 @@ def main():
changed = False
exit_args = {}
# If nonposix is used, set posix as not nonposix
if nonposix is not None:
posix = not nonposix
# Connect to IPA API
with ansible_module.ipa_connect(context=context):
has_add_member_service = ansible_module.ipa_command_param_exists(
"group_add_member", "service")
if service is not None and not has_add_member_service:
ansible_module.fail_json(
msg="Managing a service as part of a group is not supported "
"by your IPA version")
has_add_membermanager = ansible_module.ipa_command_exists(
"group_add_member_manager")
if ((membermanager_user is not None or
membermanager_group is not None) and not has_add_membermanager):
ansible_module.fail_json(
msg="Managing a membermanager user or group is not supported "
"by your IPA version"
if state == "query":
exit_args = ansible_module.execute_query(
names, query_param, group_find, query_param_settings,
convert_result=lambda res: query_convert_result(
ansible_module, res)
)
ansible_module.exit_json(changed=False, group=exit_args)
has_add_member_service = ansible_module.ipa_command_param_exists(
"group_add_member", "service")
has_add_membermanager = ansible_module.ipa_command_exists(
"group_add_member_manager")
has_idoverrideuser = api_check_param(
"group_add_member", "idoverrideuser")
if idoverrideuser is not None and not has_idoverrideuser:
ansible_module.fail_json(
msg="Managing a idoverrideuser as part of a group is not "
"supported by your IPA version")
commands = []
group_set = set()
@@ -638,42 +743,9 @@ def main():
ansible_module.fail_json(
msg="group '%s' is used more than once" % name)
group_set.add(name)
# present
description = group_name.get("description")
gid = group_name.get("gid")
nonposix = group_name.get("nonposix")
external = group_name.get("external")
idoverrideuser = group_name.get("idoverrideuser")
posix = group_name.get("posix")
# Check mutually exclusive condition for multiple groups
# creation. It's not possible to check it with
# `mutually_exclusive` argument in `IPAAnsibleModule` class
# because it accepts only (list[str] or list[list[str]]). Here
# we need to loop over all groups and fail on mutually
# exclusive ones.
if all((posix, nonposix)) or\
all((posix, external)) or\
all((nonposix, external)):
ansible_module.fail_json(
msg="parameters are mutually exclusive for group "
"`{0}`: posix|nonposix|external".format(name))
# Duplicating the condition for multiple group creation
if external is False:
ansible_module.fail_json(
msg="group can not be non-external")
# If nonposix is used, set posix as not nonposix
if nonposix is not None:
posix = not nonposix
user = group_name.get("user")
group = group_name.get("group")
service = group_name.get("service")
membermanager_user = group_name.get("membermanager_user")
membermanager_group = group_name.get("membermanager_group")
externalmember = group_name.get("externalmember")
nomembers = group_name.get("nomembers")
rename = group_name.get("rename")
check_parameters(ansible_module, state, action)
group_params = IPAAnsibleModule.extract_params_from_entry(
group_name, PARAM_MAPPING)
elif (
isinstance(
@@ -681,17 +753,54 @@ def main():
)
):
name = group_name
group_params = IPAAnsibleModule.extract_params(
ansible_module, PARAM_MAPPING)
else:
ansible_module.fail_json(msg="Group '%s' is not valid" %
repr(group_name))
# Never reached, just added to make pylint happy
name = None
group_params = {}
check_parameters(ansible_module, state, action, group_params)
convert_params(ansible_module, group_params)
rename = group_params.get("rename")
posix = group_params.get("posix")
external = group_params.get("external")
# Check API capability for params used
if group_params.get("service") is not None \
and not has_add_member_service:
ansible_module.fail_json(
msg="Managing a service as part of a group is not "
"supported by your IPA version")
if (group_params.get("membermanager_user") is not None
or group_params.get("membermanager_group") is not None) \
and not has_add_membermanager:
ansible_module.fail_json(
msg="Managing a membermanager user or group is not "
"supported by your IPA version")
if group_params.get("idoverrideuser") is not None \
and not has_idoverrideuser:
ansible_module.fail_json(
msg="Managing a idoverrideuser as part of a group is not "
"supported by your IPA version")
if (group_params.get("externalmember") is not None
or group_params.get("idoverrideuser") is not None) \
and context == "client":
ansible_module.fail_json(
msg="Cannot use externalmember in client context.")
# Make sure group exists
res_find = find_group(ansible_module, name)
res_find = group_show(ansible_module, name)
# external members must de handled as SID
externalmember = convert_to_sid(externalmember)
# external members must be handled as SID
externalmember = convert_to_sid(
group_params.get("externalmember"))
# idoverrides need to be compared through SID
idoverrideuser = group_params.get("idoverrideuser")
idoverrideuser_sid = convert_to_sid(idoverrideuser)
res_idoverrideuser_sid = convert_to_sid(
(res_find or {}).get("member_idoverrideuser", []))
@@ -705,13 +814,8 @@ def main():
)
)
user_add, user_del = [], []
group_add, group_del = [], []
service_add, service_del = [], []
externalmember_add, externalmember_del = [], []
idoverrides_add, idoverrides_del = [], []
membermanager_user_add, membermanager_user_del = [], []
membermanager_group_add, membermanager_group_del = [], []
# Create command
if state == "present":
@@ -720,7 +824,8 @@ def main():
external)
# Generate args
args = gen_args(description, gid, nomembers)
args = IPAAnsibleModule.gen_args_from_mapping(
PARAM_MAPPING, group_params)
if action == "group":
# Found the group
@@ -759,89 +864,10 @@ def main():
classes.append("posixgroup")
res_find["objectclass"] = classes
member_args = gen_member_args(
user, group, service, externalmember, idoverrideuser
)
if not compare_args_ipa(ansible_module, member_args,
res_find):
# Generate addition and removal lists
user_add, user_del = gen_add_del_lists(
user, res_find.get("member_user"))
group_add, group_del = gen_add_del_lists(
group, res_find.get("member_group"))
service_add, service_del = gen_add_del_lists(
service, res_find.get("member_service"))
(externalmember_add,
externalmember_del) = gen_add_del_lists(
externalmember, (
list(res_find.get("member_external", []))
+ list(res_find.get("ipaexternalmember", []))
)
)
# There are multiple ways to name an AD User, and any
# can be used in idoverrides, so we create the add/del
# lists based on SID, and then use the given user name
# to the idoverride.
(idoverrides_add,
idoverrides_del) = gen_add_del_lists(
idoverrideuser_sid, res_idoverrideuser_sid)
idoverrides_add = [
idoverride_set[sid] for sid in set(idoverrides_add)
]
idoverrides_del = [
idoverride_set[sid] for sid in set(idoverrides_del)
]
membermanager_user_add, membermanager_user_del = \
gen_add_del_lists(
membermanager_user,
res_find.get("membermanager_user")
)
membermanager_group_add, membermanager_group_del = \
gen_add_del_lists(
membermanager_group,
res_find.get("membermanager_group")
)
elif action == "member":
if res_find is None:
ansible_module.fail_json(msg="No group '%s'" % name)
# Reduce add lists for member_user, member_group,
# member_service and member_external to new entries
# only that are not in res_find.
user_add = gen_add_list(
user, res_find.get("member_user"))
group_add = gen_add_list(
group, res_find.get("member_group"))
service_add = gen_add_list(
service, res_find.get("member_service"))
externalmember_add = gen_add_list(
externalmember, (
list(res_find.get("member_external", []))
+ list(res_find.get("ipaexternalmember", []))
)
)
idoverrides_add = gen_add_list(
idoverrideuser_sid, res_idoverrideuser_sid)
idoverrides_add = [
idoverride_set[sid] for sid in set(idoverrides_add)
]
membermanager_user_add = gen_add_list(
membermanager_user,
res_find.get("membermanager_user")
)
membermanager_group_add = gen_add_list(
membermanager_group,
res_find.get("membermanager_group")
)
elif state == "absent":
if action == "group":
if res_find is not None:
@@ -851,36 +877,6 @@ def main():
if res_find is None:
ansible_module.fail_json(msg="No group '%s'" % name)
if not is_external_group(res_find) and externalmember:
ansible_module.fail_json(
msg="Cannot add external members to a "
"non-external group."
)
user_del = gen_intersection_list(
user, res_find.get("member_user"))
group_del = gen_intersection_list(
group, res_find.get("member_group"))
service_del = gen_intersection_list(
service, res_find.get("member_service"))
externalmember_del = gen_intersection_list(
externalmember, (
list(res_find.get("member_external", []))
+ list(res_find.get("ipaexternalmember", []))
)
)
idoverrides_del = gen_intersection_list(
idoverrideuser_sid, res_idoverrideuser_sid)
idoverrides_del = [
idoverride_set[sid] for sid in set(idoverrides_del)
]
membermanager_user_del = gen_intersection_list(
membermanager_user, res_find.get("membermanager_user"))
membermanager_group_del = gen_intersection_list(
membermanager_group,
res_find.get("membermanager_group")
)
elif state == "renamed":
if res_find is None:
ansible_module.fail_json(msg="No group '%s'" % name)
@@ -889,6 +885,71 @@ def main():
else:
ansible_module.fail_json(msg="Unkown state '%s'" % state)
# Compute member add/del lists for standard members
if not has_add_member_service:
group_params["service"] = None
if not has_add_membermanager:
group_params["membermanager_user"] = None
group_params["membermanager_group"] = None
member_lists = gen_member_add_del_lists(
PARAM_MAPPING, group_params,
res_find or {}, action, state)
user_add, user_del = member_lists.get(
"user", ([], []))
group_add, group_del = member_lists.get(
"group", ([], []))
service_add, service_del = member_lists.get(
"service", ([], []))
membermanager_user_add, membermanager_user_del = member_lists.get(
"membermanager_user", ([], []))
(membermanager_group_add,
membermanager_group_del) = member_lists.get(
"membermanager_group", ([], []))
# Compute externalmember add/del lists
# (merges two res_find keys, can't use gen_member_add_del_lists)
existing_external = (
list(res_find.get("member_external", []))
+ list(res_find.get("ipaexternalmember", []))
) if res_find else []
if state == "present" and action != "member":
externalmember_add, externalmember_del = \
gen_add_del_lists(externalmember, existing_external)
elif state == "present" and action == "member":
externalmember_add = gen_add_list(
externalmember, existing_external)
externalmember_del = []
elif state == "absent" and action == "member":
externalmember_add = []
externalmember_del = gen_intersection_list(
externalmember, existing_external)
else:
externalmember_add = []
externalmember_del = []
# Compute idoverrideuser add/del lists
# (SID-based comparison, can't use gen_member_add_del_lists)
if state == "present" and action != "member":
idoverrides_add, idoverrides_del = gen_add_del_lists(
idoverrideuser_sid, res_idoverrideuser_sid)
elif state == "present" and action == "member":
idoverrides_add = gen_add_list(
idoverrideuser_sid, res_idoverrideuser_sid)
idoverrides_del = []
elif state == "absent" and action == "member":
idoverrides_add = []
idoverrides_del = gen_intersection_list(
idoverrideuser_sid, res_idoverrideuser_sid)
else:
idoverrides_add = []
idoverrides_del = []
idoverrides_add = [
idoverride_set[sid] for sid in set(idoverrides_add)
]
idoverrides_del = [
idoverride_set[sid] for sid in set(idoverrides_del)
]
# manage members
# setup member args for add/remove members.
add_member_args = {

View File

@@ -184,7 +184,7 @@ options:
type: list
elements: str
aliases: ["krbprincipalauthind"]
choices: ["radius", "otp", "pkinit", "hardened", "idp", ""]
choices: ["radius", "otp", "pkinit", "hardened", "idp", "passkey", ""]
required: false
requires_pre_auth:
description: Pre-authentication is required for the service
@@ -356,7 +356,7 @@ options:
type: list
elements: str
aliases: ["krbprincipalauthind"]
choices: ["radius", "otp", "pkinit", "hardened", "idp", ""]
choices: ["radius", "otp", "pkinit", "hardened", "idp", "passkey", ""]
required: false
requires_pre_auth:
description: Pre-authentication is required for the service
@@ -758,7 +758,7 @@ def main():
auth_ind=dict(type='list', elements="str",
aliases=["krbprincipalauthind"], default=None,
choices=["radius", "otp", "pkinit", "hardened", "idp",
""]),
"passkey", ""]),
requires_pre_auth=dict(type="bool", aliases=["ipakrbrequirespreauth"],
default=None),
ok_as_delegate=dict(type="bool", aliases=["ipakrbokasdelegate"],

View File

@@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
# Authors:
# Rafael Guterres Jeffman <rjeffman@redhat.com>
#
# Copyright (C) 2025 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
"metadata_version": "1.0",
"supported_by": "community",
"status": ["preview"],
}
DOCUMENTATION = """
---
module: ipapasskeyconfig
short_description: Manage FreeIPA passkeyconfig
description: Manage FreeIPA passkeyconfig
extends_documentation_fragment:
- ipamodule_base_docs
options:
require_user_verification:
description: Require user verification for passkey authentication
required: false
type: bool
aliases: ["iparequireuserverification"]
author:
- Rafael Guterres Jeffman (@rjeffman)
"""
EXAMPLES = """
# Set passkeyconfig
- ipapasskeyconfig:
ipaadmin_password: SomeADMINpassword
require_user_verification: false
# Get current passkeyconfig
- ipapasskeyconfig:
ipaadmin_password: SomeADMINpassword
"""
RETURN = """
passkeyconfig:
description: Dict of passkeyconfig settings
returned: always
type: dict
contains:
require_user_verification:
description: Require user verification for passkey authentication
type: bool
returned: always
"""
from ansible.module_utils.ansible_freeipa_module import \
IPAAnsibleModule, compare_args_ipa, ipalib_errors
from ansible.module_utils import six
if six.PY3:
unicode = str
def find_passkeyconfig(module):
"""Find the current passkeyconfig settings."""
try:
_result = module.ipa_command_no_name(
"passkeyconfig_show", {"all": True})
except ipalib_errors.NotFound:
# An exception is raised if passkeyconfig is not found.
return None
return _result["result"]
def gen_args(require_user_verification):
_args = {}
if require_user_verification is not None:
_args["iparequireuserverification"] = require_user_verification
return _args
def main():
ansible_module = IPAAnsibleModule(
argument_spec=dict(
# passkeyconfig
require_user_verification=dict(
required=False, type='bool',
aliases=["iparequireuserverification"],
default=None
),
),
supports_check_mode=True,
)
ansible_module._ansible_debug = True
# Get parameters
require_user_verification = (
ansible_module.params_get("require_user_verification")
)
# Init
changed = False
exit_args = {}
# Connect to IPA API
with ansible_module.ipa_connect():
if not ansible_module.ipa_command_exists("passkeyconfig_show"):
msg = "Managing passkeyconfig is not supported by your IPA version"
ansible_module.fail_json(msg=msg)
result = find_passkeyconfig(ansible_module)
if result is None:
ansible_module.fail_json(msg="Could not retrieve passkeyconfig")
if require_user_verification is not None:
# Generate args
args = gen_args(require_user_verification)
# Check if there are different settings in the find result.
# If yes: modify
if not compare_args_ipa(ansible_module, args, result):
changed = True
if not ansible_module.check_mode:
try:
ansible_module.ipa_command_no_name(
"passkeyconfig_mod", args)
except ipalib_errors.EmptyModlist:
changed = False
except Exception as e:
ansible_module.fail_json(
msg="passkeyconfig_mod failed: %s" % str(e))
else:
# No parameters provided, just return current config
pass
# Get updated config if changes were made
if changed:
result = find_passkeyconfig(ansible_module)
# Prepare exit args
exit_args["passkeyconfig"] = {}
if result:
# Map IPA API field to module parameter
if "iparequireuserverification" in result:
exit_args["passkeyconfig"]["require_user_verification"] = \
result["iparequireuserverification"][0]
# Done
ansible_module.exit_json(changed=changed, **exit_args)
if __name__ == "__main__":
main()

View File

@@ -85,6 +85,11 @@ options:
type: list
elements: str
required: false
sysaccount:
description: List of sysaccounts.
type: list
elements: str
required: false
action:
description: Work on role or member level.
type: str
@@ -177,7 +182,7 @@ def check_parameters(module):
"description",
"user", "group",
"host", "hostgroup",
"service",
"service", "sysaccount",
"privilege",
]
@@ -225,7 +230,7 @@ def ensure_absent_state(module, name, action, res_find):
{"privilege": del_list}])
member_args = {}
for key in ['user', 'group', 'hostgroup']:
for key in ['user', 'group', 'hostgroup', 'sysaccount']:
_members = module.params_get_lowercase(key)
if _members:
del_list = gen_intersection_list(
@@ -335,7 +340,7 @@ def ensure_role_with_members_is_present(module, name, res_find, action):
add_members = {}
del_members = {}
for key in ["user", "group", "hostgroup"]:
for key in ["user", "group", "hostgroup", "sysaccount"]:
_members = module.params_get_lowercase(key)
if _members is not None:
add_list, del_list = gen_add_del_lists(
@@ -437,6 +442,8 @@ def create_module():
default=None),
service=dict(required=False, type='list', elements="str",
default=None),
sysaccount=dict(required=False, type='list', elements="str",
default=None),
# state
action=dict(type="str", default="role",
@@ -467,8 +474,15 @@ def main():
state = ansible_module.params_get("state")
action = ansible_module.params_get("action")
names = ansible_module.params_get("name")
sysaccount = ansible_module.params_get("sysaccount")
commands = []
has_sysaccount_member = ansible_module.ipa_command_param_exists(
"role_add_member", "sysaccount")
if not has_sysaccount_member and sysaccount is not None:
ansible_module.fail_json(
msg="sysaccount members are not supported by your IPA version")
for name in names:
cmds = role_commands_for_name(ansible_module, state, action, name)
commands.extend(cmds)

View File

@@ -74,7 +74,7 @@ options:
type: list
elements: str
required: false
choices: ["otp", "radius", "pkinit", "hardened", "idp", ""]
choices: ["otp", "radius", "pkinit", "hardened", "idp", "passkey", ""]
aliases: ["krbprincipalauthind"]
skip_host_check:
description: Skip checking if host object exists.
@@ -192,7 +192,7 @@ options:
type: list
elements: str
required: false
choices: ["otp", "radius", "pkinit", "hardened", "idp", ""]
choices: ["otp", "radius", "pkinit", "hardened", "idp", "passkey", ""]
aliases: ["krbprincipalauthind"]
skip_host_check:
description: Skip checking if host object exists.
@@ -560,7 +560,7 @@ def init_ansible_module():
auth_ind=dict(type="list", elements="str",
aliases=["krbprincipalauthind"],
choices=["otp", "radius", "pkinit", "hardened", "idp",
""]),
"passkey", ""]),
skip_host_check=dict(type="bool"),
force=dict(type="bool"),
requires_pre_auth=dict(

View File

@@ -51,8 +51,7 @@ options:
suboptions:
name:
description: The sudorule name
type: list
elements: str
type: str
required: true
aliases: ["cn"]
description:
@@ -462,7 +461,7 @@ def init_ansible_module():
required=False),
sudorules=dict(
type="list",
defalut=None,
default=None,
options=dict(
# name of the sudorule
name=dict(type="str", required=True, aliases=["cn"]),

View File

@@ -0,0 +1,309 @@
# -*- coding: utf-8 -*-
# Authors:
# Thomas Woerner <twoerner@redhat.com>
#
# Copyright (C) 2025 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
"metadata_version": "1.0",
"supported_by": "community",
"status": ["preview"],
}
DOCUMENTATION = """
---
module: ipasysaccount
short_description: Manage FreeIPA system account
description: Manage FreeIPA system account
extends_documentation_fragment:
- ipamodule_base_docs
- ipamodule_base_docs.delete_continue
options:
name:
description: The list of sysaccount name strings (internally uid).
required: true
type: list
elements: str
aliases: ["login"]
description:
description: A description for the sysaccount.
type: str
required: false
privileged:
description: Allow password updates without reset.
type: bool
required: false
random:
description: Generate a random user password.
required: false
type: bool
password:
description: Set the user password.
required: false
type: str
aliases: ["userpassword"]
update_password:
description:
Set password for a sysaccount in present state only on creation or always
type: str
choices: ["always", "on_create"]
required: false
state:
description: The state to ensure.
choices: ["present", "absent", "enabled", "disabled"]
default: present
type: str
author:
- Thomas Woerner (@t-woerner)
"""
EXAMPLES = """
# Ensure sysaccount my-app is present
- ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
random: true
# Ensure sysaccount my-app is absent
- ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
state: absent
# Ensure existing sysaccount my-app is privileged
- ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
privileged: true
# Ensure existing sysaccount my-app is not privileged
- ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
privileged: false
# Ensure existing sysaccount my-app is disabled
- ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
state: disabled
# Ensure existing sysaccount my-app is enabled
- ipasysaccount:
ipaadmin_password: SomeADMINpassword
name: my-app
state: enabled
"""
RETURN = """
sysaccount:
description: Sysaccount dict with random password
returned: |
If random is yes and user sysaccount not exist or update_password is yes
type: dict
contains:
randompassword:
description: The generated random password
type: str
"""
from ansible.module_utils.ansible_freeipa_module import \
IPAAnsibleModule, compare_args_ipa, ipalib_errors
from ansible.module_utils import six
if six.PY3:
unicode = str
def find_sysaccount(module, name):
"""Find if a sysaccount with the given name already exist."""
try:
_result = module.ipa_command("sysaccount_show", name, {"all": True})
except ipalib_errors.NotFound:
# An exception is raised if sysaccount name is not found.
return None
return _result["result"]
def gen_args(description, random, privileged, password):
_args = {}
if description is not None:
_args["description"] = description
if random is not None:
_args["random"] = random
if privileged is not None:
_args["privileged"] = privileged
if password is not None:
_args["userpassword"] = password
return _args
# pylint: disable=unused-argument
def result_handler(module, result, command, name, args, exit_args, errors):
if "random" in args and command in ["sysaccount_add", "sysaccount_mod"] \
and "randompassword" in result["result"]:
exit_args["randompassword"] = \
result["result"]["randompassword"]
def main():
ansible_module = IPAAnsibleModule(
argument_spec=dict(
# general
name=dict(type="list", elements="str", required=True,
aliases=["login"]),
# present
description=dict(required=False, type='str', default=None),
random=dict(required=False, type='bool', default=None),
privileged=dict(required=False, type='bool', default=None),
password=dict(required=False, type='str', no_log=True,
aliases=["userpassword"], default=None),
# mod
update_password=dict(type='str', default=None, no_log=False,
choices=['always', 'on_create']),
# state
state=dict(type="str", default="present",
choices=["present", "absent", "enabled", "disabled"]),
),
supports_check_mode=True,
ipa_module_options=["delete_continue"],
mutually_exclusive=[["random", "password"]]
)
ansible_module._ansible_debug = True
# Get parameters
# general
names = ansible_module.params_get("name")
# present
description = ansible_module.params_get("description")
random = ansible_module.params_get("random")
privileged = ansible_module.params_get("privileged")
password = ansible_module.params_get("password")
# mod
update_password = ansible_module.params_get("update_password")
# absent
delete_continue = ansible_module.params_get("delete_continue")
# state
state = ansible_module.params_get("state")
# Check parameters
invalid = []
if state == "present" and len(names) != 1:
ansible_module.fail_json(
msg="Only one sysaccount can be added at a time.")
if state in ["absent", "enabled", "disabled"]:
if len(names) < 1:
ansible_module.fail_json(msg="No name given.")
invalid = ["description", "random", "privileged", "password"]
ansible_module.params_fail_used_invalid(invalid, state)
# Init
changed = False
exit_args = {}
# Connect to IPA API
with ansible_module.ipa_connect():
if not ansible_module.ipa_command_exists("sysaccount_add"):
ansible_module.fail_json(
msg=("Managing sysaccounts is not supported by your "
"IPA version")
)
commands = []
for name in names:
# Make sure sysaccount exists
res_find = find_sysaccount(ansible_module, name)
# Create command
if state == "present":
# Generate args
args = gen_args(description, random, privileged, password)
# Found the sysaccount
if res_find is not None:
# Ignore password and random with
# update_password == on_create
if update_password == "on_create":
if "userpassword" in args:
del args["userpassword"]
if "random" in args:
del args["random"]
# if using "random:false" password should not be
# generated.
if not args.get("random", True):
del args["random"]
# For all settings is args, check if there are
# different settings in the find result.
# If yes: modify
if not compare_args_ipa(ansible_module, args,
res_find):
commands.append([name, "sysaccount_mod", args])
else:
commands.append([name, "sysaccount_add", args])
elif state == "absent":
if res_find is not None:
commands.append(
[name, "sysaccount_del", {"continue": delete_continue}]
)
elif state == "enabled":
if res_find is not None and res_find["nsaccountlock"]:
commands.append([name, "sysaccount_enable", {}])
elif state == "disabled":
if res_find is not None and not res_find["nsaccountlock"]:
commands.append([name, "sysaccount_disable", {}])
else:
ansible_module.fail_json(msg="Unkown state '%s'" % state)
# Execute commands
changed = ansible_module.execute_ipa_commands(
commands, result_handler, keeponly=["randompassword"],
exit_args=exit_args)
# Done
ansible_module.exit_json(changed=changed, sysaccount=exit_args)
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@@ -4,3 +4,4 @@ junit_family = xunit1
markers=
source_order: mark test as order bound
playbook: playbook tests
pythonpath = tests

View File

@@ -1,8 +1,8 @@
-r requirements.txt
pytest==7.1.3
pytest-sourceorder==0.6.0
pytest
pytest-sourceorder
pytest-split>=0.8.0
pytest-custom_exit_code>=0.3.0
pytest-testinfra==6.8.0
pytest-randomly==3.12.0
pytest-testinfra
pytest-randomly
pyyaml>=3

View File

@@ -0,0 +1 @@
setuptools

View File

@@ -91,20 +91,21 @@
enabled: yes
state: started
- name: Firewalld - Verify runtime zone "{{ ipabackup_firewalld_zone }}"
ansible.builtin.shell: >
firewall-cmd
--info-zone="{{ ipabackup_firewalld_zone }}"
>/dev/null
- name: Firewalld - Verify zones
when: ipabackup_firewalld_zone is defined
block:
- name: Firewalld - Verify runtime zone from ipabackup_firewalld_zone
ansible.builtin.shell: >
firewall-cmd
--info-zone="{{ ipabackup_firewalld_zone }}"
>/dev/null
- name: Firewalld - Verify permanent zone "{{ ipabackup_firewalld_zone }}"
ansible.builtin.shell: >
firewall-cmd
--permanent
--info-zone="{{ ipabackup_firewalld_zone }}"
>/dev/null
when: ipabackup_firewalld_zone is defined
- name: Firewalld - Verify permanent zone from ipabackup_firewalld_zone
ansible.builtin.shell: >
firewall-cmd
--permanent
--info-zone="{{ ipabackup_firewalld_zone }}"
>/dev/null
### RESTORE

View File

@@ -460,9 +460,10 @@ def main():
# ClientInstall
if options.ca_cert_files is not None:
# ca_cert_files is always a list of strings or None,
# therefore no isinstance(options.ca_cert_files, list) test
# needed.
for value in options.ca_cert_files:
if not isinstance(value, list):
raise ValueError("Expected list, got {0!r}".format(value))
# this is what init() does
value = value[-1]
if not os.path.exists(value):

View File

@@ -23,7 +23,9 @@
from __future__ import (absolute_import, division, print_function)
# pylint: disable=invalid-name
__metaclass__ = type
# pylint: enable=invalid-name
__all__ = ["gssapi", "version", "ipadiscovery", "api", "errors", "x509",
"constants", "sysrestore", "certmonger", "certstore",
@@ -49,7 +51,8 @@ __all__ = ["gssapi", "version", "ipadiscovery", "api", "errors", "x509",
"sssd_enable_ifp", "configure_selinux_for_client",
"getargspec", "paths", "options",
"IPA_PYTHON_VERSION", "NUM_VERSION", "certdb", "get_ca_cert",
"ipalib", "logger", "ipautil", "installer"]
"ipalib", "logger", "ipautil", "installer",
"CLIENT_SUPPORTS_NO_DNSSEC_VALIDATION"]
import sys
@@ -76,6 +79,7 @@ except ImportError:
return ArgSpec(args, varargs, varkw, defaults)
ANSIBLE_IPA_CLIENT_MODULE_IMPORT_ERROR = None # pylint: disable=invalid-name
try:
from ipapython.version import NUM_VERSION, VERSION
@@ -311,13 +315,17 @@ try:
configure_selinux_for_client = None
try:
# pylint: disable=invalid-name
CLIENT_SUPPORTS_NO_DNSSEC_VALIDATION = False
# pylint: enable=invalid-name
from ipaclient.install.client import ClientInstallInterface
except ImportError:
pass
else:
if hasattr(ClientInstallInterface, "no_dnssec_validation"):
# pylint: disable=invalid-name
CLIENT_SUPPORTS_NO_DNSSEC_VALIDATION = True
# pylint: enable=invalid-name
logger = logging.getLogger("ipa-client-install")
root_logger = logger
@@ -327,14 +335,13 @@ try:
raise RuntimeError("freeipa version '%s' is too old" % VERSION)
except ImportError as _err:
# pylint: disable=invalid-name
ANSIBLE_IPA_CLIENT_MODULE_IMPORT_ERROR = str(_err)
# pylint: enable=invalid-name
for attr in __all__:
setattr(sys.modules[__name__], attr, None)
else:
ANSIBLE_IPA_CLIENT_MODULE_IMPORT_ERROR = None
def setup_logging():
standard_logging_setup(

View File

@@ -181,10 +181,10 @@
- name: Install - Store the previously obtained OTP
no_log: yes
when: result_ipaclient_get_otp.host is defined
ansible.builtin.set_fact:
ipaadmin_orig_password: "{{ ipaadmin_password | default(omit) }}"
ipaadmin_password: "{{ result_ipaclient_get_otp.host.randompassword
if result_ipaclient_get_otp.host is defined }}"
ipaadmin_password: "{{ result_ipaclient_get_otp.host.randompassword | default(omit) }}"
rescue:
- name: Install - Report error for OTP generation
ansible.builtin.debug:

View File

@@ -385,7 +385,7 @@ def main():
dns_over_tls=dict(required=False, type='bool',
default=False),
dns_over_tls_cert=dict(required=False, type='str'),
dns_over_tls_key=dict(required=False, type='str'),
dns_over_tls_key=dict(required=False, type='str', no_log=True),
dns_policy=dict(required=False, type='str',
choices=['relaxed', 'enforced'],
default='relaxed'),

View File

@@ -168,7 +168,7 @@ def main():
default=[]),
dns_over_tls=dict(required=False, type='bool', default=False),
dns_over_tls_cert=dict(required=False, type='str'),
dns_over_tls_key=dict(required=False, type='str'),
dns_over_tls_key=dict(required=False, type='str', no_log=True),
dns_policy=dict(required=False, type='str',
choices=['relaxed', 'enforced'],
default='relaxed'),

View File

@@ -281,7 +281,7 @@ def main():
default=[]),
dns_over_tls=dict(required=False, type='bool', default=False),
dns_over_tls_cert=dict(required=False, type='str'),
dns_over_tls_key=dict(required=False, type='str'),
dns_over_tls_key=dict(required=False, type='str', no_log=True),
dns_policy=dict(required=False, type='str',
choices=['relaxed', 'enforced'],
default='relaxed'),

View File

@@ -23,7 +23,9 @@
from __future__ import (absolute_import, division, print_function)
# pylint: disable=invalid-name
__metaclass__ = type
# pylint: enable=invalid-name
__all__ = ["contextlib", "dnsexception", "dnsresolver", "dnsreversename",
"parse_version", "IPAChangeConf",
@@ -49,7 +51,8 @@ __all__ = ["contextlib", "dnsexception", "dnsresolver", "dnsreversename",
"dnsname", "kernel_keyring", "krbinstance", "getargspec",
"adtrustinstance", "paths", "api", "dsinstance", "ipaldap", "Env",
"ipautil", "installutils", "IPA_PYTHON_VERSION", "NUM_VERSION",
"ReplicaConfig", "create_api", "clean_up_hsm_nicknames"]
"ReplicaConfig", "create_api", "clean_up_hsm_nicknames",
"CLIENT_SUPPORTS_NO_DNSSEC_VALIDATION"]
import sys
import logging
@@ -77,6 +80,7 @@ except ImportError:
return ArgSpec(args, varargs, varkw, defaults)
ANSIBLE_IPA_REPLICA_MODULE_IMPORT_ERROR = None # pylint: disable=invalid-name
try:
from contextlib import contextmanager as contextlib_contextmanager
from ipapython.version import NUM_VERSION, VERSION
@@ -188,26 +192,29 @@ try:
time_service = "ntpd" # pylint: disable=invalid-name
try:
# pylint: disable=invalid-name
CLIENT_SUPPORTS_NO_DNSSEC_VALIDATION = False
# pylint: enable=invalid-name
from ipaclient.install.client import ClientInstallInterface
except ImportError:
pass
else:
if hasattr(ClientInstallInterface, "no_dnssec_validation"):
# pylint: disable=invalid-name
CLIENT_SUPPORTS_NO_DNSSEC_VALIDATION = True
# pylint: enable=invalid-name
else:
# IPA version < 4.6
raise RuntimeError("freeipa version '%s' is too old" % VERSION)
except ImportError as _err:
# pylint: disable=invalid-name
ANSIBLE_IPA_REPLICA_MODULE_IMPORT_ERROR = str(_err)
# pylint: enable=invalid-name
for attr in __all__:
setattr(sys.modules[__name__], attr, None)
else:
ANSIBLE_IPA_REPLICA_MODULE_IMPORT_ERROR = None
logger = logging.getLogger("ipa-server-install")

View File

@@ -47,20 +47,21 @@
enabled: yes
state: started
- name: Firewalld - Verify runtime zone "{{ ipareplica_firewalld_zone }}"
ansible.builtin.shell: >
firewall-cmd
--info-zone="{{ ipareplica_firewalld_zone }}"
>/dev/null
- name: Firewalld - Verify zones
when: ipareplica_firewalld_zone is defined
block:
- name: Firewalld - Verify runtime zone from ipareplica_firewalld_zone
ansible.builtin.shell: >
firewall-cmd
--info-zone="{{ ipareplica_firewalld_zone }}"
>/dev/null
- name: Firewalld - Verify permanent zone "{{ ipareplica_firewalld_zone }}"
ansible.builtin.shell: >
firewall-cmd
--permanent
--info-zone="{{ ipareplica_firewalld_zone }}"
>/dev/null
when: ipareplica_firewalld_zone is defined
- name: Firewalld - Verify permanent zone from ipareplica_firewalld_zone
ansible.builtin.shell: >
firewall-cmd
--permanent
--info-zone="{{ ipareplica_firewalld_zone }}"
>/dev/null
- name: Install - Set ipareplica_servers
ansible.builtin.set_fact:

View File

@@ -311,7 +311,7 @@ def main():
dns_over_tls=dict(required=False, type='bool',
default=False),
dns_over_tls_cert=dict(required=False, type='str'),
dns_over_tls_key=dict(required=False, type='str'),
dns_over_tls_key=dict(required=False, type='str', no_log=True),
dns_policy=dict(required=False, type='str',
choices=['relaxed', 'enforced'],
default='relaxed'),

View File

@@ -164,7 +164,7 @@ def main():
default=[]),
dns_over_tls=dict(required=False, type='bool', default=False),
dns_over_tls_cert=dict(required=False, type='str'),
dns_over_tls_key=dict(required=False, type='str'),
dns_over_tls_key=dict(required=False, type='str', no_log=True),
dns_policy=dict(required=False, type='str',
choices=['relaxed', 'enforced'],
default='relaxed'),

View File

@@ -427,7 +427,7 @@ def main():
default=[]),
dns_over_tls=dict(required=False, type='bool', default=False),
dns_over_tls_cert=dict(required=False, type='str'),
dns_over_tls_key=dict(required=False, type='str'),
dns_over_tls_key=dict(required=False, type='str', no_log=True),
dns_policy=dict(required=False, type='str',
choices=['relaxed', 'enforced'],
default='relaxed'),

View File

@@ -46,7 +46,8 @@ __all__ = ["IPAChangeConf", "certmonger", "sysrestore", "root_logger",
"check_available_memory", "getargspec", "get_min_idstart",
"paths", "api", "ipautil", "adtrust_imported", "NUM_VERSION",
"time_service", "kra_imported", "dsinstance", "IPA_PYTHON_VERSION",
"NUM_VERSION", "SerialNumber", "realm_to_ldapi_uri"]
"NUM_VERSION", "SerialNumber", "realm_to_ldapi_uri",
"CLIENT_SUPPORTS_NO_DNSSEC_VALIDATION"]
import sys
import logging
@@ -74,6 +75,7 @@ except ImportError:
return ArgSpec(args, varargs, varkw, defaults)
ANSIBLE_IPA_SERVER_MODULE_IMPORT_ERROR = None # pylint: disable=invalid-name
try:
from contextlib import contextmanager as contextlib_contextmanager
from ansible.module_utils import six
@@ -217,24 +219,28 @@ try:
SerialNumber = None
try:
# pylint: disable=invalid-name
CLIENT_SUPPORTS_NO_DNSSEC_VALIDATION = False
# pylint: enable=invalid-name
from ipaclient.install.client import ClientInstallInterface
except ImportError:
pass
else:
if hasattr(ClientInstallInterface, "no_dnssec_validation"):
# pylint: disable=invalid-name
CLIENT_SUPPORTS_NO_DNSSEC_VALIDATION = True
# pylint: enable=invalid-name
else:
# IPA version < 4.5
raise RuntimeError("freeipa version '%s' is too old" % VERSION)
except ImportError as _err:
# pylint: disable=invalid-name
ANSIBLE_IPA_SERVER_MODULE_IMPORT_ERROR = str(_err)
# pylint: enable=invalid-name
for attr in __all__:
setattr(sys.modules[__name__], attr, None)
else:
ANSIBLE_IPA_SERVER_MODULE_IMPORT_ERROR = None
logger = logging.getLogger("ipa-server-install")

View File

@@ -47,20 +47,21 @@
enabled: yes
state: started
- name: Firewalld - Verify runtime zone "{{ ipaserver_firewalld_zone }}"
ansible.builtin.shell: >
firewall-cmd
--info-zone="{{ ipaserver_firewalld_zone }}"
>/dev/null
- name: Firewalld - verify zones
when: ipaserver_firewalld_zone is defined
block:
- name: Firewalld - Verify runtime zone from ipaserver_firewalld_zone
ansible.builtin.shell: >
firewall-cmd
--info-zone="{{ ipaserver_firewalld_zone }}"
>/dev/null
- name: Firewalld - Verify permanent zone "{{ ipaserver_firewalld_zone }}"
ansible.builtin.shell: >
firewall-cmd
--permanent
--info-zone="{{ ipaserver_firewalld_zone }}"
>/dev/null
when: ipaserver_firewalld_zone is defined
- name: Firewalld - Verify permanent zone from ipaserver_firewalld_zone
ansible.builtin.shell: >
firewall-cmd
--permanent
--info-zone="{{ ipaserver_firewalld_zone }}"
>/dev/null
- name: Copy external certs
ansible.builtin.include_tasks: "{{ role_path }}/tasks/copy_external_cert.yml"

View File

@@ -61,7 +61,12 @@ good-names =
dt, ca,
# These are utils tools, and not part of the released collection.
galaxyfy-playbook, galaxyfy-README, galaxyfy-module-EXAMPLES,
module_EXAMPLES
module_EXAMPLES,
MODULE_IMPORT_ERROR, ANSIBLE_IPA_CLIENT_MODULE_IMPORT_ERROR,
CLIENT_SUPPORTS_NO_DNSSEC_VALIDATION, ANSIBLE_IPA_REPLICA_MODULE_IMPORT_ERROR,
SYSTEMD_RESOLVED_IPA_CONF, ANSIBLE_IPA_SERVER_MODULE_IMPORT_ERROR,
NETWORK_MANAGER_IPA_CONF, ANSIBLE_FREEIPA_MODULE_IMPORT_ERROR,
FIX_6741_DEEPCOPY_OBJECTCLASSES
[pylint.IMPORTS]
@@ -84,6 +89,7 @@ ignored-modules =
[pylint.DESIGN]
max-attributes=12
max-public-methods=25
[pylint.REFACTORING]
max-nested-blocks = 9

View File

@@ -66,7 +66,7 @@
recurse: no
file_type: directory
register: result
failed_when: not result.files
failed_when: result.files | length == 0
# Test backup and copy to controller, don't keep copy on server
- name: Remove all backup from server.
@@ -108,7 +108,7 @@
recurse: no
file_type: directory
register: backups
failed_when: backups.files
failed_when: backups.files | length > 0
- name: Verify backup on controller.
ansible.builtin.find:
@@ -116,7 +116,7 @@
pattern: "{{ ansible_facts.fqdn }}*"
file_type: directory
register: backups
failed_when: not backups.files
failed_when: backups.files | length == 0
delegate_to: localhost
become: no
@@ -161,7 +161,7 @@
recurse: no
file_type: directory
register: result
failed_when: not result.files
failed_when: result.files | length == 0
- name: Verify backup on controller.
ansible.builtin.find:
@@ -169,7 +169,7 @@
pattern: "{{ ansible_facts.fqdn }}*"
file_type: directory
register: backups
failed_when: not backups.files
failed_when: backups.files | length == 0
delegate_to: localhost
become: no
@@ -214,7 +214,7 @@
pattern: "{{ ansible_facts.fqdn }}*"
file_type: directory
register: backups
failed_when: not backups.files
failed_when: backups.files | length == 0
delegate_to: localhost
become: no
@@ -232,7 +232,7 @@
pattern: "{{ ansible_facts.fqdn }}*"
file_type: directory
register: backups
failed_when: not backups.files
failed_when: backups.files | length == 0
delegate_to: localhost
become: no
@@ -252,7 +252,7 @@
path: /var/lib/ipa/backup
file_type: directory
register: backups
failed_when: not backups.files
failed_when: backups.files | length == 0
# Copy backup from server to controller
- name: List all existing backups on controller
@@ -280,7 +280,7 @@
recurse: no
file_type: directory
register: server_backups
failed_when: not server_backups.files
failed_when: server_backups.files | length == 0
- name: Copy backup from server to controller.
ansible.builtin.include_role:
@@ -300,7 +300,7 @@
pattern: "{{ ansible_facts.fqdn }}*"
file_type: directory
register: backups
failed_when: not backups.files
failed_when: backups.files | length == 0
delegate_to: localhost
become: no
@@ -311,7 +311,7 @@
recurse: no
file_type: directory
register: backups
failed_when: not backups.files
failed_when: backups.files | length == 0
- name: Remov all backup from server.
ansible.builtin.include_role:
@@ -326,7 +326,7 @@
recurse: no
file_type: directory
register: backups
failed_when: backups.files
failed_when: backups.files | length > 0
# Remove all backups from server
- name: Create a backup on the server
@@ -348,7 +348,7 @@
recurse: no
file_type: directory
register: backups
failed_when: backups.files
failed_when: backups.files | length > 0
# Remove all backup from server
- name: Remove all backup from server.
@@ -370,7 +370,7 @@
recurse: no
file_type: directory
register: server_backups
failed_when: not server_backups.files
failed_when: server_backups.files | length == 0
- name: Remove backup from server.
ansible.builtin.include_role:
@@ -406,7 +406,7 @@
recurse: no
file_type: directory
register: server_backups
failed_when: server_backups.files
failed_when: server_backups.files | length > 0
# CLEANUP

View File

@@ -124,7 +124,7 @@
reason: 9
state: revoked
register: result
failed_when: not (result.failed and ("Request failed with status 404" in result.msg or "Certificate serial number 0x123456789 not found" in result.msg))
failed_when: not (result.failed and ("Request failed with status 404" in result.msg or result.msg is regex("Certificate [^0]*0x123456789 not found")))
- name: Try to release revoked certificate
ipacert:
@@ -140,7 +140,7 @@
certificate_out: "/root/cert_1.pem"
state: requested
register: result
failed_when: not result.changed or result.failed or result.certificate
failed_when: not result.changed or result.failed or result.certificate != {}
- name: Check requested certificate file
ansible.builtin.file:
@@ -155,7 +155,7 @@
certificate_out: "/root/retrieved.pem"
state: retrieved
register: result
failed_when: result.changed or result.failed or result.certificate
failed_when: result.changed or result.failed or result.certificate != {}
- name: Check retrieved certificate file
ansible.builtin.file:
@@ -194,7 +194,7 @@
profile: invalid_profile
state: requested
register: result
failed_when: not (result.failed and "Request failed with status 400" in result.msg)
failed_when: not (result.failed and ("Request failed with status 400" in result.msg or "Profile not found" in result.msg))
# CLEANUP TEST ITEMS

View File

@@ -136,7 +136,7 @@
reason: 9
state: revoked
register: result
failed_when: not (result.failed and ("Request failed with status 404" in result.msg or "Certificate serial number 0x123456789 not found" in result.msg))
failed_when: not (result.failed and ("Request failed with status 404" in result.msg or result.msg is regex("Certificate [^0]*0x123456789 not found")))
- name: Try to release revoked certificate
ipacert:
@@ -153,7 +153,7 @@
certificate_out: "/root/cert_1.pem"
state: requested
register: result
failed_when: not result.changed or result.failed or result.certificate
failed_when: not result.changed or result.failed or result.certificate != {}
- name: Check requested certificate file
ansible.builtin.file:
@@ -168,7 +168,7 @@
certificate_out: "/root/retrieved.pem"
state: retrieved
register: result
failed_when: result.changed or result.failed or result.certificate
failed_when: result.changed or result.failed or result.certificate != {}
- name: Check retrieved certificate file
ansible.builtin.file:
@@ -207,7 +207,7 @@
profile: invalid_profile
state: requested
register: result
failed_when: not (result.failed and "Request failed with status 400" in result.msg)
failed_when: not (result.failed and ("Request failed with status 400" in result.msg or "Profile not found" in result.msg))
# CLEANUP TEST ITEMS

View File

@@ -123,7 +123,7 @@
reason: 9
state: revoked
register: result
failed_when: not (result.failed and ("Request failed with status 404" in result.msg or "Certificate serial number 0x123456789 not found" in result.msg))
failed_when: not (result.failed and ("Request failed with status 404" in result.msg or result.msg is regex("Certificate [^0]*0x123456789 not found")))
- name: Try to release revoked certificate
ipacert:
@@ -140,7 +140,7 @@
certificate_out: "/root/cert_1.pem"
state: requested
register: result
failed_when: not result.changed or result.failed or result.certificate
failed_when: not result.changed or result.failed or result.certificate != {}
- name: Check requested certificate file
ansible.builtin.file:
@@ -155,7 +155,7 @@
certificate_out: "/root/retrieved.pem"
state: retrieved
register: result
failed_when: result.changed or result.failed or result.certificate
failed_when: result.changed or result.failed or result.certificate != {}
- name: Check retrieved certificate file
ansible.builtin.file:
@@ -194,7 +194,7 @@
profile: invalid_profile
state: requested
register: result
failed_when: not (result.failed and "Request failed with status 400" in result.msg)
failed_when: not (result.failed and ("Request failed with status 400" in result.msg or "Profile not found" in result.msg))
# CLEANUP TEST ITEMS

View File

@@ -400,7 +400,7 @@
searchrecordslimit: '{{ previousconfig.config.searchrecordslimit | default(100) | int }}'
usersearch: '{{ previousconfig.config.usersearch | default(omit) }}'
groupsearch: '{{ previousconfig.config.groupsearch | default(omit) }}'
enable_migration: '{{ previousconfig.config.enable_migration | default(False) | bool }}'
enable_migration: '{{ omit if previousconfig.config.enable_migration is not defined else (previousconfig.config.enable_migration | bool) }}'
groupobjectclasses: '{{ previousconfig.config.groupobjectclasses | default(omit) }}'
userobjectclasses: '{{ previousconfig.config.userobjectclasses | default(omit) }}'
pwdexpnotify: '{{ previousconfig.config.pwdexpnotify | default(4) | int }}'
@@ -436,7 +436,7 @@
searchrecordslimit: '{{ previousconfig.config.searchrecordslimit | default(omit) | int }}'
usersearch: '{{ previousconfig.config.usersearch | default(omit) }}'
groupsearch: '{{ previousconfig.config.groupsearch | default(omit) }}'
enable_migration: '{{ previousconfig.config.enable_migration | default(omit) | bool }}'
enable_migration: '{{ omit if previousconfig.config.enable_migration is not defined else (previousconfig.config.enable_migration | bool) }}'
groupobjectclasses: '{{ previousconfig.config.groupobjectclasses | default(omit) }}'
userobjectclasses: '{{ previousconfig.config.userobjectclasses | default(omit) }}'
pwdexpnotify: '{{ previousconfig.config.pwdexpnotify | default(omit) | int }}'
@@ -473,7 +473,7 @@
searchrecordslimit: '{{ previousconfig.config.searchrecordslimit | default(omit) | int }}'
usersearch: '{{ previousconfig.config.usersearch | default(omit) }}'
groupsearch: '{{ previousconfig.config.groupsearch | default(omit) }}'
enable_migration: '{{ previousconfig.config.enable_migration | default(omit) | bool }}'
enable_migration: '{{ omit if previousconfig.config.enable_migration is not defined else (previousconfig.config.enable_migration | bool) }}'
groupobjectclasses: '{{ previousconfig.config.groupobjectclasses | default(omit) }}'
userobjectclasses: '{{ previousconfig.config.userobjectclasses | default(omit) }}'
pwdexpnotify: '{{ previousconfig.config.pwdexpnotify | default(omit) | int }}'

View File

@@ -5,6 +5,8 @@
gather_facts: no
tasks:
- name: Include tasks ../env_freeipa_facts.yml
ansible.builtin.include_tasks: ../env_freeipa_facts.yml
# GET CURRENT CONFIG
@@ -80,6 +82,36 @@
register: result
failed_when: result.changed or result.failed
- name: Ensure config with user_auth_type passkey
ipaconfig:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
user_auth_type:
- passkey
register: result
failed_when: not result.changed or result.failed
when: passkey_is_supported
- name: Ensure config with user_auth_type passkey, again
ipaconfig:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
user_auth_type:
- passkey
register: result
failed_when: result.changed or result.failed
when: passkey_is_supported
- name: Check if correct message is given if passkey is not supported.
ipaconfig:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
user_auth_type:
- passkey
register: result
failed_when: not result.failed or "'passkey' is not supported" not in result.msg
when: not passkey_is_supported
- name: Ensure config with empty user_auth_type
ipaconfig:
ipaadmin_password: SomeADMINpassword
@@ -138,6 +170,6 @@
ipaconfig:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
pac_type: '{{ previousconfig.config.pac_type }}'
user_auth_type: '{{ previousconfig.config.user_auth_type }}'
configstring: '{{ previousconfig.config.configstring }}'
pac_type: '{{ previousconfig.config.pac_type | default("") }}'
user_auth_type: '{{ previousconfig.config.user_auth_type | default("") }}'
configstring: '{{ previousconfig.config.configstring | default("") }}'

View File

@@ -134,11 +134,9 @@
name: "{{ item }}"
state: absent
with_items:
- "{{ zone_prefix_reverse }}"
- "{{ zone_prefix_reverse_24 }}"
- "{{ zone_prefix_reverse_16 }}"
- "{{ zone_prefix_reverse_8 }}"
- "{{ zone_ipv6_reverse }}"
- "{{ zone_ipv6_reverse_workaround }}"
- "{{ testzone }}"
- "{{ safezone }}"

View File

@@ -15,13 +15,11 @@
skip_nameserver_check: yes
skip_overlap_check: yes
with_items:
- "{{ zone_prefix_reverse }}"
- "{{ zone_prefix_reverse_24 }}"
- "{{ zone_prefix_reverse_16 }}"
- "{{ zone_prefix_reverse_8 }}"
- "{{ zone_ipv6_reverse_workaround }}"
- "{{ testzone }}"
- "{{ zone_ipv6_reverse }}"
- name: Ensure DNSSEC zone '"{{ safezone }}"' is present.
ipadnszone:

View File

@@ -7,14 +7,17 @@
ipv4_reverse: "{{ ansible_facts['default_ipv4'].address.split('.')[:-1] |
reverse |
join('.') }}"
# The 'external_ipv4_address' represents an IP address that
# is not part of the expected CIDR zones managed by the IPA
# deployment. It is used to test the cases were the the reverse
# DNS zone must not be available in the embedded DNS nameserver.
external_ipv4_address: "1.2.3.4"
- name: Set zone prefixes.
ansible.builtin.set_fact:
testzone: 'testzone.test'
safezone: 'safezone.test'
zone_ipv6_reverse: "ip6.arpa."
zone_ipv6_reverse_workaround: "d.f.ip6.arpa."
zone_prefix_reverse: "in-addr.arpa."
zone_prefix_reverse_24: "{{ ipv4_reverse.split('.')[:] | join('.') }}.in-addr.arpa."
zone_prefix_reverse_16: "{{ ipv4_reverse.split('.')[1:] | join('.') }}.in-addr.arpa."
zone_prefix_reverse_8: "{{ ipv4_reverse.split('.')[2:] | join('.') }}.in-addr.arpa."

View File

@@ -1545,6 +1545,28 @@
register: result
failed_when: result.changed or result.failed
- name: Ensure host has IP in subnet not managed by IPA, without PTR record
ipadnsrecord:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
zone_name: "{{ testzone }}"
name: host01
a_rec: "{{ external_ipv4_address }}"
a_create_reverse: false
register: result
failed_when: not result.changed or result.failed
- name: Ensure host has IP in subnet not managed by IPA, without PTR record, again
ipadnsrecord:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
zone_name: "{{ testzone }}"
name: host01
a_rec: "{{ external_ipv4_address }}"
a_create_reverse: false
register: result
failed_when: result.changed or result.failed
# cleanup
- name: Cleanup test environment.
ansible.builtin.include_tasks: env_cleanup.yml

View File

@@ -38,12 +38,24 @@
krb5ccname: "__check_ipa_host_is_client_or_server__"
register: check_ad_support
- name: Verify if passkey tests are possible
ansible.builtin.shell:
cmd: |
echo SomeADMINpassword | kinit -c {{ krb5ccname }} admin > /dev/null
RESULT=$(KRB5CCNAME={{ krb5ccname }} ipa command-find passkey | grep "Number of entries returned")
kdestroy -A -c {{ krb5ccname }} > /dev/null
echo $RESULT
vars:
krb5ccname: "__check_ipa_host_is_client_or_server__"
register: check_passkey_support
- name: Set FreeIPA facts.
ansible.builtin.set_fact:
ipa_version: "{{ ipa_cmd_version.stdout_lines[0] }}"
ipa_api_version: "{{ ipa_cmd_version.stdout_lines[1] }}"
ipa_host_is_client: "{{ (check_client.stdout_lines[-1] == 'CLIENT') | bool }}"
trust_test_is_supported: "{{ 'AD trust agent' in check_ad_support.stdout }}"
passkey_is_supported: "{{ 'Number of entries returned 0' not in check_passkey_support.stdout }}"
- name: Ensure ipaserver_domain is set
when: ipaserver_domain is not defined

View File

@@ -49,8 +49,8 @@
- { id: 2, value: "{{ user_names[0] | upper }}", expected: false }
- { id: 3, value: "{{ user_names[0] }}", expected: false }
- { id: 4, value: "{{ user_names }}", expected: true }
- { id: 5, value: "{{ user_names | upper }}", expected: false }
- { id: 6, value: "{{ user_names | lower }}", expected: false }
- { id: 5, value: "{{ user_names | map('upper') }}", expected: false }
- { id: 6, value: "{{ user_names | map('lower') }}", expected: false }
- { id: 7, value: "{{ user_names[1] }}", expected: true }
- { id: 8, value: "{{ user_names[1] | upper }}", expected: false }
- { id: 9, value: "{{ user_names[1] | lower }}", expected: false }
@@ -64,7 +64,7 @@
failed_when: output.changed != item.expected or output.failed
loop: "{{ test_cases }}"
loop_control:
label: "Test id: {{ item.id }}"
label: "Test id: {{ item.id }} - {{ item.value }}"
- name: Test group presence with group parameter
vars:
@@ -73,8 +73,8 @@
- { id: 2, value: "{{ group_names[0] | upper }}", expected: false }
- { id: 3, value: "{{ group_names[0] }}", expected: false }
- { id: 4, value: "{{ group_names }}", expected: true }
- { id: 5, value: "{{ group_names | upper }}", expected: false }
- { id: 6, value: "{{ group_names | lower }}", expected: false }
- { id: 5, value: "{{ group_names | map('upper') }}", expected: false }
- { id: 6, value: "{{ group_names | map('lower') }}", expected: false }
- { id: 7, value: "{{ group_names[1] }}", expected: true }
- { id: 8, value: "{{ group_names[1] | upper }}", expected: false }
- { id: 9, value: "{{ group_names[1] | lower }}", expected: false }
@@ -136,7 +136,7 @@
- name: "Ensure test users test_users are absent"
ipauser:
users: "{{ test_users }}"
name: "{{ user_names }}"
state: absent
- name: Test in all IPA versions 8.4.4+
@@ -159,8 +159,8 @@
- { id: 2, value: "{{ user_names[0] | upper }}", expected: false }
- { id: 3, value: "{{ user_names[0] }}", expected: false }
- { id: 4, value: "{{ user_names }}", expected: true }
- { id: 5, value: "{{ user_names | upper }}", expected: false }
- { id: 6, value: "{{ user_names | lower }}", expected: false }
- { id: 5, value: "{{ user_names | map('upper') }}", expected: false }
- { id: 6, value: "{{ user_names | map('lower') }}", expected: false }
- { id: 7, value: "{{ user_names[1] }}", expected: true }
- { id: 8, value: "{{ user_names[1] | upper }}", expected: false }
- { id: 9, value: "{{ user_names[1] | lower }}", expected: false }
@@ -183,8 +183,8 @@
- { id: 2, value: "{{ group_names[0] | upper }}", expected: false }
- { id: 3, value: "{{ group_names[0] }}", expected: false }
- { id: 4, value: "{{ group_names }}", expected: true }
- { id: 5, value: "{{ group_names | upper }}", expected: false }
- { id: 6, value: "{{ group_names | lower }}", expected: false }
- { id: 5, value: "{{ group_names | map('upper') }}", expected: false }
- { id: 6, value: "{{ group_names | map('lower') }}", expected: false }
- { id: 7, value: "{{ group_names[1] }}", expected: true }
- { id: 8, value: "{{ group_names[1] | upper }}", expected: false }
- { id: 9, value: "{{ group_names[1] | lower }}", expected: false }

View File

@@ -0,0 +1,212 @@
---
- name: Test group query
hosts: "{{ ipa_test_host | default('ipaserver') }}"
become: false
gather_facts: false
module_defaults:
ipagroup:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipauser:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
tasks:
# CLEANUP
- name: Ensure groups "testgroup1" and "testgroup2" are absent
ipagroup:
name:
- testgroup1
- testgroup2
- non-existing-group
state: absent
- name: Ensure users "testuser1" and "testuser2" are absent
ipauser:
name:
- testuser1
- testuser2
- testuser3
state: absent
# CREATE TEST ITEMS
- name: Ensure users "testuser1" and "testuser2" are present
ipauser:
users:
- name: testuser1
first: first1
last: last1
- name: testuser2
first: first2
last: last2
- name: testuser3
first: first3
last: last3
- name: Ensure groups "testgroup1" and "testgroup2" are present
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: testgroup1
gidnumber: 2000
user:
- testuser1
- name: testgroup2
gidnumber: 2001
user:
- testuser2
- testuser3
- name: Query group "non-existing-group"
ipagroup:
name:
- non-existing-group
query_param: ALL
state: query
register: result
failed_when: result.changed or result.failed
- name: Print query information
ansible.builtin.debug:
var: result.group
- name: Fail on non empty query result
ansible.builtin.fail:
msg: "{{ result['group'] }} is not empty"
when: result['group'] | length > 0
- name: Query all groups
ipagroup:
state: query
query_param: PKEY_ONLY
register: result
failed_when: result.changed or result.failed
- name: Print query information
ansible.builtin.debug:
var: result.group
- name: Fail on missing "testgroup1" in query result
ansible.builtin.fail:
msg: "'testgroup1' not in query result {{ result.group.groups }}"
when: ("testgroup1" not in result.group.groups)
- name: Fail on missing "testgroup2" in query result
ansible.builtin.fail:
msg: "'testgroup2' not in query result {{ result.group.groups }}"
when: ("testgroup2" not in result.group.groups)
- name: Fail on "non-existing-group" in query result
ansible.builtin.fail:
msg: "'non-existing-group' in query result {{ result.group.groups }}"
when: ("non-existing-group" in result.group.groups)
- name: Query groups "testgroup1" and "testgroup2"
ipagroup:
name:
- testgroup1
- testgroup2
state: query
query_param: PKEY_ONLY
register: result
failed_when: result.changed or result.failed
- name: Print query information
ansible.builtin.debug:
var: result.group
- name: Fail on missing "testgroup1" in query result
ansible.builtin.fail:
msg: "'testgroup1' not in query result {{ result.group.groups }}"
when: ("testgroup1" not in result.group.groups)
- name: Fail on missing "testgroup2" in query result
ansible.builtin.fail:
msg: "'testgroup2' not in query result {{ result.group.groups }}"
when: ("testgroup2" not in result.group.groups)
- name: Fail on "non-existing-group" in query result
ansible.builtin.fail:
msg: "'non-existing-group' in query result {{ result.group.groups }}"
when: ("non-existing-group" in result.group.groups)
- name: Query all group parameters for "testgroup1"
ipagroup:
name:
- testgroup1
query_param: ALL
state: query
register: result
failed_when: result.changed or result.failed
- name: Print query information
ansible.builtin.debug:
var: result.group
- name: Fail on missing information in query result
ansible.builtin.fail:
msg: "Query result {{ result['group'] }} is incomplete"
when: ("cn=testgroup1,cn=groups,cn=accounts,dc=" not in result.group.dn or
result.group.gid < 1 or
result.group.ipauniqueid|length != 36 or
result.group.name != "testgroup1" or
"objectclass" not in result.group)
- name: Query "gid" and "name" parameters for all groups
ipagroup:
query_param:
- gid
- name
- user
- group
state: query
register: result
failed_when: result.changed or result.failed
- name: Print query information
ansible.builtin.debug:
var: result.group
- name: Fail on less than 3 groups in result
ansible.builtin.fail:
msg: "{{ result.group }} is not empty"
when: result.group | length < 3
- name: Fail on missing "testgroup1" information in query result
ansible.builtin.fail:
msg: "'testgroup1' not in query result {{ result.group }}"
when: ("testgroup1" not in result.group or
result.group.testgroup1.gid < 1 or
result.group.testgroup1.name != "testgroup1" or
result.group.testgroup1.user|length != 1 or
"testuser1" not in result.group.testgroup1.user)
- name: Fail on missing "testgroup2" information in query result
ansible.builtin.fail:
msg: "'testgroup2' not in query result {{ result.group }}"
when: ("testgroup2" not in result.group or
result.group.testgroup2.gid < 1 or
result.group.testgroup2.name != "testgroup2" or
result.group.testgroup2.user|length != 2 or
"testuser2" not in result.group.testgroup2.user or
"testuser3" not in result.group.testgroup2.user)
# CLEANUP
- name: Ensure groups "testgroup1" and "testgroup2" are absent
ipagroup:
name:
- testgroup1
- testgroup2
state: absent
- name: Ensure users "testuser1" and "testuser2" are absent
ipauser:
name:
- testuser1
- testuser2
- testuser3
state: absent

View File

@@ -114,7 +114,7 @@
ipagroup:
ipaadmin_password: SomeADMINpassword
register: result
failed_when: result.changed or not result.failed or "one of the following is required" not in result.msg
failed_when: result.changed or not result.failed or "At least one name or groups is required" not in result.msg
- name: Name is absent
ipagroup:

View File

@@ -97,7 +97,7 @@
ipahbacsvcgroup:
ipaadmin_password: SomeADMINpassword
name: testgroup
hbacsvc: "{{ hbacsvc_list | lower }}"
hbacsvc: "{{ hbacsvc_list | map('lower') }}"
register: result
failed_when: result.changed or result.failed
@@ -105,7 +105,7 @@
ipahbacsvcgroup:
ipaadmin_password: SomeADMINpassword
name: testgroup
hbacsvc: "{{ hbacsvc_list | upper }}"
hbacsvc: "{{ hbacsvc_list | map('upper') }}"
register: result
failed_when: result.changed or result.failed
@@ -153,7 +153,7 @@
ipahbacsvcgroup:
ipaadmin_password: SomeADMINpassword
name: testgroup
hbacsvc: "{{ hbacsvc_list | lower }}"
hbacsvc: "{{ hbacsvc_list | map('lower') }}"
action: member
register: result
failed_when: result.changed or result.failed
@@ -162,7 +162,7 @@
ipahbacsvcgroup:
ipaadmin_password: SomeADMINpassword
name: testgroup
hbacsvc: "{{ hbacsvc_list | upper }}"
hbacsvc: "{{ hbacsvc_list | map('upper') }}"
action: member
register: result
failed_when: result.changed or result.failed
@@ -171,7 +171,7 @@
ipahbacsvcgroup:
ipaadmin_password: SomeADMINpassword
name: testgroup
hbacsvc: "{{ hbacsvc_list | upper }}"
hbacsvc: "{{ hbacsvc_list | map('upper') }}"
action: member
state: absent
check_mode: yes
@@ -182,7 +182,7 @@
ipahbacsvcgroup:
ipaadmin_password: SomeADMINpassword
name: testgroup
hbacsvc: "{{ hbacsvc_list | upper }}"
hbacsvc: "{{ hbacsvc_list | map('upper') }}"
action: member
state: absent
register: result
@@ -192,7 +192,7 @@
ipahbacsvcgroup:
ipaadmin_password: SomeADMINpassword
name: testgroup
hbacsvc: "{{ hbacsvc_list | upper }}"
hbacsvc: "{{ hbacsvc_list | map('upper') }}"
action: member
state: absent
check_mode: yes
@@ -213,7 +213,7 @@
ipahbacsvcgroup:
ipaadmin_password: SomeADMINpassword
name: testgroup
hbacsvc: "{{ hbacsvc_list | lower }}"
hbacsvc: "{{ hbacsvc_list | map('lower') }}"
action: member
state: absent
register: result

View File

@@ -5,6 +5,9 @@
gather_facts: yes
tasks:
- name: Include FreeIPA facts.
ansible.builtin.include_tasks: ../env_freeipa_facts.yml
- name: Get Domain from server name
ansible.builtin.set_fact:
ipaserver_domain: "{{ ansible_facts['fqdn'].split('.')[1:] | join('.') }}"
@@ -58,6 +61,39 @@
register: result
failed_when: result.changed or result.failed
- name: Ensure host "{{ host1_fqdn }}" present with auth_ind passkey
ipahost:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: "{{ host1_fqdn }}"
auth_ind:
- passkey
register: result
when: passkey_is_supported
failed_when: not result.changed or result.failed
- name: Ensure host "{{ host1_fqdn }}" present with auth_ind passkey, again
ipahost:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: "{{ host1_fqdn }}"
auth_ind:
- passkey
register: result
when: passkey_is_supported
failed_when: result.changed or result.failed
- name: Check if correct message is given if passkey is not supported.
ipahost:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: "{{ host1_fqdn }}"
auth_ind:
- passkey
register: result
when: not passkey_is_supported
failed_when: not result.failed or "'passkey' is not supported" not in result.msg
- name: Ensure host "{{ host1_fqdn }}" present with empty auth_ind
ipahost:
ipaadmin_password: SomeADMINpassword

View File

@@ -92,11 +92,11 @@
- name: Print generated random password for "{{ host1_fqdn }}"
ansible.builtin.debug:
var: ipahost.host["{{ host1_fqdn }}"].randompassword
var: ipahost.host[host1_fqdn].randompassword
- name: Print generated random password for "{{ host2_fqdn }}"
ansible.builtin.debug:
var: ipahost.host["{{ host2_fqdn }}"].randompassword
var: ipahost.host[host2_fqdn].randompassword
- name: Enrolled host "{{ server_fqdn }}" fails to set random password with update_password always
ipahost:

View File

@@ -1,8 +1,16 @@
---
- name: Test host
- name: Test host reverse attribute
hosts: ipaserver
become: true
module_defaults:
ipahost:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipadnszone:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
tasks:
- name: Get Domain from server name
ansible.builtin.set_fact:
@@ -15,7 +23,6 @@
- name: Host absent
ipahost:
ipaadmin_password: SomeADMINpassword
name:
- "{{ host1_fqdn }}"
update_dns: yes
@@ -28,30 +35,24 @@
- name: Set zone prefixes.
ansible.builtin.set_fact:
zone_ipv6_reverse: "ip6.arpa."
zone_ipv6_reverse_workaround: "d.f.ip6.arpa."
zone_prefix_reverse: "in-addr.arpa"
zone_prefix_reverse_8: "{{ ipv4_prefix.split('.')[2::-1] | join('.') }}.in-addr.arpa"
zone_prefix_reverse_16: "{{ ipv4_prefix.split('.')[1::-1] | join('.') }}.in-addr.arpa"
zone_prefix_reverse_24: "{{ ipv4_prefix.split('.')[::-1] | join('.') }}.in-addr.arpa"
- name: Set zone for reverse address.
ipadnszone:
ipaadmin_password: SomeADMINpassword
name: "{{ item }}"
skip_nameserver_check: yes
skip_overlap_check: yes
with_items:
- "{{ zone_ipv6_reverse }}"
- "{{ zone_ipv6_reverse_workaround }}"
- "{{ zone_prefix_reverse }}"
- "{{ zone_prefix_reverse_8 }}"
- "{{ zone_prefix_reverse_16 }}"
- "{{ zone_prefix_reverse_24 }}"
- name: Host "{{ host1_fqdn }}" present
ipahost:
ipaadmin_password: SomeADMINpassword
name: "{{ host1_fqdn }}"
ip_address: "{{ ipv4_prefix + '.201' }}"
update_dns: yes
@@ -61,7 +62,6 @@
- name: Host "{{ host1_fqdn }}" present, again.
ipahost:
ipaadmin_password: SomeADMINpassword
name: "{{ host1_fqdn }}"
ip_address: "{{ ipv4_prefix + '.201' }}"
update_dns: yes
@@ -71,7 +71,6 @@
- name: Hosts host1 absent
ipahost:
ipaadmin_password: SomeADMINpassword
name:
- "{{ host1_fqdn }}"
update_dns: yes
@@ -81,7 +80,6 @@
- name: Host "{{ host1_fqdn }}" present with IPv6
ipahost:
ipaadmin_password: SomeADMINpassword
name: "{{ host1_fqdn }}"
ip_address: "fd00::0001"
update_dns: yes
@@ -91,7 +89,6 @@
- name: Host "{{ host1_fqdn }}" present with IPv6, again.
ipahost:
ipaadmin_password: SomeADMINpassword
name: "{{ host1_fqdn }}"
ip_address: "fd00::0001"
update_dns: yes
@@ -101,7 +98,6 @@
- name: Hosts host1 absent
ipahost:
ipaadmin_password: SomeADMINpassword
name:
- "{{ host1_fqdn }}"
update_dns: yes
@@ -111,13 +107,10 @@
- name: Delete zone for reverse address.
ipadnszone:
ipaadmin_password: SomeADMINpassword
name: "{{ item }}"
state: absent
with_items:
- "{{ zone_ipv6_reverse }}"
- "{{ zone_ipv6_reverse_workaround }}"
- "{{ zone_prefix_reverse }}"
- "{{ zone_prefix_reverse_8 }}"
- "{{ zone_prefix_reverse_16 }}"
- "{{ zone_prefix_reverse_24 }}"

View File

@@ -55,7 +55,7 @@
name: "{{ host1_fqdn }}"
sshpubkey:
# yamllint disable-line rule:line-length
- AAAAB3NzaC1yc2EAAAADAQABAAABgQDc8MIjaSrxLYHvu+hduoF4m6NUFSlXZWzYbd3BK4L47/U4eiXoOS6dcfuZJDjmLfOipc7XVp7NADwAgA1yBOAjbeVpXr2tC8w8saZibl75WBOEjDfNroiOh/f/ojrwwHg05QTVSZHs27sU1HBPyCQM/FHVM6EnRfmyiBkEBA/3ca0PJ9UJhWb2XisCaz6y6QcTh4gQnvHzgmEmK31GwiKnmBSEQuj8P5NGCO8RlN3cq3zpRpMDEoBRCjQYicllf/5P43r5OGvS1LhTiAMfyqE37URezNQa7aozBpH1GhIwAmjAtm84jXQjxUgZPYC0aSLuADYErScOP4792r6koH9t/DM5/M+jG2c4PNWynDczUw6Eaxl5E3hU0Ee9UN0Oee7iBnVenS/QMeZNyo5lMA/HXT5lrYiJGTYM0shRjGXXYBbJZhWerguSWDAdUd1gvuGP1nb7/+/Cvb46+HX7zYouS5Ojo0yPzMZ07X142jnKAfx9LnKdMUCwBJzbtoJ91Zc= pinky@ipaserver.el81.local # noqa 204
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDc8MIjaSrxLYHvu+hduoF4m6NUFSlXZWzYbd3BK4L47/U4eiXoOS6dcfuZJDjmLfOipc7XVp7NADwAgA1yBOAjbeVpXr2tC8w8saZibl75WBOEjDfNroiOh/f/ojrwwHg05QTVSZHs27sU1HBPyCQM/FHVM6EnRfmyiBkEBA/3ca0PJ9UJhWb2XisCaz6y6QcTh4gQnvHzgmEmK31GwiKnmBSEQuj8P5NGCO8RlN3cq3zpRpMDEoBRCjQYicllf/5P43r5OGvS1LhTiAMfyqE37URezNQa7aozBpH1GhIwAmjAtm84jXQjxUgZPYC0aSLuADYErScOP4792r6koH9t/DM5/M+jG2c4PNWynDczUw6Eaxl5E3hU0Ee9UN0Oee7iBnVenS/QMeZNyo5lMA/HXT5lrYiJGTYM0shRjGXXYBbJZhWerguSWDAdUd1gvuGP1nb7/+/Cvb46+HX7zYouS5Ojo0yPzMZ07X142jnKAfx9LnKdMUCwBJzbtoJ91Zc= pinky@ipaserver.el81.local # noqa 204
register: result
failed_when: not result.changed or result.failed
@@ -65,7 +65,7 @@
name: "{{ host1_fqdn }}"
sshpubkey:
# yamllint disable-line rule:line-length
- AAAAB3NzaC1yc2EAAAADAQABAAABgQDc8MIjaSrxLYHvu+hduoF4m6NUFSlXZWzYbd3BK4L47/U4eiXoOS6dcfuZJDjmLfOipc7XVp7NADwAgA1yBOAjbeVpXr2tC8w8saZibl75WBOEjDfNroiOh/f/ojrwwHg05QTVSZHs27sU1HBPyCQM/FHVM6EnRfmyiBkEBA/3ca0PJ9UJhWb2XisCaz6y6QcTh4gQnvHzgmEmK31GwiKnmBSEQuj8P5NGCO8RlN3cq3zpRpMDEoBRCjQYicllf/5P43r5OGvS1LhTiAMfyqE37URezNQa7aozBpH1GhIwAmjAtm84jXQjxUgZPYC0aSLuADYErScOP4792r6koH9t/DM5/M+jG2c4PNWynDczUw6Eaxl5E3hU0Ee9UN0Oee7iBnVenS/QMeZNyo5lMA/HXT5lrYiJGTYM0shRjGXXYBbJZhWerguSWDAdUd1gvuGP1nb7/+/Cvb46+HX7zYouS5Ojo0yPzMZ07X142jnKAfx9LnKdMUCwBJzbtoJ91Zc= pinky@ipaserver.el81.local # noqa 204
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDc8MIjaSrxLYHvu+hduoF4m6NUFSlXZWzYbd3BK4L47/U4eiXoOS6dcfuZJDjmLfOipc7XVp7NADwAgA1yBOAjbeVpXr2tC8w8saZibl75WBOEjDfNroiOh/f/ojrwwHg05QTVSZHs27sU1HBPyCQM/FHVM6EnRfmyiBkEBA/3ca0PJ9UJhWb2XisCaz6y6QcTh4gQnvHzgmEmK31GwiKnmBSEQuj8P5NGCO8RlN3cq3zpRpMDEoBRCjQYicllf/5P43r5OGvS1LhTiAMfyqE37URezNQa7aozBpH1GhIwAmjAtm84jXQjxUgZPYC0aSLuADYErScOP4792r6koH9t/DM5/M+jG2c4PNWynDczUw6Eaxl5E3hU0Ee9UN0Oee7iBnVenS/QMeZNyo5lMA/HXT5lrYiJGTYM0shRjGXXYBbJZhWerguSWDAdUd1gvuGP1nb7/+/Cvb46+HX7zYouS5Ojo0yPzMZ07X142jnKAfx9LnKdMUCwBJzbtoJ91Zc= pinky@ipaserver.el81.local # noqa 204
register: result
failed_when: result.changed or result.failed
@@ -77,7 +77,7 @@
# yamllint disable-line rule:line-length
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCqmVDpEX5gnSjKuv97AyzOhaUMMKz8ahOA3GY77tVC4o68KNgMCmDSEG1/kOIaElngNLaCha3p/2iAcU9Bi1tLKUlm2bbO5NHNwHfRxY/3cJtq+/7D1vxJzqThYwI4F9vr1WxyY2+mMTv3pXbfAJoR8Mu06XaEY5PDetlDKjHLuNWF+/O7ZU8PsULTa1dJZFrtXeFpmUoLoGxQBvlrlcPI1zDciCSU24t27Zan5Py2l5QchyI7yhCyMM77KDtj5+AFVpmkb9+zq50rYJAyFVeyUvwjzErvQrKJzYpA0NyBp7vskWbt36M16/M/LxEK7HA6mkcakO3ESWx5MT1LAjvdlnxbWG3787MxweHXuB8CZU+9bZPFBaJ+VQtOfJ7I8eH0S16moPC4ak8FlcFvOH8ERDPWLFDqfy09yaZ7bVIF0//5ZI7Nf3YDe3S7GrBX5ieYuECyP6UNkTx9BRsAQeVvXEc6otzB7iCSnYBMGUGzCqeigoAWaVQUONsSR3Uatks= pinky@ipaserver.el81.local # noqa 204
# yamllint disable-line rule:line-length
- AAAAB3NzaC1yc2EAAAADAQABAAABgQDc8MIjaSrxLYHvu+hduoF4m6NUFSlXZWzYbd3BK4L47/U4eiXoOS6dcfuZJDjmLfOipc7XVp7NADwAgA1yBOAjbeVpXr2tC8w8saZibl75WBOEjDfNroiOh/f/ojrwwHg05QTVSZHs27sU1HBPyCQM/FHVM6EnRfmyiBkEBA/3ca0PJ9UJhWb2XisCaz6y6QcTh4gQnvHzgmEmK31GwiKnmBSEQuj8P5NGCO8RlN3cq3zpRpMDEoBRCjQYicllf/5P43r5OGvS1LhTiAMfyqE37URezNQa7aozBpH1GhIwAmjAtm84jXQjxUgZPYC0aSLuADYErScOP4792r6koH9t/DM5/M+jG2c4PNWynDczUw6Eaxl5E3hU0Ee9UN0Oee7iBnVenS/QMeZNyo5lMA/HXT5lrYiJGTYM0shRjGXXYBbJZhWerguSWDAdUd1gvuGP1nb7/+/Cvb46+HX7zYouS5Ojo0yPzMZ07X142jnKAfx9LnKdMUCwBJzbtoJ91Zc= pinky@ipaserver.el81.local # noqa 204
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDc8MIjaSrxLYHvu+hduoF4m6NUFSlXZWzYbd3BK4L47/U4eiXoOS6dcfuZJDjmLfOipc7XVp7NADwAgA1yBOAjbeVpXr2tC8w8saZibl75WBOEjDfNroiOh/f/ojrwwHg05QTVSZHs27sU1HBPyCQM/FHVM6EnRfmyiBkEBA/3ca0PJ9UJhWb2XisCaz6y6QcTh4gQnvHzgmEmK31GwiKnmBSEQuj8P5NGCO8RlN3cq3zpRpMDEoBRCjQYicllf/5P43r5OGvS1LhTiAMfyqE37URezNQa7aozBpH1GhIwAmjAtm84jXQjxUgZPYC0aSLuADYErScOP4792r6koH9t/DM5/M+jG2c4PNWynDczUw6Eaxl5E3hU0Ee9UN0Oee7iBnVenS/QMeZNyo5lMA/HXT5lrYiJGTYM0shRjGXXYBbJZhWerguSWDAdUd1gvuGP1nb7/+/Cvb46+HX7zYouS5Ojo0yPzMZ07X142jnKAfx9LnKdMUCwBJzbtoJ91Zc= pinky@ipaserver.el81.local # noqa 204
register: result
failed_when: not result.changed or result.failed
@@ -89,7 +89,7 @@
# yamllint disable-line rule:line-length
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCqmVDpEX5gnSjKuv97AyzOhaUMMKz8ahOA3GY77tVC4o68KNgMCmDSEG1/kOIaElngNLaCha3p/2iAcU9Bi1tLKUlm2bbO5NHNwHfRxY/3cJtq+/7D1vxJzqThYwI4F9vr1WxyY2+mMTv3pXbfAJoR8Mu06XaEY5PDetlDKjHLuNWF+/O7ZU8PsULTa1dJZFrtXeFpmUoLoGxQBvlrlcPI1zDciCSU24t27Zan5Py2l5QchyI7yhCyMM77KDtj5+AFVpmkb9+zq50rYJAyFVeyUvwjzErvQrKJzYpA0NyBp7vskWbt36M16/M/LxEK7HA6mkcakO3ESWx5MT1LAjvdlnxbWG3787MxweHXuB8CZU+9bZPFBaJ+VQtOfJ7I8eH0S16moPC4ak8FlcFvOH8ERDPWLFDqfy09yaZ7bVIF0//5ZI7Nf3YDe3S7GrBX5ieYuECyP6UNkTx9BRsAQeVvXEc6otzB7iCSnYBMGUGzCqeigoAWaVQUONsSR3Uatks= pinky@ipaserver.el81.local # noqa 204
# yamllint disable-line rule:line-length
- AAAAB3NzaC1yc2EAAAADAQABAAABgQDc8MIjaSrxLYHvu+hduoF4m6NUFSlXZWzYbd3BK4L47/U4eiXoOS6dcfuZJDjmLfOipc7XVp7NADwAgA1yBOAjbeVpXr2tC8w8saZibl75WBOEjDfNroiOh/f/ojrwwHg05QTVSZHs27sU1HBPyCQM/FHVM6EnRfmyiBkEBA/3ca0PJ9UJhWb2XisCaz6y6QcTh4gQnvHzgmEmK31GwiKnmBSEQuj8P5NGCO8RlN3cq3zpRpMDEoBRCjQYicllf/5P43r5OGvS1LhTiAMfyqE37URezNQa7aozBpH1GhIwAmjAtm84jXQjxUgZPYC0aSLuADYErScOP4792r6koH9t/DM5/M+jG2c4PNWynDczUw6Eaxl5E3hU0Ee9UN0Oee7iBnVenS/QMeZNyo5lMA/HXT5lrYiJGTYM0shRjGXXYBbJZhWerguSWDAdUd1gvuGP1nb7/+/Cvb46+HX7zYouS5Ojo0yPzMZ07X142jnKAfx9LnKdMUCwBJzbtoJ91Zc= pinky@ipaserver.el81.local # noqa 204
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDc8MIjaSrxLYHvu+hduoF4m6NUFSlXZWzYbd3BK4L47/U4eiXoOS6dcfuZJDjmLfOipc7XVp7NADwAgA1yBOAjbeVpXr2tC8w8saZibl75WBOEjDfNroiOh/f/ojrwwHg05QTVSZHs27sU1HBPyCQM/FHVM6EnRfmyiBkEBA/3ca0PJ9UJhWb2XisCaz6y6QcTh4gQnvHzgmEmK31GwiKnmBSEQuj8P5NGCO8RlN3cq3zpRpMDEoBRCjQYicllf/5P43r5OGvS1LhTiAMfyqE37URezNQa7aozBpH1GhIwAmjAtm84jXQjxUgZPYC0aSLuADYErScOP4792r6koH9t/DM5/M+jG2c4PNWynDczUw6Eaxl5E3hU0Ee9UN0Oee7iBnVenS/QMeZNyo5lMA/HXT5lrYiJGTYM0shRjGXXYBbJZhWerguSWDAdUd1gvuGP1nb7/+/Cvb46+HX7zYouS5Ojo0yPzMZ07X142jnKAfx9LnKdMUCwBJzbtoJ91Zc= pinky@ipaserver.el81.local # noqa 204
register: result
failed_when: result.changed or result.failed
@@ -117,7 +117,7 @@
# yamllint disable-line rule:line-length
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCqmVDpEX5gnSjKuv97AyzOhaUMMKz8ahOA3GY77tVC4o68KNgMCmDSEG1/kOIaElngNLaCha3p/2iAcU9Bi1tLKUlm2bbO5NHNwHfRxY/3cJtq+/7D1vxJzqThYwI4F9vr1WxyY2+mMTv3pXbfAJoR8Mu06XaEY5PDetlDKjHLuNWF+/O7ZU8PsULTa1dJZFrtXeFpmUoLoGxQBvlrlcPI1zDciCSU24t27Zan5Py2l5QchyI7yhCyMM77KDtj5+AFVpmkb9+zq50rYJAyFVeyUvwjzErvQrKJzYpA0NyBp7vskWbt36M16/M/LxEK7HA6mkcakO3ESWx5MT1LAjvdlnxbWG3787MxweHXuB8CZU+9bZPFBaJ+VQtOfJ7I8eH0S16moPC4ak8FlcFvOH8ERDPWLFDqfy09yaZ7bVIF0//5ZI7Nf3YDe3S7GrBX5ieYuECyP6UNkTx9BRsAQeVvXEc6otzB7iCSnYBMGUGzCqeigoAWaVQUONsSR3Uatks= pinky@ipaserver.el81.local # noqa 204
# yamllint disable-line rule:line-length
- AAAAB3NzaC1yc2EAAAADAQABAAABgQDc8MIjaSrxLYHvu+hduoF4m6NUFSlXZWzYbd3BK4L47/U4eiXoOS6dcfuZJDjmLfOipc7XVp7NADwAgA1yBOAjbeVpXr2tC8w8saZibl75WBOEjDfNroiOh/f/ojrwwHg05QTVSZHs27sU1HBPyCQM/FHVM6EnRfmyiBkEBA/3ca0PJ9UJhWb2XisCaz6y6QcTh4gQnvHzgmEmK31GwiKnmBSEQuj8P5NGCO8RlN3cq3zpRpMDEoBRCjQYicllf/5P43r5OGvS1LhTiAMfyqE37URezNQa7aozBpH1GhIwAmjAtm84jXQjxUgZPYC0aSLuADYErScOP4792r6koH9t/DM5/M+jG2c4PNWynDczUw6Eaxl5E3hU0Ee9UN0Oee7iBnVenS/QMeZNyo5lMA/HXT5lrYiJGTYM0shRjGXXYBbJZhWerguSWDAdUd1gvuGP1nb7/+/Cvb46+HX7zYouS5Ojo0yPzMZ07X142jnKAfx9LnKdMUCwBJzbtoJ91Zc= pinky@ipaserver.el81.local # noqa 204
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDc8MIjaSrxLYHvu+hduoF4m6NUFSlXZWzYbd3BK4L47/U4eiXoOS6dcfuZJDjmLfOipc7XVp7NADwAgA1yBOAjbeVpXr2tC8w8saZibl75WBOEjDfNroiOh/f/ojrwwHg05QTVSZHs27sU1HBPyCQM/FHVM6EnRfmyiBkEBA/3ca0PJ9UJhWb2XisCaz6y6QcTh4gQnvHzgmEmK31GwiKnmBSEQuj8P5NGCO8RlN3cq3zpRpMDEoBRCjQYicllf/5P43r5OGvS1LhTiAMfyqE37URezNQa7aozBpH1GhIwAmjAtm84jXQjxUgZPYC0aSLuADYErScOP4792r6koH9t/DM5/M+jG2c4PNWynDczUw6Eaxl5E3hU0Ee9UN0Oee7iBnVenS/QMeZNyo5lMA/HXT5lrYiJGTYM0shRjGXXYBbJZhWerguSWDAdUd1gvuGP1nb7/+/Cvb46+HX7zYouS5Ojo0yPzMZ07X142jnKAfx9LnKdMUCwBJzbtoJ91Zc= pinky@ipaserver.el81.local # noqa 204
register: result
failed_when: not result.changed or result.failed

View File

@@ -81,8 +81,8 @@
- name: Test hostgroup presence with multiple hosts and action hostgroup
vars:
test_cases:
- { id: 1, value: "{{ test_hosts | lower }}", expected: true }
- { id: 2, value: "{{ test_hosts | upper }}", expected: false }
- { id: 1, value: "{{ test_hosts | map('lower') }}", expected: true }
- { id: 2, value: "{{ test_hosts | map('upper') }}", expected: false }
- { id: 3, value: "{{ test_hosts }}", expected: false }
- { id: 4, value: "{{ test_hosts[1] }}", expected: true }
- { id: 5, value: "{{ test_hosts[1] | lower }}", expected: false }
@@ -104,8 +104,8 @@
- name: Test hostgroup with multiple hosts and action member
vars:
test_cases:
- { id: 1, value: "{{ test_hosts | lower }}", state: "absent", expected: true }
- { id: 2, value: "{{ test_hosts | upper }}", state: "absent", expected: false }
- { id: 1, value: "{{ test_hosts | map('lower') }}", state: "absent", expected: true }
- { id: 2, value: "{{ test_hosts | map('upper') }}", state: "absent", expected: false }
- { id: 3, value: "{{ test_hosts }}", state: "present", expected: true }
- { id: 4, value: "{{ test_hosts[1] }}", state: "absent", expected: true }
- { id: 5, value: "{{ test_hosts[1] | lower }}", state: "absent", expected: false }
@@ -113,7 +113,7 @@
- { id: 7, value: "{{ test_hosts[0] | lower }}", state: "present", expected: false }
- { id: 8, value: "{{ test_hosts[0] }}", state: "present", expected: false }
- { id: 9, value: "{{ test_hosts[0] | upper }}", state: "present", expected: false }
- { id: 10, value: "{{ test_hosts | upper }}", state: "present", expected: true }
- { id: 10, value: "{{ test_hosts | map('upper') }}", state: "present", expected: true }
- { id: 11, value: "{{ test_hosts[1] }}", state: "present", expected: false }
- { id: 12, value: "{{ test_hosts[0] | lower }}", state: "present", expected: false }
- { id: 13, value: "{{ test_hosts[0] }}", state: "absent", expected: true }

View File

@@ -0,0 +1,67 @@
---
- name: Test passkeyconfig
hosts: "{{ ipa_test_host | default('ipaserver') }}"
# It is normally not needed to set "become" to "true" for a module test.
# Only set it to true if it is needed to execute commands as root.
become: false
# Enable "gather_facts" only if "ansible_facts" variable needs to be used.
gather_facts: false
module_defaults:
ipapasskeyconfig:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
tasks:
- name: Include FreeIPA facts.
ansible.builtin.include_tasks: ../env_freeipa_facts.yml
- name: Run tests only if passkey is supported
when: passkey_is_supported
block:
# TESTS
- name: Get current passkeyconfig
ipapasskeyconfig:
register: result_initial
failed_when: result_initial.failed
- name: Ensure require_user_verification is set to false
ipapasskeyconfig:
require_user_verification: false
register: result
failed_when: result.failed
- name: Ensure require_user_verification is set to false again
ipapasskeyconfig:
require_user_verification: false
register: result
failed_when: result.changed or result.failed
- name: Verify require_user_verification is false
ansible.builtin.assert:
that:
- result.passkeyconfig.require_user_verification == false
- name: Ensure require_user_verification is set to true
ipapasskeyconfig:
require_user_verification: true
register: result
failed_when: not result.changed or result.failed
- name: Ensure require_user_verification is set to true again
ipapasskeyconfig:
require_user_verification: true
register: result
failed_when: result.changed or result.failed
- name: Verify require_user_verification is true
ansible.builtin.assert:
that:
- result.passkeyconfig.require_user_verification == true
# CLEANUP: Restore original configuration
- name: Restore original passkeyconfig
ipapasskeyconfig:
require_user_verification: "{{ result_initial.passkeyconfig.require_user_verification }}"
when: result_initial.passkeyconfig is defined and result_initial.passkeyconfig.require_user_verification is defined

View File

@@ -0,0 +1,40 @@
---
- name: Test passkeyconfig
hosts: ipaclients, ipaserver
# It is normally not needed to set "become" to "true" for a module test.
# Only set it to true if it is needed to execute commands as root.
become: false
# Enable "gather_facts" only if "ansible_facts" variable needs to be used.
gather_facts: false
tasks:
- name: Include FreeIPA facts.
ansible.builtin.include_tasks: ../env_freeipa_facts.yml
# Test will only be executed if host is not a server.
- name: Execute with server context in the client.
ipapasskeyconfig:
ipaadmin_password: SomeADMINpassword
ipaapi_context: server
require_user_verification: false
register: result
failed_when: not (result.failed and result.msg is regex("No module named '*ipaserver'*"))
when: ipa_host_is_client and passkey_is_supported
# Import basic module tests, and execute with ipa_context set to 'client'.
# If ipaclients is set, it will be executed using the client, if not,
# ipaserver will be used.
#
# With this setup, tests can be executed against an IPA client, against
# an IPA server using "client" context, and ensure that tests are executed
# in upstream CI.
- name: Test passkeyconfig using client context, in client host.
import_playbook: test_passkeyconfig.yml
when: groups['ipaclients'] and passkey_is_supported
vars:
ipa_test_host: ipaclients
- name: Test passkeyconfig using client context, in server host.
import_playbook: test_passkeyconfig.yml
when: passkey_is_supported and (groups['ipaclients'] is not defined or not groups['ipaclients'])

View File

@@ -0,0 +1,161 @@
---
- name: Test sysaccount
hosts: "{{ ipa_test_host | default('ipaserver') }}"
# It is normally not needed to set "become" to "true" for a module test.
# Only set it to true if it is needed to execute commands as root.
become: false
# Enable "gather_facts" only if "ansible_facts" variable needs to be used.
gather_facts: false
module_defaults:
ipaprivilege:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
iparole:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipasysaccount:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
tasks:
- name: Verify if role sysaccount member tests are possible
ansible.builtin.shell:
cmd: |
echo SomeADMINpassword | kinit -c {{ krb5ccname }} admin > /dev/null
RESULT=$(KRB5CCNAME={{ krb5ccname }} ipa role-add-member --help)
kdestroy -A -c {{ krb5ccname }} > /dev/null
echo $RESULT
vars:
krb5ccname: "__check_ipa_role_add_member__"
register: check_role_add_member
- name: Execute tests
when: '"sysaccounts" in check_role_add_member.stdout'
block:
# CLEANUP TEST ITEMS
- name: Ensure sysaccount my-app is absent
ipasysaccount:
name: my-app
state: absent
- name: Ensure role "my-app role" is absent
iparole:
name: my-app role
state: absent
- name: Ensure privilege "my-app password change privilege" is absent
ipaprivilege:
name: my-app password change privilege
state: absent
# CREATE TEST ITEMS
- name: Ensure privilege "my-app password change privilege" is present
ipaprivilege:
name: my-app password change privilege
permission:
- "System: Change User password"
register: result
failed_when: not result.changed or result.failed
# TESTS
- name: Ensure sysaccount my-app is present with random password
ipasysaccount:
name: my-app
random: true
register: result
failed_when: not result.changed or result.failed
- name: Ensure role "my-app role" is present with sysaccount member my-app
iparole:
name: my-app role
sysaccount: my-app
privilege: my-app password change privilege
register: result
failed_when: not result.changed or result.failed
- name: Ensure role "my-app role" is present with sysaccount member my-app, again
iparole:
name: my-app role
sysaccount: my-app
privilege: my-app password change privilege
register: result
failed_when: result.changed or result.failed
- name: Ensure role my-app role does not have sysaccount member my-app
iparole:
name: my-app role
sysaccount: my-app
action: member
state: absent
register: result
failed_when: not result.changed or result.failed
- name: Ensure role my-app role does not have sysaccount member my-app, again
iparole:
name: my-app role
sysaccount: my-app
action: member
state: absent
register: result
failed_when: result.changed or result.failed
- name: Ensure role my-app role has sysaccount member my-app
iparole:
name: my-app role
sysaccount: my-app
action: member
register: result
failed_when: not result.changed or result.failed
- name: Ensure role my-app role has sysaccount member my-app, again
iparole:
name: my-app role
sysaccount: my-app
action: member
register: result
failed_when: result.changed or result.failed
- name: Ensure role my-app role has zero sysaccount members
iparole:
name: my-app role
sysaccount: []
register: result
failed_when: not result.changed or result.failed
- name: Ensure role my-app role has zero sysaccount members, again
iparole:
name: my-app role
sysaccount: []
register: result
failed_when: result.changed or result.failed
- name: Ensure role my-app role does not have sysaccount member my-app, again
iparole:
name: my-app role
sysaccount: my-app
action: member
state: absent
register: result
failed_when: result.changed or result.failed
# CLEANUP TEST ITEMS
- name: Ensure sysaccount my-app is absent
ipasysaccount:
name: my-app
state: absent
- name: Ensure role my-app role is absent
iparole:
name: my-app role
state: absent
- name: Ensure privilege "my-app password change privilege" is absent
ipaprivilege:
name: my-app password change privilege
state: absent

View File

@@ -19,7 +19,7 @@ pip install galaxy_importer
rm -f "$ANSIBLE_COLLECTION"-*.tar.gz
rm -f importer_result.json
utils/build-galaxy-release.sh
utils/build-collection.sh rpm
sed "s/LOCAL_IMAGE_DOCKER = True/LOCAL_IMAGE_DOCKER = ${use_docker}/" < tests/sanity/galaxy-importer.cfg > ${VENV}/galaxy-importer.cfg
export GALAXY_IMPORTER_CONFIG=${VENV}/galaxy-importer.cfg

View File

@@ -48,7 +48,8 @@
- name: Verify keytab
ansible.builtin.shell: ipa service-find "mysvc1/{{ ansible_facts['fqdn'] }}"
register: result
failed_when: result.failed or result.stdout | regex_search(" Keytab. true")
changed_when: false
failed_when: result.failed or (result.stdout | regex_search(" Keytab. [Tt]rue")) in [None, ""]
- name: Ensure service is disabled
ipaservice:
@@ -61,7 +62,8 @@
- name: Verify keytab
ansible.builtin.shell: ipa service-find "mysvc1/{{ ansible_facts['fqdn'] }}"
register: result
failed_when: result.failed or result.stdout | regex_search(" Keytab. true")
changed_when: false
failed_when: result.failed or (result.stdout | regex_search(" Keytab. [Ff]alse")) in [None, ""]
- name: Obtain keytab
ansible.builtin.shell: ipa-getkeytab -s "{{ ansible_facts['fqdn'] }}" -p "mysvc1/{{ ansible_facts['fqdn'] }}" -k mysvc1.keytab
@@ -69,7 +71,8 @@
- name: Verify keytab
ansible.builtin.shell: ipa service-find "mysvc1/{{ ansible_facts['fqdn'] }}"
register: result
failed_when: result.failed or result.stdout | regex_search(" Keytab. true")
changed_when: false
failed_when: result.failed or (result.stdout | regex_search(" Keytab. [Tt]rue")) in [None, ""]
- name: Ensure service is disabled
ipaservice:
@@ -82,7 +85,8 @@
- name: Verify keytab
ansible.builtin.shell: ipa service-find "mysvc1/{{ ansible_facts['fqdn'] }}"
register: result
failed_when: result.failed or result.stdout | regex_search(" Keytab. true")
changed_when: false
failed_when: result.failed or (result.stdout | regex_search(" Keytab. [Ff]alse")) in [None, ""]
- name: Ensure service is disabled, with no keytab.
ipaservice:

View File

@@ -5,6 +5,8 @@
gather_facts: yes
tasks:
- name: Include tasks ../env_freeipa_facts.yml
ansible.builtin.include_tasks: ../env_freeipa_facts.yml
# CLEANUP TEST ITEMS
@@ -83,6 +85,37 @@
register: result
failed_when: result.changed or result.failed
- name: Ensure service "test-service/{{ ansible_facts['fqdn'] }}" is present with auth_ind passkey
ipaservice:
ipaadmin_password: SomeADMINpassword
name: "test-service/{{ ansible_facts['fqdn'] }}"
auth_ind:
- passkey
register: result
failed_when: not result.changed or result.failed
when: passkey_is_supported
- name: Ensure service "test-service/{{ ansible_facts['fqdn'] }}" is present with auth_ind passkey, again
ipaservice:
ipaadmin_password: SomeADMINpassword
name: "test-service/{{ ansible_facts['fqdn'] }}"
auth_ind:
- passkey
register: result
failed_when: result.changed or result.failed
when: passkey_is_supported
- name: Check if correct message is given if passkey is not supported.
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: "test-service/{{ ansible_facts['fqdn'] }}"
auth_ind:
- passkey
register: result
failed_when: not result.failed or "'passkey' is not supported" not in result.msg
when: not passkey_is_supported
- name: Ensure service "test-service/{{ ansible_facts['fqdn'] }}" is present with empty auth_ind
ipaservice:
ipaadmin_password: SomeADMINpassword

View File

@@ -0,0 +1,150 @@
---
- name: Test sysaccount
hosts: "{{ ipa_test_host | default('ipaserver') }}"
# It is normally not needed to set "become" to "true" for a module test.
# Only set it to true if it is needed to execute commands as root.
become: false
# Enable "gather_facts" only if "ansible_facts" variable needs to be used.
gather_facts: false
module_defaults:
ipasysaccount:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
tasks:
- name: Verify sysaccount tests are possible
ansible.builtin.shell:
cmd: |
echo SomeADMINpassword | kinit -c {{ krb5ccname }} admin > /dev/null
RESULT=$(KRB5CCNAME={{ krb5ccname }} ipa sysaccount-add --help)
kdestroy -A -c {{ krb5ccname }} > /dev/null
echo $RESULT
vars:
krb5ccname: "__check_ipa_sysaccount_add__"
register: check_sysaccount_add
- name: Execute tests
when: '"ipa: ERROR: unknown command" not in check_sysaccount_add.stderr'
block:
# CLEANUP TEST ITEMS
- name: Ensure sysaccount my-app is absent
ipasysaccount:
name: my-app
state: absent
# CREATE TEST ITEMS
# TESTS
- name: Ensure sysaccount my-app is present with random password
ipasysaccount:
name: my-app
random: true
register: result
failed_when: not result.changed or
result.sysaccount.randompassword is not defined or
result.failed
- name: Ensure sysaccount my-app is present, again with updated random password and update_password always
ipasysaccount:
name: my-app
random: true
register: result2
failed_when: not result2.changed or
result2.sysaccount.randompassword is not defined or
result2.sysaccount.randompassword == result.sysaccount.randompassword or
result2.failed
- name: Ensure sysaccount my-app is present, again with random password and update_password on_create
ipasysaccount:
name: my-app
random: true
update_password: on_create
register: result
failed_when: not result2.changed or
result.sysaccount.randompassword is defined or
result.failed
# more tests here
- name: Ensure sysaccount my-app is disabled
ipasysaccount:
name: my-app
state: disabled
register: result
failed_when: not result.changed or result.failed
- name: Ensure sysaccount my-app is disabled, again
ipasysaccount:
name: my-app
state: disabled
register: result
failed_when: result.changed or result.failed
- name: Ensure sysaccount my-app is enabled
ipasysaccount:
name: my-app
state: enabled
register: result
failed_when: not result.changed or result.failed
- name: Ensure sysaccount my-app is enabled, again
ipasysaccount:
name: my-app
state: enabled
register: result
failed_when: result.changed or result.failed
- name: Ensure sysaccount my-app is privileged
ipasysaccount:
name: my-app
privileged: true
register: result
failed_when: not result.changed or result.failed
- name: Ensure sysaccount my-app is privileged, again
ipasysaccount:
name: my-app
privileged: true
register: result
failed_when: result.changed or result.failed
# ADDITIONAL TEST HERE?
- name: Ensure sysaccount my-app is not privileged
ipasysaccount:
name: my-app
privileged: false
register: result
failed_when: not result.changed or result.failed
- name: Ensure sysaccount my-app is not privileged, again
ipasysaccount:
name: my-app
privileged: false
register: result
failed_when: result.changed or result.failed
- name: Ensure sysaccount my-app is absent
ipasysaccount:
name: my-app
state: absent
register: result
failed_when: not result.changed or result.failed
- name: Ensure sysaccount my-app is absent again
ipasysaccount:
name: my-app
state: absent
register: result
failed_when: result.changed or result.failed
# CLEANUP TEST ITEMS
- name: Ensure sysaccount my-app is absent
ipasysaccount:
name: my-app
state: absent

View File

@@ -0,0 +1,40 @@
---
- name: Test sysaccount
hosts: ipaclients, ipaserver
# It is normally not needed to set "become" to "true" for a module test.
# Only set it to true if it is needed to execute commands as root.
become: false
# Enable "gather_facts" only if "ansible_facts" variable needs to be used.
gather_facts: false
tasks:
- name: Include FreeIPA facts.
ansible.builtin.include_tasks: ../env_freeipa_facts.yml
# Test will only be executed if host is not a server.
- name: Execute with server context in the client.
ipasysaccount:
ipaadmin_password: SomeADMINpassword
ipaapi_context: server
name: ThisShouldNotWork
register: result
failed_when: not (result.failed and result.msg is regex("No module named '*ipaserver'*"))
when: ipa_host_is_client
# Import basic module tests, and execute with ipa_context set to 'client'.
# If ipaclients is set, it will be executed using the client, if not,
# ipaserver will be used.
#
# With this setup, tests can be executed against an IPA client, against
# an IPA server using "client" context, and ensure that tests are executed
# in upstream CI.
- name: Test sysaccount using client context, in client host.
import_playbook: test_sysaccount.yml
when: groups['ipaclients']
vars:
ipa_test_host: ipaclients
- name: Test sysaccount using client context, in server host.
import_playbook: test_sysaccount.yml
when: groups['ipaclients'] is not defined or not groups['ipaclients']

View File

@@ -5,6 +5,9 @@
gather_facts: false
tasks:
- name: Include FreeIPA facts.
ansible.builtin.include_tasks: ../env_freeipa_facts.yml
- name: Remove test users
ipauser:
ipaadmin_password: SomeADMINpassword
@@ -125,6 +128,22 @@
register: result
failed_when: result.changed or result.failed
- name: User pinky with email "pinky"
ipauser:
ipaadmin_password: SomeADMINpassword
name: pinky
email: pinky
register: result
failed_when: not result.changed or result.failed
- name: User pinky with email "pinky", again
ipauser:
ipaadmin_password: SomeADMINpassword
name: pinky
email: pinky
register: result
failed_when: result.changed or result.failed
- name: User pinky present with changed settings
ipauser:
ipaadmin_password: SomeADMINpassword
@@ -138,7 +157,7 @@
# yamllint disable-line rule:line-length
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCqmVDpEX5gnSjKuv97AyzOhaUMMKz8ahOA3GY77tVC4o68KNgMCmDSEG1/kOIaElngNLaCha3p/2iAcU9Bi1tLKUlm2bbO5NHNwHfRxY/3cJtq+/7D1vxJzqThYwI4F9vr1WxyY2+mMTv3pXbfAJoR8Mu06XaEY5PDetlDKjHLuNWF+/O7ZU8PsULTa1dJZFrtXeFpmUoLoGxQBvlrlcPI1zDciCSU24t27Zan5Py2l5QchyI7yhCyMM77KDtj5+AFVpmkb9+zq50rYJAyFVeyUvwjzErvQrKJzYpA0NyBp7vskWbt36M16/M/LxEK7HA6mkcakO3ESWx5MT1LAjvdlnxbWG3787MxweHXuB8CZU+9bZPFBaJ+VQtOfJ7I8eH0S16moPC4ak8FlcFvOH8ERDPWLFDqfy09yaZ7bVIF0//5ZI7Nf3YDe3S7GrBX5ieYuECyP6UNkTx9BRsAQeVvXEc6otzB7iCSnYBMGUGzCqeigoAWaVQUONsSR3Uatks= pinky@ipaserver.el81.local # noqa 204
# yamllint disable-line rule:line-length
- AAAAB3NzaC1yc2EAAAADAQABAAABgQDc8MIjaSrxLYHvu+hduoF4m6NUFSlXZWzYbd3BK4L47/U4eiXoOS6dcfuZJDjmLfOipc7XVp7NADwAgA1yBOAjbeVpXr2tC8w8saZibl75WBOEjDfNroiOh/f/ojrwwHg05QTVSZHs27sU1HBPyCQM/FHVM6EnRfmyiBkEBA/3ca0PJ9UJhWb2XisCaz6y6QcTh4gQnvHzgmEmK31GwiKnmBSEQuj8P5NGCO8RlN3cq3zpRpMDEoBRCjQYicllf/5P43r5OGvS1LhTiAMfyqE37URezNQa7aozBpH1GhIwAmjAtm84jXQjxUgZPYC0aSLuADYErScOP4792r6koH9t/DM5/M+jG2c4PNWynDczUw6Eaxl5E3hU0Ee9UN0Oee7iBnVenS/QMeZNyo5lMA/HXT5lrYiJGTYM0shRjGXXYBbJZhWerguSWDAdUd1gvuGP1nb7/+/Cvb46+HX7zYouS5Ojo0yPzMZ07X142jnKAfx9LnKdMUCwBJzbtoJ91Zc= pinky@ipaserver.el81.local # noqa 204
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDc8MIjaSrxLYHvu+hduoF4m6NUFSlXZWzYbd3BK4L47/U4eiXoOS6dcfuZJDjmLfOipc7XVp7NADwAgA1yBOAjbeVpXr2tC8w8saZibl75WBOEjDfNroiOh/f/ojrwwHg05QTVSZHs27sU1HBPyCQM/FHVM6EnRfmyiBkEBA/3ca0PJ9UJhWb2XisCaz6y6QcTh4gQnvHzgmEmK31GwiKnmBSEQuj8P5NGCO8RlN3cq3zpRpMDEoBRCjQYicllf/5P43r5OGvS1LhTiAMfyqE37URezNQa7aozBpH1GhIwAmjAtm84jXQjxUgZPYC0aSLuADYErScOP4792r6koH9t/DM5/M+jG2c4PNWynDczUw6Eaxl5E3hU0Ee9UN0Oee7iBnVenS/QMeZNyo5lMA/HXT5lrYiJGTYM0shRjGXXYBbJZhWerguSWDAdUd1gvuGP1nb7/+/Cvb46+HX7zYouS5Ojo0yPzMZ07X142jnKAfx9LnKdMUCwBJzbtoJ91Zc= pinky@ipaserver.el81.local # noqa 204
register: result
failed_when: not result.changed or result.failed
@@ -392,6 +411,42 @@
register: result
failed_when: not result.changed or result.failed
- name: Ensure user pinky with userauthtype passkey exists
ipauser:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: pinky
first: pinky
last: user
userauthtype: passkey
register: result
failed_when: not result.changed or result.failed
when: passkey_is_supported
- name: Ensure user pinky with userauthtype passkey exists, again
ipauser:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: pinky
first: pinky
last: user
userauthtype: passkey
register: result
failed_when: result.changed or result.failed
when: passkey_is_supported
- name: Check if correct message is given if passkey is not supported.
ipauser:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: pinky
first: pinky
last: user
userauthtype: passkey
register: result
when: not passkey_is_supported
failed_when: not result.failed or "'passkey' is not supported" not in result.msg
- name: User pinky absent and preserved for future exclusion.
ipauser:
ipaadmin_password: SomeADMINpassword

View File

@@ -0,0 +1,181 @@
---
- name: Test user query
hosts: "{{ ipa_test_host | default('ipaserver') }}"
become: false
gather_facts: false
module_defaults:
ipauser:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
tasks:
# CLEANUP
- name: Ensure users "testuser1" and "testuser2" are absent
ipauser:
name:
- testuser1
- testuser2
- non-existing-user
state: absent
# CREATE TEST ITEMS
- name: Ensure users "testuser1" and "testuser2" are present
ipauser:
users:
- name: testuser1
first: first1
last: last1
- name: testuser2
first: first2
last: last2
- name: Query user "non-existing-user"
ipauser:
name:
- non-existing-user
#query_param: PKEY_ONLY
state: query
register: result
failed_when: result.changed or result.failed
- name: Print query information
ansible.builtin.debug:
var: result.user
- name: Query all users
ipauser:
state: query
query_param: PKEY_ONLY
register: result
failed_when: result.changed or result.failed
- name: Print query information
ansible.builtin.debug:
var: result.user
- name: Fail on missing "testuser1" in query result
ansible.builtin.fail:
msg: "'testuser1' not in query result {{ result.user.users }}"
when: ("testuser1" not in result.user.users)
- name: Fail on missing "testuser2" in query result
ansible.builtin.fail:
msg: "'testuser2' not in query result {{ result.user.users }}"
when: ("testuser2" not in result.user.users)
- name: Fail on "non-existing-user" in query result
ansible.builtin.fail:
msg: "'non-existing-user' in query result {{ result.user.users }}"
when: ("non-existing-user" in result.user.users)
- name: Query users "testuser1" and "testuser2"
ipauser:
name:
- testuser1
- testuser2
state: query
register: result
failed_when: result.changed or result.failed
- name: Print query information
ansible.builtin.debug:
var: result.user
- name: Fail on missing "testuser1" in query result
ansible.builtin.fail:
msg: "'testuser1' not in query result {{ result.user }}"
when: ("testuser1" not in result.user)
- name: Fail on missing "testuser2" in query result
ansible.builtin.fail:
msg: "'testuser2' not in query result {{ result.user }}"
when: ("testuser2" not in result.user)
- name: Fail on "non-existing-user" in query result
ansible.builtin.fail:
msg: "'non-existing-user' in query result {{ result.user }}"
when: ("non-existing-user" in result.user)
- name: Query all user parameters for "testuser1"
ipauser:
name:
- testuser1
query_param: ALL
state: query
register: result
failed_when: result.changed or result.failed
- name: Print query information
ansible.builtin.debug:
var: result.user
- name: Fail on missing information in query result
ansible.builtin.fail:
msg: "Query result {{ result.user }} is incomplete"
when: (result.user.disabled or
result.user.displayname != "first1 last1" or
"uid=testuser1,cn=users,cn=accounts,dc=" not in result.user.dn or
"testuser1@" not in result.user.email or
result.user.first != "first1" or
result.user.fullname != "first1 last1" or
result.user.gecos != "first1 last1" or
result.user.gid < 1 or
result.user.homedir != "/home/testuser1" or
result.user.initials != "fl" or
result.user.last != "last1" or
result.user.name != "testuser1" or
"objectclass" not in result.user or
result.user.preserved or
result.user.principal | length != 1 or
"testuser1@" not in result.user.principal[0] or
"/bin/" not in result.user.shell or
result.user.uid < 1)
- name: Query "uid", "first" and "last" parameters for all users
ipauser:
query_param:
- uid
- first
- last
state: query
register: result
failed_when: result.changed or result.failed
- name: Print query information
ansible.builtin.debug:
var: result.user
- name: Fail on less than 3 users in result
ansible.builtin.fail:
msg: "{{ result.user }} is not empty"
when: result.user | length < 3
- name: Fail on missing "testuser1" information in query result
ansible.builtin.fail:
msg: "'testuser1' not in query result {{ result.user }}"
when: ("testuser1" not in result.user or
result.user.testuser1.first != "first1" or
result.user.testuser1.last != "last1" or
result.user.testuser1.uid < 1 or
result.user.testuser1 | length != 3)
- name: Fail on missing "testuser2" information in query result
ansible.builtin.fail:
msg: "'testuser2' not in query result {{ result.user }}"
when: ("testuser2" not in result.user or
result.user.testuser2.first != "first2" or
result.user.testuser2.last != "last2" or
result.user.testuser2.uid < 1 or
result.user.testuser2 | length != 3)
# CLEANUP
- name: Ensure users "testuser1" and "testuser2" are absent
ipauser:
name:
- testuser1
- testuser2
state: absent

View File

@@ -277,7 +277,7 @@
# yamllint disable-line rule:line-length
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCqmVDpEX5gnSjKuv97AyzOhaUMMKz8ahOA3GY77tVC4o68KNgMCmDSEG1/kOIaElngNLaCha3p/2iAcU9Bi1tLKUlm2bbO5NHNwHfRxY/3cJtq+/7D1vxJzqThYwI4F9vr1WxyY2+mMTv3pXbfAJoR8Mu06XaEY5PDetlDKjHLuNWF+/O7ZU8PsULTa1dJZFrtXeFpmUoLoGxQBvlrlcPI1zDciCSU24t27Zan5Py2l5QchyI7yhCyMM77KDtj5+AFVpmkb9+zq50rYJAyFVeyUvwjzErvQrKJzYpA0NyBp7vskWbt36M16/M/LxEK7HA6mkcakO3ESWx5MT1LAjvdlnxbWG3787MxweHXuB8CZU+9bZPFBaJ+VQtOfJ7I8eH0S16moPC4ak8FlcFvOH8ERDPWLFDqfy09yaZ7bVIF0//5ZI7Nf3YDe3S7GrBX5ieYuECyP6UNkTx9BRsAQeVvXEc6otzB7iCSnYBMGUGzCqeigoAWaVQUONsSR3Uatks= pinky@ipaserver.el81.local # noqa 204
# yamllint disable-line rule:line-length
- AAAAB3NzaC1yc2EAAAADAQABAAABgQDc8MIjaSrxLYHvu+hduoF4m6NUFSlXZWzYbd3BK4L47/U4eiXoOS6dcfuZJDjmLfOipc7XVp7NADwAgA1yBOAjbeVpXr2tC8w8saZibl75WBOEjDfNroiOh/f/ojrwwHg05QTVSZHs27sU1HBPyCQM/FHVM6EnRfmyiBkEBA/3ca0PJ9UJhWb2XisCaz6y6QcTh4gQnvHzgmEmK31GwiKnmBSEQuj8P5NGCO8RlN3cq3zpRpMDEoBRCjQYicllf/5P43r5OGvS1LhTiAMfyqE37URezNQa7aozBpH1GhIwAmjAtm84jXQjxUgZPYC0aSLuADYErScOP4792r6koH9t/DM5/M+jG2c4PNWynDczUw6Eaxl5E3hU0Ee9UN0Oee7iBnVenS/QMeZNyo5lMA/HXT5lrYiJGTYM0shRjGXXYBbJZhWerguSWDAdUd1gvuGP1nb7/+/Cvb46+HX7zYouS5Ojo0yPzMZ07X142jnKAfx9LnKdMUCwBJzbtoJ91Zc= pinky@ipaserver.el81.local # noqa 204
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDc8MIjaSrxLYHvu+hduoF4m6NUFSlXZWzYbd3BK4L47/U4eiXoOS6dcfuZJDjmLfOipc7XVp7NADwAgA1yBOAjbeVpXr2tC8w8saZibl75WBOEjDfNroiOh/f/ojrwwHg05QTVSZHs27sU1HBPyCQM/FHVM6EnRfmyiBkEBA/3ca0PJ9UJhWb2XisCaz6y6QcTh4gQnvHzgmEmK31GwiKnmBSEQuj8P5NGCO8RlN3cq3zpRpMDEoBRCjQYicllf/5P43r5OGvS1LhTiAMfyqE37URezNQa7aozBpH1GhIwAmjAtm84jXQjxUgZPYC0aSLuADYErScOP4792r6koH9t/DM5/M+jG2c4PNWynDczUw6Eaxl5E3hU0Ee9UN0Oee7iBnVenS/QMeZNyo5lMA/HXT5lrYiJGTYM0shRjGXXYBbJZhWerguSWDAdUd1gvuGP1nb7/+/Cvb46+HX7zYouS5Ojo0yPzMZ07X142jnKAfx9LnKdMUCwBJzbtoJ91Zc= pinky@ipaserver.el81.local # noqa 204
register: result
failed_when: not result.changed or result.failed

View File

@@ -8,21 +8,28 @@ pwd=$(pwd)
usage() {
cat <<EOF
Usage: $prog [options] [<namespace> <name>]
Usage: $prog [options] rpm|aah|galaxy
Build Anible Collection for ansible-freeipa.
The namespace defaults to freeipa an name defaults to ansible_freeipa,
if namespace and name are not given. Namespace and name need to be set
together.
The namespace and name are defined according to the argument:
rpm freeipa.ansible_freeipa - General use and RPMs
galaxy freeipa.ansible_freeipa - Ansible Galaxy
aah redhat.rhel_idm - Ansible AutomationHub
The generated file README-COLLECTION.md is set in galaxy.yml as the
documentation entry point for the collections generated with aah and galaxy
as Ansible AutomationHub and also Ansible Galaxy are not able to render the
documentation README files in the collection properly.
Options:
-a Add all files, no only files known to git repo
-a Add all files, not only files known to git repo
-k Keep build directory
-i Install the generated collection
-o <A.B.C> Build offline without using git, using version A.B.C
Also enables -a
-p <path> Installation the generated collection in the path, the
-p <path> Install the generated collection in the given path, the
ansible_collections sub directory will be created and will
contain the collection: ansible_collections/<namespace>/<name>
Also enables -i
@@ -36,7 +43,9 @@ keep=0
install=0
path=
offline=
galaxy_version=
version=
namespace="freeipa"
name="ansible_freeipa"
while getopts "ahkio:p:" arg; do
case $arg in
a)
@@ -53,7 +62,7 @@ while getopts "ahkio:p:" arg; do
install=1
;;
o)
galaxy_version=$OPTARG
version=$OPTARG
offline=1
all=1
;;
@@ -70,60 +79,103 @@ while getopts "ahkio:p:" arg; do
done
shift $((OPTIND-1))
if [ $# != 0 ] && [ $# != 2 ]; then
if [ $# != 1 ]; then
usage
exit 1
fi
namespace="${1-freeipa}"
name="${2-ansible_freeipa}"
if [ -z "$namespace" ]; then
echo "Namespace might not be empty"
exit 1
fi
if [ -z "$name" ]; then
echo "Name might not be empty"
exit 1
fi
collection="$1"
case "$collection" in
rpm|galaxy)
# namespace and name are already set
;;
aah)
namespace="redhat"
name="rhel_idm"
;;
*)
echo "Unknown collection '$collection'"
usage
exit 1
;;
esac
collection_prefix="${namespace}.${name}"
collection_uname="${collection^^}"
[ -z "$galaxy_version" ] && \
galaxy_version=$(git describe --tags 2>/dev/null | sed -e "s/^v//")
[ -z "$version" ] && \
version=$(git describe --tags 2>/dev/null | sed -e "s/^v//")
if [ -z "$galaxy_version" ]; then
if [ -z "$version" ]; then
echo "Version could not be detected"
exit 1
fi
echo "Building collection: ${namespace}-${name}-${galaxy_version}"
echo "Building collection: ${namespace}-${name}-${version} for ${collection_uname}"
GALAXY_BUILD=".galaxy-build"
BUILD=".collection-build"
if [ -e "$GALAXY_BUILD" ]; then
echo "Removing existing $GALAXY_BUILD ..."
rm -rf "$GALAXY_BUILD"
echo -e "\033[ARemoving existing $GALAXY_BUILD ... \033[32;1mDONE\033[0m"
if [ -e "$BUILD" ]; then
echo "Removing existing $BUILD ..."
rm -rf "$BUILD"
echo -e "\033[ARemoving existing $BUILD ... \033[32;1mDONE\033[0m"
fi
mkdir "$GALAXY_BUILD"
echo "Copying files to build dir $GALAXY_BUILD ..."
mkdir "$BUILD"
echo "Copying files to build dir $BUILD ..."
if [ $all == 1 ]; then
# Copy all files except galaxy build dir
# Copy all files except collection build dir
for file in .[A-z]* [A-z]*; do
[[ "$file" == "${GALAXY_BUILD}" ]] && continue
cp -a "$file" "${GALAXY_BUILD}/"
[[ "$file" == "${BUILD}" ]] && continue
cp -a "$file" "${BUILD}/"
done
else
# git ls-tree is quoting, therefore ignore SC2046: Quote this to prevent
# word splitting
# shellcheck disable=SC2046
tar -cf - $(git ls-tree HEAD --name-only -r) | (cd "$GALAXY_BUILD/" && tar -xf -)
tar -cf - $(git ls-tree HEAD --name-only -r) | (cd "$BUILD/" && tar -xf -)
fi
echo -e "\033[ACopying files to build dir $GALAXY_BUILD ... \033[32;1mDONE\033[0m"
cd "$GALAXY_BUILD" || exit 1
echo -e "\033[ACopying files to build dir $BUILD ... \033[32;1mDONE\033[0m"
cd "$BUILD" || exit 1
sed -i -e "s/version: .*/version: \"$galaxy_version\"/" galaxy.yml
echo "Removing .copr, .git* and .pre* files from build dir $BUILD ..."
rm -rf .copr .git* .pre*
echo -e "\033[ARemoving files from build dir $BUILD ... \033[32;1mDONE\033[0m"
if [ "$collection" != "rpm" ]; then
mv README.md README-COLLECTION.md
cat > README.md <<EOF
FreeIPA Ansible collection
==========================
This repository contains Ansible roles and playbooks to install and uninstall FreeIPA servers, replicas and clients, also management modules.
Important
---------
For the documentation of this collection, please have a look at the documentation in the collection archive. Starting point: Base collection directory, file **\`README-COLLECTION.md\`**.
${collection_uname} is not providing proper user documentation nor is able to render the documentation that is part of the collection. Therefore original \`README.md\` had to be renamed to \`README-COLLECTION.md\` to ensure that ${collection_uname} is not trying to render it.
Please ignore any modules and plugins in the ${collection_uname} documentation section with the prefix \`ipaserver_\`, \`ipareplica_\`, \`ipaclient_\`, \`ipabackup_\` and \`ipasmartcard_\` and also \`module_utils\` and \`doc_fragments\`. These files are used internally only and are not supported to be used otherwise.
There is also the [generic ansible-freeipa ${version} upstream documentation](https://github.com/freeipa/ansible-freeipa/blob/v${version}/README.md) and also the [latest generic ansible-freeipa upstream documentation](https://github.com/freeipa/ansible-freeipa/blob/master/README.md), both without using the collection prefix \`${collection_prefix}\`.
EOF
if [ "$collection" == "aah" ]; then
cat >> README.md <<EOF
Support
-------
This collection is maintained by Red Hat RHEL team.
As Red Hat Ansible Certified Content, this collection is entitled to support through the Ansible Automation Platform (AAP) using the **Create issue** button on the top right corner.
EOF
fi
sed -i -e "s/readme: .*/readme: README.md/" galaxy.yml
sed -i -e "s/documentation: .*/documentation: README.md/" galaxy.yml
fi
sed -i -e "s/version: .*/version: \"$version\"/" galaxy.yml
sed -i -e "s/namespace: .*/namespace: \"$namespace\"/" galaxy.yml
sed -i -e "s/name: .*/name: \"$name\"/" galaxy.yml
find . -name "*~" -exec rm {} \;
find . -name "__py*__" -exec rm -rf {} \;
@@ -210,14 +262,14 @@ ansible-galaxy collection build --force --output-path="$pwd"
cd "$pwd" || exit 1
if [ $keep == 0 ]; then
echo "Removing build dir $GALAXY_BUILD ..."
rm -rf "$GALAXY_BUILD"
echo -e "\033[ARemoving build dir $GALAXY_BUILD ... \033[32;1mDONE\033[0m"
echo "Removing build dir $BUILD ..."
rm -rf "$BUILD"
echo -e "\033[ARemoving build dir $BUILD ... \033[32;1mDONE\033[0m"
else
echo "Keeping build dir $GALAXY_BUILD"
echo "Keeping build dir $BUILD"
fi
if [ $install == 1 ]; then
echo "Installing collection ${namespace}-${name}-${galaxy_version}.tar.gz ..."
ansible-galaxy collection install ${path:+"-p$path"} "${namespace}-${name}-${galaxy_version}.tar.gz" --force ${offline/1/--offline}
echo "Installing collection ${namespace}-${name}-${version}.tar.gz ..."
ansible-galaxy collection install ${path:+"-p$path"} "${namespace}-${name}-${version}.tar.gz" --force ${offline/1/--offline}
fi