#!/usr/bin/env ruby # For use with OpenShift Enterprise 1.0 and 1.1 # # This can be used as a script or library on an OpenShift broker host. # As a script, the default output is for command-line viewing, or choose # --format tsv for something you can analyze in your favorite spreadsheet, # or json/xml/yaml to process all the data in your tool of choice. # Run with the -h flag to view options. # # To use as a library, do the following in irb or your ruby script: # load 'oo-stats' # stats = OOStats.new # stats.gather_statistics # # ... now stats.results gives you a hash with all the data gathered. # You can also use the set_option and set_columns methods to # customize the output you get from stats.display_results. # # One serious flaw should be noted: for historical reasons, the data # coming from the node hosts about gears excludes gears that don't # have a git repository, e.g. database gears. So you will often see # a discrepancy in counts from nodes vs Mongo. Also there's no separate # count for stopped gears vs. idled gears. These problems will be # remedied with changes to the facts gathered on node hosts. #-- # Copyright 2013 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #++ require 'rubygems' require 'time' class OOStats # called when OOStats.new is called def initialize(options = nil) @options = options || @options || { # default if none given :wait => 2, :format => :text, } @time = {} @time[:load_broker_environment] = time_msecs { load_broker_rails_env } set_initial_columns end # Available options are essentially the same as script options # e.g. set_option :format => :xml def set_option(options) @options.merge! options end # set the columns displayed for text/tsv reports def set_columns(columns_for_table_hash) # text_tableize or tsv_tableize use these. If the column list they get # is nil (because it wasn't specified) they auto-generate columns for you. # See set_initial_columns below for example usage. @columns_for ||= {} @columns_for.merge! columns_for_table_hash end # these are the column lists you get by default def set_initial_columns set_columns :profile_summary => %w{district_count node_count nodes_active nodes_inactive district_capacity dist_avail_capacity dist_avail_uids total_gears total_active_gears effective_available_gears lowest_active_capacity_pct highest_active_capacity_pct avg_active_capacity_pct total_gears_in_db_records total_apps }.map{|k| k.to_sym}, :district_table => %w{name node_count dist_avail_capacity total_active_gears effective_available_gears avg_active_capacity_pct }.map{|k| k.to_sym}, :district_summary => %w{profile node_count nodes_active nodes_inactive district_capacity dist_avail_capacity dist_avail_uids total_gears total_active_gears effective_available_gears lowest_active_capacity_pct highest_active_capacity_pct avg_active_capacity_pct }.map{|k| k.to_sym}, :node_table => %w{name total_gears max_gears capacity active_gears max_active_gears active_capacity }.map{|k| k.to_sym} end # use to time an operation in milliseconds def time_msecs start_time = (Time.now.to_f * 1000).to_i yield return (Time.now.to_f * 1000).to_i - start_time end # gather all statistics and analyze def gather_statistics # read method comments about the structures they return @time[:get_node_entries] = time_msecs { @entry_for_node = get_node_entries } @time[:get_district_entries] = time_msecs { @entry_for_district = get_district_entries } if @options[:db_stats] # don't gather this data unless requested with --db @time[:get_db_stats] = time_msecs do @count_all, @count_for_profile, @count_for_user = get_db_stats end end @time[:summarize_districts] = time_msecs do @summary_for_district = summarize_districts(@entry_for_district, @entry_for_node) end @time[:summarize_profiles] = time_msecs do @summary_for_profile = summarize_profiles(@summary_for_district, @count_for_profile) end @count_all ||= {} # db count may not have occurred @count_all[:nodes] = @entry_for_node.size @count_all[:districts] = @entry_for_district.size @count_all[:profiles] = @summary_for_profile.size return @time end # Bundle up the statistics results in a hash def results r = { :timings_msecs => @time, #timing hash :node_entries => @entry_for_node.values, #array of node hashes from mcollective :district_entries => @entry_for_district.values, #array of district hashes from DB :district_summaries => @summary_for_district.values, #array of district summary hashes :profile_summaries => @summary_for_profile.values, #array of profile summary hashes # remember, unless --db option is present, the db is not scanned for apps/gears/carts # in that case, only data from the nodes and districts are included :count_all => @count_all, #overall summary hash # if db counts were gathered, hash of app/gear/cart counts per profile :db_count_for_profile => @count_for_profile, # if db counts were gathered, array of users with app/gear counts :db_count_per_user => @count_for_user ? @count_for_user.values : nil, } end # Print results to stdout according to current options def display_results r = results case @options[:format] when :yaml puts r.to_yaml when :xml puts r.to_xml when :json puts r.to_json when :tsv display_results_tsv r else # :text display_results_text r end nil end # Load the broker rails environment so we can leverage its tools def load_broker_rails_env begin require "/var/www/openshift/broker/config/environment" # Disable analytics for admin scripts Rails.configuration.analytics[:enabled] = false # Wait this long for answers from node hosts Rails.configuration.msg_broker[:rpc_options][:disctimeout] = @options[:wait] rescue Exception => e puts <<-"FAIL" Broker application failed to load; aborting. The error was: #{e.message} FAIL exit 1 end end # get the node statistics by querying the facts on every node def get_node_entries entry_for_node = {} # Which comes out looking like: # { # "node1.example.com" => { # :id => "node1.example.com", # hostname # :name => "node1", # for short # :node_profile => "small", # node/gear profile # :district_uuid => "2dfca730b863428da9af176160138651", # # or "NONE" if not in a district # :district_active => true, # node marked as active in district? # :total_gears => 12, # :active_gears => 12, # :inactive_gears => 0, # # keep in mind below we are actually talking about gears not apps # :max_apps => 100, # :capacity => 12.0, # percentage of max_apps consumed # :max_active_apps => 100, # :active_capacity => 12.0, # percentage of max_active_apps consumed # # copies of the max_ settings apps=>gears # :max_gears => 100, # :max_active_gears => 100, # }, # "node2.example.com" => ... # } OpenShift::MCollectiveApplicationContainerProxy.rpc_exec('rpcutil') do |client| client.inventory do |response| facts = response[:body][:data][:facts] host = response[:senderid] node = {} # convert from strings to relevant values %w{node_profile district_uuid}.each {|fact| node[fact.to_sym] = facts[fact]} node[:district_active] = facts['district_active'] == 'true' ? true : false %w{max_apps max_active_apps}.each {|fact| node[fact.to_sym] = facts[fact].to_i} node[:max_gears] = node[:max_apps] node[:max_active_gears] = node[:max_active_apps] %w{capacity active_capacity}.each {|fact| node[fact.to_sym] = facts[fact].to_f} # make necessary calculations node[:active_gears] = (node[:max_active_apps] * node[:active_capacity] / 100).round node[:total_gears] = (node[:max_apps] * node[:capacity] / 100).round node[:inactive_gears] = node[:total_gears] - node[:active_gears] # record that hash for this node host node[:id] = host node[:name] = host.split('.')[0] entry_for_node[host] = node end end return entry_for_node end # get the district definitions from the DB def get_district_entries entry_for_district = {} # Which looks like: # { # "2dfca730b863428da9af176160138651" => { # :profile => "small", # gear profile (aka "size") # :name => "small_district", #user-friendly name # :uuid => "2dfca730b863428da9af176160138651", #unique ID # :nodes => { # "node1.example.com" => {"active"=>true}, # "node2.example.com" => {"active"=>true} # }, # :district_capacity => 6000, # configured number of gears allowed in this district # :dist_avail_capacity => 5967, # district_capacity minus gears already allocated # :dist_avail_uids => 5967, # number of user ids left in the pool # }, # "6e5d3ccc0bb1456399687c0be51676f8" => ... # } District.find_all.each do |dist| entry_for_district[dist.uuid] = { :profile => dist.node_profile, :name => dist.name, :uuid => dist.uuid, :nodes => district_nodes_clone(dist), :district_capacity => dist.max_capacity, :dist_avail_capacity => dist.available_capacity, :dist_avail_uids => dist.available_uids.length, } end return entry_for_district end # perform a manual clone such that we don't get BSON entries def district_nodes_clone(district) cloned = {} district.server_identities.each {|id,active| cloned[id] = {"active" => active["active"]}} return cloned end def summarize_districts(entry_for_district, entry_for_node) # Returned hash looks like: # { # "2dfca730b863428da9af176160138651" => { # :uuid => "2dfca730b863428da9af176160138651", # unique ID for district # :name => "small_district", # user-friendly name for district # :profile => "small", # gear profile ("size") for district # :node_count => 2, # number of nodes responding in the district # :nodes_active => 1, # number of nodes marked "active" in district # :nodes_inactive => 1, # number of nodes marked inactive (not open for gear placement) # # N.B. un-districted nodes are always considered inactive, though they can # # have gears placed if there are no districts with capacity for the profile. # # # the following are *district* capacity numbers: # :district_capacity => 4000, # configured number of gears allowed in this district # :dist_avail_capacity => 3967, # district_capacity minus gears already allocated # :dist_avail_uids => 5967, # number of user ids left in the district uid pool # # N.B. these are set to 0 for "NONE" districts (undistricted nodes) # # # the following are capacity numbers according to responding nodes: # :total_gears => 27, # gears created on nodes # :total_active_gears => 27, # gears which are active (not stopped/idled) # :available_active_gears => 173, # how many more active gears the nodes will support # # N.B. currently gear numbers are skewed lower than reality because the # # node only counts the ones that have git repos (e.g. DB gears are excluded) # :effective_available_gears => 173, # minimum of available_active_gears and dist_avail_capacity # # # min/max/average percent usage of active gear capacity on nodes in this district: # :lowest_active_capacity_pct => 12.0, # :highest_active_capacity_pct => 15.0, # :avg_active_capacity_pct => 13.5, # # :nodes=> [ array of entry_for_node values that are members of this district ] # # :missing_nodes => [ ids of node hosts for this district that did not respond ] # }, # "6e5d3ccc0bb1456399687c0be51676f8" => { ... }, # ... # } # these are initial values, will accumulate as we go starter_stats = Hash[ %w[ node_count nodes_active nodes_inactive available_active_gears effective_available_gears total_active_gears total_gears avg_active_capacity_pct ].collect {|key| [key.to_sym, 0]}] # may need a unique "NONE" district per profile for nodes that are not in a district none_district = Hash.new do |h,profile| h[profile] = { :name => "(NONE)", :uuid => "NONE profile=#{profile}", :profile => profile, :district_capacity => 0, :dist_avail_capacity => 0, :dist_avail_uids => 0, :nodes => [], :missing_nodes => {}, }.merge starter_stats end # hash to store the summaries per district summary_for_district = {} entry_for_district.each do |uuid,dist| summary_for_district[uuid] = dist.merge(starter_stats). merge(:missing_nodes => dist[:nodes].clone, :nodes => []) end # We will drive everything according to the nodes that responded. # There may be some that didn't respond, which won't be included. entry_for_node.each do |id,node| sum = summary_for_district[node[:district_uuid]] || none_district[node[:node_profile]] sum[:nodes] << node sum[:missing_nodes].delete id # responded, so not missing sum[:node_count] += 1 sum[:nodes_active] += 1 if node[:district_active] sum[:nodes_inactive] += 1 if !node[:district_active] sum[:total_active_gears] += node[:active_gears] sum[:total_gears] += node[:total_gears] sum[:avg_active_capacity_pct] += node[:active_capacity] sum[:available_active_gears] += node[:max_active_apps] - node[:active_gears] end none_district.values.each {|sum| summary_for_district[sum[:uuid]] = sum} summary_for_district.each do |uuid,sum| sum[:avg_active_capacity_pct] /= sum[:node_count] if sum[:node_count] > 0 sum[:lowest_active_capacity_pct] = sum[:nodes].map{|node| node[:active_capacity]}.min || 0.0 sum[:highest_active_capacity_pct] = sum[:nodes].map{|node| node[:active_capacity]}.max || 0.0 sum[:effective_available_gears] = [sum[:available_active_gears], sum[:dist_avail_capacity]].min # convert :missing nodes to array sum[:missing_nodes] = sum[:missing_nodes].keys end return summary_for_district end def summarize_profiles(summary_for_district, count_for_profile) # Returned hash looks like: (a lot like the district summaries) # { # "small" => { # :profile => "small", # :districts => [ array of summaries from summary_of_district that have this profile ], # :district_count => 1, # number of districts with this profile (may include 1 "NONE" district) # :node_count => 2, # number of nodes responding with this profile # :nodes_active => 1, # number of nodes marked "active" with this profile # :nodes_inactive => 1, # number of nodes marked inactive (not open for gear placement) # # N.B. un-districted nodes are always considered inactive, though they can # # have gears placed if there are no districts with capacity for the profile. # :missing_nodes => [ ids of districted node hosts for this profile that did not respond ] # # # the following are summarized *district* capacity numbers: # :district_capacity => 4000, # configured number of gears allowed in districts # :dist_avail_capacity => 3967, # district_capacity minus gears already allocated # :dist_avail_uids => 5967, # number of user ids left in the districts' uid pool # # N.B. these will be 0 for "NONE" districts (undistricted nodes) # # # the following are usage/capacity numbers according to responding nodes: # :total_gears => 27, # gears created on nodes # :total_active_gears => 27, # gears which are active (not stopped/idled) # :available_active_gears => 173, # how many more active gears the nodes will support # # N.B. currently gear numbers are skewed lower than reality because the # # node only counts the ones that have git repos (e.g. DB gears are excluded) # :effective_available_gears => 173, # minimum of available_active_gears and dist_avail_capacity # # # the following are usage numbers according to the DB, if collected # :total_db_gears => 27, # gears recorded with this profile in the DB # :total_apps => 20, # apps recorded with this profile in the DB # :cartridges => { cartridge counts as in get_db_stats } # :cartridges_short => { cartridge short name counts as in get_db_stats } # # # min/max/average percent usage of active gear capacity on nodes in this profile: # :lowest_active_capacity_pct => 12.0, # :highest_active_capacity_pct => 15.0, # :avg_active_capacity_pct => 13.5, # }, # "medium" => {...}, # ... # } # these values will accumulate as we go starter_stats = Hash[ %w[ node_count district_count nodes_active nodes_inactive available_active_gears effective_available_gears total_active_gears total_gears avg_active_capacity_pct district_capacity dist_avail_capacity dist_avail_uids ].collect {|key| [key.to_sym, 0]}] summary_for_profile = Hash.new do |sum,p| sum[p] = { # auto-created hash per profile :profile => p, :districts => [], :missing_nodes => [], }.merge starter_stats end summary_for_district.each do |uuid,dist| sum = summary_for_profile[dist[:profile]] sum[:districts] << dist sum[:district_count] += 1 [ :node_count, :nodes_active, :nodes_inactive, :missing_nodes, :available_active_gears, :total_active_gears, :total_gears, :district_capacity, :dist_avail_capacity, :dist_avail_uids ].each {|k| sum[k] += dist[k]} sum[:avg_active_capacity_pct] += dist[:avg_active_capacity_pct] * dist[:node_count] end summary_for_profile.each do |profile,sum| sum[:avg_active_capacity_pct] /= sum[:node_count] if sum[:node_count] > 0 sum[:lowest_active_capacity_pct] = sum[:districts].map{|d| d[:lowest_active_capacity_pct]}.min || 0.0 sum[:highest_active_capacity_pct] = sum[:districts].map{|d| d[:highest_active_capacity_pct]}.max || 0.0 sum[:effective_available_gears] = [sum[:available_active_gears], sum[:dist_avail_capacity]].min if count_for_profile sum[:total_gears_in_db_records] = count_for_profile[profile][:gears] sum[:total_apps] = count_for_profile[profile][:apps] sum[:cartridges] = count_for_profile[profile][:cartridges] sum[:cartridges_short] = count_for_profile[profile][:cartridges_short] end end return summary_for_profile end # get statistics from the DB about users/apps/gears/cartridges def get_db_stats # initialize the things we will count for the entire installation count_all = { :apps => 0, :gears => 0, :cartridges => Hash.new {|h,k| h[k] = 0}, :cartridges_short => Hash.new {|h,k| h[k] = 0}, :users_with_num_apps => Hash.new {|h,k| h[k] = 0}, :users_with_num_gears => Hash.new {|h,k| h[k] = 0}, } # which ends up looking like: # { # :apps => 21, # :gears => 39, # :cartridges=> { # "ruby-1.9" => 5, # "ruby-1.8" => 7, # "perl-5.10" => 7, # "mysql-5.1" => 15, # "haproxy-1.4" => 12, # ... # }, # :cartridges_short=> { # "ruby" => 12, # "perl" => 7, # "mysql" => 15, # "haproxy" => 12, # ... # }, # :users_with_num_apps => { # 1 => 10, # 10 users have 1 app # 2 => 2, # 9 => 1, # ... # }, # :users_with_num_gears => { # 1 => 9, # 9 users have 1 gear # 2 => 2, # 4 => 1, # 11 => 1, # ... # }, # }, count_for_profile = Hash.new do |hash,profile| hash[profile] = { :apps => 0, :gears => 0, :cartridges => Hash.new {|h,k| h[k] = 0}, :cartridges_short => Hash.new {|h,k| h[k] = 0}, } end # counts broken out by profile, which ends up looking like: # { # "small" => { hash like above without :users_with_num_* }, # "medium" => { ... }, # ... # } count_for_user = Hash.new do |hash,user| hash[user] = Hash.new {|h,k| h[k] = 0} end # which ends up looking like: # { # "user1" => {:login => "user1", :small_gears =>33, :small_apps =>18}, # "user2" => {:login => "user2", :medium_apps =>1, :medium_gears =>2, :small_gears =>4, :small_apps =>2} # ... # } # # this is for the pre-model-refactor DB - will need adjustment after that's done query = {"apps.group_instances.gears.0" => {"$exists" => true}} options = {:fields => %w[ login apps.node_profile apps.group_instances.gears.node_profile apps.group_instances.gears.configured_components ], :timeout => false} OpenShift::DataStore.instance.user_collection.find(query, options) do |mcursor| mcursor.each do |user| count_of = count_for_user[user['login']] count_of[:login] = user['login'] user['apps'].each do |app| app_profile = app['node_profile'] count_of["#{app_profile}_apps".to_sym] += 1 count_all[:apps] += 1 count_for_profile[app_profile][:apps] += 1 count_of[:apps] += 1 app['group_instances'].each do |group| # group of same-configured gears group['gears'].each do |gear| gear_profile = gear['node_profile'] # for now, same as app profile, but... count_of["#{gear_profile}_gears".to_sym] += 1 count_of[:gears] += 1 count_for_profile[gear_profile][:gears] += 1 count_all[:gears] += 1 gear['configured_components'].each do |cart_desc| # e.g. extract from "@@app/cart-mysql-5.1/comp-mysql-server" => "mysql-5.1" cart = cart_desc.gsub %r{ ^.* \b cart-([^/]+) (/|$) .* $ }x, '\1' # Now pull the shorter name out without the version number # e.g. extract from "@@app/cart- mysql - 5.1 /comp-mysql-server" => "mysql" short = cart_desc.gsub %r{ ^.* \b cart-([-\w]+)-[\d.]+ (/|$) .* $ }x, '\1' # If either regex doesn't match then we are left with the whole description to ponder. count_for_profile[gear_profile][:cartridges][cart] += 1 count_for_profile[gear_profile][:cartridges_short][short] += 1 count_all[:cartridges][cart] += 1 count_all[:cartridges_short][short] += 1 end end end end count_all[:users_with_num_apps][ count_of[:apps] ] += 1 count_all[:users_with_num_gears][count_of[:gears]] += 1 end end return count_all, count_for_profile, count_for_user end def outline(str) puts '-' * str.length puts str puts '-' * str.length end def display_results_text(res=results) # first, display the per-user usage if available if @options[:db_stats] if users = res[:db_count_per_user] outline "Usage of apps and gears by user:" # columns are the keys (:small_apps :small_gears etc) with :login at front text_tableize users, @columns_for[:user_table] puts "\n\n" end if users_for_count = res[:count_all][:users_with_num_apps] outline "Distribution of apps usage (app count : users) :" text_legendize users_for_count puts "\n\n" end if users_for_count = res[:count_all][:users_with_num_gears] outline "Distribution of gears usage (gear count : users) :" text_legendize users_for_count puts "\n\n" end end unless @options[:only] if cartridges = res[:count_all][:cartridges] # display system-wide cartridge usage outline "System-wide usage of cartridges:" cartridges.sort_by {|cart,count| cart}.each { |duple| puts " #{duple[0]} : #{duple[1]}" } puts "\n\n" end # display systems info case @options[:level] when :profile res[:profile_summaries].sort_by {|a| a[:profile]}.each do |profile| # print summary for that profile outline "Profile '#{profile[:profile]}' summary:" text_legendize profile, @columns_for[:profile_summary] puts "\nDistricts:" text_tableize profile[:districts].sort_by {|a| a[:name]}, @columns_for[:district_table] unless profile[:missing_nodes].empty? puts "\nWARNING: the following districted node(s) in this profile DID NOT respond:" puts profile[:missing_nodes].join ", " end puts "\n\n" end when :district res[:district_summaries].sort_by {|a| a[:profile] + a[:name] }.each do |district| # print summary for that district outline "District '#{district[:name]}' summary:" text_legendize district, @columns_for[:district_summary] puts "\nNodes:" text_tableize district[:nodes].sort_by {|a| a[:name]}, @columns_for[:node_table] unless district[:missing_nodes].empty? puts "\nWARNING: the following node(s) in this district DID NOT respond:" puts district[:missing_nodes].join ", " end puts "\n\n" end else # :node missing_nodes = [] res[:district_summaries].sort_by {|a| a[:profile] + a[:name] }.each do |district| missing_nodes += district[:missing_nodes] puts "Nodes for district '#{district[:name]}' with profile '#{district[:profile]}':" text_tableize district[:nodes].sort_by {|a| a[:name]}, @columns_for[:node_table] puts "\n\n" end unless missing_nodes.empty? puts "\nWARNING: the following districted node(s) DID NOT respond:" puts missing_nodes.join ", " puts end end outline "Summary for all systems:" text_legendize res[:count_all], @columns_for[:count_all_legend] puts "\n\n" end end # display tab-separated values (spreadsheet friendly) def display_results_tsv(res=results) # Right now no column lists have been defined, so all are generated on the fly. # Use "set_columns" to create column orderings for these if desired puts "Resource usage summary:" tsv_legendize res[:count_all], @columns_for[:tsv_db_count_all] missing_nodes = res[:profile_summaries].inject([]) {|a,p| a += p[:missing_nodes]} unless missing_nodes.empty? puts "\n\nWARNING: these districted nodes DID NOT respond:" missing_nodes.each {|n| puts n} end puts "\n\nPer-gear-profile usage summary:" tsv_tableize res[:profile_summaries].sort_by {|h| h[:profile]}, @columns_for[:tsv_profile_summaries] puts "\n\nPer-district usage summary:" tsv_tableize res[:district_summaries].sort_by {|h| "#{h[:profile]}-#{h[:name]}"}, @columns_for[:tsv_district_summaries] puts "\n\nPer-node usage summary:" tsv_tableize res[:node_entries].sort_by {|h| "#{h[:node_profile]}-#{h[:name]}"}, @columns_for[:tsv_node_entries] if res[:db_count_per_user] # --db option specified puts "\n\nPer-user usage summary:" tsv_tableize res[:db_count_per_user], @columns_for[:tsv_db_count_for_user] end if users_for_count = res[:count_all][:users_with_num_apps] puts "\n\nDistribution of apps usage (app count : users) :" tsv_legendize users_for_count end if users_for_count = res[:count_all][:users_with_num_gears] puts "\n\nDistribution of gears usage (gear count : users) :" tsv_legendize users_for_count end if res[:count_all][:cartridges] # --db option specified puts "\n\nCartridge usage summary:" tsv_legendize res[:count_all][:cartridges], @columns_for[:tsv_cartridge_legend] # also break them down by profile res[:profile_summaries].sort_by {|h| h[:profile]}.each do |profile| puts "\n\nCartridge usage summary for profile '#{profile[:profile]}':" tsv_legendize profile[:cartridges], @columns_for[:tsv_cartridge_legend] end end puts "\n\nTimings for gathering this information (milliseconds):" tsv_legendize res[:timings_msecs], @columns_for[:tsv_timings] end def auto_column_list(hashes) # automatically build the column list using those keys which have "simple" values cols = {} hashes.each do |hash| hash.each {|key,value| cols[key] = true if [String, Symbol, Integer, Fixnum].include? value.class} end cols = cols.keys.sort # if you wanted a specific order you wouldn't be here # OK let's make an exception to bring a few identifier columns to the front [:name, :profile, :node_profile, :login].each {|col| cols.include?(col) and cols = [col] + (cols - [col])} return cols end def tsv_legendize(hash, order=nil) order ||= auto_column_list([hash]) order.each {|key| puts "#{key.to_s.humanize}\t#{hash[key]}" } end def tsv_tableize(hashes, cols=nil) cols ||= auto_column_list(hashes) # print columns puts cols.map {|col| col.to_s.humanize}.join "\t" # print all records hashes.each {|hash| puts cols.map {|col| hash[col]}.join "\t"} end # display data in formatted key:value "legend" def text_legendize(hash, order=nil) order ||= auto_column_list([hash]) humanized = {} max_size = [] order.each {|key| max_size << (humanized[key] = key.to_s.humanize).length } max_size = max_size.max.to_s order.each {|key| printf " %#{max_size}s : %s\n", humanized[key], hash[key].to_s unless hash[key].nil? } end # display data in a nicely formatted table def text_tableize(rows, cols=nil) cols ||= auto_column_list(rows) show_for_col = {} # figure out how to display each column cols.each do |col| name = col.to_s.camelize. gsub(/Total/, '#'). gsub(/Available/, 'Avail') size = (rows.map {|row| row[col].to_s.length} + [name.length]).sort[-1] show_for_col[col] = {:name => name, :size => size} end # print table header and divider puts cols.map {|col| c=show_for_col[col]; sprintf "%#{c[:size]}s", c[:name] }.join ' ' puts cols.map {|col| '-' * show_for_col[col][:size] }.join ' ' # print table contents rows.each do |row| puts cols.map {|col| sprintf "%#{show_for_col[col][:size]}s", row[col].to_s}.join ' ' end end end #class OOStats ############ EXECUTION ########## # # If this script is running directly, gather the information and display. # In a different context (e.g. irb) just load the class and don't run anything. if __FILE__ == $0 # # Options parsing... # require 'optparse' options = { :wait => 2, :level => :profile, :format => :text, } optparse = OptionParser.new { |opts| opts.banner = <<-"USAGE" #{$0}: Output usage statistics from this installation. Usage: #{$0} [switches] Example: #{$0} # standard text display Example: #{$0} --wait 10 # for slower node responses Example: #{$0} --format xml Switches: USAGE opts.on('-w','--wait SECONDS', Integer, < e puts e.message puts "\n" + optparse.to_s puts "\n" + e.message exit 1 end # # execution # o = OOStats.new(options) o.gather_statistics o.display_results end