diff --git a/plugins/modules/ipaservice.py b/plugins/modules/ipaservice.py index a6fa9cb129fb6a1bef3a8599d39579850c4402f8..209a031115f66db594a1d18a7c3f4983ffad4509 100644 --- a/plugins/modules/ipaservice.py +++ b/plugins/modules/ipaservice.py @@ -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 = [] diff --git a/tests/service/certificate/test_service_certificate_newline.yml b/tests/service/certificate/test_service_certificate_newline.yml new file mode 100644 index 0000000000000000000000000000000000000000..01f7c536d6ab2705510fc42c37d22c6004255e26 --- /dev/null +++ b/tests/service/certificate/test_service_certificate_newline.yml @@ -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 diff --git a/tests/service/certificate/test_services_certificate_newline.yml b/tests/service/certificate/test_services_certificate_newline.yml new file mode 100644 index 0000000000000000000000000000000000000000..33815a0155e80157b4f06e9253e8f8dd665aab11 --- /dev/null +++ b/tests/service/certificate/test_services_certificate_newline.yml @@ -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 diff --git a/tests/service/generate_test_data.yml b/tests/service/generate_test_data.yml new file mode 100644 index 0000000000000000000000000000000000000000..b046617ffe85106fd20190b2e327e1a8e1d6a801 --- /dev/null +++ b/tests/service/generate_test_data.yml @@ -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 }}" diff --git a/tests/service/test_services_absent.yml b/tests/service/test_services_absent.yml new file mode 100644 index 0000000000000000000000000000000000000000..473cc8e64e2ebd0e24168cb2a9a88b8896e5d7d7 --- /dev/null +++ b/tests/service/test_services_absent.yml @@ -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 diff --git a/tests/service/test_services_present.yml b/tests/service/test_services_present.yml new file mode 100644 index 0000000000000000000000000000000000000000..338737eb238051403948b81a6d77c51b583e16e5 --- /dev/null +++ b/tests/service/test_services_present.yml @@ -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 diff --git a/tests/service/test_services_present_slice.yml b/tests/service/test_services_present_slice.yml new file mode 100644 index 0000000000000000000000000000000000000000..6a500a39d7c7761b3b056746bd5ef12594baf7d7 --- /dev/null +++ b/tests/service/test_services_present_slice.yml @@ -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 diff --git a/tests/service/test_services_without_skip_host_check.yml b/tests/service/test_services_without_skip_host_check.yml new file mode 100644 index 0000000000000000000000000000000000000000..0cb71281314e53caa836aeda2078590b1b571ef0 --- /dev/null +++ b/tests/service/test_services_without_skip_host_check.yml @@ -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