crowbar/crowbar-core

View on GitHub
bin/barclamp_lib.rb

Summary

Maintainability
F
3 days
Test Coverage
#
# 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.
#

require "rubygems"
require "net/http"
require "net/http/digest_auth"
require "uri"
require "inifile"
require "json"
require "getoptlong"

@debug = false
@hostname = ENV["CROWBAR_IP"]
@hostname = "127.0.0.1" unless @hostname
@port = ENV["CROWBAR_PORT"] || 80
@headers = {
  "Accept" => "application/json",
  "Content-Type" => "application/json"
}
@data = ""
@allow_zero_args = false
@timeout = 3600

#
# Parsing options can be added by adding to this list before calling opt_parse
#
@options = [
    [["--help", "-h", GetoptLong::NO_ARGUMENT], "--help or -h - help"],
    [["--username", "-U", GetoptLong::REQUIRED_ARGUMENT], "--username <username> or -U <username>  - specifies the username"],
    [["--password", "-P", GetoptLong::REQUIRED_ARGUMENT], "--password <password> or -P <password>  - specifies the password"],
    [["--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"],
    [["--debug", "-d", GetoptLong::NO_ARGUMENT], "--debug or -d - turns on debugging information"],
    [["--data", GetoptLong::REQUIRED_ARGUMENT], "--data <data> - used by create or edit as data (must be in json format)"],
    [["--file", GetoptLong::REQUIRED_ARGUMENT], "--file <file> - used by create or edit as data when read from a file (must be in json format)"],
    [["--timeout", GetoptLong::REQUIRED_ARGUMENT], "--timeout <seconds> - timeout in seconds for read http reads"]
]

#
# New commands can be added by adding to this list before calling run_command
#
# Proposal is an example of running sub-commands
#
@proposal_commands = {
  "list" => ["proposal_list", "list - show a list of current proposals"],
  "create" => ["proposal_create ARGV.shift", "create <name> - create a proposal"],
  "show" => ["proposal_show ARGV.shift", "show <name> - show a specific proposal"],
  "edit" => ["proposal_edit ARGV.shift", "edit <name> - edit a new proposal"],
  "delete" => ["proposal_delete ARGV.shift", "delete <name> - delete a proposal"],
  "commit" => ["proposal_commit ARGV.shift", "commit <name> - Commit a proposal to active"],
  "dequeue" => ["proposal_dequeue ARGV.shift", "dequeue <name> - Dequeue a proposal to active"]
}

@commands = {
  "help" => ["help", "help - this page"],
  "api_help" => ["api_help", "crowbar API help - help for this barclamp."],
  "list" => ["list", "list - show a list of current configs"],
  "show" => ["show ARGV.shift", "show <name> - show a specific config"],
  "delete" => ["delete ARGV.shift", "delete <name> - delete a config"],
  "proposal" => ["run_sub_command(@proposal_commands, ARGV.shift)", "proposal - Proposal sub-commands", @proposal_commands],
  "elements" => ["elements", "elements - List elements of a #{@barclamp} deploy"],
  "element_node" => ["element_node ARGV.shift", "element_node <name> - List nodes that could be that element"],
  "transition" => ["transition(ARGV.shift,ARGV.shift)", "transition <name> <state> - Transition machine named name to state"]
}

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
end

def get_json(path)
  uri = URI.parse("http://#{@hostname}:#{@port}/crowbar/#{@barclamp}/1.0#{path}")
  res = authenticate(Net::HTTP::Get,uri)

  debug "(g) hostname: #{uri.host}:#{uri.port}"
  debug "(g) request: #{uri.path}"
  debug "(g) return code: #{res.code}"
  debug "(g) return body: #{res.body}"

  return [res.body, res.code.to_i] if res.code.to_i != 200

  struct = JSON.parse(res.body)

  debug "(g) JSON parse structure = #{struct.inspect}"

  return [struct, 200]
end

def post_json(path, data)
  uri = URI.parse("http://#{@hostname}:#{@port}/crowbar/#{@barclamp}/1.0#{path}")
  res = authenticate(Net::HTTP::Post,uri,data)

  debug "(post) hostname: #{uri.host}:#{uri.port}"
  debug "(post) request: #{uri.path}"
  debug "(post) data: #{@data}"
  debug "(post) return code: #{res.code}"
  debug "(post) return body: #{res.body}"

  [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)

  debug "(put) hostname: #{uri.host}:#{uri.port}"
  debug "(put) request: #{uri.path}"
  debug "(put) data: #{@data}"
  debug "(put) return code: #{res.code}"
  debug "(put) return body: #{res.body}"

  [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)

  debug "(d) hostname: #{uri.host}:#{uri.port}"
  debug "(d) request: #{uri.path}"
  debug "(d) return code: #{res.code}"
  debug "(d) return body: #{res.body}"

  [res.body, res.code.to_i]
end

def list
  struct = get_json("/")

  if struct[1] != 200
    ["Failed to talk to service list: #{struct[1]}: #{struct[0]}", 1]
  elsif struct[0].nil? or struct[0].empty?
    ["No current configurations", 0]
  else
    out = ""
    struct[0].sort.each do |name|
      out = out + "\n" if out != ""
      out = out + "#{name}"
    end
    [out, 0]
  end
end

def api_help
  struct=get_json("/help")
  if struct[1] != 200
    ["Failed to talk to service list: #{struct[1]}: #{struct[0]}", 1]
  elsif struct[0].nil? or struct[0].empty?
    ["No help", 0]
  else
    [jj(struct[0]), 0]
  end
end

def show(name)
  usage(-1) if name.nil? or name == ""

  struct = get_json("/#{name}")

  if struct[1] == 200
    ["#{JSON.pretty_generate(struct[0])}", 0]
  elsif struct[1] == 404
    ["No current configuration for #{name}", 1]
  else
    ["Failed to talk to service show: #{struct[1]}: #{struct[0]}", 1]
  end
end

def delete(name)
  usage(-1) if name.nil? or name == ""

  struct = delete_json("/#{name}")

  if struct[1] == 200
    ["Deleted #{name}", 0]
  elsif struct[1] == 404
    ["Delete failed for #{name}: Not Found", 1]
  else
    ["Failed to talk to service delete: #{struct[1]}: #{struct[0]}", 1]
  end
end

def proposal_list
  struct = get_json("/proposals/")

  if struct[1] != 200
    ["Failed to talk to service proposal list: #{struct[1]}: #{struct[0]}", 1]
  elsif struct[0].nil? or struct[0].empty?
    ["No current proposals", 0]
  else
    out = ""
    struct[0].sort.each do |name|
      out = out + "\n" if out != ""
      out = out + "#{name}"
    end
    [out, 0]
  end
end

def proposal_show(name)
  usage(-1) if name.nil? or name == ""

  struct = get_json("/proposals/#{name}")

  if struct[1] == 200
    ["#{JSON.pretty_generate(struct[0])}", 0]
  elsif struct[1] == 404
    ["No current proposal for #{name}", 1]
  else
    ["Failed to talk to service proposal show: #{struct[1]}: #{struct[0]}", 1]
  end
end

def proposal_create(name)
  usage(-1) if name.nil? or name == ""

  @data = "{\"id\":\"#{name}\"}" if @data.nil? or @data == ""

  struct = put_json("/proposals", @data)

  if struct[1] == 200
    ["Created #{name}", 0]
  else
    ["Failed to talk to service proposal create: #{struct[1]}: #{struct[0]}", 1]
  end
end

def proposal_edit(name)
  usage(-1) if name.nil? or name == ""

  if @data.nil? or @data == ""
    struct = get_json("/proposals/#{name}")

    if struct[1] == 200
      require "tempfile"

      file = Tempfile.new("proposal-#{name}")
      begin
        file.write(JSON.pretty_generate(struct[0]))
      ensure
        file.close
      end

      editor = ENV["EDITOR"] or "/usr/bin/vi"
      system("#{editor} #{file.path}")

      begin
        file.open
        #@data = JSON.pretty_generate(file.read)
        @data = JSON.pretty_generate(JSON.parse(file.read))
      ensure
        file.close
        file.unlink
      end
    elsif struct[1] == 404
      ["No current proposal for #{name}", 1]
    else
      ["Failed to talk to service proposal show: #{struct[1]}: #{struct[0]}", 1]
    end
  end

  struct = post_json("/proposals/#{name}", @data)

  if struct[1] == 200
    ["Edited #{name}", 0]
  elsif struct[1] == 404
    ["Failed to edit: #{name} : Not Found", 1]
  elsif struct[1] == 400
    ["Failed to edit: #{name} : Errors in data\n#{struct[0]}", 1]
  else
    ["Failed to talk to service proposal edit: #{struct[1]}: #{struct[0]}", 1]
  end
end

def proposal_delete(name)
  usage(-1) if name.nil? or name == ""

  struct = delete_json("/proposals/#{name}")

  if struct[1] == 200
    ["Deleted #{name}", 0]
  elsif struct[1] == 404
    ["Delete failed for #{name}: Not Found", 1]
  else
    ["Failed to talk to service delete: #{struct[1]}: #{struct[0]}", 1]
  end
end

def proposal_commit(name)
  usage(-1) if name.nil? or name == ""

  struct = post_json("/proposals/commit/#{name}", @data)

  if struct[1] == 200
    ["Committed #{name}", 0]
  elsif struct[1] == 202
    ["Queued #{name} because #{struct[0]}", 0]
  else
    ["Failed to talk to service proposal commit: #{struct[1]}: #{struct[0]}", 1]
  end
end

def proposal_dequeue(name)
  usage(-1) if name.nil? or name == ""

  struct = delete_json("/proposals/dequeue/#{name}")

  if struct[1] == 200
    ["Dequeued #{name}", 0]
  else
    ["Failed to talk to service proposal dequeue: #{struct[1]}: #{struct[0]}", 1]
  end
end

def elements
  struct = get_json("/elements")

  if struct[1] != 200
    ["Failed to talk to service elements: #{struct[1]}: #{struct[0]}", 1]
  elsif struct[0].nil? or struct[0].empty?
    ["No current elements", 1]
  else
    out = ""
    struct[0].each do |name|
      out = out + "\n" if out != ""
      out = out + "#{name}"
    end
    [out, 0]
  end
end

def element_node(element)
  usage(-1) if element.nil? or element == ""

  struct = get_json("/elements/#{element}")

  if struct[1] != 200
    ["Failed to talk to service element_node: #{struct[1]}: #{struct[0]}", 1]
  elsif struct[0].nil? or struct[0].empty?
    ["No nodes for #{element}", 1]
  else
    out = ""
    struct[0].sort.each do |name|
      out = out + "\n" if out != ""
      out = out + "#{name}"
    end
    [out, 0]
  end
end

def transition(name, state)
  usage(-1) if name.nil? or name == ""

  data = {
    "name" => name,
    "state" => state
  }
  struct = post_json("/transition/default", data.to_json)

  if struct[1] == 200
    ["Transitioned #{name}", 0]
  else
    ["Failed to talk to service transition: #{struct[1]}: #{struct[0]}", 1]
  end
end

### Start MAIN ###

def opt_parse
  get_user_password

  standard_opt_parse

  if ARGV.length == 0 and !@allow_zero_args
    usage -1
  end

  check_user_password
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 standard_opt_parse
  sub_options = @options.map { |x| x[0] }
  opts = GetoptLong.new(*sub_options)

  opts.each do |opt, arg|
    if ! parse_standard_opt(opt, arg)
      parse_extra_opt(opt, arg)
    end
  end
end

def parse_standard_opt(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 "--data"
    @data = arg
  when "--timeout"
    @timeout = arg.to_i
  when "--file"
    @data = File.read(arg)
  else
    return false
  end

  return true
end

def parse_extra_opt(opt, arg)
  found = false
  @options.each do |x|
    next unless x[0].include? opt
    x[2].call(opt, arg)
    found = true
  end
  usage(-1) unless found
end

def check_user_password
  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