mirror of
https://github.com/freeipa/ansible-freeipa.git
synced 2026-05-07 22:03:18 +00:00
ipahost: Add support for several IP addresses and also to change them
ipahost was so far ignoring IP addresses when the host already existed.
This happened because host_mod is not providing functionality to do this.
Now ipaddress is a list and it is possible to ensure a host with several
IP addresses (these can be IPv4 and IPv6). Also it is possible to ensure
presence and absence of IP addresses for an exising host using action
member.
There are no IP address conclict checks as this would lead into issues with
updating an existing host that already is using a duplicate IP address for
example for round-robin (RR). Also this might lead into issues with ensuring
a new host with several IP addresses in this case. Also to ensure a list of
hosts with changing the IP address of one host to another in the list would
result in issues here.
New example playbooks have been added:
playbooks/host/host-present-with-several-ip-addresses.yml
playbooks/host/host-member-ipaddresses-absent.yml
playbooks/host/host-member-ipaddresses-present.yml
A new test has been added for verification:
tests/host/test_host_ipaddresses.yml
Fixes: https://bugzilla.redhat.com/show_bug.cgi?id=1783976
https://bugzilla.redhat.com/show_bug.cgi?id=1783979
This commit is contained in:
@@ -42,6 +42,7 @@ try:
|
||||
from ipalib.x509 import Encoding
|
||||
except ImportError:
|
||||
from cryptography.hazmat.primitives.serialization import Encoding
|
||||
import socket
|
||||
import base64
|
||||
import six
|
||||
|
||||
@@ -285,3 +286,25 @@ def encode_certificate(cert):
|
||||
if not six.PY2:
|
||||
encoded = encoded.decode('ascii')
|
||||
return encoded
|
||||
|
||||
|
||||
def is_ipv4_addr(ipaddr):
|
||||
"""
|
||||
Test if figen IP address is a valid IPv4 address
|
||||
"""
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET, ipaddr)
|
||||
except socket.error:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def is_ipv6_addr(ipaddr):
|
||||
"""
|
||||
Test if figen IP address is a valid IPv6 address
|
||||
"""
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, ipaddr)
|
||||
except socket.error:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -176,11 +176,16 @@ options:
|
||||
default: true
|
||||
required: false
|
||||
ip_address:
|
||||
description: The host IP address
|
||||
description:
|
||||
The host IP address list (IPv4 and IPv6). No IP address conflict
|
||||
check will be done.
|
||||
aliases: ["ipaddress"]
|
||||
required: false
|
||||
update_dns:
|
||||
description: Update DNS entries
|
||||
description:
|
||||
Controls the update of the DNS SSHFP records for existing hosts and
|
||||
the removal of all DNS entries if a host gets removed with state
|
||||
absent.
|
||||
required: false
|
||||
description:
|
||||
description: The host description
|
||||
@@ -306,11 +311,16 @@ options:
|
||||
default: true
|
||||
required: false
|
||||
ip_address:
|
||||
description: The host IP address
|
||||
description:
|
||||
The host IP address list (IPv4 and IPv6). No IP address conflict
|
||||
check will be done.
|
||||
aliases: ["ipaddress"]
|
||||
required: false
|
||||
update_dns:
|
||||
description: Update DNS entries
|
||||
description:
|
||||
Controls the update of the DNS SSHFP records for existing hosts and
|
||||
the removal of all DNS entries if a host gets removed with state
|
||||
absent.
|
||||
required: false
|
||||
update_password:
|
||||
description:
|
||||
@@ -398,7 +408,8 @@ from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.ansible_freeipa_module import temp_kinit, \
|
||||
temp_kdestroy, valid_creds, api_connect, api_command, compare_args_ipa, \
|
||||
module_params_get, gen_add_del_lists, encode_certificate, api_get_realm
|
||||
module_params_get, gen_add_del_lists, encode_certificate, api_get_realm, \
|
||||
is_ipv4_addr, is_ipv6_addr
|
||||
import six
|
||||
|
||||
|
||||
@@ -428,6 +439,32 @@ def find_host(module, name):
|
||||
return None
|
||||
|
||||
|
||||
def find_dnsrecord(module, name):
|
||||
domain_name = name[name.find(".")+1:]
|
||||
host_name = name[:name.find(".")]
|
||||
|
||||
_args = {
|
||||
"all": True,
|
||||
"idnsname": to_text(host_name),
|
||||
}
|
||||
|
||||
_result = api_command(module, "dnsrecord_find", to_text(domain_name),
|
||||
_args)
|
||||
|
||||
if len(_result["result"]) > 1:
|
||||
module.fail_json(
|
||||
msg="There is more than one host '%s'" % (name))
|
||||
elif len(_result["result"]) == 1:
|
||||
_res = _result["result"][0]
|
||||
certs = _res.get("usercertificate")
|
||||
if certs is not None:
|
||||
_res["usercertificate"] = [encode_certificate(cert) for
|
||||
cert in certs]
|
||||
return _res
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def show_host(module, name):
|
||||
_result = api_command(module, "host_show", to_text(name), {})
|
||||
return _result["result"]
|
||||
@@ -470,16 +507,34 @@ def gen_args(description, locality, location, platform, os, password, random,
|
||||
_args["ipakrboktoauthasdelegate"] = ok_to_auth_as_delegate
|
||||
if force is not None:
|
||||
_args["force"] = force
|
||||
if reverse is not None:
|
||||
_args["no_reverse"] = not reverse
|
||||
if ip_address is not None:
|
||||
_args["ip_address"] = ip_address
|
||||
# IP addresses are handed extra, therefore it is needed to set
|
||||
# the force option here to make sure that host-add is able to
|
||||
# add a host without IP address.
|
||||
_args["force"] = True
|
||||
if update_dns is not None:
|
||||
_args["updatedns"] = update_dns
|
||||
|
||||
return _args
|
||||
|
||||
|
||||
def gen_dnsrecord_args(module, ip_address, reverse):
|
||||
_args = {}
|
||||
if reverse is not None:
|
||||
_args["a_extra_create_reverse"] = reverse
|
||||
_args["aaaa_extra_create_reverse"] = reverse
|
||||
if ip_address is not None:
|
||||
for ip in ip_address:
|
||||
if is_ipv4_addr(ip):
|
||||
_args.setdefault("arecord", []).append(ip)
|
||||
elif is_ipv6_addr(ip):
|
||||
_args.setdefault("aaaarecord", []).append(ip)
|
||||
else:
|
||||
module.fail_json(msg="'%s' is not a valid IP address." % ip)
|
||||
|
||||
return _args
|
||||
|
||||
|
||||
def check_parameters(
|
||||
module, state, action,
|
||||
description, locality, location, platform, os, password, random,
|
||||
@@ -499,8 +554,7 @@ def check_parameters(
|
||||
"os", "password", "random", "mac_address", "sshpubkey",
|
||||
"userclass", "auth_ind", "requires_pre_auth",
|
||||
"ok_as_delegate", "ok_to_auth_as_delegate", "force",
|
||||
"reverse", "ip_address", "update_dns",
|
||||
"update_password"]
|
||||
"reverse", "update_dns", "update_password"]
|
||||
for x in invalid:
|
||||
if vars()[x] is not None:
|
||||
module.fail_json(
|
||||
@@ -512,7 +566,7 @@ def check_parameters(
|
||||
"password", "random", "mac_address", "sshpubkey",
|
||||
"userclass", "auth_ind", "requires_pre_auth",
|
||||
"ok_as_delegate", "ok_to_auth_as_delegate", "force",
|
||||
"reverse", "ip_address", "update_password"]
|
||||
"reverse", "update_password"]
|
||||
for x in invalid:
|
||||
if vars()[x] is not None:
|
||||
module.fail_json(
|
||||
@@ -549,9 +603,6 @@ def main():
|
||||
default=None, no_log=True),
|
||||
random=dict(type="bool", aliases=["random_password"],
|
||||
default=None),
|
||||
|
||||
|
||||
|
||||
certificate=dict(type="list", aliases=["usercertificate"],
|
||||
default=None),
|
||||
managedby_host=dict(type="list",
|
||||
@@ -608,7 +659,7 @@ def main():
|
||||
default=None),
|
||||
force=dict(type='bool', default=None),
|
||||
reverse=dict(type='bool', default=None),
|
||||
ip_address=dict(type="str", aliases=["ipaddress"],
|
||||
ip_address=dict(type="list", aliases=["ipaddress"],
|
||||
default=None),
|
||||
update_dns=dict(type="bool", aliases=["updatedns"],
|
||||
default=None),
|
||||
@@ -820,6 +871,7 @@ def main():
|
||||
|
||||
# Make sure host exists
|
||||
res_find = find_host(ansible_module, name)
|
||||
res_find_dnsrecord = find_dnsrecord(ansible_module, name)
|
||||
|
||||
# Create command
|
||||
if state == "present":
|
||||
@@ -829,6 +881,8 @@ def main():
|
||||
random, mac_address, sshpubkey, userclass, auth_ind,
|
||||
requires_pre_auth, ok_as_delegate, ok_to_auth_as_delegate,
|
||||
force, reverse, ip_address, update_dns)
|
||||
dnsrecord_args = gen_dnsrecord_args(
|
||||
ansible_module, ip_address, reverse)
|
||||
|
||||
if action == "host":
|
||||
# Found the host
|
||||
@@ -938,39 +992,20 @@ def main():
|
||||
res_find.get(
|
||||
"ipaallowedtoperform_read_keys_hostgroup"))
|
||||
|
||||
else:
|
||||
certificate_add = certificate or []
|
||||
certificate_del = []
|
||||
managedby_host_add = managedby_host or []
|
||||
managedby_host_del = []
|
||||
principal_add = principal or []
|
||||
principal_del = []
|
||||
allow_create_keytab_user_add = \
|
||||
allow_create_keytab_user or []
|
||||
allow_create_keytab_user_del = []
|
||||
allow_create_keytab_group_add = \
|
||||
allow_create_keytab_group or []
|
||||
allow_create_keytab_group_del = []
|
||||
allow_create_keytab_host_add = \
|
||||
allow_create_keytab_host or []
|
||||
allow_create_keytab_host_del = []
|
||||
allow_create_keytab_hostgroup_add = \
|
||||
allow_create_keytab_hostgroup or []
|
||||
allow_create_keytab_hostgroup_del = []
|
||||
allow_retrieve_keytab_user_add = \
|
||||
allow_retrieve_keytab_user or []
|
||||
allow_retrieve_keytab_user_del = []
|
||||
allow_retrieve_keytab_group_add = \
|
||||
allow_retrieve_keytab_group or []
|
||||
allow_retrieve_keytab_group_del = []
|
||||
allow_retrieve_keytab_host_add = \
|
||||
allow_retrieve_keytab_host or []
|
||||
allow_retrieve_keytab_host_del = []
|
||||
allow_retrieve_keytab_hostgroup_add = \
|
||||
allow_retrieve_keytab_hostgroup or []
|
||||
allow_retrieve_keytab_hostgroup_del = []
|
||||
# IP addresses are not really a member of hosts, but
|
||||
# we will simply treat it as this to enable the
|
||||
# addition and removal of IPv4 and IPv6 addresses in
|
||||
# a simple way.
|
||||
_dnsrec = res_find_dnsrecord or {}
|
||||
dnsrecord_a_add, dnsrecord_a_del = gen_add_del_lists(
|
||||
dnsrecord_args.get("arecord"),
|
||||
_dnsrec.get("arecord"))
|
||||
dnsrecord_aaaa_add, dnsrecord_aaaa_del = \
|
||||
gen_add_del_lists(
|
||||
dnsrecord_args.get("aaaarecord"),
|
||||
_dnsrec.get("aaaarecord"))
|
||||
|
||||
else:
|
||||
if action != "host" or (action == "host" and res_find is None):
|
||||
certificate_add = certificate or []
|
||||
certificate_del = []
|
||||
managedby_host_add = managedby_host or []
|
||||
@@ -1001,6 +1036,10 @@ def main():
|
||||
allow_retrieve_keytab_hostgroup_add = \
|
||||
allow_retrieve_keytab_hostgroup or []
|
||||
allow_retrieve_keytab_hostgroup_del = []
|
||||
dnsrecord_a_add = dnsrecord_args.get("arecord") or []
|
||||
dnsrecord_a_del = []
|
||||
dnsrecord_aaaa_add = dnsrecord_args.get("aaaarecord") or []
|
||||
dnsrecord_aaaa_del = []
|
||||
|
||||
# Remove canonical principal from principal_del
|
||||
canonical_principal = "host/" + name + "@" + server_realm
|
||||
@@ -1135,6 +1174,36 @@ def main():
|
||||
"hostgroup": allow_retrieve_keytab_hostgroup_del,
|
||||
}])
|
||||
|
||||
if len(dnsrecord_a_add) > 0 or len(dnsrecord_aaaa_add) > 0:
|
||||
domain_name = name[name.find(".")+1:]
|
||||
host_name = name[:name.find(".")]
|
||||
|
||||
commands.append([domain_name,
|
||||
"dnsrecord_add",
|
||||
{
|
||||
"idnsname": host_name,
|
||||
"arecord": dnsrecord_a_add,
|
||||
"a_extra_create_reverse": reverse,
|
||||
"aaaarecord": dnsrecord_aaaa_add,
|
||||
"aaaa_extra_create_reverse": reverse
|
||||
}])
|
||||
|
||||
if len(dnsrecord_a_del) > 0 or len(dnsrecord_aaaa_del) > 0:
|
||||
domain_name = name[name.find(".")+1:]
|
||||
host_name = name[:name.find(".")]
|
||||
|
||||
# There seems to be an issue with dnsrecord_del (not
|
||||
# for dnsrecord_add) if aaaarecord is an empty list.
|
||||
# Therefore this is done differently here:
|
||||
_args = {"idnsname": host_name}
|
||||
if len(dnsrecord_a_del) > 0:
|
||||
_args["arecord"] = dnsrecord_a_del
|
||||
if len(dnsrecord_aaaa_del) > 0:
|
||||
_args["aaaarecord"] = dnsrecord_aaaa_del
|
||||
|
||||
commands.append([domain_name,
|
||||
"dnsrecord_del", _args])
|
||||
|
||||
elif state == "absent":
|
||||
if action == "host":
|
||||
|
||||
@@ -1215,6 +1284,17 @@ def main():
|
||||
"hostgroup": allow_retrieve_keytab_hostgroup,
|
||||
}])
|
||||
|
||||
dnsrecord_args = gen_dnsrecord_args(ansible_module,
|
||||
ip_address, reverse)
|
||||
if "arecord" in dnsrecord_args or \
|
||||
"aaaarecord" in dnsrecord_args:
|
||||
domain_name = name[name.find(".")+1:]
|
||||
host_name = name[:name.find(".")]
|
||||
dnsrecord_args["idnsname"] = host_name
|
||||
|
||||
commands.append([domain_name, "dnsrecord_del",
|
||||
dnsrecord_args])
|
||||
|
||||
elif state == "disabled":
|
||||
if res_find is not None:
|
||||
commands.append([name, "host_disable", {}])
|
||||
@@ -1259,6 +1339,11 @@ def main():
|
||||
# Host is already disabled, ignore error
|
||||
if "This entry is already disabled" in msg:
|
||||
continue
|
||||
|
||||
# Ignore no modification error.
|
||||
if "no modifications to be performed" in msg:
|
||||
continue
|
||||
|
||||
ansible_module.fail_json(msg="%s: %s: %s" % (command, name,
|
||||
msg))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user