Add code to deregister nodes and services from freeipa
A module has been added to delete hosts and clean up all the sub hosts and services associated with those hosts in IPA. It can be called for a single host - as in the case of a scale_down operation, or for multiple hosts - as in the case of a stack delete. Ultimately, we would like to add this functionality to ansible-freeipa. Co-Authored-By: Grzegorz Grasza <xek@redhat.com> Change-Id: Ib08f038773f8ca28d85a95921cdb522b5cd4de04
This commit is contained in:
parent
c9441fc968
commit
c4b5452672
37
bindep.txt
Normal file
37
bindep.txt
Normal file
@ -0,0 +1,37 @@
|
||||
# This file facilitates OpenStack-CI package installation
|
||||
# before the execution of any tests.
|
||||
#
|
||||
# See the following for details:
|
||||
# - https://docs.openstack.org/infra/bindep/
|
||||
# - https://opendev.org/opendev/bindep/
|
||||
#
|
||||
# Even if the role does not make use of this facility, it
|
||||
# is better to have this file empty, otherwise OpenStack-CI
|
||||
# will fall back to installing its default packages which
|
||||
# will potentially be detrimental to the tests executed.
|
||||
|
||||
# The gcc compiler
|
||||
gcc
|
||||
|
||||
# Base requirements for RPM distros
|
||||
gcc-c++ [platform:rpm]
|
||||
git [platform:rpm]
|
||||
libffi-devel [platform:rpm]
|
||||
openssl-devel [platform:rpm]
|
||||
python-devel [platform:rpm !platform:rhel-8 !platform:centos-8]
|
||||
python3-devel [platform:rpm !platform:rhel-7 !platform:centos-7]
|
||||
PyYAML [platform:rpm !platform:rhel-8 !platform:centos-8]
|
||||
python3-pyyaml [platform:rpm !platform:rhel-7 !platform:centos-7]
|
||||
python3-dnf [platform:rpm !platform:rhel-7 !platform:centos-7]
|
||||
|
||||
# For SELinux
|
||||
libselinux-python [platform:rpm !platform:rhel-8 !platform:centos-8]
|
||||
libsemanage-python [platform:redhat !platform:rhel-8 !platform:centos-8]
|
||||
libselinux-python3 [platform:rpm !platform:rhel-7 !platform:centos-7]
|
||||
libsemanage-python3 [platform:redhat !platform:rhel-7 !platform:centos-7]
|
||||
|
||||
# Required for compressing collected log files in CI
|
||||
gzip
|
||||
|
||||
# Required to build language docs
|
||||
gettext
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
six>=1.10.0 # MIT
|
||||
PyYAML>=3.12 # MIT
|
2
tox.ini
2
tox.ini
@ -10,6 +10,7 @@ install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/
|
||||
passenv = *
|
||||
sitepackages = True
|
||||
deps =
|
||||
-r {toxinidir}/requirements.txt
|
||||
-r {toxinidir}/ansible-requirements.txt
|
||||
-r {toxinidir}/test-requirements.txt
|
||||
commands = stestr run {posargs}
|
||||
@ -19,6 +20,7 @@ whitelist_externals =
|
||||
[testenv:molecule]
|
||||
setenv =
|
||||
ANSIBLE_FILTER_PLUGINS={toxinidir}/tripleo_ipa/ansible_plugins/filter
|
||||
ANSIBLE_LIBRARY={toxinidir}/tripleo_ipa/roles.galaxy/config_template/library:{toxinidir}/tripleo_ipa/ansible_plugins/modules
|
||||
ANSIBLE_ROLES_PATH={toxinidir}/tripleo_ipa/roles.galaxy:{toxinidir}/tripleo_ipa/roles
|
||||
deps =
|
||||
-r {toxinidir}/molecule-requirements.txt
|
||||
|
403
tripleo_ipa/ansible_plugins/modules/cleanup_ipa_services.py
Normal file
403
tripleo_ipa/ansible_plugins/modules/cleanup_ipa_services.py
Normal file
@ -0,0 +1,403 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2020 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
import yaml
|
||||
|
||||
import six
|
||||
from six.moves import http_client
|
||||
|
||||
from gssapi.exceptions import GSSError
|
||||
from ipalib import api
|
||||
from ipalib import errors
|
||||
|
||||
try:
|
||||
from ipapython.ipautil import kinit_keytab
|
||||
except ImportError:
|
||||
# The import moved in freeIPA 4.5.0
|
||||
from ipalib.install.kinit import kinit_keytab
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.openstack import openstack_full_argument_spec
|
||||
from ansible.module_utils.openstack import openstack_module_kwargs
|
||||
|
||||
ANSIBLE_METADATA = {
|
||||
'metadata_version': '1.0',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'
|
||||
}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: cleanup_ipa_services
|
||||
|
||||
short_description: Cleanup IPA Services and Hosts
|
||||
|
||||
version_added: "2.8"
|
||||
|
||||
description:
|
||||
- When hosts are deleted, delete the hosts, subhosts and services
|
||||
associated with the hosts in the FreeIPA server.
|
||||
- If the services are managed exclusively by the hosts, then
|
||||
delete the subhost for that service and the service itself.
|
||||
- If the service is managed by other hosts (not being deleted),
|
||||
then simply remove the host(s) being deleted from the managed_by
|
||||
attribute.
|
||||
|
||||
options:
|
||||
principal:
|
||||
description:
|
||||
- Principal to use when authenticating to FreeIPA.
|
||||
type: str
|
||||
keytab:
|
||||
description:
|
||||
- Keytab to use when authenticating to FreeIPA
|
||||
type: str
|
||||
hosts:
|
||||
description:
|
||||
- Hosts to be deleted (list of FQDNs)
|
||||
type: list
|
||||
author:
|
||||
- Ade Lee (@vakwetu)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Cleanup IPA hosts and services
|
||||
cleanup_ipa_services:
|
||||
principal: user/my_host@REALM
|
||||
keytab: /etc/krb5.keytab
|
||||
hosts:
|
||||
- test-server-0.exmaple.com
|
||||
- test-server-1.example.com
|
||||
- test-server-2.example.com
|
||||
'''
|
||||
|
||||
|
||||
class IPAClient(object):
|
||||
|
||||
def __init__(self, keytab, principal):
|
||||
self.ntries = 5
|
||||
self.retry_delay = 2
|
||||
self.keytab = keytab
|
||||
self.principal = principal
|
||||
|
||||
if self._ipa_client_configured() and not api.isdone('finalize'):
|
||||
self.ccache = "MEMORY:" + str(uuid.uuid4())
|
||||
os.environ['KRB5CCNAME'] = self.ccache
|
||||
kinit_keytab(self.principal, self.keytab, self.ccache)
|
||||
api.bootstrap(context='cleanup')
|
||||
api.finalize()
|
||||
else:
|
||||
self.ccache = os.environ['KRB5CCNAME']
|
||||
self.batch_args = list()
|
||||
|
||||
def split_principal(self, principal):
|
||||
"""Split a principal into its components. Copied from IPA 4.0.0"""
|
||||
service = hostname = realm = None
|
||||
|
||||
# Break down the principal into its component parts, which may or
|
||||
# may not include the realm.
|
||||
sp = principal.split('/')
|
||||
if len(sp) != 2:
|
||||
raise errors.MalformedServicePrincipal(reason='missing service')
|
||||
|
||||
service = sp[0]
|
||||
if len(service) == 0:
|
||||
raise errors.MalformedServicePrincipal(reason='blank service')
|
||||
sr = sp[1].split('@')
|
||||
if len(sr) > 2:
|
||||
raise errors.MalformedServicePrincipal(
|
||||
reason='unable to determine realm')
|
||||
|
||||
hostname = sr[0].lower()
|
||||
if len(sr) == 2:
|
||||
realm = sr[1].upper()
|
||||
# At some point we'll support multiple realms
|
||||
if realm != api.env.realm:
|
||||
raise errors.RealmMismatch()
|
||||
else:
|
||||
realm = api.env.realm
|
||||
|
||||
# Note that realm may be None.
|
||||
return (service, hostname, realm)
|
||||
|
||||
def split_hostname(self, hostname):
|
||||
"""Split a hostname into its host and domain parts"""
|
||||
parts = hostname.split('.')
|
||||
domain = six.text_type('.'.join(parts[1:]) + '.')
|
||||
return (parts[0], domain)
|
||||
|
||||
def __get_connection(self):
|
||||
"""Make a connection to IPA or raise an error."""
|
||||
tries = 0
|
||||
|
||||
while (tries <= self.ntries):
|
||||
logging.debug("Attempt %d of %d", tries, self.ntries)
|
||||
if api.Backend.rpcclient.isconnected():
|
||||
api.Backend.rpcclient.disconnect()
|
||||
try:
|
||||
api.Backend.rpcclient.connect()
|
||||
# ping to force an actual connection in case there is only one
|
||||
# IPA master
|
||||
api.Command[u'ping']()
|
||||
except (errors.CCacheError,
|
||||
errors.TicketExpired,
|
||||
errors.KerberosError) as e:
|
||||
tries += 1
|
||||
|
||||
# pylint: disable=no-member
|
||||
logging.debug("kinit new ccache in get_connection: %s", e)
|
||||
try:
|
||||
kinit_keytab(str('nova/%s@%s' %
|
||||
(api.env.host, api.env.realm)),
|
||||
self.keytab,
|
||||
self.ccache)
|
||||
except GSSError as e:
|
||||
logging.debug("kinit failed: %s", e)
|
||||
except errors.NetworkError:
|
||||
tries += 1
|
||||
except http_client.ResponseNotReady:
|
||||
# NOTE(xek): This means that the server closed the socket,
|
||||
# so keep-alive ended and we can't use that connection.
|
||||
api.Backend.rpcclient.disconnect()
|
||||
tries += 1
|
||||
else:
|
||||
# successful connection
|
||||
return
|
||||
logging.debug("Waiting %s seconds before next retry.",
|
||||
self.retry_delay)
|
||||
time.sleep(self.retry_delay)
|
||||
|
||||
logging.error(" Failed to connect to IPA after %d attempts",
|
||||
self.ntries)
|
||||
raise Exception("Failed to connect to IPA")
|
||||
|
||||
def start_batch_operation(self):
|
||||
"""Start a batch operation.
|
||||
|
||||
IPA method calls will be collected in a batch job
|
||||
and submitted to IPA once all the operations have collected
|
||||
by a call to _flush_batch_operation().
|
||||
"""
|
||||
logging.debug("start batch operation")
|
||||
self.batch_args = list()
|
||||
|
||||
def _add_batch_operation(self, command, *args, **kw):
|
||||
"""Add an IPA call to the batch operation"""
|
||||
self.batch_args.append({
|
||||
"method": six.text_type(command),
|
||||
"params": [args, kw],
|
||||
})
|
||||
|
||||
def flush_batch_operation(self):
|
||||
"""Make an IPA batch call."""
|
||||
logging.debug("flush_batch_operation")
|
||||
if not self.batch_args:
|
||||
return None
|
||||
|
||||
kw = {}
|
||||
logging.debug(" %s", self.batch_args)
|
||||
|
||||
return self._call_ipa('batch', *self.batch_args, **kw)
|
||||
|
||||
def _call_ipa(self, command, *args, **kw):
|
||||
"""Make an IPA call."""
|
||||
if not api.Backend.rpcclient.isconnected():
|
||||
self.__get_connection()
|
||||
if 'version' not in kw:
|
||||
kw['version'] = u'2.146' # IPA v4.2.0 for compatibility
|
||||
|
||||
while True:
|
||||
try:
|
||||
result = api.Command[command](*args, **kw)
|
||||
logging.debug(result)
|
||||
return result
|
||||
except (errors.CCacheError,
|
||||
errors.TicketExpired,
|
||||
errors.KerberosError):
|
||||
logging.debug("Refresh authentication")
|
||||
self.__get_connection()
|
||||
except errors.NetworkError:
|
||||
raise
|
||||
except http_client.ResponseNotReady:
|
||||
# NOTE(xek): This means that the server closed the socket,
|
||||
# so keep-alive ended and we can't use that connection.
|
||||
api.Backend.rpcclient.disconnect()
|
||||
raise
|
||||
|
||||
def _ipa_client_configured(self):
|
||||
"""Determine if the machine is an enrolled IPA client.
|
||||
|
||||
Return boolean indicating whether this machine is enrolled
|
||||
in IPA. This is a rather weak detection method but better
|
||||
than nothing.
|
||||
"""
|
||||
|
||||
return os.path.exists('/etc/ipa/default.conf')
|
||||
|
||||
def delete_host(self, hostname, batch=True):
|
||||
"""Delete a host from IPA.
|
||||
|
||||
Servers can have multiple network interfaces, and therefore can
|
||||
have multiple aliases. Moreover, they can part of a service using
|
||||
a virtual host (VIP). These aliases are denoted 'subhosts',
|
||||
"""
|
||||
logging.debug("Deleting subhost: %s", hostname)
|
||||
host_params = [hostname]
|
||||
|
||||
(hn, domain) = self.split_hostname(hostname)
|
||||
|
||||
dns_params = [domain, hn]
|
||||
|
||||
# If there is no DNS entry, this operation fails
|
||||
host_kw = {'updatedns': False, }
|
||||
|
||||
dns_kw = {'del_all': True, }
|
||||
|
||||
if batch:
|
||||
self._add_batch_operation('host_del', *host_params, **host_kw)
|
||||
self._add_batch_operation('dnsrecord_del', *dns_params,
|
||||
**dns_kw)
|
||||
else:
|
||||
self._call_ipa('host_del', *host_params, **host_kw)
|
||||
try:
|
||||
self._call_ipa('dnsrecord_del',
|
||||
*dns_params, **dns_kw)
|
||||
except (errors.NotFound, errors.ACIError):
|
||||
# Ignore DNS deletion errors
|
||||
pass
|
||||
|
||||
def host_get_services(self, service_host):
|
||||
"""Return list of services this host manages"""
|
||||
logging.debug("Checking host %s services", service_host)
|
||||
params = []
|
||||
service_args = {'man_by_host': six.text_type(service_host)}
|
||||
result = self._call_ipa('service_find',
|
||||
*params, **service_args)
|
||||
return [service['krbprincipalname'][0] for service in result['result']]
|
||||
|
||||
def service_managed_by_other_hosts(self, service_principal,
|
||||
hosts_to_be_deleted):
|
||||
"""Return True if hosts other than parent manages this service"""
|
||||
|
||||
logging.debug("Checking if principal %s has hosts", service_principal)
|
||||
params = [service_principal]
|
||||
service_args = {}
|
||||
try:
|
||||
result = self._call_ipa('service_show',
|
||||
*params, **service_args)
|
||||
except errors.NotFound:
|
||||
raise KeyError
|
||||
serviceresult = result['result']
|
||||
|
||||
try:
|
||||
(service, hostname, realm) = self.split_principal(
|
||||
service_principal
|
||||
)
|
||||
except errors.MalformedServicePrincipal as e:
|
||||
logging.error("Unable to split principal %s: %s",
|
||||
service_principal, e)
|
||||
raise
|
||||
|
||||
for candidate in serviceresult.get('managedby_host', []):
|
||||
if candidate != hostname:
|
||||
if candidate not in hosts_to_be_deleted:
|
||||
return True
|
||||
return False
|
||||
|
||||
def find_host(self, hostname):
|
||||
"""Return True if this host exists"""
|
||||
logging.debug("Checking if host %s exists", hostname)
|
||||
params = []
|
||||
service_args = {'fqdn': six.text_type(hostname)}
|
||||
result = self._call_ipa('host_find',
|
||||
*params, **service_args)
|
||||
return result['count'] > 0
|
||||
|
||||
|
||||
def cleanup_ipa_services(keytab, principal, hosts):
|
||||
ipa = IPAClient(keytab, principal)
|
||||
|
||||
hosts_to_delete = set()
|
||||
for host in hosts:
|
||||
hostname = host.decode('UTF-8')
|
||||
if ipa.find_host(hostname):
|
||||
hosts_to_delete.add(hostname)
|
||||
|
||||
# get a list of all the services associated with a given hosts
|
||||
principals = set()
|
||||
for host in hosts_to_delete:
|
||||
principals.update(ipa.host_get_services(host))
|
||||
|
||||
# Check the managed_by attribute of each service identified with
|
||||
# the given host. If it is managed by a host other than the
|
||||
# parent or the hosts to be deleted, then it is likely a VIP and it
|
||||
# is not ready to be removed.
|
||||
subhosts_to_delete = set()
|
||||
for principal in principals:
|
||||
(service, subhost, domain) = ipa.split_principal(principal)
|
||||
if ipa.service_managed_by_other_hosts(principal, hosts_to_delete):
|
||||
# this service still has other hosts
|
||||
continue
|
||||
subhosts_to_delete.add(subhost)
|
||||
|
||||
# delete the subhosts. Referential integrity should take care of the
|
||||
# services associated with these hosts.
|
||||
ipa.start_batch_operation()
|
||||
for host in hosts_to_delete:
|
||||
ipa.delete_host(host)
|
||||
for subhost in subhosts_to_delete:
|
||||
ipa.delete_host(subhost)
|
||||
ipa.flush_batch_operation()
|
||||
|
||||
|
||||
def run_module():
|
||||
argument_spec = openstack_full_argument_spec(
|
||||
**yaml.safe_load(DOCUMENTATION)['options']
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec,
|
||||
supports_check_mode=True,
|
||||
**openstack_module_kwargs()
|
||||
)
|
||||
|
||||
try:
|
||||
keytab = module.params.get('keytab')
|
||||
principal = module.params.get('principal')
|
||||
hosts = module.params.get('hosts')
|
||||
|
||||
cleanup_ipa_services(keytab, principal, hosts)
|
||||
|
||||
module.exit_json(changed=True)
|
||||
except Exception as err:
|
||||
module.fail_json(msg=str(err))
|
||||
|
||||
|
||||
def main():
|
||||
run_module()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
37
tripleo_ipa/molecule/deregister/Dockerfile
Normal file
37
tripleo_ipa/molecule/deregister/Dockerfile
Normal file
@ -0,0 +1,37 @@
|
||||
# Molecule managed
|
||||
# Copyright 2019 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
{% if item.registry is defined %}
|
||||
FROM {{ item.registry.url }}/{{ item.image }}
|
||||
{% else %}
|
||||
FROM {{ item.image }}
|
||||
{% endif %}
|
||||
|
||||
RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get install -y python sudo bash ca-certificates && apt-get clean; \
|
||||
elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python*-dnf bash {{ item.pkg_extras | default('') }} && dnf clean all; \
|
||||
elif [ $(command -v yum) ]; then yum makecache fast && yum install -y python sudo yum-plugin-ovl python-setuptools bash {{ item.pkg_extras | default('') }} && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \
|
||||
elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python sudo bash python-xml {{ item.pkg_extras | default('') }} && zypper clean -a; \
|
||||
elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo bash ca-certificates {{ item.pkg_extras | default('') }}; \
|
||||
elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates {{ item.pkg_extras | default('') }} && xbps-remove -O; fi
|
||||
|
||||
{% for pkg in item.easy_install | default([]) %}
|
||||
# install pip for centos where there is no python-pip rpm in default repos
|
||||
RUN easy_install {{ pkg }}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
CMD ["/sbin/init"]
|
163
tripleo_ipa/molecule/deregister/converge.yml
Normal file
163
tripleo_ipa/molecule/deregister/converge.yml
Normal file
@ -0,0 +1,163 @@
|
||||
---
|
||||
# Copyright 2019 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
- name: Setup server
|
||||
hosts: all
|
||||
vars:
|
||||
ipa_domain: example.test
|
||||
ipa_server_ip: 172.18.0.22
|
||||
ipa_server_user: admin
|
||||
ipa_server_password: password123
|
||||
ipa_server_hostname: ipa.example.test
|
||||
undercloud_fqdn: ipa.example.test
|
||||
tasks:
|
||||
- name: copy requirements file
|
||||
copy:
|
||||
src: "{{playbook_dir}}/../../../requirements.txt"
|
||||
dest: /tmp/requirements.txt
|
||||
- name: install requirements
|
||||
pip:
|
||||
requirements: /tmp/requirements.txt
|
||||
- name: install python urllib gssapi
|
||||
pip:
|
||||
name: urllib_gssapi
|
||||
- name: install ipa client
|
||||
package:
|
||||
name: ipa-client
|
||||
state: present
|
||||
|
||||
- name: set resolv.conf to point to the ipa server
|
||||
shell:
|
||||
cmd: cat > /etc/resolv.conf
|
||||
stdin: |
|
||||
search {{ ipa_domain }}
|
||||
nameserver {{ ipa_server_ip }}
|
||||
- name: Set fqdn in /etc/hosts
|
||||
shell:
|
||||
cmd: cat > /etc/hosts
|
||||
- name: Set fqdn in /etc/hosts
|
||||
shell:
|
||||
cmd: cat > /etc/hosts
|
||||
stdin: |
|
||||
127.0.0.1 test-0.example.test test-0 localhost localhost.localdomain
|
||||
|
||||
- name: enroll the server as an ipa client using admin creds
|
||||
shell: |
|
||||
ipa-client-install -U \
|
||||
--server "{{ ipa_server_hostname }}" \
|
||||
--domain "{{ ipa_domain }}" \
|
||||
--realm "{{ ipa_domain | upper }}" \
|
||||
--principal "{{ ipa_server_user }}" \
|
||||
--password "{{ ipa_server_password }}" \
|
||||
--no-ntp --force-join --no-nisdomain
|
||||
args:
|
||||
creates: /etc/ipa/default.conf
|
||||
|
||||
# we need this keytab for operations that we cannot do yet with ansible
|
||||
- name: kinit to get admin creds
|
||||
command: kinit "{{ ipa_server_user }}"
|
||||
args:
|
||||
stdin: "{{ ipa_server_password }}"
|
||||
|
||||
- name: ensure "tripleo-admin" group exists
|
||||
group:
|
||||
name: tripleo-admin
|
||||
state: present
|
||||
|
||||
- name: create users, perms, get keytab
|
||||
include_role:
|
||||
name: tripleo_ipa_setup
|
||||
apply:
|
||||
environment:
|
||||
IPA_USER: "{{ ipa_server_user }}"
|
||||
IPA_HOST: "{{ ipa_server_hostname }}"
|
||||
IPA_PASS: "{{ ipa_server_password }}"
|
||||
|
||||
- name: Converge - add host and relevant services
|
||||
hosts: all
|
||||
vars:
|
||||
tripleo_ipa_enroll_base_server: true
|
||||
tripleo_ipa_base_server_fqdn: test-0.example.test
|
||||
tripleo_ipa_base_server_short_name: test-0
|
||||
tripleo_ipa_base_server_domain: example.test
|
||||
tripleo_ipa_delegate_server: localhost
|
||||
tripleo_ipa_server_metadata: |
|
||||
{
|
||||
"compact_service_HTTP": [
|
||||
"ctlplane",
|
||||
"storage",
|
||||
"storagemgmt",
|
||||
"internalapi",
|
||||
"external"
|
||||
],
|
||||
"compact_service_haproxy": [
|
||||
"ctlplane",
|
||||
"storage",
|
||||
"storagemgmt",
|
||||
"internalapi"
|
||||
],
|
||||
"compact_service_libvirt-vnc": [
|
||||
"internalapi"
|
||||
],
|
||||
"compact_service_mysql": [
|
||||
"internalapi"
|
||||
],
|
||||
"compact_service_neutron_ovn": [
|
||||
"internalapi"
|
||||
],
|
||||
"compact_service_novnc-proxy": [
|
||||
"internalapi"
|
||||
],
|
||||
"compact_service_ovn_controller": [
|
||||
"internalapi"
|
||||
],
|
||||
"compact_service_ovn_dbs": [
|
||||
"internalapi"
|
||||
],
|
||||
"compact_service_rabbitmq": [
|
||||
"internalapi"
|
||||
],
|
||||
"compact_service_redis": [
|
||||
"internalapi"
|
||||
],
|
||||
"managed_service_haproxyctlplane": "haproxy/test-0.ctlplane.example.test",
|
||||
"managed_service_haproxyexternal": "haproxy/test-0.example.test",
|
||||
"managed_service_haproxyinternal_api": "haproxy/test-0.internalapi.example.test",
|
||||
"managed_service_haproxystorage": "haproxy/test-0.storage.example.test",
|
||||
"managed_service_haproxystorage_mgmt": "haproxy/test-0.storagemgmt.example.test",
|
||||
"managed_service_mysqlinternal_api": "mysql/test-0.internalapi.example.test",
|
||||
"managed_service_ovn_dbsinternal_api": "ovn_dbs/test-0.internalapi.example.test",
|
||||
"managed_service_redisinternal_api": "redis/test-0.internalapi.example.test"
|
||||
}
|
||||
roles:
|
||||
- name: tripleo_ipa_registration
|
||||
environment:
|
||||
IPA_USER: admin
|
||||
IPA_HOST: ipa.example.test
|
||||
IPA_PASS: password123
|
||||
|
||||
- name: Converge - delete host and relevant services
|
||||
hosts: all
|
||||
vars:
|
||||
ipa_server_user: nova/ipa.example.test
|
||||
ipa_server_hostname: ipa.example.test
|
||||
tasks:
|
||||
- include_role:
|
||||
name: tripleo_ipa_cleanup
|
||||
vars:
|
||||
tripleo_ipa_hosts_to_delete: [ 'test-0.example.test' ]
|
||||
tripleo_ipa_principal: "{{ ipa_server_user }}"
|
||||
tripleo_ipa_keytab: "/etc/novajoin/krb5.keytab"
|
46
tripleo_ipa/molecule/deregister/molecule.yml
Normal file
46
tripleo_ipa/molecule/deregister/molecule.yml
Normal file
@ -0,0 +1,46 @@
|
||||
---
|
||||
driver:
|
||||
name: docker
|
||||
|
||||
log: true
|
||||
|
||||
platforms:
|
||||
- name: centos7
|
||||
hostname: test-0.example.test
|
||||
image: centos:7
|
||||
security_opts:
|
||||
- seccomp=unconfined
|
||||
command: /sbin/init
|
||||
tmpfs:
|
||||
- /run
|
||||
- /tmp
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:ro
|
||||
dockerfile: Dockerfile
|
||||
network_mode: host
|
||||
easy_install:
|
||||
- pip
|
||||
environment: &env
|
||||
http_proxy: "{{ lookup('env', 'http_proxy') }}"
|
||||
https_proxy: "{{ lookup('env', 'https_proxy') }}"
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
log: true
|
||||
env:
|
||||
ANSIBLE_STDOUT_CALLBACK: yaml
|
||||
ANSIBLE_ROLES_PATH: "${ANSIBLE_ROLES_PATH:-/usr/share/ansible/roles}:${HOME}/zuul-jobs/roles"
|
||||
ANSIBLE_LIBRARY: "${ANSIBLE_LIBRARY:-/usr/share/ansible/plugins/modules}"
|
||||
ANSIBLE_FILTER_PLUGINS: "${ANSIBLE_FILTER_PLUGINS:-/usr/share/ansible/plugins/filter}"
|
||||
|
||||
scenario:
|
||||
test_sequence:
|
||||
- destroy
|
||||
- create
|
||||
- prepare
|
||||
- converge
|
||||
- verify
|
||||
- destroy
|
||||
|
||||
verifier:
|
||||
name: testinfra
|
74
tripleo_ipa/molecule/deregister/prepare.yml
Normal file
74
tripleo_ipa/molecule/deregister/prepare.yml
Normal file
@ -0,0 +1,74 @@
|
||||
---
|
||||
# Copyright 2020 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
- hosts: localhost
|
||||
connection: local
|
||||
tasks:
|
||||
- name: set facts for domains
|
||||
set_fact:
|
||||
domain: example.test
|
||||
ipa_password: password123
|
||||
|
||||
- name: Download FreeIPA Container
|
||||
docker_image:
|
||||
name: freeipa/freeipa-server:fedora-28
|
||||
source: pull
|
||||
|
||||
- name: Make IPA data dir
|
||||
file:
|
||||
path: /tmp/ipa-data
|
||||
state: directory
|
||||
|
||||
- name: Toggle SELinux boolean
|
||||
seboolean:
|
||||
name: container_manage_cgroup
|
||||
state: true
|
||||
persistent: true
|
||||
become: true
|
||||
|
||||
- name: Remove any old IPA container
|
||||
docker_container:
|
||||
name: freeipa-server-container
|
||||
state: absent
|
||||
|
||||
- name: Create network
|
||||
docker_network:
|
||||
name: ipa_network
|
||||
ipam_config:
|
||||
- subnet: 172.18.0.0/16
|
||||
|
||||
- name: Configure FreeIPA
|
||||
shell: >
|
||||
docker run --name freeipa-server-container
|
||||
--sysctl net.ipv6.conf.lo.disable_ipv6=0
|
||||
--security-opt seccomp=unconfined
|
||||
--net ipa_network --ip 172.18.0.22
|
||||
-e IPA_SERVER_IP={{ ansible_default_ipv4.address | default('127.0.0.1') }}
|
||||
-e PASSWORD={{ ipa_password }}
|
||||
-h ipa.{{ domain }}
|
||||
--read-only --tmpfs /run --tmpfs /tmp
|
||||
-v /sys/fs/cgroup:/sys/fs/cgroup:ro
|
||||
-v /tmp/ipa-data:/data:Z freeipa/freeipa-server:fedora-28 exit-on-finished
|
||||
-U -r {{ domain | upper }} --setup-dns --no-reverse --no-ntp
|
||||
--forwarder={{ unbound_primary_nameserver_v4 | default('1.1.1.1') }}
|
||||
--forwarder={{ unbound_secondary_nameserver_v4 | default('8.8.8.8') }} &
|
||||
|
||||
- name: Wait for FreeIPA server install
|
||||
wait_for:
|
||||
path: "/tmp/ipa-data/var/log/ipaserver-install.log"
|
||||
search_regex: "(INFO The ipa-server-install command was successful|ERROR The ipa-server-install command failed)"
|
||||
timeout: 900
|
||||
become: true
|
128
tripleo_ipa/molecule/deregister/tests/test_default.py
Normal file
128
tripleo_ipa/molecule/deregister/tests/test_default.py
Normal file
@ -0,0 +1,128 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import testinfra
|
||||
import testinfra.utils.ansible_runner
|
||||
|
||||
inventory = os.environ['MOLECULE_INVENTORY_FILE']
|
||||
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
|
||||
inventory).get_hosts('all')
|
||||
|
||||
|
||||
def setup_module(module):
|
||||
for host in testinfra_hosts:
|
||||
testinfra.get_host('ansible://' + host,
|
||||
ansible_inventory=inventory
|
||||
).check_output('echo password123 | kinit admin')
|
||||
|
||||
|
||||
def teardown_module(module):
|
||||
for host in testinfra_hosts:
|
||||
testinfra.get_host('ansible://' + host,
|
||||
ansible_inventory=inventory
|
||||
).check_output('kdestroy')
|
||||
|
||||
|
||||
@pytest.mark.parametrize('perm', [
|
||||
{'name': 'Modify host password', 'right': "write",
|
||||
'type': "host", 'attrs': "userpassword"},
|
||||
{'name': 'Write host certificate', 'right': "write",
|
||||
'type': "host", 'attrs': "usercertificate"},
|
||||
{'name': 'Modify host userclass', 'right': "write",
|
||||
'type': "host", 'attrs': "userclass"},
|
||||
{'name': 'Modify service managedBy attribute', 'right': "write",
|
||||
'type': "service", 'attrs': "managedby"},
|
||||
])
|
||||
def test_permissions(host, perm):
|
||||
result = host.check_output('ipa permission-find "{name}"'.format(**perm))
|
||||
assert '1 permission matched' in result
|
||||
assert 'Granted rights: {right}'.format(**perm) in result
|
||||
assert 'Type: {type}'.format(**perm) in result
|
||||
assert 'Effective attributes: {attrs}'.format(**perm) in result
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pri', [
|
||||
'Nova Host Management',
|
||||
])
|
||||
def test_privilages(host, pri):
|
||||
result = host.check_output('ipa privilege-find "{}"'.format(pri))
|
||||
assert '1 privilege matched' in result
|
||||
assert 'Privilege name: {}'.format(pri) in result
|
||||
assert 'Description: {}'.format(pri) in result
|
||||
|
||||
|
||||
def test_privilege_permissions(host):
|
||||
pri = 'Nova Host Management'
|
||||
perms = [
|
||||
'System: add hosts',
|
||||
'System: remove hosts',
|
||||
'Modify host password',
|
||||
'Modify host userclass',
|
||||
'System: Modify hosts',
|
||||
'Modify service managedBy attribute',
|
||||
'System: Add krbPrincipalName to a Host',
|
||||
'System: Add Services',
|
||||
'System: Remove Services',
|
||||
'Revoke certificate',
|
||||
'System: manage host keytab',
|
||||
'System: Manage host certificates',
|
||||
'System: modify services',
|
||||
'System: manage service keytab',
|
||||
'System: read dns entries',
|
||||
'System: remove dns entries',
|
||||
'System: add dns entries',
|
||||
'System: update dns entries',
|
||||
'Retrieve Certificates from the CA',
|
||||
]
|
||||
result = host.check_output('ipa privilege-show "{}"'.format(pri))
|
||||
assert 'Privilege name: {}'.format(pri) in result
|
||||
for perm in perms:
|
||||
assert perm.lower() in result.lower()
|
||||
|
||||
|
||||
def test_role(host):
|
||||
role = 'Nova Host Manager'
|
||||
pri = 'Nova Host Management'
|
||||
result = host.check_output('ipa role-show "{}"'.format(role))
|
||||
assert 'Role name: {}'.format(role) in result
|
||||
assert 'Description: {}'.format(role) in result
|
||||
assert 'Privileges: {}'.format(pri) in result
|
||||
assert 'nova/test-0.example.test@EXAMPLE.TEST' not in result
|
||||
|
||||
|
||||
@pytest.mark.parametrize('name', [
|
||||
'test-0.example.test',
|
||||
'test-0.ctlplane.example.test',
|
||||
'test-0.external.example.test',
|
||||
'test-0.internalapi.example.test',
|
||||
'test-0.storage.example.test',
|
||||
'test-0.storagemgmt.example.test',
|
||||
])
|
||||
def test_hosts(host, name):
|
||||
host.run_expect([1], 'ipa host-find {}'.format(name))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('service, subhost', [
|
||||
('HTTP', 'ctlplane'),
|
||||
('HTTP', 'external'),
|
||||
('HTTP', 'internalapi'),
|
||||
('HTTP', 'storage'),
|
||||
('HTTP', 'storagemgmt'),
|
||||
('haproxy', 'ctlplane'),
|
||||
('haproxy', 'internalapi'),
|
||||
('haproxy', 'storage'),
|
||||
('haproxy', 'storagemgmt'),
|
||||
('libvirt-vnc', 'internalapi'),
|
||||
('mysql', 'internalapi'),
|
||||
('neutron_ovn', 'internalapi'),
|
||||
('novnc-proxy', 'internalapi'),
|
||||
('ovn_controller', 'internalapi'),
|
||||
('ovn_dbs', 'internalapi'),
|
||||
('rabbitmq', 'internalapi'),
|
||||
('redis', 'internalapi'),
|
||||
])
|
||||
def test_services(host, service, subhost):
|
||||
host.run_expect(
|
||||
[2],
|
||||
'ipa service-show {}/test-0.{}.example.test@EXAMPLE.TEST'.format(
|
||||
service, subhost))
|
44
tripleo_ipa/roles/tripleo_ipa_cleanup/meta/main.yml
Normal file
44
tripleo_ipa/roles/tripleo_ipa_cleanup/meta/main.yml
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
# Copyright 2020 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
galaxy_info:
|
||||
author: OpenStack
|
||||
description: TripleO OpenStack Role -- tripleo_ipa_cleanup
|
||||
company: Red Hat
|
||||
license: Apache-2.0
|
||||
min_ansible_version: 2.7
|
||||
#
|
||||
# Provide a list of supported platforms, and for each platform a list of versions.
|
||||
# If you don't wish to enumerate all versions for a particular platform, use 'all'.
|
||||
# To view available platforms and versions (or releases), visit:
|
||||
# https://galaxy.ansible.com/api/v1/platforms/
|
||||
#
|
||||
platforms:
|
||||
- name: Fedora
|
||||
versions:
|
||||
- 28
|
||||
- name: CentOS
|
||||
versions:
|
||||
- 7
|
||||
|
||||
galaxy_tags:
|
||||
- tripleo
|
||||
|
||||
|
||||
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
||||
# if you add dependencies to this list.
|
||||
dependencies: []
|
29
tripleo_ipa/roles/tripleo_ipa_cleanup/tasks/main.yml
Normal file
29
tripleo_ipa/roles/tripleo_ipa_cleanup/tasks/main.yml
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
# Copyright 2020 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# This role removes a set of hosts and its required sub-hosts and services
|
||||
# in FreeIPA
|
||||
#
|
||||
# The following variables are required:
|
||||
# - tripleo_ipa_hosts_to_delete (list of FQDNs of hosts to delete)
|
||||
# - tripleo_ipa_principal (principal to use when connecting to FreeIPA)
|
||||
# - tripleo_ipa_keytab (file path to keytab to authenticate to FreeIPA)
|
||||
|
||||
- name: delete hosts, subhosts and services from freeIPA
|
||||
cleanup_ipa_services:
|
||||
principal: "{{ tripleo_ipa_principal }}"
|
||||
keytab: "{{ tripleo_ipa_keytab }}"
|
||||
hosts: "{{ tripleo_ipa_hosts_to_delete }}"
|
@ -12,5 +12,5 @@
|
||||
- zuul.d/playbooks/run.yml
|
||||
post-run:
|
||||
- zuul.d/playbooks/post.yml
|
||||
timeout: 1800
|
||||
timeout: 3600
|
||||
voting: true
|
||||
|
Loading…
x
Reference in New Issue
Block a user