Compare commits

...

45 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
77 changed files with 3017 additions and 326 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

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

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

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

@@ -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'.")

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

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

@@ -241,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")
@@ -254,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")

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

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

@@ -211,6 +211,7 @@ options:
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
@@ -1054,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:
@@ -1067,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

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

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

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/bash -eu
NUM=${1-1000}
FILE="groups.json"

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,55 +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/group/groups.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,37 +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/group/groups.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,37 +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/group/groups.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

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

View File

@@ -0,0 +1,314 @@
---
- name: Test services 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: [11, 12, 13, 21, 22, 23, 31, 32, 33]
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 for certs 11,12,13,21,22,23,31,32,33 from lookup
ansible.builtin.set_fact:
cert11: "{{ lookup('file', 'cert11.b64', rstrip=True) }}"
cert12: "{{ lookup('file', 'cert12.b64', rstrip=False) }}"
cert13: "{{ lookup('file', 'cert13.b64', rstrip=True) }}"
cert21: "{{ lookup('file', 'cert21.b64', rstrip=False) }}"
cert22: "{{ lookup('file', 'cert22.b64', rstrip=False) }}"
cert23: "{{ lookup('file', 'cert23.b64', rstrip=True) }}"
cert31: "{{ lookup('file', 'cert31.b64', rstrip=False) }}"
cert32: "{{ lookup('file', 'cert32.b64', rstrip=True) }}"
cert33: "{{ lookup('file', 'cert33.b64', rstrip=False) }}"
- name: Services FOO,BAR,BAZ/{{ svc_fqdn }} absent
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name:
- "FOO/{{ svc_fqdn }}"
- "BAR/{{ svc_fqdn }}"
- "BAZ/{{ svc_fqdn }}"
continue: true
state: absent
- 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: Services FOO,BAR,BAZ/{{ svc_fqdn }} present
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
services:
- name: "FOO/{{ svc_fqdn }}"
force: yes
- name: "BAR/{{ svc_fqdn }}"
force: yes
- name: "BAZ/{{ svc_fqdn }}"
force: yes
register: result
failed_when: not result.changed or result.failed
- name: Services FOO,BAR,BAZ/{{ svc_fqdn }} present
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
services:
- name: "FOO/{{ svc_fqdn }}"
force: yes
- name: "BAR/{{ svc_fqdn }}"
force: yes
- name: "BAZ/{{ svc_fqdn }}"
force: yes
register: result
failed_when: result.changed or result.failed
- name: Service FOO,BAR,BAZ/{{ svc_fqdn }} certs x1,x2 members present
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
services:
- name: "FOO/{{ svc_fqdn }}"
certificate:
- "{{ cert11 }}"
- "{{ cert12 }}"
- name: "BAR/{{ svc_fqdn }}"
certificate:
- "{{ cert21 }}"
- "{{ cert22 }}"
- name: "BAZ/{{ svc_fqdn }}"
certificate:
- "{{ cert31 }}"
- "{{ cert32 }}"
action: member
register: result
failed_when: not result.changed or result.failed
- name: Service FOO,BAR,BAZ/{{ svc_fqdn }} certs x1,x2 members present again
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
services:
- name: "FOO/{{ svc_fqdn }}"
certificate:
- "{{ cert11 }}"
- "{{ cert12 }}"
- name: "BAR/{{ svc_fqdn }}"
certificate:
- "{{ cert21 }}"
- "{{ cert22 }}"
- name: "BAZ/{{ svc_fqdn }}"
certificate:
- "{{ cert31 }}"
- "{{ cert32 }}"
action: member
register: result
failed_when: result.changed or result.failed
- name: Service FOO,BAR,BAZ/{{ svc_fqdn }} certs x1,x2,x3 members present
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
services:
- name: "FOO/{{ svc_fqdn }}"
certificate:
- "{{ cert11 }}"
- "{{ cert12 }}"
- "{{ cert13 }}"
- name: "BAR/{{ svc_fqdn }}"
certificate:
- "{{ cert21 }}"
- "{{ cert22 }}"
- "{{ cert23 }}"
- name: "BAZ/{{ svc_fqdn }}"
certificate:
- "{{ cert31 }}"
- "{{ cert32 }}"
- "{{ cert33 }}"
action: member
register: result
failed_when: not result.changed or result.failed
- name: Service FOO,BAR,BAZ/{{ svc_fqdn }} certs x1,x2,x3 members present again
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
services:
- name: "FOO/{{ svc_fqdn }}"
certificate:
- "{{ cert11 }}"
- "{{ cert12 }}"
- "{{ cert13 }}"
- name: "BAR/{{ svc_fqdn }}"
certificate:
- "{{ cert21 }}"
- "{{ cert22 }}"
- "{{ cert23 }}"
- name: "BAZ/{{ svc_fqdn }}"
certificate:
- "{{ cert31 }}"
- "{{ cert32 }}"
- "{{ cert33 }}"
action: member
register: result
failed_when: result.changed or result.failed
- name: Service FOO,BAR,BAZ/{{ svc_fqdn }} certs x2,x3 members absent
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
services:
- name: "FOO/{{ svc_fqdn }}"
certificate:
- "{{ cert12 }}"
- "{{ cert13 }}"
- name: "BAR/{{ svc_fqdn }}"
certificate:
- "{{ cert22 }}"
- "{{ cert23 }}"
- name: "BAZ/{{ svc_fqdn }}"
certificate:
- "{{ cert32 }}"
- "{{ cert33 }}"
action: member
state: absent
register: result
failed_when: not result.changed or result.failed
- name: Service FOO,BAR,BAZ/{{ svc_fqdn }} certs x2,x3 members absent, again
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
services:
- name: "FOO/{{ svc_fqdn }}"
certificate:
- "{{ cert12 }}"
- "{{ cert13 }}"
- name: "BAR/{{ svc_fqdn }}"
certificate:
- "{{ cert22 }}"
- "{{ cert23 }}"
- name: "BAZ/{{ svc_fqdn }}"
certificate:
- "{{ cert32 }}"
- "{{ cert33 }}"
action: member
state: absent
register: result
failed_when: result.changed or result.failed
- name: Service FOO,BAR,BAZ/{{ svc_fqdn }} certs x1,x2,x3 members absent
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
services:
- name: "FOO/{{ svc_fqdn }}"
certificate:
- "{{ cert11 }}"
- "{{ cert12 }}"
- "{{ cert13 }}"
- name: "BAR/{{ svc_fqdn }}"
certificate:
- "{{ cert21 }}"
- "{{ cert22 }}"
- "{{ cert23 }}"
- name: "BAZ/{{ svc_fqdn }}"
certificate:
- "{{ cert31 }}"
- "{{ cert32 }}"
- "{{ cert33 }}"
action: member
state: absent
register: result
failed_when: not result.changed or result.failed
- name: Service FOO,BAR,BAZ/{{ svc_fqdn }} certs x1,x2,x3 members absent, again
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
services:
- name: "FOO/{{ svc_fqdn }}"
certificate:
- "{{ cert11 }}"
- "{{ cert12 }}"
- "{{ cert13 }}"
- name: "BAR/{{ svc_fqdn }}"
certificate:
- "{{ cert21 }}"
- "{{ cert22 }}"
- "{{ cert23 }}"
- name: "BAZ/{{ svc_fqdn }}"
certificate:
- "{{ cert31 }}"
- "{{ cert32 }}"
- "{{ cert33 }}"
action: member
state: absent
register: result
failed_when: result.changed or result.failed
- name: Services FOO,BAR,BAZ/{{ svc_fqdn }} absent
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name:
- "FOO/{{ svc_fqdn }}"
- "BAR/{{ svc_fqdn }}"
- "BAZ/{{ svc_fqdn }}"
continue: true
state: absent
register: result
failed_when: not result.changed or result.failed
- name: Services FOO,BAR,BAZ/{{ svc_fqdn }} absent, again
ipaservice:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name:
- "FOO/{{ svc_fqdn }}"
- "BAR/{{ svc_fqdn }}"
- "BAZ/{{ 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: [11, 12, 13, 21, 22, 23, 31, 32, 33]
become: no
delegate_to: localhost

View File

@@ -0,0 +1,98 @@
# Generate lists for hosts and services
---
- name: Get Domain from server name
ansible.builtin.set_fact:
ipaserver_domain: "{{ ansible_facts['fqdn'].split('.')[1:] | join('.') }}"
when: ipaserver_domain is not defined
- name: Create present services.json data
ansible.builtin.shell: |
echo "["
for i in $(seq 1 "{{ NUM }}"); do
echo " {"
echo " \"name\": \"HTTP/www$i.{{ DOMAIN }}\","
echo " \"principal\": \"host/test$i.{{ DOMAIN }}\","
echo " \"force\": \"true\""
if [ "$i" -lt "{{ NUM }}" ]; then
echo " },"
else
echo " }"
fi
done
echo "]"
vars:
NUM: 500
DOMAIN: "{{ ipaserver_domain }}"
register: command
- name: Set service_list
ansible.builtin.set_fact:
service_list: "{{ command.stdout | from_json }}"
- name: Create absent services.json data
ansible.builtin.shell: |
echo "["
for i in $(seq 1 "{{ NUM }}"); do
echo " {"
echo " \"name\": \"HTTP/www$i.{{ DOMAIN }}\","
echo " \"continue\": \"true\""
if [ "$i" -lt "{{ NUM }}" ]; then
echo " },"
else
echo " }"
fi
done
echo "]"
vars:
NUM: 500
DOMAIN: "{{ ipaserver_domain }}"
register: command
- name: Set service_absent_list
ansible.builtin.set_fact:
service_absent_list: "{{ command.stdout | from_json }}"
- name: Create present hosts.json data
ansible.builtin.shell: |
echo "["
for i in $(seq 1 "{{ NUM }}"); do
echo " {"
echo " \"name\": \"www$i.{{ DOMAIN }}\","
echo " \"force\": \"true\""
if [ "$i" -lt "{{ NUM }}" ]; then
echo " },"
else
echo " }"
fi
done
echo "]"
vars:
NUM: 500
DOMAIN: "{{ ipaserver_domain }}"
register: command
- name: Set host_list
ansible.builtin.set_fact:
host_list: "{{ command.stdout | from_json }}"
- name: Create absent hosts.json data
ansible.builtin.shell: |
echo "["
for i in $(seq 1 "{{ NUM }}"); do
echo " {"
echo " \"name\": \"www$i.{{ DOMAIN }}\""
if [ "$i" -lt "{{ NUM }}" ]; then
echo " },"
else
echo " }"
fi
done
echo "]"
vars:
NUM: 500
DOMAIN: "{{ ipaserver_domain }}"
register: command
- name: Set host_absent_list
ansible.builtin.set_fact:
host_absent_list: "{{ command.stdout | from_json }}"

View File

@@ -0,0 +1,15 @@
---
- name: Test services absent
hosts: ipaserver
become: true
gather_facts: false
tasks:
- name: Include generate_test_data.yml
ansible.builtin.include_tasks: generate_test_data.yml
- name: Services absent len:{{ service_list | length }}
ipaservice:
ipaadmin_password: SomeADMINpassword
services: "{{ service_absent_list }}"
state: absent

View File

@@ -0,0 +1,71 @@
---
- name: Test services present
hosts: ipaserver
become: true
gather_facts: true
tasks:
- name: Include generate_test_data.yml
ansible.builtin.include_tasks: generate_test_data.yml
- name: Hosts present len:{{ host_list | length }}
ipahost:
ipaadmin_password: SomeADMINpassword
hosts: "{{ host_list }}"
force: true
register: result
failed_when: not result.changed or result.failed
- name: Hosts present len:{{ host_list | length }}, again
ipahost:
ipaadmin_password: SomeADMINpassword
hosts: "{{ host_list }}"
force: true
register: result
failed_when: result.changed or result.failed
- name: Services present len:{{ service_list | length }}
ipaservice:
ipaadmin_password: SomeADMINpassword
services: "{{ service_list }}"
register: result
failed_when: not result.changed or result.failed
- name: Services present len:{{ service_list | length }}, again
ipaservice:
ipaadmin_password: SomeADMINpassword
services: "{{ service_list }}"
register: result
failed_when: result.changed or result.failed
- name: Services absent len:{{ service_list | length }}
ipaservice:
ipaadmin_password: SomeADMINpassword
services: "{{ service_absent_list }}"
state: absent
register: result
failed_when: not result.changed or result.failed
- name: Services absent len:{{ service_list | length }}, again
ipaservice:
ipaadmin_password: SomeADMINpassword
services: "{{ service_absent_list }}"
state: absent
register: result
failed_when: result.changed or result.failed
- name: Hosts absent len:{{ host_list | length }}
ipahost:
ipaadmin_password: SomeADMINpassword
hosts: "{{ host_absent_list }}"
state: absent
register: result
failed_when: not result.changed or result.failed
- name: Hosts absent len:{{ host_list | length }}, again
ipahost:
ipaadmin_password: SomeADMINpassword
hosts: "{{ host_absent_list }}"
state: absent
register: result
failed_when: result.changed or result.failed

View File

@@ -0,0 +1,91 @@
---
- name: Test services present slice
hosts: ipaserver
become: true
gather_facts: true
vars:
slice_size: 100
tasks:
- name: Include generate_test_data.yml
ansible.builtin.include_tasks: generate_test_data.yml
- name: Size of slice
ansible.builtin.debug:
msg: "{{ slice_size }}"
- name: Size of services list
ansible.builtin.debug:
msg: "{{ service_list | length }}"
- name: Size of hosts list
ansible.builtin.debug:
msg: "{{ host_list | length }}"
- name: Hosts present
ipahost:
ipaadmin_password: SomeADMINpassword
hosts: "{{ host_list[item : item + slice_size] }}"
loop: "{{ range(0, host_list | length, slice_size) | list }}"
register: result
failed_when: not result.changed or result.failed
- name: Hosts present, again
ipahost:
ipaadmin_password: SomeADMINpassword
hosts: "{{ host_list[item : item + slice_size] }}"
loop: "{{ range(0, host_list | length, slice_size) | list }}"
register: result
failed_when: result.changed or result.failed
- name: Services present
ipaservice:
ipaadmin_password: SomeADMINpassword
services: "{{ service_list[item : item + slice_size] }}"
loop: "{{ range(0, service_list | length, slice_size) | list }}"
register: result
failed_when: not result.changed or result.failed
- name: Services present, again
ipaservice:
ipaadmin_password: SomeADMINpassword
services: "{{ service_list[item : item + slice_size] }}"
loop: "{{ range(0, service_list | length, slice_size) | list }}"
register: result
failed_when: result.changed or result.failed
- name: Services absent
ipaservice:
ipaadmin_password: SomeADMINpassword
services: "{{ service_absent_list[item : item + slice_size] }}"
state: absent
loop: "{{ range(0, service_absent_list | length, slice_size) | list }}"
register: result
failed_when: not result.changed or result.failed
- name: Services absent, again
ipaservice:
ipaadmin_password: SomeADMINpassword
services: "{{ service_absent_list[item : item + slice_size] }}"
state: absent
loop: "{{ range(0, service_absent_list | length, slice_size) | list }}"
register: result
failed_when: result.changed or result.failed
- name: Hosts absent
ipahost:
ipaadmin_password: SomeADMINpassword
hosts: "{{ host_absent_list[item : item + slice_size] }}"
state: absent
loop: "{{ range(0, host_absent_list | length, slice_size) | list }}"
register: result
failed_when: not result.changed or result.failed
- name: Hosts absent, again
ipahost:
ipaadmin_password: SomeADMINpassword
hosts: "{{ host_absent_list[item : item + slice_size] }}"
state: absent
loop: "{{ range(0, host_absent_list | length, slice_size) | list }}"
register: result
failed_when: result.changed or result.failed

View File

@@ -0,0 +1,100 @@
---
- name: Test services without using option skip_host_check
hosts: ipaserver
become: true
tasks:
# setup
- name: Test services without using option skip_host_check
block:
- name: Setup test environment
ansible.builtin.include_tasks: env_setup.yml
- name: Services are present
ipaservice:
ipaadmin_password: SomeADMINpassword
services:
- name: "HTTP/{{ svc_fqdn }}"
principal:
- host/test.example.com
- name: "mysvc/{{ host1_fqdn }}"
pac_type: NONE
ok_as_delegate: yes
ok_to_auth_as_delegate: yes
- name: "HTTP/{{ host1_fqdn }}"
allow_create_keytab_user:
- user01
- user02
allow_create_keytab_group:
- group01
- group02
allow_create_keytab_host:
- "{{ host1_fqdn }}"
- "{{ host2_fqdn }}"
allow_create_keytab_hostgroup:
- hostgroup01
- hostgroup02
- name: "mysvc/{{ host2_fqdn }}"
auth_ind: otp,radius
register: result
failed_when: not result.changed or result.failed
- name: Services are present again
ipaservice:
ipaadmin_password: SomeADMINpassword
services:
- name: "HTTP/{{ svc_fqdn }}"
- name: "mysvc/{{ host1_fqdn }}"
- name: "HTTP/{{ host1_fqdn }}"
- name: "mysvc/{{ host2_fqdn }}"
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 services with the same name should be added in the same
# task.
- name: Duplicate names in services failure test
ipaservice:
ipaadmin_password: SomeADMINpassword
services:
- name: "HTTP/{{ svc_fqdn }}"
- name: "mysvc/{{ host1_fqdn }}"
- name: "HTTP/{{ nohost_fqdn }}"
- name: "HTTP/{{ svc_fqdn }}"
register: result
failed_when: result.changed or not result.failed or "is used more than once" not in result.msg
- name: Services/name and name 'service' present
ipaservice:
ipaadmin_password: SomeADMINpassword
name: "HTTP/{{ svc_fqdn }}"
services:
- name: "HTTP/{{ svc_fqdn }}"
register: result
failed_when: result.changed or not result.failed or "parameters are mutually exclusive" not in result.msg
- name: Services/name and name are absent
ipaservice:
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
ipaservice:
ipaadmin_password: SomeADMINpassword
name:
register: result
failed_when: result.changed or not result.failed or "At least one name or services is required" not in result.msg
- name: Only one service can be added at a time using name.
ipaservice:
ipaadmin_password: SomeADMINpassword
name: example.com,example1.com
register: result
failed_when: result.changed or not result.failed or "Only one service can be added at a time using 'name'." not in result.msg
always:
# cleanup
- name: Cleanup test environment
ansible.builtin.include_tasks: env_cleanup.yml

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/bash -eu
NUM=${1-1000}
FILE="users.json"

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/bash -eu
NUM=1000
FILE="users_absent.json"

View File

@@ -180,7 +180,7 @@ def run_playbook(playbook, allow_failures=False):
if allow_failures:
return result
status_code_msg = "ansible-playbook return code: {}".format(
status_code_msg = "ansible-playbook return code: {0}".format(
result.returncode
)
assert_msg = "\n".join(

View File

@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Authors:

View File

@@ -125,7 +125,7 @@ done
for i in utils/*.py utils/new_module utils/changelog utils/ansible-doc-test;
do
sed -i '{s@/usr/bin/python*@%{python}@}' $i
sed -i '{s@/usr/bin/env python*@%{python}@}' $i
done

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/bash -eu
#
# Build Ansible Collection from ansible-freeipa repo
#
@@ -114,6 +114,8 @@ echo -e "\033[ACreating CHANGELOG.rst... \033[32;1mDONE\033[0m"
sed -i -e "s/ansible.module_utils.ansible_freeipa_module/ansible_collections.${collection_prefix}.plugins.module_utils.ansible_freeipa_module/" plugins/modules/*.py
python utils/create_action_group.py "meta/runtime.yml" "$collection_prefix"
(cd plugins/module_utils && {
ln -sf ../../roles/*/module_utils/*.py .
})

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/bash -eu
git_version=$(git describe --tags | sed -e "s/^v//")
version=${git_version%%-*}

View File

@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Authors:

View File

@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/env python
"""Check which tests are scheduled to be executed."""

View File

@@ -0,0 +1,24 @@
import sys
import yaml
from facts import MANAGEMENT_MODULES
def create_action_group(yml_file, project_prefix):
yaml_data = None
with open(yml_file) as f_in:
yaml_data = yaml.safe_load(f_in)
yaml_data.setdefault("action_groups", {})[
"%s.modules" % project_prefix
] = MANAGEMENT_MODULES
with open(yml_file, 'w') as f_out:
yaml.safe_dump(yaml_data, f_out, default_flow_style=False,
explicit_start=True)
if len(sys.argv) != 3:
print("Usage: %s <runtime file> <collection prefix>" % sys.argv[0])
sys.exit(-1)
create_action_group(sys.argv[1], sys.argv[2])

41
utils/facts.py Normal file
View File

@@ -0,0 +1,41 @@
import os
def get_roles(dir):
roles = []
_rolesdir = "%s/roles/" % dir
for _role in os.listdir(_rolesdir):
_roledir = "%s/%s" % (_rolesdir, _role)
if not os.path.isdir(_roledir) or \
not os.path.isdir("%s/meta" % _roledir) or \
not os.path.isdir("%s/tasks" % _roledir):
continue
roles.append(_role)
return sorted(roles)
def get_modules(dir):
management_modules = []
roles_modules = []
for root, _dirs, files in os.walk(dir):
if not root.startswith("%s/plugins/" % dir) and \
not root.startswith("%s/roles/" % dir):
continue
for _file in files:
if _file.endswith(".py"):
if root == "%s/plugins/modules" % dir:
management_modules.append(_file[:-3])
elif root.startswith("%s/roles/" % dir):
if root.endswith("/library"):
roles_modules.append(_file[:-3])
return sorted(management_modules), sorted(roles_modules)
BASE_DIR = os.path.abspath(os.path.dirname(__file__) + "/..")
ROLES = get_roles(BASE_DIR)
MANAGEMENT_MODULES, ROLES_MODULES = get_modules(BASE_DIR)
ALL_MODULES = sorted(MANAGEMENT_MODULES + ROLES_MODULES)

View File

@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Authors:

View File

@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Authors:

View File

@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Authors:

View File

@@ -1,10 +1,10 @@
#!/usr/bin/python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Authors:
# Thomas Woerner <twoerner@redhat.com>
#
# Copyright (C) 2019,2020 Red Hat
# Copyright (C) 2019-2023 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
@@ -21,49 +21,95 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
from facts import ROLES, ALL_MODULES
def get_indent(txt):
return len(txt) - len(txt.lstrip())
def galaxyfy_playbook(project_prefix, collection_prefix, lines):
po1 = re.compile('(%s.*:)$' % project_prefix)
po2 = re.compile('(.*:) (%s.*)$' % project_prefix)
po_module = re.compile('(%s.*):$' % project_prefix)
po_module_arg = re.compile('(%s.*): (.*)$' % project_prefix)
po_module_unnamed = re.compile('- (%s.*):$' % project_prefix)
po_role = re.compile('(.*:) (%s.*)$' % project_prefix)
pattern_module = r'%s.\1:' % collection_prefix
pattern_module_arg = r'%s.\1: \2' % collection_prefix
pattern_module_unnamed = r'- %s.\1:' % collection_prefix
pattern_role = r'\1 %s.\2' % collection_prefix
out_lines = []
pattern1 = r'%s.\1' % collection_prefix
pattern2 = r'\1 %s.\2' % collection_prefix
changed = False
changeable = False
include_role = False
module_defaults = False
module_defaults_indent = -1
for line in lines:
stripped = line.strip()
if stripped.startswith("- name:") or \
stripped.startswith("- block:"):
changeable = True
module_defaults = False
module_defaults_indent = -1
elif stripped in ["set_fact:", "ansible.builtin.set_fact:", "vars:"]:
changeable = False
include_role = False
module_defaults = False
module_defaults_indent = -1
elif stripped == "roles:":
changeable = True
include_role = False
module_defaults = False
module_defaults_indent = -1
elif (stripped.startswith("include_role:") or
stripped.startswith("ansible.builtin.include_role:")):
include_role = True
module_defaults = False
module_defaults_indent = -1
elif include_role and stripped.startswith("name:"):
line = po2.sub(pattern2, line)
changed = True
match = po_role.search(line)
if match and match.group(2) in ROLES:
line = po_role.sub(pattern_role, line)
changed = True
elif stripped == "module_defaults:":
changeable = True
include_role = False
module_defaults = True
module_defaults_indent = -1
elif module_defaults:
_indent = get_indent(line)
if module_defaults_indent == -1:
module_defaults_indent = _indent
if _indent == module_defaults_indent:
# only module, no YAML anchor or alias
match = po_module.search(line)
if match and match.group(1) in ALL_MODULES:
line = po_module.sub(pattern_module, line)
changed = True
# module with YAML anchor or alias
match = po_module_arg.search(line)
if match and match.group(1) in ALL_MODULES:
line = po_module_arg.sub(pattern_module_arg, line)
changed = True
elif changeable and stripped.startswith("- role:"):
line = po2.sub(pattern2, line)
changed = True
match = po_role.search(line)
if match and match.group(2) in ROLES:
line = po_role.sub(pattern_role, line)
changed = True
elif (changeable and stripped.startswith(project_prefix)
and not stripped.startswith(collection_prefix) # noqa
and stripped.endswith(":")): # noqa
line = po1.sub(pattern1, line)
changed = True
changeable = False # Only change first line in task
match = po_module.search(line)
if match and match.group(1) in ALL_MODULES:
line = po_module.sub(pattern_module, line)
changed = True
changeable = False # Only change first line in task
elif (stripped.startswith("- %s" % project_prefix)
and stripped.endswith(":")): # noqa
line = po1.sub(pattern1, line)
changed = True
match = po_module_unnamed.search(line)
if match and match.group(1) in ALL_MODULES:
line = po_module_unnamed.sub(pattern_module_unnamed, line)
changed = True
out_lines.append(line)

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/bash -eu
for i in roles/ipa*/*/*.py; do
python utils/gen_module_docs.py "$i"

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/bash -eu
INFO="\033[37;1m"
WARN="\033[33;1m"

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/bash -eu
# -*- coding: utf-8 -*-
# Authors:
@@ -63,7 +63,7 @@ for (( i=0; i<OPTIND-1; i++)); do
shift
done
if [ ${#@} -ne 3 ]; then
if [ ${#@} -ne 4 ]; then
usage;
exit 1
fi
@@ -78,7 +78,7 @@ if [ -z "$name" ] || [ -z "$author" ] || [ -z "$email" ] || [ -z "$github_user"
[ -z "$name" ] && echo "ERROR: name is not valid"
[ -z "$author" ] && echo "ERROR: author is not valid"
[ -z "$email" ] && echo "ERROR: email is not valid"
[ -z "$githubuser" ] && echo "ERROR: github_user is not valid"
[ -z "$github_user" ] && echo "ERROR: github_user is not valid"
echo
usage;
exit 1;

View File

@@ -45,7 +45,7 @@ Example playbook to make sure $name "NAME" is present:
---
- name: Playbook to manage IPA $name.
hosts: ipaserver
become: no
become: false
tasks:
- ipa$name:
@@ -60,7 +60,7 @@ Example playbook to make sure $name "NAME" member PARAMETER2 VALUE is present:
---
- name: Playbook to manage IPA $name PARAMETER2 member.
hosts: ipaserver
become: no
become: false
tasks:
- ipa$name:
@@ -78,7 +78,7 @@ Example playbook to make sure $name "NAME" member PARAMETER2 VALUE is absent:
---
- name: Playbook to manage IPA $name PARAMETER2 member.
hosts: ipaserver
become: no
become: false
tasks:
- ipa$name:
@@ -96,7 +96,7 @@ Example playbook to make sure $name "NAME" is absent:
---
- name: Playbook to manage IPA $name.
hosts: ipaserver
become: no
become: false
tasks:
- ipa$name:
@@ -117,7 +117,7 @@ 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 true. (bool) | no
`name` \| `ALIAS` | The list of $name name strings. | yes
`PARAMETER1` \| `API_PARAMETER_NAME` | DESCRIPTION | TYPE
`PARAMETER2` \| `API_PARAMETER_NAME` | DESCRIPTION | TYPE

View File

@@ -45,7 +45,7 @@ Example playbook to make sure $name "NAME" is present:
---
- name: Playbook to manage IPA $name.
hosts: ipaserver
become: no
become: false
tasks:
- ipa$name:
@@ -61,7 +61,7 @@ Example playbook to make sure $name "NAME" is absent:
---
- name: Playbook to manage IPA $name.
hosts: ipaserver
become: no
become: false
tasks:
- ipa$name:
@@ -82,7 +82,7 @@ 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 true. (bool) | no
`name` \| `ALIAS` | The list of $name name strings. | yes
`PARAMETER1` \| `API_PARAMETER_NAME` | DESCRIPTION | TYPE
`PARAMETER2` \| `API_PARAMETER_NAME` | DESCRIPTION | TYPE

View File

@@ -121,7 +121,7 @@ def main():
argument_spec=dict(
# general
name=dict(type="list", elements="str", required=True,
aliases=["API_PARAMETER_NAME"],
aliases=["API_PARAMETER_NAME"]),
# present
PARAMETER1=dict(required=False, type='str',
aliases=["API_PARAMETER_NAME"], default=None),

View File

@@ -1,10 +1,15 @@
---
- name: Test $name
hosts: "{{ ipa_test_host | default('ipaserver') }}"
# Change "become" or "gather_facts" to "yes",
# if you test playbook requires any.
become: no
gather_facts: no
# It is normally not needed to set "become" to "true" for a module test.
# Only set it to true if it is needed to execute commands as root.
become: false
# Enable "gather_facts" only if "ansible_facts" variable needs to be used.
gather_facts: false
module_defaults:
ipa$name:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
tasks:
@@ -12,7 +17,6 @@
- name: Ensure $name NAME is absent
ipa$name:
ipaadmin_password: SomeADMINpassword
name: NAME
state: absent
@@ -22,8 +26,6 @@
- name: Ensure $name NAME is present
ipa$name:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: NAME
# Add needed parameters here
register: result
@@ -31,8 +33,6 @@
- name: Ensure $name NAME is present again
ipa$name:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: NAME
# Add needed parameters here
register: result
@@ -40,8 +40,6 @@
- name: Ensure $name NAME member PARAMETER2 VALUE is present
ipa$name:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: NAME
PARAMETER2: VALUE
action: member
@@ -50,8 +48,6 @@
- name: Ensure $name NAME member PARAMETER2 VALUE is present again
ipa$name:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: NAME
PARAMETER2: VALUE
action: member
@@ -60,8 +56,6 @@
- name: Ensure $name NAME member PARAMETER2 VALUE is absent
ipa$name:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: NAME
PARAMETER2: VALUE
action: member
@@ -71,8 +65,6 @@
- name: Ensure $name NAME member PARAMETER2 VALUE is absent again
ipa$name:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: NAME
PARAMETER2: VALUE
action: member
@@ -84,8 +76,6 @@
- name: Ensure $name NAME is absent
ipa$name:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: NAME
state: absent
register: result
@@ -93,8 +83,6 @@
- name: Ensure $name NAME is absent again
ipa$name:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: NAME
state: absent
register: result
@@ -104,7 +92,5 @@
- name: Ensure $name NAME is absent
ipa$name:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: NAME
state: absent

View File

@@ -1,10 +1,15 @@
---
- name: Test $name
hosts: "{{ ipa_test_host | default('ipaserver') }}"
# Change "become" or "gather_facts" to "yes",
# if you test playbook requires any.
become: no
gather_facts: no
# It is normally not needed to set "become" to "true" for a module test.
# Only set it to true if it is needed to execute commands as root.
become: false
# Enable "gather_facts" only if "ansible_facts" variable needs to be used.
gather_facts: false
module_defaults:
ipa$name:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
tasks:
@@ -12,8 +17,6 @@
- name: Ensure $name NAME is absent
ipa$name:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: NAME
state: absent
@@ -23,7 +26,6 @@
- name: Ensure $name NAME is present
ipa$name:
ipaadmin_password: SomeADMINpassword
name: NAME
# Add needed parameters here
register: result
@@ -31,8 +33,6 @@
- name: Ensure $name NAME is present again
ipa$name:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: NAME
# Add needed parameters here
register: result
@@ -42,8 +42,6 @@
- name: Ensure $name NAME is absent
ipa$name:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: NAME
state: absent
register: result
@@ -51,8 +49,6 @@
- name: Ensure $name NAME is absent again
ipa$name:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
name: NAME
state: absent
register: result
@@ -62,6 +58,5 @@
- name: Ensure $name NAME is absent
ipa$name:
ipaadmin_password: SomeADMINpassword
name: NAME
state: absent

View File

@@ -1,10 +1,11 @@
---
- name: Test ${name}
hosts: ipaclients, ipaserver
# Change "become" or "gather_facts" to "yes",
# if you test playbook requires any.
become: no
gather_facts: no
# It is normally not needed to set "become" to "true" for a module test.
# Only set it to true if it is needed to execute commands as root.
become: false
# Enable "gather_facts" only if "ansible_facts" variable needs to be used.
gather_facts: false
tasks:
- name: Include FreeIPA facts.