]> git.proxmox.com Git - ceph.git/blob - ceph/src/tools/cephfs/cephfs-shell
import quincy beta 17.1.0
[ceph.git] / ceph / src / tools / cephfs / 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 nargs='?', default='.')
610 put_parser.add_argument('-f', '--force', action='store_true',
611 help='Overwrites the destination if it already exists.')
612
613 @with_argparser(put_parser)
614 def do_put(self, args):
615 """
616 Copy a local file/directory to CephFS.
617 """
618 root_src_dir = args.local_path
619 root_dst_dir = args.remote_path
620 if args.local_path == b'.' or args.local_path == b'./':
621 root_src_dir = os.getcwdb()
622 elif len(args.local_path.rsplit(b'/', 1)) < 2:
623 root_src_dir = os.path.join(os.getcwdb(), args.local_path)
624 else:
625 p = args.local_path.split(b'/')
626 if p[0] == b'.':
627 root_src_dir = os.getcwdb()
628 p.pop(0)
629 while len(p) > 0:
630 root_src_dir += b'/' + p.pop(0)
631
632 if root_dst_dir == b'.':
633 if args.local_path != b'-':
634 root_dst_dir = root_src_dir.rsplit(b'/', 1)[1]
635 if root_dst_dir == b'':
636 root_dst_dir = root_src_dir.rsplit(b'/', 1)[0]
637 a = root_dst_dir.rsplit(b'/', 1)
638 if len(a) > 1:
639 root_dst_dir = a[1]
640 else:
641 root_dst_dir = a[0]
642 else:
643 set_exit_code_msg(errno.EINVAL, 'error: no filename specified '
644 'for destination')
645 return
646
647 if root_dst_dir[-1] != b'/':
648 root_dst_dir += b'/'
649
650 if args.local_path == b'-' or os.path.isfile(root_src_dir):
651 if not args.force:
652 if os.path.isfile(root_src_dir):
653 dst_file = root_dst_dir
654 if is_file_exists(dst_file):
655 set_exit_code_msg(errno.EEXIST,
656 f"{dst_file.decode('utf-8')}: file "
657 "exists! use --force to overwrite")
658 return
659 if args.local_path == b'-':
660 root_src_dir = b'-'
661 copy_from_local(root_src_dir, root_dst_dir)
662 else:
663 for src_dir, dirs, files in os.walk(root_src_dir):
664 if isinstance(src_dir, str):
665 src_dir = to_bytes(src_dir)
666 dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1)
667 dst_dir = re.sub(rb'\/+', b'/', cephfs.getcwd()
668 + dst_dir)
669 if args.force and dst_dir != b'/' and not is_dir_exists(
670 dst_dir[:-1]) and not locate_file(dst_dir):
671 try:
672 cephfs.mkdirs(dst_dir, 0o777)
673 except libcephfs.Error:
674 pass
675 if (not args.force) and dst_dir != b'/' and not is_dir_exists(
676 dst_dir) and not os.path.isfile(root_src_dir):
677 try:
678 cephfs.mkdirs(dst_dir, 0o777)
679 except libcephfs.Error:
680 # TODO: perhaps, set retval to 1?
681 pass
682
683 for dir_ in dirs:
684 dir_name = os.path.join(dst_dir, dir_)
685 if not is_dir_exists(dir_name):
686 try:
687 cephfs.mkdirs(dir_name, 0o777)
688 except libcephfs.Error:
689 # TODO: perhaps, set retval to 1?
690 pass
691
692 for file_ in files:
693 src_file = os.path.join(src_dir, file_)
694 dst_file = re.sub(rb'\/+', b'/', b'/' + dst_dir + b'/' + file_)
695 if (not args.force) and is_file_exists(dst_file):
696 return
697 copy_from_local(src_file, os.path.join(cephfs.getcwd(),
698 dst_file))
699
700 def complete_get(self, text, line, begidx, endidx):
701 """
702 auto complete of file name.
703 """
704 return self.complete_filenames(text, line, begidx, endidx)
705
706 get_parser = argparse.ArgumentParser(
707 description='Copy a file from Ceph File System from Local Directory.')
708 get_parser.add_argument('remote_path', type=str, action=path_to_bytes,
709 help='Path of the file in the remote system')
710 get_parser.add_argument('local_path', type=str, action=path_to_bytes,
711 help='Path of the file in the local system',
712 nargs='?', default='.')
713 get_parser.add_argument('-f', '--force', action='store_true',
714 help='Overwrites the destination if it already exists.')
715
716 @with_argparser(get_parser)
717 def do_get(self, args):
718 """
719 Copy a file/directory from CephFS to given path.
720 """
721 root_src_dir = args.remote_path
722 root_dst_dir = args.local_path
723 fname = root_src_dir.rsplit(b'/', 1)
724 if args.local_path == b'.':
725 root_dst_dir = os.getcwdb()
726 if args.remote_path == b'.':
727 root_src_dir = cephfs.getcwd()
728 if args.local_path == b'-':
729 if args.remote_path == b'.' or args.remote_path == b'./':
730 set_exit_code_msg(errno.EINVAL, 'error: no remote file name specified')
731 return
732 copy_to_local(root_src_dir, b'-')
733 elif is_file_exists(args.remote_path):
734 copy_to_local(root_src_dir,
735 root_dst_dir + b'/' + root_src_dir)
736 elif b'/' in root_src_dir and is_file_exists(fname[1], fname[0]):
737 copy_to_local(root_src_dir, root_dst_dir)
738 else:
739 files = list(reversed(sorted(dirwalk(root_src_dir))))
740 if len(files) == 0:
741 try:
742 os.makedirs(root_dst_dir + b'/' + root_src_dir)
743 except OSError as e:
744 if args.force:
745 pass
746 else:
747 set_exit_code_msg(e.errno, f"{root_src_dir.decode('utf-8')}: "
748 "already exists! use --force to overwrite")
749 return
750
751 for file_ in files:
752 dst_dirpath, dst_file = file_.rsplit(b'/', 1)
753 if dst_dirpath in files:
754 files.remove(dst_dirpath)
755 dst_path = os.path.join(root_dst_dir, dst_dirpath, dst_file)
756 dst_path = os.path.normpath(dst_path)
757 if is_dir_exists(file_):
758 try:
759 os.makedirs(dst_path)
760 except OSError:
761 pass
762 else:
763 if not args.force:
764 try:
765 os.stat(dst_path)
766 set_exit_code_msg(errno.EEXIST, f"{file_.decode('utf-8')}: "
767 "file already exists! use --force to override")
768 return
769 except OSError:
770 copy_to_local(file_, dst_path)
771 else:
772 copy_to_local(file_, dst_path)
773
774 return 0
775
776 def complete_ls(self, text, line, begidx, endidx):
777 """
778 auto complete of file name.
779 """
780 return self.complete_filenames(text, line, begidx, endidx)
781
782 ls_parser = argparse.ArgumentParser(
783 description='Copy a file from Ceph File System from Local Directory.')
784 ls_parser.add_argument('-l', '--long', action='store_true',
785 help='Detailed list of items in the directory.')
786 ls_parser.add_argument('-r', '--reverse', action='store_true',
787 help='Reverse order of listing items in the directory.')
788 ls_parser.add_argument('-H', action='store_true', help='Human Readable')
789 ls_parser.add_argument('-a', '--all', action='store_true',
790 help='Do not Ignore entries starting with .')
791 ls_parser.add_argument('-S', action='store_true', help='Sort by file_size')
792 ls_parser.add_argument('paths', help='Name of Directories',
793 action=path_to_bytes, nargs='*', default=['.'])
794
795 @with_argparser(ls_parser)
796 def do_ls(self, args):
797 """
798 List all the files and directories in the current working directory
799 """
800 paths = args.paths
801 for path in paths:
802 values = []
803 items = []
804 try:
805 if path.count(b'*') > 0:
806 all_items = get_all_possible_paths(path)
807 if len(all_items) == 0:
808 continue
809 path = all_items[0].rsplit(b'/', 1)[0]
810 if path == b'':
811 path = b'/'
812 dirs = []
813 for i in all_items:
814 for item in ls(path):
815 d_name = item.d_name
816 if os.path.basename(i) == d_name:
817 if item.is_dir():
818 dirs.append(os.path.join(path, d_name))
819 else:
820 items.append(item)
821 if dirs:
822 paths.extend(dirs)
823 else:
824 poutput(path.decode('utf-8'), end=':\n')
825 items = sorted(items, key=lambda item: item.d_name)
826 else:
827 if path != b'' and path != cephfs.getcwd() and len(paths) > 1:
828 poutput(path.decode('utf-8'), end=':\n')
829 items = sorted(ls(path), key=lambda item: item.d_name)
830 if not args.all:
831 items = [i for i in items if not i.d_name.startswith(b'.')]
832 if args.S:
833 items = sorted(items, key=lambda item: cephfs.stat(
834 path + b'/' + item.d_name, follow_symlink=(
835 not item.is_symbol_file())).st_size)
836 if args.reverse:
837 items = reversed(items)
838 for item in items:
839 filepath = item.d_name
840 is_dir = item.is_dir()
841 is_sym_lnk = item.is_symbol_file()
842 try:
843 if args.long and args.H:
844 print_long(os.path.join(cephfs.getcwd(), path, filepath), is_dir,
845 is_sym_lnk, True)
846 elif args.long:
847 print_long(os.path.join(cephfs.getcwd(), path, filepath), is_dir,
848 is_sym_lnk, False)
849 elif is_sym_lnk or is_dir:
850 values.append(style_listing(filepath.decode('utf-8'), is_dir,
851 is_sym_lnk))
852 else:
853 values.append(filepath)
854 except libcephfs.Error as e:
855 set_exit_code_msg(msg=e)
856 if not args.long:
857 print_list(values, shutil.get_terminal_size().columns)
858 if path != paths[-1]:
859 poutput('')
860 except libcephfs.Error as e:
861 set_exit_code_msg(msg=e)
862
863 def complete_rmdir(self, text, line, begidx, endidx):
864 """
865 auto complete of file name.
866 """
867 return self.complete_filenames(text, line, begidx, endidx)
868
869 rmdir_parser = argparse.ArgumentParser(description='Remove Directory.')
870 rmdir_parser.add_argument('paths', help='Directory Path.', nargs='+',
871 action=path_to_bytes)
872 rmdir_parser.add_argument('-p', '--parent', action='store_true',
873 help='Remove parent directories as necessary. '
874 'When this option is specified, no error '
875 'is reported if a directory has any '
876 'sub-directories, files')
877
878 @with_argparser(rmdir_parser)
879 def do_rmdir(self, args):
880 self.do_rmdir_helper(args)
881
882 def do_rmdir_helper(self, args):
883 """
884 Remove a specific Directory
885 """
886 is_pattern = False
887 paths = args.paths
888 for path in paths:
889 if path.count(b'*') > 0:
890 is_pattern = True
891 all_items = get_all_possible_paths(path)
892 if len(all_items) > 0:
893 path = all_items[0].rsplit(b'/', 1)[0]
894 if path == b'':
895 path = b'/'
896 dirs = []
897 for i in all_items:
898 for item in ls(path):
899 d_name = item.d_name
900 if os.path.basename(i) == d_name:
901 if item.is_dir():
902 dirs.append(os.path.join(path, d_name))
903 paths.extend(dirs)
904 continue
905 else:
906 is_pattern = False
907
908 if args.parent:
909 path = os.path.join(cephfs.getcwd(), path.rsplit(b'/')[0])
910 files = list(sorted(set(dirwalk(path)), reverse=True))
911 if not files:
912 path = b'.'
913 for filepath in files:
914 try:
915 cephfs.rmdir(os.path.normpath(filepath))
916 except libcephfs.Error as e:
917 perror(e)
918 path = b'.'
919 break
920 else:
921 path = os.path.normpath(os.path.join(cephfs.getcwd(), path))
922 if not is_pattern and path != os.path.normpath(b''):
923 try:
924 cephfs.rmdir(path)
925 except libcephfs.Error as e:
926 set_exit_code_msg(msg=e)
927
928 def complete_rm(self, text, line, begidx, endidx):
929 """
930 auto complete of file name.
931 """
932 return self.complete_filenames(text, line, begidx, endidx)
933
934 rm_parser = argparse.ArgumentParser(description='Remove File.')
935 rm_parser.add_argument('paths', help='File Path.', nargs='+',
936 action=path_to_bytes)
937
938 @with_argparser(rm_parser)
939 def do_rm(self, args):
940 """
941 Remove a specific file
942 """
943 file_paths = args.paths
944 for path in file_paths:
945 if path.count(b'*') > 0:
946 file_paths.extend([i for i in get_all_possible_paths(
947 path) if is_file_exists(i)])
948 else:
949 try:
950 cephfs.unlink(path)
951 except libcephfs.Error as e:
952 # NOTE: perhaps we need a better msg here
953 set_exit_code_msg(msg=e)
954
955 def complete_mv(self, text, line, begidx, endidx):
956 """
957 auto complete of file name.
958 """
959 return self.complete_filenames(text, line, begidx, endidx)
960
961 mv_parser = argparse.ArgumentParser(description='Move File.')
962 mv_parser.add_argument('src_path', type=str, action=path_to_bytes,
963 help='Source File Path.')
964 mv_parser.add_argument('dest_path', type=str, action=path_to_bytes,
965 help='Destination File Path.')
966
967 @with_argparser(mv_parser)
968 def do_mv(self, args):
969 """
970 Rename a file or Move a file from source path to the destination
971 """
972 cephfs.rename(args.src_path, args.dest_path)
973
974 def complete_cd(self, text, line, begidx, endidx):
975 """
976 auto complete of file name.
977 """
978 return self.complete_filenames(text, line, begidx, endidx)
979
980 cd_parser = argparse.ArgumentParser(description='Change working directory')
981 cd_parser.add_argument('path', type=str, help='Name of the directory.',
982 action=path_to_bytes, nargs='?', default='/')
983
984 @with_argparser(cd_parser)
985 def do_cd(self, args):
986 """
987 Change working directory
988 """
989 cephfs.chdir(args.path)
990 self.working_dir = cephfs.getcwd().decode('utf-8')
991 self.set_prompt()
992
993 def do_cwd(self, arglist):
994 """
995 Get current working directory.
996 """
997 poutput(cephfs.getcwd().decode('utf-8'))
998
999 def complete_chmod(self, text, line, begidx, endidx):
1000 """
1001 auto complete of file name.
1002 """
1003 return self.complete_filenames(text, line, begidx, endidx)
1004
1005 chmod_parser = argparse.ArgumentParser(description='Create Directory.')
1006 chmod_parser.add_argument('mode', type=str, action=ModeAction, help='Mode')
1007 chmod_parser.add_argument('paths', type=str, action=path_to_bytes,
1008 help='Name of the file', nargs='+')
1009
1010 @with_argparser(chmod_parser)
1011 def do_chmod(self, args):
1012 """
1013 Change permission of a file
1014 """
1015 for path in args.paths:
1016 mode = int(args.mode, base=8)
1017 try:
1018 cephfs.chmod(path, mode)
1019 except libcephfs.Error as e:
1020 set_exit_code_msg(msg=e)
1021
1022 def complete_cat(self, text, line, begidx, endidx):
1023 """
1024 auto complete of file name.
1025 """
1026 return self.complete_filenames(text, line, begidx, endidx)
1027
1028 cat_parser = argparse.ArgumentParser(description='')
1029 cat_parser.add_argument('paths', help='Name of Files', action=path_to_bytes,
1030 nargs='+')
1031
1032 @with_argparser(cat_parser)
1033 def do_cat(self, args):
1034 """
1035 Print contents of a file
1036 """
1037 for path in args.paths:
1038 if is_file_exists(path):
1039 copy_to_local(path, b'-')
1040 else:
1041 set_exit_code_msg(errno.ENOENT, '{}: no such file'.format(
1042 path.decode('utf-8')))
1043
1044 umask_parser = argparse.ArgumentParser(description='Set umask value.')
1045 umask_parser.add_argument('mode', help='Mode', type=str, action=ModeAction,
1046 nargs='?', default='')
1047
1048 @with_argparser(umask_parser)
1049 def do_umask(self, args):
1050 """
1051 Set Umask value.
1052 """
1053 if args.mode == '':
1054 poutput(self.umask.zfill(4))
1055 else:
1056 mode = int(args.mode, 8)
1057 self.umask = str(oct(cephfs.umask(mode))[2:])
1058
1059 def complete_write(self, text, line, begidx, endidx):
1060 """
1061 auto complete of file name.
1062 """
1063 return self.complete_filenames(text, line, begidx, endidx)
1064
1065 write_parser = argparse.ArgumentParser(description='Writes data into a file')
1066 write_parser.add_argument('path', type=str, action=path_to_bytes,
1067 help='Name of File')
1068
1069 @with_argparser(write_parser)
1070 def do_write(self, args):
1071 """
1072 Write data into a file.
1073 """
1074
1075 copy_from_local(b'-', args.path)
1076
1077 def complete_lcd(self, text, line, begidx, endidx):
1078 """
1079 auto complete of file name.
1080 """
1081 index_dict = {1: self.path_complete}
1082 return self.index_based_complete(text, line, begidx, endidx, index_dict)
1083
1084 lcd_parser = argparse.ArgumentParser(description='')
1085 lcd_parser.add_argument('path', type=str, action=path_to_bytes, help='Path')
1086
1087 @with_argparser(lcd_parser)
1088 def do_lcd(self, args):
1089 """
1090 Moves into the given local directory
1091 """
1092 try:
1093 os.chdir(os.path.expanduser(args.path))
1094 except OSError as e:
1095 set_exit_code_msg(e.errno, "Cannot change to "
1096 f"{e.filename.decode('utf-8')}: {e.strerror}")
1097
1098 def complete_lls(self, text, line, begidx, endidx):
1099 """
1100 auto complete of file name.
1101 """
1102 index_dict = {1: self.path_complete}
1103 return self.index_based_complete(text, line, begidx, endidx, index_dict)
1104
1105 lls_parser = argparse.ArgumentParser(
1106 description='List files in local system.')
1107 lls_parser.add_argument('paths', help='Paths', action=path_to_bytes,
1108 nargs='*')
1109
1110 @with_argparser(lls_parser)
1111 def do_lls(self, args):
1112 """
1113 Lists all files and folders in the current local directory
1114 """
1115 if not args.paths:
1116 print_list(os.listdir(os.getcwdb()))
1117 else:
1118 for path in args.paths:
1119 try:
1120 items = os.listdir(path)
1121 poutput("{}:".format(path.decode('utf-8')))
1122 print_list(items)
1123 except OSError as e:
1124 set_exit_code_msg(e.errno, f"{e.filename.decode('utf-8')}: "
1125 f"{e.strerror}")
1126 # Arguments to the with_argpaser decorator function are sticky.
1127 # The items in args.path do not get overwritten in subsequent calls.
1128 # The arguments remain in args.paths after the function exits and we
1129 # neeed to clean it up to ensure the next call works as expected.
1130 args.paths.clear()
1131
1132 def do_lpwd(self, arglist):
1133 """
1134 Prints the absolute path of the current local directory
1135 """
1136 poutput(os.getcwd())
1137
1138 def complete_df(self, text, line, begidx, endidx):
1139 """
1140 auto complete of file name.
1141 """
1142 return self.complete_filenames(text, line, begidx, endidx)
1143
1144 df_parser = argparse.ArgumentParser(description='Show information about\
1145 the amount of available disk space')
1146 df_parser.add_argument('file', help='Name of the file', nargs='*',
1147 default=['.'], action=path_to_bytes)
1148
1149 @with_argparser(df_parser)
1150 def do_df(self, arglist):
1151 """
1152 Display the amount of available disk space for file systems
1153 """
1154 header = True # Set to true for printing header only once
1155 if b'.' == arglist.file[0]:
1156 arglist.file = ls(b'.')
1157
1158 for file in arglist.file:
1159 if isinstance(file, libcephfs.DirEntry):
1160 file = file.d_name
1161 if file == b'.' or file == b'..':
1162 continue
1163 try:
1164 statfs = cephfs.statfs(file)
1165 stat = cephfs.stat(file)
1166 block_size = (statfs['f_blocks'] * statfs['f_bsize']) // 1024
1167 available = block_size - stat.st_size
1168 use = 0
1169
1170 if block_size > 0:
1171 use = (stat.st_size * 100) // block_size
1172
1173 if header:
1174 header = False
1175 poutput('{:25s}\t{:5s}\t{:15s}{:10s}{}'.format(
1176 "1K-blocks", "Used", "Available", "Use%",
1177 "Stored on"))
1178
1179 poutput('{:d}\t{:18d}\t{:8d}\t{:10s} {}'.format(block_size,
1180 stat.st_size, available, str(int(use)) + '%',
1181 file.decode('utf-8')))
1182 except libcephfs.OSError as e:
1183 set_exit_code_msg(e.get_error_code(), "could not statfs {}: {}".format(
1184 file.decode('utf-8'), e.strerror))
1185
1186 locate_parser = argparse.ArgumentParser(
1187 description='Find file within file system')
1188 locate_parser.add_argument('name', help='name', type=str,
1189 action=path_to_bytes)
1190 locate_parser.add_argument('-c', '--count', action='store_true',
1191 help='Count list of items located.')
1192 locate_parser.add_argument(
1193 '-i', '--ignorecase', action='store_true', help='Ignore case')
1194
1195 @with_argparser(locate_parser)
1196 def do_locate(self, args):
1197 """
1198 Find a file within the File System
1199 """
1200 if args.name.count(b'*') == 1:
1201 if args.name[0] == b'*':
1202 args.name += b'/'
1203 elif args.name[-1] == '*':
1204 args.name = b'/' + args.name
1205 args.name = args.name.replace(b'*', b'')
1206 if args.ignorecase:
1207 locations = locate_file(args.name, False)
1208 else:
1209 locations = locate_file(args.name)
1210 if args.count:
1211 poutput(len(locations))
1212 else:
1213 poutput((b'\n'.join(locations)).decode('utf-8'))
1214
1215 def complete_du(self, text, line, begidx, endidx):
1216 """
1217 auto complete of file name.
1218 """
1219 return self.complete_filenames(text, line, begidx, endidx)
1220
1221 du_parser = argparse.ArgumentParser(
1222 description='Disk Usage of a Directory')
1223 du_parser.add_argument('paths', type=str, action=get_list_of_bytes_path,
1224 help='Name of the directory.', nargs='*',
1225 default=[b'.'])
1226 du_parser.add_argument('-r', action='store_true',
1227 help='Recursive Disk usage of all directories.')
1228
1229 @with_argparser(du_parser)
1230 def do_du(self, args):
1231 """
1232 Print disk usage of a given path(s).
1233 """
1234 def print_disk_usage(files):
1235 if isinstance(files, bytes):
1236 files = (files, )
1237
1238 for f in files:
1239 try:
1240 st = cephfs.lstat(f)
1241
1242 if stat.S_ISDIR(st.st_mode):
1243 dusage = int(cephfs.getxattr(f,
1244 'ceph.dir.rbytes').decode('utf-8'))
1245 else:
1246 dusage = st.st_size
1247
1248 # print path in local context
1249 f = os.path.normpath(f)
1250 if f[0] is ord('/'):
1251 f = b'.' + f
1252 poutput('{:10s} {}'.format(humansize(dusage),
1253 f.decode('utf-8')))
1254 except libcephfs.Error as e:
1255 set_exit_code_msg(msg=e)
1256 continue
1257
1258 for path in args.paths:
1259 if args.r:
1260 print_disk_usage(sorted(set(dirwalk(path)).union({path})))
1261 else:
1262 print_disk_usage(path)
1263
1264 quota_parser = argparse.ArgumentParser(
1265 description='Quota management for a Directory')
1266 quota_parser.add_argument('op', choices=['get', 'set'],
1267 help='Quota operation type.')
1268 quota_parser.add_argument('path', type=str, action=path_to_bytes,
1269 help='Name of the directory.')
1270 quota_parser.add_argument('--max_bytes', type=int, default=-1, nargs='?',
1271 help='Max cumulative size of the data under '
1272 'this directory.')
1273 quota_parser.add_argument('--max_files', type=int, default=-1, nargs='?',
1274 help='Total number of files under this '
1275 'directory tree.')
1276
1277 @with_argparser(quota_parser)
1278 def do_quota(self, args):
1279 """
1280 Quota management.
1281 """
1282 if not is_dir_exists(args.path):
1283 set_exit_code_msg(errno.ENOENT, 'error: no such directory {}'.format(
1284 args.path.decode('utf-8')))
1285 return
1286
1287 if args.op == 'set':
1288 if (args.max_bytes == -1) and (args.max_files == -1):
1289 set_exit_code_msg(errno.EINVAL, 'please specify either '
1290 '--max_bytes or --max_files or both')
1291 return
1292
1293 if args.max_bytes >= 0:
1294 max_bytes = to_bytes(str(args.max_bytes))
1295 try:
1296 cephfs.setxattr(args.path, 'ceph.quota.max_bytes',
1297 max_bytes, os.XATTR_CREATE)
1298 poutput('max_bytes set to %d' % args.max_bytes)
1299 except libcephfs.Error as e:
1300 cephfs.setxattr(args.path, 'ceph.quota.max_bytes',
1301 max_bytes, os.XATTR_REPLACE)
1302 set_exit_code_msg(e.get_error_code(), 'max_bytes reset to '
1303 f'{args.max_bytes}')
1304
1305 if args.max_files >= 0:
1306 max_files = to_bytes(str(args.max_files))
1307 try:
1308 cephfs.setxattr(args.path, 'ceph.quota.max_files',
1309 max_files, os.XATTR_CREATE)
1310 poutput('max_files set to %d' % args.max_files)
1311 except libcephfs.Error as e:
1312 cephfs.setxattr(args.path, 'ceph.quota.max_files',
1313 max_files, os.XATTR_REPLACE)
1314 set_exit_code_msg(e.get_error_code(), 'max_files reset to '
1315 f'{args.max_files}')
1316 elif args.op == 'get':
1317 max_bytes = '0'
1318 max_files = '0'
1319 try:
1320 max_bytes = cephfs.getxattr(args.path, 'ceph.quota.max_bytes')
1321 poutput('max_bytes: {}'.format(max_bytes.decode('utf-8')))
1322 except libcephfs.Error as e:
1323 set_exit_code_msg(e.get_error_code(), 'max_bytes is not set')
1324
1325 try:
1326 max_files = cephfs.getxattr(args.path, 'ceph.quota.max_files')
1327 poutput('max_files: {}'.format(max_files.decode('utf-8')))
1328 except libcephfs.Error as e:
1329 set_exit_code_msg(e.get_error_code(), 'max_files is not set')
1330
1331 snap_parser = argparse.ArgumentParser(description='Snapshot Management')
1332 snap_parser.add_argument('op', type=str,
1333 help='Snapshot operation: create or delete')
1334 snap_parser.add_argument('name', type=str, action=path_to_bytes,
1335 help='Name of snapshot')
1336 snap_parser.add_argument('dir', type=str, action=path_to_bytes,
1337 help='Directory for which snapshot '
1338 'needs to be created or deleted')
1339
1340 @with_argparser(snap_parser)
1341 def do_snap(self, args):
1342 """
1343 Snapshot management for the volume
1344 """
1345 # setting self.colors to None turns off colorizing and
1346 # perror emits plain text
1347 self.colors = None
1348
1349 snapdir = '.snap'
1350 conf_snapdir = cephfs.conf_get('client_snapdir')
1351 if conf_snapdir is not None:
1352 snapdir = conf_snapdir
1353 snapdir = to_bytes(snapdir)
1354 if args.op == 'create':
1355 try:
1356 if is_dir_exists(args.dir):
1357 cephfs.mkdir(os.path.join(args.dir, snapdir, args.name), 0o755)
1358 else:
1359 set_exit_code_msg(errno.ENOENT, "'{}': no such directory".format(
1360 args.dir.decode('utf-8')))
1361 except libcephfs.Error as e:
1362 set_exit_code_msg(e.get_error_code(),
1363 "snapshot '{}' already exists".format(
1364 args.name.decode('utf-8')))
1365 elif args.op == 'delete':
1366 snap_dir = os.path.join(args.dir, snapdir, args.name)
1367 try:
1368 if is_dir_exists(snap_dir):
1369 newargs = argparse.Namespace(paths=[snap_dir], parent=False)
1370 self.do_rmdir_helper(newargs)
1371 else:
1372 set_exit_code_msg(errno.ENOENT, "'{}': no such snapshot".format(
1373 args.name.decode('utf-8')))
1374 except libcephfs.Error as e:
1375 set_exit_code_msg(e.get_error_code(), "error while deleting "
1376 "'{}'".format(snap_dir.decode('utf-8')))
1377 else:
1378 set_exit_code_msg(errno.EINVAL, "snapshot can only be created or "
1379 "deleted; check - help snap")
1380
1381 def do_help(self, line):
1382 """
1383 Get details about a command.
1384 Usage: help <cmd> - for a specific command
1385 help all - for all the commands
1386 """
1387 if line == 'all':
1388 for k in dir(self):
1389 if k.startswith('do_'):
1390 poutput('-' * 80)
1391 super().do_help(k[3:])
1392 return
1393 parser = self.create_argparser(line)
1394 if parser:
1395 parser.print_help()
1396 else:
1397 super().do_help(line)
1398
1399 def complete_stat(self, text, line, begidx, endidx):
1400 """
1401 auto complete of file name.
1402 """
1403 return self.complete_filenames(text, line, begidx, endidx)
1404
1405 stat_parser = argparse.ArgumentParser(
1406 description='Display file or file system status')
1407 stat_parser.add_argument('paths', type=str, help='file paths',
1408 action=path_to_bytes, nargs='+')
1409
1410 @with_argparser(stat_parser)
1411 def do_stat(self, args):
1412 """
1413 Display file or file system status
1414 """
1415 for path in args.paths:
1416 try:
1417 stat = cephfs.stat(path)
1418 atime = stat.st_atime.isoformat(' ')
1419 mtime = stat.st_mtime.isoformat(' ')
1420 ctime = stat.st_mtime.isoformat(' ')
1421
1422 poutput("File: {}\nSize: {:d}\nBlocks: {:d}\nIO Block: {:d}\n"
1423 "Device: {:d}\tInode: {:d}\tLinks: {:d}\nPermission: "
1424 "{:o}/{}\tUid: {:d}\tGid: {:d}\nAccess: {}\nModify: "
1425 "{}\nChange: {}".format(path.decode('utf-8'),
1426 stat.st_size, stat.st_blocks,
1427 stat.st_blksize, stat.st_dev,
1428 stat.st_ino, stat.st_nlink,
1429 stat.st_mode,
1430 mode_notation(stat.st_mode),
1431 stat.st_uid, stat.st_gid, atime,
1432 mtime, ctime))
1433 except libcephfs.Error as e:
1434 set_exit_code_msg(msg=e)
1435
1436 setxattr_parser = argparse.ArgumentParser(
1437 description='Set extended attribute for a file')
1438 setxattr_parser.add_argument('path', type=str, action=path_to_bytes, help='Name of the file')
1439 setxattr_parser.add_argument('name', type=str, help='Extended attribute name')
1440 setxattr_parser.add_argument('value', type=str, help='Extended attribute value')
1441
1442 @with_argparser(setxattr_parser)
1443 def do_setxattr(self, args):
1444 """
1445 Set extended attribute for a file
1446 """
1447 val_bytes = to_bytes(args.value)
1448 name_bytes = to_bytes(args.name)
1449 try:
1450 cephfs.setxattr(args.path, name_bytes, val_bytes, os.XATTR_CREATE)
1451 poutput('{} is successfully set to {}'.format(args.name, args.value))
1452 except libcephfs.ObjectExists:
1453 cephfs.setxattr(args.path, name_bytes, val_bytes, os.XATTR_REPLACE)
1454 poutput('{} is successfully reset to {}'.format(args.name, args.value))
1455 except libcephfs.Error as e:
1456 set_exit_code_msg(msg=e)
1457
1458 getxattr_parser = argparse.ArgumentParser(
1459 description='Get extended attribute set for a file')
1460 getxattr_parser.add_argument('path', type=str, action=path_to_bytes,
1461 help='Name of the file')
1462 getxattr_parser.add_argument('name', type=str, help='Extended attribute name')
1463
1464 @with_argparser(getxattr_parser)
1465 def do_getxattr(self, args):
1466 """
1467 Get extended attribute for a file
1468 """
1469 try:
1470 poutput('{}'.format(cephfs.getxattr(args.path,
1471 to_bytes(args.name)).decode('utf-8')))
1472 except libcephfs.Error as e:
1473 set_exit_code_msg(msg=e)
1474
1475 listxattr_parser = argparse.ArgumentParser(
1476 description='List extended attributes set for a file')
1477 listxattr_parser.add_argument('path', type=str, action=path_to_bytes,
1478 help='Name of the file')
1479
1480 @with_argparser(listxattr_parser)
1481 def do_listxattr(self, args):
1482 """
1483 List extended attributes for a file
1484 """
1485 try:
1486 size, xattr_list = cephfs.listxattr(args.path)
1487 if size > 0:
1488 poutput('{}'.format(xattr_list.replace(b'\x00', b' ').decode('utf-8')))
1489 else:
1490 poutput('No extended attribute is set')
1491 except libcephfs.Error as e:
1492 set_exit_code_msg(msg=e)
1493
1494
1495 #######################################################
1496 #
1497 # Following are methods that get cephfs-shell started.
1498 #
1499 #####################################################
1500
1501 def setup_cephfs(args):
1502 """
1503 Mounting a cephfs
1504 """
1505 global cephfs
1506 try:
1507 cephfs = libcephfs.LibCephFS(conffile='')
1508 cephfs.mount(filesystem_name=args.fs)
1509 except libcephfs.ObjectNotFound as e:
1510 print('couldn\'t find ceph configuration not found')
1511 sys.exit(e.get_error_code())
1512 except libcephfs.Error as e:
1513 print(e)
1514 sys.exit(e.get_error_code())
1515
1516
1517 def str_to_bool(val):
1518 """
1519 Return corresponding bool values for strings like 'true' or 'false'.
1520 """
1521 if not isinstance(val, str):
1522 return val
1523
1524 val = val.replace('\n', '')
1525 if val.lower() in ['true', 'yes']:
1526 return True
1527 elif val.lower() in ['false', 'no']:
1528 return False
1529 else:
1530 return val
1531
1532
1533 def read_shell_conf(shell, shell_conf_file):
1534 import configparser
1535
1536 sec = 'cephfs-shell'
1537 opts = []
1538 if LooseVersion(cmd2_version) >= LooseVersion("0.10.0"):
1539 for attr in shell.settables.keys():
1540 opts.append(attr)
1541 else:
1542 if LooseVersion(cmd2_version) <= LooseVersion("0.9.13"):
1543 # hardcoding options for 0.7.9 because -
1544 # 1. we use cmd2 v0.7.9 with teuthology and
1545 # 2. there's no way distinguish between a shell setting and shell
1546 # object attribute until v0.10.0
1547 opts = ['abbrev', 'autorun_on_edit', 'colors',
1548 'continuation_prompt', 'debug', 'echo', 'editor',
1549 'feedback_to_output', 'locals_in_py', 'prompt', 'quiet',
1550 'timing']
1551 elif LooseVersion(cmd2_version) >= LooseVersion("0.9.23"):
1552 opts.append('allow_style')
1553 # no equivalent option was defined by cmd2.
1554 else:
1555 pass
1556
1557 # default and only section in our conf file.
1558 cp = configparser.ConfigParser(default_section=sec, strict=False)
1559 cp.read(shell_conf_file)
1560 for opt in opts:
1561 if cp.has_option(sec, opt):
1562 setattr(shell, opt, str_to_bool(cp.get(sec, opt)))
1563
1564
1565 def get_shell_conffile_path(arg_conf=''):
1566 conf_filename = 'cephfs-shell.conf'
1567 env_var = 'CEPHFS_SHELL_CONF'
1568
1569 arg_conf = '' if not arg_conf else arg_conf
1570 home_dir_conf = os.path.expanduser('~/.' + conf_filename)
1571 env_conf = os.environ[env_var] if env_var in os.environ else ''
1572
1573 # here's the priority by which conf gets read.
1574 for path in (arg_conf, env_conf, home_dir_conf):
1575 if os.path.isfile(path):
1576 return path
1577 else:
1578 return ''
1579
1580
1581 def manage_args():
1582 main_parser = argparse.ArgumentParser(description='')
1583 main_parser.add_argument('-b', '--batch', action='store',
1584 help='Path to CephFS shell script/batch file'
1585 'containing CephFS shell commands',
1586 type=str)
1587 main_parser.add_argument('-c', '--config', action='store',
1588 help='Path to Ceph configuration file.',
1589 type=str)
1590 main_parser.add_argument('-f', '--fs', action='store',
1591 help='Name of filesystem to mount.',
1592 type=str)
1593 main_parser.add_argument('-t', '--test', action='store',
1594 help='Test against transcript(s) in FILE',
1595 nargs='+')
1596 main_parser.add_argument('commands', nargs='*', help='Comma delimited '
1597 'commands. The shell executes the given command '
1598 'and quits immediately with the return value of '
1599 'command. In case no commands are provided, the '
1600 'shell is launched.', default=[])
1601
1602 args = main_parser.parse_args()
1603 args.exe_and_quit = False # Execute and quit, don't launch the shell.
1604
1605 if args.batch:
1606 if LooseVersion(cmd2_version) <= LooseVersion("0.9.13"):
1607 args.commands = ['load ' + args.batch, ',quit']
1608 else:
1609 args.commands = ['run_script ' + args.batch, ',quit']
1610 if args.test:
1611 args.commands.extend(['-t,'] + [arg + ',' for arg in args.test])
1612 if not args.batch and len(args.commands) > 0:
1613 args.exe_and_quit = True
1614
1615 manage_sys_argv(args)
1616
1617 return args
1618
1619
1620 def manage_sys_argv(args):
1621 exe = sys.argv[0]
1622 sys.argv.clear()
1623 sys.argv.append(exe)
1624 sys.argv.extend([i.strip() for i in ' '.join(args.commands).split(',')])
1625
1626 setup_cephfs(args)
1627
1628
1629 def execute_cmd_args(args):
1630 """
1631 Launch a shell session if no arguments were passed, else just execute
1632 the given argument as a shell command and exit the shell session
1633 immediately at (last) command's termination with the (last) command's
1634 return value.
1635 """
1636 if not args.exe_and_quit:
1637 return shell.cmdloop()
1638 return execute_cmds_and_quit(args)
1639
1640
1641 def execute_cmds_and_quit(args):
1642 """
1643 Multiple commands might be passed separated by commas, feed onecmd()
1644 one command at a time.
1645 """
1646 # do_* methods triggered by cephfs-shell commands return None when they
1647 # complete running successfully. Until 0.9.6, shell.onecmd() returned this
1648 # value to indicate whether the execution of the commands should stop, but
1649 # since 0.9.7 it returns the return value of do_* methods only if it's
1650 # not None. When it is None it returns False instead of None.
1651 if LooseVersion(cmd2_version) <= LooseVersion("0.9.6"):
1652 stop_exec_val = None
1653 else:
1654 stop_exec_val = False
1655
1656 args_to_onecmd = ''
1657 if len(args.commands) <= 1:
1658 args.commands = args.commands[0].split(' ')
1659 for cmdarg in args.commands:
1660 if ',' in cmdarg:
1661 args_to_onecmd += ' ' + cmdarg[0:-1]
1662 onecmd_retval = shell.onecmd(args_to_onecmd)
1663 # if the curent command failed, let's abort the execution of
1664 # series of commands passed.
1665 if onecmd_retval is not stop_exec_val:
1666 return onecmd_retval
1667 if shell.exit_code != 0:
1668 return shell.exit_code
1669
1670 args_to_onecmd = ''
1671 continue
1672
1673 args_to_onecmd += ' ' + cmdarg
1674 return shell.onecmd(args_to_onecmd)
1675
1676
1677 if __name__ == '__main__':
1678 args = manage_args()
1679
1680 shell = CephFSShell()
1681 # TODO: perhaps, we should add an option to pass ceph.conf?
1682 read_shell_conf(shell, get_shell_conffile_path(args.config))
1683 # XXX: setting shell.exit_code to zero so that in case there are no errors
1684 # and exceptions, it is not set by any method or function of cephfs-shell
1685 # and return values from shell.cmdloop() or shell.onecmd() is not an
1686 # integer, we can treat it as the return value of cephfs-shell.
1687 shell.exit_code = 0
1688
1689 retval = execute_cmd_args(args)
1690 sys.exit(retval if retval else shell.exit_code)