1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-02-03 13:47:01 +03:00

Merge branch 'feature-1383'

This commit is contained in:
Daniel Molina 2012-10-01 16:56:07 +02:00
commit 2685dc96d2
19 changed files with 580 additions and 26 deletions

View File

@ -1127,6 +1127,7 @@ ECO_LIB_FILES="src/cloud/ec2/lib/EC2QueryClient.rb \
src/cloud/ec2/lib/elastic_ip.rb \
src/cloud/ec2/lib/ebs.rb \
src/cloud/ec2/lib/instance.rb \
src/cloud/ec2/lib/keypair.rb \
src/cloud/ec2/lib/econe-server.rb"
ECO_LIB_CLIENT_FILES="src/cloud/ec2/lib/EC2QueryClient.rb"
@ -1147,6 +1148,9 @@ ECO_LIB_VIEW_FILES="src/cloud/ec2/lib/views/describe_images.erb \
src/cloud/ec2/lib/views/disassociate_address.erb \
src/cloud/ec2/lib/views/describe_addresses.erb \
src/cloud/ec2/lib/views/release_address.erb \
src/cloud/ec2/lib/views/create_keypair.erb \
src/cloud/ec2/lib/views/delete_keypair.erb \
src/cloud/ec2/lib/views/describe_keypairs.erb \
src/cloud/ec2/lib/views/terminate_instances.erb \
src/cloud/ec2/lib/views/stop_instances.erb \
src/cloud/ec2/lib/views/reboot_instances.erb \
@ -1156,11 +1160,14 @@ ECO_BIN_FILES="src/cloud/ec2/bin/econe-server \
src/cloud/ec2/bin/econe-describe-images \
src/cloud/ec2/bin/econe-describe-volumes \
src/cloud/ec2/bin/econe-describe-instances \
src/cloud/ec2/bin/econe-describe-keypairs \
src/cloud/ec2/bin/econe-register \
src/cloud/ec2/bin/econe-attach-volume \
src/cloud/ec2/bin/econe-detach-volume \
src/cloud/ec2/bin/econe-delete-volume \
src/cloud/ec2/bin/econe-delete-keypair \
src/cloud/ec2/bin/econe-create-volume \
src/cloud/ec2/bin/econe-create-keypair \
src/cloud/ec2/bin/econe-run-instances \
src/cloud/ec2/bin/econe-terminate-instances \
src/cloud/ec2/bin/econe-start-instances \

View File

@ -15,7 +15,7 @@ end
GROUPS={
:quota => [SQLITE, 'sequel'],
:sunstone => ['json', 'rack', 'sinatra', 'thin', 'sequel', SQLITE],
:cloud => %w{amazon-ec2 rack sinatra thin uuidtools curb json},
:cloud => %w{amazon-ec2 rack sinatra thin uuidtools curb json net-ssh},
:ozones_client => %w{json},
:ozones_server => %w{json sequel}+[
SQLITE, 'mysql'

View File

@ -75,7 +75,8 @@ module CommandParser
# Defines the usage information of the command
# @param [String] str
def usage(str)
@usage=str
@usage = str
@name ||= @usage.split(' ').first
end
# Defines the version the command
@ -90,6 +91,12 @@ module CommandParser
@description = str
end
# Defines the name of the command
# @param [String] str
def name(str)
@name = str
end
# Defines a block that will be used to parse the arguments
# of the command. Formats defined using this method con be used
# in the arguments section of the command method, when defining a new
@ -386,12 +393,14 @@ module CommandParser
def run
comm_name=""
if @main
comm=@main
comm_name = @name
comm = @main
elsif
if @args[0] && !@args[0].match(/^-/)
comm_name=@args.shift.to_sym
comm=@commands[comm_name]
comm_name = @args.shift.to_sym
comm = @commands[comm_name]
end
end
@ -470,8 +479,13 @@ module CommandParser
end
puts
puts "Usage:"
print " #{name} "
print_command(@commands[name])
if @main
print " #{@usage}\n"
else
print " #{name} "
print_command(@commands[name])
end
exit -1
else
id=0

View File

@ -0,0 +1,75 @@
#!/usr/bin/env ruby
# -------------------------------------------------------------------------- #
# Copyright 2002-2012, 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. #
#--------------------------------------------------------------------------- #
ONE_LOCATION=ENV["ONE_LOCATION"]
if !ONE_LOCATION
RUBY_LIB_LOCATION="/usr/lib/one/ruby"
else
RUBY_LIB_LOCATION=ONE_LOCATION+"/lib/ruby"
end
$: << RUBY_LIB_LOCATION
$: << RUBY_LIB_LOCATION+"/cloud"
require 'cli/command_parser'
require 'cli/cli_helper'
require 'econe/EC2QueryClient'
include CloudCLI
CommandParser::CmdParser.new(ARGV) do
name "econe-create-keypair"
usage "econe-create-keypair [OPTIONS] <keypair name>"
version CloudCLI.version_text
description <<-EOT
Creates the named keypair. On success returns the keypair identifier and
fingerprint, and the private key
EOT
option [
CommandParser::VERBOSE,
CommandParser::HELP,
EC2QueryClient::ACCESS_KEY,
EC2QueryClient::SECRET_KEY,
EC2QueryClient::URL
]
main :keyName do
begin
ec2_client = EC2QueryClient::Client.new(
options[:access_key],
options[:secret_key],
options[:url])
rescue Exception => e
exit_with_code -1, "#{cmd_name}: #{e.message}"
end
rc = ec2_client.create_keypair(args[0])
if CloudClient::is_error?(rc)
exit_with_code -1, "#{cmd_name}: #{rc.message}"
else
STDOUT.puts "keyName: #{rc['keyName']}"
STDOUT.puts "Fingerprint: #{rc['keyFingerprint']}"
STDOUT.puts "Private Key:"
STDOUT.puts rc['keyMaterial']
exit_with_code 0
end
end
end

View File

@ -0,0 +1,74 @@
#!/usr/bin/env ruby
# -------------------------------------------------------------------------- #
# Copyright 2002-2012, 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. #
#--------------------------------------------------------------------------- #
ONE_LOCATION=ENV["ONE_LOCATION"]
if !ONE_LOCATION
RUBY_LIB_LOCATION="/usr/lib/one/ruby"
else
RUBY_LIB_LOCATION=ONE_LOCATION+"/lib/ruby"
end
$: << RUBY_LIB_LOCATION
$: << RUBY_LIB_LOCATION+"/cloud"
require 'cli/command_parser'
require 'cli/cli_helper'
require 'econe/EC2QueryClient'
include CloudCLI
CommandParser::CmdParser.new(ARGV) do
name "econe-delete-keypair"
usage "econe-delete-keypair [OPTIONS] <keypair name>"
version CloudCLI.version_text
description <<-EOT
Deletes the named keypair, removes the associated keys. On success returns the
identifier of the removed key.
EOT
option [
CommandParser::VERBOSE,
CommandParser::HELP,
EC2QueryClient::ACCESS_KEY,
EC2QueryClient::SECRET_KEY,
EC2QueryClient::URL
]
main :keyName do
begin
ec2_client = EC2QueryClient::Client.new(
options[:access_key],
options[:secret_key],
options[:url])
rescue Exception => e
exit_with_code -1, "#{cmd_name}: #{e.message}"
end
rc = ec2_client.delete_keypair(args[0])
if CloudClient::is_error?(rc)
exit_with_code -1, "#{cmd_name}: #{rc.message}"
else
if rc["return"] == "false"
exit_with_code -1, "Key #{args[0]} could not be removed"
end
exit_with_code 0
end
end
end

View File

@ -74,7 +74,13 @@ CommandParser::CmdParser.new(ARGV) do
if CloudClient::is_error?(rc)
exit_with_code -1, "#{cmd_name}: #{rc.message}"
else
TABLE.show(rc['addressesSet']['item'] || [])
if rc.empty? || rc['addressesSet'].nil? || rc['addressesSet']['item'].nil?
addrs = []
else
addrs = rc['addressesSet']['item']
end
TABLE.show(addrs)
exit_with_code 0
end
end

View File

@ -86,7 +86,13 @@ used with an OpenNebula Cloud."
if CloudClient::is_error?(rc)
exit_with_code -1, "#{cmd_name}: #{rc.message}"
else
TABLE.show(rc['imagesSet']['item'] || [])
if rc.empty? || rc['imagesSet'].nil? || rc['imagesSet']['item'].nil?
imgs = []
else
imgs = rc['imagesSet']['item']
end
TABLE.show(imgs)
exit_with_code 0
end
end

View File

@ -84,7 +84,15 @@ CommandParser::CmdParser.new(ARGV) do
if CloudClient::is_error?(rc)
exit_with_code -1, "#{cmd_name}: #{rc.message}"
else
TABLE.show(rc['reservationSet']['item'][0]['instancesSet']['item'] || [])
if rc.empty? ||
rc['reservationSet']['item'][0]['instancesSet'].nil? ||
rc['reservationSet']['item'][0]['instancesSet']['item'].nil?
insts = []
else
insts = rc['reservationSet']['item'][0]['instancesSet']['item']
end
TABLE.show(insts)
exit_with_code 0
end
end

View File

@ -0,0 +1,85 @@
#!/usr/bin/env ruby
# -------------------------------------------------------------------------- #
# Copyright 2002-2012, 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. #
#--------------------------------------------------------------------------- #
ONE_LOCATION=ENV["ONE_LOCATION"]
if !ONE_LOCATION
RUBY_LIB_LOCATION="/usr/lib/one/ruby"
else
RUBY_LIB_LOCATION=ONE_LOCATION+"/lib/ruby"
end
$: << RUBY_LIB_LOCATION
$: << RUBY_LIB_LOCATION+"/cloud"
require 'cli/command_parser'
require 'cli/cli_helper'
require 'econe/EC2QueryClient'
include CloudCLI
TABLE = CLIHelper::ShowTable.new(nil, self) do
column :keyName, "keyName", :left, :size=>15 do |d|
d["keyName"]
end
column :keyFingerprint, "keyFingerprint", :left, :size=>65 do |d|
d["keyFingerprint"]
end
default :keyName, :keyFingerprint
end
CommandParser::CmdParser.new(ARGV) do
usage "econe-describe-keypairs [OPTIONS]"
version CloudCLI.version_text
description "List and describe the key pairs available to you."
option [
CommandParser::VERBOSE,
CommandParser::HELP,
EC2QueryClient::ACCESS_KEY,
EC2QueryClient::SECRET_KEY,
EC2QueryClient::URL
]
main do
begin
ec2_client = EC2QueryClient::Client.new(
options[:access_key],
options[:secret_key],
options[:url])
rescue Exception => e
exit_with_code -1, "#{cmd_name}: #{e.message}"
end
rc = ec2_client.describe_keypairs
if CloudClient::is_error?(rc)
exit_with_code -1, "#{cmd_name}: #{rc.message}"
else
if rc.empty? || rc['keySet'].nil? || rc['keySet']['item'].nil?
kps = []
else
kps = rc['keySet']['item']
end
TABLE.show(kps)
exit_with_code 0
end
end
end

View File

@ -18,7 +18,12 @@ NIC=[NETWORK_ID=<EC2-VNET-ID>]
IMAGE_ID = <%= erb_vm_info[:ec2_img_id] %>
INSTANCE_TYPE = <%= erb_vm_info[:instance_type ]%>
<% if erb_vm_info[:user_data] %>
<% if erb_vm_info[:user_data] && erb_vm_info[:public_key] %>
CONTEXT = [ EC2_USER_DATA="<%= erb_vm_info[:user_data] %>", EC2_PUBLIC_KEY="<%= erb_vm_info[:public_key] %>", EC2_KEYNAME ="<%= erb_vm_info[:key_name] %>" ]
<% elsif erb_vm_info[:user_data] %>
CONTEXT = [ EC2_USER_DATA="<%= erb_vm_info[:user_data] %>" ]
<% elsif erb_vm_info[:public_key] %>
CONTEXT = [ EC2_PUBLIC_KEY="<%= erb_vm_info[:public_key] %>", EC2_KEYNAME ="<%= erb_vm_info[:key_name] %>" ]
<% end %>

View File

@ -477,5 +477,62 @@ module EC2QueryClient
return response
end
######################################################################
# Lists available key pairs
# @param name[String] of the kaypair
# @return keypairs[Hash]
# {"xmlns"=>"http://ec2.amazonaws.com/doc/2010-08-31/",
# "keySet"=>{"item"=>[
# {"keyName"=>"...", "keyFingerprint"=>"..."}]}}
######################################################################
def describe_keypairs()
begin
response = @ec2_connection.describe_keypairs
rescue Exception => e
error = CloudClient::Error.new(e.message)
return error
end
return response
end
######################################################################
# Creates a new key pair
# @param name[String] of the kaypair
# @return keypair[Hash]
# {"xmlns"=>"http://ec2.amazonaws.com/doc/2010-08-31",
# "keyName"=>"...",
# "keyFingerprint"=>"...",
# "keyMaterial"=>"..."}
######################################################################
def create_keypair(name)
begin
response = @ec2_connection.create_keypair(:key_name => name)
rescue Exception => e
error = CloudClient::Error.new(e.message)
return error
end
return response
end
######################################################################
# Deletes a new key pair
# @param name[String] of the kaypair
# @return response[Hash]
# {"xmlns"=>"http://ec2.amazonaws.com/doc/2010-08-31/",
# "return"=>{"true/false"}
######################################################################
def delete_keypair(name)
begin
response = @ec2_connection.delete_keypair(:key_name => name)
rescue Exception => e
error = CloudClient::Error.new(e.message)
return error
end
return response
end
end
end

View File

@ -17,14 +17,23 @@
require 'rubygems'
require 'erb'
require 'time'
require 'AWS'
require 'base64'
require 'CloudServer'
require 'AWS'
require 'CloudServer'
require 'ImageEC2'
require 'ebs'
require 'elastic_ip'
require 'instance'
require 'keypair'
################################################################################
# Extends the OpenNebula::Error class to include an EC2 render of error
# messages
################################################################################
module OpenNebula
EC2_ERROR = %q{
@ -54,12 +63,14 @@ end
###############################################################################
class EC2QueryServer < CloudServer
###########################################################################
############################################################################
#
#
############################################################################
def initialize(client, oneadmin_client, config, logger)
super(config, logger)
@client = client
@client = client
@oneadmin_client = oneadmin_client
if config[:ssl_server]
@ -68,12 +79,15 @@ class EC2QueryServer < CloudServer
@base_url="http://#{config[:server]}:#{config[:port]}"
end
# ----------- Load EC2 API modules ------------
if @config[:elasticips_vnet_id].nil?
logger.error { 'ElasticIP module not loaded' }
else
extend ElasticIP
end
extend Keypair
extend EBS
extend Instance
end
@ -83,8 +97,7 @@ class EC2QueryServer < CloudServer
###########################################################################
def describe_availability_zones(params)
response = ERB.new(
File.read(@config[:views]+"/describe_availability_zones.erb"))
response = ERB.new(File.read(@config[:views]+"/describe_availability_zones.erb"))
return response.result(binding), 200
end
@ -153,9 +166,8 @@ class EC2QueryServer < CloudServer
return response.result(binding), 200
end
###########################################################################
# Elastic IP
# Provide defaults for Elastic IP if not loaded
###########################################################################
def allocate_address(params)
return OpenNebula::Error.new('Unsupported')
@ -182,7 +194,6 @@ class EC2QueryServer < CloudServer
###########################################################################
private
def render_launch_time(vm)
return "<launchTime>#{Time.at(vm["STIME"].to_i).xmlschema}</launchTime>"
end

View File

@ -208,6 +208,12 @@ def do_http_request(params)
result,rc = @econe_server.detach_volume(params)
when 'DeleteVolume'
result,rc = @econe_server.delete_volume(params)
when 'DescribeKeyPairs'
result,rc = @econe_server.describe_keypairs(params)
when 'CreateKeyPair'
result,rc = @econe_server.create_keypair(params)
when 'DeleteKeyPair'
result,rc = @econe_server.delete_keypair(params)
else
result = OpenNebula::Error.new(
"#{params['Action']} feature is not supported",

View File

@ -59,15 +59,18 @@ module Instance
end
# Get the image
tmp, img=params['ImageId'].split('-')
tmp, img = params['ImageId'].split('-')
# Build the VM
erb_vm_info=Hash.new
erb_vm_info = Hash.new
erb_vm_info[:img_id] = img.to_i
erb_vm_info[:ec2_img_id] = params['ImageId']
erb_vm_info[:instance_type] = instance_type_name
erb_vm_info[:template] = path
erb_vm_info[:user_data] = params['UserData']
erb_vm_info[:public_key] = fetch_publickey(params)
erb_vm_info[:key_name] = params['KeyName']
template = ERB.new(File.read(erb_vm_info[:template]))
template_text = template.result(binding)
@ -184,4 +187,4 @@ module Instance
instance_id = "i-" + sprintf('%08i', vm.id)
return "<instanceId>#{instance_id}</instanceId>"
end
end
end

View File

@ -0,0 +1,173 @@
# -------------------------------------------------------------------------- #
# Copyright 2002-2012, 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. #
#--------------------------------------------------------------------------- #
require 'json'
require 'openssl'
require 'digest/md5'
require 'net/ssh'
module Keypair
############################################################################
# Extends the OpenNebula::User class to include Keypair management
############################################################################
class ::OpenNebula::User
EC2_KP_XPATH = '/USER/TEMPLATE/EC2_KEYPAIRS'
EC2_KP_ELEM = 'EC2_KEYPAIRS'
########################################################################
# Extracts a key pair for the user. Keypairs are stored in the user
# template as base64 json documents
# @param [OpenNebula::User] the user
#
# @return [Hash] with the keypairs. It may be empty
########################################################################
def add_keypair(keypairs)
kp = keypairs.to_json
kp64 = Base64.encode64(kp)
add_element('TEMPLATE', EC2_KP_ELEM => kp64)
return update(template_xml)
end
########################################################################
# Extracts a key pair for the user. Keypairs are stored in the user
# template as base64 json documents
# @param [OpenNebula::User] the user
#
# @return [Hash] with the keypairs. It may be empty
########################################################################
def get_keypair
if has_elements?(EC2_KP_XPATH)
kp64 = Base64.decode64(self[EC2_KP_XPATH])
kp = JSON.parse(kp64)
else
kp = Hash.new
end
return kp
end
end
############################################################################
# KeyPair managment functions for EC2 Query API Server
############################################################################
############################################################################
#
############################################################################
def create_keypair(params)
erb_version = params['Version']
erb_keyname = params['KeyName']
erb_user_name = params['AWSAccessKeyId']
user = User.new_with_id(OpenNebula::User::SELF, @client)
user.info
begin
kp = user.get_keypair
rsa_kp = OpenSSL::PKey::RSA.generate(2048)
rescue Exception => e
return OpenNebula::Error.new("Error in create_keypair: #{e.message}")
end
erb_private_key = rsa_kp
erb_public_key = rsa_kp.public_key
erb_key_fingerprint = Digest::MD5.hexdigest(rsa_kp.to_der)
erb_key_fingerprint.gsub!(/(.{2})(?=.)/, '\1:\2')
erb_ssh_public_key = erb_public_key.ssh_type <<
" " <<
[ erb_public_key.to_blob ].pack('m0') <<
" " <<
erb_keyname
kp[erb_keyname] = {
"fingerprint" => erb_key_fingerprint,
"public_key" => erb_ssh_public_key
}
rc = user.add_keypair(kp)
return rc if OpenNebula::is_error?(rc)
response = ERB.new(File.read(@config[:views]+"/create_keypair.erb"))
return response.result(binding), 200
end
############################################################################
#
############################################################################
def describe_keypairs(params)
erb_version = params['Version']
erb_user_name = params['AWSAccessKeyId']
user = User.new_with_id(OpenNebula::User::SELF, @client)
user.info
erb_keypairs = user.get_keypair
response = ERB.new(File.read(@config[:views]+"/describe_keypairs.erb"))
return response.result(binding), 200
end
############################################################################
#
############################################################################
def delete_keypair(params)
erb_version = params['Version']
erb_user_name = params['AWSAccessKeyId']
erb_key_name = params['KeyName']
erb_result = "false"
vmpool = VirtualMachinePool.new(@client, OpenNebula::Pool::INFO_ALL)
vmpool.info
if !vmpool["/VM_POOL/VM/TEMPLATE/CONTEXT[EC2_KEYNAME=\'#{erb_key_name}\']"]
user = User.new_with_id(OpenNebula::User::SELF, @client)
user.info
kp = user.get_keypair
if kp.has_key?(erb_key_name)
kp.delete(erb_key_name)
rc = user.add_keypair(kp)
erb_result = "true" if !OpenNebula::is_error?(rc)
end
end
response = ERB.new(File.read(@config[:views]+"/delete_keypair.erb"))
return response.result(binding), 200
end
############################################################################
#
############################################################################
def fetch_publickey(params)
keyname = params['KeyName']
user = User.new_with_id(OpenNebula::User::SELF, @client)
user.info
kp = user.get_keypair
return nil if keyname.nil? || kp.empty? || kp[keyname].nil?
kp[keyname]['public_key']
end
end

View File

@ -0,0 +1,5 @@
<CreateKeyPairResponse xmlns="http://ec2.amazonaws.com/doc/<%=erb_version%>">
<keyName><%= erb_keyname %></keyName>
<keyFingerprint><%= erb_key_fingerprint %></keyFingerprint>
<keyMaterial><%= erb_private_key %></keyMaterial>
</CreateKeyPairResponse>

View File

@ -0,0 +1,4 @@
<?xml version="1.0"?>
<DeleteKeyPair xmlns="http://ec2.amazonaws.com/doc/<%=erb_version%>">
<return><%= erb_result %></return>
</DeleteKeyPair>

View File

@ -20,7 +20,11 @@
</instanceState>
<privateDnsName><%= vm["TEMPLATE/NIC/IP"] %></privateDnsName>
<dnsName><%= vm["TEMPLATE/NIC/IP"] %></dnsName>
<keyName>default</keyName>
<% if vm['TEMPLATE/CONTEXT/EC2_KEYNAME'].nil? %>
<keyName>none</keyName>
<% else %>
<keyName><%= vm['TEMPLATE/CONTEXT/EC2_KEYNAME'] %></keyName>
<% end %>
<amiLaunchIndex>0</amiLaunchIndex>
<productCodes/>
<instanceType><%= vm['TEMPLATE/INSTANCE_TYPE'] %></instanceType>

View File

@ -0,0 +1,11 @@
<?xml version="1.0"?>
<DescribeKeyPairsResponse xmlns="http://ec2.amazonaws.com/doc/<%=erb_version%>/">
<keySet>
<% erb_keypairs.each_pair do |k,v| %>
<item>
<keyName><%= k %></keyName>
<keyFingerprint><%= v['fingerprint'] %></keyFingerprint>
</item>
<% end %>
</keySet>
</DescribeKeyPairsResponse>