Hello,

attached patches implement a portion of improvements for ticket
https://fedorahosted.org/freeipa/ticket/4657

It came to my mind that it will be better to review them at once - the
previous threads with my patches 40 and 41 can be abandoned.

I'm sorry for the mess.

-- 
Petr^2 Spacek
From 999017d75f3044bd9abf6d8c2a4a70cede77886f Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Fri, 26 Jun 2015 16:04:00 +0200
Subject: [PATCH] DNSSEC: Detect invalid master keys in LDAP.

This should never happen ...

https://fedorahosted.org/freeipa/ticket/4657
---
 daemons/dnssec/ipa-dnskeysync-replica | 1 +
 1 file changed, 1 insertion(+)

diff --git a/daemons/dnssec/ipa-dnskeysync-replica b/daemons/dnssec/ipa-dnskeysync-replica
index c2c4c2725a9c46db4db04894a326ddf40e254eab..551c2f21d5b85b76a7281f719ce722a6c5830cf7 100755
--- a/daemons/dnssec/ipa-dnskeysync-replica
+++ b/daemons/dnssec/ipa-dnskeysync-replica
@@ -74,6 +74,7 @@ def ldap2replica_master_keys_sync(log, ldapkeydb, localhsm):
     log.debug("new master keys in LDAP HSM: %s", hex_set(new_keys))
     for mkey_id in new_keys:
         mkey_ldap = ldapkeydb.master_keys[mkey_id]
+        assert mkey_ldap.wrapped_entries, "Master key 0x%s in LDAP is missing key material referenced by ipaSecretKeyRefObject attribute" % hexlify(mkey_id)
         for wrapped_ldap in mkey_ldap.wrapped_entries:
             unwrapping_key = find_unwrapping_key(log, localhsm,
                     wrapped_ldap.single_value['ipaWrappingKey'])
-- 
2.1.0

From c927f884eaed11506587c6dbb82ccd7e07896987 Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Fri, 26 Jun 2015 17:39:47 +0200
Subject: [PATCH] DNSSEC: Accept ipa-ods-exporter commands from command line.

Previously only systemd socket activation was supported.
Ability to call the command directly is handy in special cases,
e.g. for debugging or moving key master role from one server to another.

https://fedorahosted.org/freeipa/ticket/4657
---
 daemons/dnssec/ipa-ods-exporter | 88 ++++++++++++++++++++++++++---------------
 1 file changed, 56 insertions(+), 32 deletions(-)

diff --git a/daemons/dnssec/ipa-ods-exporter b/daemons/dnssec/ipa-ods-exporter
index 913b418af2806e2660a7db221e06394b501bbb18..c6de5acbd9966a0cf5bb6a0c35c54c79aec91604 100755
--- a/daemons/dnssec/ipa-ods-exporter
+++ b/daemons/dnssec/ipa-ods-exporter
@@ -9,6 +9,9 @@ This program uses the same socket and protocol as original signerd and should
 be activated via systemd socket activation using "ods-signer" command line
 utility.
 
+Alternativelly, it can be called directly and a command can be supplied as
+first command line argument.
+
 Purpose of this replacement is to upload keys generated by OpenDNSSEC to LDAP.
 """
 
@@ -334,7 +337,7 @@ def hex_set(s):
         out.add("0x%s" % hexlify(i))
     return out
 
-def receive_zone_name(log):
+def receive_systemd_command(log):
     fds = systemd.daemon.listen_fds()
     if len(fds) != 1:
         raise KeyError('Exactly one socket is expected.')
@@ -345,52 +348,60 @@ def receive_zone_name(log):
     log.debug('accepted new connection %s', repr(conn))
 
     # this implements cmdhandler_handle_cmd() logic
-    cmd = conn.recv(ODS_SE_MAXLINE)
-    cmd = cmd.strip()
+    cmd = conn.recv(ODS_SE_MAXLINE).strip()
+    log.debug('received command "%s" from systemd socket', cmd)
+    return (cmd, conn)
 
-    try:
-        if cmd == 'ipa-hsm-update':
-            msg = 'HSM synchronization finished, exiting.'
-            conn.send('%s\n' % msg)
-            log.info(msg)
-            sys.exit(0)
+def parse_command(cmd):
+    """Parse command to (exit code, message, zone_name) tuple.
 
-        elif not cmd.startswith('update '):
-            conn.send('Command "%s" is not supported by IPA; ' \
-                      'HSM synchronization was finished and the command ' \
-                      'will be ignored.\n' % cmd)
-            log.info('Ignoring unsupported command "%s".', cmd)
-            sys.exit(0)
+    Exit code None means that execution should continue.
+    """
+    if cmd == 'ipa-hsm-update':
+        return (0,
+                'HSM synchronization finished, exiting.',
+                None)
 
-        else:
-            zone_name = cmd2ods_zone_name(cmd)
-            conn.send('Update request for zone "%s" queued.\n' % zone_name)
-            log.info('Processing command: "%s"', cmd)
+    elif not cmd.startswith('update '):
+        return (0,
+                'Command "%s" is not supported by IPA; '
+                'HSM synchronization was finished and the command '
+                'will be ignored.\n' % cmd,
+                None)
 
-    finally:
+    else:
+        zone_name = cmd2ods_zone_name(cmd)
+        return (None,
+                'Update request for zone "%s" queued.\n' % zone_name,
+                zone_name)
+
+def send_systemd_reply(conn, reply):
         # Reply & close connection early.
         # This is necessary to let Enforcer to unlock the ODS DB.
+        conn.send(reply + '\n')
         conn.shutdown(socket.SHUT_RDWR)
         conn.close()
 
-    return zone_name
-
 def cmd2ods_zone_name(cmd):
     # ODS stores zone name without trailing period
     zone_name = cmd[7:].strip()
     if len(zone_name) > 1 and zone_name[-1] == '.':
         zone_name = zone_name[:-1]
 
     return zone_name
 
 log = logging.getLogger('root')
-# this service is socket-activated
+# this service is usually socket-activated
 log.addHandler(systemd.journal.JournalHandler())
 log.setLevel(level=logging.DEBUG)
 
-if len(sys.argv) != 1:
+if len(sys.argv) > 2:
     print __doc__
     sys.exit(1)
+# program was likely invoked from console, log to it
+elif len(sys.argv) == 2:
+    console = logging.StreamHandler()
+    log.addHandler(console)
 
 # IPA framework initialization
 ipalib.api.bootstrap(in_server=True, log=None)  # no logging to file
@@ -429,16 +440,29 @@ master2ldap_zone_keys_sync(log, ldapkeydb, localhsm)
 # command receive is delayed so the command will stay in socket queue until
 # the problem with LDAP server or HSM is fixed
 try:
-    zone_name = receive_zone_name(log)
-
+    cmd, conn = receive_systemd_command(log)
+    if len(sys.argv) != 1:
+        log.critical('No additional parameters are accepted when '
+                     'socket activation is used.')
+        sys.exit(1)
 # Handle cases where somebody ran the program without systemd.
 except KeyError as e:
-    print 'HSM (key material) sychronization is finished but ' \
-          'this program should be socket-activated by OpenDNSSEC.'
-    print 'Use "ods-signer" command line utility to synchronize ' \
-          'DNS zone keys and metadata.'
-    print 'Error: %s' % e
-    sys.exit(0)
+    if len(sys.argv) != 2:
+        print(__doc__)
+        print('ERROR: Exactly one parameter or socket activation is required.')
+        sys.exit(1)
+    conn = None
+    cmd = sys.argv[1]
+
+exitcode, msg, zone_name = parse_command(cmd)
+
+if conn:
+    send_systemd_reply(conn, msg)
+if exitcode is not None:
+    log.info(msg)
+    sys.exit(exitcode)
+else:
+    log.debug(msg)
 
 ods_keys = get_ods_keys(zone_name)
 ods_keys_id = set(ods_keys.keys())
-- 
2.1.0

From 000fa4ca292481c6f4f609923db34ac9f80e9b55 Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Fri, 26 Jun 2015 17:53:17 +0200
Subject: [PATCH] DNSSEC: ipa-ods-exporter: move zone synchronization into
 separate function

https://fedorahosted.org/freeipa/ticket/4657
---
 daemons/dnssec/ipa-ods-exporter | 124 +++++++++++++++++++++-------------------
 1 file changed, 64 insertions(+), 60 deletions(-)

diff --git a/daemons/dnssec/ipa-ods-exporter b/daemons/dnssec/ipa-ods-exporter
index c6de5acbd9966a0cf5bb6a0c35c54c79aec91604..83f02d86d8e624163ca0d87125ada2a3687cac61 100755
--- a/daemons/dnssec/ipa-ods-exporter
+++ b/daemons/dnssec/ipa-ods-exporter
@@ -390,6 +390,69 @@ def cmd2ods_zone_name(cmd):
 
     return zone_name
 
+def sync_zone(log, ldap, dns_dn, zone_name):
+    ods_keys = get_ods_keys(zone_name)
+    ods_keys_id = set(ods_keys.keys())
+
+    ldap_zone = get_ldap_zone(ldap, dns_dn, zone_name)
+    zone_dn = ldap_zone.dn
+
+    keys_dn = get_ldap_keys_dn(zone_dn)
+    try:
+        ldap_keys = get_ldap_keys(ldap, zone_dn)
+    except ipalib.errors.NotFound:
+        # cn=keys container does not exist, create it
+        ldap_keys = []
+        ldap_keys_container = ldap.make_entry(keys_dn,
+                                              objectClass=['nsContainer'])
+        try:
+            ldap.add_entry(ldap_keys_container)
+        except ipalib.errors.DuplicateEntry:
+            # ldap.get_entries() does not distinguish non-existent base DN
+            # from empty result set so addition can fail because container
+            # itself exists already
+            pass
+
+    ldap_keys_dict = {}
+    for ldap_key in ldap_keys:
+        cn = ldap_key['cn'][0]
+        ldap_keys_dict[cn] = ldap_key
+
+    ldap_keys = ldap_keys_dict  # shorthand
+    ldap_keys_id = set(ldap_keys.keys())
+
+    new_keys_id = ods_keys_id - ldap_keys_id
+    log.info('new keys from ODS: %s', new_keys_id)
+    for key_id in new_keys_id:
+        cn = "cn=%s" % key_id
+        key_dn = DN(cn, keys_dn)
+        log.debug('adding key "%s" to LDAP', key_dn)
+        ldap_key = ldap.make_entry(key_dn,
+                                   objectClass=['idnsSecKey'],
+                                   **ods_keys[key_id])
+        ldap.add_entry(ldap_key)
+
+    deleted_keys_id = ldap_keys_id - ods_keys_id
+    log.info('deleted keys in LDAP: %s', deleted_keys_id)
+    for key_id in deleted_keys_id:
+        cn = "cn=%s" % key_id
+        key_dn = DN(cn, keys_dn)
+        log.debug('deleting key "%s" from LDAP', key_dn)
+        ldap.delete_entry(key_dn)
+
+    update_keys_id = ldap_keys_id.intersection(ods_keys_id)
+    log.info('keys in LDAP & ODS: %s', update_keys_id)
+    for key_id in update_keys_id:
+        ldap_key = ldap_keys[key_id]
+        ods_key = ods_keys[key_id]
+        log.debug('updating key "%s" in LDAP', ldap_key.dn)
+        ldap_key.update(ods_key)
+        try:
+            ldap.update_entry(ldap_key)
+        except ipalib.errors.EmptyModlist:
+            continue
+
+
 log = logging.getLogger('root')
 # this service is usually socket-activated
 log.addHandler(systemd.journal.JournalHandler())
@@ -464,65 +527,6 @@ if exitcode is not None:
 else:
     log.debug(msg)
 
-ods_keys = get_ods_keys(zone_name)
-ods_keys_id = set(ods_keys.keys())
-
-ldap_zone = get_ldap_zone(ldap, dns_dn, zone_name)
-zone_dn = ldap_zone.dn
-
-keys_dn = get_ldap_keys_dn(zone_dn)
-try:
-    ldap_keys = get_ldap_keys(ldap, zone_dn)
-except ipalib.errors.NotFound:
-    # cn=keys container does not exist, create it
-    ldap_keys = []
-    ldap_keys_container = ldap.make_entry(keys_dn,
-                                          objectClass=['nsContainer'])
-    try:
-        ldap.add_entry(ldap_keys_container)
-    except ipalib.errors.DuplicateEntry:
-        # ldap.get_entries() does not distinguish non-existent base DN
-        # from empty result set so addition can fail because container
-        # itself exists already
-        pass
-
-ldap_keys_dict = {}
-for ldap_key in ldap_keys:
-    cn = ldap_key['cn'][0]
-    ldap_keys_dict[cn] = ldap_key
-
-ldap_keys = ldap_keys_dict  # shorthand
-ldap_keys_id = set(ldap_keys.keys())
-
-new_keys_id = ods_keys_id - ldap_keys_id
-log.info('new keys from ODS: %s', new_keys_id)
-for key_id in new_keys_id:
-    cn = "cn=%s" % key_id
-    key_dn = DN(cn, keys_dn)
-    log.debug('adding key "%s" to LDAP', key_dn)
-    ldap_key = ldap.make_entry(key_dn,
-                               objectClass=['idnsSecKey'],
-                               **ods_keys[key_id])
-    ldap.add_entry(ldap_key)
-
-deleted_keys_id = ldap_keys_id - ods_keys_id
-log.info('deleted keys in LDAP: %s', deleted_keys_id)
-for key_id in deleted_keys_id:
-    cn = "cn=%s" % key_id
-    key_dn = DN(cn, keys_dn)
-    log.debug('deleting key "%s" from LDAP', key_dn)
-    ldap.delete_entry(key_dn)
-
-update_keys_id = ldap_keys_id.intersection(ods_keys_id)
-log.info('keys in LDAP & ODS: %s', update_keys_id)
-for key_id in update_keys_id:
-    ldap_key = ldap_keys[key_id]
-    ods_key = ods_keys[key_id]
-    log.debug('updating key "%s" in LDAP', ldap_key.dn)
-    ldap_key.update(ods_key)
-    try:
-        ldap.update_entry(ldap_key)
-    except ipalib.errors.EmptyModlist:
-        continue
+sync_zone(log, ldap, dns_dn, zone_name)
 
 log.debug('Done')
-- 
2.1.0

From 7be6c39fd4340e9c7af0fa840d0be59116733fc7 Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Fri, 26 Jun 2015 17:58:25 +0200
Subject: [PATCH] DNSSEC: log ipa-ods-exporter file lock operations into debug
 log

https://fedorahosted.org/freeipa/ticket/4657
---
 daemons/dnssec/ipa-ods-exporter | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/daemons/dnssec/ipa-ods-exporter b/daemons/dnssec/ipa-ods-exporter
index 83f02d86d8e624163ca0d87125ada2a3687cac61..57fd592f3aeff01769ea7d1d9952e80b9d6f332a 100755
--- a/daemons/dnssec/ipa-ods-exporter
+++ b/daemons/dnssec/ipa-ods-exporter
@@ -117,10 +117,13 @@ def sql2ldap_keyid(sql_keyid):
 class ods_db_lock(object):
     def __enter__(self):
         self.f = open(ODS_DB_LOCK_PATH, 'w')
+        log.debug('waiting for lock %r', self.f)
         fcntl.lockf(self.f, fcntl.LOCK_EX)
+        log.debug('acquired lock %r', self.f)
 
     def __exit__(self, *args):
         fcntl.lockf(self.f, fcntl.LOCK_UN)
+        log.debug('released lock %r', self.f)
         self.f.close()
 
 def get_ldap_zone(ldap, dns_base, name):
-- 
2.1.0

From d5f03ffd7060af08fb9b7e2975064c34f9aea84f Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Fri, 26 Jun 2015 18:21:38 +0200
Subject: [PATCH] DNSSEC: Add ability to trigger full data synchronization to
 ipa-ods-exporter.

New exporter's command 'ipa-full-update' will resynchronize all zone
keys from ODS database to LDAP.

This command holds database lock for the whole time to avoid race
conditions so it should be used only in special cases, e.g. during
master server migration.

https://fedorahosted.org/freeipa/ticket/4657
---
 daemons/dnssec/ipa-ods-exporter | 92 +++++++++++++++++++++++------------------
 1 file changed, 52 insertions(+), 40 deletions(-)

diff --git a/daemons/dnssec/ipa-ods-exporter b/daemons/dnssec/ipa-ods-exporter
index 57fd592f3aeff01769ea7d1d9952e80b9d6f332a..7758ea3d339b8af9849bf5a74b979f5202c39130 100755
--- a/daemons/dnssec/ipa-ods-exporter
+++ b/daemons/dnssec/ipa-ods-exporter
@@ -157,49 +157,42 @@ def get_ldap_keys(ldap, zone_dn):
     return ldap_keys
 
 def get_ods_keys(zone_name):
-    # Open DB directly and read key timestamps etc.
-    with ods_db_lock():
-        db = sqlite3.connect(paths.OPENDNSSEC_KASP_DB,
-                isolation_level="EXCLUSIVE")
-        db.row_factory = sqlite3.Row
-        db.execute('BEGIN')
+    # get zone ID
+    cur = db.execute("SELECT id FROM zones WHERE LOWER(name)=LOWER(?)",
+                     (zone_name,))
+    rows = cur.fetchall()
+    assert len(rows) == 1, "exactly one DNS zone should exist in ODS DB"
+    zone_id = rows[0][0]
 
-        # get zone ID
-        cur = db.execute("SELECT id FROM zones WHERE LOWER(name)=LOWER(?)",
-                         (zone_name,))
-        rows = cur.fetchall()
-        assert len(rows) == 1, "exactly one DNS zone should exist in ODS DB"
-        zone_id = rows[0][0]
+    # get all keys for given zone ID
+    cur = db.execute("SELECT kp.HSMkey_id, kp.generate, kp.algorithm, dnsk.publish, dnsk.active, dnsk.retire, dnsk.dead, dnsk.keytype "
+             "FROM keypairs AS kp JOIN dnsseckeys AS dnsk ON kp.id = dnsk.id "
+             "WHERE dnsk.zone_id = ?", (zone_id,))
+    keys = {}
+    for row in cur:
+        key_data = sql2datetimes(row)
+        if 'idnsSecKeyDelete' in key_data \
+            and key_data['idnsSecKeyDelete'] > datetime.now():
+                continue  # ignore deleted keys
 
-        # get all keys for given zone ID
-        cur = db.execute("SELECT kp.HSMkey_id, kp.generate, kp.algorithm, dnsk.publish, dnsk.active, dnsk.retire, dnsk.dead, dnsk.keytype "
-                 "FROM keypairs AS kp JOIN dnsseckeys AS dnsk ON kp.id = dnsk.id "
-                 "WHERE dnsk.zone_id = ?", (zone_id,))
-        keys = {}
-        for row in cur:
-            key_data = sql2datetimes(row)
-            if 'idnsSecKeyDelete' in key_data \
-                and key_data['idnsSecKeyDelete'] > datetime.now():
-                    continue  # ignore deleted keys
+        key_data.update(sql2ldap_flags(row['keytype']))
+        log.debug("%s", key_data)
+        assert key_data.get('idnsSecKeyZONE', None) == 'TRUE', \
+                'unexpected key type 0x%x' % row['keytype']
+        if key_data.get('idnsSecKeySEP', 'FALSE') == 'TRUE':
+            key_type = 'KSK'
+        else:
+            key_type = 'ZSK'
 
-            key_data.update(sql2ldap_flags(row['keytype']))
-            log.debug("%s", key_data)
-            assert key_data.get('idnsSecKeyZONE', None) == 'TRUE', \
-                    'unexpected key type 0x%x' % row['keytype']
-            if key_data.get('idnsSecKeySEP', 'FALSE') == 'TRUE':
-                key_type = 'KSK'
-            else:
-                key_type = 'ZSK'
+        key_data.update(sql2ldap_algorithm(row['algorithm']))
+        key_id = "%s-%s-%s" % (key_type,
+                               datetime2ldap(key_data['idnsSecKeyCreated']),
+                               row['HSMkey_id'])
 
-            key_data.update(sql2ldap_algorithm(row['algorithm']))
-            key_id = "%s-%s-%s" % (key_type,
-                                   datetime2ldap(key_data['idnsSecKeyCreated']),
-                                   row['HSMkey_id'])
+        key_data.update(sql2ldap_keyid(row['HSMkey_id']))
+        keys[key_id] = key_data
 
-            key_data.update(sql2ldap_keyid(row['HSMkey_id']))
-            keys[key_id] = key_data
-
-        return keys
+    return keys
 
 def sync_set_metadata_2ldap(log, source_set, target_set):
     """sync metadata from source key set to target key set in LDAP
@@ -365,11 +358,16 @@ def parse_command(cmd):
                 'HSM synchronization finished, exiting.',
                 None)
 
+    elif cmd == 'ipa-full-update':
+        return (None,
+                'Synchronization of all zones requested.',
+                None)
+
     elif not cmd.startswith('update '):
         return (0,
                 'Command "%s" is not supported by IPA; '
                 'HSM synchronization was finished and the command '
-                'will be ignored.\n' % cmd,
+                'will be ignored.' % cmd,
                 None)
 
     else:
@@ -394,6 +392,7 @@ def cmd2ods_zone_name(cmd):
     return zone_name
 
 def sync_zone(log, ldap, dns_dn, zone_name):
+    log.debug('synchronizing zone "%s"', zone_name)
     ods_keys = get_ods_keys(zone_name)
     ods_keys_id = set(ods_keys.keys())
 
@@ -530,6 +529,19 @@ if exitcode is not None:
 else:
     log.debug(msg)
 
-sync_zone(log, ldap, dns_dn, zone_name)
+# Open DB directly and read key timestamps etc.
+with ods_db_lock():
+    db = sqlite3.connect(paths.OPENDNSSEC_KASP_DB,
+            isolation_level="EXCLUSIVE")
+    db.row_factory = sqlite3.Row
+    db.execute('BEGIN')
+
+    if zone_name is not None:
+        # only one zone should be processed
+        sync_zone(log, ldap, dns_dn, zone_name)
+    else:
+        # process all zones
+        for zone_row in db.execute("SELECT name FROM zones"):
+            sync_zone(log, ldap, dns_dn, zone_row['name'])
 
 log.debug('Done')
-- 
2.1.0

From 596549adca4a04663dac9e16adeec2d15d813cff Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Fri, 26 Jun 2015 18:48:46 +0200
Subject: [PATCH] DNSSEC: Improve ipa-ods-exporter log messages with key
 metadata.

https://fedorahosted.org/freeipa/ticket/4657
---
 daemons/dnssec/ipa-ods-exporter | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/daemons/dnssec/ipa-ods-exporter b/daemons/dnssec/ipa-ods-exporter
index 7758ea3d339b8af9849bf5a74b979f5202c39130..03dcfbc8378cccf4b985d5f2853f072aac41d157 100755
--- a/daemons/dnssec/ipa-ods-exporter
+++ b/daemons/dnssec/ipa-ods-exporter
@@ -176,7 +176,6 @@ def get_ods_keys(zone_name):
                 continue  # ignore deleted keys
 
         key_data.update(sql2ldap_flags(row['keytype']))
-        log.debug("%s", key_data)
         assert key_data.get('idnsSecKeyZONE', None) == 'TRUE', \
                 'unexpected key type 0x%x' % row['keytype']
         if key_data.get('idnsSecKeySEP', 'FALSE') == 'TRUE':
@@ -191,6 +190,7 @@ def get_ods_keys(zone_name):
 
         key_data.update(sql2ldap_keyid(row['HSMkey_id']))
         keys[key_id] = key_data
+        log.debug("key %s metadata: %s", key_id, key_data)
 
     return keys
 
-- 
2.1.0

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to