diff --git a/install.sh b/install.sh index a0706fea87..9deb718b85 100755 --- a/install.sh +++ b/install.sh @@ -202,6 +202,7 @@ LIB_DIRS="$LIB_LOCATION/ruby \ $LIB_LOCATION/ruby/cloud/econe \ $LIB_LOCATION/ruby/cloud/econe/views \ $LIB_LOCATION/ruby/cloud/occi \ + $LIB_LOCATION/ruby/cloud/CloudAuth \ $LIB_LOCATION/ruby/onedb \ $LIB_LOCATION/tm_commands \ $LIB_LOCATION/tm_commands/shared \ @@ -345,6 +346,7 @@ INSTALL_FILES=( HOOK_FT_FILES:$VAR_LOCATION/remotes/hooks/ft HOOK_NETWORK_FILES:$VAR_LOCATION/remotes/hooks/vnm COMMON_CLOUD_LIB_FILES:$LIB_LOCATION/ruby/cloud + CLOUD_AUTH_LIB_FILES:$LIB_LOCATION/ruby/cloud/CloudAuth ECO_LIB_FILES:$LIB_LOCATION/ruby/cloud/econe ECO_LIB_VIEW_FILES:$LIB_LOCATION/ruby/cloud/econe/views ECO_BIN_FILES:$BIN_LOCATION @@ -789,10 +791,15 @@ RUBY_OPENNEBULA_LIB_FILES="src/oca/ruby/OpenNebula/Host.rb \ COMMON_CLOUD_LIB_FILES="src/cloud/common/CloudServer.rb \ src/cloud/common/CloudClient.rb \ + src/cloud/common/CloudAuth.rb src/cloud/common/Configuration.rb" COMMON_CLOUD_CLIENT_LIB_FILES="src/cloud/common/CloudClient.rb" +CLOUD_AUTH_LIB_FILES="src/cloud/common/CloudAuth/BasicCloudAuth.rb \ + src/cloud/common/CloudAuth/EC2CloudAuth.rb \ + src/cloud/common/CloudAuth/X509CloudAuth.rb" + #------------------------------------------------------------------------------- # EC2 Query for OpenNebula #------------------------------------------------------------------------------- @@ -926,7 +933,8 @@ SUNSTONE_MODELS_JSON_FILES="src/sunstone/models/OpenNebulaJSON/HostJSON.rb \ src/sunstone/models/OpenNebulaJSON/AclJSON.rb \ src/sunstone/models/OpenNebulaJSON/VirtualNetworkJSON.rb" -SUNSTONE_TEMPLATE_FILES="src/sunstone/templates/login.html" +SUNSTONE_TEMPLATE_FILES="src/sunstone/templates/login.html \ + src/sunstone/templates/login_x509.html" SUNSTONE_VIEWS_FILES="src/sunstone/views/index.erb" @@ -1007,6 +1015,7 @@ SUNSTONE_PUBLIC_IMAGES_FILES="src/sunstone/public/images/ajax-loader.gif \ src/sunstone/public/images/opennebula-sunstone-big.png \ src/sunstone/public/images/opennebula-sunstone-small.png \ src/sunstone/public/images/panel.png \ + src/sunstone/public/images/panel_short.png \ src/sunstone/public/images/pbar.gif \ src/sunstone/public/images/Refresh-icon.png \ src/sunstone/public/images/vnc_off.png \ diff --git a/src/cloud/common/CloudAuth.rb b/src/cloud/common/CloudAuth.rb new file mode 100644 index 0000000000..859e4bea39 --- /dev/null +++ b/src/cloud/common/CloudAuth.rb @@ -0,0 +1,53 @@ +# -------------------------------------------------------------------------- # +# Copyright 2002-2011, OpenNebula Project Leads (OpenNebula.org) # +# # +# 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. # +#--------------------------------------------------------------------------- # + +class CloudAuth + AUTH_MODULES = { + "basic" => 'BasicCloudAuth', + "ec2" => 'EC2CloudAuth', + "x509" => 'X509CloudAuth' + } + + attr_reader :client, :token + + def initialize(conf) + @conf = conf + + if AUTH_MODULES.include?(@conf[:auth]) + require 'CloudAuth/' + AUTH_MODULES[@conf[:auth]] + extend Kernel.const_get(AUTH_MODULES[@conf[:auth]]) + else + raise "Auth module not specified" + end + end + + protected + + def get_password(username) + @oneadmin_client ||= OpenNebula::Client.new(nil, @conf[:one_xmlrpc]) + + if @user_pool.nil? + @user_pool ||= OpenNebula::UserPool.new(@oneadmin_client) + + rc = @user_pool.info + if OpenNebula.is_error?(rc) + raise rc.message + end + end + + return @user_pool["USER[NAME=\"#{username}\"]/PASSWORD"] + end +end \ No newline at end of file diff --git a/src/cloud/common/CloudAuth/BasicCloudAuth.rb b/src/cloud/common/CloudAuth/BasicCloudAuth.rb new file mode 100644 index 0000000000..ea6e703f0d --- /dev/null +++ b/src/cloud/common/CloudAuth/BasicCloudAuth.rb @@ -0,0 +1,40 @@ +# -------------------------------------------------------------------------- # +# Copyright 2002-2011, OpenNebula Project Leads (OpenNebula.org) # +# # +# 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. # +#--------------------------------------------------------------------------- # + +module BasicCloudAuth + def auth(env, params={}) + auth = Rack::Auth::Basic::Request.new(env) + + if auth.provided? && auth.basic? + username, password = auth.credentials + + if @conf[:hash_passwords] + password = Digest::SHA1.hexdigest(password) + end + + one_pass = get_password(username) + if one_pass && one_pass == password + @token = "#{username}:#{password}" + @client = Client.new(@token, @conf[:one_xmlrpc], false) + return nil + else + return "Authentication failure" + end + else + return "Basic auth not provided" + end + end +end diff --git a/src/cloud/common/CloudAuth/EC2CloudAuth.rb b/src/cloud/common/CloudAuth/EC2CloudAuth.rb new file mode 100644 index 0000000000..fe8321f14c --- /dev/null +++ b/src/cloud/common/CloudAuth/EC2CloudAuth.rb @@ -0,0 +1,92 @@ +# -------------------------------------------------------------------------- # +# Copyright 2002-2011, OpenNebula Project Leads (OpenNebula.org) # +# # +# 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. # +#--------------------------------------------------------------------------- # + +module EC2CloudAuth + def auth(env, params={}) + username = params['AWSAccessKeyId'] + one_pass = get_password(username) + return "Invalid credentials" unless one_pass + + signature = case params['SignatureVersion'] + when "1" then signature_v1(params.clone,one_pass) + when "2" then signature_v2(params.clone,one_pass,env,true,false) + end + + if params['Signature'] != signature + if params['SignatureVersion']=="2" + signature = signature_v2(params.clone,one_pass,env,false,false) + if params['Signature'] != signature + return "Invalid Credentials" + end + else + return "Invalid Credentials" + end + end + + @token = "#{username}:#{one_pass}" + @client = Client.new(@token, @conf[:one_xmlrpc], false) + return nil + end + + private + + # Calculates signature version 1 + def signature_v1(params, secret_key, digest='sha1') + params.delete('Signature') + req_desc = params.sort {|x,y| x[0].downcase <=> y[0].downcase}.to_s + + digest_generator = OpenSSL::Digest::Digest.new(digest) + digest = OpenSSL::HMAC.digest(digest_generator, secret_key, req_desc) + b64sig = Base64.b64encode(digest) + return b64sig.strip + end + + # Calculates signature version 2 + def signature_v2(params, secret_key, env, include_port=true, urlencode=true) + params.delete('Signature') + params.delete('file') + + server_host = params.delete(:econe_host) + server_port = params.delete(:econe_port) + if include_port + server_str = "#{server_host}:#{server_port}" + else + server_str = server_host + end + + canonical_str = AWS.canonical_string( + params, + server_str, + env['REQUEST_METHOD']) + + # Use the correct signature strength + sha_strength = case params['SignatureMethod'] + when "HmacSHA1" then 'sha1' + when "HmacSHA256" then 'sha256' + else 'sha1' + end + + digest = OpenSSL::Digest::Digest.new(sha_strength) + hmac = OpenSSL::HMAC.digest(digest, secret_key, canonical_str) + b64hmac = Base64.encode64(hmac).gsub("\n","") + + if urlencode + return CGI::escape(b64hmac) + else + return b64hmac + end + end +end diff --git a/src/cloud/common/CloudAuth/X509CloudAuth.rb b/src/cloud/common/CloudAuth/X509CloudAuth.rb new file mode 100644 index 0000000000..4348ee7e9b --- /dev/null +++ b/src/cloud/common/CloudAuth/X509CloudAuth.rb @@ -0,0 +1,108 @@ +# -------------------------------------------------------------------------- # +# Copyright 2002-2011, OpenNebula Project Leads (OpenNebula.org) # +# # +# 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. # +#--------------------------------------------------------------------------- # + +module X509CloudAuth + # Gets the username associated with a password + # password:: _String_ the password + # [return] _Hash_ with the username + def get_username(password) + @oneadmin_client ||= OpenNebula::Client.new(nil, @conf[:one_xmlrpc]) + + if @user_pool.nil? + @user_pool ||= OpenNebula::UserPool.new(@oneadmin_client) + + rc = @user_pool.info + if OpenNebula.is_error?(rc) + raise rc.message + end + end + + username = @user_pool["USER[PASSWORD=\"#{password}\"]/NAME"] + return username if (username != nil) + + # Check if the DN is part of a |-separted multi-DN password + user_elts = Array.new + @user_pool.each {|e| user_elts << e['PASSWORD']} + multiple_users = user_elts.select {|e| e=~ /\|/ } + matched = nil + multiple_users.each do |e| + e.to_s.split('|').each do |w| + if (w == password) + matched=e + break + end + end + break if matched + end + if matched + password = matched.to_s + end + + return @user_pool["USER[PASSWORD=\"#{password}\"]/NAME"] + end + + def auth(env, params={}) + failed = 'Authentication failed. ' + # For https, the web service should be set to include the user cert in the environment. + cert_line = env['HTTP_SSL_CLIENT_CERT'] + cert_line = nil if cert_line == '(null)' # For Apache mod_ssl + + # Use the https credentials for authentication + require 'server_auth' + while cert_line + begin + cert_array=cert_line.scan(/([^\s]*)\s/) + cert_array = cert_array[2..-2] + cert_array.unshift('-----BEGIN CERTIFICATE-----') + cert_array.push('-----END CERTIFICATE-----') + cert_pem = cert_array.join("\n") + cert = OpenSSL::X509::Certificate.new(cert_pem) + rescue + raise failed + "Could not create X509 certificate from " + cert_line + end + + # Password should be DN with whitespace removed. + subjectname = cert.subject.to_s.delete("\s") + begin + username = get_username(subjectname) + rescue + username = nil + end + + break if username + + chain_dn = (!chain_dn ? "" : chain_dn) + "\n" + subjectname + chain_index = !chain_index ? 0 : chain_index + 1 + cert_line = env["HTTP_SSL_CLIENT_CERT_CHAIN_#{chain_index}"] + cert_line = nil if cert_line == '(null)' # For Apache mod_ssl + end + + if !cert_line + msg = "" + msg << failed + msg << "Username not found in certificate chain " + msg << chain_dn if chain_dn + raise msg + end + + auth = ServerAuth.new + + @token = auth.login_token(username, subjectname, 300) + @client = Client.new(@token, @conf[:one_xmlrpc], false) + + return nil + end +end diff --git a/src/cloud/common/CloudServer.rb b/src/cloud/common/CloudServer.rb index e05757ee07..91c5335701 100755 --- a/src/cloud/common/CloudServer.rb +++ b/src/cloud/common/CloudServer.rb @@ -14,9 +14,8 @@ # limitations under the License. # #--------------------------------------------------------------------------- # -require 'Configuration' require 'OpenNebula' -require 'pp' +require 'CloudAuth' ############################################################################## # This class represents a generic Cloud Server using the OpenNebula Cloud @@ -28,80 +27,39 @@ class CloudServer # Public attributes ########################################################################## attr_reader :config - attr_reader :one_client # Initializes the Cloud server based on a config file # config_file:: _String_ for the server. MUST include the following # variables: - # USER - # PASSWORD + # AUTH # VM_TYPE - # IMAGE_DIR - # DATABASE - def initialize(config_file) - + # XMLRPC + def initialize(config) # --- Load the Cloud Server configuration file --- + @config = config + @cloud_auth = CloudAuth.new(@config) + end - @config = Configuration.new(config_file) + def authenticate(env, params={}) + @cloud_auth.auth(env, params) + end - if @config[:vm_type] == nil - raise "No VM_TYPE defined." - end - - @instance_types = Hash.new - - if @config[:vm_type].kind_of?(Array) - @config[:vm_type].each {|type| - @instance_types[type['NAME']]=type - } - else - @instance_types[@config[:vm_type]['NAME']]=@config[:vm_type] - end - - # --- Start an OpenNebula Session --- - - @one_client = Client.new(nil,@config[:one_xmlrpc]) - @user_pool = UserPool.new(@one_client) + def client + @cloud_auth.client end # # Prints the configuration of the server # - def print_configuration + def self.print_configuration(config) puts "--------------------------------------" puts " Server configuration " puts "--------------------------------------" - pp @config + pp config - puts "--------------------------------------" - puts " Registered Instance Types " - puts "--------------------------------------" - pp @instance_types + STDOUT.flush end - ########################################################################### - # USER and OpenNebula Session Methods - ########################################################################### - - # Generates an OpenNebula Session for the given user - # user:: _Hash_ the user information - # [return] an OpenNebula client session - def one_client_user(name, password) - client = Client.new("dummy:dummy") - client.one_auth = "#{name}:#{password}" - - return client - end - - # Gets the data associated with a user - # name:: _String_ the name of the user - # [return] _Hash_ with the user data - def get_user_password(name) - @user_pool.info - return @user_pool["USER[NAME=\"#{name}\"]/PASSWORD"] - end - - # Finds out if a port is available on ip # ip:: _String_ IP address where the port to check is # port:: _String_ port to find out whether is open @@ -123,4 +81,3 @@ class CloudServer return false end end - diff --git a/src/cloud/ec2/etc/econe.conf b/src/cloud/ec2/etc/econe.conf index ca77a3ee8f..151279862d 100644 --- a/src/cloud/ec2/etc/econe.conf +++ b/src/cloud/ec2/etc/econe.conf @@ -15,14 +15,21 @@ #--------------------------------------------------------------------------- # # OpenNebula sever contact information -ONE_XMLRPC=http://localhost:2633/RPC2 +:one_xmlrpc: http://localhost:2633/RPC2 # Host and port where econe server will run -SERVER= -PORT=4567 +:server: localhost +:port: 4567 # SSL proxy that serves the API (set if is being used) -#SSL_SERVER=fqdm.of.the.server +#:ssl_server: fqdm.of.the.server + +# Authentication protocol for the econe server: +# ec2, default Acess key and Secret key scheme +# x509, for x509 certificates based authentication +:auth: ec2 # VM types allowed and its template file (inside templates directory) -VM_TYPE=[NAME=m1.small, TEMPLATE=m1.small.erb] +:instance_types: + :m1.small: + :template: m1.small.erb diff --git a/src/cloud/ec2/lib/EC2QueryServer.rb b/src/cloud/ec2/lib/EC2QueryServer.rb index 1d8c786729..0783f3da34 100644 --- a/src/cloud/ec2/lib/EC2QueryServer.rb +++ b/src/cloud/ec2/lib/EC2QueryServer.rb @@ -15,7 +15,6 @@ #--------------------------------------------------------------------------- # require 'rubygems' -require 'sinatra' require 'erb' require 'time' require 'AWS' @@ -62,67 +61,26 @@ class EC2QueryServer < CloudServer ########################################################################### - def initialize(config_file,template,views) - super(config_file) - @config.add_configuration_value("TEMPLATE_LOCATION",template) - @config.add_configuration_value("VIEWS",views) - - if @config[:ssl_server] - @server_host=@config[:ssl_server] - else - @server_host=@config[:server] - end - - @server_port=@config[:port] - - print_configuration + def initialize(config) + super(config) end + + def authenticate(env, params) + econe_host = @config[:ssl_server] + econe_host ||= @config[:server] - ########################################################################### - # Authentication functions - ########################################################################### + econe_port = @config[:port] - # EC2 protocol authentication function - # params:: of the request - # [return] true if authenticated - def authenticate(params,env) - password = get_user_password(params['AWSAccessKeyId']) - return nil if !password - - signature = case params['SignatureVersion'] - when "1" then signature_version_1(params.clone, password) - when "2" then signature_version_2(params, - password, - env, - true, - false) - end - - if params['Signature']==signature - return one_client_user(params['AWSAccessKeyId'], password) - else - if params['SignatureVersion']=="2" - signature = signature_version_2(params, - password, - env, - false, - false) - if params['Signature']==signature - return one_client_user(params['AWSAccessKeyId'], password) - end - end - end - - return nil + params.merge!({:econe_host => econe_host, :econe_port => econe_port}) + super(env, params) end - ########################################################################### # Repository Interface ########################################################################### - def upload_image(params, one_client) - image = ImageEC2.new(Image.build_xml, one_client, params['file']) + def upload_image(params) + image = ImageEC2.new(Image.build_xml, self.client, params['file']) template = image.to_one_template if OpenNebula.is_error?(template) @@ -140,11 +98,11 @@ class EC2QueryServer < CloudServer return response.result(binding), 200 end - def register_image(params, one_client) + def register_image(params) # Get the Image ID tmp, img=params['ImageLocation'].split('-') - image = Image.new(Image.build_xml(img.to_i), one_client) + image = Image.new(Image.build_xml(img.to_i), self.client) # Enable the new Image rc = image.info @@ -160,9 +118,9 @@ class EC2QueryServer < CloudServer return response.result(binding), 200 end - def describe_images(params, one_client) + def describe_images(params) user_flag = OpenNebula::Pool::INFO_GROUP - impool = ImagePool.new(one_client, user_flag) + impool = ImagePool.new(self.client, user_flag) impool.info erb_version = params['Version'] @@ -175,14 +133,14 @@ class EC2QueryServer < CloudServer # Instance Interface ########################################################################### - def run_instances(params, one_client) + def run_instances(params) # Get the instance type and path if params['InstanceType'] != nil instance_type_name = params['InstanceType'] - instance_type = @instance_types[instance_type_name] + instance_type = @config[:instance_types][instance_type_name.to_sym] if instance_type != nil - path = @config[:template_location] + "/#{instance_type['TEMPLATE']}" + path = @config[:template_location] + "/#{instance_type[:template]}" end end @@ -201,7 +159,7 @@ class EC2QueryServer < CloudServer template_text = template.result(binding) # Start the VM. - vm = VirtualMachine.new(VirtualMachine.build_xml, one_client) + vm = VirtualMachine.new(VirtualMachine.build_xml, self.client) rc = vm.allocate(template_text) if OpenNebula::is_error?(rc) @@ -219,26 +177,26 @@ class EC2QueryServer < CloudServer return response.result(binding), 200 end - def describe_instances(params, one_client) + def describe_instances(params) user_flag = OpenNebula::Pool::INFO_MINE - vmpool = VirtualMachinePool.new(one_client, user_flag) + vmpool = VirtualMachinePool.new(self.client, user_flag) vmpool.info erb_version = params['Version'] erb_user_name = params['AWSAccessKeyId'] - + response = ERB.new(File.read(@config[:views]+"/describe_instances.erb")) return response.result(binding), 200 end - def terminate_instances(params, one_client) + def terminate_instances(params) # Get the VM ID vmid=params['InstanceId.1'] vmid=params['InstanceId.01'] if !vmid tmp, vmid=vmid.split('-') if vmid[0]==?i - vm = VirtualMachine.new(VirtualMachine.build_xml(vmid),one_client) + vm = VirtualMachine.new(VirtualMachine.build_xml(vmid),self.client) rc = vm.info return OpenNebula::Error.new('Unsupported'),400 if OpenNebula::is_error?(rc) @@ -257,55 +215,6 @@ class EC2QueryServer < CloudServer return response.result(binding), 200 end -private - - # Calculates signature version 1 - def signature_version_1(params, secret_key, digest='sha1') - params.delete('Signature') - req_desc = params.sort {|x,y| x[0].downcase <=> y[0].downcase}.to_s - - digest_generator = OpenSSL::Digest::Digest.new(digest) - digest = OpenSSL::HMAC.digest(digest_generator, - secret_key, - req_desc) - b64sig = Base64.b64encode(digest) - return b64sig.strip - end - - # Calculates signature version 2 - def signature_version_2(params, secret_key, env, includeport=true, urlencode=true) - signature_params = params.reject { |key,value| - key=='Signature' or key=='file' } - - if includeport - server_str = @server_host + ':' + @server_port - else - server_str = @server_host - end - - canonical_str = AWS.canonical_string(signature_params, - server_str, - env['REQUEST_METHOD']) - - # Use the correct signature strength - sha_strength = case params['SignatureMethod'] - when "HmacSHA1" then 'sha1' - when "HmacSHA256" then 'sha256' - else 'sha1' - end - - digest = OpenSSL::Digest::Digest.new(sha_strength) - b64hmac = - Base64.encode64( - OpenSSL::HMAC.digest(digest, secret_key, canonical_str)).gsub("\n","") - - if urlencode - return CGI::escape(b64hmac) - else - return b64hmac - end - end - ########################################################################### # Helper functions ########################################################################### diff --git a/src/cloud/ec2/lib/econe-server.rb b/src/cloud/ec2/lib/econe-server.rb index 4255af408d..fc3e8413f1 100644 --- a/src/cloud/ec2/lib/econe-server.rb +++ b/src/cloud/ec2/lib/econe-server.rb @@ -42,37 +42,54 @@ require 'rubygems' require 'sinatra' require 'EC2QueryServer' +require 'Configuration' include OpenNebula +############################################################################## +# Parse Configuration file +############################################################################## begin - $econe_server = EC2QueryServer.new(CONFIGURATION_FILE, - TEMPLATE_LOCATION, VIEWS_LOCATION) + conf = YAML.load_file(CONFIGURATION_FILE) rescue Exception => e - puts "Error starting server: #{e}" - exit(-1) + puts "Error parsing config file #{CONFIGURATION_FILE}: #{e.message}" + exit 1 end -if CloudServer.is_port_open?($econe_server.config[:server], - $econe_server.config[:port]) - puts "Port busy, please shutdown the service or move econe server port." - exit -end +conf[:template_location] = TEMPLATE_LOCATION +conf[:views] = VIEWS_LOCATION + +CloudServer.print_configuration(conf) ############################################################################## # Sinatra Configuration ############################################################################## -set :host, $econe_server.config[:server] -set :port, $econe_server.config[:port] +set :config, conf +set :host, settings.config[:server] +set :port, settings.config[:port] + +if CloudServer.is_port_open?(settings.config[:server], + settings.config[:port]) + puts "Port busy, please shutdown the service or move econe server port." + exit 1 +end ############################################################################## # Actions ############################################################################## before do - @client = $econe_server.authenticate(params,env) - - if @client.nil? + @econe_server = EC2QueryServer.new(settings.config) + + begin + result = @econe_server.authenticate(request.env, params) + rescue Exception => e + # Add a log message + error 500, error_xml("AuthFailure", 0) + end + + if result + # Add a log message error 400, error_xml("AuthFailure", 0) end end @@ -80,7 +97,7 @@ end helpers do def error_xml(code,id) message = '' - + case code when 'AuthFailure' message = 'User not authorized' @@ -88,45 +105,44 @@ helpers do message = 'Specified AMI ID does not exist' when 'Unsupported' message = 'The instance type or feature is not supported in your requested Availability Zone.' - else + else message = code end - + xml = ""+ - code + - "" + - message + - "" + - id.to_s + + code + + "" + + message + + "" + + id.to_s + "" - - return xml - end + + return xml + end end post '/' do - do_http_request(params, @client) + do_http_request(params) end get '/' do - do_http_request(params, @client) + do_http_request(params) end -def do_http_request(params, client) - +def do_http_request(params) case params['Action'] when 'UploadImage' - result,rc = $econe_server.upload_image(params, client) + result,rc = @econe_server.upload_image(params) when 'RegisterImage' - result,rc = $econe_server.register_image(params, client) + result,rc = @econe_server.register_image(params) when 'DescribeImages' - result,rc = $econe_server.describe_images(params, client) + result,rc = @econe_server.describe_images(params) when 'RunInstances' - result,rc = $econe_server.run_instances(params, client) + result,rc = @econe_server.run_instances(params) when 'DescribeInstances' - result,rc = $econe_server.describe_instances(params, client) + result,rc = @econe_server.describe_instances(params) when 'TerminateInstances' - result,rc = $econe_server.terminate_instances(params, client) + result,rc = @econe_server.terminate_instances(params) end if OpenNebula::is_error?(result) @@ -135,5 +151,5 @@ def do_http_request(params, client) headers['Content-Type'] = 'application/xml' - result + result end diff --git a/src/cloud/occi/etc/occi-server.conf b/src/cloud/occi/etc/occi-server.conf index 2eddedc706..2bf8516b9f 100644 --- a/src/cloud/occi/etc/occi-server.conf +++ b/src/cloud/occi/etc/occi-server.conf @@ -14,21 +14,31 @@ # limitations under the License. # #--------------------------------------------------------------------------- # -# OpenNebula server contact information -ONE_XMLRPC=http://localhost:2633/RPC2 +# OpenNebula sever contact information +:one_xmlrpc: http://localhost:2633/RPC2 -# Host and port where the occi server will run -SERVER= -PORT=4567 +# Host and port where OCCI server will run +:server: localhost +:port: 4567 # SSL proxy that serves the API (set if is being used) -#SSL_SERVER=https://localhost:443 +#:ssl_server: fqdm.of.the.server # Configuration for OpenNebula's Virtual Networks -BRIDGE= +#:bridge: NAME_OF_DEFAULT_BRIDGE + +# Authentication protocol for the OCCI server: +# basic, for OpenNebula's user-password scheme +# x509, for x509 certificates based authentication +:auth: basic # VM types allowed and its template file (inside templates directory) -VM_TYPE=[NAME=custom, TEMPLATE=custom.erb] -VM_TYPE=[NAME=small, TEMPLATE=small.erb] -VM_TYPE=[NAME=medium, TEMPLATE=medium.erb] -VM_TYPE=[NAME=large, TEMPLATE=large.erb] +:instance_types: + :custom: + :template: custom.erb + :small: + :template: small.erb + :medium: + :template: medium.erb + :large: + :template: large.erb diff --git a/src/cloud/occi/lib/OCCIServer.rb b/src/cloud/occi/lib/OCCIServer.rb index ce24fc93b3..85a8f4432a 100755 --- a/src/cloud/occi/lib/OCCIServer.rb +++ b/src/cloud/occi/lib/OCCIServer.rb @@ -15,12 +15,9 @@ #--------------------------------------------------------------------------- # # Common cloud libs -require 'rubygems' -require 'sinatra' require 'CloudServer' # OCA -require 'OpenNebula' include OpenNebula # OCCI libs @@ -43,29 +40,13 @@ class OCCIServer < CloudServer # Server initializer # config_file:: _String_ path of the config file # template:: _String_ path to the location of the templates - def initialize(config_file,template) - super(config_file) + def initialize(config) + super(config) - @config.add_configuration_value("TEMPLATE_LOCATION",template) - - if @config[:ssl_server] - @base_url=@config[:ssl_server] + if config[:ssl_server] + @base_url=config[:ssl_server] else - @base_url="http://#{@config[:server]}:#{@config[:port]}" - end - - print_configuration - end - - # Retrieve a client with the user credentials - # requestenv:: _Hash_ Hash containing the environment of the request - # [return] _Client_ client with the user credentials - def get_client(requestenv) - auth = Rack::Auth::Basic::Request.new(requestenv) - if auth - return one_client_user(auth.credentials[0], auth.credentials[1]) - else - return nil + @base_url="http://#{config[:server]}:#{config[:port]}" end end @@ -92,13 +73,8 @@ class OCCIServer < CloudServer # --- Get User's VMs --- user_flag = -1 - one_client = get_client(request.env) - if !one_client - return "No authorization data present", 401 - end - vmpool = VirtualMachinePoolOCCI.new( - one_client, + self.client, user_flag) # --- Prepare XML Response --- @@ -124,13 +100,8 @@ class OCCIServer < CloudServer # --- Get User's VNETs --- user_flag = -1 - one_client = get_client(request.env) - if !one_client - return "No authorization data present", 401 - end - network_pool = VirtualNetworkPoolOCCI.new( - one_client, + self.client, user_flag) # --- Prepare XML Response --- @@ -155,13 +126,8 @@ class OCCIServer < CloudServer # --- Get User's Images --- user_flag = -1 - one_client = get_client(request.env) - if !one_client - return "No authorization data present", 401 - end - image_pool = ImagePoolOCCI.new( - one_client, + self.client, user_flag) # --- Prepare XML Response --- @@ -193,16 +159,11 @@ class OCCIServer < CloudServer # [return] _String_,_Integer_ COMPUTE Representation or error, status code def post_compute(request) # --- Create the new Instance --- - one_client = get_client(request.env) - if !one_client - return "No authorization data present", 401 - end - vm = VirtualMachineOCCI.new( VirtualMachine.build_xml, - one_client, + self.client, request.body.read, - @instance_types, + @config[:instance_types], @config[:template_location]) # --- Generate the template and Allocate the new Instance --- @@ -223,14 +184,9 @@ class OCCIServer < CloudServer # status code def get_compute(request, params) # --- Get the VM --- - one_client = get_client(request.env) - if !one_client - return "No authorization data present", 401 - end - vm = VirtualMachineOCCI.new( VirtualMachine.build_xml(params[:id]), - one_client) + self.client) # --- Prepare XML Response --- rc = vm.info @@ -253,14 +209,9 @@ class OCCIServer < CloudServer # status code def delete_compute(request, params) # --- Get the VM --- - one_client = get_client(request.env) - if !one_client - return "No authorization data present", 401 - end - vm = VirtualMachineOCCI.new( VirtualMachine.build_xml(params[:id]), - one_client) + self.client) rc = vm.info return rc, 404 if OpenNebula::is_error?(rc) @@ -278,14 +229,9 @@ class OCCIServer < CloudServer # status code def put_compute(request, params) # --- Get the VM --- - one_client = get_client(request.env) - if !one_client - return "No authorization data present", 401 - end - vm = VirtualMachineOCCI.new( VirtualMachine.build_xml(params[:id]), - one_client) + self.client) rc = vm.info return rc, 400 if OpenNebula.is_error?(rc) @@ -349,14 +295,9 @@ class OCCIServer < CloudServer # [return] _String_,_Integer_ Network Representation or error, status code def post_network(request) # --- Create the new Instance --- - one_client = get_client(request.env) - if !one_client - return "No authorization data present", 401 - end - network = VirtualNetworkOCCI.new( VirtualNetwork.build_xml, - one_client, + self.client, request.body, @config[:bridge]) @@ -377,15 +318,9 @@ class OCCIServer < CloudServer # [return] _String_,_Integer_ NETWORK occi representation or error, # status code def get_network(request, params) - # --- Get the VNET --- - one_client = get_client(request.env) - if !one_client - return "No authorization data present", 401 - end - network = VirtualNetworkOCCI.new( VirtualNetwork.build_xml(params[:id]), - one_client) + self.client) # --- Prepare XML Response --- rc = network.info @@ -406,15 +341,9 @@ class OCCIServer < CloudServer # [return] _String_,_Integer_ Delete confirmation msg or error, # status code def delete_network(request, params) - # --- Get the VNET --- - one_client = get_client(request.env) - if !one_client - return "No authorization data present", 401 - end - network = VirtualNetworkOCCI.new( VirtualNetwork.build_xml(params[:id]), - one_client) + self.client) rc = network.info return rc, 404 if OpenNebula::is_error?(rc) @@ -433,15 +362,10 @@ class OCCIServer < CloudServer def put_network(request, params) xmldoc = XMLElement.build_xml(request.body, 'NETWORK') vnet_info = XMLElement.new(xmldoc) if xmldoc != nil - - one_client = get_client(request.env) - if !one_client - return "No authorization data present", 401 - end vnet = VirtualNetworkOCCI.new( VirtualNetwork.build_xml(params[:id]), - one_client) + self.client) rc = vnet.info return rc, 400 if OpenNebula.is_error?(rc) @@ -474,11 +398,6 @@ class OCCIServer < CloudServer error = OpenNebula::Error.new(error_msg) return error, 400 end - - one_client = get_client(request.env) - if !one_client - return "No authorization data present", 401 - end # --- Create and Add the new Image --- occixml = request.params['occixml'] @@ -486,7 +405,7 @@ class OCCIServer < CloudServer image = ImageOCCI.new( Image.build_xml, - one_client, + self.client, occixml, request.params['file']) @@ -508,14 +427,9 @@ class OCCIServer < CloudServer # status code def get_storage(request, params) # --- Get the Image --- - one_client = get_client(request.env) - if !one_client - return "No authorization data present", 401 - end - image = ImageOCCI.new( Image.build_xml(params[:id]), - one_client) + self.client) rc = image.info @@ -537,14 +451,9 @@ class OCCIServer < CloudServer # status code def delete_storage(request, params) # --- Get the Image --- - one_client = get_client(request.env) - if !one_client - return "No authorization data present", 401 - end - image = ImageOCCI.new( Image.build_xml(params[:id]), - one_client) + self.client) rc = image.info return rc, 404 if OpenNebula::is_error?(rc) @@ -563,15 +472,10 @@ class OCCIServer < CloudServer def put_storage(request, params) xmldoc = XMLElement.build_xml(request.body, 'STORAGE') image_info = XMLElement.new(xmldoc) if xmldoc != nil - - one_client = get_client(request.env) - if !one_client - return "No authorization data present", 401 - end image = ImageOCCI.new( Image.build_xml(params[:id]), - one_client) + self.client) rc = image.info return rc, 400 if OpenNebula.is_error?(rc) diff --git a/src/cloud/occi/lib/VirtualMachineOCCI.rb b/src/cloud/occi/lib/VirtualMachineOCCI.rb index 6d62bfe724..7ed361cc00 100755 --- a/src/cloud/occi/lib/VirtualMachineOCCI.rb +++ b/src/cloud/occi/lib/VirtualMachineOCCI.rb @@ -77,8 +77,8 @@ class VirtualMachineOCCI < VirtualMachine if @vm_info != nil itype = @vm_info['INSTANCE_TYPE'] - if itype != nil and types[itype] != nil - @template = base + "/#{types[itype]['TEMPLATE']}" + if itype != nil and types[itype.to_sym] != nil + @template = base + "/#{types[itype.to_sym][:template]}" end end diff --git a/src/cloud/occi/lib/occi-server.rb b/src/cloud/occi/lib/occi-server.rb index 136bfd482c..161f0afc4c 100755 --- a/src/cloud/occi/lib/occi-server.rb +++ b/src/cloud/occi/lib/occi-server.rb @@ -43,35 +43,56 @@ $: << RUBY_LIB_LOCATION+"/cloud" # For the Repository Manager ################################################ require 'rubygems' require 'sinatra' -require 'OCCIServer' -require 'OpenNebula' +require 'OCCIServer' include OpenNebula +############################################################################## +# Parse Configuration file +############################################################################## begin - $occi_server = OCCIServer.new(CONFIGURATION_FILE, TEMPLATE_LOCATION) + conf = YAML.load_file(CONFIGURATION_FILE) rescue Exception => e - puts "Error starting server: #{e}" - exit(-1) + puts "Error parsing config file #{CONFIGURATION_FILE}: #{e.message}" + exit 1 end -if CloudServer.is_port_open?($occi_server.config[:server], - $occi_server.config[:port]) - puts "Port busy, please shutdown the service or move occi server port." - exit -end +conf[:template_location] = TEMPLATE_LOCATION + +CloudServer.print_configuration(conf) ############################################################################## # Sinatra Configuration ############################################################################## -set :host, $occi_server.config[:server] -set :port, $occi_server.config[:port] +set :config, conf + +if CloudServer.is_port_open?(settings.config[:server], + settings.config[:port]) + puts "Port busy, please shutdown the service or move occi server port." + exit +end + +set :host, settings.config[:server] +set :port, settings.config[:port] ############################################################################## # Helpers ############################################################################## +before do + @occi_server = OCCIServer.new(settings.config) + begin + result = @occi_server.authenticate(request.env) + rescue Exception => e + error 500, e.message + end + + if result + error 401, result + end +end + # Response treatment helpers do def treat_response(result,rc) @@ -93,32 +114,32 @@ end ################################################### post '/compute' do - result,rc = $occi_server.post_compute(request) + result,rc = @occi_server.post_compute(request) treat_response(result,rc) end get '/compute' do - result,rc = $occi_server.get_computes(request) + result,rc = @occi_server.get_computes(request) treat_response(result,rc) end post '/network' do - result,rc = $occi_server.post_network(request) + result,rc = @occi_server.post_network(request) treat_response(result,rc) end get '/network' do - result,rc = $occi_server.get_networks(request) + result,rc = @occi_server.get_networks(request) treat_response(result,rc) end post '/storage' do - result,rc = $occi_server.post_storage(request) + result,rc = @occi_server.post_storage(request) treat_response(result,rc) end get '/storage' do - result,rc = $occi_server.get_storages(request) + result,rc = @occi_server.get_storages(request) treat_response(result,rc) end @@ -127,46 +148,46 @@ end ################################################### get '/compute/:id' do - result,rc = $occi_server.get_compute(request, params) + result,rc = @occi_server.get_compute(request, params) treat_response(result,rc) end delete '/compute/:id' do - result,rc = $occi_server.delete_compute(request, params) + result,rc = @occi_server.delete_compute(request, params) treat_response(result,rc) end put '/compute/:id' do - result,rc = $occi_server.put_compute(request, params) + result,rc = @occi_server.put_compute(request, params) treat_response(result,rc) end get '/network/:id' do - result,rc = $occi_server.get_network(request, params) + result,rc = @occi_server.get_network(request, params) treat_response(result,rc) end delete '/network/:id' do - result,rc = $occi_server.delete_network(request, params) + result,rc = @occi_server.delete_network(request, params) treat_response(result,rc) end put '/network/:id' do - result,rc = $occi_server.put_network(request, params) + result,rc = @occi_server.put_network(request, params) treat_response(result,rc) end get '/storage/:id' do - result,rc = $occi_server.get_storage(request, params) + result,rc = @occi_server.get_storage(request, params) treat_response(result,rc) end delete '/storage/:id' do - result,rc = $occi_server.delete_storage(request, params) + result,rc = @occi_server.delete_storage(request, params) treat_response(result,rc) end put '/storage/:id' do - result,rc = $occi_server.put_storage(request, params) + result,rc = @occi_server.put_storage(request, params) treat_response(result,rc) end diff --git a/src/sunstone/bin/sunstone-server b/src/sunstone/bin/sunstone-server index 74ad261176..8afe86013f 100755 --- a/src/sunstone/bin/sunstone-server +++ b/src/sunstone/bin/sunstone-server @@ -19,13 +19,13 @@ if [ -z "$ONE_LOCATION" ]; then SUNSTONE_PID=/var/run/one/sunstone.pid - SUNSTONE_SERVER=/usr/lib/one/sunstone/config.ru + SUNSTONE_SERVER=/usr/lib/one/sunstone/sunstone-server.rb SUNSTONE_LOCK_FILE=/var/lock/one/.sunstone.lock SUNSTONE_LOG=/var/log/one/sunstone.log SUNSTONE_CONF=/etc/one/sunstone-server.conf else SUNSTONE_PID=$ONE_LOCATION/var/sunstone.pid - SUNSTONE_SERVER=$ONE_LOCATION/lib/sunstone/config.ru + SUNSTONE_SERVER=$ONE_LOCATION/lib/sunstone/sunstone-server.rb SUNSTONE_LOCK_FILE=$ONE_LOCATION/var/.sunstone.lock SUNSTONE_LOG=$ONE_LOCATION/var/sunstone.log SUNSTONE_CONF=$ONE_LOCATION/etc/sunstone-server.conf @@ -56,23 +56,16 @@ start() echo "Can not find $SUNSTONE_SERVER." exit 1 fi - - source $SUNSTONE_CONF - - lsof -i:$PORT &> /dev/null - if [ $? -eq 0 ]; then - echo "The port $PORT is being used. Please specify a different one." - exit 1 - fi # Start the sunstone daemon touch $SUNSTONE_LOCK_FILE - rackup $SUNSTONE_SERVER -s thin -p $PORT -o $HOST \ - -P $SUNSTONE_PID &> $SUNSTONE_LOG & + ruby $SUNSTONE_SERVER > $SUNSTONE_LOG 2>&1 & LASTPID=$! if [ $? -ne 0 ]; then echo "Error executing $SUNSTONE_SERVER, please check the log $SUNSTONE_LOG" exit 1 + else + echo $LASTPID > $SUNSTONE_PID fi sleep 1 @@ -83,7 +76,7 @@ start() exit 1 fi - echo "sunstone-server listening on $HOST:$PORT" + echo "sunstone-server started" } # diff --git a/src/sunstone/etc/sunstone-server.conf b/src/sunstone/etc/sunstone-server.conf index 37ff230bdc..b15fb6941d 100644 --- a/src/sunstone/etc/sunstone-server.conf +++ b/src/sunstone/etc/sunstone-server.conf @@ -1,7 +1,12 @@ +# OpenNebula sever contact information +:one_xmlrpc: http://localhost:2633/RPC2 + # Server Configuration -HOST=127.0.0.1 -PORT=9869 +:host: 127.0.0.1 +:port: 9869 + +:auth: basic # VNC Configuration -VNC_PROXY_BASE_PORT=29876 -NOVNC_PATH= +:vnc_proxy_base_port: 29876 +:novnc_path: diff --git a/src/sunstone/models/SunstoneServer.rb b/src/sunstone/models/SunstoneServer.rb index fc09d4632f..02283816a4 100644 --- a/src/sunstone/models/SunstoneServer.rb +++ b/src/sunstone/models/SunstoneServer.rb @@ -23,37 +23,8 @@ class SunstoneServer # FLAG that will filter the elements retrieved from the Pools POOL_FILTER = Pool::INFO_GROUP - def initialize(username, password) - # TBD one_client_user(name) from CloudServer - @client = Client.new("dummy:dummy") - @client.one_auth = "#{username}:#{password}" - end - - ############################################################################ - # - ############################################################################ - def self.authorize(user="", sha1_pass="") - if user.empty? || sha1_pass.empty? - return [401, false] - end - - # TBD get_user_password(name) from CloudServer - user_pool = UserPool.new(Client.new) - rc = user_pool.info - if OpenNebula.is_error?(rc) - return [500, false] - end - - user_pass = user_pool["USER[NAME=\"#{user}\"]/PASSWORD"] - user_id = user_pool["USER[NAME=\"#{user}\"]/ID"] - user_gid = user_pool["USER[NAME=\"#{user}\"]/GID"] - user_gname = user_pool["USER[NAME=\"#{user}\"]/GNAME"] - - if user_pass == sha1_pass - return [204, [user_id, user_gid, user_gname]] - else - return [401, nil] - end + def initialize(token, xmlrpc) + @client = Client.new(token, xmlrpc, false) end ############################################################################ diff --git a/src/sunstone/public/images/panel_short.png b/src/sunstone/public/images/panel_short.png new file mode 100644 index 0000000000..2eb300fe1f Binary files /dev/null and b/src/sunstone/public/images/panel_short.png differ diff --git a/src/sunstone/sunstone-server.rb b/src/sunstone/sunstone-server.rb index d348cf41f1..e2bc06ab93 100755 --- a/src/sunstone/sunstone-server.rb +++ b/src/sunstone/sunstone-server.rb @@ -36,6 +36,7 @@ end SUNSTONE_ROOT_DIR = File.dirname(__FILE__) $: << RUBY_LIB_LOCATION +$: << RUBY_LIB_LOCATION+'/cloud' $: << SUNSTONE_ROOT_DIR+'/models' ############################################################################## @@ -45,16 +46,23 @@ require 'rubygems' require 'sinatra' require 'erb' -require 'cloud/Configuration' +require 'CloudAuth' require 'SunstoneServer' require 'SunstonePlugins' -set :config, Configuration.new(CONFIGURATION_FILE) +begin + conf = YAML.load_file(CONFIGURATION_FILE) + conf[:hash_passwords] = true +rescue Exception => e + puts "Error parsing config file #{CONFIGURATION_FILE}: #{e.message}" + exit 1 +end ############################################################################## # Sinatra Configuration ############################################################################## use Rack::Session::Pool, :key => 'sunstone' +set :config, conf set :host, settings.config[:host] set :port, settings.config[:port] @@ -67,32 +75,40 @@ helpers do end def build_session - auth = Rack::Auth::Basic::Request.new(request.env) - if auth.provided? && auth.basic? && auth.credentials - user = auth.credentials[0] - sha1_pass = Digest::SHA1.hexdigest(auth.credentials[1]) + cloud_auth = CloudAuth.new(settings.config) - rc = SunstoneServer.authorize(user, sha1_pass) - if rc[1] - session[:user] = user - session[:user_id] = rc[1][0] - session[:user_gid] = rc[1][1] - session[:user_gname] = rc[1][2] - session[:password] = sha1_pass - session[:ip] = request.ip - session[:remember] = params[:remember] - - if params[:remember] - env['rack.session.options'][:expire_after] = 30*60*60*24 - end - - return [204, ""] - else - return [rc.first, ""] - end + begin + result = cloud_auth.auth(request.env, params) + rescue Exception => e + error 500, e.message end - return [401, ""] + if result + return [401, ""] + else + user_id = OpenNebula::User::SELF + user = OpenNebula::User.new_with_id(user_id, cloud_auth.client) + + rc = user.info + if OpenNebula.is_error?(rc) + # Add a log message + return [500, ""] + end + + session[:user] = user['NAME'] + session[:user_id] = user['ID'] + session[:user_gid] = user['GID'] + session[:user_gname] = user['GNAME'] + session[:token] = cloud_auth.token + session[:ip] = request.ip + session[:remember] = params[:remember] + + if params[:remember] + env['rack.session.options'][:expire_after] = 30*60*60*24 + end + + return [204, ""] + end end def destroy_session @@ -105,7 +121,9 @@ before do unless request.path=='/login' || request.path=='/' halt 401 unless authorized? - @SunstoneServer = SunstoneServer.new(session[:user], session[:password]) + @SunstoneServer = SunstoneServer.new( + session[:token], + settings.config[:one_xmlrpc]) end end @@ -125,9 +143,10 @@ end # HTML Requests ############################################################################## get '/' do - return File.read(File.dirname(__FILE__)+ - '/templates/login.html') unless authorized? - + if !authorized? + templ = settings.config[:auth]=="basic"? "login.html" : "login_x509.html" + return File.read(File.dirname(__FILE__)+'/templates/'+templ) + end time = Time.now + 60 response.set_cookie("one-user", :value=>"#{session[:user]}", @@ -146,7 +165,10 @@ get '/' do end get '/login' do - File.read(SUNSTONE_ROOT_DIR+'/templates/login.html') + if !authorized? + templ = settings.confing[:auth]=="basic"? "login.html" : "login_x509.html" + return File.read(File.dirname(__FILE__)+'/templates/'+templ) + end end ############################################################################## diff --git a/src/sunstone/templates/login_x509.html b/src/sunstone/templates/login_x509.html new file mode 100644 index 0000000000..790e52028c --- /dev/null +++ b/src/sunstone/templates/login_x509.html @@ -0,0 +1,47 @@ + + + + OpenNebula Sunstone Login + + + + + + + + + + + + + + + +
+
+
+ +
+ Invalid username or password +
+
+ OpenNebula is not running +
+ +
+
+
+ + + + +
+
+
+
+ +