]> git.proxmox.com Git - ceph.git/blame - ceph/qa/tasks/cephfs/mount.py
import quincy beta 17.1.0
[ceph.git] / ceph / qa / tasks / cephfs / mount.py
CommitLineData
f67539c2 1import hashlib
7c673cae
FG
2import json
3import logging
4import datetime
f67539c2
TL
5import os
6import re
7c673cae 7import time
f67539c2
TL
8
9from io import StringIO
10from contextlib import contextmanager
7c673cae 11from textwrap import dedent
f67539c2 12from IPy import IP
cd265ab1 13
f67539c2 14from teuthology.contextutil import safe_while
522d829b 15from teuthology.misc import get_file, write_file
7c673cae 16from teuthology.orchestra import run
20effc67
TL
17from teuthology.orchestra.run import Raw
18from teuthology.exceptions import CommandFailedError, ConnectionLostError
f67539c2 19
11fdf7f2 20from tasks.cephfs.filesystem import Filesystem
7c673cae
FG
21
22log = logging.getLogger(__name__)
23
7c673cae 24class CephFSMount(object):
f67539c2
TL
25 def __init__(self, ctx, test_dir, client_id, client_remote,
26 client_keyring_path=None, hostfs_mntpt=None,
27 cephfs_name=None, cephfs_mntpt=None, brxnet=None):
7c673cae
FG
28 """
29 :param test_dir: Global teuthology test dir
30 :param client_id: Client ID, the 'foo' in client.foo
f67539c2
TL
31 :param client_keyring_path: path to keyring for given client_id
32 :param client_remote: Remote instance for the host where client will
33 run
34 :param hostfs_mntpt: Path to directory on the FS on which Ceph FS will
35 be mounted
36 :param cephfs_name: Name of Ceph FS to be mounted
37 :param cephfs_mntpt: Path to directory inside Ceph FS that will be
38 mounted as root
7c673cae 39 """
f67539c2 40 self.mounted = False
11fdf7f2 41 self.ctx = ctx
7c673cae 42 self.test_dir = test_dir
f67539c2
TL
43
44 self._verify_attrs(client_id=client_id,
45 client_keyring_path=client_keyring_path,
46 hostfs_mntpt=hostfs_mntpt, cephfs_name=cephfs_name,
47 cephfs_mntpt=cephfs_mntpt)
48
7c673cae 49 self.client_id = client_id
f67539c2 50 self.client_keyring_path = client_keyring_path
7c673cae 51 self.client_remote = client_remote
f67539c2
TL
52 if hostfs_mntpt:
53 self.hostfs_mntpt = hostfs_mntpt
54 self.hostfs_mntpt_dirname = os.path.basename(self.hostfs_mntpt)
55 else:
56 self.hostfs_mntpt = os.path.join(self.test_dir, f'mnt.{self.client_id}')
57 self.cephfs_name = cephfs_name
58 self.cephfs_mntpt = cephfs_mntpt
59
20effc67
TL
60 self.cluster_name = 'ceph' # TODO: use config['cluster']
61
11fdf7f2 62 self.fs = None
7c673cae 63
f67539c2
TL
64 self._netns_name = None
65 self.nsid = -1
66 if brxnet is None:
67 self.ceph_brx_net = '192.168.0.0/16'
68 else:
69 self.ceph_brx_net = brxnet
70
7c673cae
FG
71 self.test_files = ['a', 'b', 'c']
72
73 self.background_procs = []
74
f67539c2
TL
75 # This will cleanup the stale netnses, which are from the
76 # last failed test cases.
77 @staticmethod
78 def cleanup_stale_netnses_and_bridge(remote):
79 p = remote.run(args=['ip', 'netns', 'list'],
80 stdout=StringIO(), timeout=(5*60))
81 p = p.stdout.getvalue().strip()
82
83 # Get the netns name list
84 netns_list = re.findall(r'ceph-ns-[^()\s][-.\w]+[^():\s]', p)
85
86 # Remove the stale netnses
87 for ns in netns_list:
88 ns_name = ns.split()[0]
89 args = ['sudo', 'ip', 'netns', 'delete', '{0}'.format(ns_name)]
90 try:
91 remote.run(args=args, timeout=(5*60), omit_sudo=False)
92 except Exception:
93 pass
94
95 # Remove the stale 'ceph-brx'
96 try:
97 args = ['sudo', 'ip', 'link', 'delete', 'ceph-brx']
98 remote.run(args=args, timeout=(5*60), omit_sudo=False)
99 except Exception:
100 pass
101
102 def _parse_netns_name(self):
103 self._netns_name = '-'.join(["ceph-ns",
104 re.sub(r'/+', "-", self.mountpoint)])
105
7c673cae
FG
106 @property
107 def mountpoint(self):
f67539c2
TL
108 if self.hostfs_mntpt == None:
109 self.hostfs_mntpt = os.path.join(self.test_dir,
110 self.hostfs_mntpt_dirname)
111 return self.hostfs_mntpt
9f95a23c
TL
112
113 @mountpoint.setter
114 def mountpoint(self, path):
115 if not isinstance(path, str):
116 raise RuntimeError('path should be of str type.')
f67539c2
TL
117 self._mountpoint = self.hostfs_mntpt = path
118
119 @property
120 def netns_name(self):
121 if self._netns_name == None:
122 self._parse_netns_name()
123 return self._netns_name
124
125 @netns_name.setter
126 def netns_name(self, name):
127 self._netns_name = name
128
20effc67
TL
129 def assert_that_ceph_fs_exists(self):
130 output = self.ctx.managers[self.cluster_name].raw_cluster_cmd("fs", "ls")
131 if self.cephfs_name:
132 assert self.cephfs_name in output, \
133 'expected ceph fs is not present on the cluster'
134 log.info(f'Mounting Ceph FS {self.cephfs_name}; just confirmed its presence on cluster')
135 else:
136 assert 'No filesystems enabled' not in output, \
137 'ceph cluster has no ceph fs, not even the default ceph fs'
138 log.info('Mounting default Ceph FS; just confirmed its presence on cluster')
139
f67539c2
TL
140 def assert_and_log_minimum_mount_details(self):
141 """
142 Make sure we have minimum details required for mounting. Ideally, this
143 method should be called at the beginning of the mount method.
144 """
145 if not self.client_id or not self.client_remote or \
146 not self.hostfs_mntpt:
147 errmsg = ('Mounting CephFS requires that at least following '
148 'details to be provided -\n'
149 '1. the client ID,\n2. the mountpoint and\n'
150 '3. the remote machine where CephFS will be mounted.\n')
151 raise RuntimeError(errmsg)
152
20effc67
TL
153 self.assert_that_ceph_fs_exists()
154
f67539c2
TL
155 log.info('Mounting Ceph FS. Following are details of mount; remember '
156 '"None" represents Python type None -')
157 log.info(f'self.client_remote.hostname = {self.client_remote.hostname}')
158 log.info(f'self.client.name = client.{self.client_id}')
159 log.info(f'self.hostfs_mntpt = {self.hostfs_mntpt}')
160 log.info(f'self.cephfs_name = {self.cephfs_name}')
161 log.info(f'self.cephfs_mntpt = {self.cephfs_mntpt}')
162 log.info(f'self.client_keyring_path = {self.client_keyring_path}')
163 if self.client_keyring_path:
164 log.info('keyring content -\n' +
165 get_file(self.client_remote, self.client_keyring_path,
166 sudo=True).decode())
7c673cae
FG
167
168 def is_mounted(self):
f67539c2 169 return self.mounted
7c673cae 170
11fdf7f2
TL
171 def setupfs(self, name=None):
172 if name is None and self.fs is not None:
173 # Previous mount existed, reuse the old name
174 name = self.fs.name
175 self.fs = Filesystem(self.ctx, name=name)
176 log.info('Wait for MDS to reach steady state...')
177 self.fs.wait_for_daemons()
178 log.info('Ready to start {}...'.format(type(self).__name__))
179
20effc67
TL
180 def _create_mntpt(self):
181 self.client_remote.run(args=f'mkdir -p -v {self.hostfs_mntpt}',
182 timeout=60)
183 # Use 0000 mode to prevent undesired modifications to the mountpoint on
184 # the local file system.
185 self.client_remote.run(args=f'chmod 0000 {self.hostfs_mntpt}',
186 timeout=60)
187
188 @property
189 def _nsenter_args(self):
190 return ['nsenter', f'--net=/var/run/netns/{self.netns_name}']
191
192 def _set_filemode_on_mntpt(self):
193 stderr = StringIO()
194 try:
195 self.client_remote.run(
196 args=['sudo', 'chmod', '1777', self.hostfs_mntpt],
197 stderr=stderr, timeout=(5*60))
198 except CommandFailedError:
199 # the client does not have write permissions in the caps it holds
200 # for the Ceph FS that was just mounted.
201 if 'permission denied' in stderr.getvalue().lower():
202 pass
203
f67539c2
TL
204 def _setup_brx_and_nat(self):
205 # The ip for ceph-brx should be
206 ip = IP(self.ceph_brx_net)[-2]
207 mask = self.ceph_brx_net.split('/')[1]
208 brd = IP(self.ceph_brx_net).broadcast()
209
210 brx = self.client_remote.run(args=['ip', 'addr'], stderr=StringIO(),
211 stdout=StringIO(), timeout=(5*60))
212 brx = re.findall(r'inet .* ceph-brx', brx.stdout.getvalue())
213 if brx:
214 # If the 'ceph-brx' already exists, then check whether
215 # the new net is conflicting with it
216 _ip, _mask = brx[0].split()[1].split('/', 1)
217 if _ip != "{}".format(ip) or _mask != mask:
218 raise RuntimeError("Conflict with existing ceph-brx {0}, new {1}/{2}".format(brx[0].split()[1], ip, mask))
219
220 # Setup the ceph-brx and always use the last valid IP
221 if not brx:
222 log.info("Setuping the 'ceph-brx' with {0}/{1}".format(ip, mask))
223
224 self.run_shell_payload(f"""
225 set -e
226 sudo ip link add name ceph-brx type bridge
227 sudo ip addr flush dev ceph-brx
228 sudo ip link set ceph-brx up
229 sudo ip addr add {ip}/{mask} brd {brd} dev ceph-brx
230 """, timeout=(5*60), omit_sudo=False, cwd='/')
231
232 args = "echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward"
233 self.client_remote.run(args=args, timeout=(5*60), omit_sudo=False)
234
235 # Setup the NAT
236 p = self.client_remote.run(args=['route'], stderr=StringIO(),
237 stdout=StringIO(), timeout=(5*60))
238 p = re.findall(r'default .*', p.stdout.getvalue())
239 if p == False:
240 raise RuntimeError("No default gw found")
241 gw = p[0].split()[7]
242
243 self.run_shell_payload(f"""
244 set -e
245 sudo iptables -A FORWARD -o {gw} -i ceph-brx -j ACCEPT
246 sudo iptables -A FORWARD -i {gw} -o ceph-brx -j ACCEPT
247 sudo iptables -t nat -A POSTROUTING -s {ip}/{mask} -o {gw} -j MASQUERADE
248 """, timeout=(5*60), omit_sudo=False, cwd='/')
249
250 def _setup_netns(self):
251 p = self.client_remote.run(args=['ip', 'netns', 'list'],
252 stderr=StringIO(), stdout=StringIO(),
253 timeout=(5*60)).stdout.getvalue().strip()
254
255 # Get the netns name list
256 netns_list = re.findall(r'[^()\s][-.\w]+[^():\s]', p)
257
258 out = re.search(r"{0}".format(self.netns_name), p)
259 if out is None:
260 # Get an uniq nsid for the new netns
261 nsid = 0
262 p = self.client_remote.run(args=['ip', 'netns', 'list-id'],
263 stderr=StringIO(), stdout=StringIO(),
264 timeout=(5*60)).stdout.getvalue()
265 while True:
266 out = re.search(r"nsid {} ".format(nsid), p)
267 if out is None:
268 break
269
270 nsid += 1
271
272 # Add one new netns and set it id
273 self.run_shell_payload(f"""
274 set -e
275 sudo ip netns add {self.netns_name}
276 sudo ip netns set {self.netns_name} {nsid}
277 """, timeout=(5*60), omit_sudo=False, cwd='/')
278 self.nsid = nsid;
279 else:
280 # The netns already exists and maybe suspended by self.kill()
281 self.resume_netns();
282
283 nsid = int(re.search(r"{0} \(id: (\d+)\)".format(self.netns_name), p).group(1))
284 self.nsid = nsid;
285 return
286
287 # Get one ip address for netns
288 ips = IP(self.ceph_brx_net)
289 for ip in ips:
290 found = False
291 if ip == ips[0]:
292 continue
293 if ip == ips[-2]:
294 raise RuntimeError("we have ran out of the ip addresses")
295
296 for ns in netns_list:
297 ns_name = ns.split()[0]
298 args = ['sudo', 'ip', 'netns', 'exec', '{0}'.format(ns_name), 'ip', 'addr']
299 try:
300 p = self.client_remote.run(args=args, stderr=StringIO(),
301 stdout=StringIO(), timeout=(5*60),
302 omit_sudo=False)
303 q = re.search("{0}".format(ip), p.stdout.getvalue())
304 if q is not None:
305 found = True
306 break
307 except CommandFailedError:
308 if "No such file or directory" in p.stderr.getvalue():
309 pass
310 if "Invalid argument" in p.stderr.getvalue():
311 pass
312
313 if found == False:
314 break
315
316 mask = self.ceph_brx_net.split('/')[1]
317 brd = IP(self.ceph_brx_net).broadcast()
318
319 log.info("Setuping the netns '{0}' with {1}/{2}".format(self.netns_name, ip, mask))
320
321 # Setup the veth interfaces
322 brxip = IP(self.ceph_brx_net)[-2]
323 self.run_shell_payload(f"""
324 set -e
325 sudo ip link add veth0 netns {self.netns_name} type veth peer name brx.{nsid}
326 sudo ip netns exec {self.netns_name} ip addr add {ip}/{mask} brd {brd} dev veth0
327 sudo ip netns exec {self.netns_name} ip link set veth0 up
328 sudo ip netns exec {self.netns_name} ip link set lo up
329 sudo ip netns exec {self.netns_name} ip route add default via {brxip}
330 """, timeout=(5*60), omit_sudo=False, cwd='/')
331
332 # Bring up the brx interface and join it to 'ceph-brx'
333 self.run_shell_payload(f"""
334 set -e
335 sudo ip link set brx.{nsid} up
336 sudo ip link set dev brx.{nsid} master ceph-brx
337 """, timeout=(5*60), omit_sudo=False, cwd='/')
338
339 def _cleanup_netns(self):
340 if self.nsid == -1:
341 return
342 log.info("Removing the netns '{0}'".format(self.netns_name))
343
344 # Delete the netns and the peer veth interface
345 self.run_shell_payload(f"""
346 set -e
347 sudo ip link set brx.{self.nsid} down
348 sudo ip link delete dev brx.{self.nsid}
349 sudo ip netns delete {self.netns_name}
350 """, timeout=(5*60), omit_sudo=False, cwd='/')
351
352 self.nsid = -1
353
354 def _cleanup_brx_and_nat(self):
355 brx = self.client_remote.run(args=['ip', 'addr'], stderr=StringIO(),
356 stdout=StringIO(), timeout=(5*60))
357 brx = re.findall(r'inet .* ceph-brx', brx.stdout.getvalue())
358 if not brx:
359 return
360
361 # If we are the last netns, will delete the ceph-brx
362 args = ['sudo', 'ip', 'link', 'show']
363 p = self.client_remote.run(args=args, stdout=StringIO(),
364 timeout=(5*60), omit_sudo=False)
365 _list = re.findall(r'brx\.', p.stdout.getvalue().strip())
366 if len(_list) != 0:
367 return
368
369 log.info("Removing the 'ceph-brx'")
370
371 self.run_shell_payload("""
372 set -e
373 sudo ip link set ceph-brx down
374 sudo ip link delete ceph-brx
375 """, timeout=(5*60), omit_sudo=False, cwd='/')
376
377 # Drop the iptables NAT rules
378 ip = IP(self.ceph_brx_net)[-2]
379 mask = self.ceph_brx_net.split('/')[1]
380
381 p = self.client_remote.run(args=['route'], stderr=StringIO(),
382 stdout=StringIO(), timeout=(5*60))
383 p = re.findall(r'default .*', p.stdout.getvalue())
384 if p == False:
385 raise RuntimeError("No default gw found")
386 gw = p[0].split()[7]
387 self.run_shell_payload(f"""
388 set -e
389 sudo iptables -D FORWARD -o {gw} -i ceph-brx -j ACCEPT
390 sudo iptables -D FORWARD -i {gw} -o ceph-brx -j ACCEPT
391 sudo iptables -t nat -D POSTROUTING -s {ip}/{mask} -o {gw} -j MASQUERADE
392 """, timeout=(5*60), omit_sudo=False, cwd='/')
393
394 def setup_netns(self):
395 """
396 Setup the netns for the mountpoint.
397 """
398 log.info("Setting the '{0}' netns for '{1}'".format(self._netns_name, self.mountpoint))
399 self._setup_brx_and_nat()
400 self._setup_netns()
401
402 def cleanup_netns(self):
403 """
404 Cleanup the netns for the mountpoint.
405 """
406 # We will defer cleaning the netnses and bridge until the last
407 # mountpoint is unmounted, this will be a temporary work around
408 # for issue#46282.
409
410 # log.info("Cleaning the '{0}' netns for '{1}'".format(self._netns_name, self.mountpoint))
411 # self._cleanup_netns()
412 # self._cleanup_brx_and_nat()
413
414 def suspend_netns(self):
415 """
416 Suspend the netns veth interface.
417 """
418 if self.nsid == -1:
419 return
420
421 log.info("Suspending the '{0}' netns for '{1}'".format(self._netns_name, self.mountpoint))
422
423 args = ['sudo', 'ip', 'link', 'set', 'brx.{0}'.format(self.nsid), 'down']
424 self.client_remote.run(args=args, timeout=(5*60), omit_sudo=False)
425
426 def resume_netns(self):
427 """
428 Resume the netns veth interface.
429 """
430 if self.nsid == -1:
431 return
432
433 log.info("Resuming the '{0}' netns for '{1}'".format(self._netns_name, self.mountpoint))
434
435 args = ['sudo', 'ip', 'link', 'set', 'brx.{0}'.format(self.nsid), 'up']
436 self.client_remote.run(args=args, timeout=(5*60), omit_sudo=False)
437
20effc67 438 def mount(self, mntopts=[], check_status=True, **kwargs):
f67539c2
TL
439 """
440 kwargs expects its members to be same as the arguments accepted by
441 self.update_attrs().
442 """
7c673cae
FG
443 raise NotImplementedError()
444
f67539c2
TL
445 def mount_wait(self, **kwargs):
446 """
447 Accepts arguments same as self.mount().
448 """
449 self.mount(**kwargs)
e306af50
TL
450 self.wait_until_mounted()
451
7c673cae
FG
452 def umount(self):
453 raise NotImplementedError()
454
f67539c2 455 def umount_wait(self, force=False, require_clean=False, timeout=None):
7c673cae
FG
456 """
457
458 :param force: Expect that the mount will not shutdown cleanly: kill
459 it hard.
460 :param require_clean: Wait for the Ceph client associated with the
461 mount (e.g. ceph-fuse) to terminate, and
462 raise if it doesn't do so cleanly.
f67539c2 463 :param timeout: amount of time to be waited for umount command to finish
7c673cae
FG
464 :return:
465 """
466 raise NotImplementedError()
467
f67539c2
TL
468 def _verify_attrs(self, **kwargs):
469 """
470 Verify that client_id, client_keyring_path, client_remote, hostfs_mntpt,
471 cephfs_name, cephfs_mntpt are either type str or None.
472 """
473 for k, v in kwargs.items():
474 if v is not None and not isinstance(v, str):
475 raise RuntimeError('value of attributes should be either str '
476 f'or None. {k} - {v}')
477
478 def update_attrs(self, client_id=None, client_keyring_path=None,
479 client_remote=None, hostfs_mntpt=None, cephfs_name=None,
480 cephfs_mntpt=None):
481 if not (client_id or client_keyring_path or client_remote or
482 cephfs_name or cephfs_mntpt or hostfs_mntpt):
483 return
484
485 self._verify_attrs(client_id=client_id,
486 client_keyring_path=client_keyring_path,
487 hostfs_mntpt=hostfs_mntpt, cephfs_name=cephfs_name,
488 cephfs_mntpt=cephfs_mntpt)
489
490 if client_id:
491 self.client_id = client_id
492 if client_keyring_path:
493 self.client_keyring_path = client_keyring_path
494 if client_remote:
495 self.client_remote = client_remote
496 if hostfs_mntpt:
497 self.hostfs_mntpt = hostfs_mntpt
498 if cephfs_name:
499 self.cephfs_name = cephfs_name
500 if cephfs_mntpt:
501 self.cephfs_mntpt = cephfs_mntpt
502
503 def remount(self, **kwargs):
504 """
505 Update mount object's attributes and attempt remount with these
506 new values for these attrbiutes.
507
508 1. Run umount_wait().
509 2. Run update_attrs().
510 3. Run mount().
511
20effc67
TL
512 Accepts arguments of self.mount() and self.update_attrs() with 1
513 exception: wait accepted too which can be True or False.
f67539c2
TL
514 """
515 self.umount_wait()
516 assert not self.mounted
517
518 mntopts = kwargs.pop('mntopts', [])
f67539c2
TL
519 check_status = kwargs.pop('check_status', True)
520 wait = kwargs.pop('wait', True)
521
522 self.update_attrs(**kwargs)
523
20effc67 524 retval = self.mount(mntopts=mntopts, check_status=check_status)
f67539c2
TL
525 # avoid this scenario (again): mount command might've failed and
526 # check_status might have silenced the exception, yet we attempt to
527 # wait which might lead to an error.
528 if retval is None and wait:
529 self.wait_until_mounted()
530
531 return retval
7c673cae
FG
532
533 def kill(self):
f67539c2
TL
534 """
535 Suspend the netns veth interface to make the client disconnected
536 from the ceph cluster
537 """
538 log.info('Killing connection on {0}...'.format(self.client_remote.name))
539 self.suspend_netns()
540
541 def kill_cleanup(self):
542 """
543 Follow up ``kill`` to get to a clean unmounted state.
544 """
545 log.info('Cleaning up killed connection on {0}'.format(self.client_remote.name))
546 self.umount_wait(force=True)
7c673cae
FG
547
548 def cleanup(self):
f67539c2
TL
549 """
550 Remove the mount point.
551
552 Prerequisite: the client is not mounted.
553 """
554 log.info('Cleaning up mount {0}'.format(self.client_remote.name))
555 stderr = StringIO()
556 try:
557 self.client_remote.run(args=['rmdir', '--', self.mountpoint],
558 cwd=self.test_dir, stderr=stderr,
559 timeout=(60*5), check_status=False)
560 except CommandFailedError:
561 if "no such file or directory" not in stderr.getvalue().lower():
562 raise
563
564 self.cleanup_netns()
7c673cae
FG
565
566 def wait_until_mounted(self):
567 raise NotImplementedError()
568
569 def get_keyring_path(self):
a4b75251 570 # N.B.: default keyring is /etc/ceph/ceph.keyring; see ceph.py and generate_caps
7c673cae
FG
571 return '/etc/ceph/ceph.client.{id}.keyring'.format(id=self.client_id)
572
f67539c2
TL
573 def get_key_from_keyfile(self):
574 # XXX: don't call run_shell(), since CephFS might be unmounted.
575 keyring = self.client_remote.run(
576 args=['sudo', 'cat', self.client_keyring_path], stdout=StringIO(),
577 omit_sudo=False).stdout.getvalue()
578 for line in keyring.split('\n'):
579 if line.find('key') != -1:
580 return line[line.find('=') + 1 : ].strip()
581
7c673cae
FG
582 @property
583 def config_path(self):
584 """
585 Path to ceph.conf: override this if you're not a normal systemwide ceph install
586 :return: stringv
587 """
588 return "/etc/ceph/ceph.conf"
589
590 @contextmanager
f67539c2 591 def mounted_wait(self):
7c673cae
FG
592 """
593 A context manager, from an initially unmounted state, to mount
594 this, yield, and then unmount and clean up.
595 """
596 self.mount()
597 self.wait_until_mounted()
598 try:
599 yield
600 finally:
601 self.umount_wait()
602
9f95a23c
TL
603 def create_file(self, filename='testfile', dirname=None, user=None,
604 check_status=True):
605 assert(self.is_mounted())
606
607 if not os.path.isabs(filename):
608 if dirname:
609 if os.path.isabs(dirname):
610 path = os.path.join(dirname, filename)
611 else:
f67539c2 612 path = os.path.join(self.hostfs_mntpt, dirname, filename)
9f95a23c 613 else:
f67539c2 614 path = os.path.join(self.hostfs_mntpt, filename)
9f95a23c
TL
615 else:
616 path = filename
617
618 if user:
619 args = ['sudo', '-u', user, '-s', '/bin/bash', '-c', 'touch ' + path]
620 else:
621 args = 'touch ' + path
622
623 return self.client_remote.run(args=args, check_status=check_status)
624
7c673cae
FG
625 def create_files(self):
626 assert(self.is_mounted())
627
628 for suffix in self.test_files:
629 log.info("Creating file {0}".format(suffix))
630 self.client_remote.run(args=[
522d829b 631 'touch', os.path.join(self.hostfs_mntpt, suffix)
7c673cae
FG
632 ])
633
9f95a23c
TL
634 def test_create_file(self, filename='testfile', dirname=None, user=None,
635 check_status=True):
636 return self.create_file(filename=filename, dirname=dirname, user=user,
637 check_status=False)
638
7c673cae
FG
639 def check_files(self):
640 assert(self.is_mounted())
641
642 for suffix in self.test_files:
643 log.info("Checking file {0}".format(suffix))
644 r = self.client_remote.run(args=[
522d829b 645 'ls', os.path.join(self.hostfs_mntpt, suffix)
7c673cae
FG
646 ], check_status=False)
647 if r.exitstatus != 0:
648 raise RuntimeError("Expected file {0} not found".format(suffix))
649
cd265ab1
TL
650 def write_file(self, path, data, perms=None):
651 """
652 Write the given data at the given path and set the given perms to the
653 file on the path.
654 """
f67539c2
TL
655 if path.find(self.hostfs_mntpt) == -1:
656 path = os.path.join(self.hostfs_mntpt, path)
cd265ab1 657
522d829b 658 write_file(self.client_remote, path, data)
cd265ab1
TL
659
660 if perms:
661 self.run_shell(args=f'chmod {perms} {path}')
662
663 def read_file(self, path):
664 """
665 Return the data from the file on given path.
666 """
f67539c2
TL
667 if path.find(self.hostfs_mntpt) == -1:
668 path = os.path.join(self.hostfs_mntpt, path)
cd265ab1 669
522d829b 670 return self.run_shell(args=['cat', path]).\
cd265ab1
TL
671 stdout.getvalue().strip()
672
7c673cae
FG
673 def create_destroy(self):
674 assert(self.is_mounted())
675
676 filename = "{0} {1}".format(datetime.datetime.now(), self.client_id)
677 log.debug("Creating test file {0}".format(filename))
678 self.client_remote.run(args=[
522d829b 679 'touch', os.path.join(self.hostfs_mntpt, filename)
7c673cae
FG
680 ])
681 log.debug("Deleting test file {0}".format(filename))
682 self.client_remote.run(args=[
522d829b 683 'rm', '-f', os.path.join(self.hostfs_mntpt, filename)
7c673cae
FG
684 ])
685
522d829b
TL
686 def _run_python(self, pyscript, py_version='python3', sudo=False):
687 args = []
688 if sudo:
689 args.append('sudo')
690 args += ['adjust-ulimits', 'daemon-helper', 'kill', py_version, '-c', pyscript]
691 return self.client_remote.run(args=args, wait=False, stdin=run.PIPE, stdout=StringIO())
91327a77 692
522d829b
TL
693 def run_python(self, pyscript, py_version='python3', sudo=False):
694 p = self._run_python(pyscript, py_version, sudo=sudo)
7c673cae 695 p.wait()
f67539c2
TL
696 return p.stdout.getvalue().strip()
697
522d829b 698 def run_shell(self, args, timeout=900, **kwargs):
f67539c2 699 args = args.split() if isinstance(args, str) else args
522d829b
TL
700 kwargs.pop('omit_sudo', False)
701 sudo = kwargs.pop('sudo', False)
f67539c2
TL
702 cwd = kwargs.pop('cwd', self.mountpoint)
703 stdout = kwargs.pop('stdout', StringIO())
704 stderr = kwargs.pop('stderr', StringIO())
705
522d829b
TL
706 if sudo:
707 args.insert(0, 'sudo')
708
f67539c2 709 return self.client_remote.run(args=args, cwd=cwd, timeout=timeout, stdout=stdout, stderr=stderr, **kwargs)
7c673cae 710
f6b5b4d7
TL
711 def run_shell_payload(self, payload, **kwargs):
712 return self.run_shell(["bash", "-c", Raw(f"'{payload}'")], **kwargs)
713
f67539c2
TL
714 def run_as_user(self, **kwargs):
715 """
716 Besides the arguments defined for run_shell() this method also
717 accepts argument 'user'.
718 """
719 args = kwargs.pop('args')
720 user = kwargs.pop('user')
9f95a23c 721 if isinstance(args, str):
f67539c2
TL
722 args = ['sudo', '-u', user, '-s', '/bin/bash', '-c', args]
723 elif isinstance(args, list):
724 cmdlist = args
725 cmd = ''
726 for i in cmdlist:
727 cmd = cmd + i + ' '
728 # get rid of extra space at the end.
729 cmd = cmd[:-1]
730
731 args = ['sudo', '-u', user, '-s', '/bin/bash', '-c', cmd]
732
733 kwargs['args'] = args
734 return self.run_shell(**kwargs)
9f95a23c 735
f67539c2
TL
736 def run_as_root(self, **kwargs):
737 """
738 Accepts same arguments as run_shell().
739 """
740 kwargs['user'] = 'root'
741 return self.run_as_user(**kwargs)
742
743 def _verify(self, proc, retval=None, errmsg=None):
744 if retval:
745 msg = ('expected return value: {}\nreceived return value: '
746 '{}\n'.format(retval, proc.returncode))
747 assert proc.returncode == retval, msg
748
749 if errmsg:
750 stderr = proc.stderr.getvalue().lower()
751 msg = ('didn\'t find given string in stderr -\nexpected string: '
752 '{}\nreceived error message: {}\nnote: received error '
753 'message is converted to lowercase'.format(errmsg, stderr))
754 assert errmsg in stderr, msg
755
756 def negtestcmd(self, args, retval=None, errmsg=None, stdin=None,
757 cwd=None, wait=True):
758 """
759 Conduct a negative test for the given command.
760
761 retval and errmsg are parameters to confirm the cause of command
762 failure.
763 """
764 proc = self.run_shell(args=args, wait=wait, stdin=stdin, cwd=cwd,
765 check_status=False)
766 self._verify(proc, retval, errmsg)
767 return proc
768
769 def negtestcmd_as_user(self, args, user, retval=None, errmsg=None,
770 stdin=None, cwd=None, wait=True):
771 proc = self.run_as_user(args=args, user=user, wait=wait, stdin=stdin,
772 cwd=cwd, check_status=False)
773 self._verify(proc, retval, errmsg)
774 return proc
775
776 def negtestcmd_as_root(self, args, retval=None, errmsg=None, stdin=None,
777 cwd=None, wait=True):
778 proc = self.run_as_root(args=args, wait=wait, stdin=stdin, cwd=cwd,
779 check_status=False)
780 self._verify(proc, retval, errmsg)
781 return proc
7c673cae
FG
782
783 def open_no_data(self, basename):
784 """
785 A pure metadata operation
786 """
787 assert(self.is_mounted())
788
f67539c2 789 path = os.path.join(self.hostfs_mntpt, basename)
7c673cae
FG
790
791 p = self._run_python(dedent(
792 """
793 f = open("{path}", 'w')
794 """.format(path=path)
795 ))
796 p.wait()
797
494da23a 798 def open_background(self, basename="background_file", write=True):
7c673cae
FG
799 """
800 Open a file for writing, then block such that the client
801 will hold a capability.
802
803 Don't return until the remote process has got as far as opening
804 the file, then return the RemoteProcess instance.
805 """
806 assert(self.is_mounted())
807
f67539c2 808 path = os.path.join(self.hostfs_mntpt, basename)
7c673cae 809
494da23a
TL
810 if write:
811 pyscript = dedent("""
812 import time
813
9f95a23c
TL
814 with open("{path}", 'w') as f:
815 f.write('content')
816 f.flush()
817 f.write('content2')
818 while True:
819 time.sleep(1)
494da23a
TL
820 """).format(path=path)
821 else:
822 pyscript = dedent("""
823 import time
824
9f95a23c
TL
825 with open("{path}", 'r') as f:
826 while True:
827 time.sleep(1)
494da23a 828 """).format(path=path)
7c673cae
FG
829
830 rproc = self._run_python(pyscript)
831 self.background_procs.append(rproc)
832
833 # This wait would not be sufficient if the file had already
834 # existed, but it's simple and in practice users of open_background
835 # are not using it on existing files.
836 self.wait_for_visible(basename)
837
838 return rproc
839
494da23a 840 def wait_for_dir_empty(self, dirname, timeout=30):
f67539c2
TL
841 dirpath = os.path.join(self.hostfs_mntpt, dirname)
842 with safe_while(sleep=5, tries=(timeout//5)) as proceed:
843 while proceed():
844 p = self.run_shell_payload(f"stat -c %h {dirpath}")
845 nr_links = int(p.stdout.getvalue().strip())
846 if nr_links == 2:
847 return
494da23a 848
7c673cae
FG
849 def wait_for_visible(self, basename="background_file", timeout=30):
850 i = 0
851 while i < timeout:
852 r = self.client_remote.run(args=[
522d829b 853 'stat', os.path.join(self.hostfs_mntpt, basename)
7c673cae
FG
854 ], check_status=False)
855 if r.exitstatus == 0:
856 log.debug("File {0} became visible from {1} after {2}s".format(
857 basename, self.client_id, i))
858 return
859 else:
860 time.sleep(1)
861 i += 1
862
863 raise RuntimeError("Timed out after {0}s waiting for {1} to become visible from {2}".format(
864 i, basename, self.client_id))
865
866 def lock_background(self, basename="background_file", do_flock=True):
867 """
868 Open and lock a files for writing, hold the lock in a background process
869 """
870 assert(self.is_mounted())
871
f67539c2 872 path = os.path.join(self.hostfs_mntpt, basename)
7c673cae
FG
873
874 script_builder = """
875 import time
876 import fcntl
877 import struct"""
878 if do_flock:
879 script_builder += """
880 f1 = open("{path}-1", 'w')
881 fcntl.flock(f1, fcntl.LOCK_EX | fcntl.LOCK_NB)"""
882 script_builder += """
883 f2 = open("{path}-2", 'w')
884 lockdata = struct.pack('hhllhh', fcntl.F_WRLCK, 0, 0, 0, 0, 0)
885 fcntl.fcntl(f2, fcntl.F_SETLK, lockdata)
886 while True:
887 time.sleep(1)
888 """
889
890 pyscript = dedent(script_builder).format(path=path)
891
31f18b77 892 log.info("lock_background file {0}".format(basename))
7c673cae
FG
893 rproc = self._run_python(pyscript)
894 self.background_procs.append(rproc)
895 return rproc
896
31f18b77
FG
897 def lock_and_release(self, basename="background_file"):
898 assert(self.is_mounted())
899
f67539c2 900 path = os.path.join(self.hostfs_mntpt, basename)
31f18b77
FG
901
902 script = """
903 import time
904 import fcntl
905 import struct
906 f1 = open("{path}-1", 'w')
907 fcntl.flock(f1, fcntl.LOCK_EX)
908 f2 = open("{path}-2", 'w')
909 lockdata = struct.pack('hhllhh', fcntl.F_WRLCK, 0, 0, 0, 0, 0)
910 fcntl.fcntl(f2, fcntl.F_SETLK, lockdata)
911 """
912 pyscript = dedent(script).format(path=path)
913
914 log.info("lock_and_release file {0}".format(basename))
915 return self._run_python(pyscript)
916
7c673cae
FG
917 def check_filelock(self, basename="background_file", do_flock=True):
918 assert(self.is_mounted())
919
f67539c2 920 path = os.path.join(self.hostfs_mntpt, basename)
7c673cae
FG
921
922 script_builder = """
923 import fcntl
924 import errno
925 import struct"""
926 if do_flock:
927 script_builder += """
928 f1 = open("{path}-1", 'r')
929 try:
930 fcntl.flock(f1, fcntl.LOCK_EX | fcntl.LOCK_NB)
9f95a23c 931 except IOError as e:
7c673cae
FG
932 if e.errno == errno.EAGAIN:
933 pass
934 else:
935 raise RuntimeError("flock on file {path}-1 not found")"""
936 script_builder += """
937 f2 = open("{path}-2", 'r')
938 try:
939 lockdata = struct.pack('hhllhh', fcntl.F_WRLCK, 0, 0, 0, 0, 0)
940 fcntl.fcntl(f2, fcntl.F_SETLK, lockdata)
9f95a23c 941 except IOError as e:
7c673cae
FG
942 if e.errno == errno.EAGAIN:
943 pass
944 else:
945 raise RuntimeError("posix lock on file {path}-2 not found")
946 """
947 pyscript = dedent(script_builder).format(path=path)
948
949 log.info("check lock on file {0}".format(basename))
950 self.client_remote.run(args=[
522d829b 951 'python3', '-c', pyscript
7c673cae
FG
952 ])
953
954 def write_background(self, basename="background_file", loop=False):
955 """
956 Open a file for writing, complete as soon as you can
957 :param basename:
958 :return:
959 """
960 assert(self.is_mounted())
961
f67539c2 962 path = os.path.join(self.hostfs_mntpt, basename)
7c673cae
FG
963
964 pyscript = dedent("""
965 import os
966 import time
967
9f95a23c 968 fd = os.open("{path}", os.O_RDWR | os.O_CREAT, 0o644)
7c673cae
FG
969 try:
970 while True:
9f95a23c 971 os.write(fd, b'content')
7c673cae
FG
972 time.sleep(1)
973 if not {loop}:
974 break
9f95a23c 975 except IOError as e:
7c673cae
FG
976 pass
977 os.close(fd)
978 """).format(path=path, loop=str(loop))
979
980 rproc = self._run_python(pyscript)
981 self.background_procs.append(rproc)
982 return rproc
983
984 def write_n_mb(self, filename, n_mb, seek=0, wait=True):
985 """
986 Write the requested number of megabytes to a file
987 """
988 assert(self.is_mounted())
989
990 return self.run_shell(["dd", "if=/dev/urandom", "of={0}".format(filename),
991 "bs=1M", "conv=fdatasync",
e306af50
TL
992 "count={0}".format(int(n_mb)),
993 "seek={0}".format(int(seek))
7c673cae
FG
994 ], wait=wait)
995
996 def write_test_pattern(self, filename, size):
997 log.info("Writing {0} bytes to {1}".format(size, filename))
998 return self.run_python(dedent("""
999 import zlib
1000 path = "{path}"
9f95a23c
TL
1001 with open(path, 'w') as f:
1002 for i in range(0, {size}):
1003 val = zlib.crc32(str(i).encode('utf-8')) & 7
1004 f.write(chr(val))
7c673cae 1005 """.format(
f67539c2 1006 path=os.path.join(self.hostfs_mntpt, filename),
7c673cae
FG
1007 size=size
1008 )))
1009
1010 def validate_test_pattern(self, filename, size):
1011 log.info("Validating {0} bytes from {1}".format(size, filename))
522d829b 1012 # Use sudo because cephfs-data-scan may recreate the file with owner==root
7c673cae
FG
1013 return self.run_python(dedent("""
1014 import zlib
1015 path = "{path}"
9f95a23c
TL
1016 with open(path, 'r') as f:
1017 bytes = f.read()
7c673cae
FG
1018 if len(bytes) != {size}:
1019 raise RuntimeError("Bad length {{0}} vs. expected {{1}}".format(
1020 len(bytes), {size}
1021 ))
1022 for i, b in enumerate(bytes):
9f95a23c 1023 val = zlib.crc32(str(i).encode('utf-8')) & 7
7c673cae
FG
1024 if b != chr(val):
1025 raise RuntimeError("Bad data at offset {{0}}".format(i))
1026 """.format(
f67539c2 1027 path=os.path.join(self.hostfs_mntpt, filename),
7c673cae 1028 size=size
522d829b 1029 )), sudo=True)
7c673cae
FG
1030
1031 def open_n_background(self, fs_path, count):
1032 """
1033 Open N files for writing, hold them open in a background process
1034
1035 :param fs_path: Path relative to CephFS root, e.g. "foo/bar"
1036 :return: a RemoteProcess
1037 """
1038 assert(self.is_mounted())
1039
f67539c2 1040 abs_path = os.path.join(self.hostfs_mntpt, fs_path)
7c673cae
FG
1041
1042 pyscript = dedent("""
1043 import sys
1044 import time
1045 import os
1046
1047 n = {count}
1048 abs_path = "{abs_path}"
1049
f91f0fd5
TL
1050 if not os.path.exists(abs_path):
1051 os.makedirs(abs_path)
7c673cae
FG
1052
1053 handles = []
1054 for i in range(0, n):
f91f0fd5
TL
1055 fname = "file_"+str(i)
1056 path = os.path.join(abs_path, fname)
1057 handles.append(open(path, 'w'))
7c673cae
FG
1058
1059 while True:
1060 time.sleep(1)
1061 """).format(abs_path=abs_path, count=count)
1062
1063 rproc = self._run_python(pyscript)
1064 self.background_procs.append(rproc)
1065 return rproc
1066
20effc67
TL
1067 def create_n_files(self, fs_path, count, sync=False, dirsync=False, unlink=False, finaldirsync=False):
1068 """
1069 Create n files.
1070
1071 :param sync: sync the file after writing
1072 :param dirsync: sync the containing directory after closing the file
1073 :param unlink: unlink the file after closing
1074 :param finaldirsync: sync the containing directory after closing the last file
1075 """
1076
7c673cae
FG
1077 assert(self.is_mounted())
1078
f67539c2 1079 abs_path = os.path.join(self.hostfs_mntpt, fs_path)
7c673cae 1080
20effc67 1081 pyscript = dedent(f"""
7c673cae
FG
1082 import os
1083
1084 n = {count}
20effc67 1085 path = "{abs_path}"
7c673cae 1086
20effc67
TL
1087 dpath = os.path.dirname(path)
1088 fnameprefix = os.path.basename(path)
1089 os.makedirs(dpath, exist_ok=True)
7c673cae 1090
20effc67
TL
1091 try:
1092 dirfd = os.open(dpath, os.O_DIRECTORY)
1093
1094 for i in range(n):
1095 fpath = os.path.join(dpath, f"{{fnameprefix}}_{{i}}")
1096 with open(fpath, 'w') as f:
1097 f.write(f"{{i}}")
1098 if {sync}:
1099 f.flush()
1100 os.fsync(f.fileno())
1101 if {unlink}:
1102 os.unlink(fpath)
1103 if {dirsync}:
1104 os.fsync(dirfd)
1105 if {finaldirsync}:
1106 os.fsync(dirfd)
1107 finally:
1108 os.close(dirfd)
1109 """)
7c673cae
FG
1110
1111 self.run_python(pyscript)
1112
1113 def teardown(self):
1114 for p in self.background_procs:
1115 log.info("Terminating background process")
1116 self._kill_background(p)
1117
1118 self.background_procs = []
1119
1120 def _kill_background(self, p):
1121 if p.stdin:
1122 p.stdin.close()
1123 try:
1124 p.wait()
1125 except (CommandFailedError, ConnectionLostError):
1126 pass
1127
1128 def kill_background(self, p):
1129 """
1130 For a process that was returned by one of the _background member functions,
1131 kill it hard.
1132 """
1133 self._kill_background(p)
1134 self.background_procs.remove(p)
1135
eafe8130
TL
1136 def send_signal(self, signal):
1137 signal = signal.lower()
1138 if signal.lower() not in ['sigstop', 'sigcont', 'sigterm', 'sigkill']:
1139 raise NotImplementedError
1140
1141 self.client_remote.run(args=['sudo', 'kill', '-{0}'.format(signal),
1142 self.client_pid], omit_sudo=False)
1143
7c673cae
FG
1144 def get_global_id(self):
1145 raise NotImplementedError()
1146
11fdf7f2
TL
1147 def get_global_inst(self):
1148 raise NotImplementedError()
1149
1150 def get_global_addr(self):
1151 raise NotImplementedError()
1152
7c673cae
FG
1153 def get_osd_epoch(self):
1154 raise NotImplementedError()
1155
f67539c2
TL
1156 def get_op_read_count(self):
1157 raise NotImplementedError()
1158
20effc67
TL
1159 def readlink(self, fs_path):
1160 abs_path = os.path.join(self.hostfs_mntpt, fs_path)
1161
1162 pyscript = dedent("""
1163 import os
1164
1165 print(os.readlink("{path}"))
1166 """).format(path=abs_path)
1167
1168 proc = self._run_python(pyscript)
1169 proc.wait()
1170 return str(proc.stdout.getvalue().strip())
1171
1172
9f95a23c
TL
1173 def lstat(self, fs_path, follow_symlinks=False, wait=True):
1174 return self.stat(fs_path, follow_symlinks=False, wait=True)
1175
522d829b 1176 def stat(self, fs_path, follow_symlinks=True, wait=True, **kwargs):
7c673cae
FG
1177 """
1178 stat a file, and return the result as a dictionary like this:
1179 {
1180 "st_ctime": 1414161137.0,
1181 "st_mtime": 1414161137.0,
1182 "st_nlink": 33,
1183 "st_gid": 0,
1184 "st_dev": 16777218,
1185 "st_size": 1190,
1186 "st_ino": 2,
1187 "st_uid": 0,
1188 "st_mode": 16877,
1189 "st_atime": 1431520593.0
1190 }
1191
1192 Raises exception on absent file.
1193 """
f67539c2 1194 abs_path = os.path.join(self.hostfs_mntpt, fs_path)
9f95a23c
TL
1195 if follow_symlinks:
1196 stat_call = "os.stat('" + abs_path + "')"
1197 else:
1198 stat_call = "os.lstat('" + abs_path + "')"
7c673cae
FG
1199
1200 pyscript = dedent("""
1201 import os
1202 import stat
1203 import json
1204 import sys
1205
1206 try:
9f95a23c 1207 s = {stat_call}
7c673cae
FG
1208 except OSError as e:
1209 sys.exit(e.errno)
1210
1211 attrs = ["st_mode", "st_ino", "st_dev", "st_nlink", "st_uid", "st_gid", "st_size", "st_atime", "st_mtime", "st_ctime"]
9f95a23c 1212 print(json.dumps(
7c673cae 1213 dict([(a, getattr(s, a)) for a in attrs]),
9f95a23c
TL
1214 indent=2))
1215 """).format(stat_call=stat_call)
522d829b 1216 proc = self._run_python(pyscript, **kwargs)
7c673cae
FG
1217 if wait:
1218 proc.wait()
1219 return json.loads(proc.stdout.getvalue().strip())
1220 else:
1221 return proc
1222
1223 def touch(self, fs_path):
1224 """
1225 Create a dentry if it doesn't already exist. This python
1226 implementation exists because the usual command line tool doesn't
1227 pass through error codes like EIO.
1228
1229 :param fs_path:
1230 :return:
1231 """
f67539c2 1232 abs_path = os.path.join(self.hostfs_mntpt, fs_path)
7c673cae
FG
1233 pyscript = dedent("""
1234 import sys
1235 import errno
1236
1237 try:
1238 f = open("{path}", "w")
1239 f.close()
1240 except IOError as e:
1241 sys.exit(errno.EIO)
1242 """).format(path=abs_path)
1243 proc = self._run_python(pyscript)
1244 proc.wait()
1245
1246 def path_to_ino(self, fs_path, follow_symlinks=True):
f67539c2 1247 abs_path = os.path.join(self.hostfs_mntpt, fs_path)
7c673cae
FG
1248
1249 if follow_symlinks:
1250 pyscript = dedent("""
1251 import os
1252 import stat
1253
9f95a23c 1254 print(os.stat("{path}").st_ino)
7c673cae
FG
1255 """).format(path=abs_path)
1256 else:
1257 pyscript = dedent("""
1258 import os
1259 import stat
1260
9f95a23c 1261 print(os.lstat("{path}").st_ino)
7c673cae
FG
1262 """).format(path=abs_path)
1263
1264 proc = self._run_python(pyscript)
1265 proc.wait()
1266 return int(proc.stdout.getvalue().strip())
1267
1268 def path_to_nlink(self, fs_path):
f67539c2 1269 abs_path = os.path.join(self.hostfs_mntpt, fs_path)
7c673cae
FG
1270
1271 pyscript = dedent("""
1272 import os
1273 import stat
1274
9f95a23c 1275 print(os.stat("{path}").st_nlink)
7c673cae
FG
1276 """).format(path=abs_path)
1277
1278 proc = self._run_python(pyscript)
1279 proc.wait()
1280 return int(proc.stdout.getvalue().strip())
1281
522d829b 1282 def ls(self, path=None, **kwargs):
7c673cae
FG
1283 """
1284 Wrap ls: return a list of strings
1285 """
1286 cmd = ["ls"]
1287 if path:
1288 cmd.append(path)
1289
522d829b 1290 ls_text = self.run_shell(cmd, **kwargs).stdout.getvalue().strip()
7c673cae
FG
1291
1292 if ls_text:
1293 return ls_text.split("\n")
1294 else:
1295 # Special case because otherwise split on empty string
1296 # gives you [''] instead of []
1297 return []
1298
522d829b 1299 def setfattr(self, path, key, val, **kwargs):
7c673cae
FG
1300 """
1301 Wrap setfattr.
1302
1303 :param path: relative to mount point
1304 :param key: xattr name
1305 :param val: xattr value
1306 :return: None
1307 """
522d829b 1308 self.run_shell(["setfattr", "-n", key, "-v", val, path], **kwargs)
7c673cae 1309
522d829b 1310 def getfattr(self, path, attr, **kwargs):
7c673cae
FG
1311 """
1312 Wrap getfattr: return the values of a named xattr on one file, or
1313 None if the attribute is not found.
1314
1315 :return: a string
1316 """
522d829b 1317 p = self.run_shell(["getfattr", "--only-values", "-n", attr, path], wait=False, **kwargs)
7c673cae
FG
1318 try:
1319 p.wait()
1320 except CommandFailedError as e:
1321 if e.exitstatus == 1 and "No such attribute" in p.stderr.getvalue():
1322 return None
1323 else:
1324 raise
1325
e306af50 1326 return str(p.stdout.getvalue())
7c673cae
FG
1327
1328 def df(self):
1329 """
1330 Wrap df: return a dict of usage fields in bytes
1331 """
1332
1333 p = self.run_shell(["df", "-B1", "."])
1334 lines = p.stdout.getvalue().strip().split("\n")
1335 fs, total, used, avail = lines[1].split()[:4]
e306af50 1336 log.warning(lines)
7c673cae
FG
1337
1338 return {
1339 "total": int(total),
1340 "used": int(used),
1341 "available": int(avail)
1342 }
f67539c2
TL
1343
1344 def dir_checksum(self, path=None, follow_symlinks=False):
1345 cmd = ["find"]
1346 if follow_symlinks:
1347 cmd.append("-L")
1348 if path:
1349 cmd.append(path)
1350 cmd.extend(["-type", "f", "-exec", "md5sum", "{}", "+"])
1351 checksum_text = self.run_shell(cmd).stdout.getvalue().strip()
1352 checksum_sorted = sorted(checksum_text.split('\n'), key=lambda v: v.split()[1])
1353 return hashlib.md5(('\n'.join(checksum_sorted)).encode('utf-8')).hexdigest()