Skip to content

Commit ce8aca9

Browse files
author
leonard
committed
Add more tests for the C Extension module.
1 parent c876b63 commit ce8aca9

File tree

1 file changed

+383
-0
lines changed

1 file changed

+383
-0
lines changed

Tests/slapd.py

Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
2+
"""
3+
Utilities for starting up a test slapd server
4+
and talking to it with ldapsearch/ldapadd.
5+
"""
6+
7+
import sys, os, socket, time, subprocess, logging
8+
9+
_log = logging.getLogger("slapd")
10+
11+
def quote(s):
12+
'''Quotes the '"' and '\' characters in a string and surrounds with "..."'''
13+
return '"' + s.replace('\\','\\\\').replace('"','\\"') + '"'
14+
15+
def mkdirs(path):
16+
"""Creates the directory path unless it already exists"""
17+
if not os.access(os.path.join(path, os.path.curdir), os.F_OK):
18+
_log.debug("creating temp directory %s", path)
19+
os.mkdir(path)
20+
return path
21+
22+
def delete_directory_content(path):
23+
for dirpath,dirnames,filenames in os.walk(path, topdown=False):
24+
for n in filenames:
25+
_log.info("remove %s", os.path.join(dirpath, n))
26+
os.remove(os.path.join(dirpath, n))
27+
for n in dirnames:
28+
_log.info("rmdir %s", os.path.join(dirpath, n))
29+
os.rmdir(os.path.join(dirpath, n))
30+
31+
LOCALHOST = '127.0.0.1'
32+
33+
def find_available_tcp_port(host=LOCALHOST):
34+
s = socket.socket()
35+
s.bind((host, 0))
36+
port = s.getsockname()[1]
37+
s.close()
38+
_log.info("Found available port %d", port)
39+
return port
40+
41+
class Slapd:
42+
"""
43+
Controller class for a slapd instance, OpenLDAP's server.
44+
45+
This class creates a temporary data store for slapd, runs it
46+
on a private port, and initialises it with a top-level dc and
47+
the root user.
48+
49+
When a reference to an instance of this class is lost, the slapd
50+
server is shut down.
51+
"""
52+
53+
_log = logging.getLogger("Slapd")
54+
55+
# Use /var/tmp to placate apparmour on Ubuntu:
56+
PATH_TMPDIR = "/var/tmp/python-ldap-test"
57+
PATH_SBINDIR = "/usr/sbin"
58+
PATH_BINDIR = "/usr/bin"
59+
PATH_SCHEMA_CORE = "/etc/ldap/schema/core.schema"
60+
PATH_LDAPADD = os.path.join(PATH_BINDIR, "ldapadd")
61+
PATH_LDAPSEARCH = os.path.join(PATH_BINDIR, "ldapsearch")
62+
PATH_SLAPD = os.path.join(PATH_SBINDIR, "slapd")
63+
PATH_SLAPTEST = os.path.join(PATH_SBINDIR, "slaptest")
64+
65+
# TODO add paths for other OSs
66+
67+
def check_paths(cls):
68+
"""
69+
Checks that the configured executable paths look valid.
70+
If they don't, then logs warning messages (not errors).
71+
"""
72+
for name,path in (
73+
("slapd", cls.PATH_SLAPD),
74+
("ldapadd", cls.PATH_LDAPADD),
75+
("ldapsearch", cls.PATH_LDAPSEARCH),
76+
):
77+
cls._log.debug("checking %s executable at %s", name, path)
78+
if not os.access(path, os.X_OK):
79+
cls._log.warn("cannot find %s executable at %s", name, path)
80+
check_paths = classmethod(check_paths)
81+
82+
def __init__(self):
83+
self._config = []
84+
self._proc = None
85+
self._port = 0
86+
self._tmpdir = self.PATH_TMPDIR
87+
self._dn_suffix = "dc=python-ldap,dc=org"
88+
self._root_cn = "Manager"
89+
self._root_password = "password"
90+
self._slapd_debug_level = 0
91+
92+
# Setters
93+
def set_port(self, port):
94+
self._port = port
95+
def set_dn_suffix(self, dn):
96+
self._dn_suffix = dn
97+
def set_root_cn(self, cn):
98+
self._root_cn = cn
99+
def set_root_password(self, pw):
100+
self._root_password = pw
101+
def set_tmpdir(self, path):
102+
self._tmpdir = path
103+
def set_slapd_debug_level(self, level):
104+
self._slapd_debug_level = level
105+
def set_debug(self):
106+
self._log.setLevel(logging.DEBUG)
107+
self.set_slapd_debug_level('Any')
108+
109+
# getters
110+
def get_url(self):
111+
return "ldap://%s:%d/" % self.get_address()
112+
def get_address(self):
113+
if self._port == 0:
114+
self._port = find_available_tcp_port(LOCALHOST)
115+
return (LOCALHOST, self._port)
116+
def get_dn_suffix(self):
117+
return self._dn_suffix
118+
def get_root_dn(self):
119+
return "cn=" + self._root_cn + "," + self.get_dn_suffix()
120+
def get_root_password(self):
121+
return self._root_password
122+
def get_tmpdir(self):
123+
return self._tmpdir
124+
125+
def __del__(self):
126+
self.stop()
127+
128+
def configure(self, cfg):
129+
"""
130+
Appends slapd.conf configuration lines to cfg.
131+
Also re-initializes any backing storage.
132+
Feel free to subclass and override this method.
133+
"""
134+
135+
# Global
136+
cfg.append("include " + quote(self.PATH_SCHEMA_CORE))
137+
cfg.append("allow bind_v2")
138+
139+
# Database
140+
ldif_dir = mkdirs(os.path.join(self.get_tmpdir(), "ldif-data"))
141+
delete_directory_content(ldif_dir) # clear it out
142+
cfg.append("database ldif")
143+
cfg.append("directory " + quote(ldif_dir))
144+
145+
cfg.append("suffix " + quote(self.get_dn_suffix()))
146+
cfg.append("rootdn " + quote(self.get_root_dn()))
147+
cfg.append("rootpw " + quote(self.get_root_password()))
148+
149+
def _write_config(self):
150+
"""Writes the slapd.conf file out, and returns the path to it."""
151+
path = os.path.join(self._tmpdir, "slapd.conf")
152+
ldif_dir = mkdirs(self._tmpdir)
153+
if os.access(path, os.F_OK):
154+
self._log.debug("deleting existing %s", path)
155+
os.remove(path)
156+
self._log.debug("writing config to %s", path)
157+
file(path, "w").writelines([line + "\n" for line in self._config])
158+
return path
159+
160+
def start(self):
161+
"""
162+
Starts the slapd server process running, and waits for it to come up.
163+
"""
164+
if self._proc is None:
165+
ok = False
166+
config_path = None
167+
try:
168+
self.configure(self._config)
169+
self._test_configuration()
170+
self._start_slapd()
171+
self._wait_for_slapd()
172+
ok = True
173+
self._log.debug("slapd ready at %s", self.get_url())
174+
self.started()
175+
finally:
176+
if not ok:
177+
if config_path:
178+
try: os.remove(config_path)
179+
except os.error: pass
180+
if self._proc:
181+
self.stop()
182+
183+
def _start_slapd(self):
184+
# Spawns/forks the slapd process
185+
config_path = self._write_config()
186+
self._log.info("starting slapd")
187+
self._proc = subprocess.Popen([self.PATH_SLAPD,
188+
"-f", config_path,
189+
"-h", self.get_url(),
190+
"-d", str(self._slapd_debug_level),
191+
])
192+
self._proc_config = config_path
193+
194+
def _wait_for_slapd(self):
195+
# Waits until the LDAP server socket is open, or slapd crashed
196+
s = socket.socket()
197+
while 1:
198+
if self._proc.poll() is not None:
199+
self._stopped()
200+
raise RuntimeError("slapd exited before opening port")
201+
try:
202+
self._log.debug("Connecting to %s", repr(self.get_address()))
203+
s.connect(self.get_address())
204+
s.close()
205+
return
206+
except socket.error:
207+
time.sleep(1)
208+
209+
def stop(self):
210+
"""Stops the slapd server, and waits for it to terminate"""
211+
if self._proc is not None:
212+
self._log.debug("stopping slapd")
213+
if hasattr(self._proc, 'terminate'):
214+
self._proc.terminate()
215+
else:
216+
import posix, signal
217+
posix.kill(self._proc.pid, signal.SIGHUP)
218+
#time.sleep(1)
219+
#posix.kill(self._proc.pid, signal.SIGTERM)
220+
#posix.kill(self._proc.pid, signal.SIGKILL)
221+
self.wait()
222+
223+
def restart(self):
224+
"""
225+
Restarts the slapd server; ERASING previous content.
226+
Starts the server even it if isn't already running.
227+
"""
228+
self.stop()
229+
self.start()
230+
231+
def wait(self):
232+
"""Waits for the slapd process to terminate by itself."""
233+
if self._proc:
234+
self._proc.wait()
235+
self._stopped()
236+
237+
def _stopped(self):
238+
"""Called when the slapd server is known to have terminated"""
239+
if self._proc is not None:
240+
self._log.info("slapd terminated")
241+
self._proc = None
242+
try:
243+
os.remove(self._proc_config)
244+
except os.error:
245+
self._log.debug("could not remove %s", self._proc_config)
246+
247+
def _test_configuration(self):
248+
config_path = self._write_config()
249+
try:
250+
self._log.debug("testing configuration")
251+
verboseflag = "-Q"
252+
if self._log.isEnabledFor(logging.DEBUG):
253+
verboseflag = "-v"
254+
p = subprocess.Popen([
255+
self.PATH_SLAPTEST,
256+
verboseflag,
257+
"-f", config_path
258+
])
259+
if p.wait() != 0:
260+
raise RuntimeError("configuration test failed")
261+
self._log.debug("configuration seems ok")
262+
finally:
263+
os.remove(config_path)
264+
265+
def ldapadd(self, ldif, extra_args=[]):
266+
"""Runs ldapadd on this slapd instance, passing it the ldif content"""
267+
self._log.debug("adding %s", repr(ldif))
268+
p = subprocess.Popen([self.PATH_LDAPADD,
269+
"-x",
270+
"-D", self.get_root_dn(),
271+
"-w", self.get_root_password(),
272+
"-H", self.get_url()] + extra_args,
273+
stdin = subprocess.PIPE, stdout=subprocess.PIPE)
274+
p.communicate(ldif)
275+
if p.wait() != 0:
276+
raise RuntimeError("ldapadd process failed")
277+
278+
def ldapsearch(self, base=None, filter='(objectClass=*)', attrs=[],
279+
scope='sub', extra_args=[]):
280+
if base is None: base = self.get_dn_suffix()
281+
self._log.debug("ldapsearch filter=%s", repr(filter))
282+
p = subprocess.Popen([self.PATH_LDAPSEARCH,
283+
"-x",
284+
"-D", self.get_root_dn(),
285+
"-w", self.get_root_password(),
286+
"-H", self.get_url(),
287+
"-b", base,
288+
"-s", scope,
289+
"-LL",
290+
] + extra_args + [ filter ] + attrs,
291+
stdout = subprocess.PIPE)
292+
output = p.communicate()[0]
293+
if p.wait() != 0:
294+
raise RuntimeError("ldapadd process failed")
295+
296+
# RFC 2849: LDIF format
297+
# unfold
298+
lines = []
299+
for l in output.split('\n'):
300+
if l.startswith(' '):
301+
lines[-1] = lines[-1] + l[1:]
302+
elif l == '' and lines and lines[-1] == '':
303+
pass # ignore multiple blank lines
304+
else:
305+
lines.append(l)
306+
# Remove comments
307+
lines = [l for l in lines if not l.startswith("#")]
308+
309+
# Remove leading version and blank line(s)
310+
if lines and lines[0] == '': del lines[0]
311+
if not lines or lines[0] != 'version: 1':
312+
raise RuntimeError("expected 'version: 1', got " + repr(lines[:1]))
313+
del lines[0]
314+
if lines and lines[0] == '': del lines[0]
315+
316+
# ensure the ldif ends with a blank line (unless it is just blank)
317+
if lines and lines[-1] != '': lines.append('')
318+
319+
objects = []
320+
obj = []
321+
for line in lines:
322+
if line == '': # end of an object
323+
if obj[0][0] != 'dn':
324+
raise RuntimeError("first line not dn", repr(obj))
325+
objects.append((obj[0][1], obj[1:]))
326+
obj = []
327+
else:
328+
attr,value = line.split(':',2)
329+
if value.startswith(': '):
330+
value = base64.decodestring(value[2:])
331+
elif value.startswith(' '):
332+
value = value[1:]
333+
else:
334+
raise RuntimeError("bad line: " + repr(line))
335+
obj.append((attr,value))
336+
assert obj == []
337+
return objects
338+
339+
def started(self):
340+
"""
341+
This method is called when the LDAP server has started up and is empty.
342+
By default, this method adds the two initial objects,
343+
the domain object and the root user object.
344+
"""
345+
assert self.get_dn_suffix().startswith("dc=")
346+
suffix_dc = self.get_dn_suffix().split(',')[0][3:]
347+
assert self.get_root_dn().startswith("cn=")
348+
assert self.get_root_dn().endswith("," + self.get_dn_suffix())
349+
root_cn = self.get_root_dn().split(',')[0][3:]
350+
351+
self._log.debug("adding %s and %s",
352+
self.get_dn_suffix(),
353+
self.get_root_dn())
354+
355+
self.ldapadd("\n".join([
356+
'dn: ' + self.get_dn_suffix(),
357+
'objectClass: dcObject',
358+
'objectClass: organization',
359+
'dc: ' + suffix_dc,
360+
'o: ' + suffix_dc,
361+
'',
362+
'dn: ' + self.get_root_dn(),
363+
'objectClass: organizationalRole',
364+
'cn: ' + root_cn,
365+
''
366+
]))
367+
368+
Slapd.check_paths()
369+
370+
if __name__ == '__main__' and sys.argv == ['run']:
371+
logging.basicConfig(level=logging.DEBUG)
372+
slapd = Slapd()
373+
print("Starting slapd...")
374+
slapd.start()
375+
print("Contents of LDAP server follow:\n")
376+
for dn,attrs in slapd.ldapsearch():
377+
print("dn: " + dn)
378+
for name,val in attrs:
379+
print(name + ": " + val)
380+
print("")
381+
print(slapd.get_url())
382+
slapd.wait()
383+

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy