URL: https://github.com/freeipa/freeipa/pull/2592
Author: wladich
 Title: #2592: [Backport][ipa-4-7] ipatests: add test for ipa-restore in 
multi-master configuration
Action: opened

PR body:
"""
This PR is manual backport of https://github.com/freeipa/freeipa/pull/2588 
please wait for CI before pushing and do not forget about backport to branches 
specified with labels if needed.
In case of questions or problems contact @wladich  who is author of the 
original PR.
"""

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/2592/head:pr2592
git checkout pr2592
From 476a24409484510e9fbe2fb197a40f6e6a1dcbe1 Mon Sep 17 00:00:00 2001
From: Sergey Orlov <sor...@redhat.com>
Date: Wed, 7 Nov 2018 11:23:05 +0100
Subject: [PATCH] ipatests: add test for ipa-restore in multi-master
 configuration

Test ensures that after ipa-restore on the master, the replica can be
re-synchronized and a new replica can be created.

https://pagure.io/freeipa/issue/7455
---
 ipatests/pytest_ipa/integration/tasks.py      |  52 +++--
 .../test_backup_and_restore.py                | 189 +++++++++++++++---
 2 files changed, 195 insertions(+), 46 deletions(-)

diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py
index 7a944c5eee..fe23ebad2a 100644
--- a/ipatests/pytest_ipa/integration/tasks.py
+++ b/ipatests/pytest_ipa/integration/tasks.py
@@ -1104,19 +1104,26 @@ def _entries_to_ldif(entries):
     return io.getvalue()
 
 
-def wait_for_replication(ldap, timeout=30):
-    """Wait until updates on all replication agreements are done (or failed)
+def wait_for_replication(ldap, timeout=30,
+                         target_status_re=r'^0 |^Error \(0\) ',
+                         raise_on_timeout=False):
+    """Wait for all replication agreements to reach desired state
 
+    With defaults waits until updates on all replication agreements are
+    done (or failed) and exits without exception
     :param ldap: LDAP client
         autenticated with necessary rights to read the mapping tree
     :param timeout: Maximum time to wait, in seconds
+    :param target_status_re: Regexp of status to wait for
+    :param raise_on_timeout: if True, raises AssertionError if status not
+        reached in specified time
 
     Note that this waits for updates originating on this host, not those
     coming from other hosts.
     """
     logger.debug('Waiting for replication to finish')
-    for i in range(timeout):
-        time.sleep(1)
+    start = time.time()
+    while True:
         status_attr = 'nsds5replicaLastUpdateStatus'
         progress_attr = 'nsds5replicaUpdateInProgress'
         entries = ldap.get_entries(
@@ -1124,25 +1131,24 @@ def wait_for_replication(ldap, timeout=30):
             filter='(objectclass=nsds5replicationagreement)',
             attrs_list=[status_attr, progress_attr])
         logger.debug('Replication agreements: \n%s', _entries_to_ldif(entries))
-        if any(
-                not (
-                    # older DS format
-                    e.single_value[status_attr].startswith('0 ') or
-                    # newer DS format
-                    e.single_value[status_attr].startswith('Error (0) ')
-                )
-            for e in entries
-        ):
-            logger.error('Replication error')
-            continue
+        statuses = [entry.single_value[status_attr] for entry in entries]
+        wrong_statuses = [s for s in statuses
+                          if not re.match(target_status_re, s)]
         if any(e.single_value[progress_attr] == 'TRUE' for e in entries):
-            logger.debug('Replication in progress (waited %s/%ss)',
-                         i, timeout)
+            msg = 'Replication not finished'
+            logger.debug(msg)
+        elif wrong_statuses:
+            msg = 'Unexpected replication status: %s' % wrong_statuses[0]
+            logger.debug(msg)
         else:
             logger.debug('Replication finished')
+            return
+        if time.time() - start > timeout:
+            logger.error('Giving up wait for replication to finish')
+            if raise_on_timeout:
+                raise AssertionError(msg)
             break
-    else:
-        logger.error('Giving up wait for replication to finish')
+        time.sleep(1)
 
 
 def wait_for_cleanallruv_tasks(ldap, timeout=30):
@@ -1538,3 +1544,11 @@ def strip_cert_header(pem):
         return s.group(1)
     else:
         return pem
+
+
+def user_add(host, login):
+    host.run_command([
+        "ipa", "user-add", login,
+        "--first", "test",
+        "--last", "user"
+    ])
diff --git a/ipatests/test_integration/test_backup_and_restore.py b/ipatests/test_integration/test_backup_and_restore.py
index 063f1d0e67..71d692dde7 100644
--- a/ipatests/test_integration/test_backup_and_restore.py
+++ b/ipatests/test_integration/test_backup_and_restore.py
@@ -24,6 +24,7 @@
 import re
 import contextlib
 from tempfile import NamedTemporaryFile
+import pytest
 
 from ipaplatform.constants import constants
 from ipaplatform.paths import paths
@@ -137,6 +138,8 @@ def restore_checker(host):
 
     yield
 
+    tasks.kinit_admin(host)
+
     for (check, assert_func), expected in zip(CHECKS, results):
         logger.info('Checking result for %s', check.__name__)
         got = check(host)
@@ -164,6 +167,24 @@ def backup(host):
         raise AssertionError('Backup directory not found in output')
 
 
+@pytest.yield_fixture(scope="function")
+def cert_sign_request(request):
+    master = request.instance.master
+    hosts = [master] + request.instance.replicas
+    csrs = {}
+    for host in hosts:
+        request_path = host.run_command(['mktemp']).stdout_text.strip()
+        openssl_command = [
+            'openssl', 'req', '-new', '-nodes', '-out', request_path,
+            '-subj', '/CN=' + master.hostname
+        ]
+        host.run_command(openssl_command)
+        csrs[host.hostname] = request_path
+    yield csrs
+    for host in hosts:
+        host.run_command(['rm', csrs[host.hostname]])
+
+
 class TestBackupAndRestore(IntegrationTest):
     topology = 'star'
 
@@ -446,36 +467,92 @@ def test_full_backup_reinstall_restore_with_vault(self):
 
 
 class TestBackupAndRestoreWithReplica(IntegrationTest):
-    """Regression test for https://pagure.io/freeipa/issue/7234""";
-    num_replicas = 1
+    """Regression tests for issues 7234 and 7455
+
+    https://pagure.io/freeipa/issue/7234
+        - check that oddjobd service is started after restore
+        - check new replica  setup after restore
+    https://pagure.io/freeipa/issue/7455
+        check that after restore and replication reinitialization
+            - users and CA data are at state before backup
+            - CA can be installed on existing replica
+            - new replica with CA can be setup
+    """
+    num_replicas = 2
     topology = "star"
 
     @classmethod
     def install(cls, mh):
+        cls.replica1 = cls.replicas[0]
+        cls.replica2 = cls.replicas[1]
         if cls.domain_level is None:
             domain_level = cls.master.config.domain_level
         else:
             domain_level = cls.domain_level
-
-        if cls.topology is None:
-            return
-        else:
-            tasks.install_topo(
-                cls.topology, cls.master, [],
-                cls.clients, domain_level
-            )
-
-    def test_full_backup_and_restore_with_replica(self):
-        replica = self.replicas[0]
+        # Configure only master and one replica.
+        # Replica is configured without CA
+        tasks.install_topo(
+            cls.topology, cls.master, [cls.replica1],
+            cls.clients, domain_level,
+            setup_replica_cas=False
+        )
+
+    def get_users(self, host):
+        res = host.run_command(['ipa', 'user-find'])
+        users = set()
+        for line in res.stdout_text.splitlines():
+            k, _unused, v = line.strip().partition(': ')
+            if k == 'User login':
+                users.add(v)
+        return users
+
+    def check_replication_error(self, host):
+        status = r'Error \(19\) Replication error acquiring replica: ' \
+                 'Replica has different database generation ID'
+        tasks.wait_for_replication(
+            host.ldap_connect(), target_status_re=status,
+            raise_on_timeout=True)
+
+    def check_replication_success(self, host):
+        status = r'Error \(0\) Replica acquired successfully: ' \
+                 'Incremental update succeeded'
+        tasks.wait_for_replication(
+            host.ldap_connect(), target_status_re=status,
+            raise_on_timeout=True)
+
+    def request_test_service_cert(self, host, request_path,
+                                  expect_connection_error=False):
+        res = host.run_command([
+            'ipa', 'cert-request', '--principal=TEST/' + self.master.hostname,
+            request_path
+        ], raiseonerr=not expect_connection_error)
+        if expect_connection_error:
+            assert (1 == res.returncode and
+                    '[Errno 111] Connection refused' in res.stderr_text)
+
+    def test_full_backup_and_restore_with_replica(self, cert_sign_request):
+        # check prerequisites
+        self.check_replication_success(self.master)
+        self.check_replication_success(self.replica1)
+
+        self.master.run_command(
+            ['ipa', 'service-add', 'TEST/' + self.master.hostname])
+
+        tasks.user_add(self.master, 'test1_master')
+        tasks.user_add(self.replica1, 'test1_replica')
 
         with restore_checker(self.master):
             backup_path = backup(self.master)
 
-            logger.info("Backup path for %s is %s", self.master, backup_path)
+            # change data after backup
+            self.master.run_command(['ipa', 'user-del', 'test1_master'])
+            self.replica1.run_command(['ipa', 'user-del', 'test1_replica'])
+            tasks.user_add(self.master, 'test2_master')
+            tasks.user_add(self.replica1, 'test2_replica')
 
-            self.master.run_command([
-                "ipa-server-install", "--uninstall", "-U"
-            ])
+            # simulate master crash
+            self.master.run_command(['ipactl', 'stop'])
+            tasks.uninstall_master(self.master, clean=False)
 
             logger.info("Stopping and disabling oddjobd service")
             self.master.run_command([
@@ -485,18 +562,76 @@ def test_full_backup_and_restore_with_replica(self):
                 "systemctl", "disable", "oddjobd"
             ])
 
-            self.master.run_command(
-                ["ipa-restore", backup_path],
-                stdin_text='yes'
-            )
+            self.master.run_command(['ipa-restore', '-U', backup_path])
 
-            status = self.master.run_command([
-                "systemctl", "status", "oddjobd"
-            ])
-            assert "active (running)" in status.stdout_text
+        status = self.master.run_command([
+            "systemctl", "status", "oddjobd"
+        ])
+        assert "active (running)" in status.stdout_text
+
+        # replication should not work after restoration
+        # create users to force master and replica to try to replicate
+        tasks.user_add(self.master, 'test3_master')
+        tasks.user_add(self.replica1, 'test3_replica')
+        self.check_replication_error(self.master)
+        self.check_replication_error(self.replica1)
+        assert {'admin', 'test1_master', 'test1_replica', 'test3_master'} == \
+            self.get_users(self.master)
+        assert {'admin', 'test2_master', 'test2_replica', 'test3_replica'} == \
+            self.get_users(self.replica1)
+
+        # reestablish and check replication
+        self.replica1.run_command(['ipa-replica-manage', 're-initialize',
+                                  '--from', self.master.hostname])
+        # create users to force master and replica to try to replicate
+        tasks.user_add(self.master, 'test4_master')
+        tasks.user_add(self.replica1, 'test4_replica')
+        self.check_replication_success(self.master)
+        self.check_replication_success(self.replica1)
+        assert {'admin', 'test1_master', 'test1_replica',
+                'test3_master', 'test4_master', 'test4_replica'} == \
+            self.get_users(self.master)
+        assert {'admin', 'test1_master', 'test1_replica',
+                'test3_master', 'test4_master', 'test4_replica'} == \
+            self.get_users(self.replica1)
+
+        # CA on master should be accesible from master and replica
+        self.request_test_service_cert(
+            self.master, cert_sign_request[self.master.hostname])
+        self.request_test_service_cert(
+            self.replica1, cert_sign_request[self.replica1.hostname])
+
+        # replica should not be able to sign certificates without CA on master
+        self.master.run_command(['ipactl', 'stop'])
+        try:
+            self.request_test_service_cert(
+                self.replica1, cert_sign_request[self.replica1.hostname],
+                expect_connection_error=True)
+        finally:
+            self.master.run_command(['ipactl', 'start'])
 
-        tasks.install_replica(self.master, replica)
-        check_replication(self.master, replica, "testuser1")
+        tasks.install_ca(self.replica1)
+
+        # now replica should be able to sign certificates without CA on master
+        self.master.run_command(['ipactl', 'stop'])
+        self.request_test_service_cert(
+            self.replica1, cert_sign_request[self.replica1.hostname])
+        self.master.run_command(['ipactl', 'start'])
+
+        # check installation of new replica
+        tasks.install_replica(self.master, self.replica2, setup_ca=True)
+        check_replication(self.master, self.replica2, "testuser")
+
+        # new replica should be able to sign certificates without CA on master
+        # and old replica
+        self.master.run_command(['ipactl', 'stop'])
+        self.replica1.run_command(['ipactl', 'stop'])
+        try:
+            self.request_test_service_cert(
+                self.replica2, cert_sign_request[self.replica2.hostname])
+        finally:
+            self.replica1.run_command(['ipactl', 'start'])
+            self.master.run_command(['ipactl', 'start'])
 
 
 class TestUserRootFilesOwnershipPermission(IntegrationTest):
_______________________________________________
FreeIPA-devel mailing list -- freeipa-devel@lists.fedorahosted.org
To unsubscribe send an email to freeipa-devel-le...@lists.fedorahosted.org
Fedora Code of Conduct: https://getfedora.org/code-of-conduct.html
List Guidelines: https://fedoraproject.org/wiki/Mailing_list_guidelines
List Archives: 
https://lists.fedorahosted.org/archives/list/freeipa-devel@lists.fedorahosted.org

Reply via email to