s4:samba3.py - ignore comments in "smb.conf" files
[ira/wip.git] / source4 / scripting / python / samba / samba3.py
index 40443bd8babcc284df6fda5a042ae1277bbcdbed..c21b457566f501c56805dd7465ebe51fbe26b4d4 100644 (file)
 
 """Support for reading Samba 3 data files."""
 
+__docformat__ = "restructuredText"
+
 REGISTRY_VALUE_PREFIX = "SAMBA_REGVAL"
 REGISTRY_DB_VERSION = 1
 
 import os
+import struct
 import tdb
 
-class Registry:
-    """Simple read-only support for reading the Samba3 registry."""
+
+def fetch_uint32(tdb, key):
+    try:
+        data = tdb[key]
+    except KeyError:
+        return None
+    assert len(data) == 4
+    return struct.unpack("<L", data)[0]
+
+
+def fetch_int32(tdb, key):
+    try:
+        data = tdb[key]
+    except KeyError:
+        return None
+    assert len(data) == 4
+    return struct.unpack("<l", data)[0]
+
+
+class TdbDatabase(object):
+    """Simple Samba 3 TDB database reader."""
     def __init__(self, file):
+        """Open a file.
+
+        :param file: Path of the file to open.
+        """
         self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
+        self._check_version()
+
+    def _check_version(self):
+        pass
 
     def close(self):
+        """Close resources associated with this object."""
         self.tdb.close()
 
+
+class Registry(TdbDatabase):
+    """Simple read-only support for reading the Samba3 registry.
+    
+    :note: This object uses the same syntax for registry key paths as 
+        Samba 3. This particular format uses forward slashes for key path 
+        separators and abbreviations for the predefined key names. 
+        e.g.: HKLM/Software/Bar.
+    """
     def __len__(self):
         """Return the number of keys."""
         return len(self.keys())
 
     def keys(self):
         """Return list with all the keys."""
-        return [k.rstrip("\x00") for k in self.tdb.keys() if not k.startswith(REGISTRY_VALUE_PREFIX)]
+        return [k.rstrip("\x00") for k in self.tdb.iterkeys() if not k.startswith(REGISTRY_VALUE_PREFIX)]
 
     def subkeys(self, key):
+        """Retrieve the subkeys for the specified key.
+
+        :param key: Key path.
+        :return: list with key names
+        """
         data = self.tdb.get("%s\x00" % key)
         if data is None:
             return []
-        # FIXME: Parse data
-        return []
+        (num, ) = struct.unpack("<L", data[0:4])
+        keys = data[4:].split("\0")
+        assert keys[-1] == ""
+        keys.pop()
+        assert len(keys) == num
+        return keys
 
     def values(self, key):
-        """Return a dictionary with the values set for a specific key."""
+        """Return a dictionary with the values set for a specific key.
+        
+        :param key: Key to retrieve values for.
+        :return: Dictionary with value names as key, tuple with type and 
+            data as value."""
         data = self.tdb.get("%s/%s\x00" % (REGISTRY_VALUE_PREFIX, key))
         if data is None:
             return {}
-        # FIXME: Parse data
-        return {}
+        ret = {}
+        (num, ) = struct.unpack("<L", data[0:4])
+        data = data[4:]
+        for i in range(num):
+            # Value name
+            (name, data) = data.split("\0", 1)
+
+            (type, ) = struct.unpack("<L", data[0:4])
+            data = data[4:]
+            (value_len, ) = struct.unpack("<L", data[0:4])
+            data = data[4:]
+
+            ret[name] = (type, data[:value_len])
+            data = data[value_len:]
 
+        return ret
 
-class PolicyDatabase:
+
+class PolicyDatabase(TdbDatabase):
+    """Samba 3 Account Policy database reader."""
     def __init__(self, file):
-        self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
-        self.min_password_length = self.tdb.fetch_uint32("min password length\x00")
-        self.password_history = self.tdb.fetch_uint32("password history\x00")
-        self.user_must_logon_to_change_password = self.tdb.fetch_uint32("user must logon to change pasword\x00")
-        self.maximum_password_age = self.tdb.fetch_uint32("maximum password age\x00")
-        self.minimum_password_age = self.tdb.fetch_uint32("minimum password age\x00")
-        self.lockout_duration = self.tdb.fetch_uint32("lockout duration\x00")
-        self.reset_count_minutes = self.tdb.fetch_uint32("reset count minutes\x00")
-        self.bad_lockout_minutes = self.tdb.fetch_uint32("bad lockout minutes\x00")
-        self.disconnect_time = self.tdb.fetch_int32("disconnect time\x00")
-        self.refuse_machine_password_change = self.tdb.fetch_uint32("refuse machine password change\x00")
+        """Open a policy database
+        
+        :param file: Path to the file to open.
+        """
+        super(PolicyDatabase, self).__init__(file)
+        self.min_password_length = fetch_uint32(self.tdb, "min password length\x00")
+        self.password_history = fetch_uint32(self.tdb, "password history\x00")
+        self.user_must_logon_to_change_password = fetch_uint32(self.tdb, "user must logon to change pasword\x00")
+        self.maximum_password_age = fetch_uint32(self.tdb, "maximum password age\x00")
+        self.minimum_password_age = fetch_uint32(self.tdb, "minimum password age\x00")
+        self.lockout_duration = fetch_uint32(self.tdb, "lockout duration\x00")
+        self.reset_count_minutes = fetch_uint32(self.tdb, "reset count minutes\x00")
+        self.bad_lockout_minutes = fetch_uint32(self.tdb, "bad lockout minutes\x00")
+        self.disconnect_time = fetch_int32(self.tdb, "disconnect time\x00")
+        self.refuse_machine_password_change = fetch_uint32(self.tdb, "refuse machine password change\x00")
 
         # FIXME: Read privileges as well
 
-    def close(self):
-        self.tdb.close()
-
 
 GROUPDB_DATABASE_VERSION_V1 = 1 # native byte format.
 GROUPDB_DATABASE_VERSION_V2 = 2 # le format.
@@ -88,24 +157,40 @@ GROUP_PREFIX = "UNIXGROUP/"
 # hanging of the member as key.
 MEMBEROF_PREFIX = "MEMBEROF/"
 
-class GroupMappingDatabase:
-    def __init__(self, file): 
-        self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
-        assert self.tdb.fetch_int32("INFO/version\x00") in (GROUPDB_DATABASE_VERSION_V1, GROUPDB_DATABASE_VERSION_V2)
+class GroupMappingDatabase(TdbDatabase):
+    """Samba 3 group mapping database reader."""
+    def _check_version(self):
+        assert fetch_int32(self.tdb, "INFO/version\x00") in (GROUPDB_DATABASE_VERSION_V1, GROUPDB_DATABASE_VERSION_V2)
 
     def groupsids(self):
-        for k in self.tdb.keys():
+        """Retrieve the SIDs for the groups in this database.
+
+        :return: List with sids as strings.
+        """
+        for k in self.tdb.iterkeys():
             if k.startswith(GROUP_PREFIX):
                 yield k[len(GROUP_PREFIX):].rstrip("\0")
 
+    def get_group(self, sid):
+        """Retrieve the group mapping information for a particular group.
+
+        :param sid: SID of the group
+        :return: None if the group can not be found, otherwise 
+            a tuple with gid, sid_name_use, the NT name and comment.
+        """
+        data = self.tdb.get("%s%s\0" % (GROUP_PREFIX, sid))
+        if data is None:
+            return data
+        (gid, sid_name_use) = struct.unpack("<lL", data[0:8])
+        (nt_name, comment, _) = data[8:].split("\0")
+        return (gid, sid_name_use, nt_name, comment)
+
     def aliases(self):
-        for k in self.tdb.keys():
+        """Retrieve the aliases in this database."""
+        for k in self.tdb.iterkeys():
             if k.startswith(MEMBEROF_PREFIX):
                 yield k[len(MEMBEROF_PREFIX):].rstrip("\0")
 
-    def close(self):
-        self.tdb.close()
-
 
 # High water mark keys
 IDMAP_HWM_GROUP = "GROUP HWM\0"
@@ -117,22 +202,29 @@ IDMAP_USER_PREFIX = "UID "
 # idmap version determines auto-conversion
 IDMAP_VERSION_V2 = 2
 
-class IdmapDatabase:
-    def __init__(self, file):
-        self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
-        assert self.tdb.fetch_int32("IDMAP_VERSION\0") == IDMAP_VERSION_V2
+class IdmapDatabase(TdbDatabase):
+    """Samba 3 ID map database reader."""
+    def _check_version(self):
+        assert fetch_int32(self.tdb, "IDMAP_VERSION\0") == IDMAP_VERSION_V2
 
     def uids(self):
-        for k in self.tdb.keys():
+        """Retrieve a list of all uids in this database."""
+        for k in self.tdb.iterkeys():
             if k.startswith(IDMAP_USER_PREFIX):
                 yield int(k[len(IDMAP_USER_PREFIX):].rstrip("\0"))
 
     def gids(self):
-        for k in self.tdb.keys():
+        """Retrieve a list of all gids in this database."""
+        for k in self.tdb.iterkeys():
             if k.startswith(IDMAP_GROUP_PREFIX):
                 yield int(k[len(IDMAP_GROUP_PREFIX):].rstrip("\0"))
 
     def get_user_sid(self, uid):
+        """Retrieve the SID associated with a particular uid.
+
+        :param uid: UID to retrieve SID for.
+        :return: A SID or None if no mapping was found.
+        """
         data = self.tdb.get("%s%d\0" % (IDMAP_USER_PREFIX, uid))
         if data is None:
             return data
@@ -145,19 +237,16 @@ class IdmapDatabase:
         return data.rstrip("\0")
 
     def get_user_hwm(self):
-        return self.tdb.fetch_uint32(IDMAP_HWM_USER)
+        """Obtain the user high-water mark."""
+        return fetch_uint32(self.tdb, IDMAP_HWM_USER)
 
     def get_group_hwm(self):
-        return self.tdb.fetch_uint32(IDMAP_HWM_GROUP)
-
-    def close(self):
-        self.tdb.close()
-
+        """Obtain the group high-water mark."""
+        return fetch_uint32(self.tdb, IDMAP_HWM_GROUP)
 
-class SecretsDatabase:
-    def __init__(self, file):
-        self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
 
+class SecretsDatabase(TdbDatabase):
+    """Samba 3 Secrets database reader."""
     def get_auth_password(self):
         return self.tdb.get("SECRETS/AUTH_PASSWORD")
 
@@ -167,16 +256,20 @@ class SecretsDatabase:
     def get_auth_user(self):
         return self.tdb.get("SECRETS/AUTH_USER")
 
-    def get_dom_guid(self, host):
+    def get_domain_guid(self, host):
         return self.tdb.get("SECRETS/DOMGUID/%s" % host)
 
     def ldap_dns(self):
-        for k in self.tdb.keys():
+        for k in self.tdb.iterkeys():
             if k.startswith("SECRETS/LDAP_BIND_PW/"):
                 yield k[len("SECRETS/LDAP_BIND_PW/"):].rstrip("\0")
 
     def domains(self):
-        for k in self.tdb.keys():
+        """Iterate over domains in this database.
+
+        :return: Iterator over the names of domains in this database.
+        """
+        for k in self.tdb.iterkeys():
             if k.startswith("SECRETS/SID/"):
                 yield k[len("SECRETS/SID/"):].rstrip("\0")
 
@@ -187,10 +280,10 @@ class SecretsDatabase:
         return self.tdb.get("SECRETS/AFS_KEYFILE/%s" % host)
 
     def get_machine_sec_channel_type(self, host):
-        return self.tdb.fetch_uint32("SECRETS/MACHINE_SEC_CHANNEL_TYPE/%s" % host)
+        return fetch_uint32(self.tdb, "SECRETS/MACHINE_SEC_CHANNEL_TYPE/%s" % host)
 
     def get_machine_last_change_time(self, host):
-        return self.tdb.fetch_uint32("SECRETS/MACHINE_LAST_CHANGE_TIME/%s" % host)
+        return fetch_uint32(self.tdb, "SECRETS/MACHINE_LAST_CHANGE_TIME/%s" % host)
             
     def get_machine_password(self, host):
         return self.tdb.get("SECRETS/MACHINE_PASSWORD/%s" % host)
@@ -202,7 +295,7 @@ class SecretsDatabase:
         return self.tdb.get("SECRETS/$DOMTRUST.ACC/%s" % host)
 
     def trusted_domains(self):
-        for k in self.tdb.keys():
+        for k in self.tdb.iterkeys():
             if k.startswith("SECRETS/$DOMTRUST.ACC/"):
                 yield k[len("SECRETS/$DOMTRUST.ACC/"):].rstrip("\0")
 
@@ -212,25 +305,39 @@ class SecretsDatabase:
     def get_sid(self, host):
         return self.tdb.get("SECRETS/SID/%s" % host.upper())
 
-    def close(self):
-        self.tdb.close()
-
 
 SHARE_DATABASE_VERSION_V1 = 1
 SHARE_DATABASE_VERSION_V2 = 2
 
-class ShareInfoDatabase:
-    def __init__(self, file):
-        self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
-        assert self.tdb.fetch_int32("INFO/version") in (SHARE_DATABASE_VERSION_V1, SHARE_DATABASE_VERSION_V2)
+class ShareInfoDatabase(TdbDatabase):
+    """Samba 3 Share Info database reader."""
+    def _check_version(self):
+        assert fetch_int32(self.tdb, "INFO/version\0") in (SHARE_DATABASE_VERSION_V1, SHARE_DATABASE_VERSION_V2)
 
     def get_secdesc(self, name):
+        """Obtain the security descriptor on a particular share.
+        
+        :param name: Name of the share
+        """
         secdesc = self.tdb.get("SECDESC/%s" % name)
         # FIXME: Run ndr_pull_security_descriptor
         return secdesc
 
-    def close(self):
-        self.tdb.close()
+
+class Shares(object):
+    """Container for share objects."""
+    def __init__(self, lp, shareinfo):
+        self.lp = lp
+        self.shareinfo = shareinfo
+
+    def __len__(self):
+        """Number of shares."""
+        return len(self.lp) - 1
+
+    def __iter__(self):
+        """Iterate over the share names."""
+        return self.lp.__iter__()
+
 
 ACB_DISABLED = 0x00000001
 ACB_HOMDIRREQ = 0x00000002
@@ -253,21 +360,26 @@ ACB_PW_EXPIRED = 0x00020000
 ACB_NO_AUTH_DATA_REQD = 0x00080000
 
 acb_info_mapping = {
-        'N': ACB_PWNOTREQ,  # 'N'o password. 
+        'N': ACB_PWNOTREQ,  # 'N'o password.
         'D': ACB_DISABLED,  # 'D'isabled.
-               'H': ACB_HOMDIRREQ, # 'H'omedir required.
-               'T': ACB_TEMPDUP,   # 'T'emp account.
-               'U': ACB_NORMAL,    # 'U'ser account (normal).
-               'M': ACB_MNS,       # 'M'NS logon user account. What is this ?
-               'W': ACB_WSTRUST,   # 'W'orkstation account.
-               'S': ACB_SVRTRUST,  # 'S'erver account. 
-               'L': ACB_AUTOLOCK,  # 'L'ocked account.
-               'X': ACB_PWNOEXP,   # No 'X'piry on password
-               'I': ACB_DOMTRUST,  # 'I'nterdomain trust account.
+        'H': ACB_HOMDIRREQ, # 'H'omedir required.
+        'T': ACB_TEMPDUP,   # 'T'emp account.
+        'U': ACB_NORMAL,    # 'U'ser account (normal).
+        'M': ACB_MNS,       # 'M'NS logon user account. What is this ?
+        'W': ACB_WSTRUST,   # 'W'orkstation account.
+        'S': ACB_SVRTRUST,  # 'S'erver account. 
+        'L': ACB_AUTOLOCK,  # 'L'ocked account.
+        'X': ACB_PWNOEXP,   # No 'X'piry on password
+        'I': ACB_DOMTRUST,  # 'I'nterdomain trust account.
         ' ': 0
         }
 
 def decode_acb(text):
+    """Decode a ACB field.
+
+    :param text: ACB text
+    :return: integer with flags set.
+    """
     assert not "[" in text and not "]" in text
     ret = 0
     for x in text:
@@ -275,7 +387,59 @@ def decode_acb(text):
     return ret
 
 
-class SmbpasswdFile:
+class SAMUser(object):
+    """Samba 3 SAM User.
+    
+    :note: Unknown or unset fields are set to None.
+    """
+    def __init__(self, name, uid=None, lm_password=None, nt_password=None, acct_ctrl=None, 
+                 last_change_time=None, nt_username=None, fullname=None, logon_time=None, logoff_time=None,
+                 acct_desc=None, group_rid=None, bad_password_count=None, logon_count=None,
+                 domain=None, dir_drive=None, munged_dial=None, homedir=None, logon_script=None,
+                 profile_path=None, workstations=None, kickoff_time=None, bad_password_time=None,
+                 pass_last_set_time=None, pass_can_change_time=None, pass_must_change_time=None,
+                 user_rid=None, unknown_6=None, nt_password_history=None,
+                 unknown_str=None, hours=None, logon_divs=None):
+        self.username = name
+        self.uid = uid
+        self.lm_password = lm_password
+        self.nt_password = nt_password
+        self.acct_ctrl = acct_ctrl
+        self.pass_last_set_time = last_change_time
+        self.nt_username = nt_username
+        self.fullname = fullname
+        self.logon_time = logon_time
+        self.logoff_time = logoff_time
+        self.acct_desc = acct_desc
+        self.group_rid = group_rid
+        self.bad_password_count = bad_password_count
+        self.logon_count = logon_count
+        self.domain = domain
+        self.dir_drive = dir_drive
+        self.munged_dial = munged_dial
+        self.homedir = homedir
+        self.logon_script = logon_script
+        self.profile_path = profile_path
+        self.workstations = workstations
+        self.kickoff_time = kickoff_time
+        self.bad_password_time = bad_password_time
+        self.pass_can_change_time = pass_can_change_time
+        self.pass_must_change_time = pass_must_change_time
+        self.user_rid = user_rid
+        self.unknown_6 = unknown_6
+        self.nt_password_history = nt_password_history
+        self.unknown_str = unknown_str
+        self.hours = hours
+        self.logon_divs = logon_divs
+
+    def __eq__(self, other): 
+        if not isinstance(other, SAMUser):
+            return False
+        return self.__dict__ == other.__dict__
+
+
+class SmbpasswdFile(object):
+    """Samba 3 smbpasswd file reader."""
     def __init__(self, file):
         self.users = {}
         f = open(file, 'r')
@@ -312,7 +476,7 @@ class SmbpasswdFile:
                     acct_ctrl &= ~ACB_NORMAL
                     acct_ctrl |= ACB_WSTRUST
 
-            self.users[username] = (uid, lm_password, nt_password, acct_ctrl, last_change_time)
+            self.users[username] = SAMUser(username, uid, lm_password, nt_password, acct_ctrl, last_change_time)
 
         f.close()
 
@@ -323,7 +487,7 @@ class SmbpasswdFile:
         return self.users[name]
 
     def __iter__(self):
-        return iter(self.entries)
+        return iter(self.users)
 
     def close(self): # For consistency
         pass
@@ -335,19 +499,105 @@ TDBSAM_FORMAT_STRING_V2 = "dddddddBBBBBBBBBBBBddBBBwwdBwwd"
 TDBSAM_USER_PREFIX = "USER_"
 
 
-class TdbSam:
-    def __init__(self, file):
-        self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
-        self.version = self.tdb.fetch_uint32("INFO/version") or 0
-        assert self.version in (0, 1, 2)
+class LdapSam(object):
+    """Samba 3 LDAP passdb backend reader."""
+    def __init__(self, url):
+        self.ldap_url = url
+
+
+class TdbSam(TdbDatabase):
+    """Samba 3 TDB passdb backend reader."""
+    def _check_version(self):
+        self.version = fetch_uint32(self.tdb, "INFO/version\0") or 0
+        assert self.version in (0, 1, 2, 3)
 
     def usernames(self):
-        for k in self.tdb.keys():
+        """Iterate over the usernames in this Tdb database."""
+        for k in self.tdb.iterkeys():
             if k.startswith(TDBSAM_USER_PREFIX):
                 yield k[len(TDBSAM_USER_PREFIX):].rstrip("\0")
 
-    def close(self):
-        self.tdb.close()
+    __iter__ = usernames
+    
+    def __getitem__(self, name):
+        data = self.tdb["%s%s\0" % (TDBSAM_USER_PREFIX, name)]
+        user = SAMUser(name)
+    
+        def unpack_string(data):
+            (length, ) = struct.unpack("<L", data[:4])
+            data = data[4:]
+            if length == 0:
+                return (None, data)
+            return (data[:length].rstrip("\0"), data[length:])
+
+        def unpack_int32(data):
+            (value, ) = struct.unpack("<l", data[:4])
+            return (value, data[4:])
+
+        def unpack_uint32(data):
+            (value, ) = struct.unpack("<L", data[:4])
+            return (value, data[4:])
+
+        def unpack_uint16(data):
+            (value, ) = struct.unpack("<H", data[:2])
+            return (value, data[2:])
+
+        (logon_time, data) = unpack_int32(data)
+        (logoff_time, data) = unpack_int32(data)
+        (kickoff_time, data) = unpack_int32(data)
+
+        if self.version > 0:
+            (bad_password_time, data) = unpack_int32(data)
+            if bad_password_time != 0:
+                user.bad_password_time = bad_password_time
+        (pass_last_set_time, data) = unpack_int32(data)
+        (pass_can_change_time, data) = unpack_int32(data)
+        (pass_must_change_time, data) = unpack_int32(data)
+
+        if logon_time != 0:
+            user.logon_time = logon_time
+        user.logoff_time = logoff_time
+        user.kickoff_time = kickoff_time
+        if pass_last_set_time != 0:
+            user.pass_last_set_time = pass_last_set_time
+        user.pass_can_change_time = pass_can_change_time
+
+        (user.username, data) = unpack_string(data)
+        (user.domain, data) = unpack_string(data)
+        (user.nt_username, data) = unpack_string(data)
+        (user.fullname, data) = unpack_string(data)
+        (user.homedir, data) = unpack_string(data)
+        (user.dir_drive, data) = unpack_string(data)
+        (user.logon_script, data) = unpack_string(data)
+        (user.profile_path, data) = unpack_string(data)
+        (user.acct_desc, data) = unpack_string(data)
+        (user.workstations, data) = unpack_string(data)
+        (user.unknown_str, data) = unpack_string(data)
+        (user.munged_dial, data) = unpack_string(data)
+
+        (user.user_rid, data) = unpack_int32(data)
+        (user.group_rid, data) = unpack_int32(data)
+
+        (user.lm_password, data) = unpack_string(data)
+        (user.nt_password, data) = unpack_string(data)
+
+        if self.version > 1:
+            (user.nt_password_history, data) = unpack_string(data)
+
+        (user.acct_ctrl, data) = unpack_uint16(data)
+        (_, data) = unpack_uint32(data) # remove_me field
+        (user.logon_divs, data) = unpack_uint16(data)
+        (hours, data) = unpack_string(data)
+        user.hours = []
+        for entry in hours:
+            for i in range(8):
+                user.hours.append(ord(entry) & (2 ** i) == (2 ** i))
+        # FIXME
+        #(user.bad_password_count, data) = unpack_uint16(data)
+        #(user.logon_count, data) = unpack_uint16(data)
+        #(user.unknown_6, data) = unpack_uint32(data)
+        assert len(data) == 0
+        return user
 
 
 def shellsplit(text):
@@ -372,7 +622,8 @@ def shellsplit(text):
     return ret
 
 
-class WinsDatabase:
+class WinsDatabase(object):
+    """Samba 3 WINS database reader."""
     def __init__(self, file):
         self.entries = {}
         f = open(file, 'r')
@@ -388,7 +639,7 @@ class WinsDatabase:
             while "." in entries[i]:
                 ips.append(entries[i])
                 i+=1
-            nb_flags = entries[i]
+            nb_flags = int(entries[i][:-1], 16)
             assert not name in self.entries, "Name %s exists twice" % name
             self.entries[name] = (ttl, ips, nb_flags)
         f.close()
@@ -402,31 +653,141 @@ class WinsDatabase:
     def __iter__(self):
         return iter(self.entries)
 
+    def items(self):
+        """Return the entries in this WINS database."""
+        return self.entries.items()
+
     def close(self): # for consistency
         pass
 
-class Samba3:
+
+class ParamFile(object):
+    """Simple smb.conf-compatible file parser
+
+    Does not use a parameter table, unlike the "normal".
+    """
+
+    def __init__(self, sections=None):
+        self._sections = sections or {}
+
+    def _sanitize_name(self, name):
+        return name.strip().lower().replace(" ","")
+
+    def __repr__(self):
+        return "ParamFile(%r)" % self._sections
+
+    def read(self, filename):
+        """Read a file.
+
+        :param filename: Path to the file
+        """
+        section = None
+        for i, l in enumerate(open(filename, 'r').xreadlines()):
+            l = l.strip()
+            if not l or l[0] == '#' or l[0] == ';':
+                continue
+            if l[0] == "[" and l[-1] == "]":
+                section = self._sanitize_name(l[1:-1])
+                self._sections.setdefault(section, {})
+            elif "=" in l:
+               (k, v) = l.split("=", 1) 
+               self._sections[section][self._sanitize_name(k)] = v
+            else:
+                raise Exception("Unable to parser line %d: %r" % (i+1,l))
+
+    def get(self, param, section=None):
+        """Return the value of a parameter.
+
+        :param param: Parameter name
+        :param section: Section name, defaults to "global"
+        :return: parameter value as string if found, None otherwise.
+        """
+        if section is None:
+            section = "global"
+        section = self._sanitize_name(section)
+        if not section in self._sections:
+            return None
+        param = self._sanitize_name(param)
+        if not param in self._sections[section]:
+            return None
+        return self._sections[section][param].strip()
+
+    def __getitem__(self, section):
+        return self._sections[section]
+
+    def get_section(self, section):
+        return self._sections.get(section)
+
+    def add_section(self, section):
+        self._sections[self._sanitize_name(section)] = {}
+
+    def set_string(self, name, value):
+        self._sections["global"][name] = value
+
+    def get_string(self, name):
+        return self._sections["global"].get(name)
+
+
+class Samba3(object):
+    """Samba 3 configuration and state data reader."""
     def __init__(self, libdir, smbconfpath):
+        """Open the configuration and data for a Samba 3 installation.
+
+        :param libdir: Library directory
+        :param smbconfpath: Path to the smb.conf file.
+        """
         self.smbconfpath = smbconfpath
         self.libdir = libdir
+        self.lp = ParamFile()
+        self.lp.read(self.smbconfpath)
+
+    def libdir_path(self, path):
+        if path[0] == "/" or path[0] == ".":
+            return path
+        return os.path.join(self.libdir, path)
+
+    def get_conf(self):
+        return self.lp
+
+    def get_sam_db(self):
+        lp = self.get_conf()
+        backends = (lp.get("passdb backend") or "").split(" ")
+        if ":" in backends[0]:
+            (name, location) = backends[0].split(":", 2)
+        else:
+            name = backends[0]
+            location = None
+        if name == "smbpasswd":
+            return SmbpasswdFile(self.libdir_path(location or "smbpasswd"))
+        elif name == "tdbsam":
+            return TdbSam(self.libdir_path(location or "passdb.tdb"))
+        elif name == "ldapsam":
+            if location is not None:
+                return LdapSam("ldap:%s" % location)
+            return LdapSam(lp.get("ldap server"))
+        else:
+            raise NotImplementedError("unsupported passdb backend %s" % backends[0])
 
     def get_policy_db(self):
-        return PolicyDatabase(os.path.join(self.libdir, "account_policy.tdb"))
+        return PolicyDatabase(self.libdir_path("account_policy.tdb"))
     
     def get_registry(self):
-        return Registry(os.path.join(self.libdir, "registry.tdb"))
+        return Registry(self.libdir_path("registry.tdb"))
 
     def get_secrets_db(self):
-        return SecretsDatabase(os.path.join(self.libdir, "secrets.tdb"))
+        return SecretsDatabase(self.libdir_path("secrets.tdb"))
 
-    def get_shares_db(self):
-        return ShareInfoDatabase(os.path.join(self.libdir, "share_info.tdb"))
+    def get_shareinfo_db(self):
+        return ShareInfoDatabase(self.libdir_path("share_info.tdb"))
 
     def get_idmap_db(self):
-        return IdmapDatabase(os.path.join(self.libdir, "winbindd_idmap.tdb"))
+        return IdmapDatabase(self.libdir_path("winbindd_idmap.tdb"))
 
     def get_wins_db(self):
-        return WinsDatabase(os.path.join(self.libdir, "wins.dat"))
+        return WinsDatabase(self.libdir_path("wins.dat"))
+
+    def get_shares(self):
+        return Shares(self.get_conf(), self.get_shareinfo_db())
 
     def get_groupmapping_db(self):
-        return GroupMappingDatabase(os.path.join(self.libdir, "group_mapping.tdb"))
+        return GroupMappingDatabase(self.libdir_path("group_mapping.tdb"))