From 48d6f339fbfc653557f1be33d481ee9e35125ab8 Mon Sep 17 00:00:00 2001 From: Ankit yadav Date: Wed, 5 Apr 2017 04:09:56 +0530 Subject: [PATCH] Ticket 1 - cn=config comparison Bug Description: Once we have enough plugin and index code, we should be able to use dsconf to compare the state of two instances. This will let us check for differing plugin, index, configuration values between two servers. Fix Description: This patch has a working compare function and tests for comparing user objects. https://pagure.io/lib389/issue/1 Author: ankity10 Review by: ??? --- .gitignore | 1 + lib389/_mapped_object.py | 67 ++++++++++++++++++++++++- lib389/idm/user.py | 3 ++ lib389/tests/idm/user_compare_test.py | 92 +++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 5 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 lib389/tests/idm/user_compare_test.py diff --git a/.gitignore b/.gitignore index ccc1a64..5233971 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ logs/ .cache .tox *.egg-info +.idea/ \ No newline at end of file diff --git a/lib389/_mapped_object.py b/lib389/_mapped_object.py index 93aad50..94f89ae 100644 --- a/lib389/_mapped_object.py +++ b/lib389/_mapped_object.py @@ -17,7 +17,7 @@ from lib389.utils import ensure_bytes, ensure_str, ensure_list_bytes, ensure_lis from lib389._entry import Entry from functools import partial - +from sys import getsizeof # This function filter and term generation provided thanks to # The University of Adelaide. @@ -76,6 +76,7 @@ class DSLogging(object): class DSLdapObject(DSLogging): + # TODO: Automatically create objects when they are requested to have properties added def __init__(self, instance, dn=None, batch=False): """ @@ -93,6 +94,8 @@ class DSLdapObject(DSLogging): self._create_objectclasses = [] self._rdn_attribute = None self._must_attributes = None + # attributes, we don't want to compare + self._compare_exclude = [] def __unicode__(self): val = self._dn @@ -225,6 +228,66 @@ class DSLdapObject(DSLogging): mod_list.append((action, key, value)) return self._instance.modify_s(self._dn, mod_list) + @classmethod + def compare(cls, obj1, obj2): + """ + Compare if two RDN objects have same attributes and values. + This comparison is a loose comparison, not a strict one i.e. "this object *is* this other object" + It will just check if the attributes are same. + 'nsUniqueId' attribute is not checked intentionally because we want to compare arbitrary objects + i.e they may have different 'nsUniqueId' but same attributes. + Example: + cn=user1,ou=a + cn=user1,ou=b + Comparision of these two objects should result in same, even though their 'nsUniqueId' attribute differs. + This function returns 'True' if objects have same attributes else returns 'False' + """ + # ensuring both the objects are RDN objects + if not issubclass(type(obj1), DSLdapObject) or not issubclass(type(obj2), DSLdapObject): + raise ValueError("Invalid arguments: Expecting object types that inherits 'DSLdapObject' class") + # check if RDN of objects is same + if obj1.rdn != obj2.rdn: + return False + obj1_attrs = obj1.get_compare_attrs() + obj2_attrs = obj2.get_compare_attrs() + # Bail fast if the size doesn't match + if getsizeof(obj1_attrs) != getsizeof(obj2_attrs): + return False + # Bail fast if the keys don't match + if set(obj1_attrs.keys()) != set(obj2_attrs.keys()): + return False + # Check the values of each key + # using obj1_attrs.keys() because obj1_attrs.iterkleys() is not supported in python3 + for key in obj1_attrs.keys(): + if set(obj1_attrs[key]) != set(obj2_attrs[key]): + return False + return True + + def get_compare_attrs(self): + """ + Get a dictionary having attributes to be compared i.e. excluding self._compare_exclude + """ + self._log.debug("%s get_compare_attrs" % (self._dn)) + all_attrs_dict = self.get_all_attrs() + # removing _compate_exclude attrs from all attrs + compare_attrs = set(all_attrs_dict.keys()) - set(self._compare_exclude) + compare_attrs_dict = {attr:all_attrs_dict[attr] for attr in compare_attrs} + return compare_attrs_dict + + def get_all_attrs(self): + """ + Get a dictionary having all the attributes i.e. real attributes + operational attributes + """ + self._log.debug("%s get_all_attrs" % (self._dn)) + if self._instance.state != DIRSRV_STATE_ONLINE: + raise ValueError("Invalid state. Cannot get properties on instance that is not ONLINE") + else: + # retrieving real(*) and operational attributes(+) + attrs_entry = self._instance.getEntry(self._dn, ldap.SCOPE_BASE, "(objectclass=*)", ["*", "+"]) + # getting dict from 'entry' object + attrs_dict = attrs_entry.data + return attrs_dict + def get_attrs_vals(self, keys): self._log.debug("%s get_attrs_vals(%r)" % (self._dn, keys)) if self._instance.state != DIRSRV_STATE_ONLINE: @@ -498,7 +561,7 @@ class DSLdapObjects(DSLogging): # Create the object # Should we inject the rdn to properties? # This may not work in all cases, especially when we consider plugins. - # + # co = self._entry_to_instance(dn=None, entry=None) # Make the rdn naming attr avaliable self._rdn_attribute = co._rdn_attribute diff --git a/lib389/idm/user.py b/lib389/idm/user.py index 5b7e4be..dba0343 100644 --- a/lib389/idm/user.py +++ b/lib389/idm/user.py @@ -18,7 +18,9 @@ MUST_ATTRIBUTES = [ ] RDN = 'uid' + class UserAccount(DSLdapObject): + def __init__(self, instance, dn=None, batch=False): super(UserAccount, self).__init__(instance, dn, batch) self._rdn_attribute = RDN @@ -36,6 +38,7 @@ class UserAccount(DSLdapObject): # Can we get this into core? # 'ldapPublicKey', ] + self._compare_exclude = ['nsuniqueid'] self._protected = False def _validate(self, rdn, properties, basedn): diff --git a/lib389/tests/idm/user_compare_test.py b/lib389/tests/idm/user_compare_test.py new file mode 100644 index 0000000..900d980 --- /dev/null +++ b/lib389/tests/idm/user_compare_test.py @@ -0,0 +1,92 @@ +import os +import sys +import time +import ldap +import logging +import pytest +from lib389._constants import * +from lib389.properties import * +from lib389.tasks import * +from lib389.utils import * + +from lib389.idm.group import Groups +from lib389.idm.user import UserAccounts, UserAccount + +from lib389.topologies import topology_st as topology + +DEBUGGING = os.getenv('DEBUGGING', False) + +if DEBUGGING is not False: + DEBUGGING = True + +if DEBUGGING: + logging.getLogger(__name__).setLevel(logging.DEBUG) +else: + logging.getLogger(__name__).setLevel(logging.INFO) + +log = logging.getLogger(__name__) + + +def test_user_compare(topology): + """ + Testing compare function + 1. Testing comparison of two different users. + 2. Testing comparison of 'str' object with itself, should raise 'ValueError'. + 3. Testing comparison of user with similar user (different object id). + 4. Testing comparison of user with group. + """ + if DEBUGGING: + # Add debugging steps(if any)... + pass + + users = UserAccounts(topology.standalone, DEFAULT_SUFFIX) + groups = Groups(topology.standalone, DEFAULT_SUFFIX) + # Create 1st user + user1_properties = { + 'uid': 'testuser1', + 'cn': 'testuser1', + 'sn': 'user', + 'uidNumber': '1000', + 'gidNumber': '2000', + 'homeDirectory': '/home/testuser1' + } + + users.create(properties=user1_properties) + testuser1 = users.get('testuser1') + # Create 2nd user + user2_properties = { + 'uid': 'testuser2', + 'cn': 'testuser2', + 'sn': 'user', + 'uidNumber': '1001', + 'gidNumber': '2002', + 'homeDirectory': '/home/testuser2' + } + + users.create(properties=user2_properties) + testuser2 = users.get('testuser2') + # create group + group_properties = { + 'cn' : 'group1', + 'description' : 'testgroup' + } + + testuser1_copy = users.get("testuser1") + group = groups.create(properties=group_properties) + + assert(UserAccount.compare(testuser1, testuser2) == False) + + with pytest.raises(ValueError): + UserAccount.compare("test_str_object","test_str_object") + + assert(UserAccount.compare(testuser1, testuser1_copy) == True) + assert(UserAccount.compare(testuser1, group) == False) + + log.info("Test PASSED") + + +if __name__ == '__main__': + # Run isolated + # -s for DEBUG mode + CURRENT_FILE = os.path.realpath(__file__) + pytest.main("-s %s" % CURRENT_FILE) diff --git a/requirements.txt b/requirements.txt index ff6b228..c4b391f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ pyasn1-modules six pytest python-dateutil +deepdiff \ No newline at end of file -- 2.9.3