#!/usr/bin/python # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 #============================================================================== # =========================================================== # ceph_show_pool_distribution - display a list of each pool's # placement groups and the OSDs on which they reside. Uses # the ceph command to get XML data # =========================================================== # Copyright (C) 2017, 2018 Peter Linich #============================================================================== # ======= # License # ======= # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # #============================================================================== # ======= # History # ======= # 03jan2018 Updated Usage to display version option # 02jan2018 Added VERSION string, "-V" option and GPL license # 27dec2017 First version VERSION = "1.0 (27dec2017)" #============================================================================== # ============= # Configuration # ============= DEFAULT_HOST_SUBSTRING_LENGTH = 2 #============================================================================== # ================= # Modules/libraries # ================= import sys import ceph_support as cs #============================================================================== # ==================================== # pgparsed and treeparsed dictionaries # ==================================== # The pgparsed dictionary contents look like this: # # .pg_stats.0.pg_stat.792.acting_primary.0 12 # .pg_stats.0.pg_stat.834.acting.0.osd.2 4 # .pg_stats.0.pg_stat.834.acting.0.osd.0 9 # .pg_stats.0.pg_stat.834.acting.0.osd.1 11 # .pg_stats.0.pg_stat.271.acting.0.osd.2 2 # .pg_stats.0.pg_stat.271.acting.0.osd.1 11 # .pg_stats.0.pg_stat.271.acting.0.osd.0 7 # .pg_stats.0.pg_stat.383.state.0 active+clean # .pg_stats.0.pg_stat.141.pgid.0 25.12 # .pg_stats.0.pg_stat.274.state.0 active+clean # .pg_stats.0.pg_stat.217.acting.0.osd.2 4 # .pg_stats.0.pg_stat.217.acting.0.osd.1 8 # .pg_stats.0.pg_stat.217.acting.0.osd.0 12 # .pg_stats.0.pg_stat.483.up.0.osd.2 11 # .pg_stats.0.pg_stat.483.up.0.osd.1 14 # .pg_stats.0.pg_stat.483.up.0.osd.0 0 # .pg_stats.0.pg_stat.529.acting_primary.0 1 # .pg_stats.0.pg_stat.285.acting.0.osd.1 6 # .pg_stats.0.pg_stat.177.state.0 active+clean # .pg_stats.0.pg_stat.636.up.0.osd.0 1 # .pg_stats.0.pg_stat.636.up.0.osd.2 0 # .pg_stats.0.pg_stat.798.acting_primary.0 10 # .pg_stats.0.pg_stat.639.state.0 active+clean # .pg_stats.0.pg_stat.142.pgid.0 23.1c # .pg_stats.0.pg_stat.341.acting_primary.0 8 # .pg_stats.0.pg_stat.867.up_primary.0 0 # .pg_stats.0.pg_stat.299.acting_primary.0 7 # .pg_stats.0.pg_stat.816.up_primary.0 8 # .pg_stats.0.pg_stat.144.up.0.osd.1 7 # .pg_stats.0.pg_stat.144.up.0.osd.0 8 # .pg_stats.0.pg_stat.87.acting_primary.0 5 # The treeparsed dictionary contents look like this: # # .tree.0.nodes.0.item.46.primary_affinity.0 1 # .tree.0.nodes.0.item.32.id.0 9 # .tree.0.nodes.0.item.27.name.0 osd.12 # .tree.0.nodes.0.item.51.name.0 node1 # .tree.0.nodes.0.item.13.id.0 9 # .tree.0.nodes.0.item.20.type.0 root # .tree.0.nodes.0.item.78.status.0 up # .tree.0.nodes.0.item.65.primary_affinity.0 1 # .tree.0.nodes.0.item.38.crush_weight.0 1.862 # .tree.0.nodes.0.item.74.children.0.child.0 10 # .tree.0.nodes.0.item.74.children.0.child.1 9 # .tree.0.nodes.0.item.29.id.0 0 # .tree.0.nodes.0.item.27.primary_affinity.0 1 # .tree.0.nodes.0.item.49.crush_weight.0 0.594986 # .tree.0.nodes.0.item.19.status.0 up # .tree.0.nodes.0.item.26.crush_weight.0 1.862 # .tree.0.nodes.0.item.66.primary_affinity.0 1 # .tree.0.nodes.0.item.68.status.0 up # .tree.0.nodes.0.item.39.children.0.child.3 -4 # .tree.0.nodes.0.item.21.type.0 host # .tree.0.nodes.0.item.67.type_id.0 1 # .tree.0.nodes.0.item.63.crush_weight.0 0.931 # .tree.0.nodes.0.item.68.id.0 2 # .tree.0.nodes.0.item.19.name.0 osd.7 # .tree.0.nodes.0.item.79.id.0 -6 #============================================================================== # ============================================================= # LoadOSDHostMap - runs the "ceph osd tree" command to generate # and return a dictionary which maps OSD numbers to host names # ============================================================= def LoadOSDHostMap (): # ------------------------------------------------- # Run the external command and parse its XML output # ------------------------------------------------- b = cs.RunCommand (["ceph", "-f", "xml", "osd", "tree"]) treeparsed = cs.ParseXML (b) # $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ # Now iterate through the array looking for host entries # $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ i = 0 # Tree array entry index hostlist = [] # Keep track of examined hosts and only # do each one once osdlookup = {} # Dictionary return product while True: j = str (i) hostkey = ".tree.0.nodes.0.item." + j + ".type.0" # --------------------------------------------------------- # Break out of the loop when we've checked all tree entries # --------------------------------------------------------- if hostkey not in treeparsed: break # ------------------------------------- # Check if current tree entry is a host # ------------------------------------- if treeparsed[hostkey] == "host": hostname = treeparsed[".tree.0.nodes.0.item." + j + ".name.0"] # ---------------------- # Only do each host once # ---------------------- if hostname not in hostlist: # print "Host:", hostname # $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ # Find all host children which are OSDs # $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ c = 0 # Host child index osdkeybase = ".tree.0.nodes.0.item." + j + ".children.0.child." while True: osdkey = osdkeybase + str (c) if osdkey not in treeparsed: break osdnum = treeparsed[osdkey] # -------------------------------------------------- # Add OSD-to-host lookup entry to our return product # -------------------------------------------------- osdlookup[osdnum] = hostname # print hostname, "-->", osdnum # ---------- # Next child # ---------- c += 1 hostlist.append (hostname) # --------------- # Next tree entry # --------------- i += 1 # ------------- # Return result # ------------- return (osdlookup) # ------- # Testing # ------- # LoadOSDHostMap () # exit (0) #============================================================================== # =============================================== # SortPgId - used by sorted() as PG ID comparator # so we can display output usefully sorted by PG # ID # =============================================== def SortPgId (i0, i1): l0 = i0.split (".") l1 = i1.split (".") for i in range (0, len (l0)): n0 = int (l0[i], 16) n1 = int (l1[i], 16) if n0 > n1: return (1) if n0 < n1: return (-1) return (0) #============================================================================== # ===================================================== # MapOSD - maps an OSD number (string) into the form # which will be output (OSD number, host name or a # combination of both) depending on the maphost setting # ===================================================== def MapOSD (osdnum): if maphost < -1: return (osdnum) hostname = osdmap[osdnum] if maphost == -1: return (hostname + "." + osdnum) if maphost == 0: return (hostname) return hostname[0:maphost] #============================================================================== # ============================================================ # GetOSDSet - return a comma-separated list of OSDs associated # with the PG whose index is j, and for the OSD set identified # by set - e.g., "acting", up". # ============================================================ def GetOSDSet (j, set): a = 0 osdset = "" osdnumkeybase = ".pg_stats.0.pg_stat." + j + "." + set + ".0.osd." while True: b = str (a) osdnumkey = osdnumkeybase + b if osdnumkey not in pgparsed: break if a != 0: osdset += "," osdset += MapOSD (pgparsed[osdnumkey]) a += 1 return osdset #============================================================================== # ============================================================ # Usage - display a usage message to, typically, stderr if the # program is badly invoked or the user wants a usage message # ============================================================ def Usage (exitcode = 1): if exitcode == 0: fd = sys.stdout else: fd = sys.stderr print >> fd, "" print >> fd, "Outputs a list of PGs and the OSDs on which they live" print >> fd, "" print >> fd, "Options:" print >> fd, " -m* Display corresponding host names instead of OSD numbers" print >> fd, " -m+ Display host names as well as OSD numbers" print >> fd, " -mn Where n is an integer: display host names instead" print >> fd, " of OSD numbers but only first n characters" print >> fd, " -m Same as -mn where n is [default value] of " + str (DEFAULT_HOST_SUBSTRING_LENGTH) print >> fd, " -V Display version(s)" print >> fd, " -h This message" print >> fd, " -? Also this message" exit (1) #============================================================================== # ====== # main() # ====== # ---------------------------------------- # Process command line arguments. The only # allowable one is "-m..." which activates # OSD-to-hostname mapping which, in turn, # causes "ceph osd tree" to be run to get # the map. See also MapOSD () above # ---------------------------------------- n = len (sys.argv) if n == 1: maphost = -2 elif n == 2: a = sys.argv[1] if a == "-h": Usage (0) if a == "-?": Usage (0) if a == "-V": print "Version = " + VERSION print "ceph_support version = " + cs.VERSION exit (0) if a[0:2] != "-m": Usage () s = a[2:] if s == "": # "-m" with no parameter means use default hostname substring maphost = DEFAULT_HOST_SUBSTRING_LENGTH elif s == "*": # "-m*" means use full hostname maphost = 0 elif s == "+": # "-m+" means use full hostname plus OSD number maphost = -1 else: try: # "-m followed by number means use this as substring length maphost = int (s) except: Usage () if maphost < 1: Usage () # -------------------------------------------------------- # Now we have a valid substring length value, load the map # which'll get used when working out what to display # instead of just OSD numbers # -------------------------------------------------------- osdmap = LoadOSDHostMap () else: Usage () # -------------------------------------------------- # Run the "ceph pg dump pgs_brief" command, parse it # into an array for ease of user later and create # a reverse-lookup table to go from PG ID to array # index # -------------------------------------------------- b = cs.RunCommand (["ceph", "-f", "xml", "pg", "dump", "pgs_brief"]) pgparsed = cs.ParseXML (b) pgreverse = {} i = 0 while True: j = str (i) idkey = ".pg_stats.0.pg_stat." + j + ".pgid.0" if idkey not in pgparsed: break id = pgparsed[idkey] pgreverse[id] = j i += 1 ## ---------------- ## Diagnostic print ## ---------------- #for id in sorted (pgreverse.iterkeys (), SortPgId): # print id, pgreverse[id] # $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ # Create and display the output as a table of rows, one per PG # $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ rows = [] rows.append (["pg", "state", "up", "primary", "acting", "primary"]) for id in sorted (pgreverse.iterkeys (), SortPgId): j = pgreverse[id] r = [ id, pgparsed[".pg_stats.0.pg_stat." + j + ".state.0"], "[" + GetOSDSet (j, "up") + "]", MapOSD (pgparsed[".pg_stats.0.pg_stat." + j + ".up_primary.0"]), "[" + GetOSDSet (j, "acting") + "]", MapOSD (pgparsed[".pg_stats.0.pg_stat." + j + ".acting_primary.0"]) ] rows.append (r) cs.PrintColumnised (rows) # ---- # Done # ---- exit (0) #==============================================================================