diff --git a/bindep.txt b/bindep.txt new file mode 100644 index 0000000..8e60cc8 --- /dev/null +++ b/bindep.txt @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1f16c01 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +six>=1.10.0 # MIT +PyYAML>=3.12 # MIT diff --git a/tox.ini b/tox.ini index 96fe8b6..920b311 100644 --- a/tox.ini +++ b/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 diff --git a/tripleo_ipa/ansible_plugins/modules/cleanup_ipa_services.py b/tripleo_ipa/ansible_plugins/modules/cleanup_ipa_services.py new file mode 100644 index 0000000..576abea --- /dev/null +++ b/tripleo_ipa/ansible_plugins/modules/cleanup_ipa_services.py @@ -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() diff --git a/tripleo_ipa/molecule/deregister/Dockerfile b/tripleo_ipa/molecule/deregister/Dockerfile new file mode 100644 index 0000000..b91d113 --- /dev/null +++ b/tripleo_ipa/molecule/deregister/Dockerfile @@ -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"] diff --git a/tripleo_ipa/molecule/deregister/converge.yml b/tripleo_ipa/molecule/deregister/converge.yml new file mode 100644 index 0000000..64033f4 --- /dev/null +++ b/tripleo_ipa/molecule/deregister/converge.yml @@ -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" diff --git a/tripleo_ipa/molecule/deregister/molecule.yml b/tripleo_ipa/molecule/deregister/molecule.yml new file mode 100644 index 0000000..a77ae8f --- /dev/null +++ b/tripleo_ipa/molecule/deregister/molecule.yml @@ -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 diff --git a/tripleo_ipa/molecule/deregister/prepare.yml b/tripleo_ipa/molecule/deregister/prepare.yml new file mode 100644 index 0000000..8cac5b3 --- /dev/null +++ b/tripleo_ipa/molecule/deregister/prepare.yml @@ -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 diff --git a/tripleo_ipa/molecule/deregister/tests/test_default.py b/tripleo_ipa/molecule/deregister/tests/test_default.py new file mode 100644 index 0000000..a599e11 --- /dev/null +++ b/tripleo_ipa/molecule/deregister/tests/test_default.py @@ -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)) diff --git a/tripleo_ipa/roles/tripleo_ipa_cleanup/meta/main.yml b/tripleo_ipa/roles/tripleo_ipa_cleanup/meta/main.yml new file mode 100644 index 0000000..5255118 --- /dev/null +++ b/tripleo_ipa/roles/tripleo_ipa_cleanup/meta/main.yml @@ -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: [] diff --git a/tripleo_ipa/roles/tripleo_ipa_cleanup/tasks/main.yml b/tripleo_ipa/roles/tripleo_ipa_cleanup/tasks/main.yml new file mode 100644 index 0000000..2b1b6e8 --- /dev/null +++ b/tripleo_ipa/roles/tripleo_ipa_cleanup/tasks/main.yml @@ -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 }}" diff --git a/zuul.d/base.yaml b/zuul.d/base.yaml index 7c9826b..f4da58f 100644 --- a/zuul.d/base.yaml +++ b/zuul.d/base.yaml @@ -12,5 +12,5 @@ - zuul.d/playbooks/run.yml post-run: - zuul.d/playbooks/post.yml - timeout: 1800 + timeout: 3600 voting: true