crowbar/crowbar-core

View on GitHub
bin/crowbar_machines

Summary

Maintainability
Test Coverage
#!/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