bin/crowbar_machines
#!/usr/bin/env ruby
#
# Copyright 2011-2013, Dell
# Copyright 2013-2014, SUSE LINUX Products GmbH
#
# 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.
#
$LOAD_PATH.unshift(File.expand_path("../../crowbar_framework/lib", __FILE__))
require "rubygems"
require "net/http"
require "net/http/digest_auth"
require "uri"
require "inifile"
require "json"
require "getoptlong"
require "utils/extended_hash"
@debug = false
@hostname = ENV["CROWBAR_IP"] || "127.0.0.1"
@port = ENV["CROWBAR_PORT"] || 80
@data = "{}"
@allow_zero_args = false
@timeout = 500
@barclamp = "machines"
@hostname = "127.0.0.1" unless @hostname
@port = 80 unless @port
@headers = {
"Accept" => "application/json",
"Content-Type" => "application/json"
}
@options = [
[["--help", "-h", GetoptLong::NO_ARGUMENT], "--help or -h - This page for further help"],
[["--debug", "-d", GetoptLong::NO_ARGUMENT], "--debug or -d - Turns on debugging information"],
[["--username", "-U", GetoptLong::REQUIRED_ARGUMENT], "--username <username> or -U <username> - Specifies the username to use"],
[["--password", "-P", GetoptLong::REQUIRED_ARGUMENT], "--password <password> or -P <password> - Specifies the password to use"],
[["--hostname", "-n", GetoptLong::REQUIRED_ARGUMENT], "--hostname <name or ip> or -n <name or ip> - Specifies the destination server"],
[["--port", "-p", GetoptLong::REQUIRED_ARGUMENT], "--port <port> or -p <port> - Specifies the destination server port"],
[["--timeout", "-t", GetoptLong::REQUIRED_ARGUMENT], "--timeout <seconds> or -t <seconds> - Timeout in seconds for read HTTP requests"]
]
@commands = {
"help" => ["help", "help - This page for further help"],
"list" => ["list", "list - Show a list of current node handles"],
"aliases" => ["aliases", "aliases - Show a list of current node aliases"],
"show" => ["show ARGV.shift, ARGV.shift", "show <name or alias> [arg] - Show a specific config"],
"delete" => ["delete ARGV.shift", "delete <name or alias> - Delete a node"],
"reboot" => ["action \"reboot\", ARGV.shift", "reboot <name or alias> - Reboot a node"],
"shutdown" => ["action \"shutdown\", ARGV.shift", "shutdown <name or alias> - Shutdown a node"],
"poweron" => ["action \"poweron\", ARGV.shift", "poweron <name or alias> - Poweron a node"],
"powercycle" => ["action \"powercycle\", ARGV.shift", "powercycle <name or alias> - Power cycle a node"],
"poweroff" => ["action \"poweroff\", ARGV.shift", "poweroff <name or alias> - Power off a node"],
"identify" => ["action \"identify\", ARGV.shift", "identify <name or alias> - Identify a node"],
"allocate" => ["action \"allocate\", ARGV.shift", "allocate <name or alias> - Allocate a node"],
"reset" => ["action \"reset\", ARGV.shift", "reset <name or alias> - Reset a node"],
"reinstall" => ["action \"reinstall\", ARGV.shift", "reinstall <name or alias> - Reinstall a node"],
"update" => ["action \"update\", ARGV.shift", "update <name or alias> - Hardware update a node"],
"rename" => ["rename ARGV.shift, ARGV.shift", "rename <name or alias> <new_alias> - Rename a node alias"],
"role" => ["role ARGV.shift, ARGV.shift", "role <name or alias> <intended role> - Assign an intended role"],
"api_help" => ["api_help", "api_help - Crowbar API help for this barclamp"]
}
def print_commands(cmds, spacer = " ")
cmds.each do |key, command|
puts "#{spacer}#{command[1]}"
print_commands(command[2], " #{spacer}") if command[0] =~ /run_sub_command\(/
end
end
def usage(rc)
puts "Usage: crowbar #{@barclamp} [options] <subcommands>"
@options.each do |options|
puts " #{options[1]}"
end
print_commands(@commands.sort)
exit rc
end
def help
usage 0
end
def debug(msg)
puts msg if @debug
end
def authenticate(req, uri, data = nil, limit = 10)
uri.user = @username
uri.password = @password
h = Net::HTTP.new uri.host, uri.port
if uri.instance_of? URI::HTTPS
h.verify_mode = OpenSSL::SSL::VERIFY_NONE
h.use_ssl = true
end
h.read_timeout = @timeout
r = req.new uri.request_uri, @headers
r.body = data if data
res = h.request r
debug "(r) hostname: #{uri.host}:#{uri.port}"
debug "(r) request: #{uri.path}"
debug "(r) method: #{req::METHOD}"
debug "(r) return code: #{res.code}"
debug "(r) return body: #{res.body}"
res.each_header do |h, v|
debug "(r) return #{h}: #{v}"
end
if res.header["location"]
uri = URI.parse(res.header["location"])
return authenticate(req, uri, data, limit)
end
if res["www-authenticate"]
digest = Net::HTTP::DigestAuth.new
auth = digest.auth_header uri, res["www-authenticate"], req::METHOD
r = req.new uri.request_uri, @headers
r.body = data if data
r.add_field "Authorization", auth
res = h.request r
debug "(a) hostname: #{uri.host}:#{uri.port}"
debug "(a) request: #{uri.path}"
debug "(a) method: #{req::METHOD}"
debug "(a) return code: #{res.code}"
debug "(a) return body: #{res.body}"
res.each_header do |h, v|
debug "(a) return #{h}: #{v}"
end
end
res
rescue Timeout::Error => e
STDERR.puts "Operation timed out while connecting to service"
exit 1
end
def post_json(path, data)
uri = URI.parse("http://#{@hostname}:#{@port}/crowbar/#{@barclamp}/1.0#{path}")
res = authenticate(Net::HTTP::Post, uri, data)
if @debug
puts "DEBUG: (post) hostname: #{uri.host}:#{uri.port}"
puts "DEBUG: (post) request: #{uri.path}"
puts "DEBUG: (post) data: #{data}"
puts "DEBUG: (post) return code: #{res.code}"
puts "DEBUG: (post) return body: #{res.body}"
end
[res.body, res.code.to_i]
end
def put_json(path, data)
uri = URI.parse("http://#{@hostname}:#{@port}/crowbar/#{@barclamp}/1.0#{path}")
res = authenticate(Net::HTTP::Put, uri, data)
if @debug
puts "DEBUG: (put) hostname: #{uri.host}:#{uri.port}"
puts "DEBUG: (put) request: #{uri.path}"
puts "DEBUG: (put) data: #{data}"
puts "DEBUG: (put) return code: #{res.code}"
puts "DEBUG: (put) return body: #{res.body}"
end
[res.body, res.code.to_i]
end
def delete_json(path)
uri = URI.parse("http://#{@hostname}:#{@port}/crowbar/#{@barclamp}/1.0#{path}")
res = authenticate(Net::HTTP::Delete, uri)
if @debug
puts "DEBUG: (d) hostname: #{uri.host}:#{uri.port}"
puts "DEBUG: (d) request: #{uri.path}"
puts "DEBUG: (d) return code: #{res.code}"
puts "DEBUG: (d) return body: #{res.body}"
end
[res.body, res.code.to_i]
end
def get_json(path)
uri = URI.parse("http://#{@hostname}:#{@port}/crowbar/#{@barclamp}/1.0#{path}")
res = authenticate(Net::HTTP::Get, uri)
if @debug
puts "DEBUG: (g) hostname: #{uri.host}:#{uri.port}"
puts "DEBUG: (g) request: #{uri.path}"
puts "DEBUG: (g) return code: #{res.code}"
puts "DEBUG: (g) return body: #{res.body}"
end
if res.code.to_i == 200
body = JSON.parse(res.body)
unless body.is_a? Array
body = Utils::ExtendedHash.new(body)
end
[body, 200]
else
[res.body, res.code.to_i]
end
end
def result(message, code, error = nil)
if error.nil? or error.empty?
[message, 0]
else
["#{message}: #{error} #{code >= 200 ? "(#{code})": ""}", 1]
end
end
def list
body, status = get_json("/")
case status
when 200
if body
output = body.nodes.map(&:name).sort
result(output.join("\n"), 0)
else
result("No configurations", 0)
end
else
# todo: Should be replaced when get_json gets refactored
body = Utils::ExtendedHash.new(JSON.parse(body))
result("Failed to talk to service index", status, body.error)
end
end
def aliases
body, status = get_json("/")
case status
when 200
if body
names = body.nodes.map(&:name)
aliases = body.nodes.map { |node|
# FIXME: This code is a duplicate of the fallback value in
# Node#alias.
hostname = node.name.split(".")[0]
node.alias == hostname ? "-" : node.alias
}
col_1 = aliases.dup
col_1.push "Alias" if $stdout.isatty # see below
col_width = col_1.map(&:length).max
format = "%-#{col_width}s %s"
output = aliases.zip(names).map { |aliaz, name|
format % [aliaz, name]
}.sort
# Don't pollute a pipe with the header. This allows grep, while | read
# loops etc. to work nicely whilst still keeping it
# human-readable in the interactive case.
if $stdout.isatty
width = output.map(&:length).max
output.unshift("-" * width)
output.unshift(format % ["Alias", "Name"])
end
result(output.join("\n"), 0)
else
result("No configurations", 0)
end
else
# todo: Should be replaced when get_json gets refactored
body = Utils::ExtendedHash.new(JSON.parse(body))
result("Failed to talk to service index", status, body.error)
end
end
def show(name, field = nil)
usage(-1) if name.nil? or name.empty?
body, status = get_json("/#{name}")
case status
when 200
if field.nil?
result(JSON.pretty_generate(body), 0)
else
begin
field.split(".").each do |x|
body = body[x]
end
output = if body.is_a? String
body
else
JSON.pretty_generate(body)
end
result(output, 0)
rescue
result("Key #{field} does not exist on #{name}", 500)
end
end
when 404
result("Failed to find node #{name}", status)
else
# todo: Should be replaced when get_json gets refactored
body = Utils::ExtendedHash.new(JSON.parse(body))
result("Failed to talk to service show", status, body.error)
end
end
def delete(name)
usage(-1) if name.nil? or name.empty?
body, status = delete_json("/#{name}")
case status
when 200
result("Executed delete for #{name}", 0)
when 404
result("Failed to find node #{name}", status)
else
# todo: Should be replaced when get_json gets refactored
body = Utils::ExtendedHash.new(JSON.parse(body))
result("Failed to talk to service delete", status, body.error)
end
end
def action(exec, name, data = {})
usage(-1) if exec.nil? or exec.empty?
usage(-1) if name.nil? or name.empty?
body, status = post_json("/#{exec}/#{name}", data.to_json)
case status
when 200
result("Executed #{exec} for #{name}", 0)
when 404
result("Failed to find node #{name}", status)
else
# todo: Should be replaced when get_json gets refactored
body = Utils::ExtendedHash.new(JSON.parse(body))
result("Failed to talk to service #{exec}", status, body.error)
end
end
def rename(name, update)
usage(-1) if name.nil? or name.empty?
usage(-1) if update.nil? or update.empty?
action("rename", name, { alias: update})
end
def role(name, update)
usage(-1) if name.nil? or name.empty?
usage(-1) if update.nil? or update.empty?
available_roles = %w(
no_role
controller
compute
network
storage
monitoring
)
unless available_roles.include? update
puts "The role have to be one of #{available_roles.join(", ")}"
exit 1
end
action("role", name, { role: update})
end
def api_help
body, status = get_json("/help")
case status
when 200
if body
result(JSON.pretty_generate(body), 0)
else
result("No help available", 0)
end
else
# todo: Should be replaced when get_json gets refactored
body = Utils::ExtendedHash.new(JSON.parse(body))
result("Failed to talk to service help", status, body.error)
end
end
def get_user_password
@username = ENV["CROWBAR_USERNAME"]
@password = ENV["CROWBAR_PASSWORD"]
return unless @username.nil?
[
File.join(ENV["HOME"], ".crowbarrc"),
File.join("/etc", "crowbarrc")
].each do |file|
next unless File.exist?(file)
begin
site = ENV["CROWBAR_ALIAS"] || "default"
config = IniFile.load(file).to_h[site]
@username = config["username"]
@password = config["password"]
rescue IniFile::Error
STDERR.puts "Could not parse config file \"#{file}\""
end
end
end
def opt_parse
get_user_password
sub_options = @options.map { |x| x[0] }
lsub_options = @options.map { |x| [x[0][0], x[2]] }
opts = GetoptLong.new(*sub_options)
opts.each do |opt, arg|
case opt
when "--help"
usage 0
when "--debug"
@debug = true
when "--hostname"
@hostname = arg
when "--username"
@username = arg
when "--password"
@password = arg
when "--port"
@port = arg.to_i
when "--timeout"
@timeout = arg.to_i
else
found = false
lsub_options.each do |x|
next if x[0] != opt
eval x[1]
found = true
end
usage -1 unless found
end
end
if ARGV.length == 0 and !@allow_zero_args
usage -1
end
if @username.nil? or @password.nil?
STDERR.puts "Incomplete credentials, will not be able to authenticate!"
STDERR.puts "Please create a crowbarrc configuration file, " \
"set CROWBAR_USERNAME and CROWBAR_PASSWORD, or use -U and -P"
exit 1
end
end
def run_sub_command(cmds, subcmd)
cmd = cmds[subcmd]
usage -2 if cmd.nil?
eval cmd[0]
end
def run_command
run_sub_command(@commands, ARGV.shift)
end
def main
opt_parse
res = run_command
puts res[0]
exit res[1]
end
main