diff --git a/base/usr/bin/onegate.rb b/base/usr/bin/onegate.rb new file mode 100755 index 0000000..a1a735a --- /dev/null +++ b/base/usr/bin/onegate.rb @@ -0,0 +1,563 @@ +#!/usr/bin/env ruby + +require 'rubygems' +require 'uri' +require 'net/https' +require 'json' +require 'pp' + +############################################################################### +# The CloudClient module contains general functionality to implement a +# Cloud Client +############################################################################### +module CloudClient + + # OpenNebula version + VERSION = '4.8.0' + + # ######################################################################### + # Default location for the authentication file + # ######################################################################### + DEFAULT_AUTH_FILE = ENV["HOME"]+"/.one/one_auth" + + # ######################################################################### + # Gets authorization credentials from ONE_AUTH or default + # auth file. + # + # Raises an error if authorization is not found + # ######################################################################### + def self.get_one_auth + if ENV["ONE_AUTH"] and !ENV["ONE_AUTH"].empty? and + File.file?(ENV["ONE_AUTH"]) + one_auth=File.read(ENV["ONE_AUTH"]).strip.split(':') + elsif File.file?(DEFAULT_AUTH_FILE) + one_auth=File.read(DEFAULT_AUTH_FILE).strip.split(':') + else + raise "No authorization data present" + end + + raise "Authorization data malformed" if one_auth.length < 2 + + one_auth + end + + # ######################################################################### + # Starts an http connection and calls the block provided. SSL flag + # is set if needed. + # ######################################################################### + def self.http_start(url, timeout, &block) + host = nil + port = nil + + if ENV['http_proxy'] + uri_proxy = URI.parse(ENV['http_proxy']) + host = uri_proxy.host + port = uri_proxy.port + end + + http = Net::HTTP::Proxy(host, port).new(url.host, url.port) + + if timeout + http.read_timeout = timeout.to_i + end + + if url.scheme=='https' + http.use_ssl = true + http.verify_mode=OpenSSL::SSL::VERIFY_NONE + end + + begin + res = http.start do |connection| + block.call(connection) + end + rescue Errno::ECONNREFUSED => e + str = "Error connecting to server (#{e.to_s}).\n" + str << "Server: #{url.host}:#{url.port}" + + return CloudClient::Error.new(str,"503") + rescue Errno::ETIMEDOUT => e + str = "Error timeout connecting to server (#{e.to_s}).\n" + str << "Server: #{url.host}:#{url.port}" + + return CloudClient::Error.new(str,"504") + rescue Timeout::Error => e + str = "Error timeout while connected to server (#{e.to_s}).\n" + str << "Server: #{url.host}:#{url.port}" + + return CloudClient::Error.new(str,"504") + rescue SocketError => e + str = "Error timeout while connected to server (#{e.to_s}).\n" + + return CloudClient::Error.new(str,"503") + rescue + return CloudClient::Error.new($!.to_s,"503") + end + + if res.is_a?(Net::HTTPSuccess) + res + else + CloudClient::Error.new(res.body, res.code) + end + end + + # ######################################################################### + # The Error Class represents a generic error in the Cloud Client + # library. It contains a readable representation of the error. + # ######################################################################### + class Error + attr_reader :message + attr_reader :code + + # +message+ a description of the error + def initialize(message=nil, code="500") + @message=message + @code=code + end + + def to_s() + @message + end + end + + # ######################################################################### + # Returns true if the object returned by a method of the OpenNebula + # library is an Error + # ######################################################################### + def self.is_error?(value) + value.class==CloudClient::Error + end +end + +module OneGate + module VirtualMachine + VM_STATE=%w{INIT PENDING HOLD ACTIVE STOPPED SUSPENDED DONE FAILED + POWEROFF UNDEPLOYED} + + LCM_STATE=%w{LCM_INIT PROLOG BOOT RUNNING MIGRATE SAVE_STOP SAVE_SUSPEND + SAVE_MIGRATE PROLOG_MIGRATE PROLOG_RESUME EPILOG_STOP EPILOG + SHUTDOWN CANCEL FAILURE CLEANUP_RESUBMIT UNKNOWN HOTPLUG SHUTDOWN_POWEROFF + BOOT_UNKNOWN BOOT_POWEROFF BOOT_SUSPENDED BOOT_STOPPED CLEANUP_DELETE + HOTPLUG_SNAPSHOT HOTPLUG_NIC HOTPLUG_SAVEAS HOTPLUG_SAVEAS_POWEROFF + HOTPLUG_SAVEAS_SUSPENDED SHUTDOWN_UNDEPLOY EPILOG_UNDEPLOY + PROLOG_UNDEPLOY BOOT_UNDEPLOY} + + SHORT_VM_STATES={ + "INIT" => "init", + "PENDING" => "pend", + "HOLD" => "hold", + "ACTIVE" => "actv", + "STOPPED" => "stop", + "SUSPENDED" => "susp", + "DONE" => "done", + "FAILED" => "fail", + "POWEROFF" => "poff", + "UNDEPLOYED"=> "unde" + } + + SHORT_LCM_STATES={ + "PROLOG" => "prol", + "BOOT" => "boot", + "RUNNING" => "runn", + "MIGRATE" => "migr", + "SAVE_STOP" => "save", + "SAVE_SUSPEND" => "save", + "SAVE_MIGRATE" => "save", + "PROLOG_MIGRATE" => "migr", + "PROLOG_RESUME" => "prol", + "EPILOG_STOP" => "epil", + "EPILOG" => "epil", + "SHUTDOWN" => "shut", + "CANCEL" => "shut", + "FAILURE" => "fail", + "CLEANUP_RESUBMIT" => "clea", + "UNKNOWN" => "unkn", + "HOTPLUG" => "hotp", + "SHUTDOWN_POWEROFF" => "shut", + "BOOT_UNKNOWN" => "boot", + "BOOT_POWEROFF" => "boot", + "BOOT_SUSPENDED" => "boot", + "BOOT_STOPPED" => "boot", + "CLEANUP_DELETE" => "clea", + "HOTPLUG_SNAPSHOT" => "snap", + "HOTPLUG_NIC" => "hotp", + "HOTPLUG_SAVEAS" => "hotp", + "HOTPLUG_SAVEAS_POWEROFF" => "hotp", + "HOTPLUG_SAVEAS_SUSPENDED" => "hotp", + "SHUTDOWN_UNDEPLOY" => "shut", + "EPILOG_UNDEPLOY" => "epil", + "PROLOG_UNDEPLOY" => "prol", + "BOOT_UNDEPLOY" => "boot" + } + + def self.state_to_str(id, lcm_id) + id = id.to_i + state_str = VM_STATE[id] + + if state_str=="ACTIVE" + lcm_id = lcm_id.to_i + return LCM_STATE[lcm_id] + end + + return state_str + end + + def self.print(json_hash) + OneGate.print_header("VM " + json_hash["VM"]["ID"]) + OneGate.print_key_value("NAME", json_hash["VM"]["NAME"]) + OneGate.print_key_value( + "STATE", + self.state_to_str( + json_hash["VM"]["STATE"], + json_hash["VM"]["LCM_STATE"])) + + vm_nics = [json_hash['VM']['TEMPLATE']['NIC']].flatten + vm_nics.each { |nic| + # TODO: IPv6 + OneGate.print_key_value("IP", nic["IP"]) + } + end + end + + module Service + STATE = { + 'PENDING' => 0, + 'DEPLOYING' => 1, + 'RUNNING' => 2, + 'UNDEPLOYING' => 3, + 'WARNING' => 4, + 'DONE' => 5, + 'FAILED_UNDEPLOYING' => 6, + 'FAILED_DEPLOYING' => 7, + 'SCALING' => 8, + 'FAILED_SCALING' => 9, + 'COOLDOWN' => 10 + } + + STATE_STR = [ + 'PENDING', + 'DEPLOYING', + 'RUNNING', + 'UNDEPLOYING', + 'WARNING', + 'DONE', + 'FAILED_UNDEPLOYING', + 'FAILED_DEPLOYING', + 'SCALING', + 'FAILED_SCALING', + 'COOLDOWN' + ] + + # Returns the string representation of the service state + # @param [String] state String number representing the state + # @return the state string + def self.state_str(state_number) + return STATE_STR[state_number.to_i] + end + + def self.print(json_hash) + OneGate.print_header("SERVICE " + json_hash["SERVICE"]["id"]) + OneGate.print_key_value("NAME", json_hash["SERVICE"]["name"]) + OneGate.print_key_value("STATE", Service.state_str(json_hash["SERVICE"]['state'])) + puts + + roles = [json_hash['SERVICE']['roles']].flatten + roles.each { |role| + OneGate.print_header("ROLE " + role["name"], false) + + if role["nodes"] + role["nodes"].each{ |node| + OneGate::VirtualMachine.print(node["vm_info"]) + } + end + + puts + } + end + end + + class Client + def initialize(opts={}) + @vmid = ENV["VMID"] + @token = ENV["TOKENTXT"] + + url = opts[:url] || ENV['ONEGATE_ENDPOINT'] + @uri = URI.parse(url) + + @user_agent = "OpenNebula #{CloudClient::VERSION} " << + "(#{opts[:user_agent]||"Ruby"})" + + @host = nil + @port = nil + + if ENV['http_proxy'] + uri_proxy = URI.parse(ENV['http_proxy']) + @host = uri_proxy.host + @port = uri_proxy.port + end + end + + def get(path) + req = Net::HTTP::Proxy(@host, @port)::Get.new(path) + + do_request(req) + end + + def delete(path) + req =Net::HTTP::Proxy(@host, @port)::Delete.new(path) + + do_request(req) + end + + def post(path, body) + req = Net::HTTP::Proxy(@host, @port)::Post.new(path) + req.body = body + + do_request(req) + end + + def put(path, body) + req = Net::HTTP::Proxy(@host, @port)::Put.new(path) + req.body = body + + do_request(req) + end + + def login + req = Net::HTTP::Proxy(@host, @port)::Post.new('/login') + + do_request(req) + end + + def logout + req = Net::HTTP::Proxy(@host, @port)::Post.new('/logout') + + do_request(req) + end + + private + + def do_request(req) + req.basic_auth @username, @password + + req['User-Agent'] = @user_agent + req['X-ONEGATE-TOKEN'] = @token + req['X-ONEGATE-VMID'] = @vmid + + res = CloudClient::http_start(@uri, @timeout) do |http| + http.request(req) + end + + res + end + end + + def self.parse_json(response) + if CloudClient::is_error?(response) + puts "ERROR: " + puts response.message + exit -1 + else + return JSON.parse(response.body) + end + end + + # Sets bold font + def self.scr_bold + print "\33[1m" + end + + # Sets underline + def self.scr_underline + print "\33[4m" + end + + # Restore normal font + def self.scr_restore + print "\33[0m" + end + + # Print header + def self.print_header(str, underline=true) + if $stdout.tty? + scr_bold + scr_underline if underline + print "%-80s" % str + scr_restore + else + print str + end + puts + end + + def self.print_key_value(key, value) + puts "%-20s: %-20s" % [key, value] + end + + def self.help_str + return <<-EOT +Available commands + $ onegate vm show [VMID] [--json] + + $ onegate vm update [VMID] --data KEY=VALUE[\\nKEY2=VALUE2] + + $ onegate vm ACTION VMID + $ onegate resume [VMID] + $ onegate stop [VMID] + $ onegate suspend [VMID] + $ onegate delete [VMID] [--hard] + $ onegate shutdown [VMID] [--hard] + $ onegate reboot [VMID] [--hard] + $ onegate poweroff [VMID] [--hard] + $ onegate resubmit [VMID] + $ onegate resched [VMID] + $ onegate unresched [VMID] + $ onegate hold [VMID] + $ onegate release [VMID] + + $ onegate service show [--json] + + $ onegate service scale --role ROLE --cardinality CARDINALITY +EOT + end +end + + +require 'optparse' + +options = {} +OptionParser.new do |opts| + opts.on("-d", "--data DATA", "Data to be included in the VM") do |data| + options[:data] = data + end + + opts.on("-r", "--role ROLE", "Service role") do |role| + options[:role] = role + end + + opts.on("-c", "--cardinality CARD", "Service cardinality") do |cardinality| + options[:cardinality] = cardinality + end + + opts.on("-j", "--json", "Print resource information in JSON") do |json| + options[:json] = json + end + + opts.on("-f", "--hard", "Hard option for power off operations") do |hard| + options[:hard] = hard + end + + opts.on("-h", "--help", "Show this message") do + puts OneGate.help_str + exit + end +end.parse! + +client = OneGate::Client.new() + +case ARGV[0] +when "vm" + case ARGV[1] + when "show" + if ARGV[2] + response = client.get("/vms/"+ARGV[2]) + else + response = client.get("/vm") + end + + json_hash = OneGate.parse_json(response) + if options[:json] + puts JSON.pretty_generate(json_hash) + else + OneGate::VirtualMachine.print(json_hash) + end + when "update" + if !options[:data] + puts "You have to provide the data as a param (--data)" + exit -1 + end + + if ARGV[2] + response = client.put("/vms/"+ARGV[2], options[:data]) + else + response = client.put("/vm", options[:data]) + end + + if CloudClient::is_error?(response) + puts "ERROR: " + puts response.message + exit -1 + end + when "resume", + "stop", + "suspend", + "delete", + "shutdown", + "reboot", + "poweroff", + "resubmit", + "resched", + "unresched", + "hold", + "release" + if ARGV[2] + action_hash = { + "action" => { + "perform" => ARGV[1] + } + } + + if options[:hard] + action_hash["action"]["params"] = true + end + + response = client.post("/vms/"+ARGV[2]+"/action", action_hash.to_json) + + if CloudClient::is_error?(response) + puts "ERROR: " + puts response.message + exit -1 + end + else + puts "You have to provide a VM ID" + exit -1 + end + else + puts OneGate.help_str + puts + puts "Action #{ARGV[1]} not supported" + exit -1 + end +when "service" + case ARGV[1] + when "show" + response = client.get("/service") + json_hash = OneGate.parse_json(response) + #pp json_hash + if options[:json] + puts JSON.pretty_generate(json_hash) + else + OneGate::Service.print(json_hash) + end + when "scale" + response = client.put( + "/service/role/" + options[:role], + { + :cardinality => options[:cardinality] + }.to_json) + + if CloudClient::is_error?(response) + puts "ERROR: " + puts response.message + exit -1 + end + else + puts OneGate.help_str + puts + puts "Action #{ARGV[1]} not supported" + exit -1 + end +else + puts OneGate.help_str + exit -1 +end +