diff --git a/plugins/modules/ipaservice.py b/plugins/modules/ipaservice.py index a6fa9cb129fb6a1bef3a8599d39579850c4402f8..5046144ad6e89a81d6cdaa10d9d512e47631d838 100644 --- a/plugins/modules/ipaservice.py +++ b/plugins/modules/ipaservice.py @@ -45,6 +45,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 +360,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 +378,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 +454,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 +472,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 +491,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,6 +585,7 @@ def main(): # general names = ansible_module.params_get("name") + services = ansible_module.params_get("services") # service attributes principal = ansible_module.params_get("principal") @@ -462,8 +612,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 +638,39 @@ 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") + 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/create_services_json.yml b/tests/service/create_services_json.yml new file mode 100644 index 0000000000000000000000000000000000000000..197648b5fd6c2168ede761a384a0ca22faf66177 --- /dev/null +++ b/tests/service/create_services_json.yml @@ -0,0 +1,22 @@ +--- +- name: Create services.json + hosts: localhost + + tasks: + - name: Check if services.json exists + ansible.builtin.stat: + path: services.json + register: register_stat_services + + - name: Create services.json + ansible.builtin.command: /bin/bash services.sh 500 + when: not register_stat_services.stat.exists + + - name: Check if hosts.json exists + ansible.builtin.stat: + path: hosts.json + register: register_stat_hosts + + - name: Create hosts.json + ansible.builtin.command: /bin/bash hosts.sh 500 + when: not register_stat_hosts.stat.exists diff --git a/tests/service/hosts.sh b/tests/service/hosts.sh new file mode 100644 index 0000000000000000000000000000000000000000..2a64855c5158bb83b4c14e0659b448a393bc9ef6 --- /dev/null +++ b/tests/service/hosts.sh @@ -0,0 +1,24 @@ +#!/bin/bash -eu + +NUM=${1-1000} +FILE="hosts.json" + +echo "{" > "$FILE" + +echo " \"host_list\": [" >> "$FILE" + +for i in $(seq 1 "$NUM"); do + { + echo " {" + echo " \"name\": \"www.example$i.com\"" + } >> "$FILE" + if [ "$i" -lt "$NUM" ]; then + echo " }," >> "$FILE" + else + echo " }" >> "$FILE" + fi +done + +echo " ]" >> "$FILE" + +echo "}" >> "$FILE" diff --git a/tests/service/services.sh b/tests/service/services.sh new file mode 100644 index 0000000000000000000000000000000000000000..79f3b38acef44c3658c8a1ca5f0ea77141f7b261 --- /dev/null +++ b/tests/service/services.sh @@ -0,0 +1,25 @@ +#!/bin/bash -eu + +NUM=${1-1000} +FILE="services.json" + +echo "{" > "$FILE" + +echo " \"service_list\": [" >> "$FILE" + +for i in $(seq 1 "$NUM"); do + { + echo " {" + echo " \"name\": \"HTTP/www.example$i.com\"," + echo " \"principal\": \"host/test.example$i.com\"" + } >> "$FILE" + if [ "$i" -lt "$NUM" ]; then + echo " }," >> "$FILE" + else + echo " }" >> "$FILE" + fi +done + +echo " ]" >> "$FILE" + +echo "}" >> "$FILE" diff --git a/tests/service/services_absent.sh b/tests/service/services_absent.sh new file mode 100644 index 0000000000000000000000000000000000000000..80d0b796fabfdcea1e9d21f1321c35119bac9ed7 --- /dev/null +++ b/tests/service/services_absent.sh @@ -0,0 +1,22 @@ +#!/bin/bash -eu + +NUM=1000 +FILE="services_absent.json" + +echo "{" > "$FILE" + +echo " \"services\": [" >> "$FILE" + +for i in $(seq 1 "$NUM"); do + echo " {" >> "$FILE" + echo " \"name\": \"HTTP/www.example$i.com\"," >> "$FILE" + if [ "$i" -lt "$NUM" ]; then + echo " }," >> "$FILE" + else + echo " }" >> "$FILE" + fi +done + +echo " ]" >> "$FILE" + +echo "}" >> "$FILE" diff --git a/tests/service/test_services_absent.yml b/tests/service/test_services_absent.yml new file mode 100644 index 0000000000000000000000000000000000000000..a212ee24f17e6afd2c36a553e867d63a0521095e --- /dev/null +++ b/tests/service/test_services_absent.yml @@ -0,0 +1,32 @@ +--- +- name: Include create_services_json.yml + ansible.builtin.import_playbook: create_services_json.yml + +- name: Test services absent + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Include services.json + ansible.builtin.include_vars: + file: services.json # noqa 505 + + - name: Create dict with service names + ansible.builtin.set_fact: + services_names: "{{ services_names | default([]) + [{'name': item.name}] }}" + loop: "{{ service_list }}" + + - name: Services absent len:{{ service_list | length }} + ipaservice: + ipaadmin_password: SomeADMINpassword + services: "{{ services_names }}" + state: absent + +- name: Remove services.json + hosts: localhost + tasks: + - name: Remove services.json + ansible.builtin.file: + state: absent + path: services.json diff --git a/tests/service/test_services_present.yml b/tests/service/test_services_present.yml new file mode 100644 index 0000000000000000000000000000000000000000..b8491fb717b0aaa63c2fa1e43156bf3a4fe3d73f --- /dev/null +++ b/tests/service/test_services_present.yml @@ -0,0 +1,39 @@ +--- +- name: Include create_services_json.yml + ansible.builtin.import_playbook: create_services_json.yml + +- name: Test services present + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Include services.json + ansible.builtin.include_vars: + file: services.json # noqa 505 + + - name: Include hosts.json + ansible.builtin.include_vars: + file: hosts.json # noqa 505 + + - name: Hosts present len:{{ host_list | length }} + ipahost: + ipaadmin_password: SomeADMINpassword + hosts: "{{ host_list }}" + + - name: Services present len:{{ service_list | length }} + ipaservice: + ipaadmin_password: SomeADMINpassword + services: "{{ service_list }}" + +- name: Remove services.json + hosts: localhost + tasks: + - name: Remove services.json + ansible.builtin.file: + state: absent + path: services.json + - name: Remove hosts.json + ansible.builtin.file: + state: absent + path: hosts.json diff --git a/tests/service/test_services_present_slice.yml b/tests/service/test_services_present_slice.yml new file mode 100644 index 0000000000000000000000000000000000000000..d9e7055d3e6e36c30b091338d52794db10a97b36 --- /dev/null +++ b/tests/service/test_services_present_slice.yml @@ -0,0 +1,46 @@ +--- +- name: Include create_services_json.yml + ansible.builtin.import_playbook: create_services_json.yml + +- name: Test services present slice + hosts: ipaserver + become: true + gather_facts: false + + vars: + slice_size: 500 + tasks: + - name: Include services.json + ansible.builtin.include_vars: + file: services.json # noqa 505 + - name: Include hosts.json + ansible.builtin.include_vars: + file: hosts.json # noqa 505 + - name: Size of services slice. + ansible.builtin.debug: + msg: "{{ service_list | length }}" + - name: Size of hosts slice. + ansible.builtin.debug: + msg: "{{ host_list | length }}" + - name: Hosts present + ipahost: + ipaadmin_password: SomeADMINpassword + hosts: "{{ host_list[item : item + slice_size] }}" + loop: "{{ range(0, service_list | length, slice_size) | list }}" + - name: Services present + ipaservice: + ipaadmin_password: SomeADMINpassword + services: "{{ service_list[item : item + slice_size] }}" + loop: "{{ range(0, service_list | length, slice_size) | list }}" + +- name: Remove services.json + hosts: localhost + tasks: + - name: Remove services.json + ansible.builtin.file: + state: absent + path: services.json + - name: Remove hosts.json + ansible.builtin.file: + state: absent + path: hosts.json 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