Skip to content
Snippets Groups Projects
Commit 62fd4cc1 authored by Thomas Woerner's avatar Thomas Woerner
Browse files

New topology managament modules

There are now two topology management modules placed in the plugins folder:

  plugins/modules/ipatopologysegment.py
  plugins/modules/ipatopologysuffix.py

Topology segments can be added, removed and reinitialized with the
ipatopologysegment module. Also it is possible to verify topology suffixes
with the ipatopologysuffix module.

A new module_utils for plugins has been added:

  plugins/module_utils/ansible_freeipa_module.py

And documentation for the modules:

  README-topology.md

New sample playbooks are available in playbooks/topology:

  playbooks/topology/add-topologysegment.yml
  playbooks/topology/delete-topologysegment.yml
  playbooks/topology/reinitialize-topologysegment.yml
  playbooks/topology/verify-topologysuffix.yml

The plugins folder can be used with the new Ansible Collections supported
by Ansible 2.8 and Ansible galaxy 3.2.
parent c822423b
No related branches found
No related tags found
No related merge requests found
Topology modules
================
Description
-----------
These modules allow to manage the topology. That means that topology segments can be added, removed and reinitialized. Also it is possible to verify topology suffixes.
Features
--------
* Topology management
Supported FreeIPA Versions
--------------------------
FreeIPA versions 4.4.0 and up are supported by the ipatopologysegment and ipatopologysuffix modules.
Requirements
------------
**Controller**
* Ansible version: 2.8+
**Node**
* Supported FreeIPA version (see above)
Usage
=====
Example inventory file
```ini
[ipaserver]
ipaserver.test.local
```
Example playbook to add a topology segment wiht default name (cn):
```yaml
---
- name: Playbook to handle topologysegment
hosts: ipaserver
become: true
tasks:
- name: Add topology segment
ipatopologysegment:
password: MyPassword123
suffix: domain
left: ipareplica1.test.local
right: ipareplica2.test.local
state: present
```
The name (cn) can also be set if it should not be the default `{left}-to-{rkight}`.
Example playbook to delete a topology segment:
```yaml
---
- name: Playbook to handle topologysegment
hosts: ipaserver
become: true
tasks:
- name: Delete topology segment
ipatopologysegment:
password: MyPassword123
suffix: domain
left: ipareplica1.test.local
right: ipareplica2.test.local
state: absent
```
It is possible to either use the name (cn) or left and right nodes. If left and right nodes are used, then the name will be searched and used internally.
Example playbook to reinitialize a topology segment:
```yaml
---
- name: Playbook to handle topologysegment
hosts: ipaserver
become: true
tasks:
- name: Reinitialize topology segment
ipatopologysegment:
password: MyPassword123
suffix: domain
left: ipareplica1.test.local
right: ipareplica2.test.local
direction: left-to-right
state: reinitialized
```
It is possible to either use the name (cn) or left and right nodes. If left and right nodes are used, then the name will be searched and used internally.
Example playbook to verify a topology suffix:
```yaml
---
- name: Playbook to handle topologysuffix
hosts: ipaserver
become: true
tasks:
- name: Verify topology suffix
ipatopologysuffix:
password: MyPassword123
suffix: domain
state: verified
```
Variables
=========
ipatopologysegment
------------------
Variable | Description | Required
-------- | ----------- | --------
`principal` | The admin principal is a string and defaults to `admin` | no
`password` | The admin password is a string and is required if there is no admin ticket available on the node | no
`suffix` | The topology suffix to be used, this can either be `domain` or `ca` | yes
`name` \| `cn` | The topology segment name (cn) is the unique identifier for a segment. | no
`left` \| `leftnode` | The left replication node string - an IPA server | no
`right` \| `rightnode` | The right replication node string - an IPA server | no
`direction` | The direction a segment will be reinitialized. It can either be `left-to-right` or `right-to-left` and only used with `state: reinitialized` |
`state` | The state to ensure. It can be one of `present`, `absent`, `enabled`, `disabled` or `reinitialized` | yes
ipatopologysuffix
-----------------
Verify FreeIPA topology suffix
Variable | Description | Required
-------- | ----------- | --------
`principal` | The admin principal is a string and defaults to `admin` | no
`password` | The admin password is a string and is required if there is no admin ticket available on the node | no
`suffix` | The topology suffix to be used, this can either be `domain` or `ca` | yes
`state` | The state to ensure. It can only be `verified` | yes
Authors
=======
Thomas Woerner
......@@ -11,6 +11,7 @@ Features
* Cluster deployments: Server, replicas and clients in one playbook
* One-time-password (OTP) support for client installation
* Repair mode for clients
* Modules for topology management
Supported FreeIPA Versions
--------------------------
......@@ -307,3 +308,9 @@ Roles
* [Server](roles/ipaserver/README.md)
* [Replica](roles/ipareplica/README.md)
* [Client](roles/ipaclient/README.md)
Plugins in plugin/modules
=========================
* [ipatopologysegment](README-topology.md)
* [ipatopologysuffix](README-topology.md)
---
- name: Playbook to handle topologysegment
hosts: ipaserver
become: true
tasks:
- name: Add topology segment
ipatopologysegment:
password: MyPassword123
suffix: domain
left: ipareplica1.test.local
right: ipareplica2.test.local
state: present
---
- name: Playbook to handle topologysegment
hosts: ipaserver
become: true
tasks:
- name: Delete topology segment
ipatopologysegment:
password: MyPassword123
suffix: domain
left: ipareplica1.test.local
right: ipareplica2.test.local
state: absent
---
- name: Playbook to handle topologysegment
hosts: ipaserver
become: true
tasks:
- name: Reinitialize topology segment
ipatopologysegment:
password: MyPassword123
suffix: domain
left: ipareplica1.test.local
right: ipareplica2.test.local
direction: left-to-right
state: reinitialized
---
- name: Playbook to handle topologysuffix
hosts: ipaserver
become: true
tasks:
- name: Verify topology suffix
ipatopologysuffix:
password: MyPassword123
suffix: domain
state: verified
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Authors:
# Thomas Woerner <twoerner@redhat.com>
#
# Copyright (C) 2019 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/>.
import os
import sys
import tempfile
import shutil
from ipalib import api, errors
from ipalib.config import Env
from ipalib.constants import DEFAULT_CONFIG
try:
from ipalib.install.kinit import kinit_password
except ImportError:
from ipapython.ipautil import kinit_password
from ipapython.ipautil import run
from ipaplatform.paths import paths
from ipalib.krb_utils import get_credentials_if_valid
def valid_creds(principal):
"""
Get valid credintials matching the princial
"""
creds = get_credentials_if_valid()
if creds and \
creds.lifetime > 0 and \
"%s@" % principal in creds.name.display_as(creds.name.name_type):
return True
return False
def temp_kinit(principal, password):
"""
kinit with password using a temporary ccache
"""
if not password:
raise RuntimeError("The password is not set")
if not principal:
principal = "admin"
ccache_dir = tempfile.mkdtemp(prefix='krbcc')
ccache_name = os.path.join(ccache_dir, 'ccache')
try:
kinit_password(principal, password, ccache_name)
except RuntimeError as e:
raise RuntimeError("Kerberos authentication failed: {}".format(e))
return ccache_dir, ccache_name
def temp_kdestroy(ccache_dir, ccache_name):
"""
Destroy temporary ticket and remove temporary ccache
"""
if ccache_name is not None:
run([paths.KDESTROY, '-c', ccache_name], raiseonerr=False)
if ccache_dir is not None:
shutil.rmtree(ccache_dir, ignore_errors=True)
def api_connect():
"""
Create environment, initialize api and connect to ldap2
"""
env = Env()
env._bootstrap()
env._finalize_core(**dict(DEFAULT_CONFIG))
api.bootstrap(context='server', debug=env.debug, log=None)
api.finalize()
api.Backend.ldap2.connect()
def api_command(module, command, name, args):
"""
Call ipa.Command, use AnsibleModule.fail_json for error handling
"""
try:
return api.Command[command](name, **args)
except Exception as e:
module.fail_json(msg="%s: %s" % (command, e))
def execute_api_command(module, principal, password, command, name, args):
"""
Get KRB ticket if not already there, initialize api, connect,
execute command and destroy ticket again if it has been created also.
"""
ccache_dir = None
ccache_name = None
try:
if not valid_creds(principal):
ccache_dir, ccache_name = temp_kinit(principal, password)
api_connect()
return api_command(module, command, name, args)
except Exception as e:
module.fail_json(msg=str(e))
finally:
temp_kdestroy(ccache_dir, ccache_name)
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Authors:
# Thomas Woerner <twoerner@redhat.com>
#
# Copyright (C) 2019 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",
"supported_by": "community",
"status": ["preview"],
}
DOCUMENTATION = """
---
module: ipatopologysegment
short description: Manage FreeIPA topology segments
description: Manage FreeIPA topology segments
options:
principal:
description: The admin principal
default: admin
password:
description: The admin password
required: false
suffix:
description: Topology suffix
required: true
choices: ["domain", "ca"]
name:
description: Topology segment name, unique identifier.
required: false
aliases: ["cn"]
left:
description: Left replication node - an IPA server
aliases: ["leftnode"]
right:
description: Right replication node - an IPA server
aliases: ["rightnode"]
direction:
description: The direction a segment will be reinitialized
required: false
choices: ["left-to-right", "right-to-left"]
state:
description: State to ensure
default: present
choices: ["present", "absent", "enabled", "disabled", "reinitialized"]
author:
- Thomas Woerner
"""
EXAMPLES = """
- ipatopologysegment:
suffix: domain
left: ipaserver.test.local
right: ipareplica1.test.local
state: present
- ipatopologysegment:
suffix: domain
name: ipaserver.test.local-to-replica1.test.local
state: absent
- ipatopologysegment:
suffix: domain
left: ipaserver.test.local
right: ipareplica1.test.local
state: absent
- ipatopologysegment:
suffix: ca
name: ipaserver.test.local-to-replica1.test.local
direction: left-to-right
state: reinitialized
"""
RETURN = """
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible.module_utils.ansible_freeipa_module import temp_kinit, \
temp_kdestroy, valid_creds, api_connect, api_command
def find_left_right(module, suffix, left, right):
_args = {
"iparepltoposegmentleftnode": to_text(left),
"iparepltoposegmentrightnode": to_text(right),
}
_result = api_command(module, "topologysegment_find",
to_text(suffix), _args)
if len(_result["result"]) > 1:
module.fail_json(
msg="Combination of left node '%s' and right node '%s' is "
"not unique for suffix '%s'" % (left, right, suffix))
elif len(_result["result"]) == 1:
return _result["result"][0]
else:
return None
def find_cn(module, suffix, name):
_args = {
"cn": to_text(name),
}
_result = api_command(module, "topologysegment_find",
to_text(suffix), _args)
if len(_result["result"]) > 1:
module.fail_json(
msg="CN '%s' is not unique for suffix '%s'" % (name, suffix))
elif len(_result["result"]) == 1:
return _result["result"][0]
else:
return None
def main():
ansible_module = AnsibleModule(
argument_spec=dict(
principal=dict(type="str", default="admin"),
password=dict(type="str", required=False, no_log=True),
suffix=dict(choices=["domain", "ca"], required=True),
name=dict(type="str", aliases=["cn"], default=None),
left=dict(type="str", aliases=["leftnode"], default=None),
right=dict(type="str", aliases=["rightnode"], default=None),
direction=dict(type="str", default=None,
choices=["left-to-right", "right-to-left"]),
state=dict(type="str", default="present",
choices=["present", "absent", "enabled", "disabled",
"reinitialized"]),
),
supports_check_mode=True,
)
ansible_module._ansible_debug = True
# Get parameters
principal = ansible_module.params.get("principal")
password = ansible_module.params.get("password")
suffix = ansible_module.params.get("suffix")
name = ansible_module.params.get("name")
left = ansible_module.params.get("left")
right = ansible_module.params.get("right")
direction = ansible_module.params.get("direction")
state = ansible_module.params.get("state")
# Check parameters
if state != "reinitialized" and direction is not None:
ansible_module.fail_json(
msg="Direction is not supported in this mode.")
# Init
changed = False
ccache_dir = None
ccache_name = None
try:
if not valid_creds(principal):
ccache_dir, ccache_name = temp_kinit(principal, password)
api_connect()
command = None
# Get name (cn) from left and right node if set for absent, disabled
# or reinitialized.
if state in ["absent", "disabled", "reinitialized"]:
if left is not None and right is not None:
left_right = find_left_right(ansible_module, suffix,
left, right)
if left_right is not None:
if name is not None and \
left_right["cn"][0] != to_text(name):
ansible_module.fail_json(
msg="Left and right nodes do not match "
"given name name (cn) '%s'" % name)
args = {
"cn": left_right["cn"][0]
}
# else: Nothing to change
elif name is not None:
result = find_cn(ansible_module, suffix, name)
if result is not None:
args = {
"cn": result["cn"][0]
}
# else: Nothing to change
else:
ansible_module.fail_json(
msg="Either left and right or name need to be set.")
# Create command
if state in ["present", "enabled"]:
# Make sure topology segment exists
if left is None or right is None:
ansible_module.fail_json(
msg="Left and right need to be set.")
args = {
"iparepltoposegmentleftnode": to_text(left),
"iparepltoposegmentrightnode": to_text(right),
}
if name is not None:
args["cn"] = to_text(name)
res_left_right = find_left_right(ansible_module, suffix,
left, right)
if res_left_right is not None:
if name is not None and \
res_left_right["cn"][0] != to_text(name):
ansible_module.fail_json(
msg="Left and right nodes already used with "
"different name (cn) '%s'" % res_left_right["cn"])
# Left and right nodes and also the name can not be
# changed
for key in [ "iparepltoposegmentleftnode",
"iparepltoposegmentrightnode" ]:
if key in args:
del args[key]
if len(args) > 1:
# cn needs to be in args always
command = "topologysegment_mod"
# else: Nothing to change
else:
if name is None:
args["cn"] = to_text("%s-to-%s" % (left, right))
command = "topologysegment_add"
elif state in ["absent", "disabled"]:
# Make sure topology segment does not exist
if len(args) > 0:
# Either name defined or found name from left and right node
command = "topologysegment_del"
elif state == "reinitialized":
# Reinitialize segment
if len(args) > 0:
# Either name defined or found name from left and right node
command = "topologysegment_reinitialize"
if direction == "left-to-right":
args["left"] = True
elif direction == "right-to-left":
args["right"] = True
else:
ansible_module.fail_json(msg="Unknown direction '%s'" %
direction)
else:
ansible_module.fail_json(msg="Unkown state '%s'" % state)
# Execute command
if command is not None:
result = api_command(ansible_module, command,
to_text(suffix), args)
changed = True
except Exception as e:
ansible_module.fail_json(msg=str(e))
finally:
temp_kdestroy(ccache_dir, ccache_name)
# Done
ansible_module.exit_json(changed=changed)
if __name__ == "__main__":
main()
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Authors:
# Thomas Woerner <twoerner@redhat.com>
#
# Copyright (C) 2019 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",
"supported_by": "community",
"status": ["preview"],
}
DOCUMENTATION = """
---
module: ipatopologysuffix
short description: Verify FreeIPA topology suffix
description: Verify FreeIPA topology suffix
options:
principal:
description: The admin principal
default: admin
password:
description: The admin password
required: false
suffix:
description: Topology suffix
required: true
choices: ["domain", "ca"]
state:
description: State to ensure
default: verified
choices: ["verified"]
author:
- Thomas Woerner
"""
EXAMPLES = """
- ipatopologysuffix:
suffix: domain
state: verified
"""
RETURN = """
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible.module_utils.ansible_freeipa_module import execute_api_command
def main():
ansible_module = AnsibleModule(
argument_spec=dict(
principal=dict(type="str", default="admin"),
password=dict(type="str", required=False, no_log=True),
suffix=dict(choices=["domain", "ca"], required=True),
state=dict(type="str", default="verified",
choices=["verified"]),
),
supports_check_mode=True,
)
ansible_module._ansible_debug = True
# Get parameters
principal = ansible_module.params.get("principal")
password = ansible_module.params.get("password")
suffix = ansible_module.params.get("suffix")
state = ansible_module.params.get("state")
# Check parameters
# Init
# Create command
if state in ["verified"]:
command = "topologysuffix_verify"
args = {}
else:
ansible_module.fail_json(msg="Unkown state '%s'" % state)
# Execute command
execute_api_command(ansible_module, principal, password,
command, to_text(suffix), args)
# Done
ansible_module.exit_json(changed=True)
if __name__ == "__main__":
main()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment