Compare commits

...

88 Commits

Author SHA1 Message Date
Rafael Guterres Jeffman
45700bc02b Merge pull request #1082 from t-woerner/fix_pwpolicy_maxsequence_test
pwpolicy test: Fix maxsequence test
2023-06-07 12:35:00 -03:00
Thomas Woerner
d04a12e522 pwpolicy test: Fix maxsequence test
The maxsequence test was testing maxrepeat. Therefore the typo reported
with https://github.com/freeipa/ansible-freeipa/pull/1081 was never
seen.

The test has been fixed.
2023-06-07 17:17:20 +02:00
Thomas Woerner
4e9ec11b23 Merge pull request #1081 from cutrightjm/patch-1
Fix typo in ipapwpolicy.py
2023-06-07 17:17:01 +02:00
Thomas Woerner
2d93051101 Merge pull request #1078 from rjeffman/ipapwpolicy_simple_attribute_test
ipapwpolicy: simplified and faster attribute verification
2023-06-07 17:12:36 +02:00
Jacob Cutright
1a7b279d78 Fix typo in ipapwpolicy.py
The 'maxsequence' attribute was never applied as there was a typo when
it was set. By fixing the field name, 'maxsequence' is correclty set.

The failure was not seen before due to missing tests. The tests will be
added in a separate PR.
2023-06-07 12:04:49 -03:00
Thomas Woerner
be228d1df3 Merge pull request #1094 from rjeffman/ci_disable_pytests
Upstream CI: Disable execution of pytest tests
2023-06-07 17:00:59 +02:00
Thomas Woerner
ce95c638be Merge pull request #1099 from rjeffman/roles_disallow_fqdn_domain_match
Don't allow the FQDN to match the domain on server installs
2023-06-07 16:56:06 +02:00
Thomas Woerner
876f39a6c5 Merge pull request #687 from yrro/ipacert
ipacert module
2023-06-07 16:54:47 +02:00
Rafael Guterres Jeffman
950840e050 Merge pull request #1101 from t-woerner/multiple_service_management
Multiple service management
2023-06-07 11:53:14 -03:00
Sam Morris
87e1edf575 New certificate management module.
There is a new certificate management module placed in the plugins
folder:

    plugins/modules/ipacert.py

The certificate module allows to request, revoke, release and retrieve
certificates for users, hosts and services.

Here is the documentation for the module:

    README-cert.md

New example playbooks have been added:

    playbooks/cert/cert-hold.yml
    playbooks/cert/cert-release.yml
    playbooks/cert/cert-request-host.yml
    playbooks/cert/cert-request-service.yml
    playbooks/cert/cert-request-user.yml
    playbooks/cert/cert-retrieve.yml
    playbooks/cert/cert-revoke.yml

New tests for the module can be found at:

    tests/cert/test_cert_client_context.yml
    tests/cert/test_cert_host.yml
    tests/cert/test_cert_service.yml
    tests/cert/test_cert_user.yml

The module has been co-authored by Sam Morris (@yrro) and Rafael
Guterres Jeffman (@rjeffman).
2023-06-07 11:35:25 -03:00
Thomas Woerner
09250cb2c5 ipaservice: Updated and new tests for certificates and multi service handling
The tests test_services_absent.yml, test_services_present.yml and
test_services_present_slice.yml have been updated to use in memory data
for testing instead of loading json files. This made is simpler to use
variables from the playbook for example for fqdn host names.

New tests for certificates with and without trailing new lines have been
added for single service and multiple service handling.
2023-06-07 13:36:48 +02:00
Thomas Woerner
872c9e4cb2 ipaservice: Add Denis Karpelevich to the authors header
Denis added the multi service handling code. Therefore he should be
listed in the file header.
2023-06-07 13:36:48 +02:00
Thomas Woerner
efe9c68600 ipaservice: Properly Handle certs with leading or trailing white space
Any leading or trailing whitespace is removed while adding the
certificates with serive_add_cert. To be able to compare the results
from service_show with the given certificates we have to remove the
white space also.
2023-06-07 13:36:28 +02:00
Denis Karpelevich
0d9873b81c Allow multiple services creation
Adding an option to create multiple services in one go.
Adding tests (present/absent/without_skip_host_check)

Copied from PR #1054

Signed-off-by: Denis Karpelevich <dkarpele@redhat.com>
2023-06-06 12:40:33 +02:00
Rafael Guterres Jeffman
5b91703bd7 Don't allow the FQDN to match the domain on server installs
If server FQDN matches the domain name, the installation will succeed,
but DNS records will not work. If 'setup_dns: true' is used, there will
be no A record for the host, only a NS record, and the PTR record will
point to the domain name.

Based on: https://github.com/freeipa/freeipa/pull/6853
Related to: https://pagure.io/freeipa/issue/9003
2023-06-05 12:56:47 -03:00
Rafael Guterres Jeffman
180afd7586 Merge pull request #1077 from rjeffman/update_gitignore
Make Git ignore temporary and output files.
2023-05-30 10:59:09 -03:00
Thomas Woerner
7f16914032 Merge pull request #1097 from rjeffman/fix_ansible_lint_var_naming
upstream CI: Disable ansible-lint var-naming check
2023-05-30 14:45:14 +02:00
Rafael Guterres Jeffman
306522acd8 upstream CI: Disable ansible-lint var-naming check
Latest ansible-lint version (6.16.1) started to raise an error when
variable names from within roles are not prefixed with  the role name.
Error: var-naming[no-role-prefix].

As Ansible sanity check does not enforce this, it will be disabled, for
now on ansible-freeipa's upstream CI.

A future effort to reduce the checks that are not being evaluated should
be done as preparation for future Ansible Galaxy and Automation Hub
requirements.
2023-05-16 16:08:51 -03:00
Rafael Guterres Jeffman
a155324188 Upstream CI: Disable execution of pytest tests.
The tests under 'tests/pytests' were a POC to bring tests that evaluate
the result of playbook execution on the IPA environment. This is
currently only implemented for dnszone tests, and similar test coverage
is obtained with other tests.

As there is an ongoing issue with Ansible's docker pluging
("the connection plugin 'docker' was not found"), which is stil under
investigation, by removing the pytest tests we'll remove the consistent
failures currently seen on upstream CI, and will not loose test
coverage, specially if we take into account downstream tests.

Also, a new version for the pytests will be available once multihost
testing is implemented for upstream.
2023-05-15 15:41:09 -03:00
Rafael Guterres Jeffman
8ec5b1fe21 Merge pull request #1092 from t-woerner/fix_requests_version_require_for_build_container
tests/azure/templates/build_container.yml: Quote requests with version
2023-05-08 11:41:38 -03:00
Thomas Woerner
316255d524 tests/azure/templates/build_container.yml: Quote requests with version
The version requirement for requests need to be quoted not to lead into
a pip install command issue.

This is related to PR #1089 (Pin requests to < 2.29 temporarily)
2023-05-08 16:28:20 +02:00
Rafael Guterres Jeffman
36b7a18e40 Merge pull request #1088 from t-woerner/fix_new_ansible_lint_disallowed_ignores
Fix new ansible lint disallowes ignores
2023-05-05 12:08:41 -03:00
Thomas Woerner
a32fcb3765 ansible_freeipa_module.py: Calm down ansible-test on print and sys.exit
The function exit_raw_json is a replacement for AnsibleModule.exit_json
without flterting out values for no_log parameters.

Ansible added checks for pylint to forbid print and also sys.exit and
fails with ansible-bad-function. As the check is not known outside of
ansible-test, the disable line needed also W0012:

    # pylint: disable=W0012,ansible-bad-function
2023-05-05 16:56:38 +02:00
Thomas Woerner
2d4cad6c1b ipaserver_test.py: Add missing default for random_serial_numbers
random_serial_numbers was missing the default value in the DOCMENTATION
section.
2023-05-05 16:56:38 +02:00
Thomas Woerner
a4b8e10a40 ansible-test: Do not use automatic field numbering specification
Automatic field numbering specification is not allowed by ansible-test.
2023-05-05 16:26:45 +02:00
Thomas Woerner
98681bd4d2 Use "#!/usr/bin/env python" for python shebang
ansible is not allowing to use "#!/usr/bin/python".

Due to a change in ansible-lint it is not possible to ignore the "bad"
shebang.
2023-05-05 16:26:45 +02:00
Thomas Woerner
2882e2426a Add -eu to all bash shebangs
ansible requires to either use "#!/bin/bash -eu" or "#!/bin/bash -eux"
for bash shebangs.
2023-05-05 16:26:45 +02:00
Thomas Woerner
f056775d95 Remove old or empty sanity ignore files
The old ignore file ignore-2.12.txt is not needed and used anymore. The
new files ignore-2.13.txt and ignore-2.14.txt are empty after
ansible-lint made nearly all ignores disallowed.

All the newly disallowed ignores need to be fixed.

See https://github.com/ansible/ansible-lint/pull/3102
2023-05-05 16:26:45 +02:00
Rafael Guterres Jeffman
ad5450cd6f Merge pull request #1089 from t-woerner/pin_requests_below_2_29
Pin requests to < 2.29 temporarily
2023-05-05 11:25:39 -03:00
Thomas Woerner
e75d82131d Pin requests to < 2.29 temporarily
Due to https://github.com/docker/docker-py/issues/3113 requests need to
be pinned below 2.29 as a temporary solution.
2023-05-05 15:06:38 +02:00
Rafael Guterres Jeffman
99e468ad60 Merge pull request #1083 from t-woerner/fix_azure_molecule_docker
tests/azure: Install molecule-plguins to get docker driver
2023-04-27 17:45:35 -03:00
Thomas Woerner
3cc111782c tests/azure: Install molecule-plguins to get docker driver
The docker driver is not part of molecule 5.0.0 anymore.
molecule-plugins need to be installed to get the driver.
2023-04-27 14:01:09 +02:00
Rafael Guterres Jeffman
b429b4495e Merge pull request #1035 from t-woerner/new_module_github_user_fix
Fixes and enhancements for utils/new_module and templates
2023-04-20 10:03:19 -03:00
Rafael Guterres Jeffman
0f99ef2199 Merge pull request #1080 from t-woerner/module_defaults
Create action group in collection for use with module_defaults
2023-04-20 10:03:10 -03:00
Thomas Woerner
1c8f1c28e1 utils/templates/test_module*.yml.in: Use generic module_defaults
The usage of module_defaults allows to reduce the size of the tests and
to have the needed information in the tasks only. The default values for the
parameters are automatically passed to the module by Ansible.

It is not possible to use a module group for module_defaults as this could
only be done with Ansible Collections. The tests are also used upstream and
downstream without a collection.

Without groups of a collection it is needed to add the defaults for all
modules separately.

Simple example:

    module_defaults:
      ipahost:
        ipaadmin_password: SomeADMINpassword
        ipaapi_context: "{{ ipa_context | default(omit) }}"

Several module example using YAML anchors and aliases:

    module_defaults:
      ipahost: &ipa_module_defaults
        ipaadmin_password: SomeADMINpassword
        ipaapi_context: "{{ ipa_context | default(omit) }}"
      ipauser: *ipa_module_defaults
      ipagroup: *ipa_module_defaults
2023-04-20 10:10:51 +02:00
Thomas Woerner
47d5211185 utils/templates/test_module*.yml.in: Better docs for become and gather_facts
The documentation for "become" and "gather_facts" has been updated to
make sure that these parameters are enabled only in new tests if it is
really needed.
2023-04-20 10:10:51 +02:00
Thomas Woerner
4a18ad03c8 utils/templates/{README*.md.in,test_module*.yml.in}: Use true and false
The values "yes" and "no" will not be valid in the future for bool
parameters. Therefore "yes" and "no" have been replaced by "true" and
"false".
2023-04-20 10:09:07 +02:00
Thomas Woerner
966797dbee utils/build-galaxy-release.sh: Create module action group
The module action group <collection-prefix>.modules is created
automatically while building the galaxy release.

The action group can be used for module_defaults in this way:

    module_defauls:
      group/<collection-prefix>.modules:
        ipaadmin_password: SomeADMINpassword

Example:

    module_defaults:
      group/freeipa.ansible_freeipa.modules:
        ipaadmin_password: SomeADMINpassword
        ipaapi_context: "{{ ipa_context | default(omit) }}"
    collections:
    - freeipa.ansible_freeipa
2023-04-20 10:04:41 +02:00
Thomas Woerner
892c0dd6f0 utils/galaxyfy.py: Handle module_defaults, match roles and modules
The section module_defaults was not handled by utils/galaxyfy.py, also
there was no verification that only roles and modules provided by
ansible-freeipa are matched for prepending the collection prefix.
2023-04-20 10:04:26 +02:00
Rafael Guterres Jeffman
645a234d92 Make Git ignore temporary and output files.
Ignore vim .swp files and files generated by creating ansible-freeipa
collection, when checking repository status.
2023-04-18 10:21:24 -03:00
Thomas Woerner
5cbc8b7ada New utils/facts.py: Provide facts about the repo like role and module lists
The list of modules and roles is needed in several scripts now,
therefore it makes sense to have one place for this.

Here are the current variables:

BASE_DIR:           Base directory of the repo
ROLES:              List of roles in the roles folder
MANAGEMENT_MODULES: List of management modules in the plugins/modules
                    folder
ROLES_MODULES:      List of modules in the roles/*/library folders
ALL_MODULES:        List of all modules, the management and the roles
                    modules

All lists are sorted.
2023-04-18 13:36:42 +02:00
Thomas Woerner
5e5fbd87bf utils/templates/ipamodule.py.in: Add missing bracket
The parameter argument spec of name was missing the closing bracket. The
bracket has been added.
2023-04-14 17:23:37 +02:00
Rafael Guterres Jeffman
35ded3bf53 utils/new_module: Ensure correct number of parameters for new_module
When testing the number parameters for new_module, the
`github_user` was not being taken into account.
2023-04-14 17:23:37 +02:00
Thomas Woerner
209c6365ea utils/new_module: Fix github_user test
new_module was always failing with "github_user is not valid". The wrong
variable was checked: $githubuser instead of $github_user.
2023-04-14 17:23:37 +02:00
Rafael Guterres Jeffman
a69446021b ipapwpolicy: simplified and faster attribute verification
Use a simpler and faster 'any()' test instead of creating two lists and
checking if resulting list is empty.
2023-04-11 18:45:49 -03:00
Varun Mylaraiah
b861a61857 Merge pull request #1073 from t-woerner/ipaserver_do_not_enable_RSN_by_default
ipaserver: Do not enable random serial numbers by default
2023-04-05 15:57:53 +05:30
Thomas Woerner
6faff2ac11 ipaserver: Do not enable random serial numbers by default
ipaserver_random_serial_numbers was enabled by default in
roles/ipaserver/defaults/main.yml. This should not be the default and
also resulted in issues in all IPA versions that do not support RSN.

The parameter now defaults to false.
2023-04-05 11:53:28 +02:00
Rafael Guterres Jeffman
82c0161245 Merge pull request #1072 from t-woerner/external_group_ipaexternalmember_fix
ipagroup: Fix ensuring external group group members (without trust-ad)
2023-04-04 17:56:11 -03:00
Thomas Woerner
ecab42b9f5 Merge pull request #1060 from rjeffman/ipaserver_random_serial_numbers
roles/ipaserver: Allow deployments with random serial numbers
2023-04-04 16:12:15 +02:00
Thomas Woerner
183ea7fd79 Merge pull request #1047 from dkarpele/dkarpele-1040
Update `EXAMPLE` sections for multiuser and multihost handling.
2023-04-04 16:00:21 +02:00
Rafael Guterres Jeffman
a4087a755b roles/ipaserver: Allow deployments with random serial numbers
Since FreeIPA version 4.10 it is possible to deploy servers that use
Random Serial Number v3 support for certificates.

This patch exposes the 'random_serial_numbers' parameter, as
'ipaserver_random_serial_numbers', allowing a user to have random serial
numbers enabled for the domain.

The use of random serial numbers is allowed on new installations only.
2023-04-04 10:35:07 -03:00
Thomas Woerner
fb3ff6d63d Merge pull request #1001 from dkarpele/dkarpele-879
[RFE] Allow multiple groups creation
2023-04-04 13:35:24 +02:00
Thomas Woerner
ee92d99243 ipagroup: Handle ensuring groups with mixed types without IPA fix 6741
Ensuring (adding) several groups with mixed types external, nonposix
and posix require to have a fix in IPA:

    FreeIPA issue: https://pagure.io/freeipa/issue/9349
    FreeIPA fix: https://github.com/freeipa/freeipa/pull/6741

The simple solution is to switch to client context for ensuring several
groups simply if the user was not explicitly asking for the server context
no matter if mixed types are used.
2023-04-04 13:13:41 +02:00
Denis Karpelevich
a649a8dfe1 [RFE] Allow multiple groups creation.
Adding an option `groups` to create multiple groups in one operation.
Adding tests (present/absent/external/nonposix) with server and
client context.
Simple example of `groups` option:
```
tasks:
- name: Ensure 2 groups are present
  ipagroup:
    ipaadmin_password: SomeADMINpassword
    groups:
    - name: group1
    - name: group2
```

Signed-off-by: Denis Karpelevich <dkarpele@redhat.com>
2023-04-04 13:13:40 +02:00
Thomas Woerner
80abf635c3 ipagroup: Fix ensuring external group group members (without trust-ad)
Due to an API misbehaviour in FreeIPA, ipaexternalmembers need to be
treated differently than other group members parameters. Even an empty
array triggers all tests for external members, including the check for
installed dcerpc bindings.

Therefore ipagroup module has been changed to not set ipaexternalmember
to an empty list if there are no external members to be added or
removed.
2023-04-03 15:00:47 +02:00
Rafael Guterres Jeffman
24e05d1df4 Merge pull request #1067 from t-woerner/ipaclient_ipaclient_defer_krb5_configuration_fix
ipaclient: Defer krb5 configuration fix
2023-03-30 16:32:16 -03:00
Rafael Guterres Jeffman
065e902182 Merge pull request #1068 from t-woerner/replica_server_uninstall_cleanup
ipareplica/server: Enable removal from domain with undeployment
2023-03-30 16:31:34 -03:00
Rafael Guterres Jeffman
96f5f5c86e Merge pull request #1069 from t-woerner/ansible_lint_fixes
Ansible lint fixes
2023-03-30 16:30:23 -03:00
Thomas Woerner
476d9d5057 ipareplica/server: Enable removal from domain with undeployment
New variables have been added to ipareplica and ipaserver role to enable
the removal from the domein with the undeployment.

`ipaserver_remove_from_domain`
This enables the removal of the server from the domain additionally to the
undeployment.

`ipaserver_remove_on_server`
The value defines the server/replica in the domain that will to be used to
remove the server/replica from the domain if
`ipaserver_ignore_topology_disconnect` and `ipaserver_remove_from_domain`
are enabled. Without the need to enable
`ipaserver_ignore_topology_disconnect`, the value will be automatically
detected using the replication agreements of the server/replica.

For the replica role it is possible to use the server variables, but
also the replica versions: `ipareplica_remove_from_domain` and
`ipareplica_remove_on_server`.

The already existing parameters `ipaserver_ignore_topology_disconnect` and
`ipaserver_ignore_last_of_role` have been added to the README files for
server and replica with descriptions. The same for the replica versions
of the parameters.

The ipareplica role is not calling the `ipa-server-install` anymore, it
is instead using (including) the server role for the task.

The new module `ipaserver_get_connected_server` has been added to the
server role to be able to get a connected server using the replication
agreements. This module is only used if
`ipaserver_ignore_topology_disconnect` is not needed.
2023-03-28 10:29:07 +02:00
Thomas Woerner
049024bbb2 tests/config/test_config_sid: Mark tasks as noqa 503
The latest ansible-lint failes for the tasks that are using
"when: sid_disabled.changed" with the error
"Tasks that run when changed should likely be handlers.". As
these tasks are tests and it would not make sense to use handlers here,
the tasks have been marked as noqa 503.
2023-03-27 12:29:30 +02:00
Thomas Woerner
ec03ad2bf9 ipareplica/server: Always cleanup root IPA cache
The cleanup of the root IPA cache was depending on the result of the
ipaserver_enable_ipa and ipareplica_enable_ipa tasks. Instead of
"when: something.changed" a handler should be used instead. As
"/root/.ipa_cache" should be removed always (same in command line) the
removal of the file has been moded into the always section and does not
need a when anymore.
2023-03-27 12:24:02 +02:00
Thomas Woerner
64c43c1ec0 ipaclient_configure_dns_resolver: Removed bad aliases
The parameters nameservers and searchdomains had both the alias "cn".
Both aliases have been removed.
2023-03-27 12:21:37 +02:00
Thomas Woerner
b1eb32993d ipapwpolicy: The alias for usercheck in argument_spec had typo
The alias for usercheck in argument_spec was "ipapwusercheck" instead of
"ipapwdusercheck".
2023-03-27 12:20:14 +02:00
Thomas Woerner
2ee7139560 ipanetgroup: Missing type for action and state DOCUMENTATION section
The types for the parameters action and state have been missing in the
DOCUMENTATION section of the module.
2023-03-27 12:17:38 +02:00
Thomas Woerner
10d072a8c4 ipaclient: ipaclient_fix_ca also needs krb_name parameter
With the fix to defer creating the final krb5.conf on clients a bug has
been introduced with ipaclient_fix_ca: The krb_name parameter that
points to the temporary krb5 configuration was not added to the module

Without this the server affinity is broken for allow_repair and additionally
ipaclient_fix_ca could fail if krb5 configuration needs to be repraied
and also CA needs to be fixed.

The krb_name parameter has been added to ipaclient_fix_ca and is also
properly set in tasks/install.yml.
2023-03-24 12:51:59 +01:00
Thomas Woerner
0ec89eb53c ipaclient: ipaclient_setup_nss also needs krb_name parameter
With the fix to defer creating the final krb5.conf on clients a bug has
been introduced with ipaclient_setup_nss: The krb_name parameter that
points to the temporary krb5 configuration was not added to the module.

With a properly configured DNS (like for example IPA DNS) the krb TXT
records have been present in the DNS configuration. These have been used
automatically as a fallback and broke server affinity for the client.
Without the TXT records creating the IPA NSS database failed with
 "Cannot find KDC for realm ..".

The krb_name parameter has been added to ipaclient_setup_nss and is also
properly set in tasks/install.yml.
2023-03-24 12:37:48 +01:00
Thomas Woerner
cf27a98c61 Merge pull request #1045 from rjeffman/ipauser_param_description
ipauser: Better description of UID and GID parameters
2023-03-20 14:09:39 +01:00
Thomas Woerner
fd3e87771a Merge pull request #1062 from rjeffman/ipareplica_remove_undefined_params
ipareplica role: Remove usage of undefined parameters.
2023-03-20 13:42:30 +01:00
Rafael Guterres Jeffman
e03752955f ipareplica role: Remove usage of undefined parameters.
Some ipareplica role had a few module calls with parameters set like
'some_argument | default(omit)' that were not actually available in such
modules. If a user provided 'some_argument', the paramater would then
be passed to the module and ipareplica deployment would fail.

By removing the parameters from the 'install' task, ipareplica
deployment works even if the variables are set by the user.
2023-03-16 22:28:29 -03:00
Rafael Guterres Jeffman
338df6e60e Merge pull request #1058 from t-woerner/ipahost_make_return_value_depending_on_hosts_param
ipahost: Make return value depending on hosts parameter
2023-03-16 10:10:26 -03:00
Thomas Woerner
3f3e495ab3 ipahost: Make return value depending on hosts parameter
The way how randompasswords are returned by the ipahost module depends
so far on the number of hosts that are handled by the module.

This is unexpected if for example a json file is provided with the hosts
parameter. As it might be unknown how many hosts are in the json file,
this behaviour is unexpected. The return should not vary in this case.

This chamge makes the return simply depend on the use of the hosts
paramater. As soon as this parameter is used, the return will always be:

"host": { "<the host>": { "randompassword": "<the host random password>" } }

In the simply case with one host it will be still

"host": { "randompassword": "<the host random password>" }

This change for ipahost is related to the ipauser PR #1053.
2023-03-14 12:56:33 +01:00
Rafael Guterres Jeffman
b05aec98c5 Merge pull request #1053 from t-woerner/ipauer_make_return_value_depending_on_users_parameter
ipauser: Make return value depending on users parameter
2023-03-10 08:26:35 -03:00
Rafael Guterres Jeffman
867f7ed520 Merge pull request #1050 from t-woerner/ipaclient_defer_krb5_configuration
ipaclient: Defer creating the final krb5.conf on clients
2023-03-09 18:05:42 -03:00
Thomas Woerner
3cc17a43aa Merge pull request #974 from dkarpele/dkarpele-919
Add subid option to select the sssd profile with-subid.
2023-03-08 13:56:48 +01:00
Denis Karpelevich
2b0b7db086 Add subid option to select the sssd profile with-subid.
This is an ansible-freeipa update for the freeipa RFE:
https://pagure.io/freeipa/issue/9159
"`ipa-client-install` should provide option to enable `subid: sss`
in `/etc/nsswitch.conf`".

This option allows to configure authselect with the sssd
profile + with-subid feature, in order to have SSSD setup as
a datasource for subid in /etc/nsswitch.conf.

The default behavior remains unchanged: without the option,
/etc/nsswitch.conf keeps the line subid: files

Signed-off-by: Denis Karpelevich <dkarpele@redhat.com>
2023-03-06 16:06:33 +01:00
Thomas Woerner
87afc56ee6 Merge pull request #1051 from rjeffman/fedora-spdx
Migrated to SPDX license.
2023-03-02 13:55:13 +01:00
Thomas Woerner
61caa57801 ipauser: Make return value depending on users parameter
The way how randompasswords are returned by the ipauser module depends
so far on the number of users that are handled by the module.

This is unexpected if for example a json file is provided with the users
parameter. As it might be unknown how many users are in the json file,
this behaviour is unexpected. The return should not vary in this case.

This chamge makes the return simply depend on the use of the users
paramater. As soon as this parameter is used, the return will always be:

"user": { "<the user>": { "randompassword": "<the user random password>" } }

In the simply case with one user it will be still

"user": { "randompassword": "<the user random password>" }

Fixes: #1052 (ipauser should consitently return randompasswords when
              used with users)
2023-03-02 11:42:32 +01:00
Thomas Woerner
6b5acd9b0c ipaclient: Defer creating the final krb5.conf on clients
A temporary krb5 configuration was used to join the domain in
ipaclient_join. After that the final krkb5 configuration was created
with enabled DNS discovery and used for the remainaing tasks, where also
a connection to the IPA API was done.

With several servers the DNS discovery could have picked up a different
server. If the client deployment was faster than the replication this
could have lead to an unknown host error.

The issue was seen in performance testing where many simultaneous client
enrollments have been done..

The goal is to keep server affinity as long as possible within the
deployment process:

The temporary krb5.conf that was used before in ipaclient_join was
pulled out into an own module. The generated temporary krb5.conf is now
used in ipaclient_join and also ipaclient_api.

The generation of the final krb5.conf is moved to the end of the
deployment process.

Same as: https://pagure.io/freeipa/issue/9228

The setup of certmonger has been pulled out of ipaclient_setup_nss and moved
to the end of the process after generating the final krb5.conf as it will
use t will only use /etc/krb5.conf.

Certificate issuance may fail during deployment due to using the final
krb5.conf, but certmonger will re-try the request in this case.

Same as: https://pagure.io/freeipa/issue/9246
2023-02-27 16:09:34 +01:00
Denis Karpelevich
78b5e66da4 Update EXAMPLE sections for multiuser and multihost handling.
Signed-off-by: Denis Karpelevich <dkarpele@redhat.com>
2023-02-23 21:53:03 +01:00
Rafael Guterres Jeffman
f6c376a68f Migrated to SPDX license.
According to [1] all Fedora packages need to be updated to use a SPDX
expression. This patch updates the ansible-freeipa spec template to
comply with this change.

[1] https://fedoraproject.org/wiki/Changes/SPDX_Licenses_Phase_1
2023-02-23 17:27:33 -03:00
Rafael Guterres Jeffman
691fbd083e ipauser: Better description of UID and GID parameters
This patch provides better text for the description of UID and GID
parameters.
2023-02-23 14:50:11 -03:00
Thomas Woerner
77cd20bc10 Merge pull request #1046 from rjeffman/fix_ansible_lint_tests
Fix ansible-lint on tests
2023-02-22 14:24:37 +01:00
Rafael Guterres Jeffman
16ce5f21de ansible-lint: License must be defined as a list. 2023-02-21 11:26:29 -03:00
Rafael Guterres Jeffman
dcf9c7d8ce ansible-lint: Fixed dangling 'when' clause.
A dangling 'when:' clause was failing anisble-lint tests as the task did
not match any valid schema. The dangling clause was removed, and the
usage of 'shell' was changed from free form to use the 'cmd' parameter.
2023-02-21 11:26:29 -03:00
Rafael Guterres Jeffman
c715d3aad2 ansible-lint: Fix key order on upstream tests
In latest ansible-lint versions, the use of "blocks" has a required
order to be implemented. According to ansible-lint error mesage, the
order is name, when, block, rescue, always.

As not following this rule is now an error, this patch fixes all tests
for the 'key-order[task]' error.
2023-02-21 11:26:29 -03:00
Rafael Guterres Jeffman
0d1e9d3f49 ansible-lint: Use 'missing-import' instead of '505'
ansible-lint is issuing an warning when using '# noqa 505' instead of
'#noqa missing-import' on playbooks. This patch changes all occurrences
of the tag to use the newer format.
2023-02-21 11:26:29 -03:00
Rafael Guterres Jeffman
b30ae1c9b5 Merge pull request #1037 from t-woerner/fix_allow_repair_missing_krb5.conf_with_DNS_lookup
ipaclient: Fix allow_repair with removed krb5.conf and DNS lookup
2023-02-09 07:57:53 -03:00
Thomas Woerner
bfeefaf454 ipaclient: Fix allow_repair with removed krb5.conf and DNS lookup
The test in ipaclient_test_keytab is at first trying to use an existing
krb5.conf to test if the host keytab can be used. With working DNS lookup
an absent krb5.conf is not reported as an error as DNS lookup is
silently used instead.

A temporary krb5.conf is now used in this test that forces to deactivate
DNS lookups and also to load /etc/krb5.conf. A missing krb5.conf is now
detected properly as the kinit call fails now properly. Thanks to Julien
Rische for this proposal.

ipaclient_test_keytab is now properly returning the state of usable or
not usable krb5.conf in krb5_conf_ok. This fixes the handling of this
case later on in the role.
2023-02-08 16:14:38 +01:00
139 changed files with 5348 additions and 659 deletions

View File

@@ -35,6 +35,7 @@ skip_list:
- yaml # yamllint should be executed separately.
- experimental # Do not run any experimental tests
- name[template] # Allow Jinja templating inside task names
- var-naming
use_default_rules: true

6
.gitignore vendored
View File

@@ -1,5 +1,11 @@
*.pyc
*.retry
*.swp
# collection files
freeipa-ansible_freeipa*.tar.gz
redhat-rhel_idm*.tar.gz
importer_result.json
# ignore virtual environments
/.tox/

175
README-cert.md Normal file
View File

@@ -0,0 +1,175 @@
Cert module
============
Description
-----------
The cert module makes it possible to request, revoke and retrieve SSL certificates for hosts, services and users.
Features
--------
* Certificate request
* Certificate hold/release
* Certificate revocation
* Certificate retrieval
Supported FreeIPA Versions
--------------------------
FreeIPA versions 4.4.0 and up are supported by the ipacert module.
Requirements
------------
**Controller**
* Ansible version: 2.8+
* Some tool to generate a certificate signing request (CSR) might be needed, like `openssl`.
**Node**
* Supported FreeIPA version (see above)
Usage
=====
Example inventory file
```ini
[ipaserver]
ipaserver.test.local
```
Example playbook to request a new certificate for a service:
```yaml
---
- name: Certificate request
hosts: ipaserver
tasks:
- name: Request a certificate for a web server
ipacert:
ipaadmin_password: SomeADMINpassword
state: requested
csr: |
-----BEGIN CERTIFICATE REQUEST-----
MIGYMEwCAQAwGTEXMBUGA1UEAwwOZnJlZWlwYSBydWxlcyEwKjAFBgMrZXADIQBs
HlqIr4b/XNK+K8QLJKIzfvuNK0buBhLz3LAzY7QDEqAAMAUGAytlcANBAF4oSCbA
5aIPukCidnZJdr491G4LBE+URecYXsPknwYb+V+ONnf5ycZHyaFv+jkUBFGFeDgU
SYaXm/gF8cDYjQI=
-----END CERTIFICATE REQUEST-----
principal: HTTP/www.example.com
register: cert
```
Example playbook to revoke an existing certificate:
```yaml
---
- name: Revoke certificate
hosts: ipaserver
tasks:
- name Revoke a certificate
ipacert:
ipaadmin_password: SomeADMINpassword
serial_number: 123456789
state: revoked
```
Example to hold a certificate (alias for revoking a certificate with reason `certificateHold (6)`):
```yaml
---
- name: Hold a certificate
hosts: ipaserver
tasks:
- name: Hold certificate
ipacert:
ipaadmin_password: SomeADMINpassword
serial_number: 0xAB1234
state: held
```
Example playbook to release hold of certificate (may be used with any revoked certificates, despite of the rovoke reason):
```yaml
---
- name: Release hold
hosts: ipaserver
tasks:
- name: Take a revoked certificate off hold
ipacert:
ipaadmin_password: SomeADMINpassword
serial_number: 0xAB1234
state: released
```
Example playbook to retrieve a certificate and save it to a file in the target node:
```yaml
---
- name: Retriev certificate
hosts: ipaserver
tasks:
- name: Retrieve a certificate and save it to file 'cert.pem'
ipacert:
ipaadmin_password: SomeADMINpassword
certificate_out: cert.pem
state: retrieved
```
ipacert
-------
Variable | Description | Required
-------- | ----------- | --------
`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no
`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no
`ipaapi_context` | The context in which the module will execute. Executing in a server context is preferred. If not provided context will be determined by the execution environment. Valid values are `server` and `client`. | no
`ipaapi_ldap_cache` | Use LDAP cache for IPA connection. The bool setting defaults to yes. (bool) | no
`csr` | X509 certificate signing request, in PEM format. | yes, if `state: requested`
`principal` | Host/service/user principal for the certificate. | yes, if `state: requested`
`add` \| `add_principal` | Automatically add the principal if it doesn't exist (service principals only). (bool) | no
`profile_id` \| `profile` | Certificate Profile to use | no
`ca` | Name of the issuing certificate authority. | no
`chain` | Include certificate chain in output. (bool) | no
`serial_number` | Certificate serial number. (int) | yes, if `state` is `retrieved`, `held`, `released` or `revoked`.
`revocation_reason` \| `reason` | Reason for revoking the certificate. Use one of the reason strings, or the corresponding value: "unspecified" (0), "keyCompromise" (1), "cACompromise" (2), "affiliationChanged" (3), "superseded" (4), "cessationOfOperation" (5), "certificateHold" (6), "removeFromCRL" (8), "privilegeWithdrawn" (9), "aACompromise" (10) | yes, if `state: revoked`
`certificate_out` | Write certificate (chain if `chain` is set) to this file, on the target node. | no
`state` | The state to ensure. It can be one of `requested`, `held`, `released`, `revoked`, or `retrieved`. `held` is the same as revoke with reason "certificateHold" (6). `released` is the same as `cert-revoke-hold` on IPA CLI, releasing the hold status of a certificate. | yes
Return Values
=============
Values are returned only if `state` is `requested` or `retrieved` and if `certificate_out` is not defined.
Variable | Description | Returned When
-------- | ----------- | -------------
`certificate` | Certificate fields and data. (dict) <br>Options: | if `state` is `requested` or `retrieved` and if `certificate_out` is not defined
&nbsp; | `certificate` - Issued X509 certificate in PEM encoding. Will include certificate chain if `chain: true`. (list) | always
&nbsp; | `san_dnsname` - X509 Subject Alternative Name. | When DNSNames are present in the Subject Alternative Name extension of the issued certificate.
&nbsp; | `issuer` - X509 distinguished name of issuer. | always
&nbsp; | `subject` - X509 distinguished name of certificate subject. | always
&nbsp; | `serial_number` - Serial number of the issued certificate. (int) | always
&nbsp; | `revoked` - Revoked status of the certificate. (bool) | if certificate was revoked
&nbsp; | `owner_user` - The username that owns the certificate. | if `state: retrieved` and certificate is owned by a user
&nbsp; | `owner_host` - The host that owns the certificate. | if `state: retrieved` and certificate is owned by a host
&nbsp; | `owner_service` - The service that owns the certificate. | if `state: retrieved` and certificate is owned by a service
&nbsp; | `valid_not_before` - Time when issued certificate becomes valid, in GeneralizedTime format (YYYYMMDDHHMMSSZ) | always
&nbsp; | `valid_not_after` - Time when issued certificate ceases to be valid, in GeneralizedTime format (YYYYMMDDHHMMSSZ) | always
Authors
=======
Sam Morris
Rafael Jeffman

View File

@@ -8,6 +8,9 @@ The group module allows to ensure presence and absence of groups and members of
The group module is as compatible as possible to the Ansible upstream `ipa_group` module, but additionally offers to add users to a group and also to remove users from a group.
## Note
Ensuring presence (adding) of several groups with mixed types (`external`, `nonposix` and `posix`) requires a fix in FreeIPA. The module implements a workaround to automatically use `client` context if the fix is not present in the target node FreeIPA and if more than one group is provided to the task using the `groups` parameter. If `ipaapi_context` is forced to be `server`, the module will fail in this case.
Features
--------
@@ -71,6 +74,62 @@ Example playbook to add groups:
name: appops
```
These three `ipagroup` module calls can be combined into one with the `groups` variable:
```yaml
---
- name: Playbook to handle groups
hosts: ipaserver
tasks:
- name: Ensure groups ops, sysops and appops are present
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: ops
gidnumber: 1234
- name: sysops
user:
- pinky
- name: appops
```
You can also alternatively use a json file containing the groups, here `groups_present.json`:
```json
{
"groups": [
{
"name": "group1",
"description": "description group1"
},
{
"name": "group2",
"description": "description group2"
}
]
}
```
And ensure the presence of the groups with this example playbook:
```yaml
---
- name: Tests
hosts: ipaserver
gather_facts: false
tasks:
- name: Include groups_present.json
include_vars:
file: groups_present.json
- name: Groups present
ipagroup:
ipaadmin_password: SomeADMINpassword
groups: "{{ groups }}"
```
Example playbook to add users to a group:
```yaml
@@ -112,11 +171,11 @@ Example playbook to add group members to a group:
Example playbook to add members from a trusted realm to an external group:
```yaml
--
---
- name: Playbook to handle groups.
hosts: ipaserver
became: true
tasks:
- name: Create an external group and add members from a trust to it.
ipagroup:
ipaadmin_password: SomeADMINpassword
@@ -127,6 +186,24 @@ Example playbook to add members from a trusted realm to an external group:
- WINIPA\\Developers
```
Example playbook to add nonposix and external groups:
```yaml
---
- name: Playbook to add nonposix and external groups
hosts: ipaserver
tasks:
- name: Add nonposix group sysops and external group appops
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: sysops
nonposix: true
- name: appops
external: true
```
Example playbook to remove groups:
```yaml
@@ -136,13 +213,29 @@ Example playbook to remove groups:
become: true
tasks:
# Remove goups sysops, appops and ops
# Remove groups sysops, appops and ops
- ipagroup:
ipaadmin_password: SomeADMINpassword
name: sysops,appops,ops
state: absent
```
Example playbook to ensure groups are absent:
```yaml
---
- name: Playbook to handle groups
hosts: ipaserver
tasks:
- name: Ensure groups ops and sysops are absent
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: ops
- name: sysops
state: absent
```
Variables
=========
@@ -152,8 +245,10 @@ Variable | Description | Required
`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no
`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no
`ipaapi_context` | The context in which the module will execute. Executing in a server context is preferred. If not provided context will be determined by the execution environment. Valid values are `server` and `client`. | no
`ipaapi_ldap_cache` | Use LDAP cache for IPA connection. The bool setting defaults to yes. (bool) | no
`ipaapi_ldap_cache` | Use LDAP cache for IPA connection. The bool setting defaults to <br/>. (bool) | no
`name` \| `cn` | The list of group name strings. | no
`groups` | The list of group dicts. Each `groups` dict entry can contain group variables.<br>There is one required option in the `groups` dict:| no
&nbsp; | `name` - The group name string of the entry. | yes
`description` | The group description string. | no
`gid` \| `gidnumber` | The GID integer. | no
`posix` | Create a non-POSIX group or change a non-POSIX to a posix group. `nonposix`, `posix` and `external` are mutually exclusive. (bool) | no

View File

@@ -372,8 +372,8 @@ There are only return values if one or more random passwords have been generated
Variable | Description | Returned When
-------- | ----------- | -------------
`host` | Host dict with random password. (dict) <br>Options: | If random is yes and host did not exist or update_password is yes
&nbsp; | `randompassword` - The generated random password | If only one host is handled by the module
&nbsp; | `name` - The host name of the host that got a new random password. (dict) <br> Options: <br> &nbsp; `randompassword` - The generated random password | If several hosts are handled by the module
&nbsp; | `randompassword` - The generated random password | If only one host is handled by the module without using the `hosts` parameter.
&nbsp; | `name` - The host name of the host that got a new random password. (dict) <br> Options: <br> &nbsp; `randompassword` - The generated random password | If several hosts are handled by the module with the `hosts` parameter.
Authors

View File

@@ -393,8 +393,8 @@ Variable | Description | Required
`passwordexpiration` \| `krbpasswordexpiration` | The kerberos password expiration date. Possible formats: `YYYYMMddHHmmssZ`, `YYYY-MM-ddTHH:mm:ssZ`, `YYYY-MM-ddTHH:mmZ`, `YYYY-MM-ddZ`, `YYYY-MM-dd HH:mm:ssZ` or `YYYY-MM-dd HH:mmZ`. The trailing 'Z' can be skipped. Only usable with IPA versions 4.7 and up. | no
`password` | The user password string. | no
`random` | Generate a random user password | no
`uid` \| `uidnumber` | The UID integer. | no
`gid` \| `gidnumber` | The GID integer. | no
`uid` \| `uidnumber` | User ID Number (system will assign one if not provided). | no
`gid` \| `gidnumber` | Group ID Number. | no
`city` | City | no
`userstate` \| `st` | State/Province | no
`postalcode` \| `zip` | Postalcode/ZIP | no
@@ -434,8 +434,8 @@ There are only return values if one or more random passwords have been generated
Variable | Description | Returned When
-------- | ----------- | -------------
`user` | User dict with random password. (dict) <br>Options: | If random is yes and user did not exist or update_password is yes
&nbsp; | `randompassword` - The generated random password | If only one user is handled by the module
&nbsp; | `name` - The user name of the user that got a new random password. (dict) <br> Options: <br> &nbsp; `randompassword` - The generated random password | If several users are handled by the module
&nbsp; | `randompassword` - The generated random password | If only one user is handled by the module without using the `users` parameter.
&nbsp; | `name` - The user name of the user that got a new random password. (dict) <br> Options: <br> &nbsp; `randompassword` - The generated random password | If several users are handled by the module with the `users` parameter.
Authors

View File

@@ -17,6 +17,7 @@ Features
* Modules for automount key management
* Modules for automount location management
* Modules for automount map management
* Modules for certificate management
* Modules for config management
* Modules for delegation management
* Modules for dns config management
@@ -436,6 +437,7 @@ Modules in plugin/modules
* [ipaautomountkey](README-automountkey.md)
* [ipaautomountlocation](README-automountlocation.md)
* [ipaautomountmap](README-automountmap.md)
* [ipacert](README-cert.md)
* [ipaconfig](README-config.md)
* [ipadelegation](README-delegation.md)
* [ipadnsconfig](README-dnsconfig.md)

View File

@@ -13,8 +13,8 @@ homepage: "https://github.com/freeipa/ansible-freeipa"
issues: "https://github.com/freeipa/ansible-freeipa/issues"
readme: "README.md"
license: "GPL-3.0-or-later"
license:
- "GPL-3.0-or-later"
tags:
- "linux"
- "system"

View File

@@ -0,0 +1,14 @@
- name: Certificate manage example
hosts: ipaserver
become: false
gather_facts: false
module_defaults:
ipacert:
ipaadmin_password: SomeADMINpassword
ipaapi_context: client
tasks:
- name: Temporarily hold a certificate
ipacert:
serial_number: 12345
state: held

View File

@@ -0,0 +1,15 @@
---
- name: Certificate manage example
hosts: ipaserver
become: false
gather_facts: false
module_defaults:
ipacert:
ipaadmin_password: SomeADMINpassword
ipaapi_context: client
tasks:
- name: Release a certificate hold
ipacert:
serial_number: 12345
state: released

View File

@@ -0,0 +1,26 @@
---
- name: Certificate manage example
hosts: ipaserver
become: false
gather_facts: false
module_defaults:
ipacert:
ipaadmin_password: SomeADMINpassword
ipaapi_context: client
tasks:
- name: Request a certificate for a host
ipacert:
csr: |
-----BEGIN CERTIFICATE REQUEST-----
MIIBWjCBxAIBADAbMRkwFwYDVQQDDBBob3N0LmV4YW1wbGUuY29tMIGfMA0GCSqG
SIb3DQEBAQUAA4GNADCBiQKBgQCzR3Vd4Cwl0uVgwB3+wxz+4JldFk3x526bPeuK
g8EEc+rEHILzJWeXC8ywCYPOgK9n7hrdMfVQiIx3yHYrY+0IYuLehWow4o1iJEf5
urPNAP9K9C4Y7MMXzzoQmoWR3IFQQpOYwvWOtiZfvrhmtflnYEGLE2tgz53gOQHD
NnbCCwIDAQABoAAwDQYJKoZIhvcNAQELBQADgYEAgF+6YC39WhnvmFgNz7pjAh5E
2ea3CgG+zrzAyiSBGG6WpXEjqMRnAQxciQNGxQacxjwWrscZidZzqg8URJPugewq
tslYB1+RkZn+9UWtfnWvz89+xnOgco7JlytnbH10Nfxt5fXXx13rY0tl54jBtk2W
422eYZ12wb4gjNcQy3A=
-----END CERTIFICATE REQUEST-----
principal: host/host.example.com
state: requested

View File

@@ -0,0 +1,23 @@
---
- name: Certificate manage example
hosts: ipaserver
become: false
gather_facts: false
module_defaults:
ipacert:
ipaadmin_password: SomeADMINpassword
ipaapi_context: client
tasks:
- name: Request a certificate for a service
ipacert:
csr: |
-----BEGIN CERTIFICATE REQUEST-----
MIGYMEwCAQAwGTEXMBUGA1UEAwwOZnJlZWlwYSBydWxlcyEwKjAFBgMrZXADIQBs
HlqIr4b/XNK+K8QLJKIzfvuNK0buBhLz3LAzY7QDEqAAMAUGAytlcANBAF4oSCbA
5aIPukCidnZJdr491G4LBE+URecYXsPknwYb+V+ONnf5ycZHyaFv+jkUBFGFeDgU
SYaXm/gF8cDYjQI=
-----END CERTIFICATE REQUEST-----
principal: HTTP/www.example.com
add: true
state: requested

View File

@@ -0,0 +1,27 @@
---
- name: Certificate manage example
hosts: ipaserver
become: false
gather_facts: false
module_defaults:
ipacert:
ipaadmin_password: SomeADMINpassword
ipaapi_context: client
tasks:
- name: Request a certificate for a user with a specific profile
ipacert:
csr: |
-----BEGIN CERTIFICATE REQUEST-----
MIIBejCB5AIBADAQMQ4wDAYDVQQDDAVwaW5reTCBnzANBgkqhkiG9w0BAQEFAAOB
jQAwgYkCgYEA7uChccy1Is1FTM0SF23WPYW472E3ozeLh2kzhKR9Ni6FLmeEGgu7
/hicR1VwvXHYkNwI1tpW9LqxRVvgr6vheqHySljrBcoRfshfYvKejp03l2327Bfq
BNxXqLcHylNEyg8SH0u63bWyxtgoDBfdZwdGAhYuJ+g4ev79J5eYoB0CAwEAAaAr
MCkGCSqGSIb3DQEJDjEcMBowGAYHKoZIzlYIAQQNDAtoZWxsbyB3b3JsZDANBgkq
hkiG9w0BAQsFAAOBgQADCi5BHDv1mrBFDWqYytFpQ1mrvr/mdax3AYXxNL2UEV8j
AqZAFTEnJXL/u1eVQtI1yotqxakyUBN4XZBP2CBgJRO93Mtry8cgvU1sPdU8Mavx
5gSnlP74Hio2ziscWWydlxpYxFx0gkKvu+0nyIpz954SVYwQ2wwk5FRqZnxI5w==
-----END CERTIFICATE REQUEST-----
principal: pinky
profile: IECUserRoles
state: requested

View File

@@ -0,0 +1,16 @@
---
- name: Certificate manage example
hosts: ipaserver
become: false
gather_facts: false
module_defaults:
ipacert:
ipaadmin_password: SomeADMINpassword
ipaapi_context: client
tasks:
- name: Retrieve a certificate
ipacert:
serial_number: 12345
state: retrieved
register: cert_retrieved

View File

@@ -0,0 +1,18 @@
---
- name: Certificate manage example
hosts: ipaserver
become: false
gather_facts: false
module_defaults:
ipacert:
ipaadmin_password: SomeADMINpassword
ipaapi_context: client
tasks:
- name: Permanently revoke a certificate issued by a lightweight sub-CA
ipacert:
serial_number: 12345
ca: vpn-ca
# reason: keyCompromise (1)
reason: 1
state: revoked

View File

@@ -0,0 +1,32 @@
---
- name: Playbook to handle multiple groups
hosts: ipaserver
tasks:
- name: Create multiple groups ops, sysops
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: ops
gidnumber: 1234
- name: sysops
- name: Add user and group members to groups sysops and appops
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: sysops
user:
- user1
- name: appops
group:
- group2
- name: Create multiple non-POSIX and external groups
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: nongroup
nonposix: true
- name: extgroup
external: true

View File

@@ -29,7 +29,8 @@ __all__ = ["gssapi", "netaddr", "api", "ipalib_errors", "Env",
"DEFAULT_CONFIG", "LDAP_GENERALIZED_TIME_FORMAT",
"kinit_password", "kinit_keytab", "run", "DN", "VERSION",
"paths", "tasks", "get_credentials_if_valid", "Encoding",
"load_pem_x509_certificate", "DNSName", "getargspec"]
"DNSName", "getargspec", "certificate_loader",
"write_certificate_list"]
import os
# ansible-freeipa requires locale to be C, IPA requires utf-8.
@@ -106,6 +107,7 @@ try:
except ImportError:
from ipalib.x509 import load_certificate
certificate_loader = load_certificate
from ipalib.x509 import write_certificate_list
# Try to import is_ipa_configured or use a fallback implementation.
try:
@@ -747,8 +749,8 @@ def exit_raw_json(module, **kwargs):
contains sensible data, it will be appear in the logs.
"""
module.do_cleanup_files()
print(jsonify(kwargs))
sys.exit(0)
print(jsonify(kwargs)) # pylint: disable=W0012,ansible-bad-function
sys.exit(0) # pylint: disable=W0012,ansible-bad-function
def __get_domain_validator():

571
plugins/modules/ipacert.py Normal file
View File

@@ -0,0 +1,571 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Authors:
# Sam Morris <sam@robots.org.uk>
# Rafael Guterres Jeffman <rjeffman@redhat.com>
#
# Copyright (C) 2021 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
"metadata_version": "1.0",
"supported_by": "community",
"status": ["preview"],
}
DOCUMENTATION = """
---
module: ipacert
short description: Manage FreeIPA certificates
description: Manage FreeIPA certificates
extends_documentation_fragment:
- ipamodule_base_docs
options:
csr:
description: |
X509 certificate signing request, in RFC 7468 PEM encoding.
Only available if `state: requested`, required if `csr_file` is not
provided.
type: str
csr_file:
description: |
Path to file with X509 certificate signing request, in RFC 7468 PEM
encoding. Only available if `state: requested`, required if `csr_file`
is not provided.
type: str
principal:
description: |
Host/service/user principal for the certificate.
Required if `state: requested`. Only available if `state: requested`.
type: str
add:
description: |
Automatically add the principal if it doesn't exist (service
principals only). Only available if `state: requested`.
type: bool
aliases: ["add_principal"]
required: false
ca:
description: Name of the issuing certificate authority.
type: str
required: false
serial_number:
description: |
Certificate serial number. Cannot be used with `state: requested`.
Required for all states, except `requested`.
type: int
profile:
description: Certificate Profile to use.
type: str
aliases: ["profile_id"]
required: false
revocation_reason:
description: |
Reason for revoking the certificate. Use one of the reason strings,
or the corresponding value: "unspecified" (0), "keyCompromise" (1),
"cACompromise" (2), "affiliationChanged" (3), "superseded" (4),
"cessationOfOperation" (5), "certificateHold" (6), "removeFromCRL" (8),
"privilegeWithdrawn" (9), "aACompromise" (10).
Use only if `state: revoked`. Required if `state: revoked`.
type: raw
aliases: ['reason']
certificate_out:
description: |
Write certificate (chain if `chain` is set) to this file, on the target
node.. Use only when `state` is `requested` or `retrieved`.
type: str
required: false
state:
description: |
The state to ensure. `held` is the same as revoke with reason
"certificateHold" (6). `released` is the same as `cert-revoke-hold`
on IPA CLI, releasing the hold status of a certificate.
choices: ["requested", "held", "released", "revoked", "retrieved"]
required: true
type: str
author:
authors:
- Sam Morris (@yrro)
- Rafael Guterres Jeffman (@rjeffman)
"""
EXAMPLES = """
- name: Request a certificate for a web server
ipacert:
ipaadmin_password: SomeADMINpassword
state: requested
csr: |
-----BEGIN CERTIFICATE REQUEST-----
MIGYMEwCAQAwGTEXMBUGA1UEAwwOZnJlZWlwYSBydWxlcyEwKjAFBgMrZXADIQBs
HlqIr4b/XNK+K8QLJKIzfvuNK0buBhLz3LAzY7QDEqAAMAUGAytlcANBAF4oSCbA
5aIPukCidnZJdr491G4LBE+URecYXsPknwYb+V+ONnf5ycZHyaFv+jkUBFGFeDgU
SYaXm/gF8cDYjQI=
-----END CERTIFICATE REQUEST-----
principal: HTTP/www.example.com
register: cert
- name: Request certificate for a user, with an appropriate profile.
ipacert:
ipaadmin_password: SomeADMINpassword
csr: |
-----BEGIN CERTIFICATE REQUEST-----
MIIBejCB5AIBADAQMQ4wDAYDVQQDDAVwaW5reTCBnzANBgkqhkiG9w0BAQEFAAOB
jQAwgYkCgYEA7uChccy1Is1FTM0SF23WPYW472E3ozeLh2kzhKR9Ni6FLmeEGgu7
/hicR1VwvXHYkNwI1tpW9LqxRVvgr6vheqHySljrBcoRfshfYvKejp03l2327Bfq
BNxXqLcHylNEyg8SH0u63bWyxtgoDBfdZwdGAhYuJ+g4ev79J5eYoB0CAwEAAaAr
MCkGCSqGSIb3DQEJDjEcMBowGAYHKoZIzlYIAQQNDAtoZWxsbyB3b3JsZDANBgkq
hkiG9w0BAQsFAAOBgQADCi5BHDv1mrBFDWqYytFpQ1mrvr/mdax3AYXxNL2UEV8j
AqZAFTEnJXL/u1eVQtI1yotqxakyUBN4XZBP2CBgJRO93Mtry8cgvU1sPdU8Mavx
5gSnlP74Hio2ziscWWydlxpYxFx0gkKvu+0nyIpz954SVYwQ2wwk5FRqZnxI5w==
-----END CERTIFICATE REQUEST-----
principal: pinky
profile_id: IECUserRoles
state: requested
- name: Temporarily hold a certificate
ipacert:
ipaadmin_password: SomeADMINpassword
serial_number: 12345
state: held
- name: Remove a certificate hold
ipacert:
ipaadmin_password: SomeADMINpassword
state: released
serial_number: 12345
- name: Permanently revoke a certificate issued by a lightweight sub-CA
ipacert:
ipaadmin_password: SomeADMINpassword
state: revoked
ca: vpn-ca
serial_number: 0x98765
reason: keyCompromise
- name: Retrieve a certificate
ipacert:
ipaadmin_password: SomeADMINpassword
serial_number: 12345
state: retrieved
register: cert_retrieved
"""
RETURN = """
certificate:
description: Certificate fields and data.
returned: |
if `state` is `requested` or `retrived` and `certificate_out`
is not defined.
type: dict
contains:
certificate:
description: |
Issued X509 certificate in PEM encoding. Will include certificate
chain if `chain: true` is used.
type: list
elements: str
returned: always
issuer:
description: X509 distinguished name of issuer.
type: str
sample: CN=Certificate Authority,O=EXAMPLE.COM
returned: always
serial_number:
description: Serial number of the issued certificate.
type: int
sample: 902156300
returned: always
valid_not_after:
description: |
Time when issued certificate ceases to be valid,
in GeneralizedTime format (YYYYMMDDHHMMSSZ).
type: str
returned: always
valid_not_before:
description: |
Time when issued certificate becomes valid, in
GeneralizedTime format (YYYYMMDDHHMMSSZ).
type: str
returned: always
subject:
description: X509 distinguished name of certificate subject.
type: str
sample: CN=www.example.com,O=EXAMPLE.COM
returned: always
san_dnsname:
description: X509 Subject Alternative Name.
type: list
elements: str
sample: ['www.example.com', 'other.example.com']
returned: |
when DNSNames are present in the Subject Alternative Name
extension of the issued certificate.
revoked:
description: Revoked status of the certificate.
type: bool
returned: always
owner_user:
description: The username that owns the certificate.
type: str
returned: when `state` is `retrieved`
owner_host:
description: The host that owns the certificate.
type: str
returned: when `state` is `retrieved`
owner_service:
description: The service that owns the certificate.
type: str
returned: when `state` is `retrieved`
"""
import base64
import time
import ssl
from ansible.module_utils import six
from ansible.module_utils._text import to_text
from ansible.module_utils.ansible_freeipa_module import (
IPAAnsibleModule, certificate_loader, write_certificate_list,
)
if six.PY3:
unicode = str
# Reasons are defined in RFC 5280 sec. 5.3.1; removeFromCRL is not present in
# this list; run the module with state=released instead.
REVOCATION_REASONS = {
'unspecified': 0,
'keyCompromise': 1,
'cACompromise': 2,
'affiliationChanged': 3,
'superseded': 4,
'cessationOfOperation': 5,
'certificateHold': 6,
'removeFromCRL': 8,
'privilegeWithdrawn': 9,
'aACompromise': 10,
}
def gen_args(
module, principal=None, add_principal=None, ca=None, chain=None,
profile=None, certificate_out=None, reason=None
):
args = {}
if principal is not None:
args['principal'] = principal
if add_principal is not None:
args['add'] = add_principal
if ca is not None:
args['cacn'] = ca
if profile is not None:
args['profile_id'] = profile
if certificate_out is not None:
args['out'] = certificate_out
if chain:
args['chain'] = True
if ca:
args['cacn'] = ca
if reason is not None:
args['revocation_reason'] = get_revocation_reason(module, reason)
return args
def get_revocation_reason(module, reason):
"""Ensure revocation reasion is a valid integer code."""
reason_int = -1
try:
reason_int = int(reason)
except ValueError:
reason_int = REVOCATION_REASONS.get(reason, -1)
if reason_int not in REVOCATION_REASONS.values():
module.fail_json(msg="Invalid revocation reason: %s" % reason)
return reason_int
def parse_cert_timestamp(dt):
"""Ensure time is in GeneralizedTime format (YYYYMMDDHHMMSSZ)."""
return time.strftime(
"%Y%m%d%H%M%SZ",
time.strptime(dt, "%a %b %d %H:%M:%S %Y UTC")
)
def result_handler(_module, result, _command, _name, _args, exit_args, chain):
"""Split certificate into fields."""
if chain:
exit_args['certificate'] = [
ssl.DER_cert_to_PEM_cert(c)
for c in result['result'].get('certificate_chain', [])
]
else:
exit_args['certificate'] = [
ssl.DER_cert_to_PEM_cert(
base64.b64decode(result['result']['certificate'])
)
]
exit_args['san_dnsname'] = [
str(dnsname)
for dnsname in result['result'].get('san_dnsname', [])
]
exit_args.update({
key: result['result'][key]
for key in [
'issuer', 'subject', 'serial_number',
'revoked', 'revocation_reason'
]
if key in result['result']
})
exit_args.update({
key: result['result'][key][0]
for key in ['owner_user', 'owner_host', 'owner_service']
if key in result['result']
})
exit_args.update({
key: parse_cert_timestamp(result['result'][key])
for key in ['valid_not_after', 'valid_not_before']
if key in result['result']
})
def do_cert_request(
module, csr, principal, add_principal=None, ca=None, profile=None,
chain=None, certificate_out=None
):
"""Request a certificate."""
args = gen_args(
module, principal=principal, ca=ca, chain=chain,
add_principal=add_principal, profile=profile,
)
exit_args = {}
commands = [[to_text(csr), "cert_request", args]]
changed = module.execute_ipa_commands(
commands,
result_handler=result_handler,
exit_args=exit_args,
chain=chain
)
if certificate_out is not None:
certs = (
certificate_loader(cert.encode("utf-8"))
for cert in exit_args['certificate']
)
write_certificate_list(certs, certificate_out)
exit_args = {}
return changed, exit_args
def do_cert_revoke(ansible_module, serial_number, reason=None, ca=None):
"""Revoke a certificate."""
_ign, cert = do_cert_retrieve(ansible_module, serial_number, ca)
if not cert or cert.get('revoked', False):
return False, cert
args = gen_args(ansible_module, ca=ca, reason=reason)
commands = [[serial_number, "cert_revoke", args]]
changed = ansible_module.execute_ipa_commands(commands)
return changed, cert
def do_cert_release(ansible_module, serial_number, ca=None):
"""Release hold on certificate."""
_ign, cert = do_cert_retrieve(ansible_module, serial_number, ca)
revoked = cert.get('revoked', True)
reason = cert.get('revocation_reason', -1)
if cert and not revoked:
return False, cert
if revoked and reason != 6: # can only release held certificates
ansible_module.fail_json(
msg="Cannot release hold on certificate revoked with"
" reason: %d" % reason
)
args = gen_args(ansible_module, ca=ca)
commands = [[serial_number, "cert_remove_hold", args]]
changed = ansible_module.execute_ipa_commands(commands)
return changed, cert
def do_cert_retrieve(
module, serial_number, ca=None, chain=None, outfile=None
):
"""Retrieve a certificate with 'cert-show'."""
args = gen_args(module, ca=ca, chain=chain, certificate_out=outfile)
exit_args = {}
commands = [[serial_number, "cert_show", args]]
module.execute_ipa_commands(
commands,
result_handler=result_handler,
exit_args=exit_args,
chain=chain,
)
if outfile is not None:
exit_args = {}
return False, exit_args
def main():
ansible_module = IPAAnsibleModule(
argument_spec=dict(
# requested
csr=dict(type="str"),
csr_file=dict(type="str"),
principal=dict(type="str"),
add_principal=dict(type="bool", required=False, aliases=["add"]),
profile_id=dict(type="str", aliases=["profile"], required=False),
# revoked
revocation_reason=dict(type="raw", aliases=["reason"]),
# general
serial_number=dict(type="int"),
ca=dict(type="str"),
chain=dict(type="bool", required=False),
certificate_out=dict(type="str", required=False),
# state
state=dict(
type="str",
required=True,
choices=[
"requested", "held", "released", "revoked", "retrieved"
]
),
),
mutually_exclusive=[["csr", "csr_file"]],
required_if=[
('state', 'requested', ['principal']),
('state', 'retrieved', ['serial_number']),
('state', 'held', ['serial_number']),
('state', 'released', ['serial_number']),
('state', 'revoked', ['serial_number', 'revocation_reason']),
],
supports_check_mode=False,
)
ansible_module._ansible_debug = True
# Get parameters
# requested
csr = ansible_module.params_get("csr")
csr_file = ansible_module.params_get("csr_file")
principal = ansible_module.params_get("principal")
add_principal = ansible_module.params_get("add_principal")
profile = ansible_module.params_get("profile_id")
# revoked
reason = ansible_module.params_get("revocation_reason")
# general
serial_number = ansible_module.params.get("serial_number")
ca = ansible_module.params_get("ca")
chain = ansible_module.params_get("chain")
certificate_out = ansible_module.params_get("certificate_out")
# state
state = ansible_module.params_get("state")
# Check parameters
if ansible_module.params_get("ipaapi_context") == "server":
ansible_module.fail_json(
msg="Context 'server' for ipacert is not yet supported."
)
invalid = []
if state == "requested":
invalid = ["serial_number", "revocation_reason"]
if csr is None and csr_file is None:
ansible_module.fail_json(
msg="Required 'csr' or 'csr_file' with 'state: requested'.")
else:
invalid = [
"csr", "principal", "add_principal", "profile"
"certificate_out"
]
if state in ["released", "held"]:
invalid.extend(["revocation_reason", "certificate_out", "chain"])
if state == "retrieved":
invalid.append("revocation_reason")
if state == "revoked":
invalid.extend(["certificate_out", "chain"])
elif state == "held":
reason = 6 # certificateHold
ansible_module.params_fail_used_invalid(invalid, state)
# Init
changed = False
exit_args = {}
# Connect to IPA API
# If executed on 'server' contexot, cert plugin uses the IPA RA agent
# TLS client certificate/key, which users are not able to access,
# resulting in a 'permission denied' exception when attempting to connect
# the CA service. Therefore 'client' context in forced for this module.
with ansible_module.ipa_connect(context="client"):
if state == "requested":
if csr_file is not None:
with open(csr_file, "rt") as csr_in:
csr = "".join(csr_in.readlines())
changed, exit_args = do_cert_request(
ansible_module,
csr,
principal,
add_principal,
ca,
profile,
chain,
certificate_out
)
elif state in ("held", "revoked"):
changed, exit_args = do_cert_revoke(
ansible_module, serial_number, reason, ca)
elif state == "released":
changed, exit_args = do_cert_release(
ansible_module, serial_number, ca)
elif state == "retrieved":
changed, exit_args = do_cert_retrieve(
ansible_module, serial_number, ca, chain, certificate_out)
# Done
ansible_module.exit_json(changed=changed, certificate=exit_args)
if __name__ == "__main__":
main()

View File

@@ -1394,15 +1394,16 @@ def gen_args(entry):
if record_value is not None:
record_type = entry['record_type']
rec = "{}record".format(record_type.lower())
rec = "{0}record".format(record_type.lower())
args[rec] = ensure_data_is_list(record_value)
else:
for field in _RECORD_FIELDS:
record_value = entry.get(field) or entry.get("%sord" % field)
if record_value is not None:
# pylint: disable=use-maxsplit-arg
record_type = field.split('_')[0]
rec = "{}record".format(record_type.lower())
rec = "{0}record".format(record_type.lower())
args[rec] = ensure_data_is_list(record_value)
records = {

View File

@@ -41,8 +41,88 @@ options:
description: The group name
type: list
elements: str
required: true
required: false
aliases: ["cn"]
groups:
description: The list of group dicts (internally gid).
type: list
elements: dict
suboptions:
name:
description: The group (internally gid).
type: str
required: true
aliases: ["cn"]
description:
description: The group description
type: str
required: false
gid:
description: The GID
type: int
required: false
aliases: ["gidnumber"]
nonposix:
description: Create as a non-POSIX group
required: false
type: bool
external:
description: Allow adding external non-IPA members from trusted domains
required: false
type: bool
posix:
description:
Create a non-POSIX group or change a non-POSIX to a posix group.
required: false
type: bool
nomembers:
description: Suppress processing of membership attributes
required: false
type: bool
user:
description: List of user names assigned to this group.
required: false
type: list
elements: str
group:
description: List of group names assigned to this group.
required: false
type: list
elements: str
service:
description:
- List of service names assigned to this group.
- Only usable with IPA versions 4.7 and up.
required: false
type: list
elements: str
membermanager_user:
description:
- List of member manager users assigned to this group.
- Only usable with IPA versions 4.8.4 and up.
required: false
type: list
elements: str
membermanager_group:
description:
- List of member manager groups assigned to this group.
- Only usable with IPA versions 4.8.4 and up.
required: false
type: list
elements: str
externalmember:
description:
- List of members of a trusted domain in DOM\\name or name@domain form.
required: false
type: list
elements: str
aliases: ["ipaexternalmember", "external_member"]
idoverrideuser:
description:
- User ID overrides to add
required: false
type: list
elements: str
description:
description: The group description
type: str
@@ -144,6 +224,14 @@ EXAMPLES = """
ipaadmin_password: SomeADMINpassword
name: appops
# Create multiple groups ops, sysops
- ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: ops
gidnumber: 1234
- name: sysops
# Add user member pinky to group sysops
- ipagroup:
ipaadmin_password: SomeADMINpassword
@@ -160,7 +248,7 @@ EXAMPLES = """
user:
- brain
# Add group members sysops and appops to group sysops
# Add group members sysops and appops to group ops
- ipagroup:
ipaadmin_password: SomeADMINpassword
name: ops
@@ -168,6 +256,17 @@ EXAMPLES = """
- sysops
- appops
# Add user and group members to groups sysops and appops
- ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: sysops
user:
- user1
- name: appops
group:
- group2
# Create a non-POSIX group
- ipagroup:
ipaadmin_password: SomeADMINpassword
@@ -189,7 +288,16 @@ EXAMPLES = """
- WINIPA\\Web Users
- WINIPA\\Developers
# Remove goups sysops, appops, ops and nongroup
# Create multiple non-POSIX and external groups
- ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: nongroup
nonposix: true
- name: extgroup
external: true
# Remove groups sysops, appops, ops and nongroup
- ipagroup:
ipaadmin_password: SomeADMINpassword
name: sysops,appops,ops, nongroup
@@ -203,6 +311,20 @@ 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
from ansible.module_utils import six
if six.PY3:
unicode = str
# Ensuring (adding) several groups with mixed types external, nonposix
# and posix require to have a fix in IPA:
# FreeIPA issue: https://pagure.io/freeipa/issue/9349
# FreeIPA fix: https://github.com/freeipa/freeipa/pull/6741
try:
from ipaserver.plugins import baseldap
except ImportError:
FIX_6741_DEEPCOPY_OBJECTCLASSES = False
else:
FIX_6741_DEEPCOPY_OBJECTCLASSES = \
"deepcopy" in baseldap.LDAPObject.__json__.__code__.co_names
def find_group(module, name):
@@ -257,6 +379,22 @@ def gen_member_args(user, group, service, externalmember, idoverrideuser):
return _args
def check_parameters(module, state, action):
invalid = []
if state == "present":
if action == "member":
invalid = ["description", "gid", "posix", "nonposix", "external",
"nomembers"]
else:
invalid = ["description", "gid", "posix", "nonposix", "external",
"nomembers"]
if action == "group":
invalid.extend(["user", "group", "service", "externalmember"])
module.params_fail_used_invalid(invalid, state, action)
def is_external_group(res_find):
"""Verify if the result group is an external group."""
return res_find and 'ipaexternalgroup' in res_find['objectclass']
@@ -285,45 +423,63 @@ def check_objectclass_args(module, res_find, posix, external):
def main():
group_spec = dict(
# present
description=dict(type="str", default=None),
gid=dict(type="int", aliases=["gidnumber"], default=None),
nonposix=dict(required=False, type='bool', default=None),
external=dict(required=False, type='bool', default=None),
posix=dict(required=False, type='bool', default=None),
nomembers=dict(required=False, type='bool', default=None),
user=dict(required=False, type='list', elements="str",
default=None),
group=dict(required=False, type='list', elements="str",
default=None),
service=dict(required=False, type='list', elements="str",
default=None),
idoverrideuser=dict(required=False, type='list', elements="str",
default=None),
membermanager_user=dict(required=False, type='list',
elements="str", default=None),
membermanager_group=dict(required=False, type='list',
elements="str", default=None),
externalmember=dict(required=False, type='list', elements="str",
default=None,
aliases=[
"ipaexternalmember",
"external_member"
])
)
ansible_module = IPAAnsibleModule(
argument_spec=dict(
# general
name=dict(type="list", elements="str", aliases=["cn"],
required=True),
# present
description=dict(type="str", default=None),
gid=dict(type="int", aliases=["gidnumber"], default=None),
nonposix=dict(required=False, type='bool', default=None),
external=dict(required=False, type='bool', default=None),
posix=dict(required=False, type='bool', default=None),
nomembers=dict(required=False, type='bool', default=None),
user=dict(required=False, type='list', elements="str",
default=None),
group=dict(required=False, type='list', elements="str",
default=None),
service=dict(required=False, type='list', elements="str",
default=None),
idoverrideuser=dict(required=False, type='list', elements="str",
default=None),
membermanager_user=dict(required=False, type='list',
elements="str", default=None),
membermanager_group=dict(required=False, type='list',
elements="str", default=None),
externalmember=dict(required=False, type='list', elements="str",
default=None,
aliases=[
"ipaexternalmember",
"external_member"
]),
default=None, required=False),
groups=dict(type="list",
default=None,
options=dict(
# Here name is a simple string
name=dict(type="str", required=True,
aliases=["cn"]),
# Add group specific parameters
**group_spec
),
elements='dict',
required=False),
# general
action=dict(type="str", default="group",
choices=["member", "group"]),
# state
state=dict(type="str", default="present",
choices=["present", "absent"]),
# Add group specific parameters for simple use case
**group_spec
),
# It does not make sense to set posix, nonposix or external at the
# same time
mutually_exclusive=[['posix', 'nonposix', 'external']],
mutually_exclusive=[['posix', 'nonposix', 'external'],
["name", "groups"]],
required_one_of=[["name", "groups"]],
supports_check_mode=True,
)
@@ -333,6 +489,7 @@ def main():
# general
names = ansible_module.params_get("name")
groups = ansible_module.params_get("groups")
# present
description = ansible_module.params_get("description")
@@ -354,31 +511,50 @@ def main():
state = ansible_module.params_get("state")
# Check parameters
invalid = []
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 == "present":
if len(names) != 1:
if names is not None and len(names) != 1:
ansible_module.fail_json(
msg="Only one group can be added at a time.")
if action == "member":
invalid = ["description", "gid", "posix", "nonposix", "external",
"nomembers"]
msg="Only one group can be added at a time using 'name'.")
if state == "absent":
if len(names) < 1:
ansible_module.fail_json(
msg="No name given.")
invalid = ["description", "gid", "posix", "nonposix", "external",
"nomembers"]
if action == "group":
invalid.extend(["user", "group", "service", "externalmember"])
ansible_module.params_fail_used_invalid(invalid, state, action)
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:
#
# FreeIPA issue: https://pagure.io/freeipa/issue/9349
# FreeIPA fix: https://github.com/freeipa/freeipa/pull/6741
#
# The simple solution is to switch to client context for ensuring
# several groups simply if the user was not explicitly asking for
# the server context no matter if mixed types are used.
context = None
if state == "present" and groups is not None and len(groups) > 1 \
and not FIX_6741_DEEPCOPY_OBJECTCLASSES:
_context = ansible_module.params_get("ipaapi_context")
if _context is None:
context = "client"
ansible_module.debug(
"Switching to client context due to an unfixed issue in "
"your IPA version: https://pagure.io/freeipa/issue/9349")
elif _context == "server":
ansible_module.fail_json(
msg="Ensuring several groups with server context is not "
"supported by your IPA version: "
"https://pagure.io/freeipa/issue/9349")
# Use groups if names is None
if groups is not None:
names = groups
# Init
changed = False
@@ -389,7 +565,7 @@ def main():
posix = not nonposix
# Connect to IPA API
with ansible_module.ipa_connect():
with ansible_module.ipa_connect(context=context):
has_add_member_service = ansible_module.ipa_command_param_exists(
"group_add_member", "service")
@@ -415,8 +591,57 @@ def main():
"supported by your IPA version")
commands = []
group_set = set()
for group_name in names:
if isinstance(group_name, dict):
name = group_name.get("name")
if name in group_set:
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")
check_parameters(ansible_module, state, action)
elif isinstance(group_name, (str, unicode)):
name = group_name
else:
ansible_module.fail_json(msg="Group '%s' is not valid" %
repr(group_name))
for name in names:
# Make sure group exists
res_find = find_group(ansible_module, name)
@@ -593,10 +818,12 @@ def main():
del_member_args["service"] = service_del
if is_external_group(res_find):
add_member_args["ipaexternalmember"] = \
externalmember_add
del_member_args["ipaexternalmember"] = \
externalmember_del
if len(externalmember_add) > 0:
add_member_args["ipaexternalmember"] = \
externalmember_add
if len(externalmember_del) > 0:
del_member_args["ipaexternalmember"] = \
externalmember_del
elif externalmember or external:
ansible_module.fail_json(
msg="Cannot add external members to a "

View File

@@ -44,7 +44,7 @@ options:
aliases: ["fqdn"]
required: false
hosts:
description: The list of user host dicts
description: The list of host dicts
required: false
type: list
elements: dict
@@ -441,6 +441,15 @@ EXAMPLES = """
description: Example host
force: yes
# Ensure multiple hosts are present with random passwords
- ipahost:
ipaadmin_password: SomeADMINpassword
hosts:
- name: host01.example.com
random: yes
- name: host02.example.com
random: yes
# Initiate generation of a random password for the host
- ipahost:
ipaadmin_password: SomeADMINpassword
@@ -449,6 +458,18 @@ EXAMPLES = """
ip_address: 192.168.0.123
random: yes
# Ensure multiple hosts are present with principals
- ipahost:
ipaadmin_password: SomeADMINpassword
hosts:
- name: host01.example.com
principal:
- host/testhost01.example.com
- name: host02.example.com
principal:
- host/myhost01.example.com
action: member
# Ensure host is disabled
- ipahost:
ipaadmin_password: SomeADMINpassword
@@ -466,16 +487,18 @@ EXAMPLES = """
RETURN = """
host:
description: Host dict with random password
returned: If random is yes and user did not exist or update_password is yes
returned: If random is yes and host did not exist or update_password is yes
type: dict
contains:
randompassword:
description: The generated random password
type: str
returned: If only one user is handled by the module
returned: |
If only one host is handled by the module without using hosts parameter
name:
description: The user name of the user that got a new random password
returned: If several users are handled by the module
description: The host name of the host that got a new random password
returned: |
If several hosts are handled by the module with the hosts parameter
type: dict
contains:
randompassword:
@@ -646,10 +669,10 @@ def check_parameters( # pylint: disable=unused-argument
# pylint: disable=unused-argument
def result_handler(module, result, command, name, args, errors, exit_args,
one_name):
single_host):
if "random" in args and command in ["host_add", "host_mod"] \
and "randompassword" in result["result"]:
if one_name:
if single_host:
exit_args["randompassword"] = \
result["result"]["randompassword"]
else:
@@ -671,7 +694,7 @@ def result_handler(module, result, command, name, args, errors, exit_args,
# pylint: disable=unused-argument
def exception_handler(module, ex, errors, exit_args, one_name):
def exception_handler(module, ex, errors, exit_args, single_host):
msg = str(ex)
if "already contains" in msg \
or "does not contain" in msg:
@@ -1468,7 +1491,7 @@ def main():
changed = ansible_module.execute_ipa_commands(
commands, result_handler, exception_handler,
exit_args=exit_args, one_name=len(names) == 1)
exit_args=exit_args, single_host=hosts is None)
# Done

View File

@@ -93,10 +93,12 @@ options:
action:
description: Work on netgroup or member level
required: false
type: str
default: netgroup
choices: ["member", "netgroup"]
state:
description: The state to ensure.
type: str
choices: ["present", "absent"]
default: present
author:

View File

@@ -197,7 +197,7 @@ def gen_args(module,
if maxrepeat is not None:
_args["ipapwdmaxrepeat"] = maxrepeat
if maxsequence is not None:
_args["ipapwdmaxrsequence"] = maxsequence
_args["ipapwdmaxsequence"] = maxsequence
if dictcheck is not None:
if module.ipa_check_version("<", "4.9.10"):
# Allowed values: "TRUE", "FALSE", ""
@@ -230,17 +230,15 @@ def check_supported_params(
"pwpolicy_add", "passwordgracelimit")
# If needed, report unsupported password checking paramteres
if not has_password_check:
check_password_params = [maxrepeat, maxsequence, dictcheck, usercheck]
unsupported = [
x for x in check_password_params if x is not None
]
if unsupported:
module.fail_json(
msg="Your IPA version does not support arguments: "
"maxrepeat, maxsequence, dictcheck, usercheck.")
if (
not has_password_check
and any([maxrepeat, maxsequence, dictcheck, usercheck])
):
module.fail_json(
msg="Your IPA version does not support arguments: "
"maxrepeat, maxsequence, dictcheck, usercheck.")
if gracelimit is not None and not has_gracelimit:
if not has_gracelimit and gracelimit is not None:
module.fail_json(
msg="Your IPA version does not support 'gracelimit'.")
@@ -275,7 +273,7 @@ def main():
default=None),
dictcheck=dict(type="str", aliases=["ipapwdictcheck"],
default=None),
usercheck=dict(type="str", aliases=["ipapwusercheck"],
usercheck=dict(type="str", aliases=["ipapwdusercheck"],
default=None),
gracelimit=dict(type="str", aliases=["passwordgracelimit"],
default=None),

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Authors:
# Denis Karpelevich <dkarpele@redhat.com>
# Rafael Guterres Jeffman <rjeffman@redhat.com>
# Thomas Woerner <twoerner@redhat.com>
#
@@ -45,6 +46,127 @@ options:
elements: str
required: true
aliases: ["service"]
services:
description: The list of service dicts.
type: list
elements: dict
suboptions:
name:
description: The service to manage
type: str
required: true
aliases: ["service"]
certificate:
description: Base-64 encoded service certificate.
required: false
type: list
elements: str
aliases: ["usercertificate"]
pac_type:
description: Supported PAC type.
required: false
choices: ["MS-PAC", "PAD", "NONE", ""]
type: list
elements: str
aliases: ["pac_type", "ipakrbauthzdata"]
auth_ind:
description: Defines an allow list for Authentication Indicators.
type: list
elements: str
required: false
choices: ["otp", "radius", "pkinit", "hardened", ""]
aliases: ["krbprincipalauthind"]
skip_host_check:
description: Skip checking if host object exists.
required: False
type: bool
force:
description: Force principal name even if host is not in DNS.
required: False
type: bool
requires_pre_auth:
description: Pre-authentication is required for the service.
required: false
type: bool
aliases: ["ipakrbrequirespreauth"]
ok_as_delegate:
description: Client credentials may be delegated to the service.
required: false
type: bool
aliases: ["ipakrbokasdelegate"]
ok_to_auth_as_delegate:
description: Allow service to authenticate on behalf of a client.
required: false
type: bool
aliases: ["ipakrboktoauthasdelegate"]
principal:
description: List of principal aliases for the service.
required: false
type: list
elements: str
aliases: ["krbprincipalname"]
smb:
description: Add a SMB service.
required: false
type: bool
netbiosname:
description: NETBIOS name for the SMB service.
required: false
type: str
host:
description: Host that can manage the service.
required: false
type: list
elements: str
aliases: ["managedby_host"]
allow_create_keytab_user:
description: Users allowed to create a keytab of this host.
required: false
type: list
elements: str
aliases: ["ipaallowedtoperform_write_keys_user"]
allow_create_keytab_group:
description: Groups allowed to create a keytab of this host.
required: false
type: list
elements: str
aliases: ["ipaallowedtoperform_write_keys_group"]
allow_create_keytab_host:
description: Hosts allowed to create a keytab of this host.
required: false
type: list
elements: str
aliases: ["ipaallowedtoperform_write_keys_host"]
allow_create_keytab_hostgroup:
description: Host group allowed to create a keytab of this host.
required: false
type: list
elements: str
aliases: ["ipaallowedtoperform_write_keys_hostgroup"]
allow_retrieve_keytab_user:
description: User allowed to retrieve a keytab of this host.
required: false
type: list
elements: str
aliases: ["ipaallowedtoperform_read_keys_user"]
allow_retrieve_keytab_group:
description: Groups allowed to retrieve a keytab of this host.
required: false
type: list
elements: str
aliases: ["ipaallowedtoperform_read_keys_group"]
allow_retrieve_keytab_host:
description: Hosts allowed to retrieve a keytab of this host.
required: false
type: list
elements: str
aliases: ["ipaallowedtoperform_read_keys_host"]
allow_retrieve_keytab_hostgroup:
description: Host groups allowed to retrieve a keytab of this host.
required: false
type: list
elements: str
aliases: ["ipaallowedtoperform_read_keys_hostgroup"]
certificate:
description: Base-64 encoded service certificate.
required: false
@@ -239,6 +361,15 @@ EXAMPLES = """
- host1.example.com
- host2.example.com
action: member
# Ensure multiple services are present.
- ipaservice:
ipaadmin_password: SomeADMINpassword
services:
- name: HTTP/www.example.com
host:
- host1.example.com
- name: HTTP/www.service.com
"""
RETURN = """
@@ -248,6 +379,9 @@ from ansible.module_utils.ansible_freeipa_module import \
IPAAnsibleModule, compare_args_ipa, encode_certificate, \
gen_add_del_lists, gen_add_list, gen_intersection_list, ipalib_errors, \
api_get_realm, to_text
from ansible.module_utils import six
if six.PY3:
unicode = str
def find_service(module, name):
@@ -321,8 +455,9 @@ def check_parameters(module, state, action, names):
'allow_retrieve_keytab_hostgroup']
if state == 'present':
if len(names) != 1:
module.fail_json(msg="Only one service can be added at a time.")
if names is not None and len(names) != 1:
module.fail_json(msg="Only one service can be added at a time "
"using 'name'.")
if action == 'service':
invalid = ['delete_continue']
@@ -338,9 +473,6 @@ def check_parameters(module, state, action, names):
invalid.append('delete_continue')
elif state == 'absent':
if len(names) < 1:
module.fail_json(msg="No name given.")
if action == "service":
invalid.extend(invalid_not_member)
else:
@@ -360,67 +492,85 @@ def check_parameters(module, state, action, names):
def init_ansible_module():
service_spec = dict(
# service attributesstr
certificate=dict(type="list", elements="str",
aliases=['usercertificate'],
default=None, required=False),
principal=dict(type="list", elements="str",
aliases=["krbprincipalname"], default=None),
smb=dict(type="bool", required=False),
netbiosname=dict(type="str", required=False),
pac_type=dict(type="list", elements="str",
aliases=["ipakrbauthzdata"],
choices=["MS-PAC", "PAD", "NONE", ""]),
auth_ind=dict(type="list", elements="str",
aliases=["krbprincipalauthind"],
choices=["otp", "radius", "pkinit", "hardened", ""]),
skip_host_check=dict(type="bool"),
force=dict(type="bool"),
requires_pre_auth=dict(
type="bool", aliases=["ipakrbrequirespreauth"]),
ok_as_delegate=dict(type="bool", aliases=["ipakrbokasdelegate"]),
ok_to_auth_as_delegate=dict(type="bool",
aliases=["ipakrboktoauthasdelegate"]),
host=dict(type="list", elements="str", aliases=["managedby_host"],
required=False),
allow_create_keytab_user=dict(
type="list", elements="str", required=False, no_log=False,
aliases=['ipaallowedtoperform_write_keys_user']),
allow_retrieve_keytab_user=dict(
type="list", elements="str", required=False, no_log=False,
aliases=['ipaallowedtoperform_read_keys_user']),
allow_create_keytab_group=dict(
type="list", elements="str", required=False, no_log=False,
aliases=['ipaallowedtoperform_write_keys_group']),
allow_retrieve_keytab_group=dict(
type="list", elements="str", required=False, no_log=False,
aliases=['ipaallowedtoperform_read_keys_group']),
allow_create_keytab_host=dict(
type="list", elements="str", required=False, no_log=False,
aliases=['ipaallowedtoperform_write_keys_host']),
allow_retrieve_keytab_host=dict(
type="list", elements="str", required=False, no_log=False,
aliases=['ipaallowedtoperform_read_keys_host']),
allow_create_keytab_hostgroup=dict(
type="list", elements="str", required=False, no_log=False,
aliases=['ipaallowedtoperform_write_keys_hostgroup']),
allow_retrieve_keytab_hostgroup=dict(
type="list", elements="str", required=False, no_log=False,
aliases=['ipaallowedtoperform_read_keys_hostgroup']),
delete_continue=dict(type="bool", required=False,
aliases=['continue']),
)
ansible_module = IPAAnsibleModule(
argument_spec=dict(
# general
name=dict(type="list", elements="str", aliases=["service"],
required=True),
# service attributesstr
certificate=dict(type="list", elements="str",
aliases=['usercertificate'],
default=None, required=False),
principal=dict(type="list", elements="str",
aliases=["krbprincipalname"], default=None),
smb=dict(type="bool", required=False),
netbiosname=dict(type="str", required=False),
pac_type=dict(type="list", elements="str",
aliases=["ipakrbauthzdata"],
choices=["MS-PAC", "PAD", "NONE", ""]),
auth_ind=dict(type="list", elements="str",
aliases=["krbprincipalauthind"],
choices=["otp", "radius", "pkinit", "hardened", ""]),
skip_host_check=dict(type="bool"),
force=dict(type="bool"),
requires_pre_auth=dict(
type="bool", aliases=["ipakrbrequirespreauth"]),
ok_as_delegate=dict(type="bool", aliases=["ipakrbokasdelegate"]),
ok_to_auth_as_delegate=dict(type="bool",
aliases=["ipakrboktoauthasdelegate"]),
host=dict(type="list", elements="str", aliases=["managedby_host"],
required=False),
allow_create_keytab_user=dict(
type="list", elements="str", required=False, no_log=False,
aliases=['ipaallowedtoperform_write_keys_user']),
allow_retrieve_keytab_user=dict(
type="list", elements="str", required=False, no_log=False,
aliases=['ipaallowedtoperform_read_keys_user']),
allow_create_keytab_group=dict(
type="list", elements="str", required=False, no_log=False,
aliases=['ipaallowedtoperform_write_keys_group']),
allow_retrieve_keytab_group=dict(
type="list", elements="str", required=False, no_log=False,
aliases=['ipaallowedtoperform_read_keys_group']),
allow_create_keytab_host=dict(
type="list", elements="str", required=False, no_log=False,
aliases=['ipaallowedtoperform_write_keys_host']),
allow_retrieve_keytab_host=dict(
type="list", elements="str", required=False, no_log=False,
aliases=['ipaallowedtoperform_read_keys_host']),
allow_create_keytab_hostgroup=dict(
type="list", elements="str", required=False, no_log=False,
aliases=['ipaallowedtoperform_write_keys_hostgroup']),
allow_retrieve_keytab_hostgroup=dict(
type="list", elements="str", required=False, no_log=False,
aliases=['ipaallowedtoperform_read_keys_hostgroup']),
delete_continue=dict(type="bool", required=False,
aliases=['continue']),
default=None, required=False),
services=dict(type="list",
default=None,
options=dict(
# Here name is a simple string
name=dict(type="str", required=True,
aliases=["service"]),
# Add service specific parameters
**service_spec
),
elements='dict',
required=False),
# action
action=dict(type="str", default="service",
choices=["member", "service"]),
# state
state=dict(type="str", default="present",
choices=["present", "absent", "disabled"]),
# Add service specific parameters for simple use case
**service_spec
),
mutually_exclusive=[["name", "services"]],
required_one_of=[["name", "services"]],
supports_check_mode=True,
)
@@ -436,10 +586,17 @@ def main():
# general
names = ansible_module.params_get("name")
services = ansible_module.params_get("services")
# service attributes
principal = ansible_module.params_get("principal")
certificate = ansible_module.params_get("certificate")
# Any leading or trailing whitespace is removed while adding the
# certificate with serive_add_cert. To be able to compare the results
# from service_show with the given certificates we have to remove the
# white space also.
if certificate is not None:
certificate = [cert.strip() for cert in certificate]
pac_type = ansible_module.params_get("pac_type", allow_empty_string=True)
auth_ind = ansible_module.params_get("auth_ind", allow_empty_string=True)
skip_host_check = ansible_module.params_get("skip_host_check")
@@ -462,8 +619,16 @@ def main():
state = ansible_module.params_get("state")
# check parameters
if (names is None or len(names) < 1) and \
(services is None or len(services) < 1):
ansible_module.fail_json(msg="At least one name or services is "
"required")
check_parameters(ansible_module, state, action, names)
# Use services if names is None
if services is not None:
names = services
# Init
changed = False
@@ -480,8 +645,45 @@ def main():
commands = []
keytab_members = ["user", "group", "host", "hostgroup"]
service_set = set()
for name in names:
for service in names:
if isinstance(service, dict):
name = service.get("name")
if name in service_set:
ansible_module.fail_json(
msg="service '%s' is used more than once" % name)
service_set.add(name)
principal = service.get("principal")
certificate = service.get("certificate")
# Any leading or trailing whitespace is removed while adding
# the certificate with serive_add_cert. To be able to compare
# the results from service_show with the given certificates
# we have to remove the white space also.
if certificate is not None:
certificate = [cert.strip() for cert in certificate]
pac_type = service.get("pac_type")
auth_ind = service.get("auth_ind")
skip_host_check = service.get("skip_host_check")
if skip_host_check and not has_skip_host_check:
ansible_module.fail_json(
msg="Skipping host check is not supported by your IPA "
"version")
force = service.get("force")
requires_pre_auth = service.get("requires_pre_auth")
ok_as_delegate = service.get("ok_as_delegate")
ok_to_auth_as_delegate = service.get("ok_to_auth_as_delegate")
smb = service.get("smb")
netbiosname = service.get("netbiosname")
host = service.get("host")
delete_continue = service.get("delete_continue")
elif isinstance(service, (str, unicode)):
name = service
else:
ansible_module.fail_json(msg="Service '%s' is not valid" %
repr(service))
res_find = find_service(ansible_module, name)
res_principals = []

View File

@@ -124,12 +124,12 @@ options:
required: false
type: bool
uid:
description: The UID
description: User ID Number (system will assign one if not provided)
type: int
required: false
aliases: ["uidnumber"]
gid:
description: The GID
description: Group ID Number
type: int
required: false
aliases: ["gidnumber"]
@@ -348,12 +348,12 @@ options:
required: false
type: bool
uid:
description: The UID
description: User ID Number (system will assign one if not provided)
type: int
required: false
aliases: ["uidnumber"]
gid:
description: The GID
description: Group ID Number
type: int
required: false
aliases: ["gidnumber"]
@@ -548,6 +548,17 @@ EXAMPLES = """
first: brain
last: Acme
# Create multiple users pinky and brain
- ipauser:
ipaadmin_password: SomeADMINpassword
users:
- name: pinky
first: pinky
last: Acme
- name: brain
first: brain
last: Acme
# Delete user pinky, but preserved
- ipauser:
ipaadmin_password: SomeADMINpassword
@@ -573,6 +584,14 @@ EXAMPLES = """
name: pinky,brain
state: enabled
# Remove but preserve user pinky
- ipauser:
ipaadmin_password: SomeADMINpassword
users:
- name: pinky
preserve: yes
state: absent
# Remove user pinky and brain
- ipauser:
ipaadmin_password: SomeADMINpassword
@@ -589,10 +608,12 @@ user:
randompassword:
description: The generated random password
type: str
returned: If only one user is handled by the module
returned: |
If only one user is handled by the module without using users parameter
name:
description: The user name of the user that got a new random password
returned: If several users are handled by the module
returned: |
If several users are handled by the module with the users parameter
type: dict
contains:
randompassword:
@@ -834,11 +855,11 @@ def gen_certmapdata_args(certmapdata):
# pylint: disable=unused-argument
def result_handler(module, result, command, name, args, errors, exit_args,
one_name):
single_user):
if "random" in args and command in ["user_add", "user_mod"] \
and "randompassword" in result["result"]:
if one_name:
if single_user:
exit_args["randompassword"] = \
result["result"]["randompassword"]
else:
@@ -861,7 +882,7 @@ def result_handler(module, result, command, name, args, errors, exit_args,
# pylint: disable=unused-argument
def exception_handler(module, ex, errors, exit_args, one_name):
def exception_handler(module, ex, errors, exit_args, single_user):
msg = str(ex)
if "already contains" in msg \
or "does not contain" in msg:
@@ -1511,7 +1532,7 @@ def main():
changed = ansible_module.execute_ipa_commands(
commands, result_handler, exception_handler,
exit_args=exit_args, one_name=len(names) == 1)
exit_args=exit_args, single_user=users is None)
# Done
ansible_module.exit_json(changed=changed, user=exit_args)

View File

@@ -183,6 +183,7 @@ Variable | Description | Required
`ipaclient_no_ssh` | The bool value defines if OpenSSH client will be configured. `ipaclient_no_ssh` defaults to `no`. | no
`ipaclient_no_sshd` | The bool value defines if OpenSSH server will be configured. `ipaclient_no_sshd` defaults to `no`. | no
`ipaclient_no_sudo` | The bool value defines if SSSD will be configured as a data source for sudo. `ipaclient_no_sudo` defaults to `no`. | no
`ipaclient_subid` | The bool value defines if SSSD will be configured as a data source for subid. `ipaclient_subid` defaults to `no`. | no
`ipaclient_no_dns_sshfp` | The bool value defines if DNS SSHFP records will not be created automatically. `ipaclient_no_dns_sshfp` defaults to `no`. | no
`ipaclient_force` | The bool value defines if settings will be forced even in the error case. `ipaclient_force` defaults to `no`. | no
`ipaclient_force_ntpd` | The bool value defines if ntpd usage will be forced. This is not supported anymore and leads to a warning. `ipaclient_force_ntpd` defaults to `no`. | no

View File

@@ -13,6 +13,7 @@ ipaclient_ssh_trust_dns: no
ipaclient_no_ssh: no
ipaclient_no_sshd: no
ipaclient_no_sudo: no
ipaclient_subid: no
ipaclient_no_dns_sshfp: no
ipaclient_force: no
ipaclient_force_ntpd: no

View File

@@ -55,6 +55,10 @@ options:
type: bool
required: no
default: no
krb_name:
description: The krb5 config file name
type: str
required: yes
author:
- Thomas Woerner (@t-woerner)
'''
@@ -65,6 +69,7 @@ EXAMPLES = '''
servers: ["server1.example.com","server2.example.com"]
domain: example.com
hostname: client1.example.com
krb_name: /tmp/tmpkrb5.conf
register: result_ipaclient_api
'''
@@ -99,6 +104,7 @@ def main():
realm=dict(required=True, type='str'),
hostname=dict(required=True, type='str'),
debug=dict(required=False, type='bool', default="false"),
krb_name=dict(required=True, type='str'),
),
supports_check_mode=False,
)
@@ -110,9 +116,11 @@ def main():
realm = module.params.get('realm')
hostname = module.params.get('hostname')
debug = module.params.get('debug')
krb_name = module.params.get('krb_name')
host_principal = 'host/%s@%s' % (hostname, realm)
os.environ['KRB5CCNAME'] = paths.IPA_DNS_CCACHE
os.environ['KRB5_CONFIG'] = krb_name
ca_certs = x509.load_certificate_list_from_file(paths.IPA_CA_CRT)
if 40500 <= NUM_VERSION < 40590:
@@ -228,7 +236,8 @@ def main():
except Exception as e:
logger.debug("config_show failed %s", e, exc_info=True)
module.fail_json(
"Failed to retrieve CA certificate subject base: {}".format(e),
"Failed to retrieve CA certificate subject base: "
"{0}".format(e),
rval=CLIENT_INSTALL_ERROR)
else:
subject_base = str(DN(config['ipacertificatesubjectbase'][0]))

View File

@@ -266,10 +266,8 @@ def unconfigure_dns_resolver(fstore=None):
def main():
module = AnsibleModule(
argument_spec=dict(
nameservers=dict(type="list", elements="str", aliases=["cn"],
required=False),
searchdomains=dict(type="list", elements="str", aliases=["cn"],
required=False),
nameservers=dict(type="list", elements="str", required=False),
searchdomains=dict(type="list", elements="str", required=False),
state=dict(type="str", default="present",
choices=["present", "absent"]),
),

View File

@@ -54,6 +54,10 @@ options:
the host entry will not be changed on the server
type: bool
required: yes
krb_name:
description: The krb5 config file name
type: str
required: yes
author:
- Thomas Woerner (@t-woerner)
'''
@@ -65,6 +69,7 @@ EXAMPLES = '''
realm: EXAMPLE.COM
basedn: dc=example,dc=com
allow_repair: yes
krb_name: /tmp/tmpkrb5.conf
'''
RETURN = '''
@@ -87,6 +92,7 @@ def main():
realm=dict(required=True, type='str'),
basedn=dict(required=True, type='str'),
allow_repair=dict(required=True, type='bool'),
krb_name=dict(required=True, type='str'),
),
)
@@ -98,6 +104,8 @@ def main():
realm = module.params.get('realm')
basedn = module.params.get('basedn')
allow_repair = module.params.get('allow_repair')
krb_name = module.params.get('krb_name')
os.environ['KRB5_CONFIG'] = krb_name
env = {'PATH': SECURE_PATH}
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)

View File

@@ -46,10 +46,6 @@ options:
type: list
elements: str
required: yes
domain:
description: Primary DNS domain of the IPA deployment
type: str
required: yes
realm:
description: Kerberos realm name of the IPA deployment
type: str
@@ -58,10 +54,6 @@ options:
description: Fully qualified name of this host
type: str
required: yes
kdc:
description: The name or address of the host running the KDC
type: str
required: yes
basedn:
description: The basedn of the IPA server (of the form dc=example,dc=com)
type: str
@@ -102,6 +94,10 @@ options:
description: Turn on extra debugging
type: bool
required: no
krb_name:
description: The krb5 config file name
type: str
required: yes
author:
- Thomas Woerner (@t-woerner)
'''
@@ -111,27 +107,25 @@ EXAMPLES = '''
- name: Join IPA in force mode with maximum 5 kinit attempts
ipaclient_join:
servers: ["server1.example.com","server2.example.com"]
domain: example.com
realm: EXAMPLE.COM
kdc: server1.example.com
basedn: dc=example,dc=com
hostname: client1.example.com
principal: admin
password: MySecretPassword
force_join: yes
kinit_attempts: 5
krb_name: /tmp/tmpkrb5.conf
# Join IPA to get the keytab using ipadiscovery return values
- name: Join IPA
ipaclient_join:
servers: "{{ ipadiscovery.servers }}"
domain: "{{ ipadiscovery.domain }}"
realm: "{{ ipadiscovery.realm }}"
kdc: "{{ ipadiscovery.kdc }}"
basedn: "{{ ipadiscovery.basedn }}"
hostname: "{{ ipadiscovery.hostname }}"
principal: admin
password: MySecretPassword
krb_name: /tmp/tmpkrb5.conf
'''
RETURN = '''
@@ -147,9 +141,9 @@ import tempfile
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.ansible_ipa_client import (
setup_logging, check_imports,
SECURE_PATH, sysrestore, paths, options, configure_krb5_conf,
realm_to_suffix, kinit_keytab, GSSError, kinit_password, NUM_VERSION,
get_ca_cert, get_ca_certs, errors, run
SECURE_PATH, sysrestore, paths, options, realm_to_suffix, kinit_keytab,
GSSError, kinit_password, NUM_VERSION, get_ca_cert, get_ca_certs, errors,
run
)
@@ -157,10 +151,8 @@ def main():
module = AnsibleModule(
argument_spec=dict(
servers=dict(required=True, type='list', elements='str'),
domain=dict(required=True, type='str'),
realm=dict(required=True, type='str'),
hostname=dict(required=True, type='str'),
kdc=dict(required=True, type='str'),
basedn=dict(required=True, type='str'),
principal=dict(required=False, type='str'),
password=dict(required=False, type='str', no_log=True),
@@ -170,6 +162,7 @@ def main():
force_join=dict(required=False, type='bool'),
kinit_attempts=dict(required=False, type='int', default=5),
debug=dict(required=False, type='bool'),
krb_name=dict(required=True, type='str'),
),
supports_check_mode=False,
)
@@ -179,11 +172,9 @@ def main():
setup_logging()
servers = module.params.get('servers')
domain = module.params.get('domain')
realm = module.params.get('realm')
hostname = module.params.get('hostname')
basedn = module.params.get('basedn')
kdc = module.params.get('kdc')
force_join = module.params.get('force_join')
principal = module.params.get('principal')
password = module.params.get('password')
@@ -192,6 +183,7 @@ def main():
ca_cert_file = module.params.get('ca_cert_file')
kinit_attempts = module.params.get('kinit_attempts')
debug = module.params.get('debug')
krb_name = module.params.get('krb_name')
if password is not None and keytab is not None:
module.fail_json(msg="Password and keytab cannot be used together")
@@ -199,12 +191,10 @@ def main():
if password is None and admin_keytab is None:
module.fail_json(msg="Password or admin_keytab is needed")
client_domain = hostname[hostname.find(".") + 1:]
nolog = tuple()
env = {'PATH': SECURE_PATH}
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
host_principal = 'host/%s@%s' % (hostname, realm)
sssd = True
options.ca_cert_file = ca_cert_file
options.principal = principal
@@ -215,19 +205,6 @@ def main():
changed = False
already_joined = False
try:
(krb_fd, krb_name) = tempfile.mkstemp()
os.close(krb_fd)
configure_krb5_conf(
cli_realm=realm,
cli_domain=domain,
cli_server=servers,
cli_kdc=kdc,
dnsok=False,
filename=krb_name,
client_domain=client_domain,
client_hostname=hostname,
configure_sssd=sssd,
force=False)
env['KRB5_CONFIG'] = krb_name
ccache_dir = tempfile.mkdtemp(prefix='krbcc')
ccache_name = os.path.join(ccache_dir, 'ccache')
@@ -264,7 +241,7 @@ def main():
config=krb_name)
except RuntimeError as e:
module.fail_json(
msg="Kerberos authentication failed: {}".format(e))
msg="Kerberos authentication failed: {0}".format(e))
elif keytab:
join_args.append("-f")
@@ -277,10 +254,10 @@ def main():
attempts=kinit_attempts)
except GSSError as e:
module.fail_json(
msg="Kerberos authentication failed: {}".format(e))
msg="Kerberos authentication failed: {0}".format(e))
else:
module.fail_json(
msg="Keytab file could not be found: {}".format(keytab))
msg="Keytab file could not be found: {0}".format(keytab))
elif password:
join_args.append("-w")
@@ -336,27 +313,17 @@ def main():
paths.IPA_DNS_CCACHE,
config=krb_name,
attempts=kinit_attempts)
env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] = paths.IPA_DNS_CCACHE
except GSSError as e:
# failure to get ticket makes it impossible to login and
# bind from sssd to LDAP, abort installation
module.fail_json(msg="Failed to obtain host TGT: %s" % e)
finally:
try:
os.remove(krb_name)
except OSError:
module.fail_json(msg="Could not remove %s" % krb_name)
if ccache_dir is not None:
try:
os.rmdir(ccache_dir)
except OSError:
pass
if os.path.exists(krb_name + ".ipabkp"):
try:
os.remove(krb_name + ".ipabkp")
except OSError:
module.fail_json(msg="Could not remove %s.ipabkp" % krb_name)
module.exit_json(changed=changed,
already_joined=already_joined)

View File

@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
# Authors:
# Thomas Woerner <twoerner@redhat.com>
#
# Based on ipa-client-install code
#
# Copyright (C) 2017-2022 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.0',
'supported_by': 'community',
'status': ['preview'],
}
DOCUMENTATION = '''
---
module: ipaclient_setup_certmonger
short_description: Setup certmonger for IPA client
description: Setup certmonger for IPA client
options:
realm:
description: Kerberos realm name of the IPA deployment
type: str
required: yes
hostname:
description: Fully qualified name of this host
type: str
required: yes
subject_base:
description: |
The certificate subject base (default O=<realm-name>).
RDNs are in LDAP order (most specific RDN first).
type: str
required: yes
ca_enabled:
description: Whether the Certificate Authority is enabled or not
type: bool
required: yes
request_cert:
description: Request certificate for the machine
type: bool
required: yes
author:
- Thomas Woerner (@t-woerner)
'''
EXAMPLES = '''
- name: Setup certmonger for IPA client
ipaclient_setup_certmonger:
realm: EXAMPLE.COM
hostname: client1.example.com
subject_base: O=EXAMPLE.COM
ca_enabled: true
request_cert: false
'''
RETURN = '''
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.ansible_ipa_client import (
setup_logging, check_imports,
options, sysrestore, paths, ScriptError, configure_certmonger
)
def main():
module = AnsibleModule(
argument_spec=dict(
realm=dict(required=True, type='str'),
hostname=dict(required=True, type='str'),
subject_base=dict(required=True, type='str'),
ca_enabled=dict(required=True, type='bool'),
request_cert=dict(required=True, type='bool'),
),
supports_check_mode=False,
)
module._ansible_debug = True
check_imports(module)
setup_logging()
cli_realm = module.params.get('realm')
hostname = module.params.get('hostname')
subject_base = module.params.get('subject_base')
ca_enabled = module.params.get('ca_enabled')
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
options.request_cert = module.params.get('request_cert')
options.hostname = hostname
try:
configure_certmonger(fstore, subject_base, cli_realm, hostname,
options, ca_enabled)
except ScriptError as e:
module.fail_json(msg=str(e))
module.exit_json(changed=True)
if __name__ == '__main__':
main()

View File

@@ -125,6 +125,10 @@ options:
description: Do not configure SSSD as data source for sudo
type: bool
required: no
subid:
description: Configure SSSD as data source for subid
type: bool
required: no
fixed_primary:
description: Configure sssd to use fixed server as primary IPA server
type: bool
@@ -148,6 +152,10 @@ options:
The dist of nss_ldap or nss-pam-ldapd files if sssd is disabled
required: yes
type: dict
krb_name:
description: The krb5 config file name
type: str
required: yes
author:
- Thomas Woerner (@t-woerner)
'''
@@ -163,6 +171,7 @@ EXAMPLES = '''
subject_base: O=EXAMPLE.COM
principal: admin
ca_enabled: yes
krb_name: /tmp/tmpkrb5.conf
'''
RETURN = '''
@@ -177,7 +186,7 @@ from ansible.module_utils.ansible_ipa_client import (
options, sysrestore, paths, ansible_module_get_parsed_ip_addresses,
api, errors, create_ipa_nssdb, ipautil, ScriptError, CLIENT_INSTALL_ERROR,
get_certs_from_ldap, DN, certstore, x509, logger, certdb,
CalledProcessError, tasks, client_dns, configure_certmonger, services,
CalledProcessError, tasks, client_dns, services,
update_ssh_keys, save_state, configure_ldap_conf, configure_nslcd_conf,
configure_openldap_conf, hardcode_ldap_server, getargspec, NUM_VERSION,
serialization
@@ -208,11 +217,13 @@ def main():
no_ssh=dict(required=False, type='bool'),
no_sshd=dict(required=False, type='bool'),
no_sudo=dict(required=False, type='bool'),
subid=dict(required=False, type='bool'),
fixed_primary=dict(required=False, type='bool'),
permit=dict(required=False, type='bool'),
no_krb5_offline_passwords=dict(required=False, type='bool'),
no_dns_sshfp=dict(required=False, type='bool', default=False),
nosssd_files=dict(required=True, type='dict'),
krb_name=dict(required=True, type='str'),
),
supports_check_mode=False,
)
@@ -251,6 +262,7 @@ def main():
options.conf_sshd = not options.no_sshd
options.no_sudo = module.params.get('no_sudo')
options.conf_sudo = not options.no_sudo
options.subid = module.params.get('subid')
options.primary = module.params.get('fixed_primary')
options.permit = module.params.get('permit')
options.no_krb5_offline_passwords = module.params.get(
@@ -262,6 +274,8 @@ def main():
options.sssd = not options.no_sssd
options.no_ac = False
nosssd_files = module.params.get('nosssd_files')
krb_name = module.params.get('krb_name')
os.environ['KRB5_CONFIG'] = krb_name
# pylint: disable=invalid-name
CCACHE_FILE = paths.IPA_DNS_CCACHE
@@ -350,8 +364,6 @@ def main():
if not options.on_master:
client_dns(cli_server[0], hostname, options)
configure_certmonger(fstore, subject_base, cli_realm, hostname,
options, ca_enabled)
if hasattr(paths, "SSH_CONFIG_DIR"):
ssh_config_dir = paths.SSH_CONFIG_DIR
@@ -430,19 +442,17 @@ def main():
# Modify nsswitch/pam stack
# pylint: disable=deprecated-method
argspec = getargspec(tasks.modify_nsswitch_pam_stack)
the_options = {
"sssd": options.sssd,
"mkhomedir": options.mkhomedir,
"statestore": statestore,
}
if "sudo" in argspec.args:
tasks.modify_nsswitch_pam_stack(
sssd=options.sssd,
mkhomedir=options.mkhomedir,
statestore=statestore,
sudo=options.conf_sudo
)
else:
tasks.modify_nsswitch_pam_stack(
sssd=options.sssd,
mkhomedir=options.mkhomedir,
statestore=statestore
)
the_options["sudo"] = options.conf_sudo
if "subid" in argspec.args:
the_options["subid"] = options.subid
tasks.modify_nsswitch_pam_stack(**the_options)
if hasattr(paths, "AUTHSELECT") and paths.AUTHSELECT is not None:
# authselect is used

View File

@@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
# Authors:
# Thomas Woerner <twoerner@redhat.com>
#
# Based on ipa-client-install code
#
# Copyright (C) 2017-2022 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.0',
'supported_by': 'community',
'status': ['preview'],
}
DOCUMENTATION = '''
---
module: ipaclient_temp_krb5
short_description:
Create temporary krb5 configuration.
description:
Create temporary krb5 configuration for deferring the creation of the final
krb5.conf on clients
options:
servers:
description: Fully qualified name of IPA servers to enroll to
type: list
elements: str
required: yes
domain:
description: Primary DNS domain of the IPA deployment
type: str
required: yes
realm:
description: Kerberos realm name of the IPA deployment
type: str
required: yes
hostname:
description: Fully qualified name of this host
type: str
required: yes
kdc:
description: The name or address of the host running the KDC
type: str
required: yes
on_master:
description: Whether the configuration is done on the master or not
type: bool
required: no
default: no
author:
- Thomas Woerner (@t-woerner)
'''
EXAMPLES = '''
# Test IPA with local keytab
- name: Test IPA in force mode with maximum 5 kinit attempts
ipaclient_test_keytab:
servers: ["server1.example.com","server2.example.com"]
domain: example.com
realm: EXAMPLE.COM
kdc: server1.example.com
hostname: client1.example.com
# Test IPA with ipadiscovery return values
- name: Join IPA
ipaclient_test_keytab:
servers: "{{ ipadiscovery.servers }}"
domain: "{{ ipadiscovery.domain }}"
realm: "{{ ipadiscovery.realm }}"
kdc: "{{ ipadiscovery.kdc }}"
hostname: "{{ ipadiscovery.hostname }}"
'''
RETURN = '''
krb_name:
description: The krb5 config file name
returned: always
type: str
'''
import os
import tempfile
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.ansible_ipa_client import (
setup_logging, check_imports, configure_krb5_conf
)
def main():
module = AnsibleModule(
argument_spec=dict(
servers=dict(required=True, type='list', elements='str'),
domain=dict(required=True, type='str'),
realm=dict(required=True, type='str'),
hostname=dict(required=True, type='str'),
kdc=dict(required=True, type='str'),
on_master=dict(required=False, type='bool', default=False),
),
supports_check_mode=False,
)
module._ansible_debug = True
check_imports(module)
setup_logging()
servers = module.params.get('servers')
domain = module.params.get('domain')
realm = module.params.get('realm')
hostname = module.params.get('hostname')
kdc = module.params.get('kdc')
client_domain = hostname[hostname.find(".") + 1:]
krb_name = None
# Create temporary krb5 configuration
try:
(krb_fd, krb_name) = tempfile.mkstemp()
os.close(krb_fd)
configure_krb5_conf(
cli_realm=realm,
cli_domain=domain,
cli_server=servers,
cli_kdc=kdc,
dnsok=False,
filename=krb_name,
client_domain=client_domain,
client_hostname=hostname,
configure_sssd=True,
force=False)
except Exception as ex:
if krb_name:
try:
os.remove(krb_name)
except OSError:
module.fail_json(msg="Could not remove %s" % krb_name)
module.fail_json(
msg="Failed to create temporary krb5 configuration: %s" % str(ex))
module.exit_json(changed=False,
krb_name=krb_name)
if __name__ == '__main__':
main()

View File

@@ -432,7 +432,7 @@ def main():
if options.ca_cert_files is not None:
for value in options.ca_cert_files:
if not isinstance(value, list):
raise ValueError("Expected list, got {!r}".format(value))
raise ValueError("Expected list, got {0!r}".format(value))
# this is what init() does
value = value[-1]
if not os.path.exists(value):
@@ -575,13 +575,13 @@ def main():
hostname_source = "Machine's FQDN"
if hostname != hostname.lower():
raise ScriptError(
"Invalid hostname '{}', must be lower-case.".format(hostname),
"Invalid hostname '{0}', must be lower-case.".format(hostname),
rval=CLIENT_INSTALL_ERROR
)
if hostname in ('localhost', 'localhost.localdomain'):
raise ScriptError(
"Invalid hostname, '{}' must not be used.".format(hostname),
"Invalid hostname, '{0}' must not be used.".format(hostname),
rval=CLIENT_INSTALL_ERROR)
if hasattr(constants, "MAXHOSTNAMELEN"):
@@ -589,7 +589,7 @@ def main():
validate_hostname(hostname, maxlen=constants.MAXHOSTNAMELEN)
except ValueError as e:
raise ScriptError(
'invalid hostname: {}'.format(e),
'invalid hostname: {0}'.format(e),
rval=CLIENT_INSTALL_ERROR)
if hasattr(tasks, "is_nosssd_supported"):
@@ -695,7 +695,7 @@ def main():
rval=CLIENT_INSTALL_ERROR)
if ret == ipadiscovery.NOT_FQDN:
raise ScriptError(
"{} is not a fully-qualified hostname".format(hostname),
"{0} is not a fully-qualified hostname".format(hostname),
rval=CLIENT_INSTALL_ERROR)
if ret in (ipadiscovery.NO_LDAP_SERVER, ipadiscovery.NOT_IPA_SERVER) \
or not ds.domain:

View File

@@ -159,11 +159,29 @@ def main():
ca_crt_exists = os.path.exists(paths.IPA_CA_CRT)
env = {'PATH': SECURE_PATH, 'KRB5CCNAME': paths.IPA_DNS_CCACHE}
# First try: Validate krb5 keytab with system krb5 configuraiton
# First try: Validate with temporary test krb5.conf that forces
# 1) no DNS lookups and
# 2) to load /etc/krb5.conf:
#
# [libdefaults]
# dns_lookup_realm = false
# dns_lookup_kdc = false
# include /etc/krb5.conf
#
try:
(krb_fd, krb_name) = tempfile.mkstemp()
os.close(krb_fd)
content = "\n".join([
"[libdefaults]",
"dns_lookup_realm = false",
"dns_lookup_kdc = false",
"include /etc/krb5.conf"
])
with open(krb_name, "w") as outf:
outf.write(content)
kinit_keytab(host_principal, paths.KRB5_KEYTAB,
paths.IPA_DNS_CCACHE,
config=paths.KRB5_CONF,
config=krb_name,
attempts=kinit_attempts)
krb5_keytab_ok = True
krb5_conf_ok = True
@@ -177,6 +195,11 @@ def main():
pass
except GSSError:
pass
finally:
try:
os.remove(krb_name)
except OSError:
module.fail_json(msg="Could not remove %s" % krb_name)
# Second try: Validate krb5 keytab with temporary krb5
# configuration
@@ -221,6 +244,12 @@ def main():
os.remove(krb_name)
except OSError:
module.fail_json(msg="Could not remove %s" % krb_name)
if os.path.exists(krb_name + ".ipabkp"):
try:
os.remove(krb_name + ".ipabkp")
except OSError:
module.fail_json(
msg="Could not remove %s.ipabkp" % krb_name)
module.exit_json(changed=False,
krb5_keytab_ok=krb5_keytab_ok,

View File

@@ -239,12 +239,19 @@
hostname: "{{ result_ipaclient_test.hostname }}"
when: not ipaclient_on_master | bool
- name: Install - Join IPA
ipaclient_join:
- name: Install - Create temporary krb5 configuration
ipaclient_temp_krb5:
servers: "{{ result_ipaclient_test.servers }}"
domain: "{{ result_ipaclient_test.domain }}"
realm: "{{ result_ipaclient_test.realm }}"
hostname: "{{ result_ipaclient_test.hostname }}"
kdc: "{{ result_ipaclient_test.kdc }}"
register: result_ipaclient_temp_krb5
- name: Install - Join IPA
ipaclient_join:
servers: "{{ result_ipaclient_test.servers }}"
realm: "{{ result_ipaclient_test.realm }}"
basedn: "{{ result_ipaclient_test.basedn }}"
hostname: "{{ result_ipaclient_test.hostname }}"
force_join: "{{ ipaclient_force_join | default(omit) }}"
@@ -255,6 +262,7 @@
admin_keytab: "{{ ipaadmin_keytab if ipaadmin_keytab is defined and not ipaclient_use_otp | bool else omit }}"
# ca_cert_file: "{{ ipaclient_ca_cert_file | default(omit) }}"
kinit_attempts: "{{ ipaclient_kinit_attempts | default(omit) }}"
krb_name: "{{ result_ipaclient_temp_krb5.krb_name }}"
register: result_ipaclient_join
when: not ipaclient_on_master | bool and
(not result_ipaclient_test_keytab.krb5_keytab_ok or
@@ -323,26 +331,13 @@
"{{ ipassd_no_krb5_offline_passwords
| default(ipasssd_no_krb5_offline_passwords) }}"
- name: Install - Configure krb5 for IPA realm
ipaclient_setup_krb5:
realm: "{{ result_ipaclient_test.realm }}"
domain: "{{ result_ipaclient_test.domain }}"
servers: "{{ result_ipaclient_test.servers }}"
kdc: "{{ result_ipaclient_test.kdc }}"
dnsok: "{{ result_ipaclient_test.dnsok }}"
client_domain: "{{ result_ipaclient_test.client_domain }}"
hostname: "{{ result_ipaclient_test.hostname }}"
sssd: "{{ result_ipaclient_test.sssd }}"
force: "{{ ipaclient_force }}"
# on_master: "{{ ipaclient_on_master }}"
when: not ipaclient_on_master | bool
- name: Install - IPA API calls for remaining enrollment parts
ipaclient_api:
servers: "{{ result_ipaclient_test.servers }}"
realm: "{{ result_ipaclient_test.realm }}"
hostname: "{{ result_ipaclient_test.hostname }}"
# debug: yes
krb_name: "{{ result_ipaclient_temp_krb5.krb_name }}"
register: result_ipaclient_api
- name: Install - Fix IPA ca
@@ -351,6 +346,7 @@
realm: "{{ result_ipaclient_test.realm }}"
basedn: "{{ result_ipaclient_test.basedn }}"
allow_repair: "{{ ipaclient_allow_repair }}"
krb_name: "{{ result_ipaclient_temp_krb5.krb_name }}"
when: not ipaclient_on_master | bool and
result_ipaclient_test_keytab.krb5_keytab_ok and
not result_ipaclient_test_keytab.ca_crt_exists
@@ -378,6 +374,7 @@
no_ssh: "{{ ipaclient_no_ssh }}"
no_sshd: "{{ ipaclient_no_sshd }}"
no_sudo: "{{ ipaclient_no_sudo }}"
subid: "{{ ipaclient_subid }}"
fixed_primary: "{{ ipassd_fixed_primary
| default(ipasssd_fixed_primary) }}"
permit: "{{ ipassd_permit | default(ipasssd_permit) }}"
@@ -386,6 +383,7 @@
| default(ipasssd_no_krb5_offline_passwords) }}"
no_dns_sshfp: "{{ ipaclient_no_dns_sshfp }}"
nosssd_files: "{{ result_ipaclient_test.nosssd_files }}"
krb_name: "{{ result_ipaclient_temp_krb5.krb_name }}"
- name: Install - Configure SSH and SSHD
ipaclient_setup_ssh:
@@ -412,6 +410,36 @@
domain: "{{ result_ipaclient_test.domain }}"
nisdomain: "{{ ipaclient_nisdomain | default(omit) }}"
when: not ipaclient_no_nisdomain | bool
- name: Remove temporary krb5.conf
ansible.builtin.file:
path: "{{ result_ipaclient_temp_krb5.krb_name }}"
state: absent
when: result_ipaclient_temp_krb5.krb_name is defined
- name: Install - Configure krb5 for IPA realm
ipaclient_setup_krb5:
realm: "{{ result_ipaclient_test.realm }}"
domain: "{{ result_ipaclient_test.domain }}"
servers: "{{ result_ipaclient_test.servers }}"
kdc: "{{ result_ipaclient_test.kdc }}"
dnsok: "{{ result_ipaclient_test.dnsok }}"
client_domain: "{{ result_ipaclient_test.client_domain }}"
hostname: "{{ result_ipaclient_test.hostname }}"
sssd: "{{ result_ipaclient_test.sssd }}"
force: "{{ ipaclient_force }}"
# on_master: "{{ ipaclient_on_master }}"
when: not ipaclient_on_master | bool
- name: Install - Configure certmonger
ipaclient_setup_certmonger:
realm: "{{ result_ipaclient_test.realm }}"
hostname: "{{ result_ipaclient_test.hostname }}"
subject_base: "{{ result_ipaclient_api.subject_base }}"
ca_enabled: "{{ result_ipaclient_api.ca_enabled }}"
request_cert: "{{ ipaclient_request_cert }}"
when: not ipaclient_on_master | bool
always:
- name: Install - Restore original admin password if overwritten by OTP
no_log: yes
@@ -423,3 +451,15 @@
ansible.builtin.file:
path: "/etc/ipa/.dns_ccache"
state: absent
- name: Remove temporary krb5.conf
ansible.builtin.file:
path: "{{ result_ipaclient_temp_krb5.krb_name }}"
state: absent
when: result_ipaclient_temp_krb5.krb_name is defined
- name: Remove temporary krb5.conf backup
ansible.builtin.file:
path: "{{ result_ipaclient_temp_krb5.krb_name }}.ipabkp"
state: absent
when: result_ipaclient_temp_krb5.krb_name is defined

View File

@@ -114,6 +114,50 @@ Example playbook to setup the IPA client(s) using principal and password from in
state: present
```
Example inventory file to remove a replica from the domain:
```ini
[ipareplicas]
ipareplica1.example.com
[ipareplicas:vars]
ipaadmin_password=MySecretPassword123
ipareplica_remove_from_domain=true
```
Example playbook to remove an IPA replica using admin passwords from the domain:
```yaml
---
- name: Playbook to remove IPA replica
hosts: ipareplica
become: true
roles:
- role: ipareplica
state: absent
```
The inventory will enable the removal of the replica (also a replica) from the domain. Additional options are needed if the removal of the replica is resulting in a topology disconnect or if the replica is the last that has a role.
To continue with the removal with a topology disconnect it is needed to set these parameters:
```ini
ipareplica_ignore_topology_disconnect=true
ipareplica_remove_on_server=ipareplica2.example.com
```
To continue with the removal for a replica that is the last that has a role:
```ini
ipareplica_ignore_last_of_role=true
```
Be careful with enabling the `ipareplica_ignore_topology_disconnect` and especially `ipareplica_ignore_last_of_role`, the change can not be reverted easily.
The parameters `ipaserver_ignore_topology_disconnect`, `ipaserver_ignore_last_of_role`, `ipaserver_remove_on_server` and `ipaserver_remove_from_domain` can be used instead.
Playbooks
=========
@@ -200,6 +244,7 @@ Variable | Description | Required
`ipaclient_no_ssh` | The bool value defines if OpenSSH client will be configured. (bool, default: false) | no
`ipaclient_no_sshd` | The bool value defines if OpenSSH server will be configured. (bool, default: false) | no
`ipaclient_no_sudo` | The bool value defines if SSSD will be configured as a data source for sudo. (bool, default: false) | no
`ipaclient_subid` | The bool value defines if SSSD will be configured as a data source for subid. (bool, default: false) | no
`ipaclient_no_dns_sshfp` | The bool value defines if DNS SSHFP records will not be created automatically. (bool, default: false) | no
Certificate system Variables
@@ -254,6 +299,19 @@ Variable | Description | Required
`ipareplica_setup_firewalld` | The value defines if the needed services will automatically be openen in the firewall managed by firewalld. (bool, default: true) | no
`ipareplica_firewalld_zone` | The value defines the firewall zone that will be used. This needs to be an existing runtime and permanent zone. (string) | no
Undeploy Variables (`state`: absent)
------------------------------------
These settings should only be used if the result is really wanted. The change might not be revertable easily.
Variable | Description | Required
-------- | ----------- | --------
`ipareplica_ignore_topology_disconnect` \| `ipaserver_ignore_topology_disconnect` | If enabled this enforces the removal of the replica even if it results in a topology disconnect. Be careful with this setting. (bool) | false
`ipareplica_ignore_last_of_role` \| `ipaserver_ignore_last_of_role` | If enabled this enforces the removal of the replica even if the replica is the last with one that has a role. Be careful, this might not be revered easily. (bool) | false
`ipareplica_remove_from_domain` \| `ipaserver_remove_from_domain` | This enables the removal of the replica from the domain additionally to the undeployment. (bool) | false
`ipareplica_remove_on_server` \| `ipaserver_remove_on_server` | The value defines the replica in the domain that will to be used to remove the replica from the domain if `ipareplica_ignore_topology_disconnect` and `ipareplica_remove_from_domain` are enabled. Without the need to enable `ipareplica_ignore_topology_disconnect`, the value will be automatically detected using the replication agreements of the replica. (string) | false
Authors
=======

View File

@@ -171,7 +171,7 @@ def main():
# Print a warning if CA role is only installed on one server
if len(ca_servers) == 1:
msg = u'''
WARNING: The CA service is only installed on one server ({}).
WARNING: The CA service is only installed on one server ({0}).
It is strongly recommended to install it on another server.
Run ipa-ca-install(1) on another master to accomplish this.
'''.format(ca_servers[0])

View File

@@ -469,7 +469,7 @@ def main():
env._finalize_core(**dict(constants.DEFAULT_CONFIG))
# pylint: disable=no-member
xmlrpc_uri = 'https://{}/ipa/xml'.format(ipautil.format_netloc(env.host))
xmlrpc_uri = 'https://{0}/ipa/xml'.format(ipautil.format_netloc(env.host))
if hasattr(ipaldap, "realm_to_ldapi_uri"):
realm_to_ldapi_uri = ipaldap.realm_to_ldapi_uri
else:
@@ -609,7 +609,7 @@ def main():
ansible_log.debug("-- REMOTE_API --")
ldapuri = 'ldaps://%s' % ipautil.format_netloc(config.master_host_name)
xmlrpc_uri = 'https://{}/ipa/xml'.format(
xmlrpc_uri = 'https://{0}/ipa/xml'.format(
ipautil.format_netloc(config.master_host_name))
remote_api = create_api(mode=None)
remote_api.bootstrap(in_server=True,

View File

@@ -450,7 +450,7 @@ def main():
if installer.ca_cert_files is not None:
if not isinstance(installer.ca_cert_files, list):
ansible_module.fail_json(
msg="Expected list, got {!r}".format(installer.ca_cert_files))
msg="Expected list, got {0!r}".format(installer.ca_cert_files))
for cert in installer.ca_cert_files:
if not os.path.exists(cert):
ansible_module.fail_json(msg="'%s' does not exist" % cert)
@@ -521,6 +521,11 @@ def main():
ansible_module.fail_json(
msg="NTP configuration cannot be updated during promotion")
# host_name an domain_name must be different at this point.
if options.host_name.lower() == options.domain_name.lower():
ansible_module.fail_json(
msg="hostname cannot be the same as the domain name")
# done #
ansible_module.exit_json(

View File

@@ -334,7 +334,7 @@ def gen_env_boostrap_finalize_core(etc_ipa, default_config):
def api_bootstrap_finalize(env):
# pylint: disable=no-member
xmlrpc_uri = \
'https://{}/ipa/xml'.format(ipautil.format_netloc(env.host))
'https://{0}/ipa/xml'.format(ipautil.format_netloc(env.host))
api.bootstrap(in_server=True,
context='installer',
confdir=paths.ETC_IPA,
@@ -479,7 +479,7 @@ def ansible_module_get_parsed_ip_addresses(ansible_module,
def gen_remote_api(master_host_name, etc_ipa):
ldapuri = 'ldaps://%s' % ipautil.format_netloc(master_host_name)
xmlrpc_uri = 'https://{}/ipa/xml'.format(
xmlrpc_uri = 'https://{0}/ipa/xml'.format(
ipautil.format_netloc(master_host_name))
remote_api = create_api(mode=None)
remote_api.bootstrap(in_server=True,

View File

@@ -255,10 +255,6 @@
dirsrv_cert_files: "{{ ipareplica_dirsrv_cert_files | default([]) }}"
### client ###
force_join: "{{ ipaclient_force_join }}"
### ad trust ###
netbios_name: "{{ ipareplica_netbios_name | default(omit) }}"
rid_base: "{{ ipareplica_rid_base | default(omit) }}"
secondary_rid_base: "{{ ipareplica_secondary_rid_base | default(omit) }}"
### additional ###
server: "{{ result_ipareplica_test.server }}"
ccache: "{{ result_ipareplica_prepare.ccache }}"
@@ -297,10 +293,6 @@
dirsrv_cert_files: "{{ ipareplica_dirsrv_cert_files | default([]) }}"
### client ###
force_join: "{{ ipaclient_force_join }}"
### ad trust ###
netbios_name: "{{ ipareplica_netbios_name | default(omit) }}"
rid_base: "{{ ipareplica_rid_base | default(omit) }}"
secondary_rid_base: "{{ ipareplica_secondary_rid_base | default(omit) }}"
### additional ###
server: "{{ result_ipareplica_test.server }}"
ccache: "{{ result_ipareplica_prepare.ccache }}"
@@ -339,10 +331,6 @@
dirsrv_cert_files: "{{ ipareplica_dirsrv_cert_files | default([]) }}"
### client ###
force_join: "{{ ipaclient_force_join }}"
### ad trust ###
netbios_name: "{{ ipareplica_netbios_name | default(omit) }}"
rid_base: "{{ ipareplica_rid_base | default(omit) }}"
secondary_rid_base: "{{ ipareplica_secondary_rid_base | default(omit) }}"
### additional ###
server: "{{ result_ipareplica_test.server }}"
config_master_host_name:
@@ -397,10 +385,6 @@
dirsrv_cert_files: "{{ ipareplica_dirsrv_cert_files | default([]) }}"
### client ###
force_join: "{{ ipaclient_force_join }}"
### ad trust ###
netbios_name: "{{ ipareplica_netbios_name | default(omit) }}"
rid_base: "{{ ipareplica_rid_base | default(omit) }}"
secondary_rid_base: "{{ ipareplica_secondary_rid_base | default(omit) }}"
### additional ###
server: "{{ result_ipareplica_test.server }}"
config_master_host_name:
@@ -481,10 +465,6 @@
dirsrv_cert_files: "{{ ipareplica_dirsrv_cert_files | default([]) }}"
### client ###
force_join: "{{ ipaclient_force_join }}"
### ad trust ###
netbios_name: "{{ ipareplica_netbios_name | default(omit) }}"
rid_base: "{{ ipareplica_rid_base | default(omit) }}"
secondary_rid_base: "{{ ipareplica_secondary_rid_base | default(omit) }}"
### additional ###
server: "{{ result_ipareplica_test.server }}"
config_master_host_name:
@@ -779,13 +759,12 @@
"{{ result_ipareplica_prepare.config_master_host_name }}"
register: result_ipareplica_enable_ipa
always:
- name: Install - Cleanup root IPA cache
ansible.builtin.file:
path: "/root/.ipa_cache"
state: absent
when: result_ipareplica_enable_ipa.changed
always:
- name: Cleanup temporary files
ansible.builtin.file:
path: "{{ item }}"

View File

@@ -1,37 +1,19 @@
---
# tasks to uninstall IPA replica
- name: Uninstall - Uninstall IPA replica
ansible.builtin.command: >
/usr/sbin/ipa-server-install
--uninstall
-U
{{ "--ignore-topology-disconnect" if
ipareplica_ignore_topology_disconnect | bool else "" }}
{{ "--ignore-last-of-role" if ipareplica_ignore_last_of_role | bool
else "" }}
register: result_uninstall
# 2 means that uninstall failed because IPA replica was not configured
failed_when: result_uninstall.rc != 0 and "'Env' object
has no attribute 'basedn'" not in result_uninstall.stderr
# IPA server is not configured on this system" not in
# result_uninstall.stdout_lines
changed_when: result_uninstall.rc == 0
# until: result_uninstall.rc == 0
retries: 2
delay: 1
- name: Set parameters
ansible.builtin.set_fact:
_ignore_topology_disconnect: "{{ ipaserver_ignore_topology_disconnect | default(ipareplica_ignore_topology_disconnect) | default(omit) }}"
_ignore_last_of_role: "{{ ipaserver_ignore_last_of_role | default(ipareplica_ignore_last_of_role) | default(omit) }}"
_remove_from_domain: "{{ ipaserver_remove_from_domain | default(ipareplica_remove_from_domain) | default(omit) }}"
_remove_on_server: "{{ ipaserver_remove_on_server | default(ipareplica_remove_on_server) | default(omit) }}"
#- name: Uninstall - Remove all replication agreements and data about replica
# ansible.builtin.command: >
# /usr/sbin/ipa-replica-manage
# del
# {{ ipareplica_hostname | default(ansible_facts['fqdn']) }}
# --force
# --password={{ ipadm_password }}
# failed_when: False
# delegate_to: "{{ groups.ipaserver[0] | default(fail) }}"
#- name: Remove IPA replica packages
# ansible.builtin.package:
# name: "{{ ipareplica_packages }}"
# state: absent
- name: Uninstall - Uninstall replica
ansible.builtin.include_role:
name: ipaserver
vars:
state: absent
ipaserver_ignore_topology_disconnect: "{{ _ignore_topology_disconnect | default(false) }}"
ipaserver_ignore_last_of_role: "{{ _ignore_last_of_role | default(false) }}"
ipaserver_remove_from_domain: "{{ _remove_from_domain | default(false) }}"
ipaserver_remove_on_server: "{{ _remove_on_server | default(NULL) }}"

View File

@@ -79,7 +79,7 @@ Example playbook to setup the IPA server using admin and dirman passwords from a
state: present
```
Example playbook to unconfigure the IPA client(s) using principal and password from inventory file:
Example playbook to unconfigure the IPA server using principal and password from inventory file:
```yaml
---
@@ -168,6 +168,64 @@ Server installation step 2: Copy `<ipaserver hostname>-chain.crt` to the IPA ser
The files can also be copied automatically: Set `ipaserver_copy_csr_to_controller` to true in the server installation step 1 and set `ipaserver_external_cert_files_from_controller` to point to the `chain.crt` file in the server installation step 2.
Since version 4.10, FreeIPA supports creating certificates using random serial numbers. Random serial numbers is a global and permanent setting, that can only be activated while deploying the first server of the domain. Replicas will inherit this setting automatically. An example of an inventory file to deploy a server with random serial numbers enabled is:
```ini
[ipaserver]
ipaserver.example.com
[ipaserver:vars]
ipaserver_domain=example.com
ipaserver_realm=EXAMPLE.COM
ipaadmin_password=MySecretPassword123
ipadm_password=MySecretPassword234
ipaserver_random_serial_number=true
```
By setting the variable in the inventory file, the same ipaserver deployment playbook, shown before, can be used.
Example inventory file to remove a server from the domain:
```ini
[ipaserver]
ipaserver.example.com
[ipaserver:vars]
ipaadmin_password=MySecretPassword123
ipaserver_remove_from_domain=true
```
Example playbook to remove an IPA server using admin passwords from the domain:
```yaml
---
- name: Playbook to remove IPA server
hosts: ipaserver
become: true
roles:
- role: ipaserver
state: absent
```
The inventory will enable the removal of the server (also a replica) from the domain. Additional options are needed if the removal of the server/replica is resulting in a topology disconnect or if the server/replica is the last that has a role.
To continue with the removal with a topology disconnect it is needed to set these parameters:
```ini
ipaserver_ignore_topology_disconnect=true
ipaserver_remove_on_server=ipaserver2.example.com
```
To continue with the removal for a server that is the last that has a role:
```ini
ipaserver_ignore_last_of_role=true
```
Be careful with enabling the `ipaserver_ignore_topology_disconnect` and especially `ipaserver_ignore_last_of_role`, the change can not be reverted easily.
Playbooks
=========
@@ -221,6 +279,7 @@ Variable | Description | Required
`ipaserver_no_ui_redirect` | Do not automatically redirect to the Web UI. (bool) | no
`ipaserver_dirsrv_config_file` | The path to LDIF file that will be used to modify configuration of dse.ldif during installation. (string) | no
`ipaserver_pki_config_override` | Path to ini file with config overrides. This is only usable with recent FreeIPA versions. (string) | no
`ipaserver_random_serial_numbers` | Enable use of random serial numbers for certificates. Requires FreeIPA version 4.10 or later. (boolean) | no
SSL certificate Variables
-------------------------
@@ -252,6 +311,7 @@ Variable | Description | Required
`ipaclient_no_ssh` | The bool value defines if OpenSSH client will be configured. `ipaclient_no_ssh` defaults to `no`. | no
`ipaclient_no_sshd` | The bool value defines if OpenSSH server will be configured. `ipaclient_no_sshd` defaults to `no`. | no
`ipaclient_no_sudo` | The bool value defines if SSSD will be configured as a data source for sudo. `ipaclient_no_sudo` defaults to `no`. | no
`ipaclient_subid` | The bool value defines if SSSD will be configured as a data source for subid. `ipaclient_subid` defaults to `no`. | no
`ipaclient_no_dns_sshfp` | The bool value defines if DNS SSHFP records will not be created automatically. `ipaclient_no_dns_sshfp` defaults to `no`. | no
Certificate system Variables
@@ -304,6 +364,19 @@ Variable | Description | Required
`ipaserver_external_cert_files_from_controller` | Files containing the IPA CA certificates and the external CA certificate chains on the controller that will be copied to the ipaserver host to `/root` folder. (list of string) | no
`ipaserver_copy_csr_to_controller` | Copy the generated CSR from the ipaserver to the controller as `"{{ inventory_hostname }}-ipa.csr"`. (bool) | no
Undeploy Variables (`state`: absent)
------------------------------------
These settings should only be used if the result is really wanted. The change might not be revertable easily.
Variable | Description | Required
-------- | ----------- | --------
`ipaserver_ignore_topology_disconnect` | If enabled this enforces the removal of the server even if it results in a topology disconnect. Be careful with this setting. (bool) | false
`ipaserver_ignore_last_of_role` | If enabled this enforces the removal of the server even if the server is the last with one that has a role. Be careful, this might not be revered easily. (bool) | false
`ipaserver_remove_from_domain` | This enables the removal of the server from the domain additionally to the undeployment. (bool) | false
`ipaserver_remove_on_server` | The value defines the server/replica in the domain that will to be used to remove the server/replica from the domain if `ipaserver_ignore_topology_disconnect` and `ipaserver_remove_from_domain` are enabled. Without the need to enable `ipaserver_ignore_topology_disconnect`, the value will be automatically detected using the replication agreements of the server/replica. (string) | false
Authors
=======

View File

@@ -11,6 +11,7 @@ ipaserver_no_hbac_allow: no
ipaserver_no_pkinit: no
ipaserver_no_ui_redirect: no
ipaserver_mem_check: yes
ipaserver_random_serial_numbers: false
### ssl certificate ###
### client ###
ipaclient_mkhomedir: no
@@ -42,3 +43,4 @@ ipaserver_copy_csr_to_controller: no
### uninstall ###
ipaserver_ignore_topology_disconnect: no
ipaserver_ignore_last_of_role: no
ipaserver_remove_from_domain: false

View File

@@ -0,0 +1,264 @@
# -*- coding: utf-8 -*-
# Authors:
# Thomas Woerner <twoerner@redhat.com>
#
# Copyright (C) 2019-2022 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
"metadata_version": "1.0",
"supported_by": "community",
"status": ["preview"],
}
DOCUMENTATION = """
---
module: ipaserver_get_connected_server
short_description: Get connected servers for server
description: Get connected servers for server
options:
ipaadmin_principal:
description: The admin principal.
default: admin
type: str
ipaadmin_password:
description: The admin password.
required: true
type: str
hostname:
description: The FQDN server name.
type: str
required: true
author:
- Thomas Woerner (@t-woerner)
"""
EXAMPLES = """
"""
RETURN = """
server:
description: Connected server name
returned: always
type: str
"""
import os
import tempfile
import shutil
from contextlib import contextmanager
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_text
from ansible.module_utils import six
try:
from ipalib import api
from ipalib import errors as ipalib_errors # noqa
from ipalib.config import Env
from ipaplatform.paths import paths
from ipapython.ipautil import run
from ipalib.constants import DEFAULT_CONFIG
try:
from ipalib.install.kinit import kinit_password
except ImportError:
from ipapython.ipautil import kinit_password
except ImportError as _err:
MODULE_IMPORT_ERROR = str(_err)
else:
MODULE_IMPORT_ERROR = None
if six.PY3:
unicode = str
def temp_kinit(principal, password):
"""Kinit with password using a temporary ccache."""
ccache_dir = tempfile.mkdtemp(prefix='krbcc')
ccache_name = os.path.join(ccache_dir, 'ccache')
try:
kinit_password(principal, password, ccache_name)
except RuntimeError as e:
raise RuntimeError("Kerberos authentication failed: %s" % str(e))
os.environ["KRB5CCNAME"] = ccache_name
return ccache_dir, ccache_name
def temp_kdestroy(ccache_dir, ccache_name):
"""Destroy temporary ticket and remove temporary ccache."""
if ccache_name is not None:
run([paths.KDESTROY, '-c', ccache_name], raiseonerr=False)
del os.environ['KRB5CCNAME']
if ccache_dir is not None:
shutil.rmtree(ccache_dir, ignore_errors=True)
@contextmanager
def ipa_connect(module, principal=None, password=None):
"""
Create a context with a connection to IPA API.
Parameters
----------
module: AnsibleModule
The AnsibleModule to use
principal: string
The optional principal name
password: string
The admin password.
"""
if not password:
module.fail_json(msg="Password is required.")
if not principal:
principal = "admin"
ccache_dir = None
ccache_name = None
try:
ccache_dir, ccache_name = temp_kinit(principal, password)
# api_connect start
env = Env()
env._bootstrap()
env._finalize_core(**dict(DEFAULT_CONFIG))
api.bootstrap(context="server", debug=env.debug, log=None)
api.finalize()
if api.env.in_server:
backend = api.Backend.ldap2
else:
backend = api.Backend.rpcclient
if not backend.isconnected():
backend.connect(ccache=ccache_name)
# api_connect end
except Exception as e:
module.fail_json(msg=str(e))
else:
try:
yield ccache_name
except Exception as e:
module.fail_json(msg=str(e))
finally:
temp_kdestroy(ccache_dir, ccache_name)
def ipa_command(command, name, args):
"""
Execute an IPA API command with a required `name` argument.
Parameters
----------
command: string
The IPA API command to execute.
name: string
The name parameter to pass to the command.
args: dict
The parameters to pass to the command.
"""
return api.Command[command](name, **args)
def _afm_convert(value):
if value is not None:
if isinstance(value, list):
return [_afm_convert(x) for x in value]
if isinstance(value, dict):
return {_afm_convert(k): _afm_convert(v)
for k, v in value.items()}
if isinstance(value, str):
return to_text(value)
return value
def module_params_get(module, name):
return _afm_convert(module.params.get(name))
def host_show(module, name):
_args = {
"all": True,
}
try:
_result = ipa_command("host_show", name, _args)
except ipalib_errors.NotFound as e:
msg = str(e)
if "host not found" in msg:
return None
module.fail_json(msg="host_show failed: %s" % msg)
return _result["result"]
def main():
module = AnsibleModule(
argument_spec=dict(
ipaadmin_principal=dict(type="str", default="admin"),
ipaadmin_password=dict(type="str", required=True, no_log=True),
hostname=dict(type="str", required=True),
),
supports_check_mode=True,
)
if MODULE_IMPORT_ERROR is not None:
module.fail_json(msg=MODULE_IMPORT_ERROR)
# In check mode always return changed.
if module.check_mode:
module.exit_json(changed=False)
ipaadmin_principal = module_params_get(module, "ipaadmin_principal")
ipaadmin_password = module_params_get(module, "ipaadmin_password")
hostname = module_params_get(module, "hostname")
server = None
right_left = ["iparepltoposegmentrightnode", "iparepltoposegmentleftnode"]
with ipa_connect(module, ipaadmin_principal, ipaadmin_password):
# At first search in the domain, then ca suffix:
# Search for the first iparepltoposegmentleftnode (node 2), where
# iparepltoposegmentrightnode is hostname (node 1), then for the
# first iparepltoposegmentrightnode (node 2) where
# iparepltoposegmentleftnode is hostname (node 1).
for suffix_name in ["domain", "ca"]:
for node1, node2 in [[right_left[0], right_left[1]],
[right_left[1], right_left[0]]]:
args = {node1: hostname}
result = api.Command.topologysegment_find(
suffix_name, **args)
if result and "result" in result and len(result["result"]) > 0:
res = result["result"][0]
if node2 in res:
if len(res[node2]) > 0:
server = res[node2][0]
break
if server is not None:
module.exit_json(changed=False, server=server)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -208,6 +208,11 @@ options:
description: The installer ca_subject setting
type: str
required: no
random_serial_numbers:
description: The installer random_serial_numbers setting
type: bool
default: no
required: no
allow_zone_overlap:
description: Create DNS zone even if it already exists
type: bool
@@ -304,7 +309,7 @@ from ansible.module_utils.ansible_ipa_server import (
check_dirsrv, ScriptError, get_fqdn, verify_fqdn, BadHostError,
validate_domain_name, load_pkcs12, IPA_PYTHON_VERSION,
encode_certificate, check_available_memory, getargspec, adtrustinstance,
get_min_idstart
get_min_idstart, SerialNumber
)
from ansible.module_utils import six
@@ -369,6 +374,8 @@ def main():
elements='str', default=None),
subject_base=dict(required=False, type='str'),
ca_subject=dict(required=False, type='str'),
random_serial_numbers=dict(required=False, type='bool',
default=False),
# ca_signing_algorithm
# dns
allow_zone_overlap=dict(required=False, type='bool',
@@ -456,6 +463,8 @@ def main():
'external_cert_files')
options.subject_base = ansible_module.params.get('subject_base')
options.ca_subject = ansible_module.params.get('ca_subject')
options._random_serial_numbers = ansible_module.params.get(
'random_serial_numbers')
# ca_signing_algorithm
# dns
options.allow_zone_overlap = ansible_module.params.get(
@@ -513,6 +522,12 @@ def main():
ansible_module.fail_json(
msg="pki_config_override: %s" % str(e))
# Check if Random Serial Numbers v3 is available
if options._random_serial_numbers and SerialNumber is None:
ansible_module.fail_json(
msg="Random Serial Numbers is not supported for this IPA version"
)
# default values ########################################################
# idstart and idmax
@@ -1040,6 +1055,11 @@ def main():
domain_name = domain_name.lower()
# Both host_name and domain_name are lowercase at this point.
if host_name == domain_name:
ansible_module.fail_json(
msg="hostname cannot be the same as the domain name")
if not options.realm_name:
realm_name = domain_name.upper()
else:
@@ -1053,7 +1073,7 @@ def main():
try:
validate_domain_name(realm_name, entity="realm")
except ValueError as e:
raise ScriptError("Invalid realm name: {}".format(unicode(e)))
raise ScriptError("Invalid realm name: {0}".format(unicode(e)))
if not options.setup_adtrust:
# If domain name and realm does not match, IPA server will not be able
@@ -1147,42 +1167,45 @@ def main():
pkinit_pkcs12_info = ("/etc/ipa/.tmp_pkcs12_pkinit", pkinit_pin)
pkinit_ca_cert = encode_certificate(pkinit_ca_cert)
ansible_module.exit_json(changed=False,
ipa_python_version=IPA_PYTHON_VERSION,
# basic
domain=options.domain_name,
realm=realm_name,
hostname=host_name,
_hostname_overridden=bool(options.host_name),
no_host_dns=options.no_host_dns,
# server
setup_adtrust=options.setup_adtrust,
setup_kra=options.setup_kra,
setup_ca=options.setup_ca,
idstart=options.idstart,
idmax=options.idmax,
no_pkinit=options.no_pkinit,
# ssl certificate
_dirsrv_pkcs12_info=dirsrv_pkcs12_info,
_dirsrv_ca_cert=dirsrv_ca_cert,
_http_pkcs12_info=http_pkcs12_info,
_http_ca_cert=http_ca_cert,
_pkinit_pkcs12_info=pkinit_pkcs12_info,
_pkinit_ca_cert=pkinit_ca_cert,
# certificate system
external_ca=options.external_ca,
external_ca_type=options.external_ca_type,
external_ca_profile=options.external_ca_profile,
# ad trust
rid_base=options.rid_base,
secondary_rid_base=options.secondary_rid_base,
# client
ntp_servers=options.ntp_servers,
ntp_pool=options.ntp_pool,
# additional
_installation_cleanup=_installation_cleanup,
domainlevel=options.domainlevel,
sid_generation_always=sid_generation_always)
ansible_module.exit_json(
changed=False,
ipa_python_version=IPA_PYTHON_VERSION,
# basic
domain=options.domain_name,
realm=realm_name,
hostname=host_name,
_hostname_overridden=bool(options.host_name),
no_host_dns=options.no_host_dns,
# server
setup_adtrust=options.setup_adtrust,
setup_kra=options.setup_kra,
setup_ca=options.setup_ca,
idstart=options.idstart,
idmax=options.idmax,
no_pkinit=options.no_pkinit,
# ssl certificate
_dirsrv_pkcs12_info=dirsrv_pkcs12_info,
_dirsrv_ca_cert=dirsrv_ca_cert,
_http_pkcs12_info=http_pkcs12_info,
_http_ca_cert=http_ca_cert,
_pkinit_pkcs12_info=pkinit_pkcs12_info,
_pkinit_ca_cert=pkinit_ca_cert,
# certificate system
external_ca=options.external_ca,
external_ca_type=options.external_ca_type,
external_ca_profile=options.external_ca_profile,
# ad trust
rid_base=options.rid_base,
secondary_rid_base=options.secondary_rid_base,
# client
ntp_servers=options.ntp_servers,
ntp_pool=options.ntp_pool,
# additional
_installation_cleanup=_installation_cleanup,
domainlevel=options.domainlevel,
sid_generation_always=sid_generation_always,
random_serial_numbers=options._random_serial_numbers,
)
if __name__ == '__main__':

View File

@@ -44,7 +44,7 @@ __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"]
"NUM_VERSION", "SerialNumber"]
import sys
import logging
@@ -203,6 +203,13 @@ try:
except ImportError:
get_min_idstart = None
# SerialNumber is defined in versions 4.10 and later and is
# used by Random Serian Number v3.
try:
from ipalib.parameters import SerialNumber
except ImportError:
SerialNumber = None
else:
# IPA version < 4.5

View File

@@ -108,6 +108,7 @@
external_cert_files: "{{ ipaserver_external_cert_files | default(omit) }}"
subject_base: "{{ ipaserver_subject_base | default(omit) }}"
ca_subject: "{{ ipaserver_ca_subject | default(omit) }}"
random_serial_numbers: "{{ ipaserver_random_serial_numbers | default(omit) }}"
# ca_signing_algorithm
### dns ###
allow_zone_overlap: "{{ ipaserver_allow_zone_overlap }}"
@@ -199,7 +200,7 @@
### additional ###
setup_ca: "{{ result_ipaserver_test.setup_ca }}"
sid_generation_always: "{{ result_ipaserver_test.sid_generation_always }}"
random_serial_numbers: no
random_serial_numbers: "{{ result_ipaserver_test.random_serial_numbers }}"
_hostname_overridden: "{{ result_ipaserver_test._hostname_overridden }}"
register: result_ipaserver_prepare
@@ -446,12 +447,6 @@
setup_ca: "{{ result_ipaserver_test.setup_ca }}"
register: result_ipaserver_enable_ipa
- name: Install - Cleanup root IPA cache
ansible.builtin.file:
path: "/root/.ipa_cache"
state: absent
when: result_ipaserver_enable_ipa.changed
- name: Install - Configure firewalld
ansible.builtin.command: >
firewall-cmd
@@ -480,6 +475,11 @@
when: ipaserver_setup_firewalld | bool
always:
- name: Install - Cleanup root IPA cache
ansible.builtin.file:
path: "/root/.ipa_cache"
state: absent
- name: Cleanup temporary files
ansible.builtin.file:
path: "{{ item }}"

View File

@@ -1,6 +1,47 @@
---
# tasks to uninstall IPA server
- name: Uninstall - Set server hostname for removal
ansible.builtin.set_fact:
_remove_hostname: "{{ ansible_facts['fqdn'] }}"
- name: Uninstall - Remove server
when: ipaserver_remove_from_domain
block:
- name: Uninstall - Fail on missing ipaadmin_password for server removal
ansible.builtin.fail:
msg: "'ipaadmin_password' is needed for 'ipaserver_remove_from_domain'"
when: ipaadmin_password is not defined
- name: Uninstall - Fail on missing ipaserver_remove_on_server with ipaserver_ignore_topology_disconnect
ansible.builtin.fail:
msg: "'ipaserver_remove_on_server' is needed for 'ipaserver_remove_from_domain' with 'ipaserver_ignore_topology_disconnect'"
when: ipaserver_ignore_topology_disconnect | bool
and ipaserver_remove_on_server is not defined
- name: Uninstall - Get connected server
ipaserver_get_connected_server:
ipaadmin_principal: "{{ ipaadmin_principal | default('admin') }}"
ipaadmin_password: "{{ ipaadmin_password }}"
hostname: "{{ _remove_hostname }}"
register: result_get_connected_server
when: ipaserver_remove_on_server is not defined
# REMOVE SERVER FROM DOMAIN
- name: Uninstall - Server del "{{ _remove_hostname }}"
ipaserver:
ipaadmin_principal: "{{ ipaadmin_principal | default('admin') }}"
ipaadmin_password: "{{ ipaadmin_password }}"
name: "{{ _remove_hostname }}"
ignore_last_of_role: "{{ ipaserver_ignore_last_of_role }}"
ignore_topology_disconnect: "{{ ipaserver_ignore_topology_disconnect }}"
# delete_continue: "{{ ipaserver_delete_continue }}"
state: absent
delegate_to: "{{ ipaserver_remove_on_server | default(result_get_connected_server.server) }}"
when: ipaserver_remove_on_server is defined or
result_get_connected_server.server is defined
- name: Uninstall - Uninstall IPA server
ansible.builtin.command: >
/usr/sbin/ipa-server-install

View File

@@ -60,6 +60,7 @@ disable =
[pylint.BASIC]
good-names =
ex, i, j, k, Run, _, e, x, dn, cn, ip, os, unicode, __metaclass__, ds,
dt, ca,
# These are utils tools, and not part of the released collection.
galaxyfy-playbook, galaxyfy-README, galaxyfy-module-EXAMPLES,
module_EXAMPLES

View File

@@ -105,7 +105,7 @@ It's also possible to run the tests in a container.
Before setting up a container you will need to install molecule framework:
```
pip install molecule[docker]>=3
pip install molecule-plugins[docker]
```
Now you can start a test container using the following command:

View File

@@ -22,7 +22,7 @@ jobs:
retryCountOnTaskFailure: 5
displayName: Install tools
- script: pip install molecule[docker]
- script: pip install molecule-plugins[docker] "requests<2.29"
retryCountOnTaskFailure: 5
displayName: Install molecule

View File

@@ -23,7 +23,8 @@ jobs:
- script: |
pip install \
"molecule[docker]>=3" \
"molecule-plugins[docker]" \
"requests<2.29" \
"ansible${{ parameters.ansible_version }}"
retryCountOnTaskFailure: 5
displayName: Install molecule and Ansible

View File

@@ -33,7 +33,8 @@ jobs:
- script: |
pip install \
"molecule[docker]>=3" \
"molecule-plugins[docker]" \
"requests<2.29" \
"ansible${{ parameters.ansible_version }}"
retryCountOnTaskFailure: 5
displayName: Install molecule and Ansible

View File

@@ -34,8 +34,9 @@ jobs:
scenario: ${{ parameters.scenario }}
ansible_version: ${{ parameters.ansible_version }}
- template: galaxy_pytest_script.yml
parameters:
build_number: ${{ parameters.build_number }}
scenario: ${{ parameters.scenario }}
ansible_version: ${{ parameters.ansible_version }}
# Temporarily disable due to issues with ansible docker plugin.
#- template: galaxy_pytest_script.yml
# parameters:
# build_number: ${{ parameters.build_number }}
# scenario: ${{ parameters.scenario }}
# ansible_version: ${{ parameters.ansible_version }}

View File

@@ -34,8 +34,9 @@ jobs:
scenario: ${{ parameters.scenario }}
ansible_version: ${{ parameters.ansible_version }}
- template: pytest_tests.yml
parameters:
build_number: ${{ parameters.build_number }}
scenario: ${{ parameters.scenario }}
ansible_version: ${{ parameters.ansible_version }}
# Temporarily disabled due to ansible docker plugin issue.
#- template: pytest_tests.yml
# parameters:
# build_number: ${{ parameters.build_number }}
# scenario: ${{ parameters.scenario }}
# ansible_version: ${{ parameters.ansible_version }}

View File

@@ -32,7 +32,8 @@ jobs:
- script: |
pip install \
"molecule[docker]>=3" \
"molecule-plugins[docker]" \
"requests<2.29" \
"ansible${{ parameters.ansible_version }}"
retryCountOnTaskFailure: 5
displayName: Install molecule and Ansible

View File

@@ -32,7 +32,8 @@ jobs:
- script: |
pip install \
"molecule[docker]>=3" \
"molecule-plugins[docker]" \
"requests<2.29" \
"ansible${{ parameters.ansible_version }}"
retryCountOnTaskFailure: 5
displayName: Install molecule and Ansible

View File

@@ -26,7 +26,8 @@ jobs:
- script: |
pip install \
"molecule[docker]>=3" \
"molecule-plugins[docker]" \
"requests<2.29" \
"ansible${{ parameters.ansible_version }}"
retryCountOnTaskFailure: 5
displayName: Install molecule and Ansible

View File

@@ -0,0 +1,60 @@
---
- name: Test cert
hosts: ipaclients, ipaserver
become: false
gather_facts: false
module_defaults:
ipacert:
ipaadmin_password: SomeADMINpassword
ipaapi_contetx: "{{ ipa_context | default(omit) }}"
tasks:
- name: Include FreeIPA facts.
ansible.builtin.include_tasks: ../env_freeipa_facts.yml
# Test will only be executed if host is not a server.
- name: Execute with server context in the client.
ipacert:
ipaapi_context: server
name: ThisShouldNotWork
register: result
failed_when: not (result.failed and result.msg is regex("No module named '*ipaserver'*"))
when: ipa_host_is_client
# Import basic module tests, and execute with ipa_context set to 'client'.
# If ipaclients is set, it will be executed using the client, if not,
# ipaserver will be used.
#
# With this setup, tests can be executed against an IPA client, against
# an IPA server using "client" context, and ensure that tests are executed
# in upstream CI.
- name: Test host certs using client context, in client host.
ansible.builtin.import_playbook: test_cert_host.yml
when: groups['ipaclients']
vars:
ipa_test_host: ipaclients
- name: Test service certs using client context, in client host.
ansible.builtin.import_playbook: test_cert_service.yml
when: groups['ipaclients']
vars:
ipa_test_host: ipaclients
- name: Test user certs using client context, in client host.
ansible.builtin.import_playbook: test_cert_user.yml
when: groups['ipaclients']
vars:
ipa_test_host: ipaclients
- name: Test host certs using client context, in server host.
ansible.builtin.import_playbook: test_cert_host.yml
when: groups['ipaclients'] is not defined or not groups['ipaclients']
- name: Test service certs using client context, in server host.
ansible.builtin.import_playbook: test_cert_service.yml
when: groups['ipaclients'] is not defined or not groups['ipaclients']
- name: Test user certs using client context, in server host.
ansible.builtin.import_playbook: test_cert_user.yml
when: groups['ipaclients'] is not defined or not groups['ipaclients']

View File

@@ -0,0 +1,214 @@
---
- name: Test host certificate requests
hosts: "{{ ipa_test_host | default('ipaserver') }}"
become: false
gather_facts: false
module_defaults:
ipahost:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipacert:
ipaadmin_password: SomeADMINpassword
# ipacert only supports client context
ipaapi_context: "client"
tasks:
# SETUP
- name: Ensure test files do not exist
ansible.builtin.file:
path: "{{ item }}"
state: absent
with_items:
- "/root/retrieved.pem"
- "/root/cert_1.pem"
- "/root/host.csr"
# Ensure test items exist
- name: Ensure domain name is set
ansible.builtin.set_fact:
ipa_domain: ipa.test
when: ipa_domain is not defined
- name: Ensure test host exists
ipahost:
name: "certhost.{{ ipa_domain }}"
state: present
force: true
- name: Create CSR
ansible.builtin.shell:
cmd: "openssl req -newkey rsa:1024 -keyout /dev/null -nodes -subj /CN=certhost.{{ ipa_domain }}"
register: host_req
- name: Create CSR file
ansible.builtin.copy:
dest: "/root/host.csr"
content: "{{ host_req.stdout }}"
mode: 0644
# TESTS
- name: Request certificate for host
ipacert:
csr: '{{ host_req.stdout }}'
principal: "host/certhost.{{ ipa_domain }}"
state: requested
register: host_cert
failed_when: not host_cert.changed or host_cert.failed
- name: Display data from the requested certificate.
ansible.builtin.debug:
var: host_cert
- name: Retrieve certificate for host
ipacert:
serial_number: "{{ host_cert.certificate.serial_number }}"
state: retrieved
register: retrieved
failed_when: retrieved.certificate.serial_number != host_cert.certificate.serial_number
- name: Display data from the retrieved certificate.
ansible.builtin.debug:
var: retrieved
- name: Place certificate on hold
ipacert:
serial_number: '{{ host_cert.certificate.serial_number }}'
state: held
register: result
failed_when: not result.changed or result.failed
- name: Place certificate on hold, again
ipacert:
serial_number: '{{ host_cert.certificate.serial_number }}'
state: held
register: result
failed_when: result.changed or result.failed
- name: Release hold on certificate
ipacert:
serial_number: '{{ host_cert.certificate.serial_number }}'
state: released
register: result
failed_when: not result.changed or result.failed
- name: Release hold on certificate, again
ipacert:
serial_number: '{{ host_cert.certificate.serial_number }}'
state: released
register: result
failed_when: result.changed or result.failed
- name: Revoke certificate
ipacert:
serial_number: '{{ host_cert.certificate.serial_number }}'
state: revoked
reason: keyCompromise
register: result
failed_when: not result.changed or result.failed
- name: Revoke certificate, again
ipacert:
serial_number: '{{ host_cert.certificate.serial_number }}'
state: revoked
reason: keyCompromise
register: result
failed_when: result.changed or result.failed
- name: Try to revoke inexistent certificate
ipacert:
serial_number: 0x123456789
reason: 9
state: revoked
register: result
failed_when: not (result.failed and ("Request failed with status 404" in result.msg or "Certificate serial number 0x123456789 not found" in result.msg))
- name: Try to release revoked certificate
ipacert:
serial_number: '{{ host_cert.certificate.serial_number }}'
state: released
register: result
failed_when: not result.failed or "Cannot release hold on certificate revoked with reason" not in result.msg
- name: Request certificate for host and save to file
ipacert:
csr: '{{ host_req.stdout }}'
principal: "host/certhost.{{ ipa_domain }}"
certificate_out: "/root/cert_1.pem"
state: requested
register: result
failed_when: not result.changed or result.failed or result.certificate
- name: Check requested certificate file
ansible.builtin.file:
path: "/root/cert_1.pem"
check_mode: true
register: result
failed_when: result.changed or result.failed
- name: Retrieve certificate for host to a file
ipacert:
serial_number: "{{ host_cert.certificate.serial_number }}"
certificate_out: "/root/retrieved.pem"
state: retrieved
register: result
failed_when: result.changed or result.failed or result.certificate
- name: Check retrieved certificate file
ansible.builtin.file:
path: "/root/retrieved.pem"
check_mode: true
register: result
failed_when: result.changed or result.failed
- name: Request with invalid CSR.
ipacert:
csr: |
-----BEGIN CERTIFICATE REQUEST-----
BNxXqLcHylNEyg8SH0u63bWyxtgoDBfdZwdGAhYuJ+g4ev79J5eYoB0CAwEAAaAr
MCkGCSqGSIb3DQEJDjEcMBowGAYHKoZIzlYIAQQNDAtoZWxsbyB3b3JsZDANBgkq
hkiG9w0BAQsFAAOBgQADCi5BHDv1mrBFDWqYytFpQ1mrvr/mdax3AYXxNL2UEV8j
AqZAFTEnJXL/u1eVQtI1yotqxakyUBN4XZBP2CBgJRO93Mtry8cgvU1sPdU8Mavx
5gSnlP74Hio2ziscWWydlxpYxFx0gkKvu+0nyIpz954SVYwQ2wwk5FRqZnxI5w==
-----END CERTIFICATE REQUEST-----
principal: "host/certhost.{{ ipa_domain }}"
state: requested
register: result
failed_when: not (result.failed and "Failure decoding Certificate Signing Request" in result.msg)
- name: Request certificate using a file
ipacert:
csr_file: "/root/host.csr"
principal: "host/certhost.{{ ipa_domain }}"
state: requested
register: result
failed_when: not result.changed or result.failed
- name: Request certificate using an invalid profile
ipacert:
csr_file: "/root/host.csr"
principal: "host/certhost.{{ ipa_domain }}"
profile: invalid_profile
state: requested
register: result
failed_when: not (result.failed and "Request failed with status 400" in result.msg)
# CLEANUP TEST ITEMS
- name: Removet test host
ipahost:
name: "certhost.{{ ipa_domain }}"
state: absent
- name: Ensure test files do not exist
ansible.builtin.file:
path: "{{ item }}"
state: absent
with_items:
- "/root/retrieved.pem"
- "/root/cert_1.pem"
- "/root/host.csr"

View File

@@ -0,0 +1,232 @@
---
- name: Test service certificate requests
hosts: "{{ ipa_test_host | default('ipaserver') }}"
# Change "become" or "gather_facts" to "yes",
# if you test playbook requires any.
become: false
gather_facts: false
module_defaults:
ipahost:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipacert:
ipaadmin_password: SomeADMINpassword
# ipacert only supports client context
ipaapi_context: "client"
tasks:
# SETUP
- name: Ensure test files do not exist
ansible.builtin.file:
path: "{{ item }}"
state: absent
with_items:
- "/root/retrieved.pem"
- "/root/cert_1.pem"
- "/root/service.csr"
# Ensure test items exist
- name: Ensure domain name is set
ansible.builtin.set_fact:
ipa_domain: ipa.test
when: ipa_domain is not defined
- name: Ensure test host exist
ipahost:
name: "certservice.{{ ipa_domain }}"
force: true
state: present
- name: Ensure service exist
ipaservice:
name: "HTTP/certservice.{{ ipa_domain }}"
force: true
state: present
- name: Create signing request for certificate
ansible.builtin.shell:
cmd: "openssl req -newkey rsa:1024 -keyout /dev/null -nodes -subj /CN=certservice.{{ ipa_domain }}"
register: service_req
- name: Create CSR file
ansible.builtin.copy:
dest: "/root/service.csr"
content: "{{ service_req.stdout }}"
mode: '0644'
# TESTS
- name: Request certificate for service
ipacert:
csr: '{{ service_req.stdout }}'
principal: "HTTP/certservice.{{ ipa_domain }}"
add_principal: true
state: requested
register: service_cert
failed_when: not service_cert.changed or service_cert.failed
- name: Display data from the requested certificate.
ansible.builtin.debug:
var: service_cert
- name: Retrieve certificate for service
ipacert:
serial_number: "{{ service_cert.certificate.serial_number }}"
state: retrieved
register: retrieved
failed_when: retrieved.certificate.serial_number != service_cert.certificate.serial_number
- name: Display data from the retrieved certificate.
ansible.builtin.debug:
var: retrieved
- name: Place certificate on hold
ipacert:
serial_number: '{{ service_cert.certificate.serial_number }}'
state: held
register: result
failed_when: not result.changed or result.failed
- name: Place certificate on hold, again
ipacert:
serial_number: '{{ service_cert.certificate.serial_number }}'
state: held
register: result
failed_when: result.changed or result.failed
- name: Release hold on certificate
ipacert:
serial_number: '{{ service_cert.certificate.serial_number }}'
state: released
register: result
failed_when: not result.changed or result.failed
- name: Release hold on certificate, again
ipacert:
serial_number: '{{ service_cert.certificate.serial_number }}'
state: released
register: result
failed_when: result.changed or result.failed
- name: Revoke certificate
ipacert:
serial_number: '{{ service_cert.certificate.serial_number }}'
state: revoked
reason: keyCompromise
register: result
failed_when: not result.changed or result.failed
- name: Revoke certificate, again
ipacert:
serial_number: '{{ service_cert.certificate.serial_number }}'
state: revoked
reason: keyCompromise
register: result
failed_when: result.changed or result.failed
- name: Try to revoke inexistent certificate
ipacert:
serial_number: 0x123456789
reason: 9
state: revoked
register: result
failed_when: not (result.failed and ("Request failed with status 404" in result.msg or "Certificate serial number 0x123456789 not found" in result.msg))
- name: Try to release revoked certificate
ipacert:
serial_number: '{{ service_cert.certificate.serial_number }}'
state: released
register: result
failed_when: not result.failed or "Cannot release hold on certificate revoked with reason" not in result.msg
- name: Request certificate for service and save to file
ipacert:
csr: '{{ service_req.stdout }}'
principal: "HTTP/certservice.{{ ipa_domain }}"
add_principal: true
certificate_out: "/root/cert_1.pem"
state: requested
register: result
failed_when: not result.changed or result.failed or result.certificate
- name: Check requested certificate file
ansible.builtin.file:
path: "/root/cert_1.pem"
check_mode: true
register: result
failed_when: result.changed or result.failed
- name: Retrieve certificate for service to a file
ipacert:
serial_number: "{{ service_cert.certificate.serial_number }}"
certificate_out: "/root/retrieved.pem"
state: retrieved
register: result
failed_when: result.changed or result.failed or result.certificate
- name: Check retrieved certificate file
ansible.builtin.file:
path: "/root/retrieved.pem"
check_mode: true
register: result
failed_when: result.changed or result.failed
- name: Request with invalid CSR.
ipacert:
csr: |
-----BEGIN CERTIFICATE REQUEST-----
BNxXqLcHylNEyg8SH0u63bWyxtgoDBfdZwdGAhYuJ+g4ev79J5eYoB0CAwEAAaAr
MCkGCSqGSIb3DQEJDjEcMBowGAYHKoZIzlYIAQQNDAtoZWxsbyB3b3JsZDANBgkq
hkiG9w0BAQsFAAOBgQADCi5BHDv1mrBFDWqYytFpQ1mrvr/mdax3AYXxNL2UEV8j
AqZAFTEnJXL/u1eVQtI1yotqxakyUBN4XZBP2CBgJRO93Mtry8cgvU1sPdU8Mavx
5gSnlP74Hio2ziscWWydlxpYxFx0gkKvu+0nyIpz954SVYwQ2wwk5FRqZnxI5w==
-----END CERTIFICATE REQUEST-----
principal: "HTTP/certservice.{{ ipa_domain }}"
state: requested
register: result
failed_when: not (result.failed and "Failure decoding Certificate Signing Request" in result.msg)
- name: Request certificate using a file
ipacert:
csr_file: "/root/service.csr"
principal: "HTTP/certservice.{{ ipa_domain }}"
state: requested
register: result
failed_when: not result.changed or result.failed
- name: Request certificate using an invalid profile
ipacert:
csr_file: "/root/service.csr"
principal: "HTTP/certservice.{{ ipa_domain }}"
profile: invalid_profile
state: requested
register: result
failed_when: not (result.failed and "Request failed with status 400" in result.msg)
# CLEANUP TEST ITEMS
- name: Remove test service
ipaservice:
name: "HTTP/certservice.{{ ipa_domain }}"
state: absent
continue: true
- name: Remove test host
ipahost:
name: certservice.example.com
state: absent
- name: Ensure test files do not exist
ansible.builtin.file:
path: "{{ item }}"
state: absent
with_items:
- "/root/retrieved.pem"
- "/root/cert_1.pem"
- "/root/service.csr"

View File

@@ -0,0 +1,213 @@
---
- name: Test user certificate requests
hosts: "{{ ipa_test_host | default('ipaserver') }}"
become: false
gather_facts: false
module_defaults:
ipauser:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipacert:
ipaadmin_password: SomeADMINpassword
# ipacert only supports client context
ipaapi_context: "client"
tasks:
# Ensure test files do not exist
- name: Check retrieved certificate file
ansible.builtin.file:
path: "{{ item }}"
state: absent
with_items:
- "/root/retrieved.pem"
- "/root/cert_1.pem"
- "/root/user.csr"
# Ensure test items exist.
- name: Ensure test user exists
ipauser:
name: certuser
first: certificate
last: user
- name: Crete CSR
ansible.builtin.shell:
cmd:
'openssl req -newkey rsa:1024 -keyout /dev/null -nodes -subj /CN=certuser -reqexts IECUserRoles
-config <(cat /etc/pki/tls/openssl.cnf; printf "[IECUserRoles]\n1.2.840.10070.8.1=ASN1:UTF8String:hello world")'
executable: /bin/bash
register: user_req
- name: Create CSR file
ansible.builtin.copy:
dest: "/root/user.csr"
content: "{{ user_req.stdout }}"
mode: 0644
# TESTS
- name: Request certificate for user
ipacert:
csr: '{{ user_req.stdout }}'
principal: certuser
profile: IECUserRoles
state: requested
register: user_cert
failed_when: not user_cert.changed or user_cert.failed
- name: Display data from the requested certificate.
ansible.builtin.debug:
var: user_cert
- name: Retrieve certificate for user
ipacert:
serial_number: "{{ user_cert.certificate.serial_number }}"
state: retrieved
register: retrieved
failed_when: retrieved.certificate.serial_number != user_cert.certificate.serial_number
- name: Display data from the retrieved certificate.
ansible.builtin.debug:
var: retrieved
- name: Place certificate on hold
ipacert:
serial_number: '{{ user_cert.certificate.serial_number }}'
state: held
register: result
failed_when: not result.changed or result.failed
- name: Place certificate on hold, again
ipacert:
serial_number: '{{ user_cert.certificate.serial_number }}'
state: held
register: result
failed_when: result.changed or result.failed
- name: Release hold on certificate
ipacert:
serial_number: '{{ user_cert.certificate.serial_number }}'
state: released
register: result
failed_when: not result.changed or result.failed
- name: Release hold on certificate, again
ipacert:
serial_number: '{{ user_cert.certificate.serial_number }}'
state: released
register: result
failed_when: result.changed or result.failed
- name: Revoke certificate
ipacert:
serial_number: '{{ user_cert.certificate.serial_number }}'
state: revoked
reason: keyCompromise
register: result
failed_when: not result.changed or result.failed
- name: Revoke certificate, again
ipacert:
serial_number: '{{ user_cert.certificate.serial_number }}'
state: revoked
reason: keyCompromise
register: result
failed_when: result.changed or result.failed
- name: Try to revoke inexistent certificate
ipacert:
serial_number: 0x123456789
reason: 9
state: revoked
register: result
failed_when: not (result.failed and ("Request failed with status 404" in result.msg or "Certificate serial number 0x123456789 not found" in result.msg))
- name: Try to release revoked certificate
ipacert:
serial_number: '{{ user_cert.certificate.serial_number }}'
state: released
register: result
failed_when: not result.failed or "Cannot release hold on certificate revoked with reason" not in result.msg
- name: Request certificate for user and save to file
ipacert:
csr: '{{ user_req.stdout }}'
principal: certuser
profile: IECUserRoles
certificate_out: "/root/cert_1.pem"
state: requested
register: result
failed_when: not result.changed or result.failed or result.certificate
- name: Check requested certificate file
ansible.builtin.file:
path: "/root/cert_1.pem"
check_mode: true
register: result
failed_when: result.changed or result.failed
- name: Retrieve certificate for user to a file
ipacert:
serial_number: "{{ user_cert.certificate.serial_number }}"
certificate_out: "/root/retrieved.pem"
state: retrieved
register: result
failed_when: result.changed or result.failed or result.certificate
- name: Check retrieved certificate file
ansible.builtin.file:
path: "/root/retrieved.pem"
check_mode: true
register: result
failed_when: result.changed or result.failed
- name: Request with invalid CSR.
ipacert:
csr: |
-----BEGIN CERTIFICATE REQUEST-----
BNxXqLcHylNEyg8SH0u63bWyxtgoDBfdZwdGAhYuJ+g4ev79J5eYoB0CAwEAAaAr
MCkGCSqGSIb3DQEJDjEcMBowGAYHKoZIzlYIAQQNDAtoZWxsbyB3b3JsZDANBgkq
hkiG9w0BAQsFAAOBgQADCi5BHDv1mrBFDWqYytFpQ1mrvr/mdax3AYXxNL2UEV8j
AqZAFTEnJXL/u1eVQtI1yotqxakyUBN4XZBP2CBgJRO93Mtry8cgvU1sPdU8Mavx
5gSnlP74Hio2ziscWWydlxpYxFx0gkKvu+0nyIpz954SVYwQ2wwk5FRqZnxI5w==
-----END CERTIFICATE REQUEST-----
principal: certuser
state: requested
register: result
failed_when: not (result.failed and "Failure decoding Certificate Signing Request" in result.msg)
- name: Request certificate using a file
ipacert:
csr_file: "/root/user.csr"
principal: certuser
state: requested
register: result
failed_when: not result.changed or result.failed
- name: Request certificate using an invalid profile
ipacert:
csr_file: "/root/user.csr"
principal: certuser
profile: invalid_profile
state: requested
register: result
failed_when: not (result.failed and "Request failed with status 400" in result.msg)
# CLEANUP TEST ITEMS
- name: Remove test user
ipauser:
name: certuser
state: absent
- name: Check retrieved certificate file
ansible.builtin.file:
path: "{{ item }}"
state: absent
with_items:
- "/root/retrieved.pem"
- "/root/cert_1.pem"
- "/root/user.csr"

View File

@@ -59,13 +59,13 @@
pac_type: ""
- name: Execute tests if ipa_version >= 4.8.0
when: ipa_version is version('4.8.0', '>=')
block:
- name: Set maxhostname to 255
ipaconfig:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
maxhostname: 255
when: ipa_version is version('4.8.0', '>=')
- name: Set maxusername to 45
ipaconfig:
@@ -225,6 +225,7 @@
failed_when: result.changed or result.failed
- name: Execute tests if ipa_version >= 4.8.0
when: ipa_version is version('4.8.0', '>=')
block:
- name: Set maxhostname to 77
ipaconfig:
@@ -241,7 +242,6 @@
maxhostname: 77
register: result
failed_when: result.changed or result.failed
when: ipa_version is version('4.8.0', '>=')
- name: Set pwdexpnotify to 17
ipaconfig:
@@ -415,13 +415,13 @@
failed_when: not result.changed or result.failed
- name: Execute tests if ipa_version >= 4.8.0
when: ipa_version is version('4.8.0', '>=')
block:
- name: Reset maxhostname
ipaconfig:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
maxhostname: '{{ previousconfig.config.maxhostname | default(omit) }}'
when: ipa_version is version('4.8.0', '>=')
- name: Reset changed fields, again
ipaconfig:
@@ -451,13 +451,13 @@
failed_when: result.changed or result.failed
- name: Execute tests if ipa_version >= 4.8.0
when: ipa_version is version('4.8.0', '>=')
block:
- name: Reset maxhostname
ipaconfig:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
maxhostname: '{{ previousconfig.config.maxhostname | default(omit) }}'
when: ipa_version is version('4.8.0', '>=')
rescue:
- name: Set fields to IPA default, due to error

View File

@@ -19,6 +19,8 @@
# TESTS
- name: Test config sid
# only run tests if version supports enable-sid
when: ipa_version is version("4.9.8", ">=")
block:
- name: Check if SID is enabled.
ipaconfig:
@@ -28,7 +30,7 @@
check_mode: yes
register: sid_disabled
- name: Ensure netbios_name can't be changed without SID enabled.
- name: Ensure netbios_name can't be changed without SID enabled. # noqa 503
ipaconfig:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
@@ -37,7 +39,7 @@
failed_when: not result.failed and "SID generation must be enabled" in result.msg
when: sid_disabled.changed
- name: Ensure SIDs can't be changed without SID enabled.
- name: Ensure SIDs can't be changed without SID enabled. # noqa 503
ipaconfig:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
@@ -115,8 +117,6 @@
ipaapi_context: "{{ ipa_context | default(omit) }}"
add_sids: yes
# only run tests if version supports enable-sid
when: ipa_version is version("4.9.8", ">=")
# REVERT TO PREVIOUS CONFIG
always:
# Once SID is enabled, it cannot be reverted.

View File

@@ -31,6 +31,7 @@
trust_test_is_supported: no
- name: Ensure ipaserver_domain is set
when: ipaserver_domain is not defined
block:
- name: Get Domain from server name
ansible.builtin.set_fact:
@@ -41,4 +42,3 @@
ansible.builtin.set_fact:
ipaserver_domain: "ipa.test"
when: "'fqdn' not in ansible_facts"
when: ipaserver_domain is not defined

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/bash -eu
master=$1
if [ -z "$master" ]; then

View File

@@ -0,0 +1,13 @@
---
- name: Create groups.json
hosts: localhost
tasks:
- name: Check if groups.json exists
ansible.builtin.stat:
path: groups.json
register: register_stat_groups
- name: Create groups.json
ansible.builtin.command: /bin/bash groups.sh 500
when: not register_stat_groups.stat.exists

25
tests/group/groups.sh Normal file
View File

@@ -0,0 +1,25 @@
#!/bin/bash -eu
NUM=${1-1000}
FILE="groups.json"
echo "{" > "$FILE"
echo " \"group_list\": [" >> "$FILE"
for i in $(seq 1 "$NUM"); do
{
echo " {"
echo " \"name\": \"group$i\","
echo " \"description\": \"group description $i\""
} >> "$FILE"
if [ "$i" -lt "$NUM" ]; then
echo " }," >> "$FILE"
else
echo " }" >> "$FILE"
fi
done
echo " ]" >> "$FILE"
echo "}" >> "$FILE"

View File

@@ -138,6 +138,7 @@
# service
- name: Execute tests if ipa_verison >= 4.7.0
when: ipa_version is version('4.7.0', '>=')
block:
- name: Ensure service "{{ 'HTTP/' + fqdn_at_domain }}" is present in group group1
@@ -282,8 +283,6 @@
register: result
failed_when: result.changed or result.failed
when: ipa_version is version('4.7.0', '>=')
# user
- name: Ensure users user1, user2 and user3 are present in group group1

View File

@@ -37,3 +37,27 @@
when: groups['ipaclients'] is not defined or not groups['ipaclients']
vars:
ipa_context: client
- name: Test groups using client context, in client host.
ansible.builtin.import_playbook: test_groups.yml
when: groups['ipaclients']
vars:
ipa_test_host: ipaclients
- name: Test groups using client context, in server host.
ansible.builtin.import_playbook: test_groups.yml
when: groups['ipaclients'] is not defined or not groups['ipaclients']
vars:
ipa_context: client
- name: Test groups with mixed types using client context, in client host.
ansible.builtin.import_playbook: test_groups_external_nonposix.yml
when: groups['ipaclients']
vars:
ipa_test_host: ipaclients
- name: Test groups with mixed types using client context, in server host.
ansible.builtin.import_playbook: test_groups_external_nonposix.yml
when: groups['ipaclients'] is not defined or not groups['ipaclients']
vars:
ipa_context: client

View File

@@ -0,0 +1,81 @@
---
- name: Test external group group members (without trust-ad installed)
hosts: ipaserver
become: true
tasks:
- name: Ensure external test groups are absent
ipagroup:
ipaadmin_password: SomeADMINpassword
name:
- externaltestgroup01
- externaltestgroup02
state: absent
- name: Create external test group 01
ipagroup:
ipaadmin_password: SomeADMINpassword
name: externaltestgroup01
external: true
register: result
failed_when: result.failed or not result.changed
- name: Create external test group 02
ipagroup:
ipaadmin_password: SomeADMINpassword
name: externaltestgroup02
external: true
register: result
failed_when: result.failed or not result.changed
- name: Ensure externaltestgroup02 is a member of externaltestgroup01
ipagroup:
ipaadmin_password: SomeADMINpassword
name: externaltestgroup01
action: member
group:
- externaltestgroup02
register: result
failed_when: result.failed or not result.changed
- name: Ensure externaltestgroup02 is a member of externaltestgroup01, again
ipagroup:
ipaadmin_password: SomeADMINpassword
name: externaltestgroup01
action: member
group:
- externaltestgroup02
register: result
failed_when: result.failed or result.changed
- name: Ensure externaltestgroup02 is not a member of externaltestgroup01
ipagroup:
ipaadmin_password: SomeADMINpassword
name: externaltestgroup01
action: member
group:
- externaltestgroup02
state: absent
register: result
failed_when: result.failed or not result.changed
- name: Ensure externaltestgroup02 is not a member of externaltestgroup01, again
ipagroup:
ipaadmin_password: SomeADMINpassword
name: externaltestgroup01
action: member
group:
- externaltestgroup02
state: absent
register: result
failed_when: result.failed or result.changed
- name: Ensure external test groups are absent
ipagroup:
ipaadmin_password: SomeADMINpassword
name:
- externaltestgroup01
- externaltestgroup02
state: absent
register: result
failed_when: result.failed or not result.changed

View File

@@ -10,6 +10,7 @@
ansible.builtin.include_tasks: ../env_freeipa_facts.yml
- name: Execute group tests if trust test environment is supported
when: trust_test_is_supported | default(false)
block:
- name: Add nonposix group.
@@ -111,5 +112,3 @@
ipaadmin_password: SomeADMINpassword
name: extgroup
state: absent
when: trust_test_is_supported | default(false)

View File

@@ -205,6 +205,7 @@
# EXTERNAL MEMBER TEST (REQUIRES AD)
- name: Execute group tests if trust test environment is supported
when: trust_test_is_supported | default(false)
block:
- name: Ensure users testuser1, testuser2 and testuser3 are present in group externalgroup
@@ -231,8 +232,6 @@
register: result
failed_when: result.changed or result.failed
when: trust_test_is_supported | default(false)
# CONVERT NONPOSIX TO POSIX GROUP WITH USERS
- name: Ensure nonposix group nonposixgroup as posix

View File

@@ -13,6 +13,7 @@
ansible.builtin.include_tasks: ../env_freeipa_facts.yml
- name: Execute tests if ipa_verison >= 4.8.7 and trust test environment is supported
when: ipa_version is version("4.8.7", ">=") and trust_test_is_supported | default(false)
block:
- name: Create idoverrideuser.
ansible.builtin.shell: |
@@ -97,10 +98,8 @@
always:
- name: Remove idoverrideuser.
ansible.builtin.shell: |
kinit -c idoverride_cache admin <<< SomeADMINpassword
ipa idoverrideuser-del "Default Trust View" {{ ad_user }}
kdestroy -A -q -c idoverride_cache
when:
when: ipa_version is version("4.8.7", ">=") and trust_test_is_supported | default(false)
ansible.builtin.shell:
cmd: |
kinit -c idoverride_cache admin <<< SomeADMINpassword
ipa idoverrideuser-del "Default Trust View" {{ ad_user }}
kdestroy -A -q -c idoverride_cache

View File

@@ -9,6 +9,7 @@
ansible.builtin.include_tasks: ../env_freeipa_facts.yml
- name: Execute tests if ipa_verison >= 4.8.4
when: ipa_version is version('4.8.4', '>=')
block:
- name: Ensure user manangeruser1 and manageruser2 is absent
ipauser:
@@ -206,5 +207,3 @@
state: absent
register: result
failed_when: not result.changed or result.failed
when: ipa_version is version('4.8.4', '>=')

143
tests/group/test_groups.yml Normal file
View File

@@ -0,0 +1,143 @@
---
- name: Test groups
hosts: "{{ ipa_test_host | default('ipaserver') }}"
gather_facts: true
tasks:
# setup
- name: Include tasks ../env_freeipa_facts.yml
ansible.builtin.include_tasks: ../env_freeipa_facts.yml
# GET FQDN_AT_DOMAIN
- name: Get fqdn_at_domain
ansible.builtin.set_fact:
fqdn_at_domain: "{{ ansible_facts['fqdn'] + '@' + ipaserver_realm }}"
# CLEANUP TEST ITEMS
- name: Remove test groups
ipagroup:
ipaadmin_password: SomeADMINpassword
name: group1,group2,group3,group4,group5,group6,group7,group8,group9,group10
state: absent
- name: Remove test users
ipauser:
ipaadmin_password: SomeADMINpassword
name: user1,user2,user3
state: absent
# CREATE TEST ITEMS
- name: Users user1..3 present
ipauser:
ipaadmin_password: SomeADMINpassword
users:
- name: user1
first: user1
last: Last
- name: user2
first: user2
last: Last
- name: user3
first: user3
last: Last
# TESTS
- name: Groups group1..10 present
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: group1
- name: group2
user:
- user1
- user2
- user3
- name: group3
group:
- group1
- group2
- name: group4
- name: group5
- name: group6
- name: group7
- name: group8
- name: group9
- name: group10
register: result
failed_when: not result.changed or result.failed
- name: Groups group1..10 present again
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: group1
- name: group2
- name: group3
- name: group4
- name: group5
- name: group6
- name: group7
- name: group8
- name: group9
- name: group10
register: result
failed_when: result.changed or result.failed
# failed_when: not result.failed has been added as this test needs to
# fail because two groups with the same name should be added in the same
# task.
- name: Duplicate names in groups failure test
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: group1
- name: group2
- name: group3
- name: group3
register: result
failed_when: result.changed or not result.failed or "is used more than once" not in result.msg
- name: Groups/name and name group11 present
ipagroup:
ipaadmin_password: SomeADMINpassword
name: group11
groups:
- name: group11
register: result
failed_when: result.changed or not result.failed or "parameters are mutually exclusive" not in result.msg
- name: Groups/name and name are absent
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
- name: Name is absent
ipagroup:
ipaadmin_password: SomeADMINpassword
name:
register: result
failed_when: result.changed or not result.failed or "At least one name or groups is required" not in result.msg
- name: Only one group can be added at a time using name.
ipagroup:
ipaadmin_password: SomeADMINpassword
name: group11,group12
register: result
failed_when: result.changed or not result.failed or "Only one group can be added at a time using 'name'." not in result.msg
- name: Remove test groups
ipagroup:
ipaadmin_password: SomeADMINpassword
name: group1,group2,group3,group4,group5,group6,group7,group8,group9,group10
state: absent
- name: Remove test users
ipauser:
ipaadmin_password: SomeADMINpassword
name: user1,user2,user3
state: absent

View File

@@ -0,0 +1,35 @@
---
- name: Include create_groups_json.yml
ansible.builtin.import_playbook: create_groups_json.yml
- name: Test groups absent
hosts: ipaserver
gather_facts: false
tasks:
- name: Include groups.json
ansible.builtin.include_vars:
file: groups.json # noqa 505
- name: Initialize groups_names
ansible.builtin.set_fact:
groups_names: []
- name: Create dict with group names
ansible.builtin.set_fact:
groups_names: "{{ groups_names | default([]) + [{'name': item.name}] }}"
loop: "{{ group_list }}"
- name: Groups absent len:{{ group_list | length }}
ipagroup:
ipaadmin_password: SomeADMINpassword
groups: "{{ groups_names }}"
state: absent
- name: Remove groups.json
hosts: localhost
tasks:
- name: Remove groups.json
ansible.builtin.file:
state: absent
path: groups.json

View File

@@ -0,0 +1,395 @@
---
- name: Test multiple external and nonposix groups
hosts: "{{ ipa_test_host | default('ipaserver') }}"
gather_facts: true
tasks:
# setup
- name: Include tasks ../env_freeipa_facts.yml
ansible.builtin.include_tasks: ../env_freeipa_facts.yml
# GET FQDN_AT_DOMAIN
- name: Get fqdn_at_domain
ansible.builtin.set_fact:
fqdn_at_domain: "{{ ansible_facts['fqdn'] + '@' + ipaserver_realm }}"
# CLEANUP TEST ITEMS
- name: Remove testing groups.
ipagroup:
ipaadmin_password: SomeADMINpassword
name:
- extgroup
- nonposixgroup
- posixgroup
- fail_group
- group_1
- posix_group_1
- nonposix_group_1
- external_group_1
- external_group_2
state: absent
- name: Ensure test users testuser1, testuser2 and testuser3 are absent
ipauser:
ipaadmin_password: SomeADMINpassword
name: testuser1,testuser2,testuser3
state: absent
# CREATE TEST ITEMS
- name: Ensure test users testuser1..testuser3 are present
ipauser:
ipaadmin_password: SomeADMINpassword
users:
- name: testuser1
first: testuser1
last: Last
- name: testuser2
first: testuser2
last: Last
- name: testuser3
first: testuser3
last: Last
register: result
failed_when: not result.changed or result.failed
- name: Add nonposix group.
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: extgroup
nonposix: true
register: result
failed_when: result.failed or not result.changed
- name: Add nonposix group, again.
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: extgroup
nonposix: true
register: result
failed_when: result.failed or result.changed
- name: Set group to be external
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: extgroup
external: true
register: result
failed_when: result.failed or not result.changed
- name: Set group to be external, again.
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: extgroup
external: true
register: result
failed_when: result.failed or result.changed
- name: Set external group to be non-external.
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: extgroup
external: false
register: result
failed_when: not result.failed or "group can not be non-external" not in result.msg
- name: Set external group to be posix.
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: extgroup
posix: true
register: result
failed_when: not result.failed or "Cannot change `external` group" not in result.msg
- name: Add nonposix group.
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: posixgroup
nonposix: true
register: result
failed_when: result.failed or not result.changed
- name: Set group to be posix
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: posixgroup
posix: true
register: result
failed_when: result.failed or not result.changed
- name: Set group to be posix, again.
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: posixgroup
posix: true
register: result
failed_when: result.failed or result.changed
- name: Set posix group to be external.
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: posixgroup
external: true
register: result
failed_when: not result.failed or "Cannot change `posix` group" not in result.msg
- name: Set posix group to be non-posix.
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: posixgroup
posix: false
register: result
failed_when: not result.failed or "Cannot change `posix` group" not in result.msg
- name: Set posix group to be non-posix.
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: posixgroup
nonposix: true
register: result
failed_when: not result.failed or "Cannot change `posix` group" not in result.msg
- name: Add nonposix group.
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: nonposixgroup
posix: false
register: result
failed_when: result.failed or not result.changed
- name: Add nonposix group, again.
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: nonposixgroup
nonposix: true
register: result
failed_when: result.failed or result.changed
# NONPOSIX MEMBER TEST
- name: Ensure users testuser1, testuser2 and testuser3 are present in group nonposixgroup
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: nonposixgroup
nonposix: true
user:
- testuser1
- testuser2
- testuser3
register: result
failed_when: not result.changed or result.failed
- name: Ensure users testuser1, testuser2 and testuser3 are present in group nonposixgroup again
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: nonposixgroup
nonposix: true
user:
- testuser1
- testuser2
- testuser3
register: result
failed_when: result.changed or result.failed
# POSIX MEMBER TEST
- name: Ensure users testuser1, testuser2 and testuser3 are present in group posixgroup
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: posixgroup
posix: true
user:
- testuser1
- testuser2
- testuser3
register: result
failed_when: not result.changed or result.failed
- name: Ensure users testuser1, testuser2 and testuser3 are present in group posixgroup again
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: posixgroup
posix: true
user:
- testuser1
- testuser2
- testuser3
register: result
failed_when: result.changed or result.failed
# EXTERNAL MEMBER TEST (REQUIRES AD)
- name: Execute group tests if trust test environment is supported
when: trust_test_is_supported | default(false)
block:
- name: Ensure users testuser1, testuser2 and testuser3 are present in group externalgroup
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: externalgroup
external: true
user:
- testuser1
- testuser2
- testuser3
register: result
failed_when: not result.changed or result.failed
- name: Ensure users testuser1, testuser2 and testuser3 are present in group externalgroup again
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: externalgroup
external: true
user:
- testuser1
- testuser2
- testuser3
register: result
failed_when: result.changed or result.failed
# CONVERT NONPOSIX TO POSIX GROUP WITH USERS
- name: Ensure nonposix group nonposixgroup as posix
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: nonposixgroup
posix: true
register: result
failed_when: not result.changed or result.failed
- name: Ensure nonposix group nonposixgroup as posix, again
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: nonposixgroup
posix: true
register: result
failed_when: result.changed or result.failed
- name: Ensure nonposix group nonposixgroup (now posix) has users still
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: nonposixgroup
posix: true
user:
- testuser1
- testuser2
- testuser3
register: result
failed_when: result.changed or result.failed
# FAIL ON COMBINATIONS OF NONPOSIX, POSIX AND EXTERNAL
- name: Fail to ensure group as nonposix and posix
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: firstgroup
nonposix: true
posix: false
- name: fail_group
nonposix: true
posix: true
register: result
failed_when: not result.failed or "parameters are mutually exclusive for group `fail_group`" not in result.msg
- name: Fail to ensure group as nonposix and external
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: firstgroup
nonposix: true
posix: false
- name: fail_group
nonposix: true
external: true
register: result
failed_when: not result.failed or "parameters are mutually exclusive for group `fail_group`" not in result.msg
- name: Fail to ensure group as posix and external
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: firstgroup
nonposix: true
posix: false
- name: fail_group
posix: true
external: true
register: result
failed_when: not result.failed or "parameters are mutually exclusive for group `fail_group`" not in result.msg
# GROUPS WITH MIXED TYPES
- name: Adding posix, nonposix and external groups in one batch
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: posix_group_1
posix: true
- name: nonposix_group_1
nonposix: true
- name: external_group_1
external: true
register: result
failed_when: not result.changed or result.failed
- name: Adding non-external and external groups in one batch
ipagroup:
ipaadmin_password: SomeADMINpassword
groups:
- name: non_external_group_2
- name: external_group_2
external: true
register: result
failed_when: not result.changed or result.failed
# CLEANUP
- name: Remove testing groups.
ipagroup:
ipaadmin_password: SomeADMINpassword
name:
- extgroup
- nonposixgroup
- posixgroup
- fail_group
- group_1
- posix_group_1
- nonposix_group_1
- external_group_1
- external_group_2
- non_external_group_2
state: absent
- name: Ensure test users testuser1, testuser2 and testuser3 are absent
ipauser:
ipaadmin_password: SomeADMINpassword
name: testuser1,testuser2,testuser3
state: absent

View File

@@ -0,0 +1,40 @@
---
- name: Include create_groups_json.yml
ansible.builtin.import_playbook: create_groups_json.yml
- name: Test groups present
hosts: ipaserver
gather_facts: false
tasks:
- name: Include groups.json
ansible.builtin.include_vars:
file: groups.json # noqa 505
- name: Groups present len:{{ group_list | length }}
ipagroup:
ipaadmin_password: SomeADMINpassword
groups: "{{ group_list }}"
- name: Initialize groups_names
ansible.builtin.set_fact:
groups_names: []
- name: Create dict with group names
ansible.builtin.set_fact:
groups_names: "{{ groups_names | default([]) + [{'name': item.name}] }}"
loop: "{{ group_list }}"
- name: Remove groups
ipagroup:
ipaadmin_password: SomeADMINpassword
groups: "{{ groups_names }}"
state: absent
- name: Remove groups.json
hosts: localhost
tasks:
- name: Remove groups.json
ansible.builtin.file:
state: absent
path: groups.json

View File

@@ -0,0 +1,47 @@
---
- name: Include create_groups_json.yml
ansible.builtin.import_playbook: create_groups_json.yml
- name: Test groups present slice
hosts: ipaserver
gather_facts: false
vars:
slice_size: 500
tasks:
- name: Include groups.json
ansible.builtin.include_vars:
file: groups.json # noqa 505
- name: Size of groups slice.
ansible.builtin.debug:
msg: "{{ group_list | length }}"
- name: Groups present
ipagroup:
ipaadmin_password: SomeADMINpassword
groups: "{{ group_list[item : item + slice_size] }}"
loop: "{{ range(0, group_list | length, slice_size) | list }}"
- name: Initialize groups_names
ansible.builtin.set_fact:
groups_names: []
- name: Create dict with group names
ansible.builtin.set_fact:
groups_names: "{{ groups_names | default([]) + [{'name': item.name}] }}"
loop: "{{ group_list }}"
- name: Remove groups
ipagroup:
ipaadmin_password: SomeADMINpassword
groups: "{{ groups_names }}"
state: absent
- name: Remove groups.json
hosts: localhost
tasks:
- name: Remove groups.json
ansible.builtin.file:
state: absent
path: groups.json

View File

@@ -49,6 +49,26 @@
- "{{ host1_fqdn }}"
state: absent
- name: Host "{{ host1_fqdn }}" is present with random password using hosts parameter
ipahost:
ipaadmin_password: SomeADMINpassword
hosts:
- name: "{{ host1_fqdn }}"
random: yes
force: yes
update_password: on_create
register: ipahost
failed_when: not ipahost.changed or
ipahost.host[host1_fqdn].randompassword is not defined or
ipahost.failed
- name: Host "{{ host1_fqdn }}" absent
ipahost:
ipaadmin_password: SomeADMINpassword
name:
- "{{ host1_fqdn }}"
state: absent
- name: Hosts "{{ host1_fqdn }}" and "{{ host2_fqdn }}" present with random password
ipahost:
ipaadmin_password: SomeADMINpassword

View File

@@ -9,6 +9,7 @@
ansible.builtin.include_tasks: ../env_freeipa_facts.yml
- name: Tests requiring IPA version 4.8.4+
when: ipa_version is version('4.8.4', '>=')
block:
- name: Ensure host-group testhostgroup is absent
ipahostgroup:
@@ -224,4 +225,3 @@
state: absent
register: result
failed_when: not result.changed or result.failed
when: ipa_version is version('4.8.4', '>=')

View File

@@ -9,6 +9,7 @@
ansible.builtin.include_tasks: ../env_freeipa_facts.yml
- name: Tests requiring IPA version 4.8.7+
when: ipa_version is version('4.8.7', '>=')
block:
- name: Ensure testing host-group are absent
ipahostgroup:
@@ -108,5 +109,3 @@
- databases
- datalake
state: absent
when: ipa_version is version('4.8.7', '>=')

View File

@@ -120,6 +120,7 @@
name: local_id_range
- name: Execute idrange tests if trust test environment is supported
when: trust_test_is_supported | default(false)
block:
# Create trust with range_type: ipa-ad-trust
- name: Create trust with range_type 'ipa-ad-trust'
@@ -367,5 +368,3 @@
- ad_posix_id_range
continue: yes
state: absent
when: trust_test_is_supported | default(false)

View File

@@ -223,7 +223,7 @@
ipapwpolicy:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
maxrepeat: 4
maxsequence: 4
register: result
failed_when: not result.changed or result.failed
@@ -231,7 +231,7 @@
ipapwpolicy:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
maxrepeat: 4
maxsequence: 4
register: result
failed_when: result.changed or result.failed
@@ -239,7 +239,7 @@
ipapwpolicy:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
maxrepeat: 0
maxsequence: 0
register: result
failed_when: not result.changed or result.failed

View File

@@ -40,7 +40,7 @@ def pytest_configure(config):
if os.path.exists(config_dir):
inventory_path = os.path.join(config_dir, "test.inventory.yml")
inventory = get_inventory(inventory_path)
print("Configuring execution using {}".format(inventory_path))
print("Configuring execution using {0}".format(inventory_path))
ipaservers = inventory["all"]["children"]["ipaserver"]["hosts"]
ipaserver = list(ipaservers.values())[0]
private_key = os.path.join(config_dir, "id_rsa")

View File

@@ -1,5 +1,6 @@
---
- name: Ensure ipaserver_domain is set
when: ipaserver_domain is not defined
block:
- name: Get Domain from server name
ansible.builtin.set_fact:
@@ -9,7 +10,6 @@
ansible.builtin.set_fact:
ipaserver_domain: "ipa.test"
when: "'fqdn' not in ansible_facts"
when: ipaserver_domain is not defined
- name: Set ipaserver_realm.
ansible.builtin.set_fact:

View File

@@ -1,54 +0,0 @@
plugins/module_utils/ansible_freeipa_module.py compile-2.6!skip
plugins/module_utils/ansible_freeipa_module.py import-2.6!skip
plugins/module_utils/ansible_freeipa_module.py pylint:ansible-bad-function
plugins/modules/ipaclient_get_facts.py compile-2.6!skip
plugins/modules/ipaclient_get_facts.py import-2.6!skip
plugins/modules/ipaclient_api.py pylint:ansible-format-automatic-specification
plugins/modules/ipaclient_join.py pylint:ansible-format-automatic-specification
plugins/modules/ipaclient_test.py pylint:ansible-format-automatic-specification
plugins/modules/ipaconfig.py compile-2.6!skip
plugins/modules/ipaconfig.py import-2.6!skip
plugins/modules/ipadnsrecord.py compile-2.6!skip
plugins/modules/ipadnsrecord.py import-2.6!skip
plugins/modules/ipadnsrecord.py pylint:ansible-format-automatic-specification
plugins/modules/ipadnsrecord.py pylint:use-maxsplit-arg
plugins/modules/ipareplica_enable_ipa.py pylint:ansible-format-automatic-specification
plugins/modules/ipareplica_prepare.py pylint:ansible-format-automatic-specification
plugins/modules/ipareplica_test.py pylint:ansible-format-automatic-specification
plugins/modules/iparole.py compile-2.6!skip
plugins/modules/iparole.py import-2.6!skip
plugins/modules/ipaserver_setup_ca.py compile-2.6!skip
plugins/modules/ipaserver_setup_ca.py import-2.6!skip
plugins/modules/ipaserver_test.py pylint:ansible-format-automatic-specification
plugins/modules/ipaservice.py compile-2.6!skip
plugins/modules/ipaservice.py import-2.6!skip
plugins/modules/ipasudorule.py compile-2.6!skip
plugins/modules/ipasudorule.py import-2.6!skip
plugins/modules/ipavault.py compile-2.6!skip
plugins/modules/ipavault.py import-2.6!skip
roles/ipaclient/library/ipaclient_api.py pylint:ansible-format-automatic-specification
roles/ipaclient/library/ipaclient_join.py pylint:ansible-format-automatic-specification
roles/ipaclient/library/ipaclient_test.py pylint:ansible-format-automatic-specification
roles/ipareplica/library/ipareplica_enable_ipa.py pylint:ansible-format-automatic-specification
roles/ipareplica/library/ipareplica_prepare.py pylint:ansible-format-automatic-specification
roles/ipareplica/library/ipareplica_test.py pylint:ansible-format-automatic-specification
roles/ipaserver/library/ipaserver_test.py pylint:ansible-format-automatic-specification
roles/ipareplica/module_utils/ansible_ipa_replica.py pylint:ansible-format-automatic-specification
tests/external-signed-ca-with-automatic-copy/external-ca.sh shebang!skip
tests/pytests/conftest.py pylint:ansible-format-automatic-specification
tests/sanity/sanity.sh shebang!skip
tests/user/users.sh shebang!skip
tests/user/users_absent.sh shebang!skip
tests/utils.py pylint:ansible-format-automatic-specification
utils/ansible-doc-test shebang!skip
utils/build-galaxy-release.sh shebang!skip
utils/build-srpm.sh shebang!skip
utils/changelog shebang!skip
utils/check_test_configuration.py shebang!skip
utils/galaxyfy-README.py shebang!skip
utils/galaxyfy-module-EXAMPLES.py shebang!skip
utils/galaxyfy-playbook.py shebang!skip
utils/galaxyfy.py shebang!skip
utils/gen_modules_docs.sh shebang!skip
utils/lint_check.sh shebang!skip
utils/new_module shebang!skip

View File

@@ -1,36 +0,0 @@
plugins/module_utils/ansible_freeipa_module.py pylint:ansible-bad-function
plugins/modules/ipaclient_api.py pylint:ansible-format-automatic-specification
plugins/modules/ipaclient_join.py pylint:ansible-format-automatic-specification
plugins/modules/ipaclient_test.py pylint:ansible-format-automatic-specification
plugins/modules/ipadnsrecord.py pylint:ansible-format-automatic-specification
plugins/modules/ipadnsrecord.py pylint:use-maxsplit-arg
plugins/modules/ipareplica_enable_ipa.py pylint:ansible-format-automatic-specification
plugins/modules/ipareplica_prepare.py pylint:ansible-format-automatic-specification
plugins/modules/ipareplica_test.py pylint:ansible-format-automatic-specification
plugins/modules/ipaserver_test.py pylint:ansible-format-automatic-specification
roles/ipaclient/library/ipaclient_api.py pylint:ansible-format-automatic-specification
roles/ipaclient/library/ipaclient_join.py pylint:ansible-format-automatic-specification
roles/ipaclient/library/ipaclient_test.py pylint:ansible-format-automatic-specification
roles/ipareplica/library/ipareplica_enable_ipa.py pylint:ansible-format-automatic-specification
roles/ipareplica/library/ipareplica_prepare.py pylint:ansible-format-automatic-specification
roles/ipareplica/library/ipareplica_test.py pylint:ansible-format-automatic-specification
roles/ipaserver/library/ipaserver_test.py pylint:ansible-format-automatic-specification
roles/ipareplica/module_utils/ansible_ipa_replica.py pylint:ansible-format-automatic-specification
tests/external-signed-ca-with-automatic-copy/external-ca.sh shebang!skip
tests/pytests/conftest.py pylint:ansible-format-automatic-specification
tests/sanity/sanity.sh shebang!skip
tests/user/users.sh shebang!skip
tests/user/users_absent.sh shebang!skip
tests/utils.py pylint:ansible-format-automatic-specification
utils/ansible-doc-test shebang!skip
utils/build-galaxy-release.sh shebang!skip
utils/build-srpm.sh shebang!skip
utils/changelog shebang!skip
utils/check_test_configuration.py shebang!skip
utils/galaxyfy-README.py shebang!skip
utils/galaxyfy-module-EXAMPLES.py shebang!skip
utils/galaxyfy-playbook.py shebang!skip
utils/galaxyfy.py shebang!skip
utils/gen_modules_docs.sh shebang!skip
utils/lint_check.sh shebang!skip
utils/new_module shebang!skip

View File

@@ -1,36 +0,0 @@
plugins/module_utils/ansible_freeipa_module.py pylint:ansible-bad-function
plugins/modules/ipaclient_api.py pylint:ansible-format-automatic-specification
plugins/modules/ipaclient_join.py pylint:ansible-format-automatic-specification
plugins/modules/ipaclient_test.py pylint:ansible-format-automatic-specification
plugins/modules/ipadnsrecord.py pylint:ansible-format-automatic-specification
plugins/modules/ipadnsrecord.py pylint:use-maxsplit-arg
plugins/modules/ipareplica_enable_ipa.py pylint:ansible-format-automatic-specification
plugins/modules/ipareplica_prepare.py pylint:ansible-format-automatic-specification
plugins/modules/ipareplica_test.py pylint:ansible-format-automatic-specification
plugins/modules/ipaserver_test.py pylint:ansible-format-automatic-specification
roles/ipaclient/library/ipaclient_api.py pylint:ansible-format-automatic-specification
roles/ipaclient/library/ipaclient_join.py pylint:ansible-format-automatic-specification
roles/ipaclient/library/ipaclient_test.py pylint:ansible-format-automatic-specification
roles/ipareplica/library/ipareplica_enable_ipa.py pylint:ansible-format-automatic-specification
roles/ipareplica/library/ipareplica_prepare.py pylint:ansible-format-automatic-specification
roles/ipareplica/library/ipareplica_test.py pylint:ansible-format-automatic-specification
roles/ipaserver/library/ipaserver_test.py pylint:ansible-format-automatic-specification
roles/ipareplica/module_utils/ansible_ipa_replica.py pylint:ansible-format-automatic-specification
tests/external-signed-ca-with-automatic-copy/external-ca.sh shebang!skip
tests/pytests/conftest.py pylint:ansible-format-automatic-specification
tests/sanity/sanity.sh shebang!skip
tests/user/users.sh shebang!skip
tests/user/users_absent.sh shebang!skip
tests/utils.py pylint:ansible-format-automatic-specification
utils/ansible-doc-test shebang!skip
utils/build-galaxy-release.sh shebang!skip
utils/build-srpm.sh shebang!skip
utils/changelog shebang!skip
utils/check_test_configuration.py shebang!skip
utils/galaxyfy-README.py shebang!skip
utils/galaxyfy-module-EXAMPLES.py shebang!skip
utils/galaxyfy-playbook.py shebang!skip
utils/galaxyfy.py shebang!skip
utils/gen_modules_docs.sh shebang!skip
utils/lint_check.sh shebang!skip
utils/new_module shebang!skip

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/bash -eu
TOPDIR=$(readlink -f "$(dirname "$0")/../..")
pushd "${TOPDIR}" >/dev/null || exit 1

View File

@@ -8,6 +8,7 @@
# CLEANUP TEST ITEMS
- name: Ensure ipa_server_name is set
when: ipa_server_name is not defined
block:
- name: Get server name from hostname
ansible.builtin.set_fact:
@@ -16,9 +17,9 @@
- name: Fallback to 'ipaserver'
ansible.builtin.set_fact:
ipa_server_name: ipaserver
when: ipa_server_name is not defined
- name: Ensure ipaserver_domain is set
when: ipaserver_domain is not defined
block:
- name: Get domain name from hostname.
ansible.builtin.set_fact:
@@ -27,7 +28,6 @@
- name: Fallback to 'ipa.test'
ansible.builtin.set_fact:
ipaserver_domain: "ipa.test"
when: ipaserver_domain is not defined
- name: Ensure server "{{ ipa_server_name + '.' + ipaserver_domain }}" without location
ipaserver:

View File

@@ -0,0 +1,200 @@
---
- name: Test service with certificates with and without trailing new line
hosts: ipaserver
become: true
tasks:
- name: Include tasks ../../env_freeipa_facts.yml
ansible.builtin.include_tasks: ../../env_freeipa_facts.yml
- name: Setup test environment
ansible.builtin.include_tasks: ../env_vars.yml
- name: Generate self-signed certificates.
ansible.builtin.shell:
cmd: |
openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout "private{{ item }}.key" -out "cert{{ item }}.pem" -subj '/CN=test'
openssl x509 -outform der -in "cert{{ item }}.pem" -out "cert{{ item }}.der"
base64 "cert{{ item }}.der" -w5000 > "cert{{ item }}.b64"
with_items: [1, 2, 3]
become: no
delegate_to: localhost
# The rstrip=False for lookup will add keep the newline at the end of the
# cert and this is automatically revoved in IPA, This is an additional
# test of ipaservice later on to behave correctly in both cases.
- name: Set fact cert1,2,3 from lookup
ansible.builtin.set_fact:
cert1: "{{ lookup('file', 'cert1.b64', rstrip=False) }}"
cert2: "{{ lookup('file', 'cert2.b64', rstrip=True) }}"
cert3: "{{ lookup('file', 'cert3.b64', rstrip=False) }}"
- name: Host {{ svc_fqdn }} absent
ipahost:
ipaadmin_password: SomeADMINpassword
name: "{{ svc_fqdn }}"
state: absent
- name: Host {{ svc_fqdn }} present
ipahost:
ipaadmin_password: SomeADMINpassword
name: "{{ svc_fqdn }}"
force: true
register: result
failed_when: not result.changed or result.failed
- name: Service FOO/{{ svc_fqdn }} absent
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: "FOO/{{ svc_fqdn }}"
continue: true
state: absent
- name: Service FOO/{{ svc_fqdn }} present
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: "FOO/{{ svc_fqdn }}"
force: yes
register: result
failed_when: not result.changed or result.failed
- name: Service FOO/{{ svc_fqdn }} certs 1,2 members present
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: "FOO/{{ svc_fqdn }}"
certificate:
- "{{ cert1 }}"
- "{{ cert2 }}"
action: member
register: result
failed_when: not result.changed or result.failed
- name: Service FOO/{{ svc_fqdn }} certs 1,2 members present again
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: "FOO/{{ svc_fqdn }}"
certificate:
- "{{ cert1 }}"
- "{{ cert2 }}"
action: member
register: result
failed_when: result.changed or result.failed
- name: Service FOO/{{ svc_fqdn }} certs 1,2,3 members present
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: "FOO/{{ svc_fqdn }}"
certificate:
- "{{ cert1 }}"
- "{{ cert2 }}"
- "{{ cert3 }}"
action: member
register: result
failed_when: not result.changed or result.failed
- name: Service FOO/{{ svc_fqdn }} certs 1,2,3 members present again
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: "FOO/{{ svc_fqdn }}"
certificate:
- "{{ cert1 }}"
- "{{ cert2 }}"
- "{{ cert3 }}"
action: member
register: result
failed_when: result.changed or result.failed
- name: Service FOO/{{ svc_fqdn }} certs 2,3 member absent
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: "FOO/{{ svc_fqdn }}"
certificate:
- "{{ cert2 }}"
- "{{ cert3 }}"
state: absent
action: member
register: result
failed_when: not result.changed or result.failed
- name: Service FOO/{{ svc_fqdn }} certs 2,3 member absent again
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: "FOO/{{ svc_fqdn }}"
certificate:
- "{{ cert2 }}"
- "{{ cert3 }}"
action: member
state: absent
register: result
failed_when: result.changed or result.failed
- name: Service FOO/{{ svc_fqdn }} certs 1,2,3 members absent
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: "FOO/{{ svc_fqdn }}"
certificate:
- "{{ cert1 }}"
- "{{ cert2 }}"
- "{{ cert3 }}"
action: member
state: absent
register: result
failed_when: not result.changed or result.failed
- name: Service FOO/{{ svc_fqdn }} certs 1,2,3 members absent again
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: "FOO/{{ svc_fqdn }}"
certificate:
- "{{ cert1 }}"
- "{{ cert2 }}"
- "{{ cert3 }}"
action: member
state: absent
register: result
failed_when: result.changed or result.failed
- name: Service FOO/{{ svc_fqdn }} absent
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: "FOO/{{ svc_fqdn }}"
continue: true
state: absent
register: result
failed_when: not result.changed or result.failed
- name: Service FOO/{{ svc_fqdn }} absent again
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: "FOO/{{ svc_fqdn }}"
continue: true
state: absent
register: result
failed_when: result.changed or result.failed
- name: Host {{ svc_fqdn }} absent
ipahost:
ipaadmin_password: SomeADMINpassword
name: "{{ svc_fqdn }}"
state: absent
register: result
failed_when: not result.changed or result.failed
- name: Remove certificate files. # noqa: deprecated-command-syntax
ansible.builtin.shell:
cmd: rm -f "private{{ item }}.key" "cert{{ item }}.pem" "cert{{ item }}.der" "cert{{ item }}.b64"
with_items: [1, 2, 3]
become: no
delegate_to: localhost

Some files were not shown because too many files have changed in this diff Show More