]> git.proxmox.com Git - ceph.git/blame - ceph/src/tools/cephfs/cephfs-shell
import ceph quincy 17.2.1
[ceph.git] / ceph / src / tools / cephfs / cephfs-shell
CommitLineData
11fdf7f2
TL
1#!/usr/bin/python3
2# coding = utf-8
3
4import argparse
5import os
6import os.path
7import sys
11fdf7f2
TL
8import cephfs as libcephfs
9import shutil
10import traceback
11import colorama
11fdf7f2
TL
12import fnmatch
13import math
14import re
15import shlex
9f95a23c 16import stat
f67539c2 17import errno
9f95a23c
TL
18
19from cmd2 import Cmd
20from cmd2 import __version__ as cmd2_version
21from distutils.version import LooseVersion
11fdf7f2 22
81eedcae 23if sys.version_info.major < 3:
9f95a23c 24 raise RuntimeError("cephfs-shell is only compatible with python3")
81eedcae 25
11fdf7f2
TL
26try:
27 from cmd2 import with_argparser
28except 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:
9f95a23c 45 shell.exit_code = 1
11fdf7f2
TL
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
9f95a23c
TL
59cephfs = None # holds CephFS Python bindings
60shell = None # holds instance of class CephFSShell
f67539c2
TL
61exit_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
81eedcae 84
9f95a23c
TL
85#########################################################################
86#
87# Following are methods are generically useful through class CephFSShell
88#
89#######################################################################
81eedcae 90
e306af50 91
81eedcae 92def poutput(s, end='\n'):
494da23a 93 shell.poutput(s, end=end)
11fdf7f2
TL
94
95
9f95a23c
TL
96def perror(msg, **kwargs):
97 shell.perror(msg, **kwargs)
11fdf7f2
TL
98
99
f67539c2
TL
100def 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
11fdf7f2
TL
112def mode_notation(mode):
113 """
114 """
115 permission_bits = {'0': '---',
116 '1': '--x',
117 '2': '-w-',
9f95a23c 118 '3': '-wx',
11fdf7f2
TL
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'
f67539c2
TL
127 elif mode[2:4] == '12':
128 notation = 'l'
11fdf7f2
TL
129 for i in mode[-3:]:
130 notation += permission_bits[i]
131 return notation
132
133
134def 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
9f95a23c
TL
144def 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):
e306af50 153 return [i.encode('utf-8') if isinstance(i, str) else to_bytes(i) for
9f95a23c
TL
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
11fdf7f2 159
e306af50 160
81eedcae
TL
161def 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
9f95a23c 173 except libcephfs.ObjectNotFound as e:
f67539c2 174 set_exit_code_msg(msg=e)
11fdf7f2 175
e306af50 176
81eedcae 177def glob(path, pattern):
11fdf7f2 178 paths = []
81eedcae 179 parent_dir = os.path.dirname(path)
eafe8130
TL
180 if parent_dir == b'':
181 parent_dir = b'/'
182 if path == b'/' or is_dir_exists(os.path.basename(path), parent_dir):
81eedcae
TL
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))
11fdf7f2
TL
186 return paths
187
188
189def locate_file(name, case_sensitive=True):
eafe8130 190 dir_list = sorted(set(dirwalk(cephfs.getcwd())))
11fdf7f2 191 if not case_sensitive:
81eedcae 192 return [dname for dname in dir_list if name.lower() in dname.lower()]
11fdf7f2 193 else:
81eedcae 194 return [dname for dname in dir_list if name in dname]
11fdf7f2
TL
195
196
197def 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:
eafe8130 204 dir_ = b'/'
11fdf7f2 205 pattern = pattern[1:]
eafe8130 206 patterns = pattern.split(b'/')
11fdf7f2
TL
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))
9f95a23c
TL
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)]
11fdf7f2
TL
215
216
217suffixes = ['B', 'K', 'M', 'G', 'T', 'P']
218
219
220def humansize(nbytes):
221 i = 0
9f95a23c 222 while nbytes >= 1024 and i < len(suffixes) - 1:
11fdf7f2
TL
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
f67539c2
TL
230def 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
245def 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)
11fdf7f2 248 if human_readable:
f67539c2 249 sizefmt = '\t {:10s}'.format(humansize(info.st_size))
11fdf7f2 250 else:
f67539c2
TL
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}')
11fdf7f2
TL
254
255
256def 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
eafe8130 265def is_dir_exists(path, dir_=b''):
81eedcae
TL
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
11fdf7f2
TL
271
272
eafe8130 273def is_file_exists(path, dir_=b''):
81eedcae
TL
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
11fdf7f2
TL
279
280
81eedcae 281def print_list(words, termwidth=79):
11fdf7f2
TL
282 if not words:
283 return
eafe8130 284 words = [word.decode('utf-8') if isinstance(word, bytes) else word for word in words]
11fdf7f2
TL
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]
81eedcae 292 print_width = width
11fdf7f2 293 if word[0] == '\x1b':
81eedcae
TL
294 print_width = print_width + 10
295
296 poutput('%-*s' % (print_width, words[i]),
297 end='\n' if i + nrows >= nwords else '')
11fdf7f2
TL
298
299
81eedcae 300def copy_from_local(local_path, remote_path):
11fdf7f2 301 stdin = -1
81eedcae
TL
302 file_ = None
303 fd = None
304 convert_to_bytes = False
eafe8130 305 if local_path == b'-':
81eedcae
TL
306 file_ = sys.stdin
307 convert_to_bytes = True
11fdf7f2 308 else:
81eedcae
TL
309 try:
310 file_ = open(local_path, 'rb')
f67539c2
TL
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')))
81eedcae 314 return
11fdf7f2 315 stdin = 1
81eedcae
TL
316 try:
317 fd = cephfs.open(remote_path, 'w', 0o666)
9f95a23c 318 except libcephfs.Error as e:
f67539c2 319 set_exit_code_msg(msg=e)
11fdf7f2
TL
320 return
321 progress = 0
322 while True:
323 data = file_.read(65536)
81eedcae 324 if not data or len(data) == 0:
11fdf7f2 325 break
81eedcae
TL
326 if convert_to_bytes:
327 data = to_bytes(data)
11fdf7f2
TL
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()
81eedcae 335 poutput('')
11fdf7f2
TL
336
337
81eedcae 338def copy_to_local(remote_path, local_path):
11fdf7f2 339 fd = None
eafe8130 340 if local_path != b'-':
11fdf7f2 341 local_dir = os.path.dirname(local_path)
eafe8130 342 dir_list = remote_path.rsplit(b'/', 1)
11fdf7f2
TL
343 if not os.path.exists(local_dir):
344 os.makedirs(local_dir)
eafe8130 345 if len(dir_list) > 2 and dir_list[1] == b'':
11fdf7f2
TL
346 return
347 fd = open(local_path, 'wb+')
81eedcae 348 file_ = cephfs.open(remote_path, 'r')
11fdf7f2
TL
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:
81eedcae 359 poutput(file_chunk.decode('utf-8'))
11fdf7f2
TL
360 cephfs.close(file_)
361 if fd:
362 fd.close()
363
364
81eedcae 365def dirwalk(path):
11fdf7f2
TL
366 """
367 walk a directory tree, using a generator
368 """
81eedcae
TL
369 path = os.path.normpath(path)
370 for item in ls(path, opts='A'):
eafe8130
TL
371 fullpath = os.path.join(path, item.d_name)
372 src_path = fullpath.rsplit(b'/', 1)[0]
81eedcae
TL
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
11fdf7f2
TL
378
379
9f95a23c
TL
380##################################################################
381#
382# Following methods are implementation for CephFS Shell commands
383#
384#################################################################
385
11fdf7f2
TL
386class CephFSShell(Cmd):
387
388 def __init__(self):
20effc67 389 super().__init__()
11fdf7f2
TL
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):
9f95a23c 396 perror('Unrecognized command')
11fdf7f2
TL
397
398 def set_prompt(self):
81eedcae
TL
399 self.prompt = ('\033[01;33mCephFS:~' + colorama.Fore.LIGHTCYAN_EX
400 + self.working_dir + colorama.Style.RESET_ALL
401 + '\033[01;33m>>>\033[00m ')
11fdf7f2
TL
402
403 def create_argparser(self, command):
404 try:
405 argparse_args = getattr(self, 'argparse_' + command)
406 except AttributeError:
f67539c2 407 set_exit_code_msg()
11fdf7f2
TL
408 return None
409 doc_lines = getattr(
410 self, 'do_' + command).__doc__.expandtabs().splitlines()
e306af50 411 if '' in doc_lines:
11fdf7f2
TL
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:
81eedcae
TL
430 completions = [x.d_name.decode('utf-8') + '/' * int(x.is_dir())
431 for x in ls(b".", opts='A')]
11fdf7f2
TL
432 else:
433 if text.count('/') > 0:
81eedcae
TL
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])]
11fdf7f2 440 else:
81eedcae
TL
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)]
11fdf7f2
TL
444 if len(completions) == 1 and completions[0][-1] == '/':
445 dir_, file_ = completions[0].rsplit('/', 1)
81eedcae
TL
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_)])
11fdf7f2
TL
450 return self.delimiter_complete(text, line, begidx, endidx, completions, '/')
451 return completions
452
9f95a23c 453 def onecmd(self, line, **kwargs):
11fdf7f2
TL
454 """
455 Global error catcher
456 """
457 try:
9f95a23c 458 res = Cmd.onecmd(self, line, **kwargs)
11fdf7f2
TL
459 if self.interactive:
460 self.set_prompt()
461 return res
462 except ConnectionError as e:
f67539c2 463 set_exit_code_msg(e.errno, f'***\n{e}')
11fdf7f2 464 except KeyboardInterrupt:
f67539c2 465 set_exit_code_msg('KeyboardInterrupt', 'Command aborted')
9f95a23c 466 except (libcephfs.Error, Exception) as e:
9f95a23c
TL
467 if shell.debug:
468 traceback.print_exc(file=sys.stdout)
f67539c2 469 set_exit_code_msg(msg=e)
11fdf7f2 470
eafe8130
TL
471 class path_to_bytes(argparse.Action):
472 def __call__(self, parser, namespace, values, option_string=None):
9f95a23c
TL
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
eafe8130
TL
488 setattr(namespace, self.dest, values)
489
11fdf7f2
TL
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
81eedcae
TL
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
11fdf7f2
TL
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,
eafe8130 566 action=path_to_bytes,
11fdf7f2
TL
567 metavar='DIR_NAME',
568 help='Name of new_directory.',
569 nargs='+')
570 mkdir_parser.add_argument('-m', '--mode', type=str,
81eedcae 571 action=ModeAction,
11fdf7f2
TL
572 help='Sets the access mode for the new directory.')
573 mkdir_parser.add_argument('-p', '--parent', action='store_true',
9f95a23c
TL
574 help='Create parent directories as necessary. '
575 'When this option is specified, no error is'
576 'reported if a directory already exists.')
11fdf7f2
TL
577
578 @with_argparser(mkdir_parser)
579 def do_mkdir(self, args):
580 """
581 Create directory.
582 """
81eedcae 583 for path in args.dirs:
11fdf7f2
TL
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:
81eedcae
TL
591 try:
592 cephfs.mkdir(path, permission)
9f95a23c 593 except libcephfs.Error as e:
f67539c2 594 set_exit_code_msg(e)
11fdf7f2
TL
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.')
eafe8130 605 put_parser.add_argument('local_path', type=str, action=path_to_bytes,
11fdf7f2 606 help='Path of the file in the local system')
eafe8130 607 put_parser.add_argument('remote_path', type=str, action=path_to_bytes,
33c7a0ef 608 help='Path of the file in the remote system')
11fdf7f2
TL
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 """
9f95a23c 615 Copy a local file/directory to CephFS.
11fdf7f2 616 """
33c7a0ef
TL
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
11fdf7f2
TL
632 root_src_dir = args.local_path
633 root_dst_dir = args.remote_path
eafe8130
TL
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)
81eedcae 638 else:
eafe8130
TL
639 p = args.local_path.split(b'/')
640 if p[0] == b'.':
641 root_src_dir = os.getcwdb()
81eedcae
TL
642 p.pop(0)
643 while len(p) > 0:
eafe8130
TL
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)
81eedcae
TL
652 if len(a) > 1:
653 root_dst_dir = a[1]
654 else:
655 root_dst_dir = a[0]
656 else:
f67539c2
TL
657 set_exit_code_msg(errno.EINVAL, 'error: no filename specified '
658 'for destination')
81eedcae
TL
659 return
660
eafe8130
TL
661 if root_dst_dir[-1] != b'/':
662 root_dst_dir += b'/'
81eedcae 663
eafe8130 664 if args.local_path == b'-' or os.path.isfile(root_src_dir):
eafe8130
TL
665 if args.local_path == b'-':
666 root_src_dir = b'-'
81eedcae 667 copy_from_local(root_src_dir, root_dst_dir)
11fdf7f2
TL
668 else:
669 for src_dir, dirs, files in os.walk(root_src_dir):
eafe8130
TL
670 if isinstance(src_dir, str):
671 src_dir = to_bytes(src_dir)
11fdf7f2 672 dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1)
eafe8130 673 dst_dir = re.sub(rb'\/+', b'/', cephfs.getcwd()
81eedcae 674 + dst_dir)
eafe8130 675 if args.force and dst_dir != b'/' and not is_dir_exists(
81eedcae
TL
676 dst_dir[:-1]) and not locate_file(dst_dir):
677 try:
678 cephfs.mkdirs(dst_dir, 0o777)
679 except libcephfs.Error:
680 pass
eafe8130 681 if (not args.force) and dst_dir != b'/' and not is_dir_exists(
81eedcae
TL
682 dst_dir) and not os.path.isfile(root_src_dir):
683 try:
684 cephfs.mkdirs(dst_dir, 0o777)
685 except libcephfs.Error:
9f95a23c 686 # TODO: perhaps, set retval to 1?
81eedcae
TL
687 pass
688
11fdf7f2 689 for dir_ in dirs:
81eedcae
TL
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:
9f95a23c 695 # TODO: perhaps, set retval to 1?
81eedcae
TL
696 pass
697
11fdf7f2
TL
698 for file_ in files:
699 src_file = os.path.join(src_dir, file_)
eafe8130 700 dst_file = re.sub(rb'\/+', b'/', b'/' + dst_dir + b'/' + file_)
11fdf7f2
TL
701 if (not args.force) and is_file_exists(dst_file):
702 return
eafe8130
TL
703 copy_from_local(src_file, os.path.join(cephfs.getcwd(),
704 dst_file))
11fdf7f2
TL
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(
33c7a0ef 713 description='Copy a file from Ceph File System to Local Directory.')
eafe8130 714 get_parser.add_argument('remote_path', type=str, action=path_to_bytes,
11fdf7f2 715 help='Path of the file in the remote system')
eafe8130 716 get_parser.add_argument('local_path', type=str, action=path_to_bytes,
33c7a0ef 717 help='Path of the file in the local system')
81eedcae
TL
718 get_parser.add_argument('-f', '--force', action='store_true',
719 help='Overwrites the destination if it already exists.')
11fdf7f2
TL
720
721 @with_argparser(get_parser)
722 def do_get(self, args):
723 """
9f95a23c 724 Copy a file/directory from CephFS to given path.
11fdf7f2 725 """
33c7a0ef
TL
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
11fdf7f2
TL
739 root_src_dir = args.remote_path
740 root_dst_dir = args.local_path
eafe8130
TL
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'./':
f67539c2 748 set_exit_code_msg(errno.EINVAL, 'error: no remote file name specified')
81eedcae 749 return
eafe8130 750 copy_to_local(root_src_dir, b'-')
11fdf7f2 751 elif is_file_exists(args.remote_path):
33c7a0ef 752 copy_to_local(root_src_dir, root_dst_dir)
e306af50 753 elif b'/' in root_src_dir and is_file_exists(fname[1], fname[0]):
81eedcae 754 copy_to_local(root_src_dir, root_dst_dir)
11fdf7f2
TL
755 else:
756 files = list(reversed(sorted(dirwalk(root_src_dir))))
11fdf7f2 757 for file_ in files:
eafe8130 758 dst_dirpath, dst_file = file_.rsplit(b'/', 1)
11fdf7f2
TL
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)
11fdf7f2 763 if is_dir_exists(file_):
81eedcae
TL
764 try:
765 os.makedirs(dst_path)
766 except OSError:
767 pass
11fdf7f2 768 else:
33c7a0ef 769 copy_to_local(file_, dst_path)
81eedcae 770
11fdf7f2
TL
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')
eafe8130
TL
789 ls_parser.add_argument('paths', help='Name of Directories',
790 action=path_to_bytes, nargs='*', default=['.'])
11fdf7f2
TL
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 """
81eedcae
TL
797 paths = args.paths
798 for path in paths:
11fdf7f2
TL
799 values = []
800 items = []
f67539c2
TL
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)
11fdf7f2 823 else:
f67539c2
TL
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)
11fdf7f2
TL
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.')
eafe8130
TL
867 rmdir_parser.add_argument('paths', help='Directory Path.', nargs='+',
868 action=path_to_bytes)
11fdf7f2 869 rmdir_parser.add_argument('-p', '--parent', action='store_true',
9f95a23c
TL
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')
11fdf7f2
TL
874
875 @with_argparser(rmdir_parser)
876 def do_rmdir(self, args):
9f95a23c
TL
877 self.do_rmdir_helper(args)
878
879 def do_rmdir_helper(self, args):
11fdf7f2
TL
880 """
881 Remove a specific Directory
882 """
883 is_pattern = False
81eedcae
TL
884 paths = args.paths
885 for path in paths:
eafe8130 886 if path.count(b'*') > 0:
11fdf7f2 887 is_pattern = True
81eedcae 888 all_items = get_all_possible_paths(path)
11fdf7f2 889 if len(all_items) > 0:
eafe8130
TL
890 path = all_items[0].rsplit(b'/', 1)[0]
891 if path == b'':
892 path = b'/'
11fdf7f2
TL
893 dirs = []
894 for i in all_items:
81eedcae
TL
895 for item in ls(path):
896 d_name = item.d_name
11fdf7f2
TL
897 if os.path.basename(i) == d_name:
898 if item.is_dir():
81eedcae
TL
899 dirs.append(os.path.join(path, d_name))
900 paths.extend(dirs)
11fdf7f2
TL
901 continue
902 else:
903 is_pattern = False
9f95a23c 904
11fdf7f2 905 if args.parent:
9f95a23c
TL
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'.'
eafe8130 910 for filepath in files:
9f95a23c
TL
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))
eafe8130 919 if not is_pattern and path != os.path.normpath(b''):
81eedcae
TL
920 try:
921 cephfs.rmdir(path)
9f95a23c 922 except libcephfs.Error as e:
f67539c2 923 set_exit_code_msg(msg=e)
11fdf7f2
TL
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.')
eafe8130
TL
932 rm_parser.add_argument('paths', help='File Path.', nargs='+',
933 action=path_to_bytes)
11fdf7f2
TL
934
935 @with_argparser(rm_parser)
936 def do_rm(self, args):
937 """
938 Remove a specific file
939 """
494da23a
TL
940 file_paths = args.paths
941 for path in file_paths:
eafe8130 942 if path.count(b'*') > 0:
494da23a 943 file_paths.extend([i for i in get_all_possible_paths(
81eedcae 944 path) if is_file_exists(i)])
11fdf7f2 945 else:
81eedcae
TL
946 try:
947 cephfs.unlink(path)
9f95a23c
TL
948 except libcephfs.Error as e:
949 # NOTE: perhaps we need a better msg here
f67539c2 950 set_exit_code_msg(msg=e)
11fdf7f2
TL
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.')
eafe8130
TL
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,
11fdf7f2
TL
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 """
9f95a23c 969 cephfs.rename(args.src_path, args.dest_path)
11fdf7f2
TL
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')
eafe8130
TL
978 cd_parser.add_argument('path', type=str, help='Name of the directory.',
979 action=path_to_bytes, nargs='?', default='/')
11fdf7f2
TL
980
981 @with_argparser(cd_parser)
982 def do_cd(self, args):
983 """
984 Change working directory
985 """
9f95a23c
TL
986 cephfs.chdir(args.path)
987 self.working_dir = cephfs.getcwd().decode('utf-8')
988 self.set_prompt()
11fdf7f2
TL
989
990 def do_cwd(self, arglist):
991 """
992 Get current working directory.
993 """
9f95a23c 994 poutput(cephfs.getcwd().decode('utf-8'))
11fdf7f2
TL
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.')
81eedcae 1003 chmod_parser.add_argument('mode', type=str, action=ModeAction, help='Mode')
eafe8130
TL
1004 chmod_parser.add_argument('paths', type=str, action=path_to_bytes,
1005 help='Name of the file', nargs='+')
11fdf7f2
TL
1006
1007 @with_argparser(chmod_parser)
1008 def do_chmod(self, args):
1009 """
1010 Change permission of a file
1011 """
81eedcae
TL
1012 for path in args.paths:
1013 mode = int(args.mode, base=8)
1014 try:
1015 cephfs.chmod(path, mode)
9f95a23c 1016 except libcephfs.Error as e:
f67539c2 1017 set_exit_code_msg(msg=e)
11fdf7f2
TL
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='')
eafe8130
TL
1026 cat_parser.add_argument('paths', help='Name of Files', action=path_to_bytes,
1027 nargs='+')
11fdf7f2
TL
1028
1029 @with_argparser(cat_parser)
1030 def do_cat(self, args):
1031 """
1032 Print contents of a file
1033 """
81eedcae
TL
1034 for path in args.paths:
1035 if is_file_exists(path):
eafe8130 1036 copy_to_local(path, b'-')
81eedcae 1037 else:
f67539c2
TL
1038 set_exit_code_msg(errno.ENOENT, '{}: no such file'.format(
1039 path.decode('utf-8')))
11fdf7f2
TL
1040
1041 umask_parser = argparse.ArgumentParser(description='Set umask value.')
81eedcae
TL
1042 umask_parser.add_argument('mode', help='Mode', type=str, action=ModeAction,
1043 nargs='?', default='')
11fdf7f2
TL
1044
1045 @with_argparser(umask_parser)
1046 def do_umask(self, args):
1047 """
1048 Set Umask value.
1049 """
1050 if args.mode == '':
9f95a23c 1051 poutput(self.umask.zfill(4))
11fdf7f2
TL
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
eafe8130
TL
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')
11fdf7f2
TL
1065
1066 @with_argparser(write_parser)
1067 def do_write(self, args):
1068 """
1069 Write data into a file.
1070 """
1071
eafe8130 1072 copy_from_local(b'-', args.path)
11fdf7f2
TL
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='')
eafe8130 1082 lcd_parser.add_argument('path', type=str, action=path_to_bytes, help='Path')
11fdf7f2
TL
1083
1084 @with_argparser(lcd_parser)
1085 def do_lcd(self, args):
1086 """
1087 Moves into the given local directory
1088 """
81eedcae
TL
1089 try:
1090 os.chdir(os.path.expanduser(args.path))
1091 except OSError as e:
f67539c2
TL
1092 set_exit_code_msg(e.errno, "Cannot change to "
1093 f"{e.filename.decode('utf-8')}: {e.strerror}")
11fdf7f2
TL
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.')
eafe8130
TL
1104 lls_parser.add_argument('paths', help='Paths', action=path_to_bytes,
1105 nargs='*')
11fdf7f2
TL
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 """
81eedcae 1112 if not args.paths:
eafe8130 1113 print_list(os.listdir(os.getcwdb()))
81eedcae
TL
1114 else:
1115 for path in args.paths:
1116 try:
1117 items = os.listdir(path)
9f95a23c 1118 poutput("{}:".format(path.decode('utf-8')))
81eedcae
TL
1119 print_list(items)
1120 except OSError as e:
f67539c2
TL
1121 set_exit_code_msg(e.errno, f"{e.filename.decode('utf-8')}: "
1122 f"{e.strerror}")
81eedcae
TL
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()
11fdf7f2
TL
1128
1129 def do_lpwd(self, arglist):
1130 """
1131 Prints the absolute path of the current local directory
1132 """
9f95a23c
TL
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)
11fdf7f2 1140
9f95a23c
TL
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)
11fdf7f2
TL
1147 def do_df(self, arglist):
1148 """
1149 Display the amount of available disk space for file systems
1150 """
9f95a23c
TL
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)
e306af50 1163 block_size = (statfs['f_blocks'] * statfs['f_bsize']) // 1024
11fdf7f2
TL
1164 available = block_size - stat.st_size
1165 use = 0
9f95a23c 1166
11fdf7f2 1167 if block_size > 0:
e306af50 1168 use = (stat.st_size * 100) // block_size
9f95a23c
TL
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:
f67539c2
TL
1180 set_exit_code_msg(e.get_error_code(), "could not statfs {}: {}".format(
1181 file.decode('utf-8'), e.strerror))
11fdf7f2
TL
1182
1183 locate_parser = argparse.ArgumentParser(
1184 description='Find file within file system')
eafe8130
TL
1185 locate_parser.add_argument('name', help='name', type=str,
1186 action=path_to_bytes)
81eedcae
TL
1187 locate_parser.add_argument('-c', '--count', action='store_true',
1188 help='Count list of items located.')
11fdf7f2
TL
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 """
eafe8130
TL
1197 if args.name.count(b'*') == 1:
1198 if args.name[0] == b'*':
1199 args.name += b'/'
11fdf7f2 1200 elif args.name[-1] == '*':
eafe8130
TL
1201 args.name = b'/' + args.name
1202 args.name = args.name.replace(b'*', b'')
11fdf7f2
TL
1203 if args.ignorecase:
1204 locations = locate_file(args.name, False)
1205 else:
1206 locations = locate_file(args.name)
1207 if args.count:
9f95a23c 1208 poutput(len(locations))
11fdf7f2 1209 else:
9f95a23c 1210 poutput((b'\n'.join(locations)).decode('utf-8'))
11fdf7f2
TL
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')
9f95a23c
TL
1220 du_parser.add_argument('paths', type=str, action=get_list_of_bytes_path,
1221 help='Name of the directory.', nargs='*',
1222 default=[b'.'])
11fdf7f2
TL
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 """
9f95a23c 1229 Print disk usage of a given path(s).
11fdf7f2 1230 """
9f95a23c
TL
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),
e306af50 1250 f.decode('utf-8')))
9f95a23c 1251 except libcephfs.Error as e:
f67539c2 1252 set_exit_code_msg(msg=e)
9f95a23c
TL
1253 continue
1254
1255 for path in args.paths:
11fdf7f2 1256 if args.r:
9f95a23c 1257 print_disk_usage(sorted(set(dirwalk(path)).union({path})))
11fdf7f2 1258 else:
9f95a23c 1259 print_disk_usage(path)
81eedcae
TL
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.')
eafe8130
TL
1265 quota_parser.add_argument('path', type=str, action=path_to_bytes,
1266 help='Name of the directory.')
81eedcae
TL
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):
f67539c2
TL
1280 set_exit_code_msg(errno.ENOENT, 'error: no such directory {}'.format(
1281 args.path.decode('utf-8')))
81eedcae
TL
1282 return
1283
1284 if args.op == 'set':
1285 if (args.max_bytes == -1) and (args.max_files == -1):
f67539c2
TL
1286 set_exit_code_msg(errno.EINVAL, 'please specify either '
1287 '--max_bytes or --max_files or both')
81eedcae
TL
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',
9f95a23c
TL
1294 max_bytes, os.XATTR_CREATE)
1295 poutput('max_bytes set to %d' % args.max_bytes)
1296 except libcephfs.Error as e:
81eedcae 1297 cephfs.setxattr(args.path, 'ceph.quota.max_bytes',
9f95a23c 1298 max_bytes, os.XATTR_REPLACE)
f67539c2
TL
1299 set_exit_code_msg(e.get_error_code(), 'max_bytes reset to '
1300 f'{args.max_bytes}')
81eedcae
TL
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',
9f95a23c
TL
1306 max_files, os.XATTR_CREATE)
1307 poutput('max_files set to %d' % args.max_files)
1308 except libcephfs.Error as e:
81eedcae 1309 cephfs.setxattr(args.path, 'ceph.quota.max_files',
9f95a23c 1310 max_files, os.XATTR_REPLACE)
f67539c2
TL
1311 set_exit_code_msg(e.get_error_code(), 'max_files reset to '
1312 f'{args.max_files}')
81eedcae
TL
1313 elif args.op == 'get':
1314 max_bytes = '0'
1315 max_files = '0'
1316 try:
9f95a23c
TL
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:
f67539c2 1320 set_exit_code_msg(e.get_error_code(), 'max_bytes is not set')
81eedcae
TL
1321
1322 try:
9f95a23c
TL
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:
f67539c2 1326 set_exit_code_msg(e.get_error_code(), 'max_files is not set')
9f95a23c
TL
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:
f67539c2
TL
1356 set_exit_code_msg(errno.ENOENT, "'{}': no such directory".format(
1357 args.dir.decode('utf-8')))
9f95a23c 1358 except libcephfs.Error as e:
f67539c2
TL
1359 set_exit_code_msg(e.get_error_code(),
1360 "snapshot '{}' already exists".format(
1361 args.name.decode('utf-8')))
9f95a23c
TL
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:
f67539c2 1369 set_exit_code_msg(errno.ENOENT, "'{}': no such snapshot".format(
9f95a23c 1370 args.name.decode('utf-8')))
9f95a23c 1371 except libcephfs.Error as e:
f67539c2
TL
1372 set_exit_code_msg(e.get_error_code(), "error while deleting "
1373 "'{}'".format(snap_dir.decode('utf-8')))
9f95a23c 1374 else:
f67539c2
TL
1375 set_exit_code_msg(errno.EINVAL, "snapshot can only be created or "
1376 "deleted; check - help snap")
11fdf7f2
TL
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_'):
9f95a23c 1387 poutput('-' * 80)
11fdf7f2
TL
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
81eedcae
TL
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(
9f95a23c 1403 description='Display file or file system status')
81eedcae 1404 stat_parser.add_argument('paths', type=str, help='file paths',
eafe8130 1405 action=path_to_bytes, nargs='+')
81eedcae
TL
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
9f95a23c
TL
1419 poutput("File: {}\nSize: {:d}\nBlocks: {:d}\nIO Block: {:d}\n"
1420 "Device: {:d}\tInode: {:d}\tLinks: {:d}\nPermission: "
e306af50 1421 "{:o}/{}\tUid: {:d}\tGid: {:d}\nAccess: {}\nModify: "
9f95a23c 1422 "{}\nChange: {}".format(path.decode('utf-8'),
e306af50
TL
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))
9f95a23c 1430 except libcephfs.Error as e:
f67539c2 1431 set_exit_code_msg(msg=e)
9f95a23c
TL
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:
f67539c2 1453 set_exit_code_msg(msg=e)
9f95a23c
TL
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:
f67539c2 1470 set_exit_code_msg(msg=e)
9f95a23c
TL
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:
f67539c2 1489 set_exit_code_msg(msg=e)
9f95a23c
TL
1490
1491
1492#######################################################
1493#
1494# Following are methods that get cephfs-shell started.
1495#
1496#####################################################
1497
20effc67 1498def setup_cephfs(args):
9f95a23c
TL
1499 """
1500 Mounting a cephfs
1501 """
1502 global cephfs
1503 try:
f67539c2 1504 cephfs = libcephfs.LibCephFS(conffile='')
20effc67 1505 cephfs.mount(filesystem_name=args.fs)
f67539c2 1506 except libcephfs.ObjectNotFound as e:
9f95a23c 1507 print('couldn\'t find ceph configuration not found')
f67539c2 1508 sys.exit(e.get_error_code())
9f95a23c
TL
1509 except libcephfs.Error as e:
1510 print(e)
f67539c2 1511 sys.exit(e.get_error_code())
9f95a23c 1512
e306af50 1513
f67539c2
TL
1514def str_to_bool(val):
1515 """
1516 Return corresponding bool values for strings like 'true' or 'false'.
1517 """
9f95a23c
TL
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
81eedcae 1528
e306af50 1529
f67539c2
TL
1530def read_shell_conf(shell, shell_conf_file):
1531 import configparser
11fdf7f2 1532
f67539c2
TL
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
e306af50 1553
f67539c2
TL
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
1562def 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 ''
9f95a23c 1576
9f95a23c 1577
f67539c2 1578def manage_args():
11fdf7f2 1579 main_parser = argparse.ArgumentParser(description='')
f67539c2
TL
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)
20effc67
TL
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)
11fdf7f2 1590 main_parser.add_argument('-t', '--test', action='store',
81eedcae
TL
1591 help='Test against transcript(s) in FILE',
1592 nargs='+')
f67539c2
TL
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
11fdf7f2 1599 args = main_parser.parse_args()
f67539c2
TL
1600 args.exe_and_quit = False # Execute and quit, don't launch the shell.
1601
11fdf7f2 1602 if args.batch:
9f95a23c
TL
1603 if LooseVersion(cmd2_version) <= LooseVersion("0.9.13"):
1604 args.commands = ['load ' + args.batch, ',quit']
1605 else:
e306af50 1606 args.commands = ['run_script ' + args.batch, ',quit']
11fdf7f2 1607 if args.test:
9f95a23c 1608 args.commands.extend(['-t,'] + [arg + ',' for arg in args.test])
f67539c2
TL
1609 if not args.batch and len(args.commands) > 0:
1610 args.exe_and_quit = True
9f95a23c 1611
f67539c2
TL
1612 manage_sys_argv(args)
1613
1614 return args
1615
1616
1617def manage_sys_argv(args):
1618 exe = sys.argv[0]
11fdf7f2
TL
1619 sys.argv.clear()
1620 sys.argv.append(exe)
1621 sys.argv.extend([i.strip() for i in ' '.join(args.commands).split(',')])
9f95a23c 1622
20effc67 1623 setup_cephfs(args)
f67539c2
TL
1624
1625
1626def 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
1638def 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
1674if __name__ == '__main__':
1675 args = manage_args()
1676
81eedcae 1677 shell = CephFSShell()
f67539c2
TL
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.
9f95a23c 1684 shell.exit_code = 0
9f95a23c 1685
f67539c2
TL
1686 retval = execute_cmd_args(args)
1687 sys.exit(retval if retval else shell.exit_code)