Initial commit

This commit is contained in:
Ansible Core Team 2020-03-09 09:11:07 +00:00
commit aebc1b03fd
4861 changed files with 812621 additions and 0 deletions

View file

@ -0,0 +1,142 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Fran Fitzpatrick <francis.x.fitzpatrick@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: ipa_config
author: Fran Fitzpatrick (@fxfitz)
short_description: Manage Global FreeIPA Configuration Settings
description:
- Modify global configuration settings of a FreeIPA Server.
options:
ipadefaultloginshell:
description: Default shell for new users.
aliases: ["loginshell"]
type: str
ipadefaultemaildomain:
description: Default e-mail domain for new users.
aliases: ["emaildomain"]
type: str
extends_documentation_fragment:
- community.general.ipa.documentation
'''
EXAMPLES = r'''
- name: Ensure the default login shell is bash.
ipa_config:
ipadefaultloginshell: /bin/bash
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
- name: Ensure the default e-mail domain is ansible.com.
ipa_config:
ipadefaultemaildomain: ansible.com
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
'''
RETURN = r'''
config:
description: Configuration as returned by IPA API.
returned: always
type: dict
'''
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
from ansible.module_utils._text import to_native
class ConfigIPAClient(IPAClient):
def __init__(self, module, host, port, protocol):
super(ConfigIPAClient, self).__init__(module, host, port, protocol)
def config_show(self):
return self._post_json(method='config_show', name=None)
def config_mod(self, name, item):
return self._post_json(method='config_mod', name=name, item=item)
def get_config_dict(ipadefaultloginshell=None, ipadefaultemaildomain=None):
config = {}
if ipadefaultloginshell is not None:
config['ipadefaultloginshell'] = ipadefaultloginshell
if ipadefaultemaildomain is not None:
config['ipadefaultemaildomain'] = ipadefaultemaildomain
return config
def get_config_diff(client, ipa_config, module_config):
return client.get_diff(ipa_data=ipa_config, module_data=module_config)
def ensure(module, client):
module_config = get_config_dict(
ipadefaultloginshell=module.params.get('ipadefaultloginshell'),
ipadefaultemaildomain=module.params.get('ipadefaultemaildomain'),
)
ipa_config = client.config_show()
diff = get_config_diff(client, ipa_config, module_config)
changed = False
new_config = {}
for module_key in diff:
if module_config.get(module_key) != ipa_config.get(module_key, None):
changed = True
new_config.update({module_key: module_config.get(module_key)})
if changed and not module.check_mode:
client.config_mod(name=None, item=new_config)
return changed, client.config_show()
def main():
argument_spec = ipa_argument_spec()
argument_spec.update(
ipadefaultloginshell=dict(type='str', aliases=['loginshell']),
ipadefaultemaildomain=dict(type='str', aliases=['emaildomain']),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True
)
client = ConfigIPAClient(
module=module,
host=module.params['ipa_host'],
port=module.params['ipa_port'],
protocol=module.params['ipa_prot']
)
try:
client.login(
username=module.params['ipa_user'],
password=module.params['ipa_pass']
)
changed, user = ensure(module, client)
module.exit_json(changed=changed, user=user)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,326 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Abhijeet Kasurde (akasurde@redhat.com)
#
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = r'''
---
module: ipa_dnsrecord
author: Abhijeet Kasurde (@Akasurde)
short_description: Manage FreeIPA DNS records
description:
- Add, modify and delete an IPA DNS Record using IPA API.
options:
zone_name:
description:
- The DNS zone name to which DNS record needs to be managed.
required: true
type: str
record_name:
description:
- The DNS record name to manage.
required: true
aliases: ["name"]
type: str
record_type:
description:
- The type of DNS record name.
- Currently, 'A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'PTR', 'TXT', 'SRV' and 'MX' are supported.
- "'A6', 'CNAME', 'DNAME' and 'TXT' are added in version 2.5."
- "'SRV' and 'MX' are added in version 2.8."
required: false
default: 'A'
choices: ['A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'MX', 'PTR', 'SRV', 'TXT']
type: str
record_value:
description:
- Manage DNS record name with this value.
- In the case of 'A' or 'AAAA' record types, this will be the IP address.
- In the case of 'A6' record type, this will be the A6 Record data.
- In the case of 'CNAME' record type, this will be the hostname.
- In the case of 'DNAME' record type, this will be the DNAME target.
- In the case of 'PTR' record type, this will be the hostname.
- In the case of 'TXT' record type, this will be a text.
- In the case of 'SRV' record type, this will be a service record.
- In the case of 'MX' record type, this will be a mail exchanger record.
required: true
type: str
record_ttl:
description:
- Set the TTL for the record.
- Applies only when adding a new or changing the value of record_value.
required: false
type: int
state:
description: State to ensure
required: false
default: present
choices: ["absent", "present"]
type: str
extends_documentation_fragment:
- community.general.ipa.documentation
'''
EXAMPLES = r'''
- name: Ensure dns record is present
ipa_dnsrecord:
ipa_host: spider.example.com
ipa_pass: Passw0rd!
state: present
zone_name: example.com
record_name: vm-001
record_type: 'AAAA'
record_value: '::1'
- name: Ensure that dns record exists with a TTL
ipa_dnsrecord:
name: host02
zone_name: example.com
record_type: 'AAAA'
record_value: '::1'
record_ttl: 300
ipa_host: ipa.example.com
ipa_pass: topsecret
state: present
- name: Ensure a PTR record is present
ipa_dnsrecord:
ipa_host: spider.example.com
ipa_pass: Passw0rd!
state: present
zone_name: 2.168.192.in-addr.arpa
record_name: 5
record_type: 'PTR'
record_value: 'internal.ipa.example.com'
- name: Ensure a TXT record is present
ipa_dnsrecord:
ipa_host: spider.example.com
ipa_pass: Passw0rd!
state: present
zone_name: example.com
record_name: _kerberos
record_type: 'TXT'
record_value: 'EXAMPLE.COM'
- name: Ensure an SRV record is present
ipa_dnsrecord:
ipa_host: spider.example.com
ipa_pass: Passw0rd!
state: present
zone_name: example.com
record_name: _kerberos._udp.example.com
record_type: 'SRV'
record_value: '10 50 88 ipa.example.com'
- name: Ensure an MX record is present
ipa_dnsrecord:
ipa_host: spider.example.com
ipa_pass: Passw0rd!
state: present
zone_name: example.com
record_name: '@'
record_type: 'MX'
record_value: '1 mailserver.example.com'
- name: Ensure that dns record is removed
ipa_dnsrecord:
name: host01
zone_name: example.com
record_type: 'AAAA'
record_value: '::1'
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
state: absent
'''
RETURN = r'''
dnsrecord:
description: DNS record as returned by IPA API.
returned: always
type: dict
'''
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
from ansible.module_utils._text import to_native
class DNSRecordIPAClient(IPAClient):
def __init__(self, module, host, port, protocol):
super(DNSRecordIPAClient, self).__init__(module, host, port, protocol)
def dnsrecord_find(self, zone_name, record_name):
if record_name == '@':
return self._post_json(method='dnsrecord_show', name=zone_name, item={'idnsname': record_name, 'all': True})
else:
return self._post_json(method='dnsrecord_find', name=zone_name, item={'idnsname': record_name, 'all': True})
def dnsrecord_add(self, zone_name=None, record_name=None, details=None):
item = dict(idnsname=record_name)
if details['record_type'] == 'A':
item.update(a_part_ip_address=details['record_value'])
elif details['record_type'] == 'AAAA':
item.update(aaaa_part_ip_address=details['record_value'])
elif details['record_type'] == 'A6':
item.update(a6_part_data=details['record_value'])
elif details['record_type'] == 'CNAME':
item.update(cname_part_hostname=details['record_value'])
elif details['record_type'] == 'DNAME':
item.update(dname_part_target=details['record_value'])
elif details['record_type'] == 'PTR':
item.update(ptr_part_hostname=details['record_value'])
elif details['record_type'] == 'TXT':
item.update(txtrecord=details['record_value'])
elif details['record_type'] == 'SRV':
item.update(srvrecord=details['record_value'])
elif details['record_type'] == 'MX':
item.update(mxrecord=details['record_value'])
if details.get('record_ttl'):
item.update(dnsttl=details['record_ttl'])
return self._post_json(method='dnsrecord_add', name=zone_name, item=item)
def dnsrecord_mod(self, zone_name=None, record_name=None, details=None):
item = get_dnsrecord_dict(details)
item.update(idnsname=record_name)
if details.get('record_ttl'):
item.update(dnsttl=details['record_ttl'])
return self._post_json(method='dnsrecord_mod', name=zone_name, item=item)
def dnsrecord_del(self, zone_name=None, record_name=None, details=None):
item = get_dnsrecord_dict(details)
item.update(idnsname=record_name)
return self._post_json(method='dnsrecord_del', name=zone_name, item=item)
def get_dnsrecord_dict(details=None):
module_dnsrecord = dict()
if details['record_type'] == 'A' and details['record_value']:
module_dnsrecord.update(arecord=details['record_value'])
elif details['record_type'] == 'AAAA' and details['record_value']:
module_dnsrecord.update(aaaarecord=details['record_value'])
elif details['record_type'] == 'A6' and details['record_value']:
module_dnsrecord.update(a6record=details['record_value'])
elif details['record_type'] == 'CNAME' and details['record_value']:
module_dnsrecord.update(cnamerecord=details['record_value'])
elif details['record_type'] == 'DNAME' and details['record_value']:
module_dnsrecord.update(dnamerecord=details['record_value'])
elif details['record_type'] == 'PTR' and details['record_value']:
module_dnsrecord.update(ptrrecord=details['record_value'])
elif details['record_type'] == 'TXT' and details['record_value']:
module_dnsrecord.update(txtrecord=details['record_value'])
elif details['record_type'] == 'SRV' and details['record_value']:
module_dnsrecord.update(srvrecord=details['record_value'])
elif details['record_type'] == 'MX' and details['record_value']:
module_dnsrecord.update(mxrecord=details['record_value'])
if details.get('record_ttl'):
module_dnsrecord.update(dnsttl=details['record_ttl'])
return module_dnsrecord
def get_dnsrecord_diff(client, ipa_dnsrecord, module_dnsrecord):
details = get_dnsrecord_dict(module_dnsrecord)
return client.get_diff(ipa_data=ipa_dnsrecord, module_data=details)
def ensure(module, client):
zone_name = module.params['zone_name']
record_name = module.params['record_name']
record_ttl = module.params.get('record_ttl')
state = module.params['state']
ipa_dnsrecord = client.dnsrecord_find(zone_name, record_name)
module_dnsrecord = dict(
record_type=module.params['record_type'],
record_value=module.params['record_value'],
record_ttl=to_native(record_ttl, nonstring='passthru'),
)
# ttl is not required to change records
if module_dnsrecord['record_ttl'] is None:
module_dnsrecord.pop('record_ttl')
changed = False
if state == 'present':
if not ipa_dnsrecord:
changed = True
if not module.check_mode:
client.dnsrecord_add(zone_name=zone_name,
record_name=record_name,
details=module_dnsrecord)
else:
diff = get_dnsrecord_diff(client, ipa_dnsrecord, module_dnsrecord)
if len(diff) > 0:
changed = True
if not module.check_mode:
client.dnsrecord_mod(zone_name=zone_name,
record_name=record_name,
details=module_dnsrecord)
else:
if ipa_dnsrecord:
changed = True
if not module.check_mode:
client.dnsrecord_del(zone_name=zone_name,
record_name=record_name,
details=module_dnsrecord)
return changed, client.dnsrecord_find(zone_name, record_name)
def main():
record_types = ['A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'PTR', 'TXT', 'SRV', 'MX']
argument_spec = ipa_argument_spec()
argument_spec.update(
zone_name=dict(type='str', required=True),
record_name=dict(type='str', aliases=['name'], required=True),
record_type=dict(type='str', default='A', choices=record_types),
record_value=dict(type='str', required=True),
state=dict(type='str', default='present', choices=['present', 'absent']),
record_ttl=dict(type='int', required=False),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True
)
client = DNSRecordIPAClient(
module=module,
host=module.params['ipa_host'],
port=module.params['ipa_port'],
protocol=module.params['ipa_prot']
)
try:
client.login(
username=module.params['ipa_user'],
password=module.params['ipa_pass']
)
changed, record = ensure(module, client)
module.exit_json(changed=changed, record=record)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,167 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Fran Fitzpatrick (francis.x.fitzpatrick@gmail.com)
# Borrowed heavily from other work by Abhijeet Kasurde (akasurde@redhat.com)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: ipa_dnszone
author: Fran Fitzpatrick (@fxfitz)
short_description: Manage FreeIPA DNS Zones
description:
- Add and delete an IPA DNS Zones using IPA API
options:
zone_name:
description:
- The DNS zone name to which needs to be managed.
required: true
type: str
state:
description: State to ensure
required: false
default: present
choices: ["absent", "present"]
type: str
dynamicupdate:
description: Apply dynamic update to zone
required: false
default: "false"
choices: ["false", "true"]
type: str
extends_documentation_fragment:
- community.general.ipa.documentation
'''
EXAMPLES = r'''
- name: Ensure dns zone is present
ipa_dnszone:
ipa_host: spider.example.com
ipa_pass: Passw0rd!
state: present
zone_name: example.com
- name: Ensure dns zone is present and is dynamic update
ipa_dnszone:
ipa_host: spider.example.com
ipa_pass: Passw0rd!
state: present
zone_name: example.com
dynamicupdate: true
- name: Ensure that dns zone is removed
ipa_dnszone:
zone_name: example.com
ipa_host: localhost
ipa_user: admin
ipa_pass: topsecret
state: absent
'''
RETURN = r'''
zone:
description: DNS zone as returned by IPA API.
returned: always
type: dict
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
from ansible.module_utils._text import to_native
class DNSZoneIPAClient(IPAClient):
def __init__(self, module, host, port, protocol):
super(DNSZoneIPAClient, self).__init__(module, host, port, protocol)
def dnszone_find(self, zone_name, details=None):
itens = {'idnsname': zone_name}
if details is not None:
itens.update(details)
return self._post_json(
method='dnszone_find',
name=zone_name,
item=itens
)
def dnszone_add(self, zone_name=None, details=None):
itens = {}
if details is not None:
itens.update(details)
return self._post_json(
method='dnszone_add',
name=zone_name,
item=itens
)
def dnszone_del(self, zone_name=None, record_name=None, details=None):
return self._post_json(
method='dnszone_del', name=zone_name, item={})
def ensure(module, client):
zone_name = module.params['zone_name']
state = module.params['state']
dynamicupdate = module.params['dynamicupdate']
ipa_dnszone = client.dnszone_find(zone_name)
changed = False
if state == 'present':
if not ipa_dnszone:
changed = True
if not module.check_mode:
client.dnszone_add(zone_name=zone_name, details={'idnsallowdynupdate': dynamicupdate})
else:
changed = False
else:
if ipa_dnszone:
changed = True
if not module.check_mode:
client.dnszone_del(zone_name=zone_name)
return changed, client.dnszone_find(zone_name)
def main():
argument_spec = ipa_argument_spec()
argument_spec.update(zone_name=dict(type='str', required=True),
state=dict(type='str', default='present', choices=['present', 'absent']),
dynamicupdate=dict(type='str', required=False, default='false', choices=['true', 'false']),
)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True,
)
client = DNSZoneIPAClient(
module=module,
host=module.params['ipa_host'],
port=module.params['ipa_port'],
protocol=module.params['ipa_prot']
)
try:
client.login(
username=module.params['ipa_user'],
password=module.params['ipa_pass']
)
changed, zone = ensure(module, client)
module.exit_json(changed=changed, zone=zone)
except Exception as e:
module.fail_json(msg=to_native(e))
if __name__ == '__main__':
main()

View file

@ -0,0 +1,263 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: ipa_group
author: Thomas Krahn (@Nosmoht)
short_description: Manage FreeIPA group
description:
- Add, modify and delete group within IPA server
options:
cn:
description:
- Canonical name.
- Can not be changed as it is the unique identifier.
required: true
aliases: ['name']
type: str
description:
description:
- Description of the group.
type: str
external:
description:
- Allow adding external non-IPA members from trusted domains.
type: bool
gidnumber:
description:
- GID (use this option to set it manually).
aliases: ['gid']
type: str
group:
description:
- List of group names assigned to this group.
- If an empty list is passed all groups will be removed from this group.
- If option is omitted assigned groups will not be checked or changed.
- Groups that are already assigned but not passed will be removed.
type: list
elements: str
nonposix:
description:
- Create as a non-POSIX group.
type: bool
user:
description:
- List of user names assigned to this group.
- If an empty list is passed all users will be removed from this group.
- If option is omitted assigned users will not be checked or changed.
- Users that are already assigned but not passed will be removed.
type: list
elements: str
state:
description:
- State to ensure
default: "present"
choices: ["absent", "present"]
type: str
extends_documentation_fragment:
- community.general.ipa.documentation
'''
EXAMPLES = r'''
- name: Ensure group is present
ipa_group:
name: oinstall
gidnumber: 54321
state: present
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Ensure that groups sysops and appops are assigned to ops but no other group
ipa_group:
name: ops
group:
- sysops
- appops
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Ensure that users linus and larry are assign to the group, but no other user
ipa_group:
name: sysops
user:
- linus
- larry
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Ensure group is absent
ipa_group:
name: sysops
state: absent
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
'''
RETURN = r'''
group:
description: Group as returned by IPA API
returned: always
type: dict
'''
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
from ansible.module_utils._text import to_native
class GroupIPAClient(IPAClient):
def __init__(self, module, host, port, protocol):
super(GroupIPAClient, self).__init__(module, host, port, protocol)
def group_find(self, name):
return self._post_json(method='group_find', name=None, item={'all': True, 'cn': name})
def group_add(self, name, item):
return self._post_json(method='group_add', name=name, item=item)
def group_mod(self, name, item):
return self._post_json(method='group_mod', name=name, item=item)
def group_del(self, name):
return self._post_json(method='group_del', name=name)
def group_add_member(self, name, item):
return self._post_json(method='group_add_member', name=name, item=item)
def group_add_member_group(self, name, item):
return self.group_add_member(name=name, item={'group': item})
def group_add_member_user(self, name, item):
return self.group_add_member(name=name, item={'user': item})
def group_remove_member(self, name, item):
return self._post_json(method='group_remove_member', name=name, item=item)
def group_remove_member_group(self, name, item):
return self.group_remove_member(name=name, item={'group': item})
def group_remove_member_user(self, name, item):
return self.group_remove_member(name=name, item={'user': item})
def get_group_dict(description=None, external=None, gid=None, nonposix=None):
group = {}
if description is not None:
group['description'] = description
if external is not None:
group['external'] = external
if gid is not None:
group['gidnumber'] = gid
if nonposix is not None:
group['nonposix'] = nonposix
return group
def get_group_diff(client, ipa_group, module_group):
data = []
# With group_add attribute nonposix is passed, whereas with group_mod only posix can be passed.
if 'nonposix' in module_group:
# Only non-posix groups can be changed to posix
if not module_group['nonposix'] and ipa_group.get('nonposix'):
module_group['posix'] = True
del module_group['nonposix']
if 'external' in module_group:
if module_group['external'] and 'ipaexternalgroup' in ipa_group.get('objectclass'):
del module_group['external']
return client.get_diff(ipa_data=ipa_group, module_data=module_group)
def ensure(module, client):
state = module.params['state']
name = module.params['cn']
group = module.params['group']
user = module.params['user']
module_group = get_group_dict(description=module.params['description'], external=module.params['external'],
gid=module.params['gidnumber'], nonposix=module.params['nonposix'])
ipa_group = client.group_find(name=name)
changed = False
if state == 'present':
if not ipa_group:
changed = True
if not module.check_mode:
ipa_group = client.group_add(name, item=module_group)
else:
diff = get_group_diff(client, ipa_group, module_group)
if len(diff) > 0:
changed = True
if not module.check_mode:
data = {}
for key in diff:
data[key] = module_group.get(key)
client.group_mod(name=name, item=data)
if group is not None:
changed = client.modify_if_diff(name, ipa_group.get('member_group', []), group,
client.group_add_member_group,
client.group_remove_member_group) or changed
if user is not None:
changed = client.modify_if_diff(name, ipa_group.get('member_user', []), user,
client.group_add_member_user,
client.group_remove_member_user) or changed
else:
if ipa_group:
changed = True
if not module.check_mode:
client.group_del(name)
return changed, client.group_find(name=name)
def main():
argument_spec = ipa_argument_spec()
argument_spec.update(cn=dict(type='str', required=True, aliases=['name']),
description=dict(type='str'),
external=dict(type='bool'),
gidnumber=dict(type='str', aliases=['gid']),
group=dict(type='list', elements='str'),
nonposix=dict(type='bool'),
state=dict(type='str', default='present', choices=['present', 'absent']),
user=dict(type='list', elements='str'))
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True,
)
client = GroupIPAClient(module=module,
host=module.params['ipa_host'],
port=module.params['ipa_port'],
protocol=module.params['ipa_prot'])
try:
client.login(username=module.params['ipa_user'],
password=module.params['ipa_pass'])
changed, group = ensure(module, client)
module.exit_json(changed=changed, group=group)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,359 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: ipa_hbacrule
author: Thomas Krahn (@Nosmoht)
short_description: Manage FreeIPA HBAC rule
description:
- Add, modify or delete an IPA HBAC rule using IPA API.
options:
cn:
description:
- Canonical name.
- Can not be changed as it is the unique identifier.
required: true
aliases: ["name"]
type: str
description:
description: Description
type: str
host:
description:
- List of host names to assign.
- If an empty list is passed all hosts will be removed from the rule.
- If option is omitted hosts will not be checked or changed.
required: false
type: list
elements: str
hostcategory:
description: Host category
choices: ['all']
type: str
hostgroup:
description:
- List of hostgroup names to assign.
- If an empty list is passed all hostgroups will be removed. from the rule
- If option is omitted hostgroups will not be checked or changed.
type: list
elements: str
service:
description:
- List of service names to assign.
- If an empty list is passed all services will be removed from the rule.
- If option is omitted services will not be checked or changed.
type: list
elements: str
servicecategory:
description: Service category
choices: ['all']
type: str
servicegroup:
description:
- List of service group names to assign.
- If an empty list is passed all assigned service groups will be removed from the rule.
- If option is omitted service groups will not be checked or changed.
type: list
elements: str
sourcehost:
description:
- List of source host names to assign.
- If an empty list if passed all assigned source hosts will be removed from the rule.
- If option is omitted source hosts will not be checked or changed.
type: list
elements: str
sourcehostcategory:
description: Source host category
choices: ['all']
type: str
sourcehostgroup:
description:
- List of source host group names to assign.
- If an empty list if passed all assigned source host groups will be removed from the rule.
- If option is omitted source host groups will not be checked or changed.
type: list
elements: str
state:
description: State to ensure
default: "present"
choices: ["absent", "disabled", "enabled","present"]
type: str
user:
description:
- List of user names to assign.
- If an empty list if passed all assigned users will be removed from the rule.
- If option is omitted users will not be checked or changed.
type: list
elements: str
usercategory:
description: User category
choices: ['all']
type: str
usergroup:
description:
- List of user group names to assign.
- If an empty list if passed all assigned user groups will be removed from the rule.
- If option is omitted user groups will not be checked or changed.
type: list
extends_documentation_fragment:
- community.general.ipa.documentation
'''
EXAMPLES = r'''
- name: Ensure rule to allow all users to access any host from any host
ipa_hbacrule:
name: allow_all
description: Allow all users to access any host from any host
hostcategory: all
servicecategory: all
usercategory: all
state: present
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Ensure rule with certain limitations
ipa_hbacrule:
name: allow_all_developers_access_to_db
description: Allow all developers to access any database from any host
hostgroup:
- db-server
usergroup:
- developers
state: present
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Ensure rule is absent
ipa_hbacrule:
name: rule_to_be_deleted
state: absent
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
'''
RETURN = r'''
hbacrule:
description: HBAC rule as returned by IPA API.
returned: always
type: dict
'''
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
from ansible.module_utils._text import to_native
class HBACRuleIPAClient(IPAClient):
def __init__(self, module, host, port, protocol):
super(HBACRuleIPAClient, self).__init__(module, host, port, protocol)
def hbacrule_find(self, name):
return self._post_json(method='hbacrule_find', name=None, item={'all': True, 'cn': name})
def hbacrule_add(self, name, item):
return self._post_json(method='hbacrule_add', name=name, item=item)
def hbacrule_mod(self, name, item):
return self._post_json(method='hbacrule_mod', name=name, item=item)
def hbacrule_del(self, name):
return self._post_json(method='hbacrule_del', name=name)
def hbacrule_add_host(self, name, item):
return self._post_json(method='hbacrule_add_host', name=name, item=item)
def hbacrule_remove_host(self, name, item):
return self._post_json(method='hbacrule_remove_host', name=name, item=item)
def hbacrule_add_service(self, name, item):
return self._post_json(method='hbacrule_add_service', name=name, item=item)
def hbacrule_remove_service(self, name, item):
return self._post_json(method='hbacrule_remove_service', name=name, item=item)
def hbacrule_add_user(self, name, item):
return self._post_json(method='hbacrule_add_user', name=name, item=item)
def hbacrule_remove_user(self, name, item):
return self._post_json(method='hbacrule_remove_user', name=name, item=item)
def hbacrule_add_sourcehost(self, name, item):
return self._post_json(method='hbacrule_add_sourcehost', name=name, item=item)
def hbacrule_remove_sourcehost(self, name, item):
return self._post_json(method='hbacrule_remove_sourcehost', name=name, item=item)
def get_hbacrule_dict(description=None, hostcategory=None, ipaenabledflag=None, servicecategory=None,
sourcehostcategory=None,
usercategory=None):
data = {}
if description is not None:
data['description'] = description
if hostcategory is not None:
data['hostcategory'] = hostcategory
if ipaenabledflag is not None:
data['ipaenabledflag'] = ipaenabledflag
if servicecategory is not None:
data['servicecategory'] = servicecategory
if sourcehostcategory is not None:
data['sourcehostcategory'] = sourcehostcategory
if usercategory is not None:
data['usercategory'] = usercategory
return data
def get_hbcarule_diff(client, ipa_hbcarule, module_hbcarule):
return client.get_diff(ipa_data=ipa_hbcarule, module_data=module_hbcarule)
def ensure(module, client):
name = module.params['cn']
state = module.params['state']
if state in ['present', 'enabled']:
ipaenabledflag = 'TRUE'
else:
ipaenabledflag = 'FALSE'
host = module.params['host']
hostcategory = module.params['hostcategory']
hostgroup = module.params['hostgroup']
service = module.params['service']
servicecategory = module.params['servicecategory']
servicegroup = module.params['servicegroup']
sourcehost = module.params['sourcehost']
sourcehostcategory = module.params['sourcehostcategory']
sourcehostgroup = module.params['sourcehostgroup']
user = module.params['user']
usercategory = module.params['usercategory']
usergroup = module.params['usergroup']
module_hbacrule = get_hbacrule_dict(description=module.params['description'],
hostcategory=hostcategory,
ipaenabledflag=ipaenabledflag,
servicecategory=servicecategory,
sourcehostcategory=sourcehostcategory,
usercategory=usercategory)
ipa_hbacrule = client.hbacrule_find(name=name)
changed = False
if state in ['present', 'enabled', 'disabled']:
if not ipa_hbacrule:
changed = True
if not module.check_mode:
ipa_hbacrule = client.hbacrule_add(name=name, item=module_hbacrule)
else:
diff = get_hbcarule_diff(client, ipa_hbacrule, module_hbacrule)
if len(diff) > 0:
changed = True
if not module.check_mode:
data = {}
for key in diff:
data[key] = module_hbacrule.get(key)
client.hbacrule_mod(name=name, item=data)
if host is not None:
changed = client.modify_if_diff(name, ipa_hbacrule.get('memberhost_host', []), host,
client.hbacrule_add_host,
client.hbacrule_remove_host, 'host') or changed
if hostgroup is not None:
changed = client.modify_if_diff(name, ipa_hbacrule.get('memberhost_hostgroup', []), hostgroup,
client.hbacrule_add_host,
client.hbacrule_remove_host, 'hostgroup') or changed
if service is not None:
changed = client.modify_if_diff(name, ipa_hbacrule.get('memberservice_hbacsvc', []), service,
client.hbacrule_add_service,
client.hbacrule_remove_service, 'hbacsvc') or changed
if servicegroup is not None:
changed = client.modify_if_diff(name, ipa_hbacrule.get('memberservice_hbacsvcgroup', []),
servicegroup,
client.hbacrule_add_service,
client.hbacrule_remove_service, 'hbacsvcgroup') or changed
if sourcehost is not None:
changed = client.modify_if_diff(name, ipa_hbacrule.get('sourcehost_host', []), sourcehost,
client.hbacrule_add_sourcehost,
client.hbacrule_remove_sourcehost, 'host') or changed
if sourcehostgroup is not None:
changed = client.modify_if_diff(name, ipa_hbacrule.get('sourcehost_group', []), sourcehostgroup,
client.hbacrule_add_sourcehost,
client.hbacrule_remove_sourcehost, 'hostgroup') or changed
if user is not None:
changed = client.modify_if_diff(name, ipa_hbacrule.get('memberuser_user', []), user,
client.hbacrule_add_user,
client.hbacrule_remove_user, 'user') or changed
if usergroup is not None:
changed = client.modify_if_diff(name, ipa_hbacrule.get('memberuser_group', []), usergroup,
client.hbacrule_add_user,
client.hbacrule_remove_user, 'group') or changed
else:
if ipa_hbacrule:
changed = True
if not module.check_mode:
client.hbacrule_del(name=name)
return changed, client.hbacrule_find(name=name)
def main():
argument_spec = ipa_argument_spec()
argument_spec.update(cn=dict(type='str', required=True, aliases=['name']),
description=dict(type='str'),
host=dict(type='list', elements='str'),
hostcategory=dict(type='str', choices=['all']),
hostgroup=dict(type='list', elements='str'),
service=dict(type='list', elements='str'),
servicecategory=dict(type='str', choices=['all']),
servicegroup=dict(type='list', elements='str'),
sourcehost=dict(type='list', elements='str'),
sourcehostcategory=dict(type='str', choices=['all']),
sourcehostgroup=dict(type='list', elements='str'),
state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']),
user=dict(type='list', elements='str'),
usercategory=dict(type='str', choices=['all']),
usergroup=dict(type='list', elements='str'))
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True
)
client = HBACRuleIPAClient(module=module,
host=module.params['ipa_host'],
port=module.params['ipa_port'],
protocol=module.params['ipa_prot'])
try:
client.login(username=module.params['ipa_user'],
password=module.params['ipa_pass'])
changed, hbacrule = ensure(module, client)
module.exit_json(changed=changed, hbacrule=hbacrule)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,312 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: ipa_host
author: Thomas Krahn (@Nosmoht)
short_description: Manage FreeIPA host
description:
- Add, modify and delete an IPA host using IPA API.
options:
fqdn:
description:
- Full qualified domain name.
- Can not be changed as it is the unique identifier.
required: true
aliases: ["name"]
type: str
description:
description:
- A description of this host.
type: str
force:
description:
- Force host name even if not in DNS.
required: false
type: bool
ip_address:
description:
- Add the host to DNS with this IP address.
type: str
mac_address:
description:
- List of Hardware MAC address(es) off this host.
- If option is omitted MAC addresses will not be checked or changed.
- If an empty list is passed all assigned MAC addresses will be removed.
- MAC addresses that are already assigned but not passed will be removed.
aliases: ["macaddress"]
type: list
elements: str
ns_host_location:
description:
- Host location (e.g. "Lab 2")
aliases: ["nshostlocation"]
type: str
ns_hardware_platform:
description:
- Host hardware platform (e.g. "Lenovo T61")
aliases: ["nshardwareplatform"]
type: str
ns_os_version:
description:
- Host operating system and version (e.g. "Fedora 9")
aliases: ["nsosversion"]
type: str
user_certificate:
description:
- List of Base-64 encoded server certificates.
- If option is omitted certificates will not be checked or changed.
- If an empty list is passed all assigned certificates will be removed.
- Certificates already assigned but not passed will be removed.
aliases: ["usercertificate"]
type: list
elements: str
state:
description: State to ensure.
default: present
choices: ["absent", "disabled", "enabled", "present"]
type: str
update_dns:
description:
- If set C("True") with state as C("absent"), then removes DNS records of the host managed by FreeIPA DNS.
- This option has no effect for states other than "absent".
default: false
type: bool
random_password:
description: Generate a random password to be used in bulk enrollment.
default: False
type: bool
extends_documentation_fragment:
- community.general.ipa.documentation
'''
EXAMPLES = r'''
- name: Ensure host is present
ipa_host:
name: host01.example.com
description: Example host
ip_address: 192.168.0.123
ns_host_location: Lab
ns_os_version: CentOS 7
ns_hardware_platform: Lenovo T61
mac_address:
- "08:00:27:E3:B1:2D"
- "52:54:00:BD:97:1E"
state: present
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Generate a random password for bulk enrolment
ipa_host:
name: host01.example.com
description: Example host
ip_address: 192.168.0.123
state: present
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
validate_certs: False
random_password: True
- name: Ensure host is disabled
ipa_host:
name: host01.example.com
state: disabled
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Ensure that all user certificates are removed
ipa_host:
name: host01.example.com
user_certificate: []
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Ensure host is absent
ipa_host:
name: host01.example.com
state: absent
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Ensure host and its DNS record is absent
ipa_host:
name: host01.example.com
state: absent
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
update_dns: True
'''
RETURN = r'''
host:
description: Host as returned by IPA API.
returned: always
type: dict
host_diff:
description: List of options that differ and would be changed
returned: if check mode and a difference is found
type: list
'''
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
from ansible.module_utils._text import to_native
class HostIPAClient(IPAClient):
def __init__(self, module, host, port, protocol):
super(HostIPAClient, self).__init__(module, host, port, protocol)
def host_show(self, name):
return self._post_json(method='host_show', name=name)
def host_find(self, name):
return self._post_json(method='host_find', name=None, item={'all': True, 'fqdn': name})
def host_add(self, name, host):
return self._post_json(method='host_add', name=name, item=host)
def host_mod(self, name, host):
return self._post_json(method='host_mod', name=name, item=host)
def host_del(self, name, update_dns):
return self._post_json(method='host_del', name=name, item={'updatedns': update_dns})
def host_disable(self, name):
return self._post_json(method='host_disable', name=name)
def get_host_dict(description=None, force=None, ip_address=None, ns_host_location=None, ns_hardware_platform=None,
ns_os_version=None, user_certificate=None, mac_address=None, random_password=None):
data = {}
if description is not None:
data['description'] = description
if force is not None:
data['force'] = force
if ip_address is not None:
data['ip_address'] = ip_address
if ns_host_location is not None:
data['nshostlocation'] = ns_host_location
if ns_hardware_platform is not None:
data['nshardwareplatform'] = ns_hardware_platform
if ns_os_version is not None:
data['nsosversion'] = ns_os_version
if user_certificate is not None:
data['usercertificate'] = [{"__base64__": item} for item in user_certificate]
if mac_address is not None:
data['macaddress'] = mac_address
if random_password is not None:
data['random'] = random_password
return data
def get_host_diff(client, ipa_host, module_host):
non_updateable_keys = ['force', 'ip_address']
if not module_host.get('random'):
non_updateable_keys.append('random')
for key in non_updateable_keys:
if key in module_host:
del module_host[key]
return client.get_diff(ipa_data=ipa_host, module_data=module_host)
def ensure(module, client):
name = module.params['fqdn']
state = module.params['state']
ipa_host = client.host_find(name=name)
module_host = get_host_dict(description=module.params['description'],
force=module.params['force'], ip_address=module.params['ip_address'],
ns_host_location=module.params['ns_host_location'],
ns_hardware_platform=module.params['ns_hardware_platform'],
ns_os_version=module.params['ns_os_version'],
user_certificate=module.params['user_certificate'],
mac_address=module.params['mac_address'],
random_password=module.params.get('random_password'),
)
changed = False
if state in ['present', 'enabled', 'disabled']:
if not ipa_host:
changed = True
if not module.check_mode:
# OTP password generated by FreeIPA is visible only for host_add command
# so, return directly from here.
return changed, client.host_add(name=name, host=module_host)
else:
diff = get_host_diff(client, ipa_host, module_host)
if len(diff) > 0:
changed = True
if not module.check_mode:
data = {}
for key in diff:
data[key] = module_host.get(key)
ipa_host_show = client.host_show(name=name)
if ipa_host_show.get('has_keytab', False) and module.params.get('random_password'):
client.host_disable(name=name)
return changed, client.host_mod(name=name, host=data)
else:
if ipa_host:
changed = True
update_dns = module.params.get('update_dns', False)
if not module.check_mode:
client.host_del(name=name, update_dns=update_dns)
return changed, client.host_find(name=name)
def main():
argument_spec = ipa_argument_spec()
argument_spec.update(description=dict(type='str'),
fqdn=dict(type='str', required=True, aliases=['name']),
force=dict(type='bool'),
ip_address=dict(type='str'),
ns_host_location=dict(type='str', aliases=['nshostlocation']),
ns_hardware_platform=dict(type='str', aliases=['nshardwareplatform']),
ns_os_version=dict(type='str', aliases=['nsosversion']),
user_certificate=dict(type='list', aliases=['usercertificate'], elements='str'),
mac_address=dict(type='list', aliases=['macaddress'], elements='str'),
update_dns=dict(type='bool'),
state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']),
random_password=dict(type='bool'),)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
client = HostIPAClient(module=module,
host=module.params['ipa_host'],
port=module.params['ipa_port'],
protocol=module.params['ipa_prot'])
try:
client.login(username=module.params['ipa_user'],
password=module.params['ipa_pass'])
changed, host = ensure(module, client)
module.exit_json(changed=changed, host=host)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,213 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: ipa_hostgroup
author: Thomas Krahn (@Nosmoht)
short_description: Manage FreeIPA host-group
description:
- Add, modify and delete an IPA host-group using IPA API.
options:
cn:
description:
- Name of host-group.
- Can not be changed as it is the unique identifier.
required: true
aliases: ["name"]
type: str
description:
description:
- Description.
type: str
host:
description:
- List of hosts that belong to the host-group.
- If an empty list is passed all hosts will be removed from the group.
- If option is omitted hosts will not be checked or changed.
- If option is passed all assigned hosts that are not passed will be unassigned from the group.
type: list
elements: str
hostgroup:
description:
- List of host-groups than belong to that host-group.
- If an empty list is passed all host-groups will be removed from the group.
- If option is omitted host-groups will not be checked or changed.
- If option is passed all assigned hostgroups that are not passed will be unassigned from the group.
type: list
elements: str
state:
description:
- State to ensure.
default: "present"
choices: ["absent", "disabled", "enabled", "present"]
type: str
extends_documentation_fragment:
- community.general.ipa.documentation
'''
EXAMPLES = r'''
- name: Ensure host-group databases is present
ipa_hostgroup:
name: databases
state: present
host:
- db.example.com
hostgroup:
- mysql-server
- oracle-server
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Ensure host-group databases is absent
ipa_hostgroup:
name: databases
state: absent
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
'''
RETURN = r'''
hostgroup:
description: Hostgroup as returned by IPA API.
returned: always
type: dict
'''
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
from ansible.module_utils._text import to_native
class HostGroupIPAClient(IPAClient):
def __init__(self, module, host, port, protocol):
super(HostGroupIPAClient, self).__init__(module, host, port, protocol)
def hostgroup_find(self, name):
return self._post_json(method='hostgroup_find', name=None, item={'all': True, 'cn': name})
def hostgroup_add(self, name, item):
return self._post_json(method='hostgroup_add', name=name, item=item)
def hostgroup_mod(self, name, item):
return self._post_json(method='hostgroup_mod', name=name, item=item)
def hostgroup_del(self, name):
return self._post_json(method='hostgroup_del', name=name)
def hostgroup_add_member(self, name, item):
return self._post_json(method='hostgroup_add_member', name=name, item=item)
def hostgroup_add_host(self, name, item):
return self.hostgroup_add_member(name=name, item={'host': item})
def hostgroup_add_hostgroup(self, name, item):
return self.hostgroup_add_member(name=name, item={'hostgroup': item})
def hostgroup_remove_member(self, name, item):
return self._post_json(method='hostgroup_remove_member', name=name, item=item)
def hostgroup_remove_host(self, name, item):
return self.hostgroup_remove_member(name=name, item={'host': item})
def hostgroup_remove_hostgroup(self, name, item):
return self.hostgroup_remove_member(name=name, item={'hostgroup': item})
def get_hostgroup_dict(description=None):
data = {}
if description is not None:
data['description'] = description
return data
def get_hostgroup_diff(client, ipa_hostgroup, module_hostgroup):
return client.get_diff(ipa_data=ipa_hostgroup, module_data=module_hostgroup)
def ensure(module, client):
name = module.params['cn']
state = module.params['state']
host = module.params['host']
hostgroup = module.params['hostgroup']
ipa_hostgroup = client.hostgroup_find(name=name)
module_hostgroup = get_hostgroup_dict(description=module.params['description'])
changed = False
if state == 'present':
if not ipa_hostgroup:
changed = True
if not module.check_mode:
ipa_hostgroup = client.hostgroup_add(name=name, item=module_hostgroup)
else:
diff = get_hostgroup_diff(client, ipa_hostgroup, module_hostgroup)
if len(diff) > 0:
changed = True
if not module.check_mode:
data = {}
for key in diff:
data[key] = module_hostgroup.get(key)
client.hostgroup_mod(name=name, item=data)
if host is not None:
changed = client.modify_if_diff(name, ipa_hostgroup.get('member_host', []), [item.lower() for item in host],
client.hostgroup_add_host, client.hostgroup_remove_host) or changed
if hostgroup is not None:
changed = client.modify_if_diff(name, ipa_hostgroup.get('member_hostgroup', []),
[item.lower() for item in hostgroup],
client.hostgroup_add_hostgroup,
client.hostgroup_remove_hostgroup) or changed
else:
if ipa_hostgroup:
changed = True
if not module.check_mode:
client.hostgroup_del(name=name)
return changed, client.hostgroup_find(name=name)
def main():
argument_spec = ipa_argument_spec()
argument_spec.update(cn=dict(type='str', required=True, aliases=['name']),
description=dict(type='str'),
host=dict(type='list', elements='str'),
hostgroup=dict(type='list', elements='str'),
state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']))
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
client = HostGroupIPAClient(module=module,
host=module.params['ipa_host'],
port=module.params['ipa_port'],
protocol=module.params['ipa_prot'])
try:
client.login(username=module.params['ipa_user'],
password=module.params['ipa_pass'])
changed, hostgroup = ensure(module, client)
module.exit_json(changed=changed, hostgroup=hostgroup)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,307 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: ipa_role
author: Thomas Krahn (@Nosmoht)
short_description: Manage FreeIPA role
description:
- Add, modify and delete a role within FreeIPA server using FreeIPA API.
options:
cn:
description:
- Role name.
- Can not be changed as it is the unique identifier.
required: true
aliases: ['name']
type: str
description:
description:
- A description of this role-group.
type: str
group:
description:
- List of group names assign to this role.
- If an empty list is passed all assigned groups will be unassigned from the role.
- If option is omitted groups will not be checked or changed.
- If option is passed all assigned groups that are not passed will be unassigned from the role.
type: list
elements: str
host:
description:
- List of host names to assign.
- If an empty list is passed all assigned hosts will be unassigned from the role.
- If option is omitted hosts will not be checked or changed.
- If option is passed all assigned hosts that are not passed will be unassigned from the role.
type: list
elements: str
hostgroup:
description:
- List of host group names to assign.
- If an empty list is passed all assigned host groups will be removed from the role.
- If option is omitted host groups will not be checked or changed.
- If option is passed all assigned hostgroups that are not passed will be unassigned from the role.
type: list
elements: str
privilege:
description:
- List of privileges granted to the role.
- If an empty list is passed all assigned privileges will be removed.
- If option is omitted privileges will not be checked or changed.
- If option is passed all assigned privileges that are not passed will be removed.
type: list
elements: str
service:
description:
- List of service names to assign.
- If an empty list is passed all assigned services will be removed from the role.
- If option is omitted services will not be checked or changed.
- If option is passed all assigned services that are not passed will be removed from the role.
type: list
elements: str
state:
description: State to ensure.
default: "present"
choices: ["absent", "present"]
type: str
user:
description:
- List of user names to assign.
- If an empty list is passed all assigned users will be removed from the role.
- If option is omitted users will not be checked or changed.
type: list
elements: str
extends_documentation_fragment:
- community.general.ipa.documentation
'''
EXAMPLES = r'''
- name: Ensure role is present
ipa_role:
name: dba
description: Database Administrators
state: present
user:
- pinky
- brain
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Ensure role with certain details
ipa_role:
name: another-role
description: Just another role
group:
- editors
host:
- host01.example.com
hostgroup:
- hostgroup01
privilege:
- Group Administrators
- User Administrators
service:
- service01
- name: Ensure role is absent
ipa_role:
name: dba
state: absent
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
'''
RETURN = r'''
role:
description: Role as returned by IPA API.
returned: always
type: dict
'''
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
from ansible.module_utils._text import to_native
class RoleIPAClient(IPAClient):
def __init__(self, module, host, port, protocol):
super(RoleIPAClient, self).__init__(module, host, port, protocol)
def role_find(self, name):
return self._post_json(method='role_find', name=None, item={'all': True, 'cn': name})
def role_add(self, name, item):
return self._post_json(method='role_add', name=name, item=item)
def role_mod(self, name, item):
return self._post_json(method='role_mod', name=name, item=item)
def role_del(self, name):
return self._post_json(method='role_del', name=name)
def role_add_member(self, name, item):
return self._post_json(method='role_add_member', name=name, item=item)
def role_add_group(self, name, item):
return self.role_add_member(name=name, item={'group': item})
def role_add_host(self, name, item):
return self.role_add_member(name=name, item={'host': item})
def role_add_hostgroup(self, name, item):
return self.role_add_member(name=name, item={'hostgroup': item})
def role_add_service(self, name, item):
return self.role_add_member(name=name, item={'service': item})
def role_add_user(self, name, item):
return self.role_add_member(name=name, item={'user': item})
def role_remove_member(self, name, item):
return self._post_json(method='role_remove_member', name=name, item=item)
def role_remove_group(self, name, item):
return self.role_remove_member(name=name, item={'group': item})
def role_remove_host(self, name, item):
return self.role_remove_member(name=name, item={'host': item})
def role_remove_hostgroup(self, name, item):
return self.role_remove_member(name=name, item={'hostgroup': item})
def role_remove_service(self, name, item):
return self.role_remove_member(name=name, item={'service': item})
def role_remove_user(self, name, item):
return self.role_remove_member(name=name, item={'user': item})
def role_add_privilege(self, name, item):
return self._post_json(method='role_add_privilege', name=name, item={'privilege': item})
def role_remove_privilege(self, name, item):
return self._post_json(method='role_remove_privilege', name=name, item={'privilege': item})
def get_role_dict(description=None):
data = {}
if description is not None:
data['description'] = description
return data
def get_role_diff(client, ipa_role, module_role):
return client.get_diff(ipa_data=ipa_role, module_data=module_role)
def ensure(module, client):
state = module.params['state']
name = module.params['cn']
group = module.params['group']
host = module.params['host']
hostgroup = module.params['hostgroup']
privilege = module.params['privilege']
service = module.params['service']
user = module.params['user']
module_role = get_role_dict(description=module.params['description'])
ipa_role = client.role_find(name=name)
changed = False
if state == 'present':
if not ipa_role:
changed = True
if not module.check_mode:
ipa_role = client.role_add(name=name, item=module_role)
else:
diff = get_role_diff(client, ipa_role, module_role)
if len(diff) > 0:
changed = True
if not module.check_mode:
data = {}
for key in diff:
data[key] = module_role.get(key)
client.role_mod(name=name, item=data)
if group is not None:
changed = client.modify_if_diff(name, ipa_role.get('member_group', []), group,
client.role_add_group,
client.role_remove_group) or changed
if host is not None:
changed = client.modify_if_diff(name, ipa_role.get('member_host', []), host,
client.role_add_host,
client.role_remove_host) or changed
if hostgroup is not None:
changed = client.modify_if_diff(name, ipa_role.get('member_hostgroup', []), hostgroup,
client.role_add_hostgroup,
client.role_remove_hostgroup) or changed
if privilege is not None:
changed = client.modify_if_diff(name, ipa_role.get('memberof_privilege', []), privilege,
client.role_add_privilege,
client.role_remove_privilege) or changed
if service is not None:
changed = client.modify_if_diff(name, ipa_role.get('member_service', []), service,
client.role_add_service,
client.role_remove_service) or changed
if user is not None:
changed = client.modify_if_diff(name, ipa_role.get('member_user', []), user,
client.role_add_user,
client.role_remove_user) or changed
else:
if ipa_role:
changed = True
if not module.check_mode:
client.role_del(name)
return changed, client.role_find(name=name)
def main():
argument_spec = ipa_argument_spec()
argument_spec.update(cn=dict(type='str', required=True, aliases=['name']),
description=dict(type='str'),
group=dict(type='list', elements='str'),
host=dict(type='list', elements='str'),
hostgroup=dict(type='list', elements='str'),
privilege=dict(type='list', elements='str'),
service=dict(type='list', elements='str'),
state=dict(type='str', default='present', choices=['present', 'absent']),
user=dict(type='list', elements='str'))
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
client = RoleIPAClient(module=module,
host=module.params['ipa_host'],
port=module.params['ipa_port'],
protocol=module.params['ipa_prot'])
try:
client.login(username=module.params['ipa_user'],
password=module.params['ipa_pass'])
changed, role = ensure(module, client)
module.exit_json(changed=changed, role=role)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,213 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: ipa_service
author: Cédric Parent (@cprh)
short_description: Manage FreeIPA service
description:
- Add and delete an IPA service using IPA API.
options:
krbcanonicalname:
description:
- Principal of the service.
- Can not be changed as it is the unique identifier.
required: true
aliases: ["name"]
type: str
hosts:
description:
- Defines the list of 'ManagedBy' hosts.
required: false
type: list
elements: str
force:
description:
- Force principal name even if host is not in DNS.
required: false
type: bool
state:
description: State to ensure.
required: false
default: present
choices: ["absent", "present"]
type: str
extends_documentation_fragment:
- community.general.ipa.documentation
'''
EXAMPLES = r'''
- name: Ensure service is present
ipa_service:
name: http/host01.example.com
state: present
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Ensure service is absent
ipa_service:
name: http/host01.example.com
state: absent
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Changing Managing hosts list
ipa_service:
name: http/host01.example.com
host:
- host01.example.com
- host02.example.com
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
'''
RETURN = r'''
service:
description: Service as returned by IPA API.
returned: always
type: dict
'''
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
from ansible.module_utils._text import to_native
class ServiceIPAClient(IPAClient):
def __init__(self, module, host, port, protocol):
super(ServiceIPAClient, self).__init__(module, host, port, protocol)
def service_find(self, name):
return self._post_json(method='service_find', name=None, item={'all': True, 'krbcanonicalname': name})
def service_add(self, name, service):
return self._post_json(method='service_add', name=name, item=service)
def service_mod(self, name, service):
return self._post_json(method='service_mod', name=name, item=service)
def service_del(self, name):
return self._post_json(method='service_del', name=name)
def service_disable(self, name):
return self._post_json(method='service_disable', name=name)
def service_add_host(self, name, item):
return self._post_json(method='service_add_host', name=name, item={'host': item})
def service_remove_host(self, name, item):
return self._post_json(method='service_remove_host', name=name, item={'host': item})
def get_service_dict(force=None, krbcanonicalname=None):
data = {}
if force is not None:
data['force'] = force
if krbcanonicalname is not None:
data['krbcanonicalname'] = krbcanonicalname
return data
def get_service_diff(client, ipa_host, module_service):
non_updateable_keys = ['force', 'krbcanonicalname']
for key in non_updateable_keys:
if key in module_service:
del module_service[key]
return client.get_diff(ipa_data=ipa_host, module_data=module_service)
def ensure(module, client):
name = module.params['krbcanonicalname']
state = module.params['state']
hosts = module.params['hosts']
ipa_service = client.service_find(name=name)
module_service = get_service_dict(force=module.params['force'])
changed = False
if state in ['present', 'enabled', 'disabled']:
if not ipa_service:
changed = True
if not module.check_mode:
client.service_add(name=name, service=module_service)
else:
diff = get_service_diff(client, ipa_service, module_service)
if len(diff) > 0:
changed = True
if not module.check_mode:
data = {}
for key in diff:
data[key] = module_service.get(key)
client.service_mod(name=name, service=data)
if hosts is not None:
if 'managedby_host' in ipa_service:
for host in ipa_service['managedby_host']:
if host not in hosts:
if not module.check_mode:
client.service_remove_host(name=name, item=host)
changed = True
for host in hosts:
if host not in ipa_service['managedby_host']:
if not module.check_mode:
client.service_add_host(name=name, item=host)
changed = True
else:
for host in hosts:
if not module.check_mode:
client.service_add_host(name=name, item=host)
changed = True
else:
if ipa_service:
changed = True
if not module.check_mode:
client.service_del(name=name)
return changed, client.service_find(name=name)
def main():
argument_spec = ipa_argument_spec()
argument_spec.update(
krbcanonicalname=dict(type='str', required=True, aliases=['name']),
force=dict(type='bool', required=False),
hosts=dict(type='list', required=False, elements='str'),
state=dict(type='str', required=False, default='present',
choices=['present', 'absent']))
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
client = ServiceIPAClient(module=module,
host=module.params['ipa_host'],
port=module.params['ipa_port'],
protocol=module.params['ipa_prot'])
try:
client.login(username=module.params['ipa_user'],
password=module.params['ipa_pass'])
changed, host = ensure(module, client)
module.exit_json(changed=changed, host=host)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,216 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Abhijeet Kasurde (akasurde@redhat.com)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: ipa_subca
author: Abhijeet Kasurde (@Akasurde)
short_description: Manage FreeIPA Lightweight Sub Certificate Authorities.
description:
- Add, modify, enable, disable and delete an IPA Lightweight Sub Certificate Authorities using IPA API.
options:
subca_name:
description:
- The Sub Certificate Authority name which needs to be managed.
required: true
aliases: ["name"]
type: str
subca_subject:
description:
- The Sub Certificate Authority's Subject. e.g., 'CN=SampleSubCA1,O=testrelm.test'.
required: true
type: str
subca_desc:
description:
- The Sub Certificate Authority's description.
type: str
state:
description:
- State to ensure.
- State 'disable' and 'enable' is available for FreeIPA 4.4.2 version and onwards.
required: false
default: present
choices: ["absent", "disabled", "enabled", "present"]
type: str
extends_documentation_fragment:
- community.general.ipa.documentation
'''
EXAMPLES = '''
- name: Ensure IPA Sub CA is present
ipa_subca:
ipa_host: spider.example.com
ipa_pass: Passw0rd!
state: present
subca_name: AnsibleSubCA1
subca_subject: 'CN=AnsibleSubCA1,O=example.com'
subca_desc: Ansible Sub CA
- name: Ensure that IPA Sub CA is removed
ipa_subca:
ipa_host: spider.example.com
ipa_pass: Passw0rd!
state: absent
subca_name: AnsibleSubCA1
- name: Ensure that IPA Sub CA is disabled
ipa_subca:
ipa_host: spider.example.com
ipa_pass: Passw0rd!
state: disable
subca_name: AnsibleSubCA1
'''
RETURN = r'''
subca:
description: IPA Sub CA record as returned by IPA API.
returned: always
type: dict
'''
from distutils.version import LooseVersion
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
from ansible.module_utils._text import to_native
class SubCAIPAClient(IPAClient):
def __init__(self, module, host, port, protocol):
super(SubCAIPAClient, self).__init__(module, host, port, protocol)
def subca_find(self, subca_name):
return self._post_json(method='ca_find', name=subca_name, item=None)
def subca_add(self, subca_name=None, subject_dn=None, details=None):
item = dict(ipacasubjectdn=subject_dn)
subca_desc = details.get('description', None)
if subca_desc is not None:
item.update(description=subca_desc)
return self._post_json(method='ca_add', name=subca_name, item=item)
def subca_mod(self, subca_name=None, diff=None, details=None):
item = get_subca_dict(details)
for change in diff:
update_detail = dict()
if item[change] is not None:
update_detail.update(setattr="{0}={1}".format(change, item[change]))
self._post_json(method='ca_mod', name=subca_name, item=update_detail)
def subca_del(self, subca_name=None):
return self._post_json(method='ca_del', name=subca_name)
def subca_disable(self, subca_name=None):
return self._post_json(method='ca_disable', name=subca_name)
def subca_enable(self, subca_name=None):
return self._post_json(method='ca_enable', name=subca_name)
def get_subca_dict(details=None):
module_subca = dict()
if details['description'] is not None:
module_subca['description'] = details['description']
if details['subca_subject'] is not None:
module_subca['ipacasubjectdn'] = details['subca_subject']
return module_subca
def get_subca_diff(client, ipa_subca, module_subca):
details = get_subca_dict(module_subca)
return client.get_diff(ipa_data=ipa_subca, module_data=details)
def ensure(module, client):
subca_name = module.params['subca_name']
subca_subject_dn = module.params['subca_subject']
subca_desc = module.params['subca_desc']
state = module.params['state']
ipa_subca = client.subca_find(subca_name)
module_subca = dict(description=subca_desc,
subca_subject=subca_subject_dn)
changed = False
if state == 'present':
if not ipa_subca:
changed = True
if not module.check_mode:
client.subca_add(subca_name=subca_name, subject_dn=subca_subject_dn, details=module_subca)
else:
diff = get_subca_diff(client, ipa_subca, module_subca)
# IPA does not allow to modify Sub CA's subject DN
# So skip it for now.
if 'ipacasubjectdn' in diff:
diff.remove('ipacasubjectdn')
del module_subca['subca_subject']
if len(diff) > 0:
changed = True
if not module.check_mode:
client.subca_mod(subca_name=subca_name, diff=diff, details=module_subca)
elif state == 'absent':
if ipa_subca:
changed = True
if not module.check_mode:
client.subca_del(subca_name=subca_name)
elif state == 'disable':
ipa_version = client.get_ipa_version()
if LooseVersion(ipa_version) < LooseVersion('4.4.2'):
module.fail_json(msg="Current version of IPA server [%s] does not support 'CA disable' option. Please upgrade to "
"version greater than 4.4.2")
if ipa_subca:
changed = True
if not module.check_mode:
client.subca_disable(subca_name=subca_name)
elif state == 'enable':
ipa_version = client.get_ipa_version()
if LooseVersion(ipa_version) < LooseVersion('4.4.2'):
module.fail_json(msg="Current version of IPA server [%s] does not support 'CA enable' option. Please upgrade to "
"version greater than 4.4.2")
if ipa_subca:
changed = True
if not module.check_mode:
client.subca_enable(subca_name=subca_name)
return changed, client.subca_find(subca_name)
def main():
argument_spec = ipa_argument_spec()
argument_spec.update(subca_name=dict(type='str', required=True, aliases=['name']),
subca_subject=dict(type='str', required=True),
subca_desc=dict(type='str'),
state=dict(type='str', default='present',
choices=['present', 'absent', 'enabled', 'disabled']),)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True,)
client = SubCAIPAClient(module=module,
host=module.params['ipa_host'],
port=module.params['ipa_port'],
protocol=module.params['ipa_prot'])
try:
client.login(username=module.params['ipa_user'],
password=module.params['ipa_pass'])
changed, record = ensure(module, client)
module.exit_json(changed=changed, record=record)
except Exception as exc:
module.fail_json(msg=to_native(exc))
if __name__ == '__main__':
main()

View file

@ -0,0 +1,156 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: ipa_sudocmd
author: Thomas Krahn (@Nosmoht)
short_description: Manage FreeIPA sudo command
description:
- Add, modify or delete sudo command within FreeIPA server using FreeIPA API.
options:
sudocmd:
description:
- Sudo command.
aliases: ['name']
required: true
type: str
description:
description:
- A description of this command.
type: str
state:
description: State to ensure.
default: present
choices: ['absent', 'disabled', 'enabled', 'present']
type: str
extends_documentation_fragment:
- community.general.ipa.documentation
'''
EXAMPLES = r'''
- name: Ensure sudo command exists
ipa_sudocmd:
name: su
description: Allow to run su via sudo
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Ensure sudo command does not exist
ipa_sudocmd:
name: su
state: absent
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
'''
RETURN = r'''
sudocmd:
description: Sudo command as return from IPA API
returned: always
type: dict
'''
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
from ansible.module_utils._text import to_native
class SudoCmdIPAClient(IPAClient):
def __init__(self, module, host, port, protocol):
super(SudoCmdIPAClient, self).__init__(module, host, port, protocol)
def sudocmd_find(self, name):
return self._post_json(method='sudocmd_find', name=None, item={'all': True, 'sudocmd': name})
def sudocmd_add(self, name, item):
return self._post_json(method='sudocmd_add', name=name, item=item)
def sudocmd_mod(self, name, item):
return self._post_json(method='sudocmd_mod', name=name, item=item)
def sudocmd_del(self, name):
return self._post_json(method='sudocmd_del', name=name)
def get_sudocmd_dict(description=None):
data = {}
if description is not None:
data['description'] = description
return data
def get_sudocmd_diff(client, ipa_sudocmd, module_sudocmd):
return client.get_diff(ipa_data=ipa_sudocmd, module_data=module_sudocmd)
def ensure(module, client):
name = module.params['sudocmd']
state = module.params['state']
module_sudocmd = get_sudocmd_dict(description=module.params['description'])
ipa_sudocmd = client.sudocmd_find(name=name)
changed = False
if state == 'present':
if not ipa_sudocmd:
changed = True
if not module.check_mode:
client.sudocmd_add(name=name, item=module_sudocmd)
else:
diff = get_sudocmd_diff(client, ipa_sudocmd, module_sudocmd)
if len(diff) > 0:
changed = True
if not module.check_mode:
data = {}
for key in diff:
data[key] = module_sudocmd.get(key)
client.sudocmd_mod(name=name, item=data)
else:
if ipa_sudocmd:
changed = True
if not module.check_mode:
client.sudocmd_del(name=name)
return changed, client.sudocmd_find(name=name)
def main():
argument_spec = ipa_argument_spec()
argument_spec.update(description=dict(type='str'),
state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']),
sudocmd=dict(type='str', required=True, aliases=['name']))
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
client = SudoCmdIPAClient(module=module,
host=module.params['ipa_host'],
port=module.params['ipa_port'],
protocol=module.params['ipa_prot'])
try:
client.login(username=module.params['ipa_user'],
password=module.params['ipa_pass'])
changed, sudocmd = ensure(module, client)
module.exit_json(changed=changed, sudocmd=sudocmd)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,184 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: ipa_sudocmdgroup
author: Thomas Krahn (@Nosmoht)
short_description: Manage FreeIPA sudo command group
description:
- Add, modify or delete sudo command group within IPA server using IPA API.
options:
cn:
description:
- Sudo Command Group.
aliases: ['name']
required: true
type: str
description:
description:
- Group description.
type: str
state:
description: State to ensure.
default: present
choices: ['absent', 'disabled', 'enabled', 'present']
type: str
sudocmd:
description:
- List of sudo commands to assign to the group.
- If an empty list is passed all assigned commands will be removed from the group.
- If option is omitted sudo commands will not be checked or changed.
type: list
elements: str
extends_documentation_fragment:
- community.general.ipa.documentation
'''
EXAMPLES = r'''
- name: Ensure sudo command group exists
ipa_sudocmdgroup:
name: group01
description: Group of important commands
sudocmd:
- su
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Ensure sudo command group does not exist
ipa_sudocmdgroup:
name: group01
state: absent
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
'''
RETURN = r'''
sudocmdgroup:
description: Sudo command group as returned by IPA API
returned: always
type: dict
'''
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
from ansible.module_utils._text import to_native
class SudoCmdGroupIPAClient(IPAClient):
def __init__(self, module, host, port, protocol):
super(SudoCmdGroupIPAClient, self).__init__(module, host, port, protocol)
def sudocmdgroup_find(self, name):
return self._post_json(method='sudocmdgroup_find', name=None, item={'all': True, 'cn': name})
def sudocmdgroup_add(self, name, item):
return self._post_json(method='sudocmdgroup_add', name=name, item=item)
def sudocmdgroup_mod(self, name, item):
return self._post_json(method='sudocmdgroup_mod', name=name, item=item)
def sudocmdgroup_del(self, name):
return self._post_json(method='sudocmdgroup_del', name=name)
def sudocmdgroup_add_member(self, name, item):
return self._post_json(method='sudocmdgroup_add_member', name=name, item=item)
def sudocmdgroup_add_member_sudocmd(self, name, item):
return self.sudocmdgroup_add_member(name=name, item={'sudocmd': item})
def sudocmdgroup_remove_member(self, name, item):
return self._post_json(method='sudocmdgroup_remove_member', name=name, item=item)
def sudocmdgroup_remove_member_sudocmd(self, name, item):
return self.sudocmdgroup_remove_member(name=name, item={'sudocmd': item})
def get_sudocmdgroup_dict(description=None):
data = {}
if description is not None:
data['description'] = description
return data
def get_sudocmdgroup_diff(client, ipa_sudocmdgroup, module_sudocmdgroup):
return client.get_diff(ipa_data=ipa_sudocmdgroup, module_data=module_sudocmdgroup)
def ensure(module, client):
name = module.params['cn']
state = module.params['state']
sudocmd = module.params['sudocmd']
module_sudocmdgroup = get_sudocmdgroup_dict(description=module.params['description'])
ipa_sudocmdgroup = client.sudocmdgroup_find(name=name)
changed = False
if state == 'present':
if not ipa_sudocmdgroup:
changed = True
if not module.check_mode:
ipa_sudocmdgroup = client.sudocmdgroup_add(name=name, item=module_sudocmdgroup)
else:
diff = get_sudocmdgroup_diff(client, ipa_sudocmdgroup, module_sudocmdgroup)
if len(diff) > 0:
changed = True
if not module.check_mode:
data = {}
for key in diff:
data[key] = module_sudocmdgroup.get(key)
client.sudocmdgroup_mod(name=name, item=data)
if sudocmd is not None:
changed = client.modify_if_diff(name, ipa_sudocmdgroup.get('member_sudocmd', []), sudocmd,
client.sudocmdgroup_add_member_sudocmd,
client.sudocmdgroup_remove_member_sudocmd)
else:
if ipa_sudocmdgroup:
changed = True
if not module.check_mode:
client.sudocmdgroup_del(name=name)
return changed, client.sudocmdgroup_find(name=name)
def main():
argument_spec = ipa_argument_spec()
argument_spec.update(cn=dict(type='str', required=True, aliases=['name']),
description=dict(type='str'),
state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']),
sudocmd=dict(type='list', elements='str'))
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
client = SudoCmdGroupIPAClient(module=module,
host=module.params['ipa_host'],
port=module.params['ipa_port'],
protocol=module.params['ipa_prot'])
try:
client.login(username=module.params['ipa_user'],
password=module.params['ipa_pass'])
changed, sudocmdgroup = ensure(module, client)
module.exit_json(changed=changed, sudorule=sudocmdgroup)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,405 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: ipa_sudorule
author: Thomas Krahn (@Nosmoht)
short_description: Manage FreeIPA sudo rule
description:
- Add, modify or delete sudo rule within IPA server using IPA API.
options:
cn:
description:
- Canonical name.
- Can not be changed as it is the unique identifier.
required: true
aliases: ['name']
type: str
cmdcategory:
description:
- Command category the rule applies to.
choices: ['all']
type: str
cmd:
description:
- List of commands assigned to the rule.
- If an empty list is passed all commands will be removed from the rule.
- If option is omitted commands will not be checked or changed.
type: list
elements: str
description:
description:
- Description of the sudo rule.
type: str
host:
description:
- List of hosts assigned to the rule.
- If an empty list is passed all hosts will be removed from the rule.
- If option is omitted hosts will not be checked or changed.
- Option C(hostcategory) must be omitted to assign hosts.
type: list
elements: str
hostcategory:
description:
- Host category the rule applies to.
- If 'all' is passed one must omit C(host) and C(hostgroup).
- Option C(host) and C(hostgroup) must be omitted to assign 'all'.
choices: ['all']
type: str
hostgroup:
description:
- List of host groups assigned to the rule.
- If an empty list is passed all host groups will be removed from the rule.
- If option is omitted host groups will not be checked or changed.
- Option C(hostcategory) must be omitted to assign host groups.
type: list
elements: str
runasusercategory:
description:
- RunAs User category the rule applies to.
choices: ['all']
type: str
runasgroupcategory:
description:
- RunAs Group category the rule applies to.
choices: ['all']
type: str
sudoopt:
description:
- List of options to add to the sudo rule.
type: list
elements: str
user:
description:
- List of users assigned to the rule.
- If an empty list is passed all users will be removed from the rule.
- If option is omitted users will not be checked or changed.
type: list
elements: str
usercategory:
description:
- User category the rule applies to.
choices: ['all']
type: str
usergroup:
description:
- List of user groups assigned to the rule.
- If an empty list is passed all user groups will be removed from the rule.
- If option is omitted user groups will not be checked or changed.
type: list
elements: str
state:
description: State to ensure.
default: present
choices: ['absent', 'disabled', 'enabled', 'present']
type: str
extends_documentation_fragment:
- community.general.ipa.documentation
'''
EXAMPLES = r'''
- name: Ensure sudo rule is present that's allows all every body to execute any command on any host without being asked for a password.
ipa_sudorule:
name: sudo_all_nopasswd
cmdcategory: all
description: Allow to run every command with sudo without password
hostcategory: all
sudoopt:
- '!authenticate'
usercategory: all
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Ensure user group developers can run every command on host group db-server as well as on host db01.example.com.
ipa_sudorule:
name: sudo_dev_dbserver
description: Allow developers to run every command with sudo on all database server
cmdcategory: all
host:
- db01.example.com
hostgroup:
- db-server
sudoopt:
- '!authenticate'
usergroup:
- developers
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
'''
RETURN = r'''
sudorule:
description: Sudorule as returned by IPA
returned: always
type: dict
'''
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
from ansible.module_utils._text import to_native
class SudoRuleIPAClient(IPAClient):
def __init__(self, module, host, port, protocol):
super(SudoRuleIPAClient, self).__init__(module, host, port, protocol)
def sudorule_find(self, name):
return self._post_json(method='sudorule_find', name=None, item={'all': True, 'cn': name})
def sudorule_add(self, name, item):
return self._post_json(method='sudorule_add', name=name, item=item)
def sudorule_mod(self, name, item):
return self._post_json(method='sudorule_mod', name=name, item=item)
def sudorule_del(self, name):
return self._post_json(method='sudorule_del', name=name)
def sudorule_add_option(self, name, item):
return self._post_json(method='sudorule_add_option', name=name, item=item)
def sudorule_add_option_ipasudoopt(self, name, item):
return self.sudorule_add_option(name=name, item={'ipasudoopt': item})
def sudorule_remove_option(self, name, item):
return self._post_json(method='sudorule_remove_option', name=name, item=item)
def sudorule_remove_option_ipasudoopt(self, name, item):
return self.sudorule_remove_option(name=name, item={'ipasudoopt': item})
def sudorule_add_host(self, name, item):
return self._post_json(method='sudorule_add_host', name=name, item=item)
def sudorule_add_host_host(self, name, item):
return self.sudorule_add_host(name=name, item={'host': item})
def sudorule_add_host_hostgroup(self, name, item):
return self.sudorule_add_host(name=name, item={'hostgroup': item})
def sudorule_remove_host(self, name, item):
return self._post_json(method='sudorule_remove_host', name=name, item=item)
def sudorule_remove_host_host(self, name, item):
return self.sudorule_remove_host(name=name, item={'host': item})
def sudorule_remove_host_hostgroup(self, name, item):
return self.sudorule_remove_host(name=name, item={'hostgroup': item})
def sudorule_add_allow_command(self, name, item):
return self._post_json(method='sudorule_add_allow_command', name=name, item={'sudocmd': item})
def sudorule_remove_allow_command(self, name, item):
return self._post_json(method='sudorule_remove_allow_command', name=name, item=item)
def sudorule_add_user(self, name, item):
return self._post_json(method='sudorule_add_user', name=name, item=item)
def sudorule_add_user_user(self, name, item):
return self.sudorule_add_user(name=name, item={'user': item})
def sudorule_add_user_group(self, name, item):
return self.sudorule_add_user(name=name, item={'group': item})
def sudorule_remove_user(self, name, item):
return self._post_json(method='sudorule_remove_user', name=name, item=item)
def sudorule_remove_user_user(self, name, item):
return self.sudorule_remove_user(name=name, item={'user': item})
def sudorule_remove_user_group(self, name, item):
return self.sudorule_remove_user(name=name, item={'group': item})
def get_sudorule_dict(cmdcategory=None, description=None, hostcategory=None, ipaenabledflag=None, usercategory=None,
runasgroupcategory=None, runasusercategory=None):
data = {}
if cmdcategory is not None:
data['cmdcategory'] = cmdcategory
if description is not None:
data['description'] = description
if hostcategory is not None:
data['hostcategory'] = hostcategory
if ipaenabledflag is not None:
data['ipaenabledflag'] = ipaenabledflag
if usercategory is not None:
data['usercategory'] = usercategory
if runasusercategory is not None:
data['ipasudorunasusercategory'] = runasusercategory
if runasgroupcategory is not None:
data['ipasudorunasgroupcategory'] = runasgroupcategory
return data
def category_changed(module, client, category_name, ipa_sudorule):
if ipa_sudorule.get(category_name, None) == ['all']:
if not module.check_mode:
# cn is returned as list even with only a single value.
client.sudorule_mod(name=ipa_sudorule.get('cn')[0], item={category_name: None})
return True
return False
def ensure(module, client):
state = module.params['state']
name = module.params['cn']
cmd = module.params['cmd']
cmdcategory = module.params['cmdcategory']
host = module.params['host']
hostcategory = module.params['hostcategory']
hostgroup = module.params['hostgroup']
runasusercategory = module.params['runasusercategory']
runasgroupcategory = module.params['runasgroupcategory']
if state in ['present', 'enabled']:
ipaenabledflag = 'TRUE'
else:
ipaenabledflag = 'FALSE'
sudoopt = module.params['sudoopt']
user = module.params['user']
usercategory = module.params['usercategory']
usergroup = module.params['usergroup']
module_sudorule = get_sudorule_dict(cmdcategory=cmdcategory,
description=module.params['description'],
hostcategory=hostcategory,
ipaenabledflag=ipaenabledflag,
usercategory=usercategory,
runasusercategory=runasusercategory,
runasgroupcategory=runasgroupcategory)
ipa_sudorule = client.sudorule_find(name=name)
changed = False
if state in ['present', 'disabled', 'enabled']:
if not ipa_sudorule:
changed = True
if not module.check_mode:
ipa_sudorule = client.sudorule_add(name=name, item=module_sudorule)
else:
diff = client.get_diff(ipa_sudorule, module_sudorule)
if len(diff) > 0:
changed = True
if not module.check_mode:
if 'hostcategory' in diff:
if ipa_sudorule.get('memberhost_host', None) is not None:
client.sudorule_remove_host_host(name=name, item=ipa_sudorule.get('memberhost_host'))
if ipa_sudorule.get('memberhost_hostgroup', None) is not None:
client.sudorule_remove_host_hostgroup(name=name,
item=ipa_sudorule.get('memberhost_hostgroup'))
client.sudorule_mod(name=name, item=module_sudorule)
if cmd is not None:
changed = category_changed(module, client, 'cmdcategory', ipa_sudorule) or changed
if not module.check_mode:
client.sudorule_add_allow_command(name=name, item=cmd)
if runasusercategory is not None:
changed = category_changed(module, client, 'iparunasusercategory', ipa_sudorule) or changed
if runasgroupcategory is not None:
changed = category_changed(module, client, 'iparunasgroupcategory', ipa_sudorule) or changed
if host is not None:
changed = category_changed(module, client, 'hostcategory', ipa_sudorule) or changed
changed = client.modify_if_diff(name, ipa_sudorule.get('memberhost_host', []), host,
client.sudorule_add_host_host,
client.sudorule_remove_host_host) or changed
if hostgroup is not None:
changed = category_changed(module, client, 'hostcategory', ipa_sudorule) or changed
changed = client.modify_if_diff(name, ipa_sudorule.get('memberhost_hostgroup', []), hostgroup,
client.sudorule_add_host_hostgroup,
client.sudorule_remove_host_hostgroup) or changed
if sudoopt is not None:
# client.modify_if_diff does not work as each option must be removed/added by its own
ipa_list = ipa_sudorule.get('ipasudoopt', [])
module_list = sudoopt
diff = list(set(ipa_list) - set(module_list))
if len(diff) > 0:
changed = True
if not module.check_mode:
for item in diff:
client.sudorule_remove_option_ipasudoopt(name, item)
diff = list(set(module_list) - set(ipa_list))
if len(diff) > 0:
changed = True
if not module.check_mode:
for item in diff:
client.sudorule_add_option_ipasudoopt(name, item)
if user is not None:
changed = category_changed(module, client, 'usercategory', ipa_sudorule) or changed
changed = client.modify_if_diff(name, ipa_sudorule.get('memberuser_user', []), user,
client.sudorule_add_user_user,
client.sudorule_remove_user_user) or changed
if usergroup is not None:
changed = category_changed(module, client, 'usercategory', ipa_sudorule) or changed
changed = client.modify_if_diff(name, ipa_sudorule.get('memberuser_group', []), usergroup,
client.sudorule_add_user_group,
client.sudorule_remove_user_group) or changed
else:
if ipa_sudorule:
changed = True
if not module.check_mode:
client.sudorule_del(name)
return changed, client.sudorule_find(name)
def main():
argument_spec = ipa_argument_spec()
argument_spec.update(cmd=dict(type='list', elements='str'),
cmdcategory=dict(type='str', choices=['all']),
cn=dict(type='str', required=True, aliases=['name']),
description=dict(type='str'),
host=dict(type='list', elements='str'),
hostcategory=dict(type='str', choices=['all']),
hostgroup=dict(type='list', elements='str'),
runasusercategory=dict(type='str', choices=['all']),
runasgroupcategory=dict(type='str', choices=['all']),
sudoopt=dict(type='list', elements='str'),
state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']),
user=dict(type='list', elements='str'),
usercategory=dict(type='str', choices=['all']),
usergroup=dict(type='list', elements='str'))
module = AnsibleModule(argument_spec=argument_spec,
mutually_exclusive=[['cmdcategory', 'cmd'],
['hostcategory', 'host'],
['hostcategory', 'hostgroup'],
['usercategory', 'user'],
['usercategory', 'usergroup']],
supports_check_mode=True)
client = SudoRuleIPAClient(module=module,
host=module.params['ipa_host'],
port=module.params['ipa_port'],
protocol=module.params['ipa_prot'])
try:
client.login(username=module.params['ipa_user'],
password=module.params['ipa_pass'])
changed, sudorule = ensure(module, client)
module.exit_json(changed=changed, sudorule=sudorule)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,374 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: ipa_user
author: Thomas Krahn (@Nosmoht)
short_description: Manage FreeIPA users
description:
- Add, modify and delete user within IPA server.
options:
displayname:
description: Display name.
type: str
update_password:
description:
- Set password for a user.
type: str
default: 'always'
choices: [ always, on_create ]
givenname:
description: First name.
type: str
krbpasswordexpiration:
description:
- Date at which the user password will expire.
- In the format YYYYMMddHHmmss.
- e.g. 20180121182022 will expire on 21 January 2018 at 18:20:22.
type: str
loginshell:
description: Login shell.
type: str
mail:
description:
- List of mail addresses assigned to the user.
- If an empty list is passed all assigned email addresses will be deleted.
- If None is passed email addresses will not be checked or changed.
type: list
elements: str
password:
description:
- Password for a user.
- Will not be set for an existing user unless I(update_password=always), which is the default.
type: str
sn:
description: Surname.
type: str
sshpubkey:
description:
- List of public SSH key.
- If an empty list is passed all assigned public keys will be deleted.
- If None is passed SSH public keys will not be checked or changed.
type: list
elements: str
state:
description: State to ensure.
default: "present"
choices: ["absent", "disabled", "enabled", "present"]
type: str
telephonenumber:
description:
- List of telephone numbers assigned to the user.
- If an empty list is passed all assigned telephone numbers will be deleted.
- If None is passed telephone numbers will not be checked or changed.
type: list
elements: str
title:
description: Title.
type: str
uid:
description: uid of the user.
required: true
aliases: ["name"]
type: str
uidnumber:
description:
- Account Settings UID/Posix User ID number.
type: str
gidnumber:
description:
- Posix Group ID.
type: str
homedirectory:
description:
- Default home directory of the user.
type: str
extends_documentation_fragment:
- community.general.ipa.documentation
requirements:
- base64
- hashlib
'''
EXAMPLES = r'''
- name: Ensure pinky is present and always reset password
ipa_user:
name: pinky
state: present
krbpasswordexpiration: 20200119235959
givenname: Pinky
sn: Acme
mail:
- pinky@acme.com
telephonenumber:
- '+555123456'
sshpubkey:
- ssh-rsa ....
- ssh-dsa ....
uidnumber: 1001
gidnumber: 100
homedirectory: /home/pinky
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Ensure brain is absent
ipa_user:
name: brain
state: absent
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Ensure pinky is present but don't reset password if already exists
ipa_user:
name: pinky
state: present
givenname: Pinky
sn: Acme
password: zounds
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
update_password: on_create
'''
RETURN = r'''
user:
description: User as returned by IPA API
returned: always
type: dict
'''
import base64
import hashlib
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
from ansible.module_utils._text import to_native
class UserIPAClient(IPAClient):
def __init__(self, module, host, port, protocol):
super(UserIPAClient, self).__init__(module, host, port, protocol)
def user_find(self, name):
return self._post_json(method='user_find', name=None, item={'all': True, 'uid': name})
def user_add(self, name, item):
return self._post_json(method='user_add', name=name, item=item)
def user_mod(self, name, item):
return self._post_json(method='user_mod', name=name, item=item)
def user_del(self, name):
return self._post_json(method='user_del', name=name)
def user_disable(self, name):
return self._post_json(method='user_disable', name=name)
def user_enable(self, name):
return self._post_json(method='user_enable', name=name)
def get_user_dict(displayname=None, givenname=None, krbpasswordexpiration=None, loginshell=None,
mail=None, nsaccountlock=False, sn=None, sshpubkey=None, telephonenumber=None,
title=None, userpassword=None, gidnumber=None, uidnumber=None, homedirectory=None):
user = {}
if displayname is not None:
user['displayname'] = displayname
if krbpasswordexpiration is not None:
user['krbpasswordexpiration'] = krbpasswordexpiration + "Z"
if givenname is not None:
user['givenname'] = givenname
if loginshell is not None:
user['loginshell'] = loginshell
if mail is not None:
user['mail'] = mail
user['nsaccountlock'] = nsaccountlock
if sn is not None:
user['sn'] = sn
if sshpubkey is not None:
user['ipasshpubkey'] = sshpubkey
if telephonenumber is not None:
user['telephonenumber'] = telephonenumber
if title is not None:
user['title'] = title
if userpassword is not None:
user['userpassword'] = userpassword
if gidnumber is not None:
user['gidnumber'] = gidnumber
if uidnumber is not None:
user['uidnumber'] = uidnumber
if homedirectory is not None:
user['homedirectory'] = homedirectory
return user
def get_user_diff(client, ipa_user, module_user):
"""
Return the keys of each dict whereas values are different. Unfortunately the IPA
API returns everything as a list even if only a single value is possible.
Therefore some more complexity is needed.
The method will check if the value type of module_user.attr is not a list and
create a list with that element if the same attribute in ipa_user is list. In this way I hope that the method
must not be changed if the returned API dict is changed.
:param ipa_user:
:param module_user:
:return:
"""
# sshpubkeyfp is the list of ssh key fingerprints. IPA doesn't return the keys itself but instead the fingerprints.
# These are used for comparison.
sshpubkey = None
if 'ipasshpubkey' in module_user:
hash_algo = 'md5'
if 'sshpubkeyfp' in ipa_user and ipa_user['sshpubkeyfp'][0][:7].upper() == 'SHA256:':
hash_algo = 'sha256'
module_user['sshpubkeyfp'] = [get_ssh_key_fingerprint(pubkey, hash_algo) for pubkey in module_user['ipasshpubkey']]
# Remove the ipasshpubkey element as it is not returned from IPA but save it's value to be used later on
sshpubkey = module_user['ipasshpubkey']
del module_user['ipasshpubkey']
result = client.get_diff(ipa_data=ipa_user, module_data=module_user)
# If there are public keys, remove the fingerprints and add them back to the dict
if sshpubkey is not None:
del module_user['sshpubkeyfp']
module_user['ipasshpubkey'] = sshpubkey
return result
def get_ssh_key_fingerprint(ssh_key, hash_algo='sha256'):
"""
Return the public key fingerprint of a given public SSH key
in format "[fp] [user@host] (ssh-rsa)" where fp is of the format:
FB:0C:AC:0A:07:94:5B:CE:75:6E:63:32:13:AD:AD:D7
for md5 or
SHA256:[base64]
for sha256
:param ssh_key:
:param hash_algo:
:return:
"""
parts = ssh_key.strip().split()
if len(parts) == 0:
return None
key_type = parts[0]
key = base64.b64decode(parts[1].encode('ascii'))
if hash_algo == 'md5':
fp_plain = hashlib.md5(key).hexdigest()
key_fp = ':'.join(a + b for a, b in zip(fp_plain[::2], fp_plain[1::2])).upper()
elif hash_algo == 'sha256':
fp_plain = base64.b64encode(hashlib.sha256(key).digest()).decode('ascii').rstrip('=')
key_fp = 'SHA256:{fp}'.format(fp=fp_plain)
if len(parts) < 3:
return "%s (%s)" % (key_fp, key_type)
else:
user_host = parts[2]
return "%s %s (%s)" % (key_fp, user_host, key_type)
def ensure(module, client):
state = module.params['state']
name = module.params['uid']
nsaccountlock = state == 'disabled'
module_user = get_user_dict(displayname=module.params.get('displayname'),
krbpasswordexpiration=module.params.get('krbpasswordexpiration'),
givenname=module.params.get('givenname'),
loginshell=module.params['loginshell'],
mail=module.params['mail'], sn=module.params['sn'],
sshpubkey=module.params['sshpubkey'], nsaccountlock=nsaccountlock,
telephonenumber=module.params['telephonenumber'], title=module.params['title'],
userpassword=module.params['password'],
gidnumber=module.params.get('gidnumber'), uidnumber=module.params.get('uidnumber'),
homedirectory=module.params.get('homedirectory'))
update_password = module.params.get('update_password')
ipa_user = client.user_find(name=name)
changed = False
if state in ['present', 'enabled', 'disabled']:
if not ipa_user:
changed = True
if not module.check_mode:
ipa_user = client.user_add(name=name, item=module_user)
else:
if update_password == 'on_create':
module_user.pop('userpassword', None)
diff = get_user_diff(client, ipa_user, module_user)
if len(diff) > 0:
changed = True
if not module.check_mode:
ipa_user = client.user_mod(name=name, item=module_user)
else:
if ipa_user:
changed = True
if not module.check_mode:
client.user_del(name)
return changed, ipa_user
def main():
argument_spec = ipa_argument_spec()
argument_spec.update(displayname=dict(type='str'),
givenname=dict(type='str'),
update_password=dict(type='str', default="always",
choices=['always', 'on_create']),
krbpasswordexpiration=dict(type='str'),
loginshell=dict(type='str'),
mail=dict(type='list', elements='str'),
sn=dict(type='str'),
uid=dict(type='str', required=True, aliases=['name']),
gidnumber=dict(type='str'),
uidnumber=dict(type='str'),
password=dict(type='str', no_log=True),
sshpubkey=dict(type='list', elements='str'),
state=dict(type='str', default='present',
choices=['present', 'absent', 'enabled', 'disabled']),
telephonenumber=dict(type='list', elements='str'),
title=dict(type='str'),
homedirectory=dict(type='str'))
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
client = UserIPAClient(module=module,
host=module.params['ipa_host'],
port=module.params['ipa_port'],
protocol=module.params['ipa_prot'])
# If sshpubkey is defined as None than module.params['sshpubkey'] is [None]. IPA itself returns None (not a list).
# Therefore a small check here to replace list(None) by None. Otherwise get_user_diff() would return sshpubkey
# as different which should be avoided.
if module.params['sshpubkey'] is not None:
if len(module.params['sshpubkey']) == 1 and module.params['sshpubkey'][0] == "":
module.params['sshpubkey'] = None
try:
client.login(username=module.params['ipa_user'],
password=module.params['ipa_pass'])
changed, user = ensure(module, client)
module.exit_json(changed=changed, user=user)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,253 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Juan Manuel Parrilla <jparrill@redhat.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: ipa_vault
author: Juan Manuel Parrilla (@jparrill)
short_description: Manage FreeIPA vaults
description:
- Add, modify and delete vaults and secret vaults.
- KRA service should be enabled to use this module.
options:
cn:
description:
- Vault name.
- Can not be changed as it is the unique identifier.
required: true
aliases: ["name"]
type: str
description:
description:
- Description.
type: str
ipavaulttype:
description:
- Vault types are based on security level.
default: "symmetric"
choices: ["asymmetric", "standard", "symmetric"]
aliases: ["vault_type"]
type: str
ipavaultpublickey:
description:
- Public key.
aliases: ["vault_public_key"]
type: str
ipavaultsalt:
description:
- Vault Salt.
aliases: ["vault_salt"]
type: str
username:
description:
- Any user can own one or more user vaults.
- Mutually exclusive with service.
aliases: ["user"]
type: list
elements: str
service:
description:
- Any service can own one or more service vaults.
- Mutually exclusive with user.
type: str
state:
description:
- State to ensure.
default: "present"
choices: ["absent", "present"]
type: str
replace:
description:
- Force replace the existant vault on IPA server.
type: bool
default: False
choices: ["True", "False"]
validate_certs:
description:
- Validate IPA server certificates.
type: bool
default: true
extends_documentation_fragment:
- community.general.ipa.documentation
'''
EXAMPLES = r'''
- name: Ensure vault is present
ipa_vault:
name: vault01
vault_type: standard
user: user01
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
validate_certs: false
- name: Ensure vault is present for Admin user
ipa_vault:
name: vault01
vault_type: standard
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Ensure vault is absent
ipa_vault:
name: vault01
vault_type: standard
user: user01
state: absent
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Modify vault if already exists
ipa_vault:
name: vault01
vault_type: standard
description: "Vault for test"
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
replace: True
- name: Get vault info if already exists
ipa_vault:
name: vault01
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
'''
RETURN = r'''
vault:
description: Vault as returned by IPA API
returned: always
type: dict
'''
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
from ansible.module_utils._text import to_native
class VaultIPAClient(IPAClient):
def __init__(self, module, host, port, protocol):
super(VaultIPAClient, self).__init__(module, host, port, protocol)
def vault_find(self, name):
return self._post_json(method='vault_find', name=None, item={'all': True, 'cn': name})
def vault_add_internal(self, name, item):
return self._post_json(method='vault_add_internal', name=name, item=item)
def vault_mod_internal(self, name, item):
return self._post_json(method='vault_mod_internal', name=name, item=item)
def vault_del(self, name):
return self._post_json(method='vault_del', name=name)
def get_vault_dict(description=None, vault_type=None, vault_salt=None, vault_public_key=None, service=None):
vault = {}
if description is not None:
vault['description'] = description
if vault_type is not None:
vault['ipavaulttype'] = vault_type
if vault_salt is not None:
vault['ipavaultsalt'] = vault_salt
if vault_public_key is not None:
vault['ipavaultpublickey'] = vault_public_key
if service is not None:
vault['service'] = service
return vault
def get_vault_diff(client, ipa_vault, module_vault, module):
return client.get_diff(ipa_data=ipa_vault, module_data=module_vault)
def ensure(module, client):
state = module.params['state']
name = module.params['cn']
user = module.params['username']
replace = module.params['replace']
module_vault = get_vault_dict(description=module.params['description'], vault_type=module.params['ipavaulttype'],
vault_salt=module.params['ipavaultsalt'],
vault_public_key=module.params['ipavaultpublickey'],
service=module.params['service'])
ipa_vault = client.vault_find(name=name)
changed = False
if state == 'present':
if not ipa_vault:
# New vault
changed = True
if not module.check_mode:
ipa_vault = client.vault_add_internal(name, item=module_vault)
else:
# Already exists
if replace:
diff = get_vault_diff(client, ipa_vault, module_vault, module)
if len(diff) > 0:
changed = True
if not module.check_mode:
data = {}
for key in diff:
data[key] = module_vault.get(key)
client.vault_mod_internal(name=name, item=data)
else:
if ipa_vault:
changed = True
if not module.check_mode:
client.vault_del(name)
return changed, client.vault_find(name=name)
def main():
argument_spec = ipa_argument_spec()
argument_spec.update(cn=dict(type='str', required=True, aliases=['name']),
description=dict(type='str'),
ipavaulttype=dict(type='str', default='symmetric',
choices=['standard', 'symmetric', 'asymmetric'], aliases=['vault_type']),
ipavaultsalt=dict(type='str', aliases=['vault_salt']),
ipavaultpublickey=dict(type='str', aliases=['vault_public_key']),
service=dict(type='str'),
replace=dict(type='bool', default=False, choices=[True, False]),
state=dict(type='str', default='present', choices=['present', 'absent']),
username=dict(type='list', elements='str', aliases=['user']))
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True,
mutually_exclusive=[['username', 'service']])
client = VaultIPAClient(module=module,
host=module.params['ipa_host'],
port=module.params['ipa_port'],
protocol=module.params['ipa_prot'])
try:
client.login(username=module.params['ipa_user'],
password=module.params['ipa_pass'])
changed, vault = ensure(module, client)
module.exit_json(changed=changed, vault=vault)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,853 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Eike Frost <ei@kefro.st>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: keycloak_client
short_description: Allows administration of Keycloak clients via Keycloak API
description:
- This module allows the administration of Keycloak clients via the Keycloak REST API. It
requires access to the REST API via OpenID Connect; the user connecting and the client being
used must have the requisite access rights. In a default Keycloak installation, admin-cli
and an admin user would work, as would a separate client definition with the scope tailored
to your needs and a user having the expected roles.
- The names of module options are snake_cased versions of the camelCase ones found in the
Keycloak API and its documentation at U(https://www.keycloak.org/docs-api/8.0/rest-api/index.html).
Aliases are provided so camelCased versions can be used as well.
- The Keycloak API does not always sanity check inputs e.g. you can set
SAML-specific settings on an OpenID Connect client for instance and vice versa. Be careful.
If you do not specify a setting, usually a sensible default is chosen.
options:
state:
description:
- State of the client
- On C(present), the client will be created (or updated if it exists already).
- On C(absent), the client will be removed if it exists
choices: ['present', 'absent']
default: 'present'
realm:
description:
- The realm to create the client in.
client_id:
description:
- Client id of client to be worked on. This is usually an alphanumeric name chosen by
you. Either this or I(id) is required. If you specify both, I(id) takes precedence.
This is 'clientId' in the Keycloak REST API.
aliases:
- clientId
id:
description:
- Id of client to be worked on. This is usually an UUID. Either this or I(client_id)
is required. If you specify both, this takes precedence.
name:
description:
- Name of the client (this is not the same as I(client_id))
description:
description:
- Description of the client in Keycloak
root_url:
description:
- Root URL appended to relative URLs for this client
This is 'rootUrl' in the Keycloak REST API.
aliases:
- rootUrl
admin_url:
description:
- URL to the admin interface of the client
This is 'adminUrl' in the Keycloak REST API.
aliases:
- adminUrl
base_url:
description:
- Default URL to use when the auth server needs to redirect or link back to the client
This is 'baseUrl' in the Keycloak REST API.
aliases:
- baseUrl
enabled:
description:
- Is this client enabled or not?
type: bool
client_authenticator_type:
description:
- How do clients authenticate with the auth server? Either C(client-secret) or
C(client-jwt) can be chosen. When using C(client-secret), the module parameter
I(secret) can set it, while for C(client-jwt), you can use the keys C(use.jwks.url),
C(jwks.url), and C(jwt.credential.certificate) in the I(attributes) module parameter
to configure its behavior.
This is 'clientAuthenticatorType' in the Keycloak REST API.
choices: ['client-secret', 'client-jwt']
aliases:
- clientAuthenticatorType
secret:
description:
- When using I(client_authenticator_type) C(client-secret) (the default), you can
specify a secret here (otherwise one will be generated if it does not exit). If
changing this secret, the module will not register a change currently (but the
changed secret will be saved).
registration_access_token:
description:
- The registration access token provides access for clients to the client registration
service.
This is 'registrationAccessToken' in the Keycloak REST API.
aliases:
- registrationAccessToken
default_roles:
description:
- list of default roles for this client. If the client roles referenced do not exist
yet, they will be created.
This is 'defaultRoles' in the Keycloak REST API.
aliases:
- defaultRoles
redirect_uris:
description:
- Acceptable redirect URIs for this client.
This is 'redirectUris' in the Keycloak REST API.
aliases:
- redirectUris
web_origins:
description:
- List of allowed CORS origins.
This is 'webOrigins' in the Keycloak REST API.
aliases:
- webOrigins
not_before:
description:
- Revoke any tokens issued before this date for this client (this is a UNIX timestamp).
This is 'notBefore' in the Keycloak REST API.
aliases:
- notBefore
bearer_only:
description:
- The access type of this client is bearer-only.
This is 'bearerOnly' in the Keycloak REST API.
aliases:
- bearerOnly
type: bool
consent_required:
description:
- If enabled, users have to consent to client access.
This is 'consentRequired' in the Keycloak REST API.
aliases:
- consentRequired
type: bool
standard_flow_enabled:
description:
- Enable standard flow for this client or not (OpenID connect).
This is 'standardFlowEnabled' in the Keycloak REST API.
aliases:
- standardFlowEnabled
type: bool
implicit_flow_enabled:
description:
- Enable implicit flow for this client or not (OpenID connect).
This is 'implicitFlowEnabled' in the Keycloak REST API.
aliases:
- implicitFlowEnabled
type: bool
direct_access_grants_enabled:
description:
- Are direct access grants enabled for this client or not (OpenID connect).
This is 'directAccessGrantsEnabled' in the Keycloak REST API.
aliases:
- directAccessGrantsEnabled
type: bool
service_accounts_enabled:
description:
- Are service accounts enabled for this client or not (OpenID connect).
This is 'serviceAccountsEnabled' in the Keycloak REST API.
aliases:
- serviceAccountsEnabled
type: bool
authorization_services_enabled:
description:
- Are authorization services enabled for this client or not (OpenID connect).
This is 'authorizationServicesEnabled' in the Keycloak REST API.
aliases:
- authorizationServicesEnabled
type: bool
public_client:
description:
- Is the access type for this client public or not.
This is 'publicClient' in the Keycloak REST API.
aliases:
- publicClient
type: bool
frontchannel_logout:
description:
- Is frontchannel logout enabled for this client or not.
This is 'frontchannelLogout' in the Keycloak REST API.
aliases:
- frontchannelLogout
type: bool
protocol:
description:
- Type of client (either C(openid-connect) or C(saml).
choices: ['openid-connect', 'saml']
full_scope_allowed:
description:
- Is the "Full Scope Allowed" feature set for this client or not.
This is 'fullScopeAllowed' in the Keycloak REST API.
aliases:
- fullScopeAllowed
type: bool
node_re_registration_timeout:
description:
- Cluster node re-registration timeout for this client.
This is 'nodeReRegistrationTimeout' in the Keycloak REST API.
aliases:
- nodeReRegistrationTimeout
registered_nodes:
description:
- dict of registered cluster nodes (with C(nodename) as the key and last registration
time as the value).
This is 'registeredNodes' in the Keycloak REST API.
aliases:
- registeredNodes
client_template:
description:
- Client template to use for this client. If it does not exist this field will silently
be dropped.
This is 'clientTemplate' in the Keycloak REST API.
aliases:
- clientTemplate
use_template_config:
description:
- Whether or not to use configuration from the I(client_template).
This is 'useTemplateConfig' in the Keycloak REST API.
aliases:
- useTemplateConfig
type: bool
use_template_scope:
description:
- Whether or not to use scope configuration from the I(client_template).
This is 'useTemplateScope' in the Keycloak REST API.
aliases:
- useTemplateScope
type: bool
use_template_mappers:
description:
- Whether or not to use mapper configuration from the I(client_template).
This is 'useTemplateMappers' in the Keycloak REST API.
aliases:
- useTemplateMappers
type: bool
surrogate_auth_required:
description:
- Whether or not surrogate auth is required.
This is 'surrogateAuthRequired' in the Keycloak REST API.
aliases:
- surrogateAuthRequired
type: bool
authorization_settings:
description:
- a data structure defining the authorization settings for this client. For reference,
please see the Keycloak API docs at U(https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_resourceserverrepresentation).
This is 'authorizationSettings' in the Keycloak REST API.
aliases:
- authorizationSettings
protocol_mappers:
description:
- a list of dicts defining protocol mappers for this client.
This is 'protocolMappers' in the Keycloak REST API.
aliases:
- protocolMappers
suboptions:
consentRequired:
description:
- Specifies whether a user needs to provide consent to a client for this mapper to be active.
consentText:
description:
- The human-readable name of the consent the user is presented to accept.
id:
description:
- Usually a UUID specifying the internal ID of this protocol mapper instance.
name:
description:
- The name of this protocol mapper.
protocol:
description:
- This is either C(openid-connect) or C(saml), this specifies for which protocol this protocol mapper
is active.
choices: ['openid-connect', 'saml']
protocolMapper:
description:
- The Keycloak-internal name of the type of this protocol-mapper. While an exhaustive list is
impossible to provide since this may be extended through SPIs by the user of Keycloak,
by default Keycloak as of 3.4 ships with at least
- C(docker-v2-allow-all-mapper)
- C(oidc-address-mapper)
- C(oidc-full-name-mapper)
- C(oidc-group-membership-mapper)
- C(oidc-hardcoded-claim-mapper)
- C(oidc-hardcoded-role-mapper)
- C(oidc-role-name-mapper)
- C(oidc-script-based-protocol-mapper)
- C(oidc-sha256-pairwise-sub-mapper)
- C(oidc-usermodel-attribute-mapper)
- C(oidc-usermodel-client-role-mapper)
- C(oidc-usermodel-property-mapper)
- C(oidc-usermodel-realm-role-mapper)
- C(oidc-usersessionmodel-note-mapper)
- C(saml-group-membership-mapper)
- C(saml-hardcode-attribute-mapper)
- C(saml-hardcode-role-mapper)
- C(saml-role-list-mapper)
- C(saml-role-name-mapper)
- C(saml-user-attribute-mapper)
- C(saml-user-property-mapper)
- C(saml-user-session-note-mapper)
- An exhaustive list of available mappers on your installation can be obtained on
the admin console by going to Server Info -> Providers and looking under
'protocol-mapper'.
config:
description:
- Dict specifying the configuration options for the protocol mapper; the
contents differ depending on the value of I(protocolMapper) and are not documented
other than by the source of the mappers and its parent class(es). An example is given
below. It is easiest to obtain valid config values by dumping an already-existing
protocol mapper configuration through check-mode in the I(existing) field.
attributes:
description:
- A dict of further attributes for this client. This can contain various configuration
settings; an example is given in the examples section. While an exhaustive list of
permissible options is not available; possible options as of Keycloak 3.4 are listed below. The Keycloak
API does not validate whether a given option is appropriate for the protocol used; if specified
anyway, Keycloak will simply not use it.
suboptions:
saml.authnstatement:
description:
- For SAML clients, boolean specifying whether or not a statement containing method and timestamp
should be included in the login response.
saml.client.signature:
description:
- For SAML clients, boolean specifying whether a client signature is required and validated.
saml.encrypt:
description:
- Boolean specifying whether SAML assertions should be encrypted with the client's public key.
saml.force.post.binding:
description:
- For SAML clients, boolean specifying whether always to use POST binding for responses.
saml.onetimeuse.condition:
description:
- For SAML clients, boolean specifying whether a OneTimeUse condition should be included in login responses.
saml.server.signature:
description:
- Boolean specifying whether SAML documents should be signed by the realm.
saml.server.signature.keyinfo.ext:
description:
- For SAML clients, boolean specifying whether REDIRECT signing key lookup should be optimized through inclusion
of the signing key id in the SAML Extensions element.
saml.signature.algorithm:
description:
- Signature algorithm used to sign SAML documents. One of C(RSA_SHA256), C(RSA_SHA1), C(RSA_SHA512), or C(DSA_SHA1).
saml.signing.certificate:
description:
- SAML signing key certificate, base64-encoded.
saml.signing.private.key:
description:
- SAML signing key private key, base64-encoded.
saml_assertion_consumer_url_post:
description:
- SAML POST Binding URL for the client's assertion consumer service (login responses).
saml_assertion_consumer_url_redirect:
description:
- SAML Redirect Binding URL for the client's assertion consumer service (login responses).
saml_force_name_id_format:
description:
- For SAML clients, Boolean specifying whether to ignore requested NameID subject format and using the configured one instead.
saml_name_id_format:
description:
- For SAML clients, the NameID format to use (one of C(username), C(email), C(transient), or C(persistent))
saml_signature_canonicalization_method:
description:
- SAML signature canonicalization method. This is one of four values, namely
C(http://www.w3.org/2001/10/xml-exc-c14n#) for EXCLUSIVE,
C(http://www.w3.org/2001/10/xml-exc-c14n#WithComments) for EXCLUSIVE_WITH_COMMENTS,
C(http://www.w3.org/TR/2001/REC-xml-c14n-20010315) for INCLUSIVE, and
C(http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments) for INCLUSIVE_WITH_COMMENTS.
saml_single_logout_service_url_post:
description:
- SAML POST binding url for the client's single logout service.
saml_single_logout_service_url_redirect:
description:
- SAML redirect binding url for the client's single logout service.
user.info.response.signature.alg:
description:
- For OpenID-Connect clients, JWA algorithm for signed UserInfo-endpoint responses. One of C(RS256) or C(unsigned).
request.object.signature.alg:
description:
- For OpenID-Connect clients, JWA algorithm which the client needs to use when sending
OIDC request object. One of C(any), C(none), C(RS256).
use.jwks.url:
description:
- For OpenID-Connect clients, boolean specifying whether to use a JWKS URL to obtain client
public keys.
jwks.url:
description:
- For OpenID-Connect clients, URL where client keys in JWK are stored.
jwt.credential.certificate:
description:
- For OpenID-Connect clients, client certificate for validating JWT issued by
client and signed by its key, base64-encoded.
extends_documentation_fragment:
- community.general.keycloak
author:
- Eike Frost (@eikef)
'''
EXAMPLES = '''
- name: Create or update Keycloak client (minimal example)
local_action:
module: keycloak_client
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
client_id: test
state: present
- name: Delete a Keycloak client
local_action:
module: keycloak_client
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
client_id: test
state: absent
- name: Create or update a Keycloak client (with all the bells and whistles)
local_action:
module: keycloak_client
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
state: present
realm: master
client_id: test
id: d8b127a3-31f6-44c8-a7e4-4ab9a3e78d95
name: this_is_a_test
description: Description of this wonderful client
root_url: https://www.example.com/
admin_url: https://www.example.com/admin_url
base_url: basepath
enabled: True
client_authenticator_type: client-secret
secret: REALLYWELLKEPTSECRET
redirect_uris:
- https://www.example.com/*
- http://localhost:8888/
web_origins:
- https://www.example.com/*
not_before: 1507825725
bearer_only: False
consent_required: False
standard_flow_enabled: True
implicit_flow_enabled: False
direct_access_grants_enabled: False
service_accounts_enabled: False
authorization_services_enabled: False
public_client: False
frontchannel_logout: False
protocol: openid-connect
full_scope_allowed: false
node_re_registration_timeout: -1
client_template: test
use_template_config: False
use_template_scope: false
use_template_mappers: no
registered_nodes:
node01.example.com: 1507828202
registration_access_token: eyJWT_TOKEN
surrogate_auth_required: false
default_roles:
- test01
- test02
protocol_mappers:
- config:
access.token.claim: True
claim.name: "family_name"
id.token.claim: True
jsonType.label: String
user.attribute: lastName
userinfo.token.claim: True
consentRequired: True
consentText: "${familyName}"
name: family name
protocol: openid-connect
protocolMapper: oidc-usermodel-property-mapper
- config:
attribute.name: Role
attribute.nameformat: Basic
single: false
consentRequired: false
name: role list
protocol: saml
protocolMapper: saml-role-list-mapper
attributes:
saml.authnstatement: True
saml.client.signature: True
saml.force.post.binding: True
saml.server.signature: True
saml.signature.algorithm: RSA_SHA256
saml.signing.certificate: CERTIFICATEHERE
saml.signing.private.key: PRIVATEKEYHERE
saml_force_name_id_format: False
saml_name_id_format: username
saml_signature_canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#"
user.info.response.signature.alg: RS256
request.object.signature.alg: RS256
use.jwks.url: true
jwks.url: JWKS_URL_FOR_CLIENT_AUTH_JWT
jwt.credential.certificate: JWT_CREDENTIAL_CERTIFICATE_FOR_CLIENT_AUTH
'''
RETURN = '''
msg:
description: Message as to what action was taken
returned: always
type: str
sample: "Client testclient has been updated"
proposed:
description: client representation of proposed changes to client
returned: always
type: dict
sample: {
clientId: "test"
}
existing:
description: client representation of existing client (sample is truncated)
returned: always
type: dict
sample: {
"adminUrl": "http://www.example.com/admin_url",
"attributes": {
"request.object.signature.alg": "RS256",
}
}
end_state:
description: client representation of client after module execution (sample is truncated)
returned: always
type: dict
sample: {
"adminUrl": "http://www.example.com/admin_url",
"attributes": {
"request.object.signature.alg": "RS256",
}
}
'''
from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \
keycloak_argument_spec, get_token, KeycloakError
from ansible.module_utils.basic import AnsibleModule
def sanitize_cr(clientrep):
""" Removes probably sensitive details from a client representation
:param clientrep: the clientrep dict to be sanitized
:return: sanitized clientrep dict
"""
result = clientrep.copy()
if 'secret' in result:
result['secret'] = 'no_log'
if 'attributes' in result:
if 'saml.signing.private.key' in result['attributes']:
result['attributes']['saml.signing.private.key'] = 'no_log'
return result
def main():
"""
Module execution
:return:
"""
argument_spec = keycloak_argument_spec()
protmapper_spec = dict(
consentRequired=dict(type='bool'),
consentText=dict(type='str'),
id=dict(type='str'),
name=dict(type='str'),
protocol=dict(type='str', choices=['openid-connect', 'saml']),
protocolMapper=dict(type='str'),
config=dict(type='dict'),
)
meta_args = dict(
state=dict(default='present', choices=['present', 'absent']),
realm=dict(type='str', default='master'),
id=dict(type='str'),
client_id=dict(type='str', aliases=['clientId']),
name=dict(type='str'),
description=dict(type='str'),
root_url=dict(type='str', aliases=['rootUrl']),
admin_url=dict(type='str', aliases=['adminUrl']),
base_url=dict(type='str', aliases=['baseUrl']),
surrogate_auth_required=dict(type='bool', aliases=['surrogateAuthRequired']),
enabled=dict(type='bool'),
client_authenticator_type=dict(type='str', choices=['client-secret', 'client-jwt'], aliases=['clientAuthenticatorType']),
secret=dict(type='str', no_log=True),
registration_access_token=dict(type='str', aliases=['registrationAccessToken']),
default_roles=dict(type='list', aliases=['defaultRoles']),
redirect_uris=dict(type='list', aliases=['redirectUris']),
web_origins=dict(type='list', aliases=['webOrigins']),
not_before=dict(type='int', aliases=['notBefore']),
bearer_only=dict(type='bool', aliases=['bearerOnly']),
consent_required=dict(type='bool', aliases=['consentRequired']),
standard_flow_enabled=dict(type='bool', aliases=['standardFlowEnabled']),
implicit_flow_enabled=dict(type='bool', aliases=['implicitFlowEnabled']),
direct_access_grants_enabled=dict(type='bool', aliases=['directAccessGrantsEnabled']),
service_accounts_enabled=dict(type='bool', aliases=['serviceAccountsEnabled']),
authorization_services_enabled=dict(type='bool', aliases=['authorizationServicesEnabled']),
public_client=dict(type='bool', aliases=['publicClient']),
frontchannel_logout=dict(type='bool', aliases=['frontchannelLogout']),
protocol=dict(type='str', choices=['openid-connect', 'saml']),
attributes=dict(type='dict'),
full_scope_allowed=dict(type='bool', aliases=['fullScopeAllowed']),
node_re_registration_timeout=dict(type='int', aliases=['nodeReRegistrationTimeout']),
registered_nodes=dict(type='dict', aliases=['registeredNodes']),
client_template=dict(type='str', aliases=['clientTemplate']),
use_template_config=dict(type='bool', aliases=['useTemplateConfig']),
use_template_scope=dict(type='bool', aliases=['useTemplateScope']),
use_template_mappers=dict(type='bool', aliases=['useTemplateMappers']),
protocol_mappers=dict(type='list', elements='dict', options=protmapper_spec, aliases=['protocolMappers']),
authorization_settings=dict(type='dict', aliases=['authorizationSettings']),
)
argument_spec.update(meta_args)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True,
required_one_of=([['client_id', 'id']]))
result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})
# Obtain access token, initialize API
try:
connection_header = get_token(
base_url=module.params.get('auth_keycloak_url'),
validate_certs=module.params.get('validate_certs'),
auth_realm=module.params.get('auth_realm'),
client_id=module.params.get('auth_client_id'),
auth_username=module.params.get('auth_username'),
auth_password=module.params.get('auth_password'),
client_secret=module.params.get('auth_client_secret'),
)
except KeycloakError as e:
module.fail_json(msg=str(e))
kc = KeycloakAPI(module, connection_header)
realm = module.params.get('realm')
cid = module.params.get('id')
state = module.params.get('state')
# convert module parameters to client representation parameters (if they belong in there)
client_params = [x for x in module.params
if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm'] and
module.params.get(x) is not None]
keycloak_argument_spec().keys()
# See whether the client already exists in Keycloak
if cid is None:
before_client = kc.get_client_by_clientid(module.params.get('client_id'), realm=realm)
if before_client is not None:
cid = before_client['id']
else:
before_client = kc.get_client_by_id(cid, realm=realm)
if before_client is None:
before_client = dict()
# Build a proposed changeset from parameters given to this module
changeset = dict()
for client_param in client_params:
new_param_value = module.params.get(client_param)
# some lists in the Keycloak API are sorted, some are not.
if isinstance(new_param_value, list):
if client_param in ['attributes']:
try:
new_param_value = sorted(new_param_value)
except TypeError:
pass
# Unfortunately, the ansible argument spec checker introduces variables with null values when
# they are not specified
if client_param == 'protocol_mappers':
new_param_value = [dict((k, v) for k, v in x.items() if x[k] is not None) for x in new_param_value]
changeset[camel(client_param)] = new_param_value
# Whether creating or updating a client, take the before-state and merge the changeset into it
updated_client = before_client.copy()
updated_client.update(changeset)
result['proposed'] = sanitize_cr(changeset)
result['existing'] = sanitize_cr(before_client)
# If the client does not exist yet, before_client is still empty
if before_client == dict():
if state == 'absent':
# do nothing and exit
if module._diff:
result['diff'] = dict(before='', after='')
result['msg'] = 'Client does not exist, doing nothing.'
module.exit_json(**result)
# create new client
result['changed'] = True
if 'clientId' not in updated_client:
module.fail_json(msg='client_id needs to be specified when creating a new client')
if module._diff:
result['diff'] = dict(before='', after=sanitize_cr(updated_client))
if module.check_mode:
module.exit_json(**result)
kc.create_client(updated_client, realm=realm)
after_client = kc.get_client_by_clientid(updated_client['clientId'], realm=realm)
result['end_state'] = sanitize_cr(after_client)
result['msg'] = 'Client %s has been created.' % updated_client['clientId']
module.exit_json(**result)
else:
if state == 'present':
# update existing client
result['changed'] = True
if module.check_mode:
# We can only compare the current client with the proposed updates we have
if module._diff:
result['diff'] = dict(before=sanitize_cr(before_client),
after=sanitize_cr(updated_client))
result['changed'] = (before_client != updated_client)
module.exit_json(**result)
kc.update_client(cid, updated_client, realm=realm)
after_client = kc.get_client_by_id(cid, realm=realm)
if before_client == after_client:
result['changed'] = False
if module._diff:
result['diff'] = dict(before=sanitize_cr(before_client),
after=sanitize_cr(after_client))
result['end_state'] = sanitize_cr(after_client)
result['msg'] = 'Client %s has been updated.' % updated_client['clientId']
module.exit_json(**result)
else:
# Delete existing client
result['changed'] = True
if module._diff:
result['diff']['before'] = sanitize_cr(before_client)
result['diff']['after'] = ''
if module.check_mode:
module.exit_json(**result)
kc.delete_client(cid, realm=realm)
result['proposed'] = dict()
result['end_state'] = dict()
result['msg'] = 'Client %s has been deleted.' % before_client['clientId']
module.exit_json(**result)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,420 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Eike Frost <ei@kefro.st>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: keycloak_clienttemplate
short_description: Allows administration of Keycloak client templates via Keycloak API
description:
- This module allows the administration of Keycloak client templates via the Keycloak REST API. It
requires access to the REST API via OpenID Connect; the user connecting and the client being
used must have the requisite access rights. In a default Keycloak installation, admin-cli
and an admin user would work, as would a separate client definition with the scope tailored
to your needs and a user having the expected roles.
- The names of module options are snake_cased versions of the camelCase ones found in the
Keycloak API and its documentation at U(https://www.keycloak.org/docs-api/8.0/rest-api/index.html)
- The Keycloak API does not always enforce for only sensible settings to be used -- you can set
SAML-specific settings on an OpenID Connect client for instance and vice versa. Be careful.
If you do not specify a setting, usually a sensible default is chosen.
options:
state:
description:
- State of the client template
- On C(present), the client template will be created (or updated if it exists already).
- On C(absent), the client template will be removed if it exists
choices: ['present', 'absent']
default: 'present'
id:
description:
- Id of client template to be worked on. This is usually a UUID.
realm:
description:
- Realm this client template is found in.
name:
description:
- Name of the client template
description:
description:
- Description of the client template in Keycloak
protocol:
description:
- Type of client template (either C(openid-connect) or C(saml).
choices: ['openid-connect', 'saml']
full_scope_allowed:
description:
- Is the "Full Scope Allowed" feature set for this client template or not.
This is 'fullScopeAllowed' in the Keycloak REST API.
type: bool
protocol_mappers:
description:
- a list of dicts defining protocol mappers for this client template.
This is 'protocolMappers' in the Keycloak REST API.
suboptions:
consentRequired:
description:
- Specifies whether a user needs to provide consent to a client for this mapper to be active.
consentText:
description:
- The human-readable name of the consent the user is presented to accept.
id:
description:
- Usually a UUID specifying the internal ID of this protocol mapper instance.
name:
description:
- The name of this protocol mapper.
protocol:
description:
- is either 'openid-connect' or 'saml', this specifies for which protocol this protocol mapper
is active.
choices: ['openid-connect', 'saml']
protocolMapper:
description:
- The Keycloak-internal name of the type of this protocol-mapper. While an exhaustive list is
impossible to provide since this may be extended through SPIs by the user of Keycloak,
by default Keycloak as of 3.4 ships with at least
- C(docker-v2-allow-all-mapper)
- C(oidc-address-mapper)
- C(oidc-full-name-mapper)
- C(oidc-group-membership-mapper)
- C(oidc-hardcoded-claim-mapper)
- C(oidc-hardcoded-role-mapper)
- C(oidc-role-name-mapper)
- C(oidc-script-based-protocol-mapper)
- C(oidc-sha256-pairwise-sub-mapper)
- C(oidc-usermodel-attribute-mapper)
- C(oidc-usermodel-client-role-mapper)
- C(oidc-usermodel-property-mapper)
- C(oidc-usermodel-realm-role-mapper)
- C(oidc-usersessionmodel-note-mapper)
- C(saml-group-membership-mapper)
- C(saml-hardcode-attribute-mapper)
- C(saml-hardcode-role-mapper)
- C(saml-role-list-mapper)
- C(saml-role-name-mapper)
- C(saml-user-attribute-mapper)
- C(saml-user-property-mapper)
- C(saml-user-session-note-mapper)
- An exhaustive list of available mappers on your installation can be obtained on
the admin console by going to Server Info -> Providers and looking under
'protocol-mapper'.
config:
description:
- Dict specifying the configuration options for the protocol mapper; the
contents differ depending on the value of I(protocolMapper) and are not documented
other than by the source of the mappers and its parent class(es). An example is given
below. It is easiest to obtain valid config values by dumping an already-existing
protocol mapper configuration through check-mode in the "existing" field.
attributes:
description:
- A dict of further attributes for this client template. This can contain various
configuration settings, though in the default installation of Keycloak as of 3.4, none
are documented or known, so this is usually empty.
notes:
- The Keycloak REST API defines further fields (namely I(bearerOnly), I(consentRequired), I(standardFlowEnabled),
I(implicitFlowEnabled), I(directAccessGrantsEnabled), I(serviceAccountsEnabled), I(publicClient), and
I(frontchannelLogout)) which, while available with keycloak_client, do not have any effect on
Keycloak client-templates and are discarded if supplied with an API request changing client-templates. As such,
they are not available through this module.
extends_documentation_fragment:
- community.general.keycloak
author:
- Eike Frost (@eikef)
'''
EXAMPLES = '''
- name: Create or update Keycloak client template (minimal)
local_action:
module: keycloak_clienttemplate
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
realm: master
name: this_is_a_test
- name: delete Keycloak client template
local_action:
module: keycloak_clienttemplate
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
realm: master
state: absent
name: test01
- name: Create or update Keycloak client template (with a protocol mapper)
local_action:
module: keycloak_clienttemplate
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
realm: master
name: this_is_a_test
protocol_mappers:
- config:
access.token.claim: True
claim.name: "family_name"
id.token.claim: True
jsonType.label: String
user.attribute: lastName
userinfo.token.claim: True
consentRequired: True
consentText: "${familyName}"
name: family name
protocol: openid-connect
protocolMapper: oidc-usermodel-property-mapper
full_scope_allowed: false
id: bce6f5e9-d7d3-4955-817e-c5b7f8d65b3f
'''
RETURN = '''
msg:
description: Message as to what action was taken
returned: always
type: str
sample: "Client template testclient has been updated"
proposed:
description: client template representation of proposed changes to client template
returned: always
type: dict
sample: {
name: "test01"
}
existing:
description: client template representation of existing client template (sample is truncated)
returned: always
type: dict
sample: {
"description": "test01",
"fullScopeAllowed": false,
"id": "9c3712ab-decd-481e-954f-76da7b006e5f",
"name": "test01",
"protocol": "saml"
}
end_state:
description: client template representation of client template after module execution (sample is truncated)
returned: always
type: dict
sample: {
"description": "test01",
"fullScopeAllowed": false,
"id": "9c3712ab-decd-481e-954f-76da7b006e5f",
"name": "test01",
"protocol": "saml"
}
'''
from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \
keycloak_argument_spec, get_token, KeycloakError
from ansible.module_utils.basic import AnsibleModule
def main():
"""
Module execution
:return:
"""
argument_spec = keycloak_argument_spec()
protmapper_spec = dict(
consentRequired=dict(type='bool'),
consentText=dict(type='str'),
id=dict(type='str'),
name=dict(type='str'),
protocol=dict(type='str', choices=['openid-connect', 'saml']),
protocolMapper=dict(type='str'),
config=dict(type='dict'),
)
meta_args = dict(
realm=dict(type='str', default='master'),
state=dict(default='present', choices=['present', 'absent']),
id=dict(type='str'),
name=dict(type='str'),
description=dict(type='str'),
protocol=dict(type='str', choices=['openid-connect', 'saml']),
attributes=dict(type='dict'),
full_scope_allowed=dict(type='bool'),
protocol_mappers=dict(type='list', elements='dict', options=protmapper_spec),
)
argument_spec.update(meta_args)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True,
required_one_of=([['id', 'name']]))
result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})
# Obtain access token, initialize API
try:
connection_header = get_token(
base_url=module.params.get('auth_keycloak_url'),
validate_certs=module.params.get('validate_certs'),
auth_realm=module.params.get('auth_realm'),
client_id=module.params.get('auth_client_id'),
auth_username=module.params.get('auth_username'),
auth_password=module.params.get('auth_password'),
client_secret=module.params.get('auth_client_secret'),
)
except KeycloakError as e:
module.fail_json(msg=str(e))
kc = KeycloakAPI(module, connection_header)
realm = module.params.get('realm')
state = module.params.get('state')
cid = module.params.get('id')
# convert module parameters to client representation parameters (if they belong in there)
clientt_params = [x for x in module.params
if x not in ['state', 'auth_keycloak_url', 'auth_client_id', 'auth_realm',
'auth_client_secret', 'auth_username', 'auth_password',
'validate_certs', 'realm'] and module.params.get(x) is not None]
# See whether the client template already exists in Keycloak
if cid is None:
before_clientt = kc.get_client_template_by_name(module.params.get('name'), realm=realm)
if before_clientt is not None:
cid = before_clientt['id']
else:
before_clientt = kc.get_client_template_by_id(cid, realm=realm)
if before_clientt is None:
before_clientt = dict()
result['existing'] = before_clientt
# Build a proposed changeset from parameters given to this module
changeset = dict()
for clientt_param in clientt_params:
# lists in the Keycloak API are sorted
new_param_value = module.params.get(clientt_param)
if isinstance(new_param_value, list):
try:
new_param_value = sorted(new_param_value)
except TypeError:
pass
changeset[camel(clientt_param)] = new_param_value
# Whether creating or updating a client, take the before-state and merge the changeset into it
updated_clientt = before_clientt.copy()
updated_clientt.update(changeset)
result['proposed'] = changeset
# If the client template does not exist yet, before_client is still empty
if before_clientt == dict():
if state == 'absent':
# do nothing and exit
if module._diff:
result['diff'] = dict(before='', after='')
result['msg'] = 'Client template does not exist, doing nothing.'
module.exit_json(**result)
# create new client template
result['changed'] = True
if 'name' not in updated_clientt:
module.fail_json(msg='name needs to be specified when creating a new client')
if module._diff:
result['diff'] = dict(before='', after=updated_clientt)
if module.check_mode:
module.exit_json(**result)
kc.create_client_template(updated_clientt, realm=realm)
after_clientt = kc.get_client_template_by_name(updated_clientt['name'], realm=realm)
result['end_state'] = after_clientt
result['msg'] = 'Client template %s has been created.' % updated_clientt['name']
module.exit_json(**result)
else:
if state == 'present':
# update existing client template
result['changed'] = True
if module.check_mode:
# We can only compare the current client template with the proposed updates we have
if module._diff:
result['diff'] = dict(before=before_clientt,
after=updated_clientt)
module.exit_json(**result)
kc.update_client_template(cid, updated_clientt, realm=realm)
after_clientt = kc.get_client_template_by_id(cid, realm=realm)
if before_clientt == after_clientt:
result['changed'] = False
if module._diff:
result['diff'] = dict(before=before_clientt,
after=after_clientt)
result['end_state'] = after_clientt
result['msg'] = 'Client template %s has been updated.' % updated_clientt['name']
module.exit_json(**result)
else:
# Delete existing client
result['changed'] = True
if module._diff:
result['diff']['before'] = before_clientt
result['diff']['after'] = ''
if module.check_mode:
module.exit_json(**result)
kc.delete_client_template(cid, realm=realm)
result['proposed'] = dict()
result['end_state'] = dict()
result['msg'] = 'Client template %s has been deleted.' % before_clientt['name']
module.exit_json(**result)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,370 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Adam Goossens <adam.goossens@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: keycloak_group
short_description: Allows administration of Keycloak groups via Keycloak API
description:
- This module allows you to add, remove or modify Keycloak groups via the Keycloak REST API.
It requires access to the REST API via OpenID Connect; the user connecting and the client being
used must have the requisite access rights. In a default Keycloak installation, admin-cli
and an admin user would work, as would a separate client definition with the scope tailored
to your needs and a user having the expected roles.
- The names of module options are snake_cased versions of the camelCase ones found in the
Keycloak API and its documentation at U(https://www.keycloak.org/docs-api/8.0/rest-api/index.html).
- Attributes are multi-valued in the Keycloak API. All attributes are lists of individual values and will
be returned that way by this module. You may pass single values for attributes when calling the module,
and this will be translated into a list suitable for the API.
- When updating a group, where possible provide the group ID to the module. This removes a lookup
to the API to translate the name into the group ID.
options:
state:
description:
- State of the group.
- On C(present), the group will be created if it does not yet exist, or updated with the parameters you provide.
- On C(absent), the group will be removed if it exists.
default: 'present'
type: str
choices:
- present
- absent
name:
type: str
description:
- Name of the group.
- This parameter is required only when creating or updating the group.
realm:
type: str
description:
- They Keycloak realm under which this group resides.
default: 'master'
id:
type: str
description:
- The unique identifier for this group.
- This parameter is not required for updating or deleting a group but
providing it will reduce the number of API calls required.
attributes:
type: dict
description:
- A dict of key/value pairs to set as custom attributes for the group.
- Values may be single values (e.g. a string) or a list of strings.
notes:
- Presently, the I(realmRoles), I(clientRoles) and I(access) attributes returned by the Keycloak API
are read-only for groups. This limitation will be removed in a later version of this module.
extends_documentation_fragment:
- community.general.keycloak
author:
- Adam Goossens (@adamgoossens)
'''
EXAMPLES = '''
- name: Create a Keycloak group
keycloak_group:
name: my-new-kc-group
realm: MyCustomRealm
state: present
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
delegate_to: localhost
- name: Delete a keycloak group
keycloak_group:
id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd'
state: absent
realm: MyCustomRealm
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
delegate_to: localhost
- name: Delete a Keycloak group based on name
keycloak_group:
name: my-group-for-deletion
state: absent
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
delegate_to: localhost
- name: Update the name of a Keycloak group
keycloak_group:
id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd'
name: an-updated-kc-group-name
state: present
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
delegate_to: localhost
- name: Create a keycloak group with some custom attributes
keycloak_group:
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
name: my-new_group
attributes:
attrib1: value1
attrib2: value2
attrib3:
- with
- numerous
- individual
- list
- items
delegate_to: localhost
'''
RETURN = '''
group:
description: Group representation of the group after module execution (sample is truncated).
returned: always
type: complex
contains:
id:
description: GUID that identifies the group
type: str
returned: always
sample: 23f38145-3195-462c-97e7-97041ccea73e
name:
description: Name of the group
type: str
returned: always
sample: grp-test-123
attributes:
description: Attributes applied to this group
type: dict
returned: always
sample:
attr1: ["val1", "val2", "val3"]
path:
description: URI path to the group
type: str
returned: always
sample: /grp-test-123
realmRoles:
description: An array of the realm-level roles granted to this group
type: list
returned: always
sample: []
subGroups:
description: A list of groups that are children of this group. These groups will have the same parameters as
documented here.
type: list
returned: always
clientRoles:
description: A list of client-level roles granted to this group
type: list
returned: always
sample: []
access:
description: A dict describing the accesses you have to this group based on the credentials used.
type: dict
returned: always
sample:
manage: true
manageMembership: true
view: true
'''
from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \
keycloak_argument_spec, get_token, KeycloakError
from ansible.module_utils.basic import AnsibleModule
def main():
"""
Module execution
:return:
"""
argument_spec = keycloak_argument_spec()
meta_args = dict(
state=dict(default='present', choices=['present', 'absent']),
realm=dict(default='master'),
id=dict(type='str'),
name=dict(type='str'),
attributes=dict(type='dict')
)
argument_spec.update(meta_args)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True,
required_one_of=([['id', 'name']]))
result = dict(changed=False, msg='', diff={}, group='')
# Obtain access token, initialize API
try:
connection_header = get_token(
base_url=module.params.get('auth_keycloak_url'),
validate_certs=module.params.get('validate_certs'),
auth_realm=module.params.get('auth_realm'),
client_id=module.params.get('auth_client_id'),
auth_username=module.params.get('auth_username'),
auth_password=module.params.get('auth_password'),
client_secret=module.params.get('auth_client_secret'),
)
except KeycloakError as e:
module.fail_json(msg=str(e))
kc = KeycloakAPI(module, connection_header)
realm = module.params.get('realm')
state = module.params.get('state')
gid = module.params.get('id')
name = module.params.get('name')
attributes = module.params.get('attributes')
before_group = None # current state of the group, for merging.
# does the group already exist?
if gid is None:
before_group = kc.get_group_by_name(name, realm=realm)
else:
before_group = kc.get_group_by_groupid(gid, realm=realm)
before_group = {} if before_group is None else before_group
# attributes in Keycloak have their values returned as lists
# via the API. attributes is a dict, so we'll transparently convert
# the values to lists.
if attributes is not None:
for key, val in module.params['attributes'].items():
module.params['attributes'][key] = [val] if not isinstance(val, list) else val
group_params = [x for x in module.params
if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm'] and
module.params.get(x) is not None]
# build a changeset
changeset = {}
for param in group_params:
new_param_value = module.params.get(param)
old_value = before_group[param] if param in before_group else None
if new_param_value != old_value:
changeset[camel(param)] = new_param_value
# prepare the new group
updated_group = before_group.copy()
updated_group.update(changeset)
# if before_group is none, the group doesn't exist.
if before_group == {}:
if state == 'absent':
# nothing to do.
if module._diff:
result['diff'] = dict(before='', after='')
result['msg'] = 'Group does not exist; doing nothing.'
result['group'] = dict()
module.exit_json(**result)
# for 'present', create a new group.
result['changed'] = True
if name is None:
module.fail_json(msg='name must be specified when creating a new group')
if module._diff:
result['diff'] = dict(before='', after=updated_group)
if module.check_mode:
module.exit_json(**result)
# do it for real!
kc.create_group(updated_group, realm=realm)
after_group = kc.get_group_by_name(name, realm)
result['group'] = after_group
result['msg'] = 'Group {name} has been created with ID {id}'.format(name=after_group['name'],
id=after_group['id'])
else:
if state == 'present':
# no changes
if updated_group == before_group:
result['changed'] = False
result['group'] = updated_group
result['msg'] = "No changes required to group {name}.".format(name=before_group['name'])
module.exit_json(**result)
# update the existing group
result['changed'] = True
if module._diff:
result['diff'] = dict(before=before_group, after=updated_group)
if module.check_mode:
module.exit_json(**result)
# do the update
kc.update_group(updated_group, realm=realm)
after_group = kc.get_group_by_groupid(updated_group['id'], realm=realm)
result['group'] = after_group
result['msg'] = "Group {id} has been updated".format(id=after_group['id'])
module.exit_json(**result)
elif state == 'absent':
result['group'] = dict()
if module._diff:
result['diff'] = dict(before=before_group, after='')
if module.check_mode:
module.exit_json(**result)
# delete for real
gid = before_group['id']
kc.delete_group(groupid=gid, realm=realm)
result['changed'] = True
result['msg'] = "Group {name} has been deleted".format(name=before_group['name'])
module.exit_json(**result)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1 @@
onepassword_info.py

View file

@ -0,0 +1,395 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2018, Ryan Conway (@rylon)
# (c) 2018, Scott Buchanan <sbuchanan@ri.pn> (onepassword.py used as starting point)
# (c) 2018, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
module: onepassword_info
author:
- Ryan Conway (@Rylon)
requirements:
- C(op) 1Password command line utility. See U(https://support.1password.com/command-line/)
notes:
- Tested with C(op) version 0.5.5
- "Based on the C(onepassword) lookup plugin by Scott Buchanan <sbuchanan@ri.pn>."
- When this module is called with the deprecated C(onepassword_facts) name, potentially sensitive data
from 1Password is returned as Ansible facts. Facts are subject to caching if enabled, which means this
data could be stored in clear text on disk or in a database.
short_description: Gather items from 1Password
description:
- M(onepassword_info) wraps the C(op) command line utility to fetch data about one or more 1Password items.
- A fatal error occurs if any of the items being searched for can not be found.
- Recommend using with the C(no_log) option to avoid logging the values of the secrets being retrieved.
- This module was called C(onepassword_facts) before Ansible 2.9, returning C(ansible_facts).
Note that the M(onepassword_info) module no longer returns C(ansible_facts)!
You must now use the C(register) option to use the facts in other tasks.
options:
search_terms:
type: list
description:
- A list of one or more search terms.
- Each search term can either be a simple string or it can be a dictionary for more control.
- When passing a simple string, I(field) is assumed to be C(password).
- When passing a dictionary, the following fields are available.
suboptions:
name:
type: str
description:
- The name of the 1Password item to search for (required).
field:
type: str
description:
- The name of the field to search for within this item (optional, defaults to "password" (or "document" if the item has an attachment).
section:
type: str
description:
- The name of a section within this item containing the specified field (optional, will search all sections if not specified).
vault:
type: str
description:
- The name of the particular 1Password vault to search, useful if your 1Password user has access to multiple vaults (optional).
required: True
auto_login:
type: dict
description:
- A dictionary containing authentication details. If this is set, M(onepassword_info) will attempt to sign in to 1Password automatically.
- Without this option, you must have already logged in via the 1Password CLI before running Ansible.
- It is B(highly) recommended to store 1Password credentials in an Ansible Vault. Ensure that the key used to encrypt
the Ansible Vault is equal to or greater in strength than the 1Password master password.
suboptions:
subdomain:
type: str
description:
- 1Password subdomain name (<subdomain>.1password.com).
- If this is not specified, the most recent subdomain will be used.
username:
type: str
description:
- 1Password username.
- Only required for initial sign in.
master_password:
type: str
description:
- The master password for your subdomain.
- This is always required when specifying C(auto_login).
required: True
secret_key:
type: str
description:
- The secret key for your subdomain.
- Only required for initial sign in.
default: {}
required: False
cli_path:
type: path
description: Used to specify the exact path to the C(op) command line interface
required: False
default: 'op'
'''
EXAMPLES = '''
# Gather secrets from 1Password, assuming there is a 'password' field:
- name: Get a password
onepassword_info:
search_terms: My 1Password item
delegate_to: localhost
register: my_1password_item
no_log: true # Don't want to log the secrets to the console!
# Gather secrets from 1Password, with more advanced search terms:
- name: Get a password
onepassword_info:
search_terms:
- name: My 1Password item
field: Custom field name # optional, defaults to 'password'
section: Custom section name # optional, defaults to 'None'
vault: Name of the vault # optional, only necessary if there is more than 1 Vault available
delegate_to: localhost
register: my_1password_item
no_log: True # Don't want to log the secrets to the console!
# Gather secrets combining simple and advanced search terms to retrieve two items, one of which we fetch two
# fields. In the first 'password' is fetched, as a field name is not specified (default behaviour) and in the
# second, 'Custom field name' is fetched, as that is specified explicitly.
- name: Get a password
onepassword_info:
search_terms:
- My 1Password item # 'name' is optional when passing a simple string...
- name: My Other 1Password item # ...but it can also be set for consistency
- name: My 1Password item
field: Custom field name # optional, defaults to 'password'
section: Custom section name # optional, defaults to 'None'
vault: Name of the vault # optional, only necessary if there is more than 1 Vault available
- name: A 1Password item with document attachment
delegate_to: localhost
register: my_1password_item
no_log: true # Don't want to log the secrets to the console!
- name: Debug a password (for example)
debug:
msg: "{{ my_1password_item['onepassword']['My 1Password item'] }}"
'''
RETURN = '''
---
# One or more dictionaries for each matching item from 1Password, along with the appropriate fields.
# This shows the response you would expect to receive from the third example documented above.
onepassword:
description: Dictionary of each 1password item matching the given search terms, shows what would be returned from the third example above.
returned: success
type: dict
sample:
"My 1Password item":
password: the value of this field
Custom field name: the value of this field
"My Other 1Password item":
password: the value of this field
"A 1Password item with document attachment":
document: the contents of the document attached to this item
'''
import errno
import json
import os
import re
from subprocess import Popen, PIPE
from ansible.module_utils._text import to_bytes, to_native
from ansible.module_utils.basic import AnsibleModule
class AnsibleModuleError(Exception):
def __init__(self, results):
self.results = results
def __repr__(self):
return self.results
class OnePasswordInfo(object):
def __init__(self):
self.cli_path = module.params.get('cli_path')
self.config_file_path = '~/.op/config'
self.auto_login = module.params.get('auto_login')
self.logged_in = False
self.token = None
terms = module.params.get('search_terms')
self.terms = self.parse_search_terms(terms)
def _run(self, args, expected_rc=0, command_input=None, ignore_errors=False):
if self.token:
# Adds the session token to all commands if we're logged in.
args += [to_bytes('--session=') + self.token]
command = [self.cli_path] + args
p = Popen(command, stdout=PIPE, stderr=PIPE, stdin=PIPE)
out, err = p.communicate(input=command_input)
rc = p.wait()
if not ignore_errors and rc != expected_rc:
raise AnsibleModuleError(to_native(err))
return rc, out, err
def _parse_field(self, data_json, item_id, field_name, section_title=None):
data = json.loads(data_json)
if ('documentAttributes' in data['details']):
# This is actually a document, let's fetch the document data instead!
document = self._run(["get", "document", data['overview']['title']])
return {'document': document[1].strip()}
else:
# This is not a document, let's try to find the requested field
# Some types of 1Password items have a 'password' field directly alongside the 'fields' attribute,
# not inside it, so we need to check there first.
if (field_name in data['details']):
return {field_name: data['details'][field_name]}
# Otherwise we continue looking inside the 'fields' attribute for the specified field.
else:
if section_title is None:
for field_data in data['details'].get('fields', []):
if field_data.get('name', '').lower() == field_name.lower():
return {field_name: field_data.get('value', '')}
# Not found it yet, so now lets see if there are any sections defined
# and search through those for the field. If a section was given, we skip
# any non-matching sections, otherwise we search them all until we find the field.
for section_data in data['details'].get('sections', []):
if section_title is not None and section_title.lower() != section_data['title'].lower():
continue
for field_data in section_data.get('fields', []):
if field_data.get('t', '').lower() == field_name.lower():
return {field_name: field_data.get('v', '')}
# We will get here if the field could not be found in any section and the item wasn't a document to be downloaded.
optional_section_title = '' if section_title is None else " in the section '%s'" % section_title
module.fail_json(msg="Unable to find an item in 1Password named '%s' with the field '%s'%s." % (item_id, field_name, optional_section_title))
def parse_search_terms(self, terms):
processed_terms = []
for term in terms:
if not isinstance(term, dict):
term = {'name': term}
if 'name' not in term:
module.fail_json(msg="Missing required 'name' field from search term, got: '%s'" % to_native(term))
term['field'] = term.get('field', 'password')
term['section'] = term.get('section', None)
term['vault'] = term.get('vault', None)
processed_terms.append(term)
return processed_terms
def get_raw(self, item_id, vault=None):
try:
args = ["get", "item", item_id]
if vault is not None:
args += ['--vault={0}'.format(vault)]
rc, output, dummy = self._run(args)
return output
except Exception as e:
if re.search(".*not found.*", to_native(e)):
module.fail_json(msg="Unable to find an item in 1Password named '%s'." % item_id)
else:
module.fail_json(msg="Unexpected error attempting to find an item in 1Password named '%s': %s" % (item_id, to_native(e)))
def get_field(self, item_id, field, section=None, vault=None):
output = self.get_raw(item_id, vault)
return self._parse_field(output, item_id, field, section) if output != '' else ''
def full_login(self):
if self.auto_login is not None:
if None in [self.auto_login.get('subdomain'), self.auto_login.get('username'),
self.auto_login.get('secret_key'), self.auto_login.get('master_password')]:
module.fail_json(msg='Unable to perform initial sign in to 1Password. '
'subdomain, username, secret_key, and master_password are required to perform initial sign in.')
args = [
'signin',
'{0}.1password.com'.format(self.auto_login['subdomain']),
to_bytes(self.auto_login['username']),
to_bytes(self.auto_login['secret_key']),
'--output=raw',
]
try:
rc, out, err = self._run(args, command_input=to_bytes(self.auto_login['master_password']))
self.token = out.strip()
except AnsibleModuleError as e:
module.fail_json(msg="Failed to perform initial sign in to 1Password: %s" % to_native(e))
else:
module.fail_json(msg="Unable to perform an initial sign in to 1Password. Please run '%s sigin' "
"or define credentials in 'auto_login'. See the module documentation for details." % self.cli_path)
def get_token(self):
# If the config file exists, assume an initial signin has taken place and try basic sign in
if os.path.isfile(self.config_file_path):
if self.auto_login is not None:
# Since we are not currently signed in, master_password is required at a minimum
if not self.auto_login.get('master_password'):
module.fail_json(msg="Unable to sign in to 1Password. 'auto_login.master_password' is required.")
# Try signing in using the master_password and a subdomain if one is provided
try:
args = ['signin', '--output=raw']
if self.auto_login.get('subdomain'):
args = ['signin', self.auto_login['subdomain'], '--output=raw']
rc, out, err = self._run(args, command_input=to_bytes(self.auto_login['master_password']))
self.token = out.strip()
except AnsibleModuleError:
self.full_login()
else:
self.full_login()
else:
# Attempt a full sign in since there appears to be no existing sign in
self.full_login()
def assert_logged_in(self):
try:
rc, out, err = self._run(['get', 'account'], ignore_errors=True)
if rc == 0:
self.logged_in = True
if not self.logged_in:
self.get_token()
except OSError as e:
if e.errno == errno.ENOENT:
module.fail_json(msg="1Password CLI tool '%s' not installed in path on control machine" % self.cli_path)
raise e
def run(self):
result = {}
self.assert_logged_in()
for term in self.terms:
value = self.get_field(term['name'], term['field'], term['section'], term['vault'])
if term['name'] in result:
# If we already have a result for this key, we have to append this result dictionary
# to the existing one. This is only applicable when there is a single item
# in 1Password which has two different fields, and we want to retrieve both of them.
result[term['name']].update(value)
else:
# If this is the first result for this key, simply set it.
result[term['name']] = value
return result
def main():
global module
module = AnsibleModule(
argument_spec=dict(
cli_path=dict(type='path', default='op'),
auto_login=dict(type='dict', options=dict(
subdomain=dict(type='str'),
username=dict(type='str'),
master_password=dict(required=True, type='str', no_log=True),
secret_key=dict(type='str', no_log=True),
), default=None),
search_terms=dict(required=True, type='list')
),
supports_check_mode=True
)
results = {'onepassword': OnePasswordInfo().run()}
if module._name == 'onepassword_facts':
module.deprecate("The 'onepassword_facts' module has been renamed to 'onepassword_info'. "
"When called with the new name it no longer returns 'ansible_facts'", version='2.13')
module.exit_json(changed=False, ansible_facts=results)
else:
module.exit_json(changed=False, **results)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,203 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2016, Werner Dijkerman (ikben@werner-dijkerman.nl)
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: opendj_backendprop
short_description: Will update the backend configuration of OpenDJ via the dsconfig set-backend-prop command.
description:
- This module will update settings for OpenDJ with the command set-backend-prop.
- It will check first via de get-backend-prop if configuration needs to be applied.
author:
- Werner Dijkerman (@dj-wasabi)
options:
opendj_bindir:
description:
- The path to the bin directory of OpenDJ.
required: false
default: /opt/opendj/bin
hostname:
description:
- The hostname of the OpenDJ server.
required: true
port:
description:
- The Admin port on which the OpenDJ instance is available.
required: true
username:
description:
- The username to connect to.
required: false
default: cn=Directory Manager
password:
description:
- The password for the cn=Directory Manager user.
- Either password or passwordfile is needed.
required: false
passwordfile:
description:
- Location to the password file which holds the password for the cn=Directory Manager user.
- Either password or passwordfile is needed.
required: false
backend:
description:
- The name of the backend on which the property needs to be updated.
required: true
name:
description:
- The configuration setting to update.
required: true
value:
description:
- The value for the configuration item.
required: true
state:
description:
- If configuration needs to be added/updated
required: false
default: "present"
'''
EXAMPLES = '''
- name: "Add or update OpenDJ backend properties"
action: opendj_backendprop
hostname=localhost
port=4444
username="cn=Directory Manager"
password=password
backend=userRoot
name=index-entry-limit
value=5000
'''
RETURN = '''
'''
from ansible.module_utils.basic import AnsibleModule
class BackendProp(object):
def __init__(self, module):
self._module = module
def get_property(self, opendj_bindir, hostname, port, username, password_method, backend_name):
my_command = [
opendj_bindir + '/dsconfig',
'get-backend-prop',
'-h', hostname,
'--port', str(port),
'--bindDN', username,
'--backend-name', backend_name,
'-n', '-X', '-s'
] + password_method
rc, stdout, stderr = self._module.run_command(my_command)
if rc == 0:
return stdout
else:
self._module.fail_json(msg="Error message: " + str(stderr))
def set_property(self, opendj_bindir, hostname, port, username, password_method, backend_name, name, value):
my_command = [
opendj_bindir + '/dsconfig',
'set-backend-prop',
'-h', hostname,
'--port', str(port),
'--bindDN', username,
'--backend-name', backend_name,
'--set', name + ":" + value,
'-n', '-X'
] + password_method
rc, stdout, stderr = self._module.run_command(my_command)
if rc == 0:
return True
else:
self._module.fail_json(msg="Error message: " + stderr)
def validate_data(self, data=None, name=None, value=None):
for config_line in data.split('\n'):
if config_line:
split_line = config_line.split()
if split_line[0] == name:
if split_line[1] == value:
return True
return False
def main():
module = AnsibleModule(
argument_spec=dict(
opendj_bindir=dict(default="/opt/opendj/bin", type="path"),
hostname=dict(required=True),
port=dict(required=True),
username=dict(default="cn=Directory Manager", required=False),
password=dict(required=False, no_log=True),
passwordfile=dict(required=False, type="path"),
backend=dict(required=True),
name=dict(required=True),
value=dict(required=True),
state=dict(default="present"),
),
supports_check_mode=True,
mutually_exclusive=[['password', 'passwordfile']],
required_one_of=[['password', 'passwordfile']]
)
opendj_bindir = module.params['opendj_bindir']
hostname = module.params['hostname']
port = module.params['port']
username = module.params['username']
password = module.params['password']
passwordfile = module.params['passwordfile']
backend_name = module.params['backend']
name = module.params['name']
value = module.params['value']
state = module.params['state']
if module.params["password"] is not None:
password_method = ['-w', password]
elif module.params["passwordfile"] is not None:
password_method = ['-j', passwordfile]
opendj = BackendProp(module)
validate = opendj.get_property(opendj_bindir=opendj_bindir,
hostname=hostname,
port=port,
username=username,
password_method=password_method,
backend_name=backend_name)
if validate:
if not opendj.validate_data(data=validate, name=name, value=value):
if module.check_mode:
module.exit_json(changed=True)
if opendj.set_property(opendj_bindir=opendj_bindir,
hostname=hostname,
port=port,
username=username,
password_method=password_method,
backend_name=backend_name,
name=name,
value=value):
module.exit_json(changed=True)
else:
module.exit_json(changed=False)
else:
module.exit_json(changed=False)
else:
module.exit_json(changed=False)
if __name__ == '__main__':
main()