On 09/17/2013 10:43 AM, Petr Viktorin wrote:
On 09/16/2013 03:45 PM, Tomas Babej wrote:
Hi,

this set of patches extends ipatests module to support integration
testing with Active Directory,
as well as provides an basic (working without artificial sleeps!) trust
test case.

Thanks, this is great news!
I haven't testes the test yet. Here are my comments from reading them.


Patch 100:
Please document the new configuration options. There are two places:
- ipa-test-config man page
- http://www.freeipa.org/page/V3/Integration_testing#Test_configuration (or if you end up writing a separate design page for AD tests, link to it)


I added the options to the the man page.


Patch 101:
The `if role == 'ad':` block should rather be in the `Host.from_env` factory.


Moved.


Patch 102:
Thanks for cleaning that up!
You could go one step further with
cls.replicas = get_resources(domain.replicas, 'replicas', cls.num_replicas)
and `return container[:num_needed]` there

You're welcome, refactoring part expanded.



Patch 103:
This patch should come before 101 which uses it.


We can push this in the correct order if this is an issue, right? Patches are independent (meaning they do not touch the same source code, so they should apply cleanly).

Ideally there would be a BaseHost with common functionality, and concrete Host and WinHost subclassing it.

Yes, I agree, that's what we discussed.

I'll be making changes here for #3890; please concentrate on other parts for now to avoid conflicts. I'll take Windows hosts into consideration.

Raise instances rather than the exception classes: raise NotImplementedError()
Fixed.



Patch 104:
Instead of stdout_re, allow the user to pass in a checking function.
For example, `lambda result: re.search(sid_regex, result.stdout_text)`
This makes wait_assert complete, you won't need to add other cases to it when you need to check stderr or evaluate some condition a regex is too weak for.
+1, improved.


Pass `raiseonerr` to run_command directly, not by setting it in kwargs. That way wait_assert will fail if it gets raiseonerr. Also, run_command's arguments except `command` are supposed to be keyword-only. (This is only not enforced because that's awkward to do in Python 2.) Don't accept or pass along *args.

Agreed, fixed.

Use parentheses instead of \ for line continuation (see PEP8).

I'd prefer to keep the function in test_trust.py until we see there's a wider scope for it. And rename it to run_repeatedly or some other name that describes it better.


I renamed the function.

Why do you think there's not scope for it? I'd rather keep it in tasks, since it addresses a general use case of waiting in a loop until a command returns expected output.

This can happen with any command that starts asynchronous actions and we want to make sure that the changes are propagated before continuing (such as DNS updates via our CLI).

I wouldn't consider this being specific for AD trust testing.



Patch 105:
Please add these tasks to ipa-test-task (and its man page) as well.
The instructions in the docstring in configure_dns_for_trust should go to a test design document. I think just starting a section in Integration_testing would be fine if you mark it as not implemented yet (and link to the ticket).

Use parentheses instead of \ for line continuation (see PEP8).

I don't really like wrapping arbitrary expressions in parentheses, particularly when assigning the result to a variable:

result = (something or
              something_completely_else)

To me, the following looks better:

result = something or \
             something completely else

I checked with PEP8. I remember there was a section that backslash can be used in cases it produces nicer results, but it's gone now.
Backslash is still recommended in some cases.

Still (but no strong opinions here), I would not consider this a violation unless it happens between brackets (in which case it is redundant).


Patch 106:
The instructions in the TestADTrust docstring should go to a test design document.

Please use the SID regex from a shared location. You'll need to assign it to a variable and make the Fuzzy from that, so that variable can be imported and used with the standard re module.


This is something that I tried to address with the Fuzzy refactoring. This is a temporary solution, once we resolve that patchset, of course, the test will use the shared location.
I added a comment there, I'd rather not create conflicts.

Avoid commented-out code in patches for review.
No need to import fuzzy.

Fixed.


test_user_gid_uid_resolution_in_nonposix_trust:
- For a one-off regex, compile() is unnecessary:
    assert re.search(testuser_regex, result.stdout_text)
- Whenever substituting a literal string into a regex, please use re.escape().

Fixed.

Use parentheses instead of \ for line continuation (see PEP8).



Design will follow soon.

--
Tomas Babej
Associate Software Engeneer | Red Hat | Identity Management
RHCE | Brno Site | IRC: tbabej | freeipa.org

From b649c96223252265363196094fa267150173628a Mon Sep 17 00:00:00 2001
From: Tomas Babej <tba...@redhat.com>
Date: Wed, 4 Sep 2013 14:12:28 +0200
Subject: [PATCH 100/106] ipatests: Add Active Directory support to
 configuration

---
 ipatests/man/ipa-test-config.1      | 13 +++++++++++++
 ipatests/test_integration/config.py | 21 +++++++++++++++++++++
 2 files changed, 34 insertions(+)

diff --git a/ipatests/man/ipa-test-config.1 b/ipatests/man/ipa-test-config.1
index 4b998adb4f65265f4b0cfc49cac2979740562db5..786380791eb52a25aa6d36a5d1793afcd948d3eb 100644
--- a/ipatests/man/ipa-test-config.1
+++ b/ipatests/man/ipa-test-config.1
@@ -84,6 +84,11 @@ Host configuration:
 \fB$MASTER_env2\fR, \fB$REPLICA_env2\fR, \fB$CLIENT_env2\fR, \fB$MASTER_env3\fR, ...
     can be used for additional domains when needed
 .TP
+\fB$AD_env1\fR, \fB$AD_env2\fR, \fB$AD_env3\fR, \fB$AD_env4\fR, ...
+    can be used to define Active Directory domains. Please note that these
+    domains are separate from the IPA domains in that sense that AD_env1 does
+    not have to correspond only to the MASTER_env1.
+.TP
 \fB$BEAKER\fR<role><num>\fB_IP_env\fR<e>, e.g. \fB$BEAKERREPLICA1_IP_env1\fR
     the IP address of the given host
     Default: resolved via gethostbyname (or DNS if $IPv6SETUP is set)
@@ -139,6 +144,14 @@ Test customization:
     Admin user password
     Default: Secret123
 .TP
+\fB$ADADMINID\fR
+    Active Directory Administrator username
+    Default: Administrator
+.TP
+\fB$ADADMINPW\fR
+    Active Directory Administrator password
+    Default: Secret123
+.TP
 \fB$ROOTDN\fR
     Directory manager DN
     Default: cn=Directory Manager
diff --git a/ipatests/test_integration/config.py b/ipatests/test_integration/config.py
index d43812c514bf1c9d23740e6f75cc3574235e86d3..ac0131e435d33b6730093cbc64a1b47910d3971e 100644
--- a/ipatests/test_integration/config.py
+++ b/ipatests/test_integration/config.py
@@ -1,5 +1,6 @@
 # Authors:
 #   Petr Viktorin <pvikt...@redhat.com>
+#   Tomas Babej <tba...@redhat.com>
 #
 # Copyright (C) 2013  Red Hat
 # see file 'COPYING' for use and warranty information
@@ -50,11 +51,14 @@ class Config(object):
         self.nis_domain = kwargs.get('nis_domain') or 'ipatest'
         self.ntp_server = kwargs.get('ntp_server') or (
             '%s.pool.ntp.org' % random.randint(0, 3))
+        self.ad_admin_name = kwargs.get('ad_admin_name') or 'Administrator'
+        self.ad_admin_password = kwargs.get('ad_admin_password') or 'Secret123'
 
         if not self.root_password and not self.root_ssh_key_filename:
             self.root_ssh_key_filename = '~/.ssh/id_rsa'
 
         self.domains = []
+        self.ad_domains = []
 
     @classmethod
     def from_env(cls, env):
@@ -76,6 +80,8 @@ class Config(object):
         ADMINPW: Administrator password
         ROOTDN: Directory Manager DN
         ROOTDNPWD: Directory Manager password
+        ADADMINID: Active Directory Administrator username
+        ADADMINPW: Active Directory Administrator password
         DNSFORWARD: DNS forwarder
         NISDOMAIN
         NTPSERVER
@@ -83,6 +89,7 @@ class Config(object):
         MASTER_env1: FQDN of the master
         REPLICA_env1: space-separated FQDNs of the replicas
         CLIENT_env1: space-separated FQDNs of the clients
+        AD_env1: space-separated FQDNs of the Active Directories
         OTHER_env1: space-separated FQDNs of other hosts
         (same for _env2, _env3, etc)
         BEAKERREPLICA1_IP_env1: IP address of replica 1 in env 1
@@ -104,13 +111,23 @@ class Config(object):
                    dns_forwarder=env.get('DNSFORWARD'),
                    nis_domain=env.get('NISDOMAIN'),
                    ntp_server=env.get('NTPSERVER'),
+                   ad_admin_name=env.get('ADADMINID'),
+                   ad_admin_password=env.get('ADADMINPW'),
                    )
 
+        # Either IPA master or AD can define a domain
+
         domain_index = 1
         while env.get('MASTER_env%s' % domain_index):
             self.domains.append(Domain.from_env(env, self, domain_index))
             domain_index += 1
 
+        domain_index = 1
+        while env.get('AD_env%s' % domain_index):
+            self.ad_domains.append(Domain.from_env(env, self, domain_index,
+                                                   ad_domain=True))
+            domain_index += 1
+
         return self
 
     def to_env(self, simple=True):
@@ -133,6 +150,9 @@ class Config(object):
         env['ROOTDN'] = str(self.dirman_dn)
         env['ROOTDNPWD'] = self.dirman_password
 
+        env['ADADMINID'] = self.ad_admin_name
+        env['ADADMINPW'] = self.ad_admin_password
+
         env['DNSFORWARD'] = self.dns_forwarder
         env['NISDOMAIN'] = self.nis_domain
         env['NTPSERVER'] = self.ntp_server
@@ -145,6 +165,7 @@ class Config(object):
             for role, hosts in [('MASTER', domain.masters),
                                 ('REPLICA', domain.replicas),
                                 ('CLIENT', domain.clients),
+                                ('AD', domain.ads),
                                 ('OTHER', domain.other_hosts)]:
                 hostnames = ' '.join(h.hostname for h in hosts)
                 env['%s%s' % (role, domain._env)] = hostnames
-- 
1.8.3.1

From d8956603deeb0eedfd2261b99563a2306d88ec60 Mon Sep 17 00:00:00 2001
From: Tomas Babej <tba...@redhat.com>
Date: Wed, 4 Sep 2013 14:24:41 +0200
Subject: [PATCH 101/106] ipatests: Extend domain object with 'ad' role support
 and WinHosts

---
 ipatests/test_integration/config.py | 34 ++++++++++++++++------------------
 1 file changed, 16 insertions(+), 18 deletions(-)

diff --git a/ipatests/test_integration/config.py b/ipatests/test_integration/config.py
index ac0131e435d33b6730093cbc64a1b47910d3971e..94aae0335c4db22bd88e6c9394d9ec8f6432035a 100644
--- a/ipatests/test_integration/config.py
+++ b/ipatests/test_integration/config.py
@@ -27,7 +27,7 @@ import random
 from ipapython import ipautil
 from ipapython.dn import DN
 from ipapython.ipa_log_manager import log_mgr
-from ipatests.test_integration.host import Host
+from ipatests.test_integration.host import Host, WinHost
 
 
 class Config(object):
@@ -248,7 +248,7 @@ def env_normalize(env):
 
 
 class Domain(object):
-    """Configuration for an IPA domain"""
+    """Configuration for an IPA / AD domain"""
     def __init__(self, config, name, index):
         self.log = log_mgr.get_logger(self)
 
@@ -263,27 +263,21 @@ class Domain(object):
         self.basedn = DN(*(('dc', p) for p in name.split('.')))
 
     @classmethod
-    def from_env(cls, env, config, index):
-        try:
-            default_domain = env['DOMAIN']
-        except KeyError:
-            hostname, dot, default_domain = env['MASTER_env1'].partition('.')
-        parts = default_domain.split('.')
+    def from_env(cls, env, config, index, ad_domain=False):
 
-        if index == 1:
-            name = default_domain
-        else:
-            # For $DOMAIN = dom.example.com, additional domains are
-            # dom1.example.com, dom2.example.com, etc.
-            parts[0] += str(index)
-            name = '.'.join(parts)
+        master_role = 'AD' if ad_domain else 'MASTER'
+        master_env = '%s_env%s' % (master_role, index)
 
-        self = cls(config, name, index)
+        hostname, dot, domain_name = env[master_env].partition('.')
 
-        for role in 'master', 'replica', 'client', 'other':
+        self = cls(config, domain_name, index)
+
+        for role in 'master', 'replica', 'client', 'ad', 'other':
             value = env.get('%s%s' % (role.upper(), self._env), '')
             for index, hostname in enumerate(value.split(), start=1):
+
                 host = Host.from_env(env, self, hostname, role, index)
+
                 self.hosts.append(host)
 
         if not self.hosts:
@@ -318,9 +312,13 @@ class Domain(object):
         return [h for h in self.hosts if h.role == 'client']
 
     @property
+    def ads(self):
+        return [h for h in self.hosts if h.role == 'ad']
+
+    @property
     def other_hosts(self):
         return [h for h in self.hosts
-                if h.role not in ('master', 'client', 'replica')]
+                if h.role not in ('master', 'client', 'replica', 'ad')]
 
     def host_by_name(self, name):
         for host in self.hosts:
-- 
1.8.3.1

From 1e52559e9ee1714215b1e33872590d2dbd7addc3 Mon Sep 17 00:00:00 2001
From: Tomas Babej <tba...@redhat.com>
Date: Wed, 4 Sep 2013 15:02:21 +0200
Subject: [PATCH 102/106] ipatests: Extend IntegrationTest with multiple AD
 domain support

---
 ipatests/test_integration/base.py | 28 ++++++++++++++++++----------
 1 file changed, 18 insertions(+), 10 deletions(-)

diff --git a/ipatests/test_integration/base.py b/ipatests/test_integration/base.py
index 43360a83a4d2df656051beada7b69556066c2801..1bed7d55b0e89f88507165807e01ca7e185f2c80 100644
--- a/ipatests/test_integration/base.py
+++ b/ipatests/test_integration/base.py
@@ -35,10 +35,19 @@ log = log_mgr.get_logger(__name__)
 class IntegrationTest(object):
     num_replicas = 0
     num_clients = 0
+    num_ad_domains = 0
     topology = None
 
     @classmethod
     def setup_class(cls):
+
+        def get_resources(resource_container, resource_str, num_needed):
+            if len(resource_container) < num_needed:
+                raise nose.SkipTest(
+                    'Not enough %s available (have %s, need %s)' %
+                    (resource_str, len(resource_container), num_needed))
+            return resource_container[:num_needed]
+
         config = get_global_config()
         if not config.domains:
             raise nose.SkipTest('Integration testing not configured')
@@ -46,17 +55,15 @@ class IntegrationTest(object):
         cls.logs_to_collect = {}
 
         domain = config.domains[0]
+
         cls.master = domain.master
-        if len(domain.replicas) < cls.num_replicas:
-            raise nose.SkipTest(
-                'Not enough replicas available (have %s, need %s)' %
-                (len(domain.replicas), cls.num_replicas))
-        if len(domain.clients) < cls.num_clients:
-            raise nose.SkipTest(
-                'Not enough clients available (have %s, need %s)' %
-                (len(domain.clients), cls.num_clients))
-        cls.replicas = domain.replicas[:cls.num_replicas]
-        cls.clients = domain.clients[:cls.num_clients]
+        cls.replicas = get_resources(domain.replicas, 'replicas',
+                                     cls.num_replicas)
+        cls.clients = get_resources(domain.clients, 'clients',
+                                    cls.num_clients)
+        cls.ad_domains = get_resources(config.ad_domains, 'AD domains',
+                                       cls.num_ad_domains)
+
         for host in cls.get_all_hosts():
             host.add_log_collector(cls.collect_log)
             cls.prepare_host(host)
@@ -95,6 +102,7 @@ class IntegrationTest(object):
             del cls.master
             del cls.replicas
             del cls.clients
+            del cls.ad_domains
 
     @classmethod
     def uninstall(cls):
-- 
1.8.3.1

From 51a47d8427d89bee34ce1758951b2718d9de66cd Mon Sep 17 00:00:00 2001
From: Tomas Babej <tba...@redhat.com>
Date: Wed, 4 Sep 2013 16:26:23 +0200
Subject: [PATCH 103/106] ipatests: Add WinHost class

---
 ipatests/test_integration/host.py | 58 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 58 insertions(+)

diff --git a/ipatests/test_integration/host.py b/ipatests/test_integration/host.py
index 1614b5fd3e95052e868a42a35dd75ce78c7a9079..6e6a69a259a4b3b113d2a9a4218b4670ceba3126 100644
--- a/ipatests/test_integration/host.py
+++ b/ipatests/test_integration/host.py
@@ -146,6 +146,8 @@ class Host(object):
         self.hostname = shortname + '.' + self.domain.name
         self.external_hostname = hostname
 
+        self.netbios = self.domain.name.split('.')[0].upper()
+
         self.logger_name = '%s.%s.%s' % (
             self.__module__, type(self).__name__, shortname)
         self.log = log_mgr.get_logger(self.logger_name)
@@ -200,6 +202,10 @@ class Host(object):
     def from_env(cls, env, domain, hostname, role, index):
         ip = env.get('BEAKER%s%s_IP_env%s' %
                         (role.upper(), index, domain.index), None)
+
+        if role == 'ad':
+            cls = WinHost
+
         self = cls(domain, hostname, role, index, ip)
         return self
 
@@ -362,3 +368,55 @@ class Host(object):
     def collect_log(self, filename):
         for collector in self.log_collectors:
             collector(self, filename)
+
+
+class WinHost(Host):
+    """
+    Representation of a remote Windows host.
+
+    This serves as a sketch class once we move from manual preparation of
+    Active Directory to the automated setup.
+    """
+
+    def run_command(self, argv, set_env=True, stdin_text=None,
+                    log_stdout=True, raiseonerr=True,
+                    cwd=None):
+        raise NotImplementedError()
+
+    @property
+    def transport(self):
+        raise NotImplementedError()
+
+    @property
+    def sftp(self):
+        raise NotImplementedError()
+
+    def ldap_connect(self):
+        """Return an LDAPClient authenticated to this host as directory manager
+        """
+        raise NotImplementedError()
+
+    def mkdir_recursive(self, path):
+        """`mkdir -p` on the remote host"""
+        raise NotImplementedError()
+
+    def get_file_contents(self, filename):
+        """Read the named remote file and return the contents as a string"""
+        raise NotImplementedError()
+
+    def put_file_contents(self, filename, contents):
+        """Write the given string to the named remote file"""
+        raise NotImplementedError()
+
+    def file_exists(self, filename):
+        """Return true if the named remote file exists"""
+        raise NotImplementedError()
+
+    def get_file(self, remotepath, localpath):
+        raise NotImplementedError()
+
+    def put_file(self, localpath, remotepath):
+        raise NotImplementedError()
+
+    def collect_log(self, filename):
+        raise NotImplementedError()
-- 
1.8.3.1

From 581854e39b0ad09a69d3674c227b56266d7b1d18 Mon Sep 17 00:00:00 2001
From: Tomas Babej <tba...@redhat.com>
Date: Thu, 12 Sep 2013 12:08:15 +0200
Subject: [PATCH 104/106] ipatests: Add run_repeatedly method to tasks suite

---
 ipatests/test_integration/tasks.py | 35 +++++++++++++++++++++++++++++++++++
 1 file changed, 35 insertions(+)

diff --git a/ipatests/test_integration/tasks.py b/ipatests/test_integration/tasks.py
index 69a34a2871204405ade745b12e1afe7e80f98a29..d613e6490b8a9c57d1e9af7d3b11a8142a428a57 100644
--- a/ipatests/test_integration/tasks.py
+++ b/ipatests/test_integration/tasks.py
@@ -429,3 +429,38 @@ def wait_for_replication(ldap, timeout=30):
             break
     else:
         log.error('Giving up wait for replication to finish')
+
+
+def run_repeatedly(host, command, assert_zero_rc=True, test=None,
+                timeout=30, **kwargs):
+    """
+    Runs command on host repeatedly until it's finished successfully (returns
+    0 exit code and its stdout passes the test function).
+
+    Returns True if the command was executed succesfully, False otherwise.
+
+    This method accepts additional kwargs and passes these arguments
+    to the actual run_command method.
+    """
+
+    time_waited = 0
+
+    # Check that the test is a function
+    if test:
+        assert callable(test)
+
+    while(time_waited <= timeout):
+        result = host.run_command(command, raiseonerr=False, **kwargs)
+
+        return_code_ok = not assert_zero_rc or (result.returncode == 0)
+        test_ok = not test or test(result.stdout_text)
+
+        if return_code_ok and test_ok:
+            # Command successful
+            return True
+        else:
+            # Command not successful
+            time.sleep(2)
+            time_waited += 2
+
+    return False
-- 
1.8.3.1

From ebfa68e628a7bb8f5ed939042dfb061102d185cf Mon Sep 17 00:00:00 2001
From: Tomas Babej <tba...@redhat.com>
Date: Wed, 4 Sep 2013 16:29:06 +0200
Subject: [PATCH 105/106] ipatests: Add AD-integration related tasks

---
 ipatests/test_integration/tasks.py | 155 +++++++++++++++++++++++++++++++++++++
 1 file changed, 155 insertions(+)

diff --git a/ipatests/test_integration/tasks.py b/ipatests/test_integration/tasks.py
index d613e6490b8a9c57d1e9af7d3b11a8142a428a57..c0dbd4acace608532694d85c76f6d3d7f8030154 100644
--- a/ipatests/test_integration/tasks.py
+++ b/ipatests/test_integration/tasks.py
@@ -197,6 +197,7 @@ def install_replica(master, replica, setup_ca=True):
 
     kinit_admin(replica)
 
+
 def install_client(master, client):
     client.collect_log('/var/log/ipaclient-install.log')
 
@@ -212,6 +213,160 @@ def install_client(master, client):
     kinit_admin(client)
 
 
+def install_adtrust(host, ad):
+    # ipa-adtrust-install appends to ipaserver-install.log
+    host.collect_log('/var/log/ipaserver-install.log')
+
+    inst = host.domain.realm.replace('.', '-')
+    host.collect_log('/var/log/dirsrv/slapd-%s/errors' % inst)
+    host.collect_log('/var/log/dirsrv/slapd-%s/access' % inst)
+
+    kinit_admin(host)
+    host.run_command(['ipa-adtrust-install', '-U',
+                      '--netbios-name', host.netbios,
+                      '-a', host.config.admin_password,
+                      '--add-sids'])
+
+    # Restart named because it lost connection to dirsrv
+    # (Directory server restarts during the ipa-adtrust-install)
+    host.run_command(['systemctl', 'restart', 'named'])
+
+    # Check that named is running and has loaded the information from LDAP
+    dig_command = ['dig', 'SRV', '+short', '@localhost',
+               '_ldap._tcp.%s' % host.domain.name]
+    dig_output = '0 100 389 %s.' % host.hostname
+
+    assert wait_assert(host, dig_command, stdout_re=dig_output)
+
+
+def configure_dns_for_trust(master, ad):
+    """
+    This configures DNS on IPA master according to the relationship of the
+    IPA's and AD's domains.
+
+    DNS has to be setup manually on AD.
+
+    Steps to be done on the AD:
+
+    1.) IPA is subdomain of the AD:
+
+    dnscmd 127.0.0.1 /RecordAdd ad.domain ipa_hostname.ipa_netbios A ipa_ip_address
+    dnscmd 127.0.0.1 /RecordAdd ad_domain ipa_netbios NS ipa_hostname.ipa_domain
+
+    2.) AD is subdomain of the IPA:
+
+    dnscmd 127 0.0.1 /ZoneAdd ipa_domain /Secondary ipa_ip_address
+
+    3.) IPA and AD domains are parallel:
+
+    dnscmd 127.0.0.1 /ZoneAdd ipa_domain /Forwarder ipa_ip_address
+    """
+
+    def is_subdomain(subdomain, domain):
+        subdomain_unpacked = subdomain.split('.')
+        domain_unpacked = domain.split('.')
+
+        subdomain_unpacked.reverse()
+        domain_unpacked.reverse()
+
+        subdomain = False
+
+        if len(subdomain_unpacked) > len(domain_unpacked):
+            subdomain = True
+
+            for i in range(len(domain_unpacked)):
+                subdomain = (subdomain and
+                             subdomain_unpacked[i] == domain_unpacked[i])
+
+        return subdomain
+
+    kinit_admin(master)
+
+    if is_subdomain(master.domain.name, ad.domain.name):
+        master.run_command(['ipa', 'dnszone-add', ad.domain.name,
+                            '--name-server', ad.hostname,
+                            '--admin-email', 'hostmaster@%s' % ad.domain.name,
+                            '--forwarder', ad.ip,
+                            '--forward-policy', 'only',
+                            '--ip-address', ad.ip,
+                            '--force'])
+    elif is_subdomain(ad.domain.name, master.domain.name):
+        master.run_command(['ipa', 'dnsrecord-add', master.domain.name,
+                            '%s.%s' % (ad.shortname, ad.netbios),
+                            '--a-ip-address', ad.ip])
+
+        master.run_command(['ipa', 'dnsrecord-add', master.domain.name,
+                            ad.netbios,
+                            '--ns-hostname',
+                            '%s.%s' % (ad.shortname, ad.netbios)])
+
+        master.run_command(['ipa', 'dnszone-mod', master.domain.name,
+                            '--allow-transfer', ad.ip])
+    else:
+        master.run_command(['ipa', 'dnszone-add', ad.domain.name,
+                            '--name-server', ad.hostname,
+                            '--admin-email', 'hostmaster@%s' % ad.domain.name,
+                            '--forwarder', ad.ip,
+                            '--forward-policy', 'only',
+                            '--ip-address', ad.ip,
+                            '--force'])
+
+
+def estabilish_trust_with_ad(master, ad, extra_args=[]):
+    kinit_admin(master)
+
+    assert wait_assert(master,
+                       ['ipa', 'trust-add',
+                       '--type', 'ad', ad.domain.name,
+                       '--admin', 'Administrator',
+                       '--password'] + extra_args,
+                       stdin_text=master.config.ad_admin_password)
+
+
+def remove_trust_with_ad(master, ad):
+    kinit_admin(master)
+
+    # Remove the trust
+    master.run_command(['ipa', 'trust-del', ad.domain.name])
+
+    # Remove the range
+    range_name = ad.domain.name.upper() + '_id_range'
+    master.run_command(['ipa', 'idrange-del', range_name])
+
+
+def configure_realm_domain_mapping(master, ad):
+    section_identifier = " %s = {" % master.domain.realm
+    line1 = "  auth_to_local = RULE:[1:$1@$0](^.*@%s$)s/@%s/@%s/"\
+            % (ad.domain.realm, ad.domain.realm, ad.domain.name)
+    line2 = "  auth_to_local = DEFAULT"
+
+    krb5_conf_content = master.get_file_contents('/etc/krb5.conf')
+    krb5_lines = [line.rstrip() for line in krb5_conf_content.split('\n')]
+    realm_section_index = krb5_lines.index(section_identifier)
+
+    krb5_lines.insert(realm_section_index + 1, line1)
+    krb5_lines.insert(realm_section_index + 2, line2)
+
+    krb5_conf_new_content = '\n'.join(krb5_lines)
+    master.put_file_contents('/etc/krb5.conf', krb5_conf_new_content)
+
+    master.run_command(['systemctl', 'restart', 'sssd'])
+
+
+def clear_sssd_cache(host):
+    host.run_command(['systemctl', 'stop', 'sssd'])
+    host.run_command(['rm', '-rfv', '/var/lib/sss/db/cache_%s.ldb'
+                                    % host.domain.name])
+    host.run_command(['rm', '-rfv', '/var/lib/sss/mc/group'])
+    host.run_command(['rm', '-rfv', '/var/lib/sss/mc/passwd'])
+    host.run_command(['systemctl', 'start', 'sssd'])
+
+
+def sync_time(host, server):
+    host.run_command(['sudo', 'systemctl', 'stop', 'ntpd'])
+    host.run_command(['sudo', 'ntpdate', server.hostname])
+
+
 def connect_replica(master, replica):
     kinit_admin(replica)
     replica.run_command(['ipa-replica-manage', 'connect', master.hostname])
-- 
1.8.3.1

From c33a492fc6cf4d8f53608102a4b0430012684880 Mon Sep 17 00:00:00 2001
From: Tomas Babej <tba...@redhat.com>
Date: Fri, 6 Sep 2013 13:42:51 +0200
Subject: [PATCH 106/106] ipatests: Add AD integration test case

---
 ipatests/test_integration/tasks.py      |   2 +
 ipatests/test_integration/test_trust.py | 139 ++++++++++++++++++++++++++++++++
 2 files changed, 141 insertions(+)
 create mode 100644 ipatests/test_integration/test_trust.py

diff --git a/ipatests/test_integration/tasks.py b/ipatests/test_integration/tasks.py
index c0dbd4acace608532694d85c76f6d3d7f8030154..48c4683a4faf48815c4ab07e915343faacddd0a3 100644
--- a/ipatests/test_integration/tasks.py
+++ b/ipatests/test_integration/tasks.py
@@ -322,6 +322,8 @@ def estabilish_trust_with_ad(master, ad, extra_args=[]):
                        '--password'] + extra_args,
                        stdin_text=master.config.ad_admin_password)
 
+    clear_sssd_cache(master)
+
 
 def remove_trust_with_ad(master, ad):
     kinit_admin(master)
diff --git a/ipatests/test_integration/test_trust.py b/ipatests/test_integration/test_trust.py
new file mode 100644
index 0000000000000000000000000000000000000000..58c9ee65babdf905058e5d3e4f22654f641be042
--- /dev/null
+++ b/ipatests/test_integration/test_trust.py
@@ -0,0 +1,139 @@
+# Authors:
+#   Tomas Babej <tba...@redhat.com>
+#
+# Copyright (C) 2013  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 re
+
+from ipatests.test_integration.base import IntegrationTest
+from ipatests.test_integration import tasks
+
+
+class TestADTrust(IntegrationTest):
+    """Integration test for Active Directory
+
+    Active Directory used for this Integration test should have support
+    for POSIX attributes installed, as described in:
+
+        http://technet.microsoft.com/en-us/library/cc731178.aspx
+
+    There should be one user, called testuser, with:
+        UID: 10042
+        Home directory: /home/testuser
+        Shell: /bin/sh
+        Full name: Test User
+        Primary group: Test Group
+
+    There should be one group, called Test Group (testgroup), with GID 10047.
+
+    Additionally, DNS records need to be properly set on the AD side, as
+    described in configure_dns_for_trust method docstring.
+    """
+
+    num_ad_domains = 1
+
+    @classmethod
+    def setup_class(cls):
+        super(TestADTrust, cls).setup_class()
+        cls.ad = cls.ad_domains[0].ads[0]
+
+    def test_install_adtrust(self):
+        """Test adtrust support installation"""
+
+        tasks.install_adtrust(self.master, self.ad)
+
+    def test_check_sid_generation(self):
+        """Test SID generation"""
+
+        command = ['ipa', 'user-show', 'admin', '--all', '--raw']
+
+        # TODO: remove duplicate definition and import from common module
+        _sid_identifier_authority = '(0x[0-9a-f]{1,12}|[0-9]{1,10})'
+        sid_regex = 'S-1-5-21-%(idauth)s-%(idauth)s-%(idauth)s'\
+                    % dict(idauth=_sid_identifier_authority)
+        stdout_re = re.escape('  ipaNTSecurityIdentifier: ') + sid_regex
+
+        assert tasks.run_repeatedly(self.master, command,
+                                    test=lambda x: re.search(stdout_re, x))
+
+    def test_estabilish_trust(self):
+        """Tests estabilishing trust with Active Directory"""
+
+        tasks.configure_dns_for_trust(self.master, self.ad)
+        tasks.sync_time(self.master, self.ad)
+
+        tasks.estabilish_trust_with_ad(self.master, self.ad,
+            extra_args=['--range-type', 'ipa-ad-trust'])
+
+    def test_range_properties_in_nonposix_trust(self):
+        """Check the properties of the created range"""
+
+        range_name = self.ad.domain.name.upper() + '_id_range'
+        result = self.master.run_command(['ipa', 'idrange-show', range_name,
+                                          '--all', '--raw'])
+        assert "  ipaRangeType: ipa-ad-trust" in result.stdout_text
+        assert "  ipaIDRangeSize: 200000" in result.stdout_text
+
+    def test_user_gid_uid_resolution_in_nonposix_trust(self):
+        """Check that user has SID-generated UID"""
+
+        testuser = 'testuser@%s' % self.ad.domain.realm
+        result = self.master.run_command(['getent', 'passwd', testuser])
+
+        # This regex checks that Test User does not have UID 10042 nor belongs
+        # to the group with GID 10047
+        testuser_regex = "^testuser@%s:\*:(?!10042)(\d+):(?!10047)(\d+):"\
+                         "Test User:/home/testuser:/bin/sh$"\
+                         % self.ad.domain.name.replace('.', '\.')
+
+        assert re.search(testuser_regex, result.stdout_text)
+
+    def test_remove_nonposix_trust(self):
+        tasks.remove_trust_with_ad(self.master, self.ad)
+        tasks.clear_sssd_cache(self.master)
+
+    def test_estabilish_trust_with_posix_attributes(self):
+
+        # DNS records presisted from previous test
+        # Not specifying the --range-type directly, it should be detected
+        tasks.estabilish_trust_with_ad(self.master, self.ad)
+
+    def test_range_properties_in_posix_trust(self):
+        # Check the properties of the created range
+        range_name = self.ad.domain.name.upper() + '_id_range'
+
+        result = self.master.run_command(['ipa', 'idrange-show', range_name,
+                                          '--all', '--raw'])
+
+        # Check the range type and size
+        assert "  ipaRangeType: ipa-ad-trust-posix" in result.stdout_text
+        assert "  ipaIDRangeSize: 200000" in result.stdout_text
+
+    def test_user_uid_gid_resolution_in_posix_trust(self):
+        # Check that user has AD-defined UID
+        testuser = 'testuser@%s' % self.ad.domain.realm
+        result = self.master.run_command(['getent', 'passwd', testuser])
+
+        testuser_stdout = "testuser@%s:*:10042:10047:"\
+                          "Test User:/home/testuser:/bin/sh"\
+                          % self.ad.domain.name
+
+        assert testuser_stdout in result.stdout_text
+
+    def test_remove_trust_with_posix_attributes(self):
+        tasks.remove_trust_with_ad(self.master, self.ad)
+        tasks.clear_sssd_cache(self.master)
-- 
1.8.3.1

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to