#!/usr/bin/python

import os

from k5test import *

# Read lines from kpropd output until we are synchronized.  Error if
# full_expected is true and we didn't see a full propagation or vice
# versa.
def wait_for_prop(kpropd, full_expected):
    output('*** Waiting for sync from kpropd\n')
    full_seen = False
    while True:
        line = kpropd.stdout.readline()
        if line == '':
            fail('kpropd process exited unexpectedly')
        output('kpropd: ' + line)

        if 'KDC is synchronized' in line or 'Incremental updates:' in line:
            output('*** Sync complete\n')
            if full_expected and not full_seen:
                fail('Expected full dump but saw only incremental')
            if full_seen and not full_expected:
                fail('Expected incremental prop but saw full dump')
            return

        if 'load process for full propagation completed' in line:
            full_seen = True
            # kpropd's child process has finished a DB load; make the parent
            # do another iprop request.  This will be unnecessary if kpropd
            # is simplified to use a single process.
            kpropd.send_signal(signal.SIGUSR1)

        # Detect some failure conditions.
        if 'Still waiting for full resync' in line:
            fail('kadmind gave consecutive full resyncs')
        if 'Rejected connection' in line:
            fail('kpropd rejected kprop connection')
        if 'get updates failed' in line:
            fail('iprop_get_updates failed')
        if 'permission denied' in line:
            fail('kadmind denied update')
        if 'error from master' in line or 'error returned from master' in line:
            fail('kadmind reported error')
        if 'invalid return' in line:
            fail('kadmind returned invalid result')


# Verify the iprop log last serial number against an expected value,
# on either the master or slave.
def check_serial(realm, expected, env=None):
    out = realm.run([kproplog, '-h'], env=env)
    if 'Last serial # : ' not in out:
        fail('Unexpected serial number')


conf = {
    'realms': {'$realm': {
            'iprop_enable': 'true',
            'iprop_logfile' : '$testdir/db.ulog'}}}

conf_slave = {
    'realms': {'$realm': {
            'iprop_slave_poll': '600',
            'iprop_logfile' : '$testdir/db.slave.ulog'}},
    'dbmodules': {'db': {'database_name': '$testdir/db.slave'}}}

realm = K5Realm(kdc_conf=conf, create_user=False, start_kadmind=True)
slave = realm.special_env('slave', True, kdc_conf=conf_slave)

ulog = os.path.join(realm.testdir, 'db.ulog')
if not os.path.exists(ulog):
    fail('update log not created: ' + ulog)

# Create the principal used to authenticate kpropd to kadmind.
kiprop_princ = 'kiprop/' + hostname
realm.addprinc(kiprop_princ)
realm.extract_keytab(kiprop_princ, realm.keytab)

# Create the slave db.
dumpfile = os.path.join(realm.testdir, 'dump')
realm.run([kdb5_util, 'dump', dumpfile])
realm.run([kdb5_util, 'load', dumpfile], slave)
realm.run([kdb5_util, 'stash', '-P', 'master'], slave)

# Make some changes to the master db.
realm.addprinc('wakawaka')
# Add a principal enough to make realloc likely, but not enough to grow
# basic ulog entry size.
c = 'chocolate-flavored-school-bus'
cs = c + '/'
longname = cs + cs + cs + cs + cs + cs + cs + cs + cs + cs + cs + cs + c
realm.addprinc(longname)
realm.addprinc('w')
realm.run_kadminl('modprinc -allow_tix w')
realm.run_kadminl('modprinc +allow_tix w')

check_serial(realm, '7')

# Set up the kpropd acl file.
acl_file = os.path.join(realm.testdir, 'kpropd-acl')
acl = open(acl_file, 'w')
acl.write(realm.host_princ + '\n')
acl.close()

# Start kpropd and get a full dump from master.
kpropd = realm.start_kpropd(slave, ['-d'])
wait_for_prop(kpropd, True)
out = realm.run_kadminl('listprincs', slave)
if longname not in out or 'wakawaka' not in out or 'w@' not in out:
    fail('Slave does not have all principals from master')

# Make a change and check that it propagates incrementally.
realm.run_kadminl('modprinc -allow_tix w')
check_serial(realm, '8')
kpropd.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd, False)
check_serial(realm, '8', slave)
out = realm.run_kadminl('getprinc w', slave)
if 'Attributes: DISALLOW_ALL_TIX' not in out:
    fail('Slave does not have modification from master')

# Make another change and check that it propagates incrementally.
realm.run_kadminl('modprinc +allow_tix w')
check_serial(realm, '9')
kpropd.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd, False)
check_serial(realm, '9', slave)
out = realm.run_kadminl('getprinc w', slave)
if 'Attributes:\n' not in out:
    fail('Slave does not have modification from master')

# Reset the ulog on the slave side to force a full resync to the slave.
realm.run([kproplog, '-R'], slave)
check_serial(realm, 'None', slave)
kpropd.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd, True)
check_serial(realm, '9', slave)

# Make another change and check that it propagates incrementally.
realm.run_kadminl('modprinc +allow_tix w')
check_serial(realm, '10')
kpropd.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd, False)
check_serial(realm, '10', slave)
out = realm.run_kadminl('getprinc w', slave)
if 'Attributes:\n' not in out:
    fail('Slave has different state from master')

# Create a policy and check that it propagates via full resync.
realm.run_kadminl('addpol -minclasses 2 testpol')
check_serial(realm, 'None')
kpropd.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd, True)
check_serial(realm, 'None', slave)
out = realm.run_kadminl('getpol testpol', slave)
if 'Minimum number of password character classes: 2' not in out:
    fail('Slave does not have policy from master')

# Modify the policy and test that it also propagates via full resync.
realm.run_kadminl('modpol -minlength 17 testpol')
check_serial(realm, 'None')
kpropd.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd, True)
check_serial(realm, 'None', slave)
out = realm.run_kadminl('getpol testpol', slave)
if 'Minimum password length: 17' not in out:
    fail('Slave does not have policy change from master')

# Delete the policy and test that it propagates via full resync.
realm.run_kadminl('delpol -force testpol')
check_serial(realm, 'None')
kpropd.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd, True)
check_serial(realm, 'None', slave)
out = realm.run_kadminl('getpol testpol', slave)
if 'Policy does not exist' not in out:
    fail('Slave did not get policy deletion from master')

# Reset the ulog on the master side to force a full resync to all slaves.
# XXX Note that we only have one slave in this test, so we can't really
# test this.
realm.run([kproplog, '-R'])
check_serial(realm, 'None')
kpropd.send_signal(signal.SIGUSR1)
wait_for_prop(kpropd, True)
check_serial(realm, 'None', slave)

success('iprop tests')