2013-04-26 08:21:35 -05:00
|
|
|
# Authors: Petr Viktorin <pviktori@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 pprint
|
|
|
|
|
|
|
|
import ldap.schema
|
|
|
|
|
|
|
|
import ipapython.version
|
2015-07-20 09:04:07 -05:00
|
|
|
from ipalib import api
|
2013-04-26 08:21:35 -05:00
|
|
|
from ipapython.ipa_log_manager import log_mgr
|
|
|
|
from ipapython.dn import DN
|
|
|
|
from ipaserver.install.ldapupdate import connect
|
|
|
|
from ipaserver.install import installutils
|
|
|
|
|
|
|
|
|
2014-07-23 07:42:33 -05:00
|
|
|
SCHEMA_ELEMENT_CLASSES = (
|
2013-04-26 08:21:35 -05:00
|
|
|
# All schema model classes this tool can modify
|
2014-07-23 07:42:33 -05:00
|
|
|
# Depends on order, attributes first, then objectclasses
|
|
|
|
('attributetypes', ldap.schema.models.AttributeType),
|
|
|
|
('objectclasses', ldap.schema.models.ObjectClass),
|
|
|
|
)
|
2013-04-26 08:21:35 -05:00
|
|
|
|
|
|
|
ORIGIN = 'IPA v%s' % ipapython.version.VERSION
|
|
|
|
|
|
|
|
log = log_mgr.get_logger(__name__)
|
|
|
|
|
|
|
|
|
2014-07-23 07:42:33 -05:00
|
|
|
def _get_oid_dependency_order(schema, cls):
|
|
|
|
"""
|
|
|
|
Returns a ordered list of OIDs sets, in order which respects inheritance in LDAP
|
|
|
|
OIDs in second set, depend on first set, etc.
|
|
|
|
|
|
|
|
:return [set(1st-tree-level), set(2nd-tree-level), ...]
|
|
|
|
"""
|
|
|
|
top_node = '_'
|
|
|
|
ordered_oid_groups = []
|
|
|
|
|
|
|
|
tree = schema.tree(cls) # tree structure of schema
|
|
|
|
|
|
|
|
# remove top_node from tree, it breaks ordering
|
|
|
|
# we don't need this, tree from file is not consistent
|
|
|
|
del tree[top_node]
|
2015-09-03 05:55:05 -05:00
|
|
|
unordered_oids = set(tree.keys())
|
2014-07-23 07:42:33 -05:00
|
|
|
|
|
|
|
# split into two groups, parents and child nodes, and iterate until
|
|
|
|
# child nodes are not empty
|
|
|
|
while unordered_oids:
|
|
|
|
parent_nodes = set()
|
|
|
|
child_nodes = set()
|
|
|
|
|
|
|
|
for node in unordered_oids:
|
|
|
|
if node not in child_nodes:
|
|
|
|
# if node was child once, must remain as child
|
|
|
|
parent_nodes.add(node)
|
|
|
|
|
|
|
|
for child_oid in tree[node]:
|
|
|
|
# iterate over all child nodes stored in tree[node] per node
|
|
|
|
# child node must be removed from parents
|
|
|
|
parent_nodes.discard(child_oid)
|
|
|
|
child_nodes.add(child_oid)
|
|
|
|
|
|
|
|
ordered_oid_groups.append(parent_nodes) # parents nodes are not dependent
|
|
|
|
|
|
|
|
assert len(child_nodes) < len(unordered_oids) # while iteration must be finite
|
|
|
|
unordered_oids = child_nodes # extract new parent nodes in next iteration
|
|
|
|
|
|
|
|
return ordered_oid_groups
|
|
|
|
|
|
|
|
|
2015-03-17 06:23:06 -05:00
|
|
|
def update_schema(schema_files, ldapi=False, dm_password=None,):
|
2013-04-26 08:21:35 -05:00
|
|
|
"""Update schema to match the given ldif files
|
|
|
|
|
|
|
|
Schema elements present in the LDIF files but missing from the DS schema
|
|
|
|
are added.
|
|
|
|
Schema elements that differ between LDIF files and DS schema are updated
|
|
|
|
to match the LDIF files. The comparison ignores tags that python-ldap's
|
|
|
|
schema parser does not understand (such as X-ORIGIN).
|
|
|
|
Extra elements present only in the DS schema are left untouched.
|
|
|
|
|
|
|
|
An X-ORIGIN tag containing the current IPA version is added to all new
|
|
|
|
and updated schema elements.
|
|
|
|
|
|
|
|
:param schema_files: List of filenames to update from
|
|
|
|
:param ldapi: if true, use ldapi to connect
|
|
|
|
:param dm_password: directory manager password
|
|
|
|
|
|
|
|
:return:
|
|
|
|
True if modifications were made
|
|
|
|
"""
|
2014-07-23 07:42:33 -05:00
|
|
|
SCHEMA_ELEMENT_CLASSES_KEYS = [x[0] for x in SCHEMA_ELEMENT_CLASSES]
|
|
|
|
|
2013-04-26 08:21:35 -05:00
|
|
|
conn = connect(ldapi=ldapi, dm_password=dm_password,
|
2015-07-20 09:04:07 -05:00
|
|
|
realm=api.env.realm,
|
2013-04-26 08:21:35 -05:00
|
|
|
fqdn=installutils.get_fqdn())
|
|
|
|
|
|
|
|
old_schema = conn.schema
|
|
|
|
|
2014-07-23 07:42:33 -05:00
|
|
|
|
2013-04-26 08:21:35 -05:00
|
|
|
schema_entry = conn.get_entry(DN(('cn', 'schema')),
|
2014-07-23 07:42:33 -05:00
|
|
|
SCHEMA_ELEMENT_CLASSES_KEYS)
|
2013-04-26 08:21:35 -05:00
|
|
|
|
|
|
|
modified = False
|
|
|
|
|
|
|
|
# The exact representation the DS gives us for each OID
|
|
|
|
# (for debug logging)
|
|
|
|
old_entries_by_oid = {cls(str(attr)).oid: str(attr)
|
2014-07-23 07:42:33 -05:00
|
|
|
for (attrname, cls) in SCHEMA_ELEMENT_CLASSES
|
2013-04-26 08:21:35 -05:00
|
|
|
for attr in schema_entry[attrname]}
|
|
|
|
|
|
|
|
for filename in schema_files:
|
2015-06-08 10:33:11 -05:00
|
|
|
log.debug('Processing schema LDIF file %s', filename)
|
2013-04-26 08:21:35 -05:00
|
|
|
dn, new_schema = ldap.schema.subentry.urlfetch(filename)
|
|
|
|
|
2014-07-23 07:42:33 -05:00
|
|
|
for attrname, cls in SCHEMA_ELEMENT_CLASSES:
|
|
|
|
for oids_set in _get_oid_dependency_order(new_schema, cls):
|
|
|
|
# Set of all elements of this class, as strings given by the DS
|
|
|
|
new_elements = []
|
|
|
|
for oid in oids_set:
|
|
|
|
new_obj = new_schema.get_obj(cls, oid)
|
|
|
|
old_obj = old_schema.get_obj(cls, oid)
|
|
|
|
# Compare python-ldap's sanitized string representations
|
|
|
|
# to see if the value is different
|
|
|
|
# This can give false positives, e.g. with case differences
|
|
|
|
# in case-insensitive names.
|
|
|
|
# But, false positives are harmless (and infrequent)
|
|
|
|
if not old_obj or str(new_obj) != str(old_obj):
|
|
|
|
# Note: An add will automatically replace any existing
|
|
|
|
# schema with the same OID. So, we only add.
|
|
|
|
value = add_x_origin(new_obj)
|
|
|
|
new_elements.append(value)
|
|
|
|
|
|
|
|
if old_obj:
|
|
|
|
old_attr = old_entries_by_oid.get(oid)
|
2015-06-08 10:33:11 -05:00
|
|
|
log.debug('Replace: %s', old_attr)
|
|
|
|
log.debug(' with: %s', value)
|
2014-07-23 07:42:33 -05:00
|
|
|
else:
|
2015-06-08 10:33:11 -05:00
|
|
|
log.debug('Add: %s', value)
|
2014-07-23 07:42:33 -05:00
|
|
|
|
|
|
|
modified = modified or new_elements
|
|
|
|
schema_entry[attrname].extend(new_elements)
|
|
|
|
# we need to iterate schema updates, due to dependencies (SUP)
|
|
|
|
# schema_entry doesn't respect order of objectclasses/attributes
|
|
|
|
# so updates must be executed with groups of independent OIDs
|
|
|
|
if new_elements:
|
|
|
|
modlist = schema_entry.generate_modlist()
|
|
|
|
log.debug("Schema modlist:\n%s", pprint.pformat(modlist))
|
2015-03-17 06:23:06 -05:00
|
|
|
conn.update_entry(schema_entry)
|
2014-07-23 07:42:33 -05:00
|
|
|
|
2015-03-17 06:23:06 -05:00
|
|
|
if not modified:
|
2015-06-08 10:33:11 -05:00
|
|
|
log.debug('Not updating schema')
|
2013-04-26 08:21:35 -05:00
|
|
|
|
|
|
|
return modified
|
|
|
|
|
|
|
|
|
|
|
|
def add_x_origin(element):
|
|
|
|
"""Add X-ORIGIN tag to a schema element if it does not already contain one
|
|
|
|
"""
|
|
|
|
# Note that python-ldap drops X-ORIGIN when it parses schema elements,
|
|
|
|
# so we need to resort to string manipulation
|
|
|
|
element = str(element)
|
|
|
|
if 'X-ORIGIN' not in element:
|
|
|
|
assert element[-2:] == ' )'
|
|
|
|
element = element[:-1] + "X-ORIGIN '%s' )" % ORIGIN
|
|
|
|
return element
|