From 2939af7787d57313a0cecb9f45123ccfd3e182b1 Mon Sep 17 00:00:00 2001 From: Packit Service Date: Dec 09 2020 07:43:39 +0000 Subject: Apply patch ansible-freeipa-0.1.12-Add-support-for-option-name_from_ip-in-ipadnszone-mo_rhbz#1845056.patch patch_name: ansible-freeipa-0.1.12-Add-support-for-option-name_from_ip-in-ipadnszone-mo_rhbz#1845056.patch present_in_specfile: true --- diff --git a/README-dnszone.md b/README-dnszone.md index 9c9b12c..c5a7ab3 100644 --- a/README-dnszone.md +++ b/README-dnszone.md @@ -152,6 +152,46 @@ Example playbook to remove a zone: ``` +Example playbook to create a zone for reverse DNS lookup, from an IP address: + +```yaml + +--- +- name: dnszone present + hosts: ipaserver + become: true + + tasks: + - name: Ensure zone for reverse DNS lookup is present. + ipadnszone: + ipaadmin_password: SomeADMINpassword + name_from_ip: 192.168.1.2 + state: present +``` + +Note that, on the previous example the zone created with `name_from_ip` might be "1.168.192.in-addr.arpa.", "168.192.in-addr.arpa.", or "192.in-addr.arpa.", depending on the DNS response the system get while querying for zones, and for this reason, when creating a zone using `name_from_ip`, the inferred zone name is returned to the controller, in the attribute `dnszone.name`. Since the zone inferred might not be what a user expects, `name_from_ip` can only be used with `state: present`. To have more control over the zone name, the prefix length for the IP address can be provided. + +Example playbook to create a zone for reverse DNS lookup, from an IP address, given the prefix length and displaying the resulting zone name: + +```yaml + +--- +- name: dnszone present + hosts: ipaserver + become: true + + tasks: + - name: Ensure zone for reverse DNS lookup is present. + ipadnszone: + ipaadmin_password: SomeADMINpassword + name_from_ip: 192.168.1.2/24 + state: present + register: result + - name: Display inferred zone name. + debug: + msg: "Zone name: {{ result.dnszone.name }}" +``` + Variables ========= @@ -163,7 +203,8 @@ 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 -`name` \| `zone_name` | The zone name string or list of strings. | yes +`name` \| `zone_name` | The zone name string or list of strings. | no +`name_from_ip` | Derive zone name from reverse of IP (PTR). Can only be used with `state: present`. | no `forwarders` | The list of forwarders dicts. Each `forwarders` dict entry has:| no   | `ip_address` - The IPv4 or IPv6 address of the DNS server. | yes   | `port` - The custom port that should be used on this server. | no @@ -189,6 +230,17 @@ Variable | Description | Required `skip_nameserver_check` | Force DNS zone creation even if nameserver is not resolvable | no +Return Values +============= + +ipadnszone +---------- + +Variable | Description | Returned When +-------- | ----------- | ------------- +`dnszone` | DNS Zone dict with zone name infered from `name_from_ip`.
Options: | If `state` is `present`, `name_from_ip` is used, and a zone was created. +  | `name` - The name of the zone created, inferred from `name_from_ip`. | Always + Authors ======= diff --git a/playbooks/dnszone/dnszone-reverse-from-ip.yml b/playbooks/dnszone/dnszone-reverse-from-ip.yml new file mode 100644 index 0000000..218a318 --- /dev/null +++ b/playbooks/dnszone/dnszone-reverse-from-ip.yml @@ -0,0 +1,15 @@ +--- +- name: Playbook to ensure DNS zone exist + hosts: ipaserver + become: true + + tasks: + - name: Ensure zone exist, finding zone name from IP address. + ipadnszone: + ipaadmin_password: SomeADMINpassword + name_from_ip: 10.1.2.3/24 + register: result + + - name: Zone name inferred from `name_from_ip` + debug: + msg: "Zone created: {{ result.dnszone.name }}" diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py index 4799e5a..30302b4 100644 --- a/plugins/module_utils/ansible_freeipa_module.py +++ b/plugins/module_utils/ansible_freeipa_module.py @@ -619,7 +619,7 @@ class FreeIPABaseModule(AnsibleModule): if exc_val: self.fail_json(msg=str(exc_val)) - self.exit_json(changed=self.changed, user=self.exit_args) + self.exit_json(changed=self.changed, **self.exit_args) def get_command_errors(self, command, result): """Look for erros into command results.""" @@ -658,14 +658,22 @@ class FreeIPABaseModule(AnsibleModule): except Exception as excpt: self.fail_json(msg="%s: %s: %s" % (command, name, str(excpt))) else: - if "completed" in result: - if result["completed"] > 0: - self.changed = True - else: - self.changed = True - + self.process_command_result(name, command, args, result) self.get_command_errors(command, result) + def process_command_result(self, name, command, args, result): + """ + Process an API command result. + + This method can be overriden in subclasses, and change self.exit_values + to return data in the result for the controller. + """ + if "completed" in result: + if result["completed"] > 0: + self.changed = True + else: + self.changed = True + def require_ipa_attrs_change(self, command_args, ipa_attrs): """ Compare given args with current object attributes. diff --git a/plugins/modules/ipadnszone.py b/plugins/modules/ipadnszone.py index c5e812a..ff6bfff 100644 --- a/plugins/modules/ipadnszone.py +++ b/plugins/modules/ipadnszone.py @@ -43,6 +43,12 @@ options: required: true type: list alises: ["zone_name"] + name_from_ip: + description: | + Derive zone name from reverse of IP (PTR). + Can only be used with `state: present`. + required: false + type: str forwarders: description: The list of global DNS forwarders. required: false @@ -188,6 +194,14 @@ EXAMPLES = """ """ RETURN = """ +dnszone: + description: DNS Zone dict with zone name infered from `name_from_ip`. + returned: + If `state` is `present`, `name_from_ip` is used, and a zone was created. + options: + name: + description: The name of the zone created, inferred from `name_from_ip`. + returned: always """ from ipapython.dnsutil import DNSName # noqa: E402 @@ -197,6 +211,12 @@ from ansible.module_utils.ansible_freeipa_module import ( is_ipv6_addr, is_valid_port, ) # noqa: E402 +import netaddr +import six + + +if six.PY3: + unicode = str class DNSZoneModule(FreeIPABaseModule): @@ -354,6 +374,31 @@ class DNSZoneModule(FreeIPABaseModule): if not zone and self.ipa_params.skip_nameserver_check is not None: return self.ipa_params.skip_nameserver_check + def __reverse_zone_name(self, ipaddress): + """ + Infer reverse zone name from an ip address. + + This function uses the same heuristics as FreeIPA to infer the zone + name from ip. + """ + try: + ip = netaddr.IPAddress(str(ipaddress)) + except (netaddr.AddrFormatError, ValueError): + net = netaddr.IPNetwork(ipaddress) + items = net.ip.reverse_dns.split('.') + prefixlen = net.prefixlen + ip_version = net.version + else: + items = ip.reverse_dns.split('.') + prefixlen = 24 if ip.version == 4 else 64 + ip_version = ip.version + if ip_version == 4: + return u'.'.join(items[4 - prefixlen // 8:]) + elif ip_version == 6: + return u'.'.join(items[32 - prefixlen // 4:]) + else: + self.fail_json(msg="Invalid IP version for reverse zone.") + def get_zone(self, zone_name): get_zone_args = {"idnsname": zone_name, "all": True} response = self.api_command("dnszone_find", args=get_zone_args) @@ -368,14 +413,33 @@ class DNSZoneModule(FreeIPABaseModule): return zone, is_zone_active def get_zone_names(self): - if len(self.ipa_params.name) > 1 and self.ipa_params.state != "absent": + zone_names = self.__get_zone_names_from_params() + if len(zone_names) > 1 and self.ipa_params.state != "absent": self.fail_json( msg=("Please provide a single name. Multiple values for 'name'" "can only be supplied for state 'absent'.") ) + return zone_names + + def __get_zone_names_from_params(self): + if not self.ipa_params.name: + return [self.__reverse_zone_name(self.ipa_params.name_from_ip)] return self.ipa_params.name + def check_ipa_params(self): + if not self.ipa_params.name and not self.ipa_params.name_from_ip: + self.fail_json( + msg="Either `name` or `name_from_ip` must be provided." + ) + if self.ipa_params.state != "present" and self.ipa_params.name_from_ip: + self.fail_json( + msg=( + "Cannot use argument `name_from_ip` with state `%s`." + % self.ipa_params.state + ) + ) + def define_ipa_commands(self): for zone_name in self.get_zone_names(): # Look for existing zone in IPA @@ -418,6 +482,14 @@ class DNSZoneModule(FreeIPABaseModule): } self.add_ipa_command("dnszone_mod", zone_name, args) + def process_command_result(self, name, command, args, result): + super(DNSZoneModule, self).process_command_result( + name, command, args, result + ) + if command == "dnszone_add" and self.ipa_params.name_from_ip: + dnszone_exit_args = self.exit_args.setdefault('dnszone', {}) + dnszone_exit_args['name'] = name + def get_argument_spec(): forwarder_spec = dict( @@ -434,8 +506,9 @@ def get_argument_spec(): ipaadmin_principal=dict(type="str", default="admin"), ipaadmin_password=dict(type="str", required=False, no_log=True), name=dict( - type="list", default=None, required=True, aliases=["zone_name"] + type="list", default=None, required=False, aliases=["zone_name"] ), + name_from_ip=dict(type="str", default=None, required=False), forwarders=dict( type="list", default=None, @@ -475,7 +548,11 @@ def get_argument_spec(): def main(): - DNSZoneModule(argument_spec=get_argument_spec()).ipa_run() + DNSZoneModule( + argument_spec=get_argument_spec(), + mutually_exclusive=[["name", "name_from_ip"]], + required_one_of=[["name", "name_from_ip"]], + ).ipa_run() if __name__ == "__main__": diff --git a/tests/dnszone/test_dnszone_name_from_ip.yml b/tests/dnszone/test_dnszone_name_from_ip.yml new file mode 100644 index 0000000..9bd2eb0 --- /dev/null +++ b/tests/dnszone/test_dnszone_name_from_ip.yml @@ -0,0 +1,112 @@ +--- +- name: Test dnszone + hosts: ipaserver + become: yes + gather_facts: yes + + tasks: + + # Setup + - name: Ensure zone is absent. + ipadnszone: + ipaadmin_password: SomeADMINpassword + name: "{{ item }}" + state: absent + with_items: + - 2.0.192.in-addr.arpa. + - 0.0.0.0.0.0.0.0.0.0.0.0.0.0.d.f.ip6.arpa. + - 1.0.0.0.e.f.a.c.8.b.d.0.1.0.0.2.ip6.arpa. + + # tests + - name: Ensure zone exists for reverse IP. + ipadnszone: + ipaadmin_password: SomeADMINpassword + name_from_ip: 192.0.2.3/24 + register: ipv4_zone + failed_when: not ipv4_zone.changed or ipv4_zone.failed + + - name: Ensure zone exists for reverse IP, again. + ipadnszone: + ipaadmin_password: SomeADMINpassword + name_from_ip: 192.0.2.3/24 + register: result + failed_when: result.changed or result.failed + + - name: Ensure zone exists for reverse IP, given the zone name. + ipadnszone: + ipaadmin_password: SomeADMINpassword + name: "{{ ipv4_zone.dnszone.name }}" + register: result + failed_when: result.changed or result.failed + + - name: Modify existing zone, using `name_from_ip`. + ipadnszone: + ipaadmin_password: SomeADMINpassword + name_from_ip: 192.0.2.3/24 + default_ttl: 1234 + register: result + failed_when: not result.changed + + - name: Modify existing zone, using `name_from_ip`, again. + ipadnszone: + ipaadmin_password: SomeADMINpassword + name_from_ip: 192.0.2.3/24 + default_ttl: 1234 + register: result + failed_when: result.changed or result.failed + + - name: Ensure ipv6 zone exists for reverse IPv6. + ipadnszone: + ipaadmin_password: SomeADMINpassword + name_from_ip: fd00::0001 + register: ipv6_zone + failed_when: not ipv6_zone.changed or ipv6_zone.failed + + # - debug: + # msg: "{{ipv6_zone}}" + + - name: Ensure ipv6 zone was created. + ipadnszone: + ipaadmin_password: SomeADMINpassword + name: "{{ ipv6_zone.dnszone.name }}" + register: result + failed_when: result.changed or result.failed + + - name: Ensure ipv6 zone exists for reverse IPv6, again. + ipadnszone: + ipaadmin_password: SomeADMINpassword + name_from_ip: fd00::0001 + register: result + failed_when: result.changed + + - name: Ensure second ipv6 zone exists for reverse IPv6. + ipadnszone: + ipaadmin_password: SomeADMINpassword + name_from_ip: 2001:db8:cafe:1::1 + register: ipv6_sec_zone + failed_when: not ipv6_sec_zone.changed or ipv6_zone.failed + + - name: Ensure second ipv6 zone was created. + ipadnszone: + ipaadmin_password: SomeADMINpassword + name: "{{ ipv6_sec_zone.dnszone.name }}" + register: result + failed_when: result.changed or result.failed + + - name: Ensure second ipv6 zone exists for reverse IPv6, again. + ipadnszone: + ipaadmin_password: SomeADMINpassword + name_from_ip: 2001:db8:cafe:1::1 + register: result + failed_when: result.changed + + # Cleanup + - name: Ensure zone is absent. + ipadnszone: + ipaadmin_password: SomeADMINpassword + name: "{{ item }}" + state: absent + with_items: + - "{{ ipv6_zone.dnszone.name }}" + - "{{ ipv6_sec_zone.dnszone.name }}" + - "{{ ipv4_zone.dnszone.name }}"