Skip to content

Instantly share code, notes, and snippets.

@hassenius
Created May 21, 2014 17:11
Show Gist options
  • Save hassenius/7f8ca7027034cecddcc4 to your computer and use it in GitHub Desktop.
Save hassenius/7f8ca7027034cecddcc4 to your computer and use it in GitHub Desktop.
Find Softlayer Monthly Bare Metal Servers based on cpu,memory and/or disk requirements
#!/usr/bin/ruby
################################################################################
# sl_config_finder.rb
# ©Copyright IBM Corporation 2014.
#
# LICENSE: MIT (http://opensource.org/licenses/MIT)
################################################################################
require 'rubygems'
require 'softlayer_api'
require 'optparse'
$SL_API_USERNAME = " SET ME PLEASE "
$SL_API_KEY = " PLEASE SET ME TOO "
class Compute
@@compute_catalog = Array.new
@@ram_index = Hash.new { |hash, capacity| hash[capacity] = [] }
@@cpu_core_index = Hash.new { |hash, cores| hash[cores] = [] }
@@disk_index = Hash.new { |hash, disk_capacity| hash[disk_capacity] = [] }
@@package_index = Hash.new
@possible_packages = Array.new
@priority_order = Array.new
@desired_cpu_cores = 0
@cpu_cores = 0
@desired_cpu_type = nil
@cpu_type = nil
@desired_ram = 0
@ram = 0
@desired_disks = 0
@disks = 0
@desired_disk_size = 0
@disk_size = 0
@desired_disk_type = nil
@disk_type = nil
attr_accessor(:cpu_cores, :desired_cpu_cores, :ram, :desired_ram, :disks, :desired_disks, :possible_packages)
def initialize(args = {})
@desired_cpu_cores = args[:cpu_cores]
@desired_cpu_type = args[:cpu_type] # => Can be virtual, physical or a GHz value
@desired_ram = args[:ram].to_i
@desired_disks = args[:min_disks] # => Number of spindels desired. Typically only relevant for dedicated machines
@desired_disk_type = args[:disk_type] # getPrivateBlockDeviceTemplateGroups # => Typically most relevant for dedicated machines -- SSD, SAS, or SATA
@priority_order = args[:priority] # => Array of order of priority for resources from most to least important. i.e. [cpu,memory,disk]
@bias = args[:bias] # => higher/lower/nearest. When looking for resource match, whether to go matching or higher, matching or lower or closest match
# Build the compute catalog if it has not been done already
build_compute_catalog() if @@compute_catalog.empty?
# Cycle through and find closest match for each resource item in order of priority
@priority_order.each { |resource|
case resource
when "cpu"
@cpu_cores = find_package_match(@@cpu_core_index, @desired_cpu_cores, @bias)
when "memory"
# Find closest ram
# TODO: Check that @desired_ram is set
@ram = find_package_match(@@ram_index, @desired_ram, @bias)
when "disk"
# Find closest disk
## This is more complicated as we have more variables: amount of spindles, total size and type
## For now just do amount of spindles
# TODO: Check that @desired_disks is set
@disks = find_package_match(@@disk_index, @desired_disks, "higher") # makes no sense to bias lower with disks
else
puts "You didn't tell me a priority"
end
}
end
def find_candidate(candidates, desired_number, bias)
# Get the closest matching cpu/ram/disk configuration from a list of candidates
## Example from CPU, migth be that there are 2,4,6,8,12,16,24,32,40 core configurations available (candidates)
## Select the best option for the desired number (i.e. 7) based on a set bias (higher, lower or nearest)
## Previous selections (RAM or Disk) will limit the amount of available options available
case bias
when "higher"
return candidates.select{|item| item.to_i > desired_number.to_i}.min
when "lower"
return candidates.select{|item| item.to_i < desired_number.to_i}.max
when "nearest"
return candidates.min{|a,b| (desired_number-a).abs <=> (desired_number-b).abs }
else
puts "Error: Not sure what #{bias} means"
end
end
def find_package_match(index, desired, bias)
# Create a new sorted array with the list of all the CPU/RAM/disk options
options = index.keys.map(&:to_i).sort
# Start out by finding a candidate match for the requested amount
if index.key?(desired.to_s)
# Exact match available
candidate = desired
else
# find closest match available
candidate = find_candidate(options, desired, bias)
end
# Compile a list of possible packages
if @possible_packages == nil
# This was the first resource type to be checked, all of our packages are potential final matches at this stage
@possible_packages = index[candidate.to_s]
else
# Check if there are any of the package options in the existing @possible_packages also exists in the options we found in this run
## If not select a different option and try again
until ((@possible_packages & index[candidate.to_s]).length != 0)
# Previous candidates have not been a match, delete current candidate from options list
options.delete(candidate)
# find new candidate
candidate = find_candidate(options, desired, bias)
end
# Update the list of possible packages
@possible_packages = index[candidate.to_s] & @possible_packages
end
# Return the selected candidate so the caller knows what option we landed on
return candidate
end
def build_compute_catalog()
mask = "mask[id,name,description,availableStorageUnits,activeServerItems,activeRamItems,type]"
package_types_of_interest = ["BARE_METAL_CPU"]
# Pull down the categories list from SoftLayer
sl_package_type_client = SoftLayer::Service.new("SoftLayer_Product_Package_Type")
package_types = sl_package_type_client.getAllObjects
# Check which categories we're interested in, and add the packages from these categories to the list
package_types.each { |type|
add_packages_to_catalog(type["id"]) if package_types_of_interest.include? type["keyName"]
}
# Now create the indexes we will use later to search for the specific configuration we want
build_indexes
end
def add_packages_to_catalog(package_type_id)
mask = "mask[id,name,description,availableStorageUnits,activeServerItems[id,description,totalPhysicalCoreCount],activeRamItems[id,description,capacity],type]"
# Pull down the full list
sl_packages = SoftLayer::Service.new("SoftLayer_Product_Package_Type")
packages = sl_packages.object_mask(mask).object_with_id(package_type_id).getPackages()
@@compute_catalog.concat(packages)
end
def build_indexes()
# Create neccessary indexes (Hashes) for lookups
@@compute_catalog.each_index { |index|
# Create a Package index with packageID as key
@@package_index[@@compute_catalog[index]["id"]] = index
# Create a CPU Index with coreCount as Key and packageID as array of values
@@compute_catalog[index]["activeServerItems"].each { |coreItem|
@@cpu_core_index[coreItem["totalPhysicalCoreCount"].to_s] << @@compute_catalog[index]["id"]
}
# Create a RAM index with memory size as key and packageID as array of values
@@compute_catalog[index]["activeRamItems"].each { |ramItem|
@@ram_index[ramItem["capacity"].to_s] << @@compute_catalog[index]["id"]
}
# Create a disk index with available disk slots as key and packageId as array of values
@@disk_index[@@compute_catalog[index]["availableStorageUnits"].to_s] << @@compute_catalog[index]["id"]
}
end
def print_result
# Print the packages that match our requirements
puts "Desired versus selected specs"
puts "CPU: desired - #{@desired_cpu_cores} == selected - #{@cpu_cores}"
puts "RAM: desired - #{@desired_ram} == selected - #{@ram}"
puts "Disk spindles: desired - #{@desired_disks} == selected - #{@disks}"
puts "=========== Possible packages meeting these requirements =================="
@possible_packages.each { |package|
puts "==========================================================================="
puts "ID: #{package}"
puts "Name: #{@@compute_catalog[@@package_index[package]]["name"]}"
puts "CPU options"
@@compute_catalog[@@package_index[package]]["activeServerItems"].each { |cpu|
puts "Name: #{cpu["description"]}"
puts "ID: #{cpu["id"]} == Cores: #{cpu["totalPhysicalCoreCount"]}"
puts "--------------------------------------------"
}
puts "\nRam options:"
@@compute_catalog[@@package_index[package]]["activeRamItems"].each { |ram|
puts "ID: #{ram["id"]} Name: #{ram["description"]}"
}
puts "==========================================================================="
}
end
end
options = {}
parser = OptionParser.new do |opts|
opts.banner = "Usage: #$0 [options]"
opts.on('-c', '--cpu cores', Integer, 'Amount of CPU cores desired') do |cores|
options[:cores] = cores
end
opts.on('-m', '--memory amount', Integer, 'Amount of memory desired in GB') do |memory|
options[:memory] = memory;
end
opts.on('-d', '--disk amount', Integer, 'Amount of disk spindels desired') do |disk|
options[:disk] = disk;
end
opts.on('-p', '--priority pri1,pri2[,pri3]', Array, 'What order to prioritse resources (cpu, memory, disk) when searching') do |priority|
options[:priority] = priority;
end
opts.on('-b', '--bias {higher, lower, nearest}', ["higher", "lower", "nearest"], 'If exact match for given resource is not found, which way to search') do |bias|
options[:bias] = bias;
end
opts.on('-h', '--help', 'Displays Help') do
puts opts
exit
end
end
begin
parser.parse!
mandatory = [:cores, :memory, :priority, :bias]
missing = mandatory.select{ |param| options[param].nil? }
unless missing.empty?
puts parser
unless options.empty?
puts "\n Missing the following options: #{missing.join(', ')}"
end
exit
end
rescue OptionParser::InvalidOption, OptionParser::MissingArgument, OptionParser::InvalidArgument
puts parser
puts $!.to_s
exit
end
# Disk is the only optional input
if defined?(options[:disk])
item = Compute.new(:ram => options[:memory], :cpu_cores => options[:cores], :min_disks => options[:disk], :priority => options[:priority], :bias => options[:bias])
else
item = Compute.new(:ram => options[:memory], :cpu_cores => options[:cores], :priority => options[:priority], :bias => options[:bias])
end
item.print_result
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment