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