From: Dan Mick Date: Fri, 7 Jun 2013 03:58:51 +0000 (-0700) Subject: ceph: add -v for version. Makefile processes ceph_ver.h X-Git-Tag: v0.65~131 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=11e1afd84ce43ebfa7d57ae4cf17dffb08cdafab;p=ceph.git ceph: add -v for version. Makefile processes ceph_ver.h Signed-off-by: Dan Mick --- diff --git a/src/Makefile.am b/src/Makefile.am index e851f9888bf1..c792f1042233 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1240,7 +1240,7 @@ EXTRA_DIST += \ $(srcdir)/upstart/radosgw.conf \ $(srcdir)/upstart/radosgw-all.conf \ $(srcdir)/upstart/radosgw-all-starter.conf \ - ceph \ + ceph.in \ ceph-disk \ ceph-disk-prepare \ ceph-disk-activate \ @@ -1305,6 +1305,21 @@ ceph_ver.c: ./ceph_ver.h common/version.cc: ./ceph_ver.h test/encoding/ceph_dencoder.cc: ./ceph_ver.h +# assemble Python script with global version variables +# NB: depends on format of ceph_ver.h + +ceph: ceph.in ./ceph_ver.h Makefile + rm -f $@ $@.tmp + echo "#!/usr/bin/python" >$@.tmp + grep "#define CEPH_GIT_NICE_VER" ./ceph_ver.h | \ + sed -e 's/#define \(.*VER\) /\1=/' >>$@.tmp + grep "#define CEPH_GIT_VER" ./ceph_ver.h | \ + sed -e 's/#define \(.*VER\) /\1=/' -e 's/=\(.*\)$$/="\1"/' >>$@.tmp + cat $@.in >>$@.tmp + chmod a+x $@.tmp + chmod a-w $@.tmp + mv $@.tmp $@ + # cleaning clean-local: -rm *.so *.gcno *.gcda diff --git a/src/ceph b/src/ceph deleted file mode 100755 index 678d83484860..000000000000 --- a/src/ceph +++ /dev/null @@ -1,1501 +0,0 @@ -#!/usr/bin/python - -import argparse -import copy -import errno -import json -import os -import rados -# for raw_input to do readline cmd editing -import readline -import socket -import stat -import string -import struct -import subprocess -import sys -import types -import uuid - -# just a couple of globals - -verbose = False -cluster_handle = None - -class ArgumentError(Exception): - """ - Something wrong with arguments - """ - pass - -class ArgumentNumber(ArgumentError): - """ - Wrong number of a repeated argument - """ - pass - -class ArgumentFormat(ArgumentError): - """ - Argument value has wrong format - """ - pass - -class ArgumentValid(ArgumentError): - """ - Argument value is otherwise invalid (doesn't match choices, for instance) - """ - pass - -class ArgumentPrefix(ArgumentError): - """ - Special for mismatched prefix; less severe, don't report by default - """ - pass - -class JsonFormat(Exception): - """ - some syntactic or semantic issue with the JSON - """ - pass - -class CephArgtype(object): - """ - Base class for all Ceph argument types - - Instantiating an object sets any validation parameters - (allowable strings, numeric ranges, etc.). The 'valid' - method validates a string against that initialized instance, - throwing ArgumentError if there's a problem. - """ - def __init__(self, **kwargs): - """ - set any per-instance validation parameters here - from kwargs (fixed string sets, integer ranges, etc) - """ - pass - - def valid(self, s, partial=False): - """ - Run validation against given string s (generally one word); - partial means to accept partial string matches (begins-with). - If cool, set self.val to the value that should be returned - (a copy of the input string, or a numeric or boolean interpretation - thereof, for example), and return True - if not, throw ArgumentError(msg-as-to-why) - """ - self.val = s - return True - - def __repr__(self): - """ - return string representation of description of type. Note, - this is not a representation of the actual value. Subclasses - probably also override __str__() to give a more user-friendly - 'name/type' description for use in command format help messages. - """ - a = '' - if hasattr(self, 'typeargs'): - a = self.typeargs - return '{0}(\'{1}\')'.format(self.__class__.__name__, a) - - def __str__(self): - """ - where __repr__ (ideally) returns a string that could be used to - reproduce the object, __str__ returns one you'd like to see in - print messages. Use __str__ to format the argtype descriptor - as it would be useful in a command usage message. - """ - return '<{0}>'.format(self.__class__.__name__) - -class CephInt(CephArgtype): - """ - range-limited integers, [+|-][0-9]+ or 0x[0-9a-f]+ - range: list of 1 or 2 ints, [min] or [min,max] - """ - def __init__(self, range=''): - if range == '': - self.range = list() - else: - self.range = list(range.split('|')) - self.range = map(long, self.range) - - def valid(self, s, partial=False): - try: - val = long(s) - except ValueError: - raise ArgumentValid("{0} doesn't represent an int".format(s)) - if len(self.range) == 2: - if val < self.range[0] or val > self.range[1]: - raise ArgumentValid("{0} not in range {1}".format(val, self.range)) - elif len(self.range) == 1: - if val < self.range[0]: - raise ArgumentValid("{0} not in range {1}".format(val, self.range)) - self.val = val - return True - - def __str__(self): - r = '' - if len(self.range) == 1: - r = '[{0}-]'.format(self.range[0]) - if len(self.range) == 2: - r = '[{0}-{1}]'.format(self.range[0], self.range[1]) - - return ''.format(r) - - -class CephFloat(CephArgtype): - """ - range-limited float type - range: list of 1 or 2 floats, [min] or [min, max] - """ - def __init__(self, range=''): - if range == '': - self.range = list() - else: - self.range = list(range.split('|')) - self.range = map(float, self.range) - - def valid(self, s, partial=False): - try: - val = float(s) - except ValueError: - raise ArgumentValid("{0} doesn't represent a float".format(s)) - if len(self.range) == 2: - if val < self.range[0] or val > self.range[1]: - raise ArgumentValid("{0} not in range {1}".format(val, self.range)) - elif len(self.range) == 1: - if val < self.range[0]: - raise ArgumentValid("{0} not in range {1}".format(val, self.range)) - self.val = val - return True - - def __str__(self): - r = '' - if len(self.range) == 1: - r = '[{0}-]'.format(self.range[0]) - if len(self.range) == 2: - r = '[{0}-{1}]'.format(self.range[0], self.range[1]) - return ''.format(r) - -class CephString(CephArgtype): - """ - String; pretty generic. - """ - def __init__(self, badchars=''): - self.badchars = badchars - - def valid(self, s, partial=False): - for c in self.badchars: - if c in s: - raise ArgumentFormat("bad char {0} in {1}".format(c, s)) - self.val = s - return True - - def __str__(self): - b = '' - if len(self.badchars): - b = '(without chars in {0})'.format(self.badchars) - return ''.format(b) - -class CephSocketpath(CephArgtype): - """ - Admin socket path; check that it's readable and S_ISSOCK - """ - def valid(self, s, partial=False): - mode = os.stat(s).st_mode - if not stat.S_ISSOCK(mode): - raise ArgumentValid('socket path {0} is not a socket'.format(s)) - self.val = s - return True - def __str__(self): - return '' - -class CephIPAddr(CephArgtype): - """ - IP address (v4 or v6) with optional port - """ - def valid(self, s, partial=False): - # parse off port, use socket to validate addr - type = 6 - if s.startswith('['): - type = 6 - elif s.find('.') != -1: - type = 4 - if type == 4: - port = s.find(':') - if (port != -1): - a = s[:port] - p = s[port+1:] - if int(p) > 65535: - raise ArgumentValid('{0}: invalid IPv4 port'.format(p)) - else: - a = s - p = None - try: - socket.inet_pton(socket.AF_INET, a) - except: - raise ArgumentValid('{0}: invalid IPv4 address'.format(a)) - else: - # v6 - if s.startswith('['): - end = s.find(']') - if end == -1: - raise ArgumentFormat('{0} missing terminating ]'.format(s)) - if s[end+1] == ':': - try: - p = int(s[end+2]) - except: - raise ArgumentValid('{0}: bad port number'.format(s)) - a = s[1:end] - else: - a = s - p = None - try: - socket.inet_pton(socket.AF_INET6, a) - except: - raise ArgumentValid('{0} not valid IPv6 address'.format(s)) - if p is not None and long(p) > 65535: - raise ArgumentValid("{0} not a valid port number".format(p)) - self.val = s - return True - - def __str__(self): - return '' - -class CephEntityAddr(CephIPAddr): - """ - EntityAddress, that is, IP address/nonce - """ - def valid(self, s, partial=False): - ip, nonce = s.split('/') - if not super(self.__class__, self).valid(ip): - raise ArgumentValid('CephEntityAddr {0}: ip address invalid'.format(s)) - self.val = s - return True - - def __str__(self): - return '' - -class CephPoolname(CephArgtype): - """ - Pool name; very little utility - """ - def __str__(self): - return '' - -class CephObjectname(CephArgtype): - """ - Object name. Maybe should be combined with Pool name as they're always - present in pairs, and then could be checked for presence - """ - def valid(self, s, partial=False): - self.val = s - return True - - def __str__(self): - return '' - -class CephPgid(CephArgtype): - """ - pgid, in form N.xxx (N = pool number, xxx = hex pgnum) - """ - def valid(self, s, partial=False): - if s.find('.') == -1: - raise ArgumentFormat('pgid has no .') - poolid, pgnum = s.split('.') - if poolid < 0: - raise ArgumentFormat('pool {0} < 0'.format(poolid)) - try: - pgnum = int(pgnum, 16) - except: - raise ArgumentFormat('pgnum {0} not hex integer'.format(pgnum)) - self.val = s - return True - - def __str__(self): - return '' - -class CephName(CephArgtype): - """ - Name, or type.id, where type is osd|mon|client|mds, and id is a base10 int, - or just id. - - Also accept '*' - """ - def valid(self, s, partial=False): - if s == '*': - self.val = s - self.nametype = None - self.nameid = None - return True - if s.find('.') == -1: - i = s - else: - t, i = s.split('.') - if not t in ('osd', 'mon', 'client', 'mds'): - raise ArgumentValid('unknown type ' + self.t) - if t == 'osd': - try: - i = int(i) - except: - raise ArgumentFormat('osd id ' + i + ' not integer') - self.nametype = t - self.val = s - self.nameid = i - return True - - def __str__(self): - return '' - - -class CephChoices(CephArgtype): - """ - Set of string literals; init with valid choices - """ - def __init__(self, strings='', **kwargs): - self.strings=strings.split('|') - - def valid(self, s, partial=False): - if not partial: - if not s in self.strings: - # show as __str__ does: {s1|s2..} - raise ArgumentValid("{0} not in {1}".format(s, self)) - self.val = s - return True - - # partial - for t in self.strings: - if t.startswith(s): - self.val = s - return True - raise ArgumentValid("{0} not in {1}". format(s, self)) - - def __str__(self): - if len(self.strings) == 1: - return '{0}'.format(self.strings[0]) - else: - return '{0}'.format('|'.join(self.strings)) - -class CephFilepath(CephArgtype): - """ - Openable file - """ - def valid(self, s, partial=False): - try: - f = open(s, 'a+') - except Exception as e: - raise ArgumentValid('can\'t open {0}: {1}'.format(s, e)) - f.close() - self.val = s - return True - - def __str__(self): - return '' - -class CephFragment(CephArgtype): - """ - 'Fragment' ??? XXX - """ - def valid(self, s, partial=False): - if s.find('/') == -1: - raise ArgumentFormat('{0}: no /'.format(s)) - val, bits = s.split('/') - # XXX is this right? - if not val.startswith('0x'): - raise ArgumentFormat("{0} not a hex integer".format(val)) - try: - long(val) - except: - raise ArgumentFormat('can\'t convert {0} to integer'.format(val)) - try: - long(bits) - except: - raise ArgumentFormat('can\'t convert {0} to integer'.format(bits)) - self.val = s - return True - - def __str__(self): - return "" - - -class CephUUID(CephArgtype): - """ - CephUUID: pretty self-explanatory - """ - def valid(self, s, partial=False): - try: - uuid.UUID(s) - except Exception as e: - raise ArgumentFormat('invalid UUID {0}: {1}'.format(s, e)) - self.val = s - return True - - def __str__(self): - return '' - - -class CephPrefix(CephArgtype): - """ - CephPrefix: magic type for "all the first n fixed strings" - """ - def __init__(self, prefix=''): - self.prefix = prefix - - def valid(self, s, partial=False): - if partial: - if self.prefix.startswith(s): - self.val = s - return True - else: - if (s == self.prefix): - self.val = s - return True - raise ArgumentPrefix("no match for {0}".format(s)) - - def __str__(self): - return self.prefix - -############################################################################ - - -class argdesc(object): - """ - argdesc(typename, name='name', n=numallowed|N, - req=False, helptext=helptext, **kwargs (type-specific)) - - validation rules: - typename: type(**kwargs) will be constructed - later, type.valid(w) will be called with a word in that position - - name is used for parse errors and for constructing JSON output - n is a numeric literal or 'n|N', meaning "at least one, but maybe more" - req=False means the argument need not be present in the list - helptext is the associated help for the command - anything else are arguments to pass to the type constructor. - - self.instance is an instance of type t constructed with typeargs. - - valid() will later be called with input to validate against it, - and will store the validated value in self.instance.val for extraction. - """ - def __init__(self, t, name=None, n=1, req=True, **kwargs): - if isinstance(t, types.StringTypes): - self.t = CephPrefix - self.typeargs = {'prefix':t} - self.req = True - else: - self.t = t - self.typeargs = kwargs - self.req = bool(req == True or req == 'True') - - self.name = name - self.N = (n in ['n', 'N']) - if self.N: - self.n = 1 - else: - self.n = int(n) - self.instance = self.t(**self.typeargs) - - def __repr__(self): - r = 'argdesc(' + str(self.t) + ', ' - internals = ['N', 'typeargs', 'instance', 't'] - for (k,v) in self.__dict__.iteritems(): - if k.startswith('__') or k in internals: - pass - else: - # undo mods above - if k == 'n' and self.N: - v = 'N' - r += '{0}={1}, '.format(k,v) - for (k,v) in self.typeargs.iteritems(): - r += '{0}={1}, '.format(k,v) - return r[:-2] + ')' - - def __str__(self): - if ((self.t == CephChoices and len(self.instance.strings) == 1) - or (self.t == CephPrefix)): - s = '{0}'.format(str(self.instance)) - else: - s = '{0}({1})'.format(self.name, str(self.instance)) - if self.N: - s += ' [' + str(self.instance) + '...]' - if not self.req: - s = '{' + s + '}' - return s - - def helpstr(self): - """ - like str(), but omit parameter names (except for CephString, - which really needs them) - """ - if self.t == CephString: - chunk = '<{0}>'.format(self.name) - else: - chunk = str(self.instance) - s = '{0}'.format(chunk) - if self.N: - s += ' [' + chunk + '...]' - if not self.req: - s = '{' + s + '}' - return s - -def concise_sig(sig): - """ - Return string representation of sig useful for syntax reference in help - """ - first = True - s = '' - for d in sig: - if first: - first = False - else: - s += ' ' - s += d.helpstr() - return s - -def parse_funcsig(sig): - """ - parse a single descriptor (array of strings or dicts) into a - dict of function descriptor/validators (objects of CephXXX type) - """ - newsig = [] - argnum = 0 - for desc in sig: - argnum += 1 - if isinstance(desc, types.StringTypes): - t = CephPrefix - desc = {'type':t, 'name':'prefix', 'prefix':desc} - else: - # not a simple string, must be dict - if not 'type' in desc: - s = 'JSON descriptor {0} has no type'.format(sig) - raise JsonFormat(s) - # look up type string in our globals() dict; if it's an - # object of type types.TypeType, it must be a - # locally-defined class. otherwise, we haven't a clue. - if desc['type'] in globals(): - t = globals()[desc['type']] - if type(t) != types.TypeType: - s = 'unknown type {0}'.format(desc['type']) - raise JsonFormat(s) - else: - s = 'unknown type {0}'.format(desc['type']) - raise JsonFormat(s) - - kwargs = dict() - for key, val in desc.items(): - if key not in ['type', 'name', 'n', 'req']: - kwargs[key] = val - newsig.append(argdesc(t, - name=desc.get('name', None), - n=desc.get('n', 1), - req=desc.get('req', True), - **kwargs)) - return newsig - - -def parse_json_funcsigs(s): - """ - parse_json_funcsigs(s) - - A function signature is mostly an array of argdesc; it's represented - in JSON as - { - "cmd001": {"sig":[ "type": type, "name": name, "n": num, "req":true|false ], "help":helptext} - . - . - . - ] - - A set of sigs is in an dict mapped by a unique number: - { - "cmd1": { - "sig": ["type.. ], "help":{"text":helptext} - } - "cmd2"{ - "sig": [.. ], "help":{"text":helptext} - } - } - - Parse the string s and return an dict of dicts, keyed by opcode; - each dict contains 'sig' with the array of descriptors, and 'help' - with the helptext. - """ - try: - overall = json.loads(s) - except Exception as e: - print >> sys.stderr, "Couldn't parse JSON {0}: {1}".format(s, e) - raise e - sigdict = {} - for cmdtag, cmd in overall.iteritems(): - helptext = cmd.get('help', 'no help available') - try: - sig = cmd['sig'] - except KeyError: - s = "JSON descriptor {0} has no 'sig'".format(cmdtag) - raise JsonFormat(s) - newsig = parse_funcsig(sig) - sigdict[cmdtag] = {'sig':newsig, 'helptext':helptext} - return sigdict - -def validate_one(word, desc, partial=False): - """ - validate_one(word, desc, partial=False) - - validate word against the constructed instance of the type - in desc. May raise exception. If it returns false (and doesn't - raise an exception), desc.instance.val will - contain the validated value (in the appropriate type). - """ - if desc.instance.valid(word, partial): - desc.numseen += 1 - if desc.N: - desc.n = desc.numseen + 1 - return True - return False - -def matchnum(args, signature, partial=False): - """ - matchnum(s, signature, partial=False) - - Returns number of arguments matched in s against signature. - Can be used to determine most-likely command for full or partial - matches (partial applies to string matches). - """ - words = args[:] - mysig = copy.deepcopy(signature) - matchcnt = 0 - for desc in mysig: - setattr(desc, 'numseen', 0) - while desc.numseen < desc.n: - # if there are no more arguments, return - if not words: - return matchcnt; - word = words.pop(0) - try: - validate_one(word, desc, partial) - except: - if not desc.req: - # this wasn't required, so word may match the next desc - words.insert(0, word) - break - else: - # it was required, and didn't match, return - return matchcnt - if desc.req: - matchcnt += 1 - return matchcnt - -def validate(args, signature, partial=False): - """ - validate(s, signature, partial=False) - - Assumes s represents a possible command input following format of - signature. Runs a validation; no exception means it's OK. Return - a dict containing all arguments named by their descriptor name - (with duplicate args per name accumulated into a space-separated - value). - - If partial is set, allow partial matching (with partial dict returned) - """ - words = args[:] - mysig = copy.deepcopy(signature) - d = dict() - for desc in mysig: - setattr(desc, 'numseen', 0) - while desc.numseen < desc.n: - if words: - word = words.pop(0) - else: - if desc.req: - if desc.N and desc.numseen < 1: - # wanted N, didn't even get 1 - if partial: - return d - raise ArgumentNumber('saw {0} of {1}, expected at least 1'.format(desc.numseen, desc)) - elif not desc.N and desc.numseen < desc.n: - # wanted n, got too few - if partial: - return d - raise ArgumentNumber('saw {0} of {1}, expected {2}'.format(desc.numseen, desc, desc.n)) - break - try: - validate_one(word, desc) - except Exception as e: - # not valid; if not required, just push back for the next one - if not desc.req: - words.insert(0, word) - break - else: - # hm, but it was required, so quit - if partial: - return d - raise e - - if desc.N: - # value should be a list - if desc.name in d: - d[desc.name] += [desc.instance.val] - else: - d[desc.name] = [desc.instance.val] - elif (desc.t == CephPrefix) and (desc.name in d): - # value should be a space-joined concatenation - d[desc.name] += ' ' + desc.instance.val - else: - # if first CephPrefix or any other type, just set it - d[desc.name] = desc.instance.val - return d - -def osdids(): - ret, outbuf, outs = json_command(prefix='osd ls') - if ret: - raise RuntimeError('Can\'t contact mon for osd list') - return [i for i in outbuf.split('\n')] - -def monids(): - ret, outbuf, outs = json_command(prefix='mon dump', - argdict={'format':'json'}) - if ret: - raise RuntimeError('Can\'t contact mon for mon list') - d = json.loads(outbuf) - return [m['name'] for m in d['mons']] - -def mdsids(): - ret, outbuf, outs = json_command(prefix='mds dump', - argdict={'format':'json'}) - if ret: - raise RuntimeError('Can\'t contact mon for mds list') - d = json.loads(outbuf) - l = [] - infodict = d['info'] - for mdsdict in infodict.values(): - l.append(mdsdict['name']) - return l - -def parse_cmdargs(args=None, target=''): - # alias: let the line-wrapping be sane - AP = argparse.ArgumentParser - - # format our own help - parser = AP(description='Frontend for ceph CLI', add_help=False) - - parser.add_argument('--completion', action='store_true') - - parser.add_argument('-h', '--help', help='request mon help', - action='store_true') - parser.add_argument('--help-all', help='request help for all daemons', - action='store_true') - - parser.add_argument('-c', '--conf', dest='cephconf', - help='ceph configuration file') - parser.add_argument('-i', '--in-file', dest='input_file', - help='input file') - parser.add_argument('-o', '--out-file', dest='output_file', - help='output file') - parser.add_argument('-k', '--keyring', dest='keyring_file', - help='keyring file') - - parser.add_argument('--id', '--user', dest='client_id', - help='client id for authentication') - parser.add_argument('--name', '-n', dest='client_name', - help='client name for authentication') - parser.add_argument('--cluster', help='cluster name') - - parser.add_argument('--admin-daemon', dest='admin_socket', - help='submit admin-socket commands (\"help\" for help') - - parser.add_argument('-s', '--status', action='store_true', - help='show cluster status') - - parser.add_argument('-w', '--watch', action='store_true', - help='watch live cluster changes') - parser.add_argument('--watch-debug', action='store_true', - help='watch debug events') - parser.add_argument('--watch-info', action='store_true', - help='watch info events') - parser.add_argument('--watch-sec', action='store_true', - help='watch security events') - parser.add_argument('--watch-warn', action='store_true', - help='watch warn events') - parser.add_argument('--watch-error', action='store_true', - help='watch error events') - - parser.add_argument('--verbose', action="store_true") - parser.add_argument('--concise', dest='verbose', action="store_false") - - parser.add_argument('-f', '--format', choices=['json', 'json-pretty', - 'xml', 'xml-pretty', 'plain'], dest='output_format') - # for pg dump_stuck - parser.add_argument('--threshold', type=int, help='number of seconds for a pg to be considered stuck for pg dump_stuck') - - # returns a Namespace with the parsed args, and a list of all extras - parsed_args, extras = parser.parse_known_args(args) - - return parser, parsed_args, extras - -def do_help(parser, help_all = False): - """ - Print basic parser help - If the cluster is available: - get and print monitor help; - if help_all, print help for daemon commands as well - """ - - def help_for_target(target): - ret, outbuf, outs = json_command(target=target, - prefix='get_command_descriptions', - timeout=10) - if ret: - print >> sys.stderr, \ - "couldn't get command descriptions for {0}: {1}".\ - format(target, outs) - else: - sys.stdout.write(format_help(parse_json_funcsigs(outbuf))) - - parser.print_help() - print '\n' - if (cluster_handle): - help_for_target(target=('mon', '')) - - if help_all and cluster_handle: - # try/except in case there are no daemons of that type - try: - firstosd = osdids()[0] - print '\nOSD.{0} tell commands and pg pgid commands:\n\n'.\ - format(firstosd) - help_for_target(target=('osd', osdids()[0])) - - print '\nOSD daemon commands:\n\n' - sys.stdout.write(format_help(parse_json_funcsigs(admin_socket(ceph_conf('admin_socket', 'osd.' + firstosd), ['get_command_descriptions'])))) - except: - pass - - try: - firstmon = monids()[0] - print '\nmon.{0} daemon commands:\n\n'.format(firstmon) - sys.stdout.write(format_help(parse_json_funcsigs(admin_socket(ceph_conf('admin_socket', 'mon.' + firstmon), ['get_command_descriptions'])))) - except: - pass - - try: - firstmds = mdsids()[0] - print '\nmds.{0} daemon commands:\n\n'.format(firstmds) - sys.stdout.write(format_help(parse_json_funcsigs(admin_socket(ceph_conf('admin_socket', 'mds.' + firstmds), ['get_command_descriptions'])))) - except: - pass - - return 0 - - -def descsort(sh1, sh2): - """ - sort descriptors by prefixes, defined as the concatenation of all simple - strings in the descriptor; this works out to just the leading strings. - """ - return cmp(concise_sig(sh1['sig']), concise_sig(sh2['sig'])) - - -dontsplit = string.letters + '{[<>]}' - -def wrap(s, width, indent): - """ - generator to transform s into a sequence of strings width or shorter, - for wrapping text to a specific column width. - Attempt to break on anything but dontsplit characters. - indent is amount to indent 2nd-through-nth lines. - - so "long string long string long string" width=11 indent=1 becomes - 'long string', ' long string', ' long string' so that it can be printed - as - long string - long string - long string - - Consumes s. - """ - result = '' - leader = '' - while len(s): - - if (len(s) <= width): - # no splitting; just possibly indent - result = leader + s - s = '' - yield result - - else: - splitpos = width - while (splitpos > 0) and (s[splitpos-1] in dontsplit): - splitpos -= 1 - - if splitpos == 0: - splitpos = width - - if result: - # prior result means we're mid-iteration, indent - result = leader - else: - # first time, set leader and width for next - leader = ' ' * indent - width -= 1 # for subsequent space additions - - # remove any leading spaces in this chunk of s - result += s[:splitpos].lstrip() - s = s[splitpos:] - - yield result - - raise StopIteration - -def format_help(cmddict): - """ - Formats all the cmdsigs and helptexts from cmddict into a sorted-by- - cmdsig 2-column display, with each column wrapped and indented to - fit into 40 characters. - """ - - fullusage = '' - for cmd in sorted(cmddict.itervalues(), cmp=descsort): - - if not cmd['helptext']: - continue - siglines = [l for l in wrap(concise_sig(cmd['sig']), 40, 1)] - helplines = [l for l in wrap(cmd['helptext'], 39, 1)] - - # make lists the same length - maxlen = max(len(siglines), len(helplines)) - siglines.extend([''] * (maxlen - len(siglines))) - helplines.extend([''] * (maxlen - len(helplines))) - - # so we can zip them for output - for (s, h) in zip(siglines, helplines): - fullusage += '{0:40s} {1}\n'.format(s, h) - - return fullusage - -def validate_command(parsed_args, sigdict, args): - """ - turn args into a valid dictionary ready to be sent off as JSON, - validated against sigdict. - parsed_args is the namespace back from argparse - """ - found = [] - valid_dict = {} - if args: - # Repulsive hack to handle tell: lop off 'tell' and target - # and validate the rest of the command. 'target' is already - # determined in our callers, so it's ok to remove it here. - if args[0] == 'tell': - args = args[2:] - # look for best match, accumulate possibles in bestcmds - # (so we can maybe give a more-useful error message) - best_match_cnt = 0 - bestcmds = [] - for cmdtag, cmd in sigdict.iteritems(): - sig = cmd['sig'] - matched = matchnum(args, sig, partial=True) - if (matched > best_match_cnt): - if verbose: - print >> sys.stderr, \ - "better match: {0} > {1}: {2}:{3} ".format(matched, - best_match_cnt, cmdtag, concise_sig(sig)) - best_match_cnt = matched - bestcmds = [{cmdtag:cmd}] - elif matched == best_match_cnt: - if verbose: - print >> sys.stderr, \ - "equal match: {0} > {1}: {2}:{3} ".format(matched, - best_match_cnt, cmdtag, concise_sig(sig)) - bestcmds.append({cmdtag:cmd}) - - if verbose: - print >> sys.stderr, "bestcmds: ", bestcmds - - # for everything in bestcmds, look for a true match - for cmdsig in bestcmds: - for cmd in cmdsig.itervalues(): - sig = cmd['sig'] - helptext = cmd['helptext'] - try: - valid_dict = validate(args, sig) - found = sig - break - except ArgumentPrefix: - # this means a CephPrefix type didn't match; since - # this is common, just eat it - pass - except ArgumentError as e: - # prefixes matched, but some other arg didn't; - # this is interesting information - print >> sys.stderr, '{0}: invalid command'.\ - format(' '.join(args)) - print >> sys.stderr, '{0}'.format(e) - print >> sys.stderr, "did you mean {0}?\n\t{1}".\ - format(concise_sig(sig), helptext) - pass - - if not found: - print >> sys.stderr, 'no valid command found' - print >> sys.stderr, 'close matches:' - for cmdsig in bestcmds: - for (cmdtag, cmd) in cmdsig.iteritems(): - print >> sys.stderr, concise_sig(cmd['sig']) - return None - - if parsed_args.output_format: - valid_dict['format'] = parsed_args.output_format - - if parsed_args.threshold: - valid_dict['threshold'] = parsed_args.threshold - - return valid_dict - -def json_command(target=('mon', ''), prefix=None, argdict=None, inbuf='', - timeout=0): - """ - Send a new_style command to a daemon using librados's - mon_command, osd_command, or pg_command. Prefix may be supplied - separately or in argdict. Any bulk input data comes in inbuf. - Returns (ret, outbuf, outs); ret is the return code, outbuf is - the outbl "bulk useful output" buffer, and outs is any status - or error message (intended for stderr). - - If target is osd.N, send command to that osd (except for pgid cmds) - """ - cmddict = {} - if prefix: - cmddict.update({'prefix':prefix}) - if argdict: - cmddict.update(argdict) - - # grab prefix for error messages - prefix = cmddict['prefix'] - - try: - if target[0] == 'osd': - osdtarg = CephName() - if 'target' in cmddict: - osdtarget = cmddict.pop('target') - else: - osdtarget = '{0}.{1}'.format(*target) - - try: - osdtarg.valid(osdtarget) - osdid = osdtarg.nameid - except: - # uh..I dunno, try osd.0? - osdid = 0 - - if verbose: - print >> sys.stderr, 'submit {0} to osd.{1}'.\ - format(json.dumps(cmddict), osdid) - ret, outbuf, outs = \ - cluster_handle.osd_command(osdid, json.dumps(cmddict), inbuf, - timeout) - - elif target[0] == 'pg': - # leave it in cmddict for the OSD to use too - pgid = target[1] - if verbose: - print >> sys.stderr, 'submit {0} for pgid {1}'.\ - format(json.dumps(cmddict), pgid) - ret, outbuf, outs = \ - cluster_handle.pg_command(pgid, json.dumps(cmddict), inbuf, - timeout) - - elif target[0] == 'mon': - if verbose: - print >> sys.stderr, '{0} to {1}'.\ - format(json.dumps(cmddict), target[0]) - ret, outbuf, outs = cluster_handle.mon_command(json.dumps(cmddict), - inbuf, timeout) - - except Exception as e: - raise RuntimeError('"{0}": exception {1}'.format(prefix, e)) - - return ret, outbuf, outs - -def admin_socket(asok_path, cmd): - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - try: - sock.connect(asok_path) - sock.sendall(' '.join(cmd) + '\0') - - len_str = sock.recv(4) - l, = struct.unpack(">I", len_str) - ret = '' - - got = 0 - while got < l: - bit = sock.recv(l - got) - ret += bit - got += len(bit) - - except Exception as e: - raise RuntimeError('exception: {0}'.format(e)) - - return ret - - -def ceph_conf(field, name): - p = subprocess.Popen( - args=[ - 'ceph-conf', - field, - '-n', - name, - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - outdata, errdata = p.communicate() - if (len(errdata)): - raise RuntimeError('unable to get conf option %s for %s: %s' % (field, name, errdata)) - return outdata.rstrip() - -def new_style_command(parsed_args, cmdargs, target, sigdict, inbuf, verbose): - """ - Do new-style command dance. - target: daemon to receive command: mon (any) or osd.N - sigdict - the parsed output from the new monitor describing commands - inbuf - any -i input file data - verbose - bool - """ - if verbose: - for cmdtag in sorted(sigdict.keys()): - cmd = sigdict[cmdtag] - sig = cmd['sig'] - print '{0}: {1}'.format(cmdtag, concise_sig(sig)) - - got_command = False - - if not got_command: - if cmdargs: - # Validate input args against list of sigs - valid_dict = validate_command(parsed_args, sigdict, cmdargs) - if valid_dict: - got_command = True - else: - return -errno.EINVAL, '', 'invalid command' - else: - # do the command-interpreter looping - while True: - interactive_input = raw_input('ceph> ') - if interactive_input in ['q', 'quit', 'Q']: - return 0, '', '' - cmdargs = parse_cmdargs(interactive_input.split())[2] - target = find_cmd_target(cmdargs) - valid_dict = validate_command(parsed_args, sigdict, cmdargs) - if valid_dict: - if verbose: - print >> sys.stderr, "Submitting command ", valid_dict - ret, outbuf, outs = json_command(target=target, - argdict=valid_dict) - if ret: - sys.stderr.write('Error {0}: {1}'.format(ret, outs)) - return ret, '', outs - else: - print "invalid command" - - if verbose: - print >> sys.stderr, "Submitting command ", valid_dict - return json_command(target=target, argdict=valid_dict, inbuf=inbuf) - -OSD_TELL_MATCH = { \ - 'sig': ['tell', {'name':'target','type':'CephName'}], \ - 'matchnum': 2, \ - 'return_key': 'target', \ -} -PGID_MATCH = { \ - 'sig': ['pg', {'name':'pgid','type':'CephPgid'}], \ - 'matchnum': 2, \ - 'return_key': 'pgid', \ -} - -def find_cmd_target(childargs): - """ - Using a minimal validation, figure out whether the command - should be sent to a monitor or an osd. We do this before even - asking for the 'real' set of command signatures, so we can ask the - right daemon. - Returns ('osd', osdid), ('pg', pgid), or ('mon', '') - """ - sig = parse_funcsig(['tell', {'name':'target','type':'CephName'}]) - valid_dict = validate(childargs, sig, partial=True); - if len(valid_dict) == 2: - name = CephName() - name.valid(valid_dict['target']) - return 'osd', name.nameid - - sig = parse_funcsig(['pg', {'name':'pgid','type':'CephPgid'}]) - valid_dict = validate(childargs, sig, partial=True); - if len(valid_dict) == 2: - return 'pg', valid_dict['pgid'] - - return 'mon', '' - -def complete(sigdict, args, target): - """ - Command completion. Match as much of [args] as possible, - and print every possible match separated by newlines. - Return exitcode. - """ - # XXX this looks a lot like the front of validate_command(). Refactor? - - complete_verbose = 'COMPVERBOSE' in os.environ - - # Repulsive hack to handle tell: lop off 'tell' and target - # and validate the rest of the command. 'target' is already - # determined in our callers, so it's ok to remove it here. - if len(args) and args[0] == 'tell': - args = args[2:] - # look for best match, accumulate possibles in bestcmds - # (so we can maybe give a more-useful error message) - best_match_cnt = 0 - bestcmds = [] - for cmdtag, cmd in sigdict.iteritems(): - sig = cmd['sig'] - matched = matchnum(args, sig, partial=True) - if (matched > best_match_cnt): - if complete_verbose: - print >> sys.stderr, \ - "better match: {0} > {1}: {2}:{3} ".format(matched, - best_match_cnt, cmdtag, concise_sig(sig)) - best_match_cnt = matched - bestcmds = [{cmdtag:cmd}] - elif matched == best_match_cnt: - if complete_verbose: - print >> sys.stderr, \ - "equal match: {0} > {1}: {2}:{3} ".format(matched, - best_match_cnt, cmdtag, concise_sig(sig)) - bestcmds.append({cmdtag:cmd}) - - # look through all matching sigs - comps = [] - for cmddict in bestcmds: - for cmd in cmddict.itervalues(): - sig = cmd['sig'] - # either: - # we match everything fully, so we want the next desc, or - # we match more partially, so we want the partial match - fullindex = matchnum(args, sig, partial=False) - 1 - partindex = matchnum(args, sig, partial=True) - 1 - if complete_verbose: - print >> sys.stderr, '{}: f {} p {} len {}'.format(sig, fullindex, partindex, len(sig)) - if fullindex == partindex and fullindex + 1 < len(sig): - d = sig[fullindex + 1] - else: - d = sig[partindex] - comps.append(str(d)) - if complete_verbose: - print >> sys.stderr, '\n'.join(comps) - print '\n'.join(comps) - - return 0 - -### -# main -### - -def main(): - - parser, parsed_args, childargs = parse_cmdargs() - - global verbose - verbose = parsed_args.verbose - - # pass on --id, --name, --conf - name = 'client.admin' - if parsed_args.client_id: - name = 'client.' + parsed_args.client_id - if parsed_args.client_name: - name = parsed_args.client_name - - # default '' means default conf search - conffile = '' - if parsed_args.cephconf: - conffile = parsed_args.cephconf - # For now, --admin-daemon is handled as usual. Try it - # first in case we can't connect() to the cluster - if parsed_args.admin_socket: - try: - print admin_socket(parsed_args.admin_socket, childargs) - except Exception as e: - print >> sys.stderr, 'admin_socket: {0}'.format(e) - return 0 - - if len(childargs) > 0 and childargs[0] == "daemon": - if len(childargs) > 2: - if childargs[1].find('/') >= 0: - try: - print admin_socket(childargs[1], childargs[2:]) - except Exception as e: - print >> sys.stderr, 'admin_socket: {0}'.format(e) - return 0 - else: - # try resolve daemon name - path = ceph_conf('admin_socket', childargs[1]) - try: - print admin_socket(path, childargs[2:]) - except Exception as e: - print >> sys.stderr, 'admin_socket: {0}'.format(e) - return 0 - else: - print >> sys.stderr, 'Daemon requires at least 2 arguments' - return 1 - - # handle any 'generic' ceph arguments that we didn't parse here - global cluster_handle - - # rados.Rados() will call rados_create2, and then read the conf file, - # and then set the keys from the dict. So we must do these - # "pre-file defaults" first (see common_preinit in librados) - conf_defaults = { - 'log_to_stderr':'true', - 'err_to_stderr':'true', - 'log_flush_on_exit':'true', - } - - clustername = 'ceph' - if parsed_args.cluster: - clustername = parsed_args.cluster - - cluster_handle = rados.Rados(name=name, clustername=clustername, - conf_defaults=conf_defaults, conffile='') - - retargs = cluster_handle.conf_parse_argv(childargs) - #tmp = childargs - childargs = retargs - if not childargs: - childargs = [] - - # -- means "stop parsing args", but we don't want to see it either - if '--' in childargs: - childargs.remove('--') - - try: - cluster_handle.connect() - except KeyboardInterrupt: - print >> sys.stderr, 'Cluster connection aborted' - return 1 - except Exception as e: - print >> sys.stderr, 'Error connecting to cluster: {0}'.\ - format(e.__class__.__name__) - return 1 - - if parsed_args.help or parsed_args.help_all: - return do_help(parser, parsed_args.help_all) - - # implement -w/--watch_* - # This is ugly, but Namespace() isn't quite rich enough. - level = '' - for k,v in parsed_args._get_kwargs(): - if k.startswith('watch') and v: - if k == 'watch': - level = 'info' - else: - level = k.replace('watch_', '') - if level: - - # an awfully simple callback - def watch_cb(arg, line, who, stamp_sec, stamp_nsec, seq, level, msg): - print line - - # first do a ceph status - ret, outbuf, outs = json_command(prefix='status') - if ret: - print >> sys.stderr, "status query failed: ", outs - return ret - print outbuf - - # this instance keeps the watch connection alive, but is - # otherwise unused - logwatch = rados.MonitorLog(cluster_handle, level, watch_cb, 0) - # loop forever letting watch_cb print lines - while True: - try: - pass - except KeyboardInterrupt: - # or until ^C, at least - return 0 - - # read input file, if any - inbuf = '' - if parsed_args.input_file: - try: - with open(parsed_args.input_file, 'r') as f: - inbuf = f.read() - except Exception as e: - print >> sys.stderr, 'Can\'t open input file {0}: {1}'.format(parsed_args.input_file, e) - return 1 - - # prepare output file, if any - if parsed_args.output_file: - try: - outf = open(parsed_args.output_file, 'w') - except: - print >> sys.stderr, \ - 'Can\'t open output file {0}: {1}'.\ - format(parsed_args.output_file, e) - return 1 - - # -s behaves like a command (ceph status). - if parsed_args.status: - childargs.insert(0, 'status') - - target = find_cmd_target(childargs) - - # fetch JSON sigs from command - # each line contains one command signature (a placeholder name - # of the form 'cmdNNN' followed by an array of argument descriptors) - # as part of the validated argument JSON object - - ret, outbuf, outs = json_command(target=target, - prefix='get_command_descriptions') - if ret == -errno.EINVAL: - # send command to old monitor - if verbose: - print '{0} to old monitor'.format(' '.join(childargs)) - ret, outbuf, outs = cluster_handle.mon_command(childargs, inbuf) - elif ret: - if ret < 0: - ret = -ret - print >> sys.stderr, \ - 'Problem getting command descriptions from {0}, {1}'.\ - format(target, errno.errorcode[ret]) - return ret - else: - sigdict = parse_json_funcsigs(outbuf) - - if parsed_args.completion: - return complete(sigdict, childargs, target) - - ret, outbuf, outs = new_style_command(parsed_args, childargs, target, - sigdict, inbuf, verbose) - - if ret < 0: - ret = -ret - print >> sys.stderr, 'Error {0}: {1}'.format(errno.errorcode[ret], outs) - return ret - - # this assumes outs never has useful command output, only status - if outs: - print >> sys.stderr, outs - - if (parsed_args.output_file): - outf.write(outbuf) - outf.close() - else: - # hack: old code printed status line before many json outputs - # (osd dump, etc.) that consumers know to ignore. Add blank line - # to satisfy consumers that skip the first line, but not annoy - # consumers that don't. - if parsed_args.output_format and \ - parsed_args.output_format.startswith('json'): - sys.stdout.write('\n'); - - sys.stdout.write(outbuf) - - return 0 - -if __name__ == '__main__': - sys.exit(main()) diff --git a/src/ceph.in b/src/ceph.in new file mode 100755 index 000000000000..0b345a593021 --- /dev/null +++ b/src/ceph.in @@ -0,0 +1,1508 @@ +# +# Processed in Makefile to add python #! line and version variable +# + +import argparse +import copy +import errno +import json +import os +import rados +# for raw_input to do readline cmd editing +import readline +import socket +import stat +import string +import struct +import subprocess +import sys +import types +import uuid + +# just a couple of globals + +verbose = False +cluster_handle = None + +class ArgumentError(Exception): + """ + Something wrong with arguments + """ + pass + +class ArgumentNumber(ArgumentError): + """ + Wrong number of a repeated argument + """ + pass + +class ArgumentFormat(ArgumentError): + """ + Argument value has wrong format + """ + pass + +class ArgumentValid(ArgumentError): + """ + Argument value is otherwise invalid (doesn't match choices, for instance) + """ + pass + +class ArgumentPrefix(ArgumentError): + """ + Special for mismatched prefix; less severe, don't report by default + """ + pass + +class JsonFormat(Exception): + """ + some syntactic or semantic issue with the JSON + """ + pass + +class CephArgtype(object): + """ + Base class for all Ceph argument types + + Instantiating an object sets any validation parameters + (allowable strings, numeric ranges, etc.). The 'valid' + method validates a string against that initialized instance, + throwing ArgumentError if there's a problem. + """ + def __init__(self, **kwargs): + """ + set any per-instance validation parameters here + from kwargs (fixed string sets, integer ranges, etc) + """ + pass + + def valid(self, s, partial=False): + """ + Run validation against given string s (generally one word); + partial means to accept partial string matches (begins-with). + If cool, set self.val to the value that should be returned + (a copy of the input string, or a numeric or boolean interpretation + thereof, for example), and return True + if not, throw ArgumentError(msg-as-to-why) + """ + self.val = s + return True + + def __repr__(self): + """ + return string representation of description of type. Note, + this is not a representation of the actual value. Subclasses + probably also override __str__() to give a more user-friendly + 'name/type' description for use in command format help messages. + """ + a = '' + if hasattr(self, 'typeargs'): + a = self.typeargs + return '{0}(\'{1}\')'.format(self.__class__.__name__, a) + + def __str__(self): + """ + where __repr__ (ideally) returns a string that could be used to + reproduce the object, __str__ returns one you'd like to see in + print messages. Use __str__ to format the argtype descriptor + as it would be useful in a command usage message. + """ + return '<{0}>'.format(self.__class__.__name__) + +class CephInt(CephArgtype): + """ + range-limited integers, [+|-][0-9]+ or 0x[0-9a-f]+ + range: list of 1 or 2 ints, [min] or [min,max] + """ + def __init__(self, range=''): + if range == '': + self.range = list() + else: + self.range = list(range.split('|')) + self.range = map(long, self.range) + + def valid(self, s, partial=False): + try: + val = long(s) + except ValueError: + raise ArgumentValid("{0} doesn't represent an int".format(s)) + if len(self.range) == 2: + if val < self.range[0] or val > self.range[1]: + raise ArgumentValid("{0} not in range {1}".format(val, self.range)) + elif len(self.range) == 1: + if val < self.range[0]: + raise ArgumentValid("{0} not in range {1}".format(val, self.range)) + self.val = val + return True + + def __str__(self): + r = '' + if len(self.range) == 1: + r = '[{0}-]'.format(self.range[0]) + if len(self.range) == 2: + r = '[{0}-{1}]'.format(self.range[0], self.range[1]) + + return ''.format(r) + + +class CephFloat(CephArgtype): + """ + range-limited float type + range: list of 1 or 2 floats, [min] or [min, max] + """ + def __init__(self, range=''): + if range == '': + self.range = list() + else: + self.range = list(range.split('|')) + self.range = map(float, self.range) + + def valid(self, s, partial=False): + try: + val = float(s) + except ValueError: + raise ArgumentValid("{0} doesn't represent a float".format(s)) + if len(self.range) == 2: + if val < self.range[0] or val > self.range[1]: + raise ArgumentValid("{0} not in range {1}".format(val, self.range)) + elif len(self.range) == 1: + if val < self.range[0]: + raise ArgumentValid("{0} not in range {1}".format(val, self.range)) + self.val = val + return True + + def __str__(self): + r = '' + if len(self.range) == 1: + r = '[{0}-]'.format(self.range[0]) + if len(self.range) == 2: + r = '[{0}-{1}]'.format(self.range[0], self.range[1]) + return ''.format(r) + +class CephString(CephArgtype): + """ + String; pretty generic. + """ + def __init__(self, badchars=''): + self.badchars = badchars + + def valid(self, s, partial=False): + for c in self.badchars: + if c in s: + raise ArgumentFormat("bad char {0} in {1}".format(c, s)) + self.val = s + return True + + def __str__(self): + b = '' + if len(self.badchars): + b = '(without chars in {0})'.format(self.badchars) + return ''.format(b) + +class CephSocketpath(CephArgtype): + """ + Admin socket path; check that it's readable and S_ISSOCK + """ + def valid(self, s, partial=False): + mode = os.stat(s).st_mode + if not stat.S_ISSOCK(mode): + raise ArgumentValid('socket path {0} is not a socket'.format(s)) + self.val = s + return True + def __str__(self): + return '' + +class CephIPAddr(CephArgtype): + """ + IP address (v4 or v6) with optional port + """ + def valid(self, s, partial=False): + # parse off port, use socket to validate addr + type = 6 + if s.startswith('['): + type = 6 + elif s.find('.') != -1: + type = 4 + if type == 4: + port = s.find(':') + if (port != -1): + a = s[:port] + p = s[port+1:] + if int(p) > 65535: + raise ArgumentValid('{0}: invalid IPv4 port'.format(p)) + else: + a = s + p = None + try: + socket.inet_pton(socket.AF_INET, a) + except: + raise ArgumentValid('{0}: invalid IPv4 address'.format(a)) + else: + # v6 + if s.startswith('['): + end = s.find(']') + if end == -1: + raise ArgumentFormat('{0} missing terminating ]'.format(s)) + if s[end+1] == ':': + try: + p = int(s[end+2]) + except: + raise ArgumentValid('{0}: bad port number'.format(s)) + a = s[1:end] + else: + a = s + p = None + try: + socket.inet_pton(socket.AF_INET6, a) + except: + raise ArgumentValid('{0} not valid IPv6 address'.format(s)) + if p is not None and long(p) > 65535: + raise ArgumentValid("{0} not a valid port number".format(p)) + self.val = s + return True + + def __str__(self): + return '' + +class CephEntityAddr(CephIPAddr): + """ + EntityAddress, that is, IP address/nonce + """ + def valid(self, s, partial=False): + ip, nonce = s.split('/') + if not super(self.__class__, self).valid(ip): + raise ArgumentValid('CephEntityAddr {0}: ip address invalid'.format(s)) + self.val = s + return True + + def __str__(self): + return '' + +class CephPoolname(CephArgtype): + """ + Pool name; very little utility + """ + def __str__(self): + return '' + +class CephObjectname(CephArgtype): + """ + Object name. Maybe should be combined with Pool name as they're always + present in pairs, and then could be checked for presence + """ + def valid(self, s, partial=False): + self.val = s + return True + + def __str__(self): + return '' + +class CephPgid(CephArgtype): + """ + pgid, in form N.xxx (N = pool number, xxx = hex pgnum) + """ + def valid(self, s, partial=False): + if s.find('.') == -1: + raise ArgumentFormat('pgid has no .') + poolid, pgnum = s.split('.') + if poolid < 0: + raise ArgumentFormat('pool {0} < 0'.format(poolid)) + try: + pgnum = int(pgnum, 16) + except: + raise ArgumentFormat('pgnum {0} not hex integer'.format(pgnum)) + self.val = s + return True + + def __str__(self): + return '' + +class CephName(CephArgtype): + """ + Name, or type.id, where type is osd|mon|client|mds, and id is a base10 int, + or just id. + + Also accept '*' + """ + def valid(self, s, partial=False): + if s == '*': + self.val = s + self.nametype = None + self.nameid = None + return True + if s.find('.') == -1: + i = s + else: + t, i = s.split('.') + if not t in ('osd', 'mon', 'client', 'mds'): + raise ArgumentValid('unknown type ' + self.t) + if t == 'osd': + try: + i = int(i) + except: + raise ArgumentFormat('osd id ' + i + ' not integer') + self.nametype = t + self.val = s + self.nameid = i + return True + + def __str__(self): + return '' + + +class CephChoices(CephArgtype): + """ + Set of string literals; init with valid choices + """ + def __init__(self, strings='', **kwargs): + self.strings=strings.split('|') + + def valid(self, s, partial=False): + if not partial: + if not s in self.strings: + # show as __str__ does: {s1|s2..} + raise ArgumentValid("{0} not in {1}".format(s, self)) + self.val = s + return True + + # partial + for t in self.strings: + if t.startswith(s): + self.val = s + return True + raise ArgumentValid("{0} not in {1}". format(s, self)) + + def __str__(self): + if len(self.strings) == 1: + return '{0}'.format(self.strings[0]) + else: + return '{0}'.format('|'.join(self.strings)) + +class CephFilepath(CephArgtype): + """ + Openable file + """ + def valid(self, s, partial=False): + try: + f = open(s, 'a+') + except Exception as e: + raise ArgumentValid('can\'t open {0}: {1}'.format(s, e)) + f.close() + self.val = s + return True + + def __str__(self): + return '' + +class CephFragment(CephArgtype): + """ + 'Fragment' ??? XXX + """ + def valid(self, s, partial=False): + if s.find('/') == -1: + raise ArgumentFormat('{0}: no /'.format(s)) + val, bits = s.split('/') + # XXX is this right? + if not val.startswith('0x'): + raise ArgumentFormat("{0} not a hex integer".format(val)) + try: + long(val) + except: + raise ArgumentFormat('can\'t convert {0} to integer'.format(val)) + try: + long(bits) + except: + raise ArgumentFormat('can\'t convert {0} to integer'.format(bits)) + self.val = s + return True + + def __str__(self): + return "" + + +class CephUUID(CephArgtype): + """ + CephUUID: pretty self-explanatory + """ + def valid(self, s, partial=False): + try: + uuid.UUID(s) + except Exception as e: + raise ArgumentFormat('invalid UUID {0}: {1}'.format(s, e)) + self.val = s + return True + + def __str__(self): + return '' + + +class CephPrefix(CephArgtype): + """ + CephPrefix: magic type for "all the first n fixed strings" + """ + def __init__(self, prefix=''): + self.prefix = prefix + + def valid(self, s, partial=False): + if partial: + if self.prefix.startswith(s): + self.val = s + return True + else: + if (s == self.prefix): + self.val = s + return True + raise ArgumentPrefix("no match for {0}".format(s)) + + def __str__(self): + return self.prefix + +############################################################################ + + +class argdesc(object): + """ + argdesc(typename, name='name', n=numallowed|N, + req=False, helptext=helptext, **kwargs (type-specific)) + + validation rules: + typename: type(**kwargs) will be constructed + later, type.valid(w) will be called with a word in that position + + name is used for parse errors and for constructing JSON output + n is a numeric literal or 'n|N', meaning "at least one, but maybe more" + req=False means the argument need not be present in the list + helptext is the associated help for the command + anything else are arguments to pass to the type constructor. + + self.instance is an instance of type t constructed with typeargs. + + valid() will later be called with input to validate against it, + and will store the validated value in self.instance.val for extraction. + """ + def __init__(self, t, name=None, n=1, req=True, **kwargs): + if isinstance(t, types.StringTypes): + self.t = CephPrefix + self.typeargs = {'prefix':t} + self.req = True + else: + self.t = t + self.typeargs = kwargs + self.req = bool(req == True or req == 'True') + + self.name = name + self.N = (n in ['n', 'N']) + if self.N: + self.n = 1 + else: + self.n = int(n) + self.instance = self.t(**self.typeargs) + + def __repr__(self): + r = 'argdesc(' + str(self.t) + ', ' + internals = ['N', 'typeargs', 'instance', 't'] + for (k,v) in self.__dict__.iteritems(): + if k.startswith('__') or k in internals: + pass + else: + # undo mods above + if k == 'n' and self.N: + v = 'N' + r += '{0}={1}, '.format(k,v) + for (k,v) in self.typeargs.iteritems(): + r += '{0}={1}, '.format(k,v) + return r[:-2] + ')' + + def __str__(self): + if ((self.t == CephChoices and len(self.instance.strings) == 1) + or (self.t == CephPrefix)): + s = '{0}'.format(str(self.instance)) + else: + s = '{0}({1})'.format(self.name, str(self.instance)) + if self.N: + s += ' [' + str(self.instance) + '...]' + if not self.req: + s = '{' + s + '}' + return s + + def helpstr(self): + """ + like str(), but omit parameter names (except for CephString, + which really needs them) + """ + if self.t == CephString: + chunk = '<{0}>'.format(self.name) + else: + chunk = str(self.instance) + s = '{0}'.format(chunk) + if self.N: + s += ' [' + chunk + '...]' + if not self.req: + s = '{' + s + '}' + return s + +def concise_sig(sig): + """ + Return string representation of sig useful for syntax reference in help + """ + first = True + s = '' + for d in sig: + if first: + first = False + else: + s += ' ' + s += d.helpstr() + return s + +def parse_funcsig(sig): + """ + parse a single descriptor (array of strings or dicts) into a + dict of function descriptor/validators (objects of CephXXX type) + """ + newsig = [] + argnum = 0 + for desc in sig: + argnum += 1 + if isinstance(desc, types.StringTypes): + t = CephPrefix + desc = {'type':t, 'name':'prefix', 'prefix':desc} + else: + # not a simple string, must be dict + if not 'type' in desc: + s = 'JSON descriptor {0} has no type'.format(sig) + raise JsonFormat(s) + # look up type string in our globals() dict; if it's an + # object of type types.TypeType, it must be a + # locally-defined class. otherwise, we haven't a clue. + if desc['type'] in globals(): + t = globals()[desc['type']] + if type(t) != types.TypeType: + s = 'unknown type {0}'.format(desc['type']) + raise JsonFormat(s) + else: + s = 'unknown type {0}'.format(desc['type']) + raise JsonFormat(s) + + kwargs = dict() + for key, val in desc.items(): + if key not in ['type', 'name', 'n', 'req']: + kwargs[key] = val + newsig.append(argdesc(t, + name=desc.get('name', None), + n=desc.get('n', 1), + req=desc.get('req', True), + **kwargs)) + return newsig + + +def parse_json_funcsigs(s): + """ + parse_json_funcsigs(s) + + A function signature is mostly an array of argdesc; it's represented + in JSON as + { + "cmd001": {"sig":[ "type": type, "name": name, "n": num, "req":true|false ], "help":helptext} + . + . + . + ] + + A set of sigs is in an dict mapped by a unique number: + { + "cmd1": { + "sig": ["type.. ], "help":{"text":helptext} + } + "cmd2"{ + "sig": [.. ], "help":{"text":helptext} + } + } + + Parse the string s and return an dict of dicts, keyed by opcode; + each dict contains 'sig' with the array of descriptors, and 'help' + with the helptext. + """ + try: + overall = json.loads(s) + except Exception as e: + print >> sys.stderr, "Couldn't parse JSON {0}: {1}".format(s, e) + raise e + sigdict = {} + for cmdtag, cmd in overall.iteritems(): + helptext = cmd.get('help', 'no help available') + try: + sig = cmd['sig'] + except KeyError: + s = "JSON descriptor {0} has no 'sig'".format(cmdtag) + raise JsonFormat(s) + newsig = parse_funcsig(sig) + sigdict[cmdtag] = {'sig':newsig, 'helptext':helptext} + return sigdict + +def validate_one(word, desc, partial=False): + """ + validate_one(word, desc, partial=False) + + validate word against the constructed instance of the type + in desc. May raise exception. If it returns false (and doesn't + raise an exception), desc.instance.val will + contain the validated value (in the appropriate type). + """ + if desc.instance.valid(word, partial): + desc.numseen += 1 + if desc.N: + desc.n = desc.numseen + 1 + return True + return False + +def matchnum(args, signature, partial=False): + """ + matchnum(s, signature, partial=False) + + Returns number of arguments matched in s against signature. + Can be used to determine most-likely command for full or partial + matches (partial applies to string matches). + """ + words = args[:] + mysig = copy.deepcopy(signature) + matchcnt = 0 + for desc in mysig: + setattr(desc, 'numseen', 0) + while desc.numseen < desc.n: + # if there are no more arguments, return + if not words: + return matchcnt; + word = words.pop(0) + try: + validate_one(word, desc, partial) + except: + if not desc.req: + # this wasn't required, so word may match the next desc + words.insert(0, word) + break + else: + # it was required, and didn't match, return + return matchcnt + if desc.req: + matchcnt += 1 + return matchcnt + +def validate(args, signature, partial=False): + """ + validate(s, signature, partial=False) + + Assumes s represents a possible command input following format of + signature. Runs a validation; no exception means it's OK. Return + a dict containing all arguments named by their descriptor name + (with duplicate args per name accumulated into a space-separated + value). + + If partial is set, allow partial matching (with partial dict returned) + """ + words = args[:] + mysig = copy.deepcopy(signature) + d = dict() + for desc in mysig: + setattr(desc, 'numseen', 0) + while desc.numseen < desc.n: + if words: + word = words.pop(0) + else: + if desc.req: + if desc.N and desc.numseen < 1: + # wanted N, didn't even get 1 + if partial: + return d + raise ArgumentNumber('saw {0} of {1}, expected at least 1'.format(desc.numseen, desc)) + elif not desc.N and desc.numseen < desc.n: + # wanted n, got too few + if partial: + return d + raise ArgumentNumber('saw {0} of {1}, expected {2}'.format(desc.numseen, desc, desc.n)) + break + try: + validate_one(word, desc) + except Exception as e: + # not valid; if not required, just push back for the next one + if not desc.req: + words.insert(0, word) + break + else: + # hm, but it was required, so quit + if partial: + return d + raise e + + if desc.N: + # value should be a list + if desc.name in d: + d[desc.name] += [desc.instance.val] + else: + d[desc.name] = [desc.instance.val] + elif (desc.t == CephPrefix) and (desc.name in d): + # value should be a space-joined concatenation + d[desc.name] += ' ' + desc.instance.val + else: + # if first CephPrefix or any other type, just set it + d[desc.name] = desc.instance.val + return d + +def osdids(): + ret, outbuf, outs = json_command(prefix='osd ls') + if ret: + raise RuntimeError('Can\'t contact mon for osd list') + return [i for i in outbuf.split('\n')] + +def monids(): + ret, outbuf, outs = json_command(prefix='mon dump', + argdict={'format':'json'}) + if ret: + raise RuntimeError('Can\'t contact mon for mon list') + d = json.loads(outbuf) + return [m['name'] for m in d['mons']] + +def mdsids(): + ret, outbuf, outs = json_command(prefix='mds dump', + argdict={'format':'json'}) + if ret: + raise RuntimeError('Can\'t contact mon for mds list') + d = json.loads(outbuf) + l = [] + infodict = d['info'] + for mdsdict in infodict.values(): + l.append(mdsdict['name']) + return l + +def parse_cmdargs(args=None, target=''): + # alias: let the line-wrapping be sane + AP = argparse.ArgumentParser + + # format our own help + parser = AP(description='Frontend for ceph CLI', add_help=False) + + parser.add_argument('--completion', action='store_true') + + parser.add_argument('-h', '--help', help='request mon help', + action='store_true') + parser.add_argument('--help-all', help='request help for all daemons', + action='store_true') + + parser.add_argument('-c', '--conf', dest='cephconf', + help='ceph configuration file') + parser.add_argument('-i', '--in-file', dest='input_file', + help='input file') + parser.add_argument('-o', '--out-file', dest='output_file', + help='output file') + parser.add_argument('-k', '--keyring', dest='keyring_file', + help='keyring file') + + parser.add_argument('--id', '--user', dest='client_id', + help='client id for authentication') + parser.add_argument('--name', '-n', dest='client_name', + help='client name for authentication') + parser.add_argument('--cluster', help='cluster name') + + parser.add_argument('--admin-daemon', dest='admin_socket', + help='submit admin-socket commands (\"help\" for help') + + parser.add_argument('-s', '--status', action='store_true', + help='show cluster status') + + parser.add_argument('-w', '--watch', action='store_true', + help='watch live cluster changes') + parser.add_argument('--watch-debug', action='store_true', + help='watch debug events') + parser.add_argument('--watch-info', action='store_true', + help='watch info events') + parser.add_argument('--watch-sec', action='store_true', + help='watch security events') + parser.add_argument('--watch-warn', action='store_true', + help='watch warn events') + parser.add_argument('--watch-error', action='store_true', + help='watch error events') + + parser.add_argument('-v', action="store_true") + parser.add_argument('--verbose', action="store_true") + parser.add_argument('--concise', dest='verbose', action="store_false") + + parser.add_argument('-f', '--format', choices=['json', 'json-pretty', + 'xml', 'xml-pretty', 'plain'], dest='output_format') + # for pg dump_stuck + parser.add_argument('--threshold', type=int, help='number of seconds for a pg to be considered stuck for pg dump_stuck') + + # returns a Namespace with the parsed args, and a list of all extras + parsed_args, extras = parser.parse_known_args(args) + + return parser, parsed_args, extras + +def do_help(parser, help_all = False): + """ + Print basic parser help + If the cluster is available: + get and print monitor help; + if help_all, print help for daemon commands as well + """ + + def help_for_target(target): + ret, outbuf, outs = json_command(target=target, + prefix='get_command_descriptions', + timeout=10) + if ret: + print >> sys.stderr, \ + "couldn't get command descriptions for {0}: {1}".\ + format(target, outs) + else: + sys.stdout.write(format_help(parse_json_funcsigs(outbuf))) + + parser.print_help() + print '\n' + if (cluster_handle): + help_for_target(target=('mon', '')) + + if help_all and cluster_handle: + # try/except in case there are no daemons of that type + try: + firstosd = osdids()[0] + print '\nOSD.{0} tell commands and pg pgid commands:\n\n'.\ + format(firstosd) + help_for_target(target=('osd', osdids()[0])) + + print '\nOSD daemon commands:\n\n' + sys.stdout.write(format_help(parse_json_funcsigs(admin_socket(ceph_conf('admin_socket', 'osd.' + firstosd), ['get_command_descriptions'])))) + except: + pass + + try: + firstmon = monids()[0] + print '\nmon.{0} daemon commands:\n\n'.format(firstmon) + sys.stdout.write(format_help(parse_json_funcsigs(admin_socket(ceph_conf('admin_socket', 'mon.' + firstmon), ['get_command_descriptions'])))) + except: + pass + + try: + firstmds = mdsids()[0] + print '\nmds.{0} daemon commands:\n\n'.format(firstmds) + sys.stdout.write(format_help(parse_json_funcsigs(admin_socket(ceph_conf('admin_socket', 'mds.' + firstmds), ['get_command_descriptions'])))) + except: + pass + + return 0 + + +def descsort(sh1, sh2): + """ + sort descriptors by prefixes, defined as the concatenation of all simple + strings in the descriptor; this works out to just the leading strings. + """ + return cmp(concise_sig(sh1['sig']), concise_sig(sh2['sig'])) + + +dontsplit = string.letters + '{[<>]}' + +def wrap(s, width, indent): + """ + generator to transform s into a sequence of strings width or shorter, + for wrapping text to a specific column width. + Attempt to break on anything but dontsplit characters. + indent is amount to indent 2nd-through-nth lines. + + so "long string long string long string" width=11 indent=1 becomes + 'long string', ' long string', ' long string' so that it can be printed + as + long string + long string + long string + + Consumes s. + """ + result = '' + leader = '' + while len(s): + + if (len(s) <= width): + # no splitting; just possibly indent + result = leader + s + s = '' + yield result + + else: + splitpos = width + while (splitpos > 0) and (s[splitpos-1] in dontsplit): + splitpos -= 1 + + if splitpos == 0: + splitpos = width + + if result: + # prior result means we're mid-iteration, indent + result = leader + else: + # first time, set leader and width for next + leader = ' ' * indent + width -= 1 # for subsequent space additions + + # remove any leading spaces in this chunk of s + result += s[:splitpos].lstrip() + s = s[splitpos:] + + yield result + + raise StopIteration + +def format_help(cmddict): + """ + Formats all the cmdsigs and helptexts from cmddict into a sorted-by- + cmdsig 2-column display, with each column wrapped and indented to + fit into 40 characters. + """ + + fullusage = '' + for cmd in sorted(cmddict.itervalues(), cmp=descsort): + + if not cmd['helptext']: + continue + siglines = [l for l in wrap(concise_sig(cmd['sig']), 40, 1)] + helplines = [l for l in wrap(cmd['helptext'], 39, 1)] + + # make lists the same length + maxlen = max(len(siglines), len(helplines)) + siglines.extend([''] * (maxlen - len(siglines))) + helplines.extend([''] * (maxlen - len(helplines))) + + # so we can zip them for output + for (s, h) in zip(siglines, helplines): + fullusage += '{0:40s} {1}\n'.format(s, h) + + return fullusage + +def validate_command(parsed_args, sigdict, args): + """ + turn args into a valid dictionary ready to be sent off as JSON, + validated against sigdict. + parsed_args is the namespace back from argparse + """ + found = [] + valid_dict = {} + if args: + # Repulsive hack to handle tell: lop off 'tell' and target + # and validate the rest of the command. 'target' is already + # determined in our callers, so it's ok to remove it here. + if args[0] == 'tell': + args = args[2:] + # look for best match, accumulate possibles in bestcmds + # (so we can maybe give a more-useful error message) + best_match_cnt = 0 + bestcmds = [] + for cmdtag, cmd in sigdict.iteritems(): + sig = cmd['sig'] + matched = matchnum(args, sig, partial=True) + if (matched > best_match_cnt): + if verbose: + print >> sys.stderr, \ + "better match: {0} > {1}: {2}:{3} ".format(matched, + best_match_cnt, cmdtag, concise_sig(sig)) + best_match_cnt = matched + bestcmds = [{cmdtag:cmd}] + elif matched == best_match_cnt: + if verbose: + print >> sys.stderr, \ + "equal match: {0} > {1}: {2}:{3} ".format(matched, + best_match_cnt, cmdtag, concise_sig(sig)) + bestcmds.append({cmdtag:cmd}) + + if verbose: + print >> sys.stderr, "bestcmds: ", bestcmds + + # for everything in bestcmds, look for a true match + for cmdsig in bestcmds: + for cmd in cmdsig.itervalues(): + sig = cmd['sig'] + helptext = cmd['helptext'] + try: + valid_dict = validate(args, sig) + found = sig + break + except ArgumentPrefix: + # this means a CephPrefix type didn't match; since + # this is common, just eat it + pass + except ArgumentError as e: + # prefixes matched, but some other arg didn't; + # this is interesting information + print >> sys.stderr, '{0}: invalid command'.\ + format(' '.join(args)) + print >> sys.stderr, '{0}'.format(e) + print >> sys.stderr, "did you mean {0}?\n\t{1}".\ + format(concise_sig(sig), helptext) + pass + + if not found: + print >> sys.stderr, 'no valid command found' + print >> sys.stderr, 'close matches:' + for cmdsig in bestcmds: + for (cmdtag, cmd) in cmdsig.iteritems(): + print >> sys.stderr, concise_sig(cmd['sig']) + return None + + if parsed_args.output_format: + valid_dict['format'] = parsed_args.output_format + + if parsed_args.threshold: + valid_dict['threshold'] = parsed_args.threshold + + return valid_dict + +def json_command(target=('mon', ''), prefix=None, argdict=None, inbuf='', + timeout=0): + """ + Send a new_style command to a daemon using librados's + mon_command, osd_command, or pg_command. Prefix may be supplied + separately or in argdict. Any bulk input data comes in inbuf. + Returns (ret, outbuf, outs); ret is the return code, outbuf is + the outbl "bulk useful output" buffer, and outs is any status + or error message (intended for stderr). + + If target is osd.N, send command to that osd (except for pgid cmds) + """ + cmddict = {} + if prefix: + cmddict.update({'prefix':prefix}) + if argdict: + cmddict.update(argdict) + + # grab prefix for error messages + prefix = cmddict['prefix'] + + try: + if target[0] == 'osd': + osdtarg = CephName() + if 'target' in cmddict: + osdtarget = cmddict.pop('target') + else: + osdtarget = '{0}.{1}'.format(*target) + + try: + osdtarg.valid(osdtarget) + osdid = osdtarg.nameid + except: + # uh..I dunno, try osd.0? + osdid = 0 + + if verbose: + print >> sys.stderr, 'submit {0} to osd.{1}'.\ + format(json.dumps(cmddict), osdid) + ret, outbuf, outs = \ + cluster_handle.osd_command(osdid, json.dumps(cmddict), inbuf, + timeout) + + elif target[0] == 'pg': + # leave it in cmddict for the OSD to use too + pgid = target[1] + if verbose: + print >> sys.stderr, 'submit {0} for pgid {1}'.\ + format(json.dumps(cmddict), pgid) + ret, outbuf, outs = \ + cluster_handle.pg_command(pgid, json.dumps(cmddict), inbuf, + timeout) + + elif target[0] == 'mon': + if verbose: + print >> sys.stderr, '{0} to {1}'.\ + format(json.dumps(cmddict), target[0]) + ret, outbuf, outs = cluster_handle.mon_command(json.dumps(cmddict), + inbuf, timeout) + + except Exception as e: + raise RuntimeError('"{0}": exception {1}'.format(prefix, e)) + + return ret, outbuf, outs + +def admin_socket(asok_path, cmd): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + sock.connect(asok_path) + sock.sendall(' '.join(cmd) + '\0') + + len_str = sock.recv(4) + l, = struct.unpack(">I", len_str) + ret = '' + + got = 0 + while got < l: + bit = sock.recv(l - got) + ret += bit + got += len(bit) + + except Exception as e: + raise RuntimeError('exception: {0}'.format(e)) + + return ret + + +def ceph_conf(field, name): + p = subprocess.Popen( + args=[ + 'ceph-conf', + field, + '-n', + name, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + outdata, errdata = p.communicate() + if (len(errdata)): + raise RuntimeError('unable to get conf option %s for %s: %s' % (field, name, errdata)) + return outdata.rstrip() + +def new_style_command(parsed_args, cmdargs, target, sigdict, inbuf, verbose): + """ + Do new-style command dance. + target: daemon to receive command: mon (any) or osd.N + sigdict - the parsed output from the new monitor describing commands + inbuf - any -i input file data + verbose - bool + """ + if verbose: + for cmdtag in sorted(sigdict.keys()): + cmd = sigdict[cmdtag] + sig = cmd['sig'] + print '{0}: {1}'.format(cmdtag, concise_sig(sig)) + + got_command = False + + if not got_command: + if cmdargs: + # Validate input args against list of sigs + valid_dict = validate_command(parsed_args, sigdict, cmdargs) + if valid_dict: + got_command = True + else: + return -errno.EINVAL, '', 'invalid command' + else: + # do the command-interpreter looping + while True: + interactive_input = raw_input('ceph> ') + if interactive_input in ['q', 'quit', 'Q']: + return 0, '', '' + cmdargs = parse_cmdargs(interactive_input.split())[2] + target = find_cmd_target(cmdargs) + valid_dict = validate_command(parsed_args, sigdict, cmdargs) + if valid_dict: + if verbose: + print >> sys.stderr, "Submitting command ", valid_dict + ret, outbuf, outs = json_command(target=target, + argdict=valid_dict) + if ret: + sys.stderr.write('Error {0}: {1}'.format(ret, outs)) + return ret, '', outs + else: + print "invalid command" + + if verbose: + print >> sys.stderr, "Submitting command ", valid_dict + return json_command(target=target, argdict=valid_dict, inbuf=inbuf) + +OSD_TELL_MATCH = { \ + 'sig': ['tell', {'name':'target','type':'CephName'}], \ + 'matchnum': 2, \ + 'return_key': 'target', \ +} +PGID_MATCH = { \ + 'sig': ['pg', {'name':'pgid','type':'CephPgid'}], \ + 'matchnum': 2, \ + 'return_key': 'pgid', \ +} + +def find_cmd_target(childargs): + """ + Using a minimal validation, figure out whether the command + should be sent to a monitor or an osd. We do this before even + asking for the 'real' set of command signatures, so we can ask the + right daemon. + Returns ('osd', osdid), ('pg', pgid), or ('mon', '') + """ + sig = parse_funcsig(['tell', {'name':'target','type':'CephName'}]) + valid_dict = validate(childargs, sig, partial=True); + if len(valid_dict) == 2: + name = CephName() + name.valid(valid_dict['target']) + return 'osd', name.nameid + + sig = parse_funcsig(['pg', {'name':'pgid','type':'CephPgid'}]) + valid_dict = validate(childargs, sig, partial=True); + if len(valid_dict) == 2: + return 'pg', valid_dict['pgid'] + + return 'mon', '' + +def complete(sigdict, args, target): + """ + Command completion. Match as much of [args] as possible, + and print every possible match separated by newlines. + Return exitcode. + """ + # XXX this looks a lot like the front of validate_command(). Refactor? + + complete_verbose = 'COMPVERBOSE' in os.environ + + # Repulsive hack to handle tell: lop off 'tell' and target + # and validate the rest of the command. 'target' is already + # determined in our callers, so it's ok to remove it here. + if len(args) and args[0] == 'tell': + args = args[2:] + # look for best match, accumulate possibles in bestcmds + # (so we can maybe give a more-useful error message) + best_match_cnt = 0 + bestcmds = [] + for cmdtag, cmd in sigdict.iteritems(): + sig = cmd['sig'] + matched = matchnum(args, sig, partial=True) + if (matched > best_match_cnt): + if complete_verbose: + print >> sys.stderr, \ + "better match: {0} > {1}: {2}:{3} ".format(matched, + best_match_cnt, cmdtag, concise_sig(sig)) + best_match_cnt = matched + bestcmds = [{cmdtag:cmd}] + elif matched == best_match_cnt: + if complete_verbose: + print >> sys.stderr, \ + "equal match: {0} > {1}: {2}:{3} ".format(matched, + best_match_cnt, cmdtag, concise_sig(sig)) + bestcmds.append({cmdtag:cmd}) + + # look through all matching sigs + comps = [] + for cmddict in bestcmds: + for cmd in cmddict.itervalues(): + sig = cmd['sig'] + # either: + # we match everything fully, so we want the next desc, or + # we match more partially, so we want the partial match + fullindex = matchnum(args, sig, partial=False) - 1 + partindex = matchnum(args, sig, partial=True) - 1 + if complete_verbose: + print >> sys.stderr, '{}: f {} p {} len {}'.format(sig, fullindex, partindex, len(sig)) + if fullindex == partindex and fullindex + 1 < len(sig): + d = sig[fullindex + 1] + else: + d = sig[partindex] + comps.append(str(d)) + if complete_verbose: + print >> sys.stderr, '\n'.join(comps) + print '\n'.join(comps) + + return 0 + +### +# main +### + +def main(): + + parser, parsed_args, childargs = parse_cmdargs() + + if parsed_args.v: + print 'ceph version {0} ({1})'.format(CEPH_GIT_NICE_VER, CEPH_GIT_VER) + return 0 + + global verbose + verbose = parsed_args.verbose + + # pass on --id, --name, --conf + name = 'client.admin' + if parsed_args.client_id: + name = 'client.' + parsed_args.client_id + if parsed_args.client_name: + name = parsed_args.client_name + + # default '' means default conf search + conffile = '' + if parsed_args.cephconf: + conffile = parsed_args.cephconf + # For now, --admin-daemon is handled as usual. Try it + # first in case we can't connect() to the cluster + if parsed_args.admin_socket: + try: + print admin_socket(parsed_args.admin_socket, childargs) + except Exception as e: + print >> sys.stderr, 'admin_socket: {0}'.format(e) + return 0 + + if len(childargs) > 0 and childargs[0] == "daemon": + if len(childargs) > 2: + if childargs[1].find('/') >= 0: + try: + print admin_socket(childargs[1], childargs[2:]) + except Exception as e: + print >> sys.stderr, 'admin_socket: {0}'.format(e) + return 0 + else: + # try resolve daemon name + path = ceph_conf('admin_socket', childargs[1]) + try: + print admin_socket(path, childargs[2:]) + except Exception as e: + print >> sys.stderr, 'admin_socket: {0}'.format(e) + return 0 + else: + print >> sys.stderr, 'Daemon requires at least 2 arguments' + return 1 + + # handle any 'generic' ceph arguments that we didn't parse here + global cluster_handle + + # rados.Rados() will call rados_create2, and then read the conf file, + # and then set the keys from the dict. So we must do these + # "pre-file defaults" first (see common_preinit in librados) + conf_defaults = { + 'log_to_stderr':'true', + 'err_to_stderr':'true', + 'log_flush_on_exit':'true', + } + + clustername = 'ceph' + if parsed_args.cluster: + clustername = parsed_args.cluster + + cluster_handle = rados.Rados(name=name, clustername=clustername, + conf_defaults=conf_defaults, conffile='') + + retargs = cluster_handle.conf_parse_argv(childargs) + #tmp = childargs + childargs = retargs + if not childargs: + childargs = [] + + # -- means "stop parsing args", but we don't want to see it either + if '--' in childargs: + childargs.remove('--') + + try: + cluster_handle.connect() + except KeyboardInterrupt: + print >> sys.stderr, 'Cluster connection aborted' + return 1 + except Exception as e: + print >> sys.stderr, 'Error connecting to cluster: {0}'.\ + format(e.__class__.__name__) + return 1 + + if parsed_args.help or parsed_args.help_all: + return do_help(parser, parsed_args.help_all) + + # implement -w/--watch_* + # This is ugly, but Namespace() isn't quite rich enough. + level = '' + for k,v in parsed_args._get_kwargs(): + if k.startswith('watch') and v: + if k == 'watch': + level = 'info' + else: + level = k.replace('watch_', '') + if level: + + # an awfully simple callback + def watch_cb(arg, line, who, stamp_sec, stamp_nsec, seq, level, msg): + print line + + # first do a ceph status + ret, outbuf, outs = json_command(prefix='status') + if ret: + print >> sys.stderr, "status query failed: ", outs + return ret + print outbuf + + # this instance keeps the watch connection alive, but is + # otherwise unused + logwatch = rados.MonitorLog(cluster_handle, level, watch_cb, 0) + # loop forever letting watch_cb print lines + while True: + try: + pass + except KeyboardInterrupt: + # or until ^C, at least + return 0 + + # read input file, if any + inbuf = '' + if parsed_args.input_file: + try: + with open(parsed_args.input_file, 'r') as f: + inbuf = f.read() + except Exception as e: + print >> sys.stderr, 'Can\'t open input file {0}: {1}'.format(parsed_args.input_file, e) + return 1 + + # prepare output file, if any + if parsed_args.output_file: + try: + outf = open(parsed_args.output_file, 'w') + except: + print >> sys.stderr, \ + 'Can\'t open output file {0}: {1}'.\ + format(parsed_args.output_file, e) + return 1 + + # -s behaves like a command (ceph status). + if parsed_args.status: + childargs.insert(0, 'status') + + target = find_cmd_target(childargs) + + # fetch JSON sigs from command + # each line contains one command signature (a placeholder name + # of the form 'cmdNNN' followed by an array of argument descriptors) + # as part of the validated argument JSON object + + ret, outbuf, outs = json_command(target=target, + prefix='get_command_descriptions') + if ret == -errno.EINVAL: + # send command to old monitor + if verbose: + print '{0} to old monitor'.format(' '.join(childargs)) + ret, outbuf, outs = cluster_handle.mon_command(childargs, inbuf) + elif ret: + if ret < 0: + ret = -ret + print >> sys.stderr, \ + 'Problem getting command descriptions from {0}, {1}'.\ + format(target, errno.errorcode[ret]) + return ret + else: + sigdict = parse_json_funcsigs(outbuf) + + if parsed_args.completion: + return complete(sigdict, childargs, target) + + ret, outbuf, outs = new_style_command(parsed_args, childargs, target, + sigdict, inbuf, verbose) + + if ret < 0: + ret = -ret + print >> sys.stderr, 'Error {0}: {1}'.format(errno.errorcode[ret], outs) + return ret + + # this assumes outs never has useful command output, only status + if outs: + print >> sys.stderr, outs + + if (parsed_args.output_file): + outf.write(outbuf) + outf.close() + else: + # hack: old code printed status line before many json outputs + # (osd dump, etc.) that consumers know to ignore. Add blank line + # to satisfy consumers that skip the first line, but not annoy + # consumers that don't. + if parsed_args.output_format and \ + parsed_args.output_format.startswith('json'): + sys.stdout.write('\n'); + + sys.stdout.write(outbuf) + + return 0 + +if __name__ == '__main__': + sys.exit(main())