]> git.proxmox.com Git - ceph.git/blob - ceph/src/tools/cephfs/shell/cephfs-shell
import ceph quincy 17.2.4
[ceph.git] / ceph / src / tools / cephfs / shell / cephfs-shell
1 #!/usr/bin/python3
2 # coding = utf-8
3
4 import argparse
5 import os
6 import os.path
7 import sys
8 import cephfs as libcephfs
9 import shutil
10 import traceback
11 import colorama
12 import fnmatch
13 import math
14 import re
15 import shlex
16 import stat
17 import errno
18
19 from cmd2 import Cmd
20 from cmd2 import __version__ as cmd2_version
21 from distutils.version import LooseVersion
22
23 if sys.version_info.major < 3:
24 raise RuntimeError("cephfs-shell is only compatible with python3")
25
26 try:
27 from cmd2 import with_argparser
28 except ImportError:
29 def with_argparser(argparser):
30 import functools
31
32 def argparser_decorator(func):
33 @functools.wraps(func)
34 def wrapper(thiz, cmdline):
35 if isinstance(cmdline, list):
36 arglist = cmdline
37 else:
38 # do not split if it's already a list
39 arglist = shlex.split(cmdline, posix=False)
40 # in case user quotes the command args
41 arglist = [arg.strip('\'""') for arg in arglist]
42 try:
43 args = argparser.parse_args(arglist)
44 except SystemExit:
45 shell.exit_code = 1
46 # argparse exits at seeing bad arguments
47 return
48 else:
49 return func(thiz, args)
50 argparser.prog = func.__name__[3:]
51 if argparser.description is None and func.__doc__:
52 argparser.description = func.__doc__
53
54 return wrapper
55
56 return argparser_decorator
57
58
59 cephfs = None # holds CephFS Python bindings
60 shell = None # holds instance of class CephFSShell
61 exit_codes = {'Misc': 1,
62 'KeyboardInterrupt': 2,
63 errno.EPERM: 3,
64 errno.EACCES: 4,
65 errno.ENOENT: 5,
66 errno.EIO: 6,
67 errno.ENOSPC: 7,
68 errno.EEXIST: 8,
69 errno.ENODATA: 9,
70 errno.EINVAL: 10,
71 errno.EOPNOTSUPP: 11,
72 errno.ERANGE: 12,
73 errno.EWOULDBLOCK: 13,
74 errno.ENOTEMPTY: 14,
75 errno.ENOTDIR: 15,
76 errno.EDQUOT: 16,
77 errno.EPIPE: 17,
78 errno.ESHUTDOWN: 18,
79 errno.ECONNABORTED: 19,
80 errno.ECONNREFUSED: 20,
81 errno.ECONNRESET: 21,
82 errno.EINTR: 22}
83
84
85 #########################################################################
86 #
87 # Following are methods are generically useful through class CephFSShell
88 #
89 #######################################################################
90
91
92 def poutput(s, end='\n'):
93 shell.poutput(s, end=end)
94
95
96 def perror(msg, **kwargs):
97 shell.perror(msg, **kwargs)
98
99
100 def set_exit_code_msg(errcode='Misc', msg=''):
101 """
102 Set exit code and print error message
103 """
104 if isinstance(msg, libcephfs.Error):
105 shell.exit_code = exit_codes[msg.get_error_code()]
106 else:
107 shell.exit_code = exit_codes[errcode]
108 if msg:
109 perror(msg)
110
111
112 def mode_notation(mode):
113 """
114 """
115 permission_bits = {'0': '---',
116 '1': '--x',
117 '2': '-w-',
118 '3': '-wx',
119 '4': 'r--',
120 '5': 'r-x',
121 '6': 'rw-',
122 '7': 'rwx'}
123 mode = str(oct(mode))
124 notation = '-'
125 if mode[2] == '4':
126 notation = 'd'
127 elif mode[2:4] == '12':
128 notation = 'l'
129 for i in mode[-3:]:
130 notation += permission_bits[i]
131 return notation
132
133
134 def get_chunks(file_size):
135 chunk_start = 0
136 chunk_size = 0x20000 # 131072 bytes, default max ssl buffer size
137 while chunk_start + chunk_size < file_size:
138 yield chunk_start, chunk_size
139 chunk_start += chunk_size
140 final_chunk_size = file_size - chunk_start
141 yield chunk_start, final_chunk_size
142
143
144 def to_bytes(param):
145 # don't convert as follows as it can lead unusable results like coverting
146 # [1, 2, 3, 4] to '[1, 2, 3, 4]' -
147 # str(param).encode('utf-8')
148 if isinstance(param, bytes):
149 return param
150 elif isinstance(param, str):
151 return bytes(param, encoding='utf-8')
152 elif isinstance(param, list):
153 return [i.encode('utf-8') if isinstance(i, str) else to_bytes(i) for
154 i in param]
155 elif isinstance(param, int) or isinstance(param, float):
156 return str(param).encode('utf-8')
157 elif param is None:
158 return None
159
160
161 def ls(path, opts=''):
162 # opts tries to be like /bin/ls opts
163 almost_all = 'A' in opts
164 try:
165 with cephfs.opendir(path) as d:
166 while True:
167 dent = cephfs.readdir(d)
168 if dent is None:
169 return
170 elif almost_all and dent.d_name in (b'.', b'..'):
171 continue
172 yield dent
173 except libcephfs.ObjectNotFound as e:
174 set_exit_code_msg(msg=e)
175
176
177 def glob(path, pattern):
178 paths = []
179 parent_dir = os.path.dirname(path)
180 if parent_dir == b'':
181 parent_dir = b'/'
182 if path == b'/' or is_dir_exists(os.path.basename(path), parent_dir):
183 for i in ls(path, opts='A'):
184 if fnmatch.fnmatch(i.d_name, pattern):
185 paths.append(os.path.join(path, i.d_name))
186 return paths
187
188
189 def locate_file(name, case_sensitive=True):
190 dir_list = sorted(set(dirwalk(cephfs.getcwd())))
191 if not case_sensitive:
192 return [dname for dname in dir_list if name.lower() in dname.lower()]
193 else:
194 return [dname for dname in dir_list if name in dname]
195
196
197 def get_all_possible_paths(pattern):
198 complete_pattern = pattern[:]
199 paths = []
200 is_rel_path = not os.path.isabs(pattern)
201 if is_rel_path:
202 dir_ = cephfs.getcwd()
203 else:
204 dir_ = b'/'
205 pattern = pattern[1:]
206 patterns = pattern.split(b'/')
207 paths.extend(glob(dir_, patterns[0]))
208 patterns.pop(0)
209 for pattern in patterns:
210 for path in paths:
211 paths.extend(glob(path, pattern))
212 if is_rel_path:
213 complete_pattern = os.path.join(cephfs.getcwd(), complete_pattern)
214 return [path for path in paths if fnmatch.fnmatch(path, complete_pattern)]
215
216
217 suffixes = ['B', 'K', 'M', 'G', 'T', 'P']
218
219
220 def humansize(nbytes):
221 i = 0
222 while nbytes >= 1024 and i < len(suffixes) - 1:
223 nbytes /= 1024.
224 i += 1
225 nbytes = math.ceil(nbytes)
226 f = ('%d' % nbytes).rstrip('.')
227 return '%s%s' % (f, suffixes[i])
228
229
230 def style_listing(path, is_dir, is_symlink, ls_long=False):
231 if not (is_dir or is_symlink):
232 return path
233 pretty = colorama.Style.BRIGHT
234 if is_symlink:
235 pretty += colorama.Fore.CYAN + path
236 if ls_long:
237 # Add target path
238 pretty += ' -> ' + cephfs.readlink(path, size=255).decode('utf-8')
239 elif is_dir:
240 pretty += colorama.Fore.BLUE + path + '/'
241 pretty += colorama.Style.RESET_ALL
242 return pretty
243
244
245 def print_long(path, is_dir, is_symlink, human_readable):
246 info = cephfs.stat(path, follow_symlink=(not is_symlink))
247 pretty = style_listing(os.path.basename(path.decode('utf-8')), is_dir, is_symlink, True)
248 if human_readable:
249 sizefmt = '\t {:10s}'.format(humansize(info.st_size))
250 else:
251 sizefmt = '{:12d}'.format(info.st_size)
252 poutput(f'{mode_notation(info.st_mode)} {sizefmt} {info.st_uid} {info.st_gid} {info.st_mtime}'
253 f' {pretty}')
254
255
256 def word_len(word):
257 """
258 Returns the word length, minus any color codes.
259 """
260 if word[0] == '\x1b':
261 return len(word) - 9
262 return len(word)
263
264
265 def is_dir_exists(path, dir_=b''):
266 path_to_stat = os.path.join(dir_, path)
267 try:
268 return ((cephfs.stat(path_to_stat).st_mode & 0o0040000) != 0)
269 except libcephfs.Error:
270 return False
271
272
273 def is_file_exists(path, dir_=b''):
274 try:
275 # if its not a directory, then its a file
276 return ((cephfs.stat(os.path.join(dir_, path)).st_mode & 0o0040000) == 0)
277 except libcephfs.Error:
278 return False
279
280
281 def print_list(words, termwidth=79):
282 if not words:
283 return
284 words = [word.decode('utf-8') if isinstance(word, bytes) else word for word in words]
285 width = max([word_len(word) for word in words]) + 2
286 nwords = len(words)
287 ncols = max(1, (termwidth + 1) // (width + 1))
288 nrows = (nwords + ncols - 1) // ncols
289 for row in range(nrows):
290 for i in range(row, nwords, nrows):
291 word = words[i]
292 print_width = width
293 if word[0] == '\x1b':
294 print_width = print_width + 10
295
296 poutput('%-*s' % (print_width, words[i]),
297 end='\n' if i + nrows >= nwords else '')
298
299
300 def copy_from_local(local_path, remote_path):
301 stdin = -1
302 file_ = None
303 fd = None
304 convert_to_bytes = False
305 if local_path == b'-':
306 file_ = sys.stdin
307 convert_to_bytes = True
308 else:
309 try:
310 file_ = open(local_path, 'rb')
311 except PermissionError as e:
312 set_exit_code_msg(e.errno, 'error: no permission to read local file {}'.format(
313 local_path.decode('utf-8')))
314 return
315 stdin = 1
316 try:
317 fd = cephfs.open(remote_path, 'w', 0o666)
318 except libcephfs.Error as e:
319 set_exit_code_msg(msg=e)
320 return
321 progress = 0
322 while True:
323 data = file_.read(65536)
324 if not data or len(data) == 0:
325 break
326 if convert_to_bytes:
327 data = to_bytes(data)
328 wrote = cephfs.write(fd, data, progress)
329 if wrote < 0:
330 break
331 progress += wrote
332 cephfs.close(fd)
333 if stdin > 0:
334 file_.close()
335 poutput('')
336
337
338 def copy_to_local(remote_path, local_path):
339 fd = None
340 if local_path != b'-':
341 local_dir = os.path.dirname(local_path)
342 dir_list = remote_path.rsplit(b'/', 1)
343 if not os.path.exists(local_dir):
344 os.makedirs(local_dir)
345 if len(dir_list) > 2 and dir_list[1] == b'':
346 return
347 fd = open(local_path, 'wb+')
348 file_ = cephfs.open(remote_path, 'r')
349 file_size = cephfs.stat(remote_path).st_size
350 if file_size <= 0:
351 return
352 progress = 0
353 for chunk_start, chunk_size in get_chunks(file_size):
354 file_chunk = cephfs.read(file_, chunk_start, chunk_size)
355 progress += len(file_chunk)
356 if fd:
357 fd.write(file_chunk)
358 else:
359 poutput(file_chunk.decode('utf-8'))
360 cephfs.close(file_)
361 if fd:
362 fd.close()
363
364
365 def dirwalk(path):
366 """
367 walk a directory tree, using a generator
368 """
369 path = os.path.normpath(path)
370 for item in ls(path, opts='A'):
371 fullpath = os.path.join(path, item.d_name)
372 src_path = fullpath.rsplit(b'/', 1)[0]
373
374 yield os.path.normpath(fullpath)
375 if is_dir_exists(item.d_name, src_path):
376 for x in dirwalk(fullpath):
377 yield x
378
379
380 ##################################################################
381 #
382 # Following methods are implementation for CephFS Shell commands
383 #
384 #################################################################
385
386 class CephFSShell(Cmd):
387
388 def __init__(self):
389 super().__init__()
390 self.working_dir = cephfs.getcwd().decode('utf-8')
391 self.set_prompt()
392 self.interactive = False
393 self.umask = '2'
394
395 def default(self, line):
396 perror('Unrecognized command')
397
398 def set_prompt(self):
399 self.prompt = ('\033[01;33mCephFS:~' + colorama.Fore.LIGHTCYAN_EX
400 + self.working_dir + colorama.Style.RESET_ALL
401 + '\033[01;33m>>>\033[00m ')
402
403 def create_argparser(self, command):
404 try:
405 argparse_args = getattr(self, 'argparse_' + command)
406 except AttributeError:
407 set_exit_code_msg()
408 return None
409 doc_lines = getattr(
410 self, 'do_' + command).__doc__.expandtabs().splitlines()
411 if '' in doc_lines:
412 blank_idx = doc_lines.index('')
413 usage = doc_lines[:blank_idx]
414 description = doc_lines[blank_idx + 1:]
415 else:
416 usage = doc_lines
417 description = []
418 parser = argparse.ArgumentParser(
419 prog=command,
420 usage='\n'.join(usage),
421 description='\n'.join(description),
422 formatter_class=argparse.ArgumentDefaultsHelpFormatter
423 )
424 for args, kwargs in argparse_args:
425 parser.add_argument(*args, **kwargs)
426 return parser
427
428 def complete_filenames(self, text, line, begidx, endidx):
429 if not text:
430 completions = [x.d_name.decode('utf-8') + '/' * int(x.is_dir())
431 for x in ls(b".", opts='A')]
432 else:
433 if text.count('/') > 0:
434 completions = [text.rsplit('/', 1)[0] + '/'
435 + x.d_name.decode('utf-8') + '/'
436 * int(x.is_dir()) for x in ls('/'
437 + text.rsplit('/', 1)[0], opts='A')
438 if x.d_name.decode('utf-8').startswith(
439 text.rsplit('/', 1)[1])]
440 else:
441 completions = [x.d_name.decode('utf-8') + '/'
442 * int(x.is_dir()) for x in ls(b".", opts='A')
443 if x.d_name.decode('utf-8').startswith(text)]
444 if len(completions) == 1 and completions[0][-1] == '/':
445 dir_, file_ = completions[0].rsplit('/', 1)
446 completions.extend([dir_ + '/' + x.d_name.decode('utf-8')
447 + '/' * int(x.is_dir()) for x in
448 ls('/' + dir_, opts='A')
449 if x.d_name.decode('utf-8').startswith(file_)])
450 return self.delimiter_complete(text, line, begidx, endidx, completions, '/')
451 return completions
452
453 def onecmd(self, line, **kwargs):
454 """
455 Global error catcher
456 """
457 try:
458 res = Cmd.onecmd(self, line, **kwargs)
459 if self.interactive:
460 self.set_prompt()
461 return res
462 except ConnectionError as e:
463 set_exit_code_msg(e.errno, f'***\n{e}')
464 except KeyboardInterrupt:
465 set_exit_code_msg('KeyboardInterrupt', 'Command aborted')
466 except (libcephfs.Error, Exception) as e:
467 if shell.debug:
468 traceback.print_exc(file=sys.stdout)
469 set_exit_code_msg(msg=e)
470
471 class path_to_bytes(argparse.Action):
472 def __call__(self, parser, namespace, values, option_string=None):
473 values = to_bytes(values)
474 setattr(namespace, self.dest, values)
475
476 # TODO: move the necessary contents from here to `class path_to_bytes`.
477 class get_list_of_bytes_path(argparse.Action):
478 def __call__(self, parser, namespace, values, option_string=None):
479 values = to_bytes(values)
480
481 if values == b'.':
482 values = cephfs.getcwd()
483 else:
484 for i in values:
485 if i == b'.':
486 values[values.index(i)] = cephfs.getcwd()
487
488 setattr(namespace, self.dest, values)
489
490 def complete_mkdir(self, text, line, begidx, endidx):
491 """
492 auto complete of file name.
493 """
494 return self.complete_filenames(text, line, begidx, endidx)
495
496 class ModeAction(argparse.Action):
497 def __init__(self, option_strings, dest, nargs=None, **kwargs):
498 if nargs is not None and nargs != '?':
499 raise ValueError("more than one modes not allowed")
500 super().__init__(option_strings, dest, **kwargs)
501
502 def __call__(self, parser, namespace, values, option_string=None):
503 o_mode = 0
504 res = None
505 try:
506 o_mode = int(values, base=8)
507 except ValueError:
508 res = re.match('((u?g?o?)|(a?))(=)(r?w?x?)', values)
509 if res is None:
510 parser.error("invalid mode: %s\n"
511 "mode must be a numeric octal literal\n"
512 "or ((u?g?o?)|(a?))(=)(r?w?x?)" %
513 values)
514 else:
515 # we are supporting only assignment of mode and not + or -
516 # as is generally available with the chmod command
517 # eg.
518 # >>> res = re.match('((u?g?o?)|(a?))(=)(r?w?x?)', 'go=')
519 # >>> res.groups()
520 # ('go', 'go', None, '=', '')
521 val = res.groups()
522
523 if val[3] != '=':
524 parser.error("need assignment operator between user "
525 "and mode specifiers")
526 if val[4] == '':
527 parser.error("invalid mode: %s\n"
528 "mode must be combination of: r | w | x" %
529 values)
530 users = ''
531 if val[2] is None:
532 users = val[1]
533 else:
534 users = val[2]
535
536 t_mode = 0
537 if users == 'a':
538 users = 'ugo'
539
540 if 'r' in val[4]:
541 t_mode |= 4
542 if 'w' in val[4]:
543 t_mode |= 2
544 if 'x' in val[4]:
545 t_mode |= 1
546
547 if 'u' in users:
548 o_mode |= (t_mode << 6)
549 if 'g' in users:
550 o_mode |= (t_mode << 3)
551 if 'o' in users:
552 o_mode |= t_mode
553
554 if o_mode < 0:
555 parser.error("invalid mode: %s\n"
556 "mode cannot be negative" % values)
557 if o_mode > 0o777:
558 parser.error("invalid mode: %s\n"
559 "mode cannot be greater than octal 0777" % values)
560
561 setattr(namespace, self.dest, str(oct(o_mode)))
562
563 mkdir_parser = argparse.ArgumentParser(
564 description='Create the directory(ies), if they do not already exist.')
565 mkdir_parser.add_argument('dirs', type=str,
566 action=path_to_bytes,
567 metavar='DIR_NAME',
568 help='Name of new_directory.',
569 nargs='+')
570 mkdir_parser.add_argument('-m', '--mode', type=str,
571 action=ModeAction,
572 help='Sets the access mode for the new directory.')
573 mkdir_parser.add_argument('-p', '--parent', action='store_true',
574 help='Create parent directories as necessary. '
575 'When this option is specified, no error is'
576 'reported if a directory already exists.')
577
578 @with_argparser(mkdir_parser)
579 def do_mkdir(self, args):
580 """
581 Create directory.
582 """
583 for path in args.dirs:
584 if args.mode:
585 permission = int(args.mode, 8)
586 else:
587 permission = 0o777
588 if args.parent:
589 cephfs.mkdirs(path, permission)
590 else:
591 try:
592 cephfs.mkdir(path, permission)
593 except libcephfs.Error as e:
594 set_exit_code_msg(e)
595
596 def complete_put(self, text, line, begidx, endidx):
597 """
598 auto complete of file name.
599 """
600 index_dict = {1: self.path_complete}
601 return self.index_based_complete(text, line, begidx, endidx, index_dict)
602
603 put_parser = argparse.ArgumentParser(
604 description='Copy a file/directory to Ceph File System from Local File System.')
605 put_parser.add_argument('local_path', type=str, action=path_to_bytes,
606 help='Path of the file in the local system')
607 put_parser.add_argument('remote_path', type=str, action=path_to_bytes,
608 help='Path of the file in the remote system')
609 put_parser.add_argument('-f', '--force', action='store_true',
610 help='Overwrites the destination if it already exists.')
611
612 @with_argparser(put_parser)
613 def do_put(self, args):
614 """
615 Copy a local file/directory to CephFS.
616 """
617 if args.local_path != b'-' and not os.path.isfile(args.local_path) \
618 and not os.path.isdir(args.local_path):
619 set_exit_code_msg(errno.ENOENT,
620 msg=f"error: "
621 f"{args.local_path.decode('utf-8')}: "
622 f"No such file or directory")
623 return
624
625 if (is_file_exists(args.remote_path) or is_dir_exists(
626 args.remote_path)) and not args.force:
627 set_exit_code_msg(msg=f"error: file/directory "
628 f"{args.remote_path.decode('utf-8')} "
629 f"exists, use --force to overwrite")
630 return
631
632 root_src_dir = args.local_path
633 root_dst_dir = args.remote_path
634 if args.local_path == b'.' or args.local_path == b'./':
635 root_src_dir = os.getcwdb()
636 elif len(args.local_path.rsplit(b'/', 1)) < 2:
637 root_src_dir = os.path.join(os.getcwdb(), args.local_path)
638 else:
639 p = args.local_path.split(b'/')
640 if p[0] == b'.':
641 root_src_dir = os.getcwdb()
642 p.pop(0)
643 while len(p) > 0:
644 root_src_dir += b'/' + p.pop(0)
645
646 if root_dst_dir == b'.':
647 if args.local_path != b'-':
648 root_dst_dir = root_src_dir.rsplit(b'/', 1)[1]
649 if root_dst_dir == b'':
650 root_dst_dir = root_src_dir.rsplit(b'/', 1)[0]
651 a = root_dst_dir.rsplit(b'/', 1)
652 if len(a) > 1:
653 root_dst_dir = a[1]
654 else:
655 root_dst_dir = a[0]
656 else:
657 set_exit_code_msg(errno.EINVAL, 'error: no filename specified '
658 'for destination')
659 return
660
661 if root_dst_dir[-1] != b'/':
662 root_dst_dir += b'/'
663
664 if args.local_path == b'-' or os.path.isfile(root_src_dir):
665 if args.local_path == b'-':
666 root_src_dir = b'-'
667 copy_from_local(root_src_dir, root_dst_dir)
668 else:
669 for src_dir, dirs, files in os.walk(root_src_dir):
670 if isinstance(src_dir, str):
671 src_dir = to_bytes(src_dir)
672 dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1)
673 dst_dir = re.sub(rb'\/+', b'/', cephfs.getcwd()
674 + dst_dir)
675 if args.force and dst_dir != b'/' and not is_dir_exists(
676 dst_dir[:-1]) and not locate_file(dst_dir):
677 try:
678 cephfs.mkdirs(dst_dir, 0o777)
679 except libcephfs.Error:
680 pass
681 if (not args.force) and dst_dir != b'/' and not is_dir_exists(
682 dst_dir) and not os.path.isfile(root_src_dir):
683 try:
684 cephfs.mkdirs(dst_dir, 0o777)
685 except libcephfs.Error:
686 # TODO: perhaps, set retval to 1?
687 pass
688
689 for dir_ in dirs:
690 dir_name = os.path.join(dst_dir, dir_)
691 if not is_dir_exists(dir_name):
692 try:
693 cephfs.mkdirs(dir_name, 0o777)
694 except libcephfs.Error:
695 # TODO: perhaps, set retval to 1?
696 pass
697
698 for file_ in files:
699 src_file = os.path.join(src_dir, file_)
700 dst_file = re.sub(rb'\/+', b'/', b'/' + dst_dir + b'/' + file_)
701 if (not args.force) and is_file_exists(dst_file):
702 return
703 copy_from_local(src_file, os.path.join(cephfs.getcwd(),
704 dst_file))
705
706 def complete_get(self, text, line, begidx, endidx):
707 """
708 auto complete of file name.
709 """
710 return self.complete_filenames(text, line, begidx, endidx)
711
712 get_parser = argparse.ArgumentParser(
713 description='Copy a file from Ceph File System to Local Directory.')
714 get_parser.add_argument('remote_path', type=str, action=path_to_bytes,
715 help='Path of the file in the remote system')
716 get_parser.add_argument('local_path', type=str, action=path_to_bytes,
717 help='Path of the file in the local system')
718 get_parser.add_argument('-f', '--force', action='store_true',
719 help='Overwrites the destination if it already exists.')
720
721 @with_argparser(get_parser)
722 def do_get(self, args):
723 """
724 Copy a file/directory from CephFS to given path.
725 """
726 if not is_file_exists(args.remote_path) and not \
727 is_dir_exists(args.remote_path):
728 set_exit_code_msg(errno.ENOENT, "error: no file/directory"
729 " found at specified remote "
730 "path")
731 return
732 if (os.path.isfile(args.local_path) or os.path.isdir(
733 args.local_path)) and not args.force:
734 set_exit_code_msg(msg=f"error: file/directory "
735 f"{args.local_path.decode('utf-8')}"
736 f" already exists, use --force to "
737 f"overwrite")
738 return
739 root_src_dir = args.remote_path
740 root_dst_dir = args.local_path
741 fname = root_src_dir.rsplit(b'/', 1)
742 if args.local_path == b'.':
743 root_dst_dir = os.getcwdb()
744 if args.remote_path == b'.':
745 root_src_dir = cephfs.getcwd()
746 if args.local_path == b'-':
747 if args.remote_path == b'.' or args.remote_path == b'./':
748 set_exit_code_msg(errno.EINVAL, 'error: no remote file name specified')
749 return
750 copy_to_local(root_src_dir, b'-')
751 elif is_file_exists(args.remote_path):
752 copy_to_local(root_src_dir, root_dst_dir)
753 elif b'/' in root_src_dir and is_file_exists(fname[1], fname[0]):
754 copy_to_local(root_src_dir, root_dst_dir)
755 else:
756 files = list(reversed(sorted(dirwalk(root_src_dir))))
757 for file_ in files:
758 dst_dirpath, dst_file = file_.rsplit(b'/', 1)
759 if dst_dirpath in files:
760 files.remove(dst_dirpath)
761 dst_path = os.path.join(root_dst_dir, dst_dirpath, dst_file)
762 dst_path = os.path.normpath(dst_path)
763 if is_dir_exists(file_):
764 try:
765 os.makedirs(dst_path)
766 except OSError:
767 pass
768 else:
769 copy_to_local(file_, dst_path)
770
771 return 0
772
773 def complete_ls(self, text, line, begidx, endidx):
774 """
775 auto complete of file name.
776 """
777 return self.complete_filenames(text, line, begidx, endidx)
778
779 ls_parser = argparse.ArgumentParser(
780 description='Copy a file from Ceph File System from Local Directory.')
781 ls_parser.add_argument('-l', '--long', action='store_true',
782 help='Detailed list of items in the directory.')
783 ls_parser.add_argument('-r', '--reverse', action='store_true',
784 help='Reverse order of listing items in the directory.')
785 ls_parser.add_argument('-H', action='store_true', help='Human Readable')
786 ls_parser.add_argument('-a', '--all', action='store_true',
787 help='Do not Ignore entries starting with .')
788 ls_parser.add_argument('-S', action='store_true', help='Sort by file_size')
789 ls_parser.add_argument('paths', help='Name of Directories',
790 action=path_to_bytes, nargs='*', default=['.'])
791
792 @with_argparser(ls_parser)
793 def do_ls(self, args):
794 """
795 List all the files and directories in the current working directory
796 """
797 paths = args.paths
798 for path in paths:
799 values = []
800 items = []
801 try:
802 if path.count(b'*') > 0:
803 all_items = get_all_possible_paths(path)
804 if len(all_items) == 0:
805 continue
806 path = all_items[0].rsplit(b'/', 1)[0]
807 if path == b'':
808 path = b'/'
809 dirs = []
810 for i in all_items:
811 for item in ls(path):
812 d_name = item.d_name
813 if os.path.basename(i) == d_name:
814 if item.is_dir():
815 dirs.append(os.path.join(path, d_name))
816 else:
817 items.append(item)
818 if dirs:
819 paths.extend(dirs)
820 else:
821 poutput(path.decode('utf-8'), end=':\n')
822 items = sorted(items, key=lambda item: item.d_name)
823 else:
824 if path != b'' and path != cephfs.getcwd() and len(paths) > 1:
825 poutput(path.decode('utf-8'), end=':\n')
826 items = sorted(ls(path), key=lambda item: item.d_name)
827 if not args.all:
828 items = [i for i in items if not i.d_name.startswith(b'.')]
829 if args.S:
830 items = sorted(items, key=lambda item: cephfs.stat(
831 path + b'/' + item.d_name, follow_symlink=(
832 not item.is_symbol_file())).st_size)
833 if args.reverse:
834 items = reversed(items)
835 for item in items:
836 filepath = item.d_name
837 is_dir = item.is_dir()
838 is_sym_lnk = item.is_symbol_file()
839 try:
840 if args.long and args.H:
841 print_long(os.path.join(cephfs.getcwd(), path, filepath), is_dir,
842 is_sym_lnk, True)
843 elif args.long:
844 print_long(os.path.join(cephfs.getcwd(), path, filepath), is_dir,
845 is_sym_lnk, False)
846 elif is_sym_lnk or is_dir:
847 values.append(style_listing(filepath.decode('utf-8'), is_dir,
848 is_sym_lnk))
849 else:
850 values.append(filepath)
851 except libcephfs.Error as e:
852 set_exit_code_msg(msg=e)
853 if not args.long:
854 print_list(values, shutil.get_terminal_size().columns)
855 if path != paths[-1]:
856 poutput('')
857 except libcephfs.Error as e:
858 set_exit_code_msg(msg=e)
859
860 def complete_rmdir(self, text, line, begidx, endidx):
861 """
862 auto complete of file name.
863 """
864 return self.complete_filenames(text, line, begidx, endidx)
865
866 rmdir_parser = argparse.ArgumentParser(description='Remove Directory.')
867 rmdir_parser.add_argument('paths', help='Directory Path.', nargs='+',
868 action=path_to_bytes)
869 rmdir_parser.add_argument('-p', '--parent', action='store_true',
870 help='Remove parent directories as necessary. '
871 'When this option is specified, no error '
872 'is reported if a directory has any '
873 'sub-directories, files')
874
875 @with_argparser(rmdir_parser)
876 def do_rmdir(self, args):
877 self.do_rmdir_helper(args)
878
879 def do_rmdir_helper(self, args):
880 """
881 Remove a specific Directory
882 """
883 is_pattern = False
884 paths = args.paths
885 for path in paths:
886 if path.count(b'*') > 0:
887 is_pattern = True
888 all_items = get_all_possible_paths(path)
889 if len(all_items) > 0:
890 path = all_items[0].rsplit(b'/', 1)[0]
891 if path == b'':
892 path = b'/'
893 dirs = []
894 for i in all_items:
895 for item in ls(path):
896 d_name = item.d_name
897 if os.path.basename(i) == d_name:
898 if item.is_dir():
899 dirs.append(os.path.join(path, d_name))
900 paths.extend(dirs)
901 continue
902 else:
903 is_pattern = False
904
905 if args.parent:
906 path = os.path.join(cephfs.getcwd(), path.rsplit(b'/')[0])
907 files = list(sorted(set(dirwalk(path)), reverse=True))
908 if not files:
909 path = b'.'
910 for filepath in files:
911 try:
912 cephfs.rmdir(os.path.normpath(filepath))
913 except libcephfs.Error as e:
914 perror(e)
915 path = b'.'
916 break
917 else:
918 path = os.path.normpath(os.path.join(cephfs.getcwd(), path))
919 if not is_pattern and path != os.path.normpath(b''):
920 try:
921 cephfs.rmdir(path)
922 except libcephfs.Error as e:
923 set_exit_code_msg(msg=e)
924
925 def complete_rm(self, text, line, begidx, endidx):
926 """
927 auto complete of file name.
928 """
929 return self.complete_filenames(text, line, begidx, endidx)
930
931 rm_parser = argparse.ArgumentParser(description='Remove File.')
932 rm_parser.add_argument('paths', help='File Path.', nargs='+',
933 action=path_to_bytes)
934
935 @with_argparser(rm_parser)
936 def do_rm(self, args):
937 """
938 Remove a specific file
939 """
940 file_paths = args.paths
941 for path in file_paths:
942 if path.count(b'*') > 0:
943 file_paths.extend([i for i in get_all_possible_paths(
944 path) if is_file_exists(i)])
945 else:
946 try:
947 cephfs.unlink(path)
948 except libcephfs.Error as e:
949 # NOTE: perhaps we need a better msg here
950 set_exit_code_msg(msg=e)
951
952 def complete_mv(self, text, line, begidx, endidx):
953 """
954 auto complete of file name.
955 """
956 return self.complete_filenames(text, line, begidx, endidx)
957
958 mv_parser = argparse.ArgumentParser(description='Move File.')
959 mv_parser.add_argument('src_path', type=str, action=path_to_bytes,
960 help='Source File Path.')
961 mv_parser.add_argument('dest_path', type=str, action=path_to_bytes,
962 help='Destination File Path.')
963
964 @with_argparser(mv_parser)
965 def do_mv(self, args):
966 """
967 Rename a file or Move a file from source path to the destination
968 """
969 cephfs.rename(args.src_path, args.dest_path)
970
971 def complete_cd(self, text, line, begidx, endidx):
972 """
973 auto complete of file name.
974 """
975 return self.complete_filenames(text, line, begidx, endidx)
976
977 cd_parser = argparse.ArgumentParser(description='Change working directory')
978 cd_parser.add_argument('path', type=str, help='Name of the directory.',
979 action=path_to_bytes, nargs='?', default='/')
980
981 @with_argparser(cd_parser)
982 def do_cd(self, args):
983 """
984 Change working directory
985 """
986 cephfs.chdir(args.path)
987 self.working_dir = cephfs.getcwd().decode('utf-8')
988 self.set_prompt()
989
990 def do_cwd(self, arglist):
991 """
992 Get current working directory.
993 """
994 poutput(cephfs.getcwd().decode('utf-8'))
995
996 def complete_chmod(self, text, line, begidx, endidx):
997 """
998 auto complete of file name.
999 """
1000 return self.complete_filenames(text, line, begidx, endidx)
1001
1002 chmod_parser = argparse.ArgumentParser(description='Create Directory.')
1003 chmod_parser.add_argument('mode', type=str, action=ModeAction, help='Mode')
1004 chmod_parser.add_argument('paths', type=str, action=path_to_bytes,
1005 help='Name of the file', nargs='+')
1006
1007 @with_argparser(chmod_parser)
1008 def do_chmod(self, args):
1009 """
1010 Change permission of a file
1011 """
1012 for path in args.paths:
1013 mode = int(args.mode, base=8)
1014 try:
1015 cephfs.chmod(path, mode)
1016 except libcephfs.Error as e:
1017 set_exit_code_msg(msg=e)
1018
1019 def complete_cat(self, text, line, begidx, endidx):
1020 """
1021 auto complete of file name.
1022 """
1023 return self.complete_filenames(text, line, begidx, endidx)
1024
1025 cat_parser = argparse.ArgumentParser(description='')
1026 cat_parser.add_argument('paths', help='Name of Files', action=path_to_bytes,
1027 nargs='+')
1028
1029 @with_argparser(cat_parser)
1030 def do_cat(self, args):
1031 """
1032 Print contents of a file
1033 """
1034 for path in args.paths:
1035 if is_file_exists(path):
1036 copy_to_local(path, b'-')
1037 else:
1038 set_exit_code_msg(errno.ENOENT, '{}: no such file'.format(
1039 path.decode('utf-8')))
1040
1041 umask_parser = argparse.ArgumentParser(description='Set umask value.')
1042 umask_parser.add_argument('mode', help='Mode', type=str, action=ModeAction,
1043 nargs='?', default='')
1044
1045 @with_argparser(umask_parser)
1046 def do_umask(self, args):
1047 """
1048 Set Umask value.
1049 """
1050 if args.mode == '':
1051 poutput(self.umask.zfill(4))
1052 else:
1053 mode = int(args.mode, 8)
1054 self.umask = str(oct(cephfs.umask(mode))[2:])
1055
1056 def complete_write(self, text, line, begidx, endidx):
1057 """
1058 auto complete of file name.
1059 """
1060 return self.complete_filenames(text, line, begidx, endidx)
1061
1062 write_parser = argparse.ArgumentParser(description='Writes data into a file')
1063 write_parser.add_argument('path', type=str, action=path_to_bytes,
1064 help='Name of File')
1065
1066 @with_argparser(write_parser)
1067 def do_write(self, args):
1068 """
1069 Write data into a file.
1070 """
1071
1072 copy_from_local(b'-', args.path)
1073
1074 def complete_lcd(self, text, line, begidx, endidx):
1075 """
1076 auto complete of file name.
1077 """
1078 index_dict = {1: self.path_complete}
1079 return self.index_based_complete(text, line, begidx, endidx, index_dict)
1080
1081 lcd_parser = argparse.ArgumentParser(description='')
1082 lcd_parser.add_argument('path', type=str, action=path_to_bytes, help='Path')
1083
1084 @with_argparser(lcd_parser)
1085 def do_lcd(self, args):
1086 """
1087 Moves into the given local directory
1088 """
1089 try:
1090 os.chdir(os.path.expanduser(args.path))
1091 except OSError as e:
1092 set_exit_code_msg(e.errno, "Cannot change to "
1093 f"{e.filename.decode('utf-8')}: {e.strerror}")
1094
1095 def complete_lls(self, text, line, begidx, endidx):
1096 """
1097 auto complete of file name.
1098 """
1099 index_dict = {1: self.path_complete}
1100 return self.index_based_complete(text, line, begidx, endidx, index_dict)
1101
1102 lls_parser = argparse.ArgumentParser(
1103 description='List files in local system.')
1104 lls_parser.add_argument('paths', help='Paths', action=path_to_bytes,
1105 nargs='*')
1106
1107 @with_argparser(lls_parser)
1108 def do_lls(self, args):
1109 """
1110 Lists all files and folders in the current local directory
1111 """
1112 if not args.paths:
1113 print_list(os.listdir(os.getcwdb()))
1114 else:
1115 for path in args.paths:
1116 try:
1117 items = os.listdir(path)
1118 poutput("{}:".format(path.decode('utf-8')))
1119 print_list(items)
1120 except OSError as e:
1121 set_exit_code_msg(e.errno, f"{e.filename.decode('utf-8')}: "
1122 f"{e.strerror}")
1123 # Arguments to the with_argpaser decorator function are sticky.
1124 # The items in args.path do not get overwritten in subsequent calls.
1125 # The arguments remain in args.paths after the function exits and we
1126 # neeed to clean it up to ensure the next call works as expected.
1127 args.paths.clear()
1128
1129 def do_lpwd(self, arglist):
1130 """
1131 Prints the absolute path of the current local directory
1132 """
1133 poutput(os.getcwd())
1134
1135 def complete_df(self, text, line, begidx, endidx):
1136 """
1137 auto complete of file name.
1138 """
1139 return self.complete_filenames(text, line, begidx, endidx)
1140
1141 df_parser = argparse.ArgumentParser(description='Show information about\
1142 the amount of available disk space')
1143 df_parser.add_argument('file', help='Name of the file', nargs='*',
1144 default=['.'], action=path_to_bytes)
1145
1146 @with_argparser(df_parser)
1147 def do_df(self, arglist):
1148 """
1149 Display the amount of available disk space for file systems
1150 """
1151 header = True # Set to true for printing header only once
1152 if b'.' == arglist.file[0]:
1153 arglist.file = ls(b'.')
1154
1155 for file in arglist.file:
1156 if isinstance(file, libcephfs.DirEntry):
1157 file = file.d_name
1158 if file == b'.' or file == b'..':
1159 continue
1160 try:
1161 statfs = cephfs.statfs(file)
1162 stat = cephfs.stat(file)
1163 block_size = (statfs['f_blocks'] * statfs['f_bsize']) // 1024
1164 available = block_size - stat.st_size
1165 use = 0
1166
1167 if block_size > 0:
1168 use = (stat.st_size * 100) // block_size
1169
1170 if header:
1171 header = False
1172 poutput('{:25s}\t{:5s}\t{:15s}{:10s}{}'.format(
1173 "1K-blocks", "Used", "Available", "Use%",
1174 "Stored on"))
1175
1176 poutput('{:d}\t{:18d}\t{:8d}\t{:10s} {}'.format(block_size,
1177 stat.st_size, available, str(int(use)) + '%',
1178 file.decode('utf-8')))
1179 except libcephfs.OSError as e:
1180 set_exit_code_msg(e.get_error_code(), "could not statfs {}: {}".format(
1181 file.decode('utf-8'), e.strerror))
1182
1183 locate_parser = argparse.ArgumentParser(
1184 description='Find file within file system')
1185 locate_parser.add_argument('name', help='name', type=str,
1186 action=path_to_bytes)
1187 locate_parser.add_argument('-c', '--count', action='store_true',
1188 help='Count list of items located.')
1189 locate_parser.add_argument(
1190 '-i', '--ignorecase', action='store_true', help='Ignore case')
1191
1192 @with_argparser(locate_parser)
1193 def do_locate(self, args):
1194 """
1195 Find a file within the File System
1196 """
1197 if args.name.count(b'*') == 1:
1198 if args.name[0] == b'*':
1199 args.name += b'/'
1200 elif args.name[-1] == '*':
1201 args.name = b'/' + args.name
1202 args.name = args.name.replace(b'*', b'')
1203 if args.ignorecase:
1204 locations = locate_file(args.name, False)
1205 else:
1206 locations = locate_file(args.name)
1207 if args.count:
1208 poutput(len(locations))
1209 else:
1210 poutput((b'\n'.join(locations)).decode('utf-8'))
1211
1212 def complete_du(self, text, line, begidx, endidx):
1213 """
1214 auto complete of file name.
1215 """
1216 return self.complete_filenames(text, line, begidx, endidx)
1217
1218 du_parser = argparse.ArgumentParser(
1219 description='Disk Usage of a Directory')
1220 du_parser.add_argument('paths', type=str, action=get_list_of_bytes_path,
1221 help='Name of the directory.', nargs='*',
1222 default=[b'.'])
1223 du_parser.add_argument('-r', action='store_true',
1224 help='Recursive Disk usage of all directories.')
1225
1226 @with_argparser(du_parser)
1227 def do_du(self, args):
1228 """
1229 Print disk usage of a given path(s).
1230 """
1231 def print_disk_usage(files):
1232 if isinstance(files, bytes):
1233 files = (files, )
1234
1235 for f in files:
1236 try:
1237 st = cephfs.lstat(f)
1238
1239 if stat.S_ISDIR(st.st_mode):
1240 dusage = int(cephfs.getxattr(f,
1241 'ceph.dir.rbytes').decode('utf-8'))
1242 else:
1243 dusage = st.st_size
1244
1245 # print path in local context
1246 f = os.path.normpath(f)
1247 if f[0] is ord('/'):
1248 f = b'.' + f
1249 poutput('{:10s} {}'.format(humansize(dusage),
1250 f.decode('utf-8')))
1251 except libcephfs.Error as e:
1252 set_exit_code_msg(msg=e)
1253 continue
1254
1255 for path in args.paths:
1256 if args.r:
1257 print_disk_usage(sorted(set(dirwalk(path)).union({path})))
1258 else:
1259 print_disk_usage(path)
1260
1261 quota_parser = argparse.ArgumentParser(
1262 description='Quota management for a Directory')
1263 quota_parser.add_argument('op', choices=['get', 'set'],
1264 help='Quota operation type.')
1265 quota_parser.add_argument('path', type=str, action=path_to_bytes,
1266 help='Name of the directory.')
1267 quota_parser.add_argument('--max_bytes', type=int, default=-1, nargs='?',
1268 help='Max cumulative size of the data under '
1269 'this directory.')
1270 quota_parser.add_argument('--max_files', type=int, default=-1, nargs='?',
1271 help='Total number of files under this '
1272 'directory tree.')
1273
1274 @with_argparser(quota_parser)
1275 def do_quota(self, args):
1276 """
1277 Quota management.
1278 """
1279 if not is_dir_exists(args.path):
1280 set_exit_code_msg(errno.ENOENT, 'error: no such directory {}'.format(
1281 args.path.decode('utf-8')))
1282 return
1283
1284 if args.op == 'set':
1285 if (args.max_bytes == -1) and (args.max_files == -1):
1286 set_exit_code_msg(errno.EINVAL, 'please specify either '
1287 '--max_bytes or --max_files or both')
1288 return
1289
1290 if args.max_bytes >= 0:
1291 max_bytes = to_bytes(str(args.max_bytes))
1292 try:
1293 cephfs.setxattr(args.path, 'ceph.quota.max_bytes',
1294 max_bytes, os.XATTR_CREATE)
1295 poutput('max_bytes set to %d' % args.max_bytes)
1296 except libcephfs.Error as e:
1297 cephfs.setxattr(args.path, 'ceph.quota.max_bytes',
1298 max_bytes, os.XATTR_REPLACE)
1299 set_exit_code_msg(e.get_error_code(), 'max_bytes reset to '
1300 f'{args.max_bytes}')
1301
1302 if args.max_files >= 0:
1303 max_files = to_bytes(str(args.max_files))
1304 try:
1305 cephfs.setxattr(args.path, 'ceph.quota.max_files',
1306 max_files, os.XATTR_CREATE)
1307 poutput('max_files set to %d' % args.max_files)
1308 except libcephfs.Error as e:
1309 cephfs.setxattr(args.path, 'ceph.quota.max_files',
1310 max_files, os.XATTR_REPLACE)
1311 set_exit_code_msg(e.get_error_code(), 'max_files reset to '
1312 f'{args.max_files}')
1313 elif args.op == 'get':
1314 max_bytes = '0'
1315 max_files = '0'
1316 try:
1317 max_bytes = cephfs.getxattr(args.path, 'ceph.quota.max_bytes')
1318 poutput('max_bytes: {}'.format(max_bytes.decode('utf-8')))
1319 except libcephfs.Error as e:
1320 set_exit_code_msg(e.get_error_code(), 'max_bytes is not set')
1321
1322 try:
1323 max_files = cephfs.getxattr(args.path, 'ceph.quota.max_files')
1324 poutput('max_files: {}'.format(max_files.decode('utf-8')))
1325 except libcephfs.Error as e:
1326 set_exit_code_msg(e.get_error_code(), 'max_files is not set')
1327
1328 snap_parser = argparse.ArgumentParser(description='Snapshot Management')
1329 snap_parser.add_argument('op', type=str,
1330 help='Snapshot operation: create or delete')
1331 snap_parser.add_argument('name', type=str, action=path_to_bytes,
1332 help='Name of snapshot')
1333 snap_parser.add_argument('dir', type=str, action=path_to_bytes,
1334 help='Directory for which snapshot '
1335 'needs to be created or deleted')
1336
1337 @with_argparser(snap_parser)
1338 def do_snap(self, args):
1339 """
1340 Snapshot management for the volume
1341 """
1342 # setting self.colors to None turns off colorizing and
1343 # perror emits plain text
1344 self.colors = None
1345
1346 snapdir = '.snap'
1347 conf_snapdir = cephfs.conf_get('client_snapdir')
1348 if conf_snapdir is not None:
1349 snapdir = conf_snapdir
1350 snapdir = to_bytes(snapdir)
1351 if args.op == 'create':
1352 try:
1353 if is_dir_exists(args.dir):
1354 cephfs.mkdir(os.path.join(args.dir, snapdir, args.name), 0o755)
1355 else:
1356 set_exit_code_msg(errno.ENOENT, "'{}': no such directory".format(
1357 args.dir.decode('utf-8')))
1358 except libcephfs.Error as e:
1359 set_exit_code_msg(e.get_error_code(),
1360 "snapshot '{}' already exists".format(
1361 args.name.decode('utf-8')))
1362 elif args.op == 'delete':
1363 snap_dir = os.path.join(args.dir, snapdir, args.name)
1364 try:
1365 if is_dir_exists(snap_dir):
1366 newargs = argparse.Namespace(paths=[snap_dir], parent=False)
1367 self.do_rmdir_helper(newargs)
1368 else:
1369 set_exit_code_msg(errno.ENOENT, "'{}': no such snapshot".format(
1370 args.name.decode('utf-8')))
1371 except libcephfs.Error as e:
1372 set_exit_code_msg(e.get_error_code(), "error while deleting "
1373 "'{}'".format(snap_dir.decode('utf-8')))
1374 else:
1375 set_exit_code_msg(errno.EINVAL, "snapshot can only be created or "
1376 "deleted; check - help snap")
1377
1378 def do_help(self, line):
1379 """
1380 Get details about a command.
1381 Usage: help <cmd> - for a specific command
1382 help all - for all the commands
1383 """
1384 if line == 'all':
1385 for k in dir(self):
1386 if k.startswith('do_'):
1387 poutput('-' * 80)
1388 super().do_help(k[3:])
1389 return
1390 parser = self.create_argparser(line)
1391 if parser:
1392 parser.print_help()
1393 else:
1394 super().do_help(line)
1395
1396 def complete_stat(self, text, line, begidx, endidx):
1397 """
1398 auto complete of file name.
1399 """
1400 return self.complete_filenames(text, line, begidx, endidx)
1401
1402 stat_parser = argparse.ArgumentParser(
1403 description='Display file or file system status')
1404 stat_parser.add_argument('paths', type=str, help='file paths',
1405 action=path_to_bytes, nargs='+')
1406
1407 @with_argparser(stat_parser)
1408 def do_stat(self, args):
1409 """
1410 Display file or file system status
1411 """
1412 for path in args.paths:
1413 try:
1414 stat = cephfs.stat(path)
1415 atime = stat.st_atime.isoformat(' ')
1416 mtime = stat.st_mtime.isoformat(' ')
1417 ctime = stat.st_mtime.isoformat(' ')
1418
1419 poutput("File: {}\nSize: {:d}\nBlocks: {:d}\nIO Block: {:d}\n"
1420 "Device: {:d}\tInode: {:d}\tLinks: {:d}\nPermission: "
1421 "{:o}/{}\tUid: {:d}\tGid: {:d}\nAccess: {}\nModify: "
1422 "{}\nChange: {}".format(path.decode('utf-8'),
1423 stat.st_size, stat.st_blocks,
1424 stat.st_blksize, stat.st_dev,
1425 stat.st_ino, stat.st_nlink,
1426 stat.st_mode,
1427 mode_notation(stat.st_mode),
1428 stat.st_uid, stat.st_gid, atime,
1429 mtime, ctime))
1430 except libcephfs.Error as e:
1431 set_exit_code_msg(msg=e)
1432
1433 setxattr_parser = argparse.ArgumentParser(
1434 description='Set extended attribute for a file')
1435 setxattr_parser.add_argument('path', type=str, action=path_to_bytes, help='Name of the file')
1436 setxattr_parser.add_argument('name', type=str, help='Extended attribute name')
1437 setxattr_parser.add_argument('value', type=str, help='Extended attribute value')
1438
1439 @with_argparser(setxattr_parser)
1440 def do_setxattr(self, args):
1441 """
1442 Set extended attribute for a file
1443 """
1444 val_bytes = to_bytes(args.value)
1445 name_bytes = to_bytes(args.name)
1446 try:
1447 cephfs.setxattr(args.path, name_bytes, val_bytes, os.XATTR_CREATE)
1448 poutput('{} is successfully set to {}'.format(args.name, args.value))
1449 except libcephfs.ObjectExists:
1450 cephfs.setxattr(args.path, name_bytes, val_bytes, os.XATTR_REPLACE)
1451 poutput('{} is successfully reset to {}'.format(args.name, args.value))
1452 except libcephfs.Error as e:
1453 set_exit_code_msg(msg=e)
1454
1455 getxattr_parser = argparse.ArgumentParser(
1456 description='Get extended attribute set for a file')
1457 getxattr_parser.add_argument('path', type=str, action=path_to_bytes,
1458 help='Name of the file')
1459 getxattr_parser.add_argument('name', type=str, help='Extended attribute name')
1460
1461 @with_argparser(getxattr_parser)
1462 def do_getxattr(self, args):
1463 """
1464 Get extended attribute for a file
1465 """
1466 try:
1467 poutput('{}'.format(cephfs.getxattr(args.path,
1468 to_bytes(args.name)).decode('utf-8')))
1469 except libcephfs.Error as e:
1470 set_exit_code_msg(msg=e)
1471
1472 listxattr_parser = argparse.ArgumentParser(
1473 description='List extended attributes set for a file')
1474 listxattr_parser.add_argument('path', type=str, action=path_to_bytes,
1475 help='Name of the file')
1476
1477 @with_argparser(listxattr_parser)
1478 def do_listxattr(self, args):
1479 """
1480 List extended attributes for a file
1481 """
1482 try:
1483 size, xattr_list = cephfs.listxattr(args.path)
1484 if size > 0:
1485 poutput('{}'.format(xattr_list.replace(b'\x00', b' ').decode('utf-8')))
1486 else:
1487 poutput('No extended attribute is set')
1488 except libcephfs.Error as e:
1489 set_exit_code_msg(msg=e)
1490
1491
1492 #######################################################
1493 #
1494 # Following are methods that get cephfs-shell started.
1495 #
1496 #####################################################
1497
1498 def setup_cephfs(args):
1499 """
1500 Mounting a cephfs
1501 """
1502 global cephfs
1503 try:
1504 cephfs = libcephfs.LibCephFS(conffile='')
1505 cephfs.mount(filesystem_name=args.fs)
1506 except libcephfs.ObjectNotFound as e:
1507 print('couldn\'t find ceph configuration not found')
1508 sys.exit(e.get_error_code())
1509 except libcephfs.Error as e:
1510 print(e)
1511 sys.exit(e.get_error_code())
1512
1513
1514 def str_to_bool(val):
1515 """
1516 Return corresponding bool values for strings like 'true' or 'false'.
1517 """
1518 if not isinstance(val, str):
1519 return val
1520
1521 val = val.replace('\n', '')
1522 if val.lower() in ['true', 'yes']:
1523 return True
1524 elif val.lower() in ['false', 'no']:
1525 return False
1526 else:
1527 return val
1528
1529
1530 def read_shell_conf(shell, shell_conf_file):
1531 import configparser
1532
1533 sec = 'cephfs-shell'
1534 opts = []
1535 if LooseVersion(cmd2_version) >= LooseVersion("0.10.0"):
1536 for attr in shell.settables.keys():
1537 opts.append(attr)
1538 else:
1539 if LooseVersion(cmd2_version) <= LooseVersion("0.9.13"):
1540 # hardcoding options for 0.7.9 because -
1541 # 1. we use cmd2 v0.7.9 with teuthology and
1542 # 2. there's no way distinguish between a shell setting and shell
1543 # object attribute until v0.10.0
1544 opts = ['abbrev', 'autorun_on_edit', 'colors',
1545 'continuation_prompt', 'debug', 'echo', 'editor',
1546 'feedback_to_output', 'locals_in_py', 'prompt', 'quiet',
1547 'timing']
1548 elif LooseVersion(cmd2_version) >= LooseVersion("0.9.23"):
1549 opts.append('allow_style')
1550 # no equivalent option was defined by cmd2.
1551 else:
1552 pass
1553
1554 # default and only section in our conf file.
1555 cp = configparser.ConfigParser(default_section=sec, strict=False)
1556 cp.read(shell_conf_file)
1557 for opt in opts:
1558 if cp.has_option(sec, opt):
1559 setattr(shell, opt, str_to_bool(cp.get(sec, opt)))
1560
1561
1562 def get_shell_conffile_path(arg_conf=''):
1563 conf_filename = 'cephfs-shell.conf'
1564 env_var = 'CEPHFS_SHELL_CONF'
1565
1566 arg_conf = '' if not arg_conf else arg_conf
1567 home_dir_conf = os.path.expanduser('~/.' + conf_filename)
1568 env_conf = os.environ[env_var] if env_var in os.environ else ''
1569
1570 # here's the priority by which conf gets read.
1571 for path in (arg_conf, env_conf, home_dir_conf):
1572 if os.path.isfile(path):
1573 return path
1574 else:
1575 return ''
1576
1577
1578 def manage_args():
1579 main_parser = argparse.ArgumentParser(description='')
1580 main_parser.add_argument('-b', '--batch', action='store',
1581 help='Path to CephFS shell script/batch file'
1582 'containing CephFS shell commands',
1583 type=str)
1584 main_parser.add_argument('-c', '--config', action='store',
1585 help='Path to Ceph configuration file.',
1586 type=str)
1587 main_parser.add_argument('-f', '--fs', action='store',
1588 help='Name of filesystem to mount.',
1589 type=str)
1590 main_parser.add_argument('-t', '--test', action='store',
1591 help='Test against transcript(s) in FILE',
1592 nargs='+')
1593 main_parser.add_argument('commands', nargs='*', help='Comma delimited '
1594 'commands. The shell executes the given command '
1595 'and quits immediately with the return value of '
1596 'command. In case no commands are provided, the '
1597 'shell is launched.', default=[])
1598
1599 args = main_parser.parse_args()
1600 args.exe_and_quit = False # Execute and quit, don't launch the shell.
1601
1602 if args.batch:
1603 if LooseVersion(cmd2_version) <= LooseVersion("0.9.13"):
1604 args.commands = ['load ' + args.batch, ',quit']
1605 else:
1606 args.commands = ['run_script ' + args.batch, ',quit']
1607 if args.test:
1608 args.commands.extend(['-t,'] + [arg + ',' for arg in args.test])
1609 if not args.batch and len(args.commands) > 0:
1610 args.exe_and_quit = True
1611
1612 manage_sys_argv(args)
1613
1614 return args
1615
1616
1617 def manage_sys_argv(args):
1618 exe = sys.argv[0]
1619 sys.argv.clear()
1620 sys.argv.append(exe)
1621 sys.argv.extend([i.strip() for i in ' '.join(args.commands).split(',')])
1622
1623 setup_cephfs(args)
1624
1625
1626 def execute_cmd_args(args):
1627 """
1628 Launch a shell session if no arguments were passed, else just execute
1629 the given argument as a shell command and exit the shell session
1630 immediately at (last) command's termination with the (last) command's
1631 return value.
1632 """
1633 if not args.exe_and_quit:
1634 return shell.cmdloop()
1635 return execute_cmds_and_quit(args)
1636
1637
1638 def execute_cmds_and_quit(args):
1639 """
1640 Multiple commands might be passed separated by commas, feed onecmd()
1641 one command at a time.
1642 """
1643 # do_* methods triggered by cephfs-shell commands return None when they
1644 # complete running successfully. Until 0.9.6, shell.onecmd() returned this
1645 # value to indicate whether the execution of the commands should stop, but
1646 # since 0.9.7 it returns the return value of do_* methods only if it's
1647 # not None. When it is None it returns False instead of None.
1648 if LooseVersion(cmd2_version) <= LooseVersion("0.9.6"):
1649 stop_exec_val = None
1650 else:
1651 stop_exec_val = False
1652
1653 args_to_onecmd = ''
1654 if len(args.commands) <= 1:
1655 args.commands = args.commands[0].split(' ')
1656 for cmdarg in args.commands:
1657 if ',' in cmdarg:
1658 args_to_onecmd += ' ' + cmdarg[0:-1]
1659 onecmd_retval = shell.onecmd(args_to_onecmd)
1660 # if the curent command failed, let's abort the execution of
1661 # series of commands passed.
1662 if onecmd_retval is not stop_exec_val:
1663 return onecmd_retval
1664 if shell.exit_code != 0:
1665 return shell.exit_code
1666
1667 args_to_onecmd = ''
1668 continue
1669
1670 args_to_onecmd += ' ' + cmdarg
1671 return shell.onecmd(args_to_onecmd)
1672
1673
1674 if __name__ == '__main__':
1675 args = manage_args()
1676
1677 shell = CephFSShell()
1678 # TODO: perhaps, we should add an option to pass ceph.conf?
1679 read_shell_conf(shell, get_shell_conffile_path(args.config))
1680 # XXX: setting shell.exit_code to zero so that in case there are no errors
1681 # and exceptions, it is not set by any method or function of cephfs-shell
1682 # and return values from shell.cmdloop() or shell.onecmd() is not an
1683 # integer, we can treat it as the return value of cephfs-shell.
1684 shell.exit_code = 0
1685
1686 retval = execute_cmd_args(args)
1687 sys.exit(retval if retval else shell.exit_code)