Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Authors:
# Florence Blanc-Renaud <frenaud@redhat.com>
#
# Copyright (C) 2017 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
ANSIBLE_METADATA = {'metadata_version': '1.0',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: ipahost
short description: Manage IPA hosts
description:
Manage hosts in a IPA domain.
The operation needs to be authenticated with Kerberos either by providing
a password or a keytab corresponding to a principal allowed to perform
host operations.
options:
principal:
description: Kerberos principal used to manage the host
Florence Blanc-Renaud
committed
required: true
default: admin
password:
description: Password for the kerberos principal
required: false
keytab:
description: Keytab file containing the Kerberos principal and encrypted key
required: false
Florence Blanc-Renaud
committed
lifetime:
description: Sets the default lifetime for initial ticket requests
required: false
default: 1h
fqdn:
description: the fully-qualified hostname of the host to add/modify/remove
required: true
random:
description: generate a random password to be used in bulk enrollment
required: false
state:
description: the host state
required: false
default: present
choices: [ "present", "absent" ]
certificates:
description: a list of host certificates
required: false
type: list
sshpubkey:
description: the SSH public key for the host
required: false
ipaddress:
description: the IP address for the host
required: false
requirements:
- gssapi on the Ansible controller
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
author:
- "Florence Blanc-Renaud"
'''
EXAMPLES = '''
# Example from Ansible Playbooks
# Add a new host with a random OTP, authenticate using principal/password
- ipahost:
principal: admin
password: MySecretPassword
fqdn: ipaclient.ipa.domain.com
ipaddress: 192.168.100.23
random: True
register: ipahost
# Add a new host, authenticate with a keytab stored on the controller node
- ipahost:
keytab: admin.keytab
fqdn: ipaclient.ipa.domain.com
# Remove a host, authenticate using principal/password
- ipahost:
principal: admin
password: MySecretPassword
fqdn: ipaclient.ipa.domain.com
state: absent
# Modify a host, add ssh public key:
- ipahost:
principal: admin
password: MySecretPassword
fqdn: ipaclient.ipa.domain.com
sshpubkey: ssh-rsa AAAA...
'''
RETURN = '''
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
host:
description: the host structure as returned from IPA API
returned: always
type: complex
contains:
dn:
description: the DN of the host entry
type: string
returned: always
fqdn:
description: the fully qualified host name
type: string
returned: always
has_keytab:
description: whether the host entry contains a keytab
type: bool
returned: always
has_password:
description: whether the host entry contains a password
type: bool
returned: always
managedby_host:
description: the list of hosts managing the host
type: list
returned: always
randompassword:
description: the OneTimePassword generated for this host
type: string
returned: changed
certificates:
description: the list of host certificates
type: list
returned: when present
sshpubkey:
description: the SSH public key for the host
type: string
returned: when present
ipaddress:
description: the IP address for the host
type: string
returned: when present
from ansible.module_utils.basic import AnsibleModule
from ipalib import api, errors
from ipaplatform.paths import paths
from ipapython.ipautil import run
if six.PY3:
unicode = str
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
def get_host_diff(ipa_host, module_host):
"""
Compares two dictionaries containing host attributes and builds a dict
of differences.
:param ipa_host: the host structure seen from IPA
:param module_host: the target host structure seen from the module params
:return: a dict representing the host attributes to apply
"""
non_updateable_keys = ['ip_address']
data = dict()
for key in non_updateable_keys:
if key in module_host:
del module_host[key]
for key in module_host.keys():
ipa_value = ipa_host.get(key, None)
module_value = module_host.get(key, None)
if isinstance(ipa_value, list) and not isinstance(module_value, list):
module_value = [module_value]
if isinstance(ipa_value, list) and isinstance(module_value, list):
ipa_value = sorted(ipa_value)
module_value = sorted(module_value)
if ipa_value != module_value:
data[key]=unicode(module_value)
return data
def get_module_host(module):
"""
Creates a structure representing the host information
Reads the module parameters and builds the host structure as expected from
the module
:param module: the ansible module
:returns: a dict representing the host attributes
"""
data = dict()
certificates = module.params.get('certificates')
if certificates:
data['usercertificate'] = certificates
sshpubkey = module.params.get('sshpubkey')
if sshpubkey:
data['ipasshpubkey'] = unicode(sshpubkey)
ipaddress = module.params.get('ipaddress')
if ipaddress:
data['ip_address'] = unicode(ipaddress)
random = module.params.get('random')
if random:
data['random'] = random
return data
def ensure_host_present(module, api, ipahost):
"""
Ensures that the host exists in IPA and has the same attributes.
:param module: the ansible module
:param api: IPA api handle
:param ipahost: the host information present in IPA, can be none if the
host does not exist
"""
fqdn = unicode(module.params.get('fqdn'))
if ipahost:
# Host already present, need to compare the attributes
module_host = get_module_host(module)
diffs = get_host_diff(ipahost, module_host)
if not diffs:
# Same attributes, success
module.exit_json(changed=False, host=ipahost)
# Need to modify the host - only if not in check_mode
if module.check_mode:
module.exit_json(changed=True)
# If we want to create a random password, and the host
# already has Keytab: true, then we need first to run
# ipa host-disable in order to remove OTP and keytab
if module.params.get('random') and ipahost['has_keytab'] == True:
api.Command.host_disable(fqdn)
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
result = api.Command.host_mod(fqdn, **diffs)
# Save random password as it is not displayed by host-show
if module.params.get('random'):
randompassword = result['result']['randompassword']
result = api.Command.host_show(fqdn)
if module.params.get('random'):
result['result']['randompassword'] = randompassword
module.exit_json(changed=True, host=result['result'])
if not ipahost:
# Need to add the user, only if not in check_mode
if module.check_mode:
module.exit_json(changed=True)
# Must add the user
module_host = get_module_host(module)
result = api.Command.host_add(fqdn, **module_host)
# Save random password as it is not displayed by host-show
if module.params.get('random'):
randompassword = result['result']['randompassword']
result = api.Command.host_show(fqdn)
if module.params.get('random'):
result['result']['randompassword'] = randompassword
module.exit_json(changed=True, host=result['result'])
def ensure_host_absent(module, api, host):
"""
Ensures that the host does not exist in IPA
:param module: the ansible module
:param api: the IPA API handle
:param host: the host information present in IPA, can be none if the
host does not exist
"""
if not host:
# Nothing to do, host already removed
module.exit_json(changed=False)
# Need to remove the host - only if not in check_mode
if module.check_mode:
module.exit_json(changed=True, host=host)
fqdn = unicode(module.params.get('fqdn'))
try:
api.Command.host_del(fqdn)
except Exception as e:
module.fail_json(msg="Failed to remove host: %s" % e)
module.exit_json(changed=True)
def main():
"""
Main routine for the ansible module.
"""
module = AnsibleModule(
argument_spec=dict(
principal = dict(default='admin'),
Florence Blanc-Renaud
committed
ccache = dict(required=False, type='path'),
fqdn = dict(required=True),
certificates = dict(required=False, type='list'),
sshpubkey= dict(required=False),
ipaddress = dict(required=False),
random = dict(default=False, type='bool'),
state = dict(default='present', choices=[ 'present', 'absent' ]),
),
supports_check_mode=True,
)
principal = module.params.get('principal', 'admin')
Florence Blanc-Renaud
committed
ccache = module.params.get('ccache')
fqdn = unicode(module.params.get('fqdn'))
state = module.params.get('state')
try:
Florence Blanc-Renaud
committed
os.environ['KRB5CCNAME']=ccache
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
cfg = dict(
context='ansible_module',
confdir=paths.ETC_IPA,
in_server=False,
debug=False,
verbose=0,
)
api.bootstrap(**cfg)
api.finalize()
api.Backend.rpcclient.connect()
changed = False
try:
result = api.Command.host_show(fqdn, all=True)
host = result['result']
except errors.NotFound:
host = None
if state == 'present' or state == 'disabled':
changed = ensure_host_present(module, api, host)
elif state == 'absent':
changed = ensure_host_absent(module, api, host)
except Exception as e:
module.fail_json(msg="ipahost module failed : %s" % str(e))
finally:
run([paths.KDESTROY], raiseonerr=False, env=os.environ)
module.exit_json(changed=changed, host=host)
if __name__ == '__main__':
main()