]> git.proxmox.com Git - ceph.git/blame - ceph/qa/tasks/cephfs/mount.py
import ceph 14.2.5
[ceph.git] / ceph / qa / tasks / cephfs / mount.py
CommitLineData
7c673cae
FG
1from contextlib import contextmanager
2import json
3import logging
4import datetime
5import time
6from textwrap import dedent
7import os
8from StringIO import StringIO
9from teuthology.orchestra import run
10from teuthology.orchestra.run import CommandFailedError, ConnectionLostError
11fdf7f2 11from tasks.cephfs.filesystem import Filesystem
7c673cae
FG
12
13log = logging.getLogger(__name__)
14
15
16class CephFSMount(object):
11fdf7f2 17 def __init__(self, ctx, test_dir, client_id, client_remote):
7c673cae
FG
18 """
19 :param test_dir: Global teuthology test dir
20 :param client_id: Client ID, the 'foo' in client.foo
21 :param client_remote: Remote instance for the host where client will run
22 """
23
11fdf7f2 24 self.ctx = ctx
7c673cae
FG
25 self.test_dir = test_dir
26 self.client_id = client_id
27 self.client_remote = client_remote
28 self.mountpoint_dir_name = 'mnt.{id}'.format(id=self.client_id)
11fdf7f2 29 self.fs = None
7c673cae
FG
30
31 self.test_files = ['a', 'b', 'c']
32
33 self.background_procs = []
34
35 @property
36 def mountpoint(self):
37 return os.path.join(
38 self.test_dir, '{dir_name}'.format(dir_name=self.mountpoint_dir_name))
39
40 def is_mounted(self):
41 raise NotImplementedError()
42
11fdf7f2
TL
43 def setupfs(self, name=None):
44 if name is None and self.fs is not None:
45 # Previous mount existed, reuse the old name
46 name = self.fs.name
47 self.fs = Filesystem(self.ctx, name=name)
48 log.info('Wait for MDS to reach steady state...')
49 self.fs.wait_for_daemons()
50 log.info('Ready to start {}...'.format(type(self).__name__))
51
7c673cae
FG
52 def mount(self, mount_path=None, mount_fs_name=None):
53 raise NotImplementedError()
54
55 def umount(self):
56 raise NotImplementedError()
57
58 def umount_wait(self, force=False, require_clean=False):
59 """
60
61 :param force: Expect that the mount will not shutdown cleanly: kill
62 it hard.
63 :param require_clean: Wait for the Ceph client associated with the
64 mount (e.g. ceph-fuse) to terminate, and
65 raise if it doesn't do so cleanly.
66 :return:
67 """
68 raise NotImplementedError()
69
70 def kill_cleanup(self):
71 raise NotImplementedError()
72
73 def kill(self):
74 raise NotImplementedError()
75
76 def cleanup(self):
77 raise NotImplementedError()
78
79 def wait_until_mounted(self):
80 raise NotImplementedError()
81
82 def get_keyring_path(self):
83 return '/etc/ceph/ceph.client.{id}.keyring'.format(id=self.client_id)
84
85 @property
86 def config_path(self):
87 """
88 Path to ceph.conf: override this if you're not a normal systemwide ceph install
89 :return: stringv
90 """
91 return "/etc/ceph/ceph.conf"
92
93 @contextmanager
94 def mounted(self):
95 """
96 A context manager, from an initially unmounted state, to mount
97 this, yield, and then unmount and clean up.
98 """
99 self.mount()
100 self.wait_until_mounted()
101 try:
102 yield
103 finally:
104 self.umount_wait()
105
11fdf7f2
TL
106 def is_blacklisted(self):
107 addr = self.get_global_addr()
108 blacklist = json.loads(self.fs.mon_manager.raw_cluster_cmd("osd", "blacklist", "ls", "--format=json"))
109 for b in blacklist:
110 if addr == b["addr"]:
111 return True
112 return False
113
7c673cae
FG
114 def create_files(self):
115 assert(self.is_mounted())
116
117 for suffix in self.test_files:
118 log.info("Creating file {0}".format(suffix))
119 self.client_remote.run(args=[
120 'sudo', 'touch', os.path.join(self.mountpoint, suffix)
121 ])
122
123 def check_files(self):
124 assert(self.is_mounted())
125
126 for suffix in self.test_files:
127 log.info("Checking file {0}".format(suffix))
128 r = self.client_remote.run(args=[
129 'sudo', 'ls', os.path.join(self.mountpoint, suffix)
130 ], check_status=False)
131 if r.exitstatus != 0:
132 raise RuntimeError("Expected file {0} not found".format(suffix))
133
134 def create_destroy(self):
135 assert(self.is_mounted())
136
137 filename = "{0} {1}".format(datetime.datetime.now(), self.client_id)
138 log.debug("Creating test file {0}".format(filename))
139 self.client_remote.run(args=[
140 'sudo', 'touch', os.path.join(self.mountpoint, filename)
141 ])
142 log.debug("Deleting test file {0}".format(filename))
143 self.client_remote.run(args=[
144 'sudo', 'rm', '-f', os.path.join(self.mountpoint, filename)
145 ])
146
91327a77
AA
147 def _run_python(self, pyscript, py_version='python'):
148 return self.client_remote.run(
149 args=['sudo', 'adjust-ulimits', 'daemon-helper', 'kill',
150 py_version, '-c', pyscript], wait=False, stdin=run.PIPE,
151 stdout=StringIO())
152
153 def run_python(self, pyscript, py_version='python'):
154 p = self._run_python(pyscript, py_version)
7c673cae
FG
155 p.wait()
156 return p.stdout.getvalue().strip()
157
158 def run_shell(self, args, wait=True):
159 args = ["cd", self.mountpoint, run.Raw('&&'), "sudo"] + args
160 return self.client_remote.run(args=args, stdout=StringIO(),
161 stderr=StringIO(), wait=wait)
162
163 def open_no_data(self, basename):
164 """
165 A pure metadata operation
166 """
167 assert(self.is_mounted())
168
169 path = os.path.join(self.mountpoint, basename)
170
171 p = self._run_python(dedent(
172 """
173 f = open("{path}", 'w')
174 """.format(path=path)
175 ))
176 p.wait()
177
494da23a 178 def open_background(self, basename="background_file", write=True):
7c673cae
FG
179 """
180 Open a file for writing, then block such that the client
181 will hold a capability.
182
183 Don't return until the remote process has got as far as opening
184 the file, then return the RemoteProcess instance.
185 """
186 assert(self.is_mounted())
187
188 path = os.path.join(self.mountpoint, basename)
189
494da23a
TL
190 if write:
191 pyscript = dedent("""
192 import time
193
194 f = open("{path}", 'w')
195 f.write('content')
196 f.flush()
197 f.write('content2')
198 while True:
199 time.sleep(1)
200 """).format(path=path)
201 else:
202 pyscript = dedent("""
203 import time
204
205 f = open("{path}", 'r')
206 while True:
207 time.sleep(1)
208 """).format(path=path)
7c673cae
FG
209
210 rproc = self._run_python(pyscript)
211 self.background_procs.append(rproc)
212
213 # This wait would not be sufficient if the file had already
214 # existed, but it's simple and in practice users of open_background
215 # are not using it on existing files.
216 self.wait_for_visible(basename)
217
218 return rproc
219
494da23a
TL
220 def wait_for_dir_empty(self, dirname, timeout=30):
221 i = 0
222 dirpath = os.path.join(self.mountpoint, dirname)
223 while i < timeout:
224 nr_entries = int(self.getfattr(dirpath, "ceph.dir.entries"))
225 if nr_entries == 0:
226 log.debug("Directory {0} seen empty from {1} after {2}s ".format(
227 dirname, self.client_id, i))
228 return
229 else:
230 time.sleep(1)
231 i += 1
232
233 raise RuntimeError("Timed out after {0}s waiting for {1} to become empty from {2}".format(
234 i, dirname, self.client_id))
235
7c673cae
FG
236 def wait_for_visible(self, basename="background_file", timeout=30):
237 i = 0
238 while i < timeout:
239 r = self.client_remote.run(args=[
240 'sudo', 'ls', os.path.join(self.mountpoint, basename)
241 ], check_status=False)
242 if r.exitstatus == 0:
243 log.debug("File {0} became visible from {1} after {2}s".format(
244 basename, self.client_id, i))
245 return
246 else:
247 time.sleep(1)
248 i += 1
249
250 raise RuntimeError("Timed out after {0}s waiting for {1} to become visible from {2}".format(
251 i, basename, self.client_id))
252
253 def lock_background(self, basename="background_file", do_flock=True):
254 """
255 Open and lock a files for writing, hold the lock in a background process
256 """
257 assert(self.is_mounted())
258
259 path = os.path.join(self.mountpoint, basename)
260
261 script_builder = """
262 import time
263 import fcntl
264 import struct"""
265 if do_flock:
266 script_builder += """
267 f1 = open("{path}-1", 'w')
268 fcntl.flock(f1, fcntl.LOCK_EX | fcntl.LOCK_NB)"""
269 script_builder += """
270 f2 = open("{path}-2", 'w')
271 lockdata = struct.pack('hhllhh', fcntl.F_WRLCK, 0, 0, 0, 0, 0)
272 fcntl.fcntl(f2, fcntl.F_SETLK, lockdata)
273 while True:
274 time.sleep(1)
275 """
276
277 pyscript = dedent(script_builder).format(path=path)
278
31f18b77 279 log.info("lock_background file {0}".format(basename))
7c673cae
FG
280 rproc = self._run_python(pyscript)
281 self.background_procs.append(rproc)
282 return rproc
283
31f18b77
FG
284 def lock_and_release(self, basename="background_file"):
285 assert(self.is_mounted())
286
287 path = os.path.join(self.mountpoint, basename)
288
289 script = """
290 import time
291 import fcntl
292 import struct
293 f1 = open("{path}-1", 'w')
294 fcntl.flock(f1, fcntl.LOCK_EX)
295 f2 = open("{path}-2", 'w')
296 lockdata = struct.pack('hhllhh', fcntl.F_WRLCK, 0, 0, 0, 0, 0)
297 fcntl.fcntl(f2, fcntl.F_SETLK, lockdata)
298 """
299 pyscript = dedent(script).format(path=path)
300
301 log.info("lock_and_release file {0}".format(basename))
302 return self._run_python(pyscript)
303
7c673cae
FG
304 def check_filelock(self, basename="background_file", do_flock=True):
305 assert(self.is_mounted())
306
307 path = os.path.join(self.mountpoint, basename)
308
309 script_builder = """
310 import fcntl
311 import errno
312 import struct"""
313 if do_flock:
314 script_builder += """
315 f1 = open("{path}-1", 'r')
316 try:
317 fcntl.flock(f1, fcntl.LOCK_EX | fcntl.LOCK_NB)
318 except IOError, e:
319 if e.errno == errno.EAGAIN:
320 pass
321 else:
322 raise RuntimeError("flock on file {path}-1 not found")"""
323 script_builder += """
324 f2 = open("{path}-2", 'r')
325 try:
326 lockdata = struct.pack('hhllhh', fcntl.F_WRLCK, 0, 0, 0, 0, 0)
327 fcntl.fcntl(f2, fcntl.F_SETLK, lockdata)
328 except IOError, e:
329 if e.errno == errno.EAGAIN:
330 pass
331 else:
332 raise RuntimeError("posix lock on file {path}-2 not found")
333 """
334 pyscript = dedent(script_builder).format(path=path)
335
336 log.info("check lock on file {0}".format(basename))
337 self.client_remote.run(args=[
338 'sudo', 'python', '-c', pyscript
339 ])
340
341 def write_background(self, basename="background_file", loop=False):
342 """
343 Open a file for writing, complete as soon as you can
344 :param basename:
345 :return:
346 """
347 assert(self.is_mounted())
348
349 path = os.path.join(self.mountpoint, basename)
350
351 pyscript = dedent("""
352 import os
353 import time
354
355 fd = os.open("{path}", os.O_RDWR | os.O_CREAT, 0644)
356 try:
357 while True:
358 os.write(fd, 'content')
359 time.sleep(1)
360 if not {loop}:
361 break
362 except IOError, e:
363 pass
364 os.close(fd)
365 """).format(path=path, loop=str(loop))
366
367 rproc = self._run_python(pyscript)
368 self.background_procs.append(rproc)
369 return rproc
370
371 def write_n_mb(self, filename, n_mb, seek=0, wait=True):
372 """
373 Write the requested number of megabytes to a file
374 """
375 assert(self.is_mounted())
376
377 return self.run_shell(["dd", "if=/dev/urandom", "of={0}".format(filename),
378 "bs=1M", "conv=fdatasync",
379 "count={0}".format(n_mb),
380 "seek={0}".format(seek)
381 ], wait=wait)
382
383 def write_test_pattern(self, filename, size):
384 log.info("Writing {0} bytes to {1}".format(size, filename))
385 return self.run_python(dedent("""
386 import zlib
387 path = "{path}"
388 f = open(path, 'w')
389 for i in range(0, {size}):
390 val = zlib.crc32("%s" % i) & 7
391 f.write(chr(val))
392 f.close()
393 """.format(
394 path=os.path.join(self.mountpoint, filename),
395 size=size
396 )))
397
398 def validate_test_pattern(self, filename, size):
399 log.info("Validating {0} bytes from {1}".format(size, filename))
400 return self.run_python(dedent("""
401 import zlib
402 path = "{path}"
403 f = open(path, 'r')
404 bytes = f.read()
405 f.close()
406 if len(bytes) != {size}:
407 raise RuntimeError("Bad length {{0}} vs. expected {{1}}".format(
408 len(bytes), {size}
409 ))
410 for i, b in enumerate(bytes):
411 val = zlib.crc32("%s" % i) & 7
412 if b != chr(val):
413 raise RuntimeError("Bad data at offset {{0}}".format(i))
414 """.format(
415 path=os.path.join(self.mountpoint, filename),
416 size=size
417 )))
418
419 def open_n_background(self, fs_path, count):
420 """
421 Open N files for writing, hold them open in a background process
422
423 :param fs_path: Path relative to CephFS root, e.g. "foo/bar"
424 :return: a RemoteProcess
425 """
426 assert(self.is_mounted())
427
428 abs_path = os.path.join(self.mountpoint, fs_path)
429
430 pyscript = dedent("""
431 import sys
432 import time
433 import os
434
435 n = {count}
436 abs_path = "{abs_path}"
437
438 if not os.path.exists(os.path.dirname(abs_path)):
439 os.makedirs(os.path.dirname(abs_path))
440
441 handles = []
442 for i in range(0, n):
443 fname = "{{0}}_{{1}}".format(abs_path, i)
444 handles.append(open(fname, 'w'))
445
446 while True:
447 time.sleep(1)
448 """).format(abs_path=abs_path, count=count)
449
450 rproc = self._run_python(pyscript)
451 self.background_procs.append(rproc)
452 return rproc
453
454 def create_n_files(self, fs_path, count, sync=False):
455 assert(self.is_mounted())
456
457 abs_path = os.path.join(self.mountpoint, fs_path)
458
459 pyscript = dedent("""
460 import sys
461 import time
462 import os
463
464 n = {count}
465 abs_path = "{abs_path}"
466
467 if not os.path.exists(os.path.dirname(abs_path)):
468 os.makedirs(os.path.dirname(abs_path))
469
470 for i in range(0, n):
471 fname = "{{0}}_{{1}}".format(abs_path, i)
472 h = open(fname, 'w')
473 h.write('content')
474 if {sync}:
475 h.flush()
476 os.fsync(h.fileno())
477 h.close()
478 """).format(abs_path=abs_path, count=count, sync=str(sync))
479
480 self.run_python(pyscript)
481
482 def teardown(self):
483 for p in self.background_procs:
484 log.info("Terminating background process")
485 self._kill_background(p)
486
487 self.background_procs = []
488
489 def _kill_background(self, p):
490 if p.stdin:
491 p.stdin.close()
492 try:
493 p.wait()
494 except (CommandFailedError, ConnectionLostError):
495 pass
496
497 def kill_background(self, p):
498 """
499 For a process that was returned by one of the _background member functions,
500 kill it hard.
501 """
502 self._kill_background(p)
503 self.background_procs.remove(p)
504
eafe8130
TL
505 def send_signal(self, signal):
506 signal = signal.lower()
507 if signal.lower() not in ['sigstop', 'sigcont', 'sigterm', 'sigkill']:
508 raise NotImplementedError
509
510 self.client_remote.run(args=['sudo', 'kill', '-{0}'.format(signal),
511 self.client_pid], omit_sudo=False)
512
7c673cae
FG
513 def get_global_id(self):
514 raise NotImplementedError()
515
11fdf7f2
TL
516 def get_global_inst(self):
517 raise NotImplementedError()
518
519 def get_global_addr(self):
520 raise NotImplementedError()
521
7c673cae
FG
522 def get_osd_epoch(self):
523 raise NotImplementedError()
524
525 def stat(self, fs_path, wait=True):
526 """
527 stat a file, and return the result as a dictionary like this:
528 {
529 "st_ctime": 1414161137.0,
530 "st_mtime": 1414161137.0,
531 "st_nlink": 33,
532 "st_gid": 0,
533 "st_dev": 16777218,
534 "st_size": 1190,
535 "st_ino": 2,
536 "st_uid": 0,
537 "st_mode": 16877,
538 "st_atime": 1431520593.0
539 }
540
541 Raises exception on absent file.
542 """
543 abs_path = os.path.join(self.mountpoint, fs_path)
544
545 pyscript = dedent("""
546 import os
547 import stat
548 import json
549 import sys
550
551 try:
552 s = os.stat("{path}")
553 except OSError as e:
554 sys.exit(e.errno)
555
556 attrs = ["st_mode", "st_ino", "st_dev", "st_nlink", "st_uid", "st_gid", "st_size", "st_atime", "st_mtime", "st_ctime"]
557 print json.dumps(
558 dict([(a, getattr(s, a)) for a in attrs]),
559 indent=2)
560 """).format(path=abs_path)
561 proc = self._run_python(pyscript)
562 if wait:
563 proc.wait()
564 return json.loads(proc.stdout.getvalue().strip())
565 else:
566 return proc
567
568 def touch(self, fs_path):
569 """
570 Create a dentry if it doesn't already exist. This python
571 implementation exists because the usual command line tool doesn't
572 pass through error codes like EIO.
573
574 :param fs_path:
575 :return:
576 """
577 abs_path = os.path.join(self.mountpoint, fs_path)
578 pyscript = dedent("""
579 import sys
580 import errno
581
582 try:
583 f = open("{path}", "w")
584 f.close()
585 except IOError as e:
586 sys.exit(errno.EIO)
587 """).format(path=abs_path)
588 proc = self._run_python(pyscript)
589 proc.wait()
590
591 def path_to_ino(self, fs_path, follow_symlinks=True):
592 abs_path = os.path.join(self.mountpoint, fs_path)
593
594 if follow_symlinks:
595 pyscript = dedent("""
596 import os
597 import stat
598
599 print os.stat("{path}").st_ino
600 """).format(path=abs_path)
601 else:
602 pyscript = dedent("""
603 import os
604 import stat
605
606 print os.lstat("{path}").st_ino
607 """).format(path=abs_path)
608
609 proc = self._run_python(pyscript)
610 proc.wait()
611 return int(proc.stdout.getvalue().strip())
612
613 def path_to_nlink(self, fs_path):
614 abs_path = os.path.join(self.mountpoint, fs_path)
615
616 pyscript = dedent("""
617 import os
618 import stat
619
620 print os.stat("{path}").st_nlink
621 """).format(path=abs_path)
622
623 proc = self._run_python(pyscript)
624 proc.wait()
625 return int(proc.stdout.getvalue().strip())
626
627 def ls(self, path=None):
628 """
629 Wrap ls: return a list of strings
630 """
631 cmd = ["ls"]
632 if path:
633 cmd.append(path)
634
635 ls_text = self.run_shell(cmd).stdout.getvalue().strip()
636
637 if ls_text:
638 return ls_text.split("\n")
639 else:
640 # Special case because otherwise split on empty string
641 # gives you [''] instead of []
642 return []
643
644 def setfattr(self, path, key, val):
645 """
646 Wrap setfattr.
647
648 :param path: relative to mount point
649 :param key: xattr name
650 :param val: xattr value
651 :return: None
652 """
653 self.run_shell(["setfattr", "-n", key, "-v", val, path])
654
655 def getfattr(self, path, attr):
656 """
657 Wrap getfattr: return the values of a named xattr on one file, or
658 None if the attribute is not found.
659
660 :return: a string
661 """
662 p = self.run_shell(["getfattr", "--only-values", "-n", attr, path], wait=False)
663 try:
664 p.wait()
665 except CommandFailedError as e:
666 if e.exitstatus == 1 and "No such attribute" in p.stderr.getvalue():
667 return None
668 else:
669 raise
670
671 return p.stdout.getvalue()
672
673 def df(self):
674 """
675 Wrap df: return a dict of usage fields in bytes
676 """
677
678 p = self.run_shell(["df", "-B1", "."])
679 lines = p.stdout.getvalue().strip().split("\n")
680 fs, total, used, avail = lines[1].split()[:4]
681 log.warn(lines)
682
683 return {
684 "total": int(total),
685 "used": int(used),
686 "available": int(avail)
687 }