Compare commits

...

22 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
33 changed files with 1961 additions and 926 deletions

View File

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

View File

@@ -17,8 +17,8 @@ jobs:
- 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: v25.9.2
rev: v26.4.0
hooks:
- id: ansible-lint
always_run: false
@@ -18,10 +18,9 @@ repos:
--profile production
--exclude tests/integration/
--exclude tests/unit/
--parseable
--nocolor
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.37.1
rev: v1.38.0
hooks:
- id: yamllint
files: \.(yaml|yml)$
@@ -30,7 +29,7 @@ repos:
hooks:
- id: flake8
- repo: https://github.com/pycqa/pylint
rev: v4.0.2
rev: v4.0.6
hooks:
- id: pylint
args:

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

@@ -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

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

@@ -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

@@ -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

@@ -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

@@ -41,7 +41,6 @@ options:
description: Require user verification for passkey authentication
required: false
type: bool
default: true
aliases: ["iparequireuserverification"]
author:
- Rafael Guterres Jeffman (@rjeffman)

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

@@ -176,7 +176,7 @@ def main():
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',
password=dict(required=False, type='str', no_log=True,
aliases=["userpassword"], default=None),
# mod

File diff suppressed because it is too large Load Diff

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

@@ -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

@@ -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

@@ -89,6 +89,7 @@ ignored-modules =
[pylint.DESIGN]
max-attributes=12
max-public-methods=25
[pylint.REFACTORING]
max-nested-blocks = 9

View File

@@ -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+

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

@@ -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

@@ -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

@@ -128,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
@@ -141,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

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