From 6817c5d885a75a1af0e646827c3fa8220df8c7a5 Mon Sep 17 00:00:00 2001 From: Jelmer Vernooij Date: Sat, 29 Dec 2007 18:14:15 -0600 Subject: r26628: python: Add more documentation, simplify code in Samba3 module. (This used to be commit 3c329ee73d9979236313c37e51750ec06b8dd69e) --- source4/scripting/python/config.mk | 2 +- source4/scripting/python/samba/__init__.py | 8 ++ source4/scripting/python/samba/getopt.py | 2 +- source4/scripting/python/samba/provision.py | 96 ++++++++++++++-------- source4/scripting/python/samba/samba3.py | 100 +++++++++++++++-------- source4/scripting/python/samba/samdb.py | 50 ++++++++---- source4/scripting/python/samba/tests/__init__.py | 4 + 7 files changed, 175 insertions(+), 87 deletions(-) diff --git a/source4/scripting/python/config.mk b/source4/scripting/python/config.mk index 4a531f5062..cfd179aff5 100644 --- a/source4/scripting/python/config.mk +++ b/source4/scripting/python/config.mk @@ -30,7 +30,7 @@ realdistclean:: pythonmods: $(PYTHON_DSOS) -PYDOCTOR_MODULES=bin/python/ldb.py bin/python/auth.py bin/python/credentials.py bin/python/registry.py bin/python/tdb.py bin/python/security.py +PYDOCTOR_MODULES=bin/python/ldb.py bin/python/auth.py bin/python/credentials.py bin/python/registry.py bin/python/tdb.py bin/python/security.py bin/python/events.py bin/python/net.py pydoctor:: pythonmods LD_LIBRARY_PATH=bin/shared PYTHONPATH=bin/python pydoctor --make-html --docformat=restructuredtext --add-package scripting/python/samba/ $(addprefix --add-module , $(PYDOCTOR_MODULES)) diff --git a/source4/scripting/python/samba/__init__.py b/source4/scripting/python/samba/__init__.py index 359457d815..01fdea6665 100644 --- a/source4/scripting/python/samba/__init__.py +++ b/source4/scripting/python/samba/__init__.py @@ -159,11 +159,19 @@ class Ldb(ldb.Ldb): self.add_ldif(open(ldif_path, 'r').read()) def add_ldif(self, ldif): + """Add data based on a LDIF string. + + :param ldif: LDIF text. + """ for changetype, msg in self.parse_ldif(ldif): assert changetype == ldb.CHANGETYPE_NONE self.add(msg) def modify_ldif(self, ldif): + """Modify database based on a LDIF string. + + :param ldif: LDIF text. + """ for (changetype, msg) in self.parse_ldif(ldif): assert changetype == ldb.CHANGETYPE_MODIFY self.modify(msg) diff --git a/source4/scripting/python/samba/getopt.py b/source4/scripting/python/samba/getopt.py index 87cf171ca2..c0e7053062 100644 --- a/source4/scripting/python/samba/getopt.py +++ b/source4/scripting/python/samba/getopt.py @@ -58,7 +58,7 @@ class CredentialsOptions(optparse.OptionGroup): self.creds.set_password(arg) def set_simple_bind_dn(self, option, opt_str, arg, parser): - self.creds.set_simple_bind_dn(arg) + self.creds.set_bind_dn(arg) def get_credentials(self): return self.creds diff --git a/source4/scripting/python/samba/provision.py b/source4/scripting/python/samba/provision.py index fa45f7a79d..bdfe035c41 100644 --- a/source4/scripting/python/samba/provision.py +++ b/source4/scripting/python/samba/provision.py @@ -74,6 +74,14 @@ def findnss(nssfn, *names): def open_ldb(session_info, credentials, lp, dbname): + """Open a LDB, thrashing it if it is corrupt. + + :param session_info: auth session information + :param credentials: credentials + :param lp: Loadparm context + :param dbname: Path of the database to open. + :return: a Ldb object + """ assert session_info is not None try: return Ldb(dbname, session_info=session_info, credentials=credentials, @@ -86,7 +94,12 @@ def open_ldb(session_info, credentials, lp, dbname): def setup_add_ldif(ldb, ldif_path, subst_vars=None): - """Setup a ldb in the private dir.""" + """Setup a ldb in the private dir. + + :param ldb: LDB file to import data into + :param ldif_path: Path of the LDIF file to load + :param subst_vars: Optional variables to subsitute in LDIF. + """ assert isinstance(ldif_path, str) data = open(ldif_path, 'r').read() @@ -126,7 +139,12 @@ def setup_ldb(ldb, ldif_path, subst_vars): def setup_file(template, fname, substvars): - """Setup a file in the private dir.""" + """Setup a file in the private dir. + + :param template: Path of the template file. + :param fname: Path of the file to create. + :param substvars: Substitution variables. + """ f = fname if os.path.exists(f): @@ -179,7 +197,17 @@ def provision_paths_from_lp(lp, dnsdomain): def setup_name_mappings(ldb, sid, domaindn, root, nobody, nogroup, users, wheel, backup): - """setup reasonable name mappings for sam names to unix names.""" + """setup reasonable name mappings for sam names to unix names. + + :param ldb: SamDB object. + :param sid: The domain sid. + :param domaindn: The domain DN. + :param root: Name of the UNIX root user. + :param nobody: Name of the UNIX nobody user. + :param nogroup: Name of the unix nobody group. + :param users: Name of the unix users group. + :param wheel: Name of the wheel group (users that can become root). + :param backup: Name of the backup group.""" # add some foreign sids if they are not present already ldb.add_foreign(domaindn, "S-1-5-7", "Anonymous") ldb.add_foreign(domaindn, "S-1-1-0", "World") @@ -591,7 +619,8 @@ def provision(lp, setup_dir, message, blank, paths, session_info, if nogroup is None: nogroup = findnss(grp.getgrnam, "nogroup", "nobody")[2] if users is None: - users = findnss(grp.getgrnam, "users", "guest", "other", "unknown", "usr")[2] + users = findnss(grp.getgrnam, "users", "guest", "other", "unknown", + "usr")[2] if wheel is None: wheel = findnss(grp.getgrnam, "wheel", "root", "staff", "adm")[2] if backup is None: @@ -748,13 +777,32 @@ def provision(lp, setup_dir, message, blank, paths, session_info, return domaindn def create_phplpapdadmin_config(path, setup_path, s4_ldapi_path): + """Create a PHP LDAP admin configuration file. + + :param path: Path to write the configuration to. + :param setup_path: Function to generate setup paths. + :param s4_ldapi_path: Path to Samba 4 LDAPI socket. + """ setup_file(setup_path("phpldapadmin-config.php"), path, {"S4_LDAPI_URI": "ldapi://%s" % s4_ldapi_path.replace("/", "%2F")}) def create_zone_file(path, setup_path, samdb, dnsdomain, domaindn, hostip, hostname, dnspass, realm, domainguid, hostguid): - """Write out a DNS zone file, from the info in the current database.""" + """Write out a DNS zone file, from the info in the current database. + + :param path: Path of the new file. + :param setup_path": Setup path function. + :param samdb: SamDB object + :param dnsdomain: DNS Domain name + :param domaindn: DN of the Domain + :param hostip: Local IP + :param hostname: Local hostname + :param dnspass: Password for DNS + :param realm: Realm name + :param domainguid: GUID of the domain. + :param hostguid: GUID of the host. + """ setup_file(setup_path("provision.zone"), path, { "DNSPASS_B64": b64encode(dnspass), @@ -795,7 +843,14 @@ def provision_ldapbase(setup_dir, message, paths): def load_schema(setup_path, samdb, schemadn, netbiosname, configdn): - """Load schema.""" + """Load schema. + + :param samdb: Load a schema into a SamDB. + :param setup_path: Setup path function. + :param schemadn: DN of the schema + :param netbiosname: NetBIOS name of the host. + :param configdn: DN of the configuration + """ schema_data = open(setup_path("schema.ldif"), 'r').read() schema_data += open(setup_path("schema_samba4.ldif"), 'r').read() schema_data = substitute_var(schema_data, {"SCHEMADN": schemadn}) @@ -807,32 +862,3 @@ def load_schema(setup_path, samdb, schemadn, netbiosname, configdn): "DEFAULTSITE": DEFAULTSITE}) samdb.attach_schema_from_ldif(head_data, schema_data) - -def join_domain(domain, netbios_name, join_type, creds): - ctx = NetContext(creds) - joindom = object() - joindom.domain = domain - joindom.join_type = join_type - joindom.netbios_name = netbios_name - if not ctx.JoinDomain(joindom): - raise Exception("Domain Join failed: " + joindom.error_string) - - -def vampire(domain, session_info, credentials, message): - """Vampire a remote domain. - - Session info and credentials are required for for - access to our local database (might be remote ldap) - """ - ctx = NetContext(credentials) - machine_creds = Credentials() - machine_creds.set_domain(form.domain) - if not machine_creds.set_machine_account(): - raise Exception("Failed to access domain join information!") - vampire_ctx.machine_creds = machine_creds - vampire_ctx.session_info = session_info - if not ctx.SamSyncLdb(vampire_ctx): - raise Exception("Migration of remote domain to Samba failed: %s " % vampire_ctx.error_string) - - - diff --git a/source4/scripting/python/samba/samba3.py b/source4/scripting/python/samba/samba3.py index 27656900ad..113348e6f1 100644 --- a/source4/scripting/python/samba/samba3.py +++ b/source4/scripting/python/samba/samba3.py @@ -25,14 +25,33 @@ REGISTRY_DB_VERSION = 1 import os import tdb -class Registry: - """Simple read-only support for reading the Samba3 registry.""" + +class TdbDatabase: + """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()) @@ -42,6 +61,11 @@ class Registry: return [k.rstrip("\x00") for k in self.tdb.keys() 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 [] @@ -54,7 +78,11 @@ class Registry: 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 {} @@ -77,9 +105,14 @@ class Registry: 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) + """Open a policy database + + :param file: Path to the file to open. + """ + super(PolicyDatabase, self).__init__(file) 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") @@ -93,9 +126,6 @@ class PolicyDatabase: # 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. @@ -108,17 +138,27 @@ 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) +class GroupMappingDatabase(TdbDatabase): + """Samba 3 group mapping database reader.""" + def _check_version(self): assert self.tdb.fetch_int32("INFO/version\x00") in (GROUPDB_DATABASE_VERSION_V1, GROUPDB_DATABASE_VERSION_V2) def groupsids(self): + """Retrieve the SIDs for the groups in this database. + + :return: List with sids as strings. + """ for k in self.tdb.keys(): 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 @@ -128,13 +168,11 @@ class GroupMappingDatabase: return (gid, sid_name_use, nt_name, comment) def aliases(self): + """Retrieve the aliases in this database.""" for k in self.tdb.keys(): 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" @@ -146,22 +184,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) +class IdmapDatabase(TdbDatabase): + """Samba 3 ID map database reader.""" + def _check_version(self): assert self.tdb.fetch_int32("IDMAP_VERSION\0") == IDMAP_VERSION_V2 def uids(self): + """Retrieve a list of all uids in this database.""" for k in self.tdb.keys(): if k.startswith(IDMAP_USER_PREFIX): yield int(k[len(IDMAP_USER_PREFIX):].rstrip("\0")) def gids(self): + """Retrieve a list of all gids in this database.""" for k in self.tdb.keys(): 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 @@ -174,19 +219,15 @@ class IdmapDatabase: return data.rstrip("\0") def get_user_hwm(self): + """Obtain the user high-water mark.""" return self.tdb.fetch_uint32(IDMAP_HWM_USER) def get_group_hwm(self): + """Obtain the group high-water mark.""" return self.tdb.fetch_uint32(IDMAP_HWM_GROUP) - def close(self): - self.tdb.close() - - -class SecretsDatabase: - def __init__(self, file): - self.tdb = tdb.Tdb(file, flags=os.O_RDONLY) +class SecretsDatabase(TdbDatabase): def get_auth_password(self): return self.tdb.get("SECRETS/AUTH_PASSWORD") @@ -241,16 +282,12 @@ 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) +class ShareInfoDatabase(TdbDatabase): + def _check_version(self): assert self.tdb.fetch_int32("INFO/version\0") in (SHARE_DATABASE_VERSION_V1, SHARE_DATABASE_VERSION_V2) def get_secdesc(self, name): @@ -258,9 +295,6 @@ class ShareInfoDatabase: # FIXME: Run ndr_pull_security_descriptor return secdesc - def close(self): - self.tdb.close() - class Shares: def __init__(self, lp, shareinfo): diff --git a/source4/scripting/python/samba/samdb.py b/source4/scripting/python/samba/samdb.py index 46707f060f..2af56d8d8e 100644 --- a/source4/scripting/python/samba/samdb.py +++ b/source4/scripting/python/samba/samdb.py @@ -20,13 +20,20 @@ # along with this program. If not, see . # +"""Convenience functions for using the SAM.""" + import samba import misc import ldb class SamDB(samba.Ldb): + """The SAM database.""" def __init__(self, url=None, session_info=None, credentials=None, modules_dir=None, lp=None): + """Open the Sam Database. + + :param url: URL of the database. + """ super(SamDB, self).__init__(session_info=session_info, credentials=credentials, modules_dir=modules_dir, lp=lp) assert misc.dsdb_set_global_schema(self) == 0 @@ -47,7 +54,12 @@ description: %s self.add(msg[1]) def setup_name_mapping(self, domaindn, sid, unixname): - """Setup a mapping between a sam name and a unix name.""" + """Setup a mapping between a sam name and a unix name. + + :param domaindn: DN of the domain. + :param sid: SID of the NT-side of the mapping. + :param unixname: Unix name to map to. + """ res = self.search(ldb.Dn(self, domaindn), ldb.SCOPE_SUBTREE, "objectSid=%s" % sid, ["dn"]) assert len(res) == 1, "Failed to find record for objectSid %s" % sid @@ -61,7 +73,7 @@ unixName: %s self.modify(self.parse_ldif(mod).next()[1]) def enable_account(self, user_dn): - """enable the account. + """Enable an account. :param user_dn: Dn of the account to enable. """ @@ -75,10 +87,15 @@ changetype: modify replace: userAccountControl userAccountControl: %u """ % (user_dn, userAccountControl) - self.modify(mod) + self.modify_ldif(mod) - def newuser(self, username, unixname, password, message): - """add a new user record""" + def newuser(self, username, unixname, password): + """add a new user record. + + :param username: Name of the new user. + :param unixname: Name of the unix user to map to. + :param password: Password for the new user + """ # connect to the sam self.transaction_start() @@ -97,13 +114,13 @@ userAccountControl: %u # the new user record. note the reliance on the samdb module to fill # in a sid, guid etc # - ldif = """ -dn: %s -sAMAccountName: %s -unixName: %s -sambaPassword: %s -objectClass: user - """ % (user_dn, username, unixname, password) + # now the real work + self.add({"dn": user_dn, + "sAMAccountName": username, + "unixName": unixname, + "sambaPassword": password, + "objectClass": "user"}) + # add the user to the users group as well modgroup = """ dn: %s @@ -113,11 +130,6 @@ member: %s """ % (dom_users, user_dn) - # now the real work - message("Adding user %s" % user_dn) - self.add(ldif) - - message("Modifying group %s" % dom_users) self.modify(modgroup) # modify the userAccountControl to remove the disabled bit @@ -125,6 +137,10 @@ member: %s self.transaction_commit() def set_domain_sid(self, sid): + """Change the domain SID used by this SamDB. + + :param sid: The new domain sid to use. + """ misc.samdb_set_domain_sid(self, sid) def attach_schema_from_ldif(self, pf, df): diff --git a/source4/scripting/python/samba/tests/__init__.py b/source4/scripting/python/samba/tests/__init__.py index 5e1ff87c2b..ad8a2524b5 100644 --- a/source4/scripting/python/samba/tests/__init__.py +++ b/source4/scripting/python/samba/tests/__init__.py @@ -17,6 +17,8 @@ # along with this program. If not, see . # +"""Samba Python tests.""" + import os import ldb import samba @@ -24,11 +26,13 @@ import tempfile import unittest class LdbTestCase(unittest.TestCase): + """Trivial test case for running tests against a LDB.""" def setUp(self): self.filename = os.tempnam() self.ldb = samba.Ldb(self.filename) def set_modules(self, modules=[]): + """Change the modules for this Ldb.""" m = ldb.Message() m.dn = ldb.Dn(self.ldb, "@MODULES") m["@LIST"] = ",".join(modules) -- cgit