#!/usr/bin/env python from __future__ import print_function import subprocess import uuid import re import json import sys import ast import requests from operator import itemgetter from heapq import nlargest CLUSTER_UUID_NAME='cluster-uuid' CLUSTER_OWNERSHIP_NAME='cluster-ownership' verbose = False try: from collections import Counter except ImportError: from itertools import repeat, ifilter class Counter(dict): '''Dict subclass for counting hashable objects. Sometimes called a bag or multiset. Elements are stored as dictionary keys and their counts are stored as dictionary values. >>> Counter('zyzygy') Counter({'y': 3, 'z': 2, 'g': 1}) ''' def __init__(self, iterable=None, **kwds): '''Create a new, empty Counter object. And if given, count elements from an input iterable. Or, initialize the count from another mapping of elements to their counts. >>> c = Counter() # a new, empty counter >>> c = Counter('gallahad') # a new counter from an iterable >>> c = Counter({'a': 4, 'b': 2}) # a new counter from a mapping >>> c = Counter(a=4, b=2) # a new counter from keyword args ''' self.update(iterable, **kwds) def __missing__(self, key): return 0 def most_common(self, n=None): '''List the n most common elements and their counts from the most common to the least. If n is None, then list all element counts. >>> Counter('abracadabra').most_common(3) [('a', 5), ('r', 2), ('b', 2)] ''' if n is None: return sorted(self.iteritems(), key=itemgetter(1), reverse=True) return nlargest(n, self.iteritems(), key=itemgetter(1)) def elements(self): '''Iterator over elements repeating each as many times as its count. >>> c = Counter('ABCABC') >>> sorted(c.elements()) ['A', 'A', 'B', 'B', 'C', 'C'] If an element's count has been set to zero or is a negative number, elements() will ignore it. ''' for elem, count in self.iteritems(): for _ in repeat(None, count): yield elem # Override dict methods where the meaning changes for Counter objects. @classmethod def fromkeys(cls, iterable, v=None): raise NotImplementedError( 'Counter.fromkeys() is undefined. Use Counter(iterable) instead.') def update(self, iterable=None, **kwds): '''Like dict.update() but add counts instead of replacing them. Source can be an iterable, a dictionary, or another Counter instance. >>> c = Counter('which') >>> c.update('witch') # add elements from another iterable >>> d = Counter('watch') >>> c.update(d) # add elements from another counter >>> c['h'] # four 'h' in which, witch, and watch 4 ''' if iterable is not None: if hasattr(iterable, 'iteritems'): if self: self_get = self.get for elem, count in iterable.iteritems(): self[elem] = self_get(elem, 0) + count else: dict.update(self, iterable) # fast path when counter is empty else: self_get = self.get for elem in iterable: self[elem] = self_get(elem, 0) + 1 if kwds: self.update(kwds) def copy(self): 'Like dict.copy() but returns a Counter instance instead of a dict.' return Counter(self) def __delitem__(self, elem): 'Like dict.__delitem__() but does not raise KeyError for missing values.' if elem in self: dict.__delitem__(self, elem) def __repr__(self): if not self: return '%s()' % self.__class__.__name__ items = ', '.join(map('%r: %r'.__mod__, self.most_common())) return '%s({%s})' % (self.__class__.__name__, items) # Multiset-style mathematical operations discussed in: # Knuth TAOCP Volume II section 4.6.3 exercise 19 # and at http://en.wikipedia.org/wiki/Multiset # # Outputs guaranteed to only include positive counts. # # To strip negative and zero counts, add-in an empty counter: # c += Counter() def __add__(self, other): '''Add counts from two counters. >>> Counter('abbb') + Counter('bcc') Counter({'b': 4, 'c': 2, 'a': 1}) ''' if not isinstance(other, Counter): return NotImplemented result = Counter() for elem in set(self) | set(other): newcount = self[elem] + other[elem] if newcount > 0: result[elem] = newcount return result def __sub__(self, other): ''' Subtract count, but keep only results with positive counts. >>> Counter('abbbc') - Counter('bccd') Counter({'b': 2, 'a': 1}) ''' if not isinstance(other, Counter): return NotImplemented result = Counter() for elem in set(self) | set(other): newcount = self[elem] - other[elem] if newcount > 0: result[elem] = newcount return result def __or__(self, other): '''Union is the maximum of value in either of the input counters. >>> Counter('abbb') | Counter('bcc') Counter({'b': 3, 'c': 2, 'a': 1}) ''' if not isinstance(other, Counter): return NotImplemented _max = max result = Counter() for elem in set(self) | set(other): newcount = _max(self[elem], other[elem]) if newcount > 0: result[elem] = newcount return result def __and__(self, other): ''' Intersection is the minimum of corresponding counts. >>> Counter('abbb') & Counter('bcc') Counter({'b': 1}) ''' if not isinstance(other, Counter): return NotImplemented _min = min result = Counter() if len(self) < len(other): self, other = other, self for elem in ifilter(self.__contains__, other): newcount = _min(self[elem], other[elem]) if newcount > 0: result[elem] = newcount return result def print_stderr(*args, **kwargs): kwargs.setdefault('file', sys.stderr) print(*args, **kwargs) def run_command(cmd): if verbose: print_stderr("run_command: " + str(cmd)) child = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (o, e) = child.communicate() o = o.decode('utf-8', 'ignore') e = e.decode('utf-8', 'ignore') return (child.returncode, o, e) def get_uuid(): (rc,uid,e) = run_command(['ceph', 'config-key', 'get', CLUSTER_UUID_NAME]) if rc: #uuid is not yet set. uid = str(uuid.uuid4()) (rc, o, e) = run_command(['ceph', 'config-key', 'set', CLUSTER_UUID_NAME, uid]) if rc: raise RuntimeError("\'ceph config-key set\' failed -" + e) return uid def bytes_pretty_to_raw(byte_count, byte_scale): if byte_scale == 'kB': return byte_count >> 10 if byte_scale == 'MB': return byte_count >> 20 if byte_scale == 'GB': return byte_count >> 30 if byte_scale == 'TB': return byte_count >> 40 if byte_scale == 'PB': return byte_count >> 50 if byte_scale == 'EB': return byte_count >> 60 return byte_count def get_nums(): (rc, o, e) = run_command(['ceph', '-s', '-f', 'json']) if rc: raise RuntimeError("\'ceph -s\' failed - " + e) oj = json.loads(o) num_mons = len(oj['monmap']['mons']) num_osds = int(oj['osdmap']['osdmap']['num_in_osds']) try: num_mdss = oj['fsmap']['in'] except KeyError: num_mdss = 0 pgmap = oj['pgmap'] num_pgs = pgmap['num_pgs'] num_data_bytes = pgmap['data_bytes'] num_bytes_total = pgmap['bytes_total'] (rc, o, e) = run_command(['ceph', 'pg', 'dump', 'pools', '-f', 'json-pretty']) if rc: raise RuntimeError("\'ceph pg dump pools\' failed - " + e) pools = json.loads(o) num_pools = len(pools) num_objs = 0 for p in pools: num_objs += p['stat_sum']['num_objects'] nums = {'num_mons':num_mons, 'num_osds':num_osds, 'num_mdss':num_mdss, 'num_pgs':num_pgs, 'num_data_bytes':num_data_bytes, 'num_bytes_total':num_bytes_total, 'num_pools':num_pools, 'num_objects':num_objs} return nums def get_crush_types(): (rc, o, e) = run_command(['ceph', 'osd', 'crush', 'dump']) if rc: raise RuntimeError("\'ceph osd crush dump\' failed - " + e) crush_dump = json.loads(o) if crush_dump['types'] is None: raise RuntimeError("\'types\' item missing in \'ceph osd crush dump\'") crush_types = {} for t in crush_dump['types']: crush_types[t['type_id']] = t['name'] types_list = [] for bucket in crush_dump['buckets']: types_list.append(bucket['type_id']) crush_map = [] types_counter = Counter(types_list) append = lambda t,c: crush_map.append({'type':t, 'count':c}) for id,count in types_counter.items(): append(crush_types[id], count) if 'devices' in crush_dump: append('devices', len(crush_dump['devices'])) return crush_map def get_osd_dump_info(): (rc, o, e) = run_command(['ceph', 'osd', 'dump', '-f', 'json']) if rc: raise RuntimeError("\'ceph osd dump\' failed - " + e) pool_meta = [] oj = json.loads(o) proc = lambda x: {'id':x['pool'], 'type':x['type'], 'size':x['size']} for p in oj['pools']: pool_meta.append(proc(p)) return oj['created'], pool_meta def get_sysinfo(max_osds): count = 0 osd_metadata_available = False os = {} kern_version = {} kern_description = {} distro = {} cpu = {} arch = {} ceph_version = {} incr = lambda a,k: 1 if k not in a else a[k]+1 while count < max_osds: (rc, o, e) = run_command(['ceph', 'osd', 'metadata', str(count)]) if rc == 0: if not osd_metadata_available: osd_metadata_available = True jmeta = json.loads(o) version = jmeta['ceph_version'].split() cv = version[2] if (len(version) > 3): cv += version[3] ceph_version[cv] = incr(ceph_version, cv) os[jmeta['os']] = incr(os, jmeta['os']) kern_version[jmeta['kernel_version']] = \ incr(kern_version, jmeta['kernel_version']) kern_description[jmeta['kernel_description']] = \ incr(kern_description, jmeta['kernel_description']) try: dstr = jmeta['distro'] + ' ' dstr += jmeta['distro_version'] + ' ' dstr += jmeta['distro_codename'] + ' (' dstr += jmeta['distro_description'] + ')' distro[dstr] = incr(distro, dstr) except KeyError: pass cpu[jmeta['cpu']] = incr(cpu, jmeta['cpu']) arch[jmeta['arch']] = incr(arch, jmeta['arch']) count = count + 1 sysinfo = {} if not osd_metadata_available: print_stderr("'ceph osd metadata' is not available at all") return sysinfo def jsonify(type_count, name, type_name): tmp = [] for k, v in type_count.items(): tmp.append({type_name:k, 'count':v}) sysinfo[name] = tmp jsonify(os, 'os_info', 'os') jsonify(kern_version, 'kernel_versions', 'version') jsonify(kern_description, 'kernel_types', 'type') jsonify(distro, 'distros', 'distro') jsonify(cpu, 'cpus', 'cpu') jsonify(arch, 'cpu_archs', 'arch') jsonify(ceph_version, 'ceph_versions', 'version') return sysinfo def get_ownership_info(): (rc, o, e) = run_command(['ceph', 'config-key', 'get', CLUSTER_OWNERSHIP_NAME]) if rc: return {} return ast.literal_eval(o) def output_json(): out = {} url = None out['uuid'] = get_uuid() nums = get_nums() num_osds = int(nums['num_osds']) out['components_count'] = nums out['crush_types'] = get_crush_types() out['cluster_creation_date'], out['pool_metadata'] = get_osd_dump_info() out['sysinfo'] = get_sysinfo(num_osds) owner = get_ownership_info() if owner is not None: out['ownership'] = owner if 'url' in owner: url = owner.pop('url') return json.dumps(out, indent=2, separators=(',', ': ')), url def describe_usage(): print_stderr("Usage:") print_stderr("======") print_stderr() print_stderr(sys.argv[0] + " [-v|--verbose] [ [command-options]]") print_stderr() print_stderr("without any option, shows the data to be published and do nothing") print_stderr() print_stderr("-v|--verbose: toggle verbose output on stdout") print_stderr() print_stderr("commands:") print_stderr("publish - publish the brag report to the server") print_stderr("update-metadata - Update") print_stderr(" ownership information for bragging") print_stderr("clear-metadata - Clear information set by update-metadata") print_stderr("unpublish --yes-i-am-shy - delete the brag report from the server") print_stderr() print_stderr("update-metadata options:") print_stderr("--name= - Name of the cluster") print_stderr("--organization= - Name of the organization") print_stderr("--email= - Email contact address") print_stderr("--description= - Reporting use-case") print_stderr("--url= - The URL that is used to publish and unpublish") print_stderr() def update_metadata(): info = {} possibles = ['name', 'organization', 'email', 'description', 'url'] #get the existing values info = get_ownership_info(); for index in range(2, len(sys.argv)): mo = re.search("--(\S+)=(.*)", sys.argv[index]) if not mo: describe_usage() return 22 k = mo.group(1) v = mo.group(2) if k in possibles: info[k] = v else: print_stderr("Unexpect option --" + k) describe_usage() return 22 (rc, o, e) = run_command(['ceph', 'config-key', 'put', CLUSTER_OWNERSHIP_NAME, str(info)]) return rc def clear_metadata(): (rc, o, e) = run_command(['ceph', 'config-key', 'del', CLUSTER_OWNERSHIP_NAME]) return rc def publish(): data, url = output_json() if url is None: print_stderr("Cannot publish until a URL is set using update-metadata") return 1 if verbose: print_stderr("PUT " + str(url) + " : " + str(data)) req = requests.put(url, data=data) if req.status_code != 201: print_stderr("Failed to publish, server responded with code " + str(req.status_code)) print_stderr(req.text) return 1 return 0 def unpublish(): if len(sys.argv) <= 2 or sys.argv[2] != '--yes-i-am-shy': print_stderr("unpublish should be followed by --yes-i-am-shy") return 22 fail = False owner = get_ownership_info() if owner is None: fail = True try: url = owner['url'] except KeyError: fail = True if fail: print_stderr("URL is not updated yet") return 1 uuid = get_uuid() params = {'uuid':uuid} req = requests.delete(url, params=params) if req.status_code != 200: print_stderr("Failed to unpublish, server responsed with code " + str(req.status_code)) return 1 return 0 def main(): if len(sys.argv) > 1 and ( sys.argv[1] == '--verbose' or sys.argv[1] == '-v' ): global verbose verbose = True sys.argv.pop(1) if len(sys.argv) == 1: print(output_json()[0]) return 0 if sys.argv[1] == 'update-metadata': return update_metadata() elif sys.argv[1] == 'clear-metadata': return clear_metadata() elif sys.argv[1] == 'publish': return publish() elif sys.argv[1] == 'unpublish': return unpublish() else: describe_usage() return 22 if __name__ == '__main__': sys.exit(main())