ec2_driver.rb
| 1 |
#!/usr/bin/env ruby
|
|---|---|
| 2 |
# -------------------------------------------------------------------------- #
|
| 3 |
# Copyright 2002-2017, OpenNebula Project, OpenNebula Systems #
|
| 4 |
# #
|
| 5 |
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
|
| 6 |
# not use this file except in compliance with the License. You may obtain #
|
| 7 |
# a copy of the License at #
|
| 8 |
# #
|
| 9 |
# http://www.apache.org/licenses/LICENSE-2.0 #
|
| 10 |
# #
|
| 11 |
# Unless required by applicable law or agreed to in writing, software #
|
| 12 |
# distributed under the License is distributed on an "AS IS" BASIS, #
|
| 13 |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
|
| 14 |
# See the License for the specific language governing permissions and #
|
| 15 |
# limitations under the License. #
|
| 16 |
# -------------------------------------------------------------------------- #
|
| 17 |
|
| 18 |
ONE_LOCATION = ENV["ONE_LOCATION"] if !defined?(ONE_LOCATION) |
| 19 |
|
| 20 |
if !ONE_LOCATION |
| 21 |
RUBY_LIB_LOCATION = "/usr/lib/one/ruby" if !defined?(RUBY_LIB_LOCATION) |
| 22 |
ETC_LOCATION = "/etc/one/" if !defined?(ETC_LOCATION) |
| 23 |
VAR_LOCATION = "/var/lib/one/" if !defined?(VAR_LOCATION) |
| 24 |
else
|
| 25 |
RUBY_LIB_LOCATION = ONE_LOCATION + "/lib/ruby" if !defined?(RUBY_LIB_LOCATION) |
| 26 |
ETC_LOCATION = ONE_LOCATION + "/etc/" if !defined?(ETC_LOCATION) |
| 27 |
VAR_LOCATION = ONE_LOCATION + "/var/" if !defined?(VAR_LOCATION) |
| 28 |
end
|
| 29 |
|
| 30 |
EC2_DRIVER_CONF = "#{ETC_LOCATION}/ec2_driver.conf" |
| 31 |
EC2_DRIVER_DEFAULT = "#{ETC_LOCATION}/ec2_driver.default" |
| 32 |
|
| 33 |
gem 'aws-sdk', '>= 2.0' |
| 34 |
|
| 35 |
# Load EC2 credentials and environment
|
| 36 |
require 'yaml'
|
| 37 |
require 'rubygems'
|
| 38 |
require 'aws-sdk'
|
| 39 |
require 'uri'
|
| 40 |
require 'resolv'
|
| 41 |
|
| 42 |
$: << RUBY_LIB_LOCATION |
| 43 |
|
| 44 |
require 'CommandManager'
|
| 45 |
require 'scripts_common'
|
| 46 |
require 'rexml/document'
|
| 47 |
require 'VirtualMachineDriver'
|
| 48 |
require 'opennebula'
|
| 49 |
|
| 50 |
require 'thread'
|
| 51 |
|
| 52 |
# >> /var/log/one/oned.log
|
| 53 |
def handle_exception(action, ex, host, did, id = nil, file = nil) |
| 54 |
|
| 55 |
file ||= ""
|
| 56 |
id ||= ""
|
| 57 |
OpenNebula::log_error(action + " of VM #{id} #{did} on host #{host} #{file} "+ |
| 58 |
"due to \"#{ex.message}\"")
|
| 59 |
OpenNebula.error_message("There is a problem: #{ex.message}") |
| 60 |
|
| 61 |
STDERR.puts "********* STACK TRACE *********" |
| 62 |
STDERR.puts ex.backtrace
|
| 63 |
STDERR.puts "*******************************" |
| 64 |
exit (-1)
|
| 65 |
end
|
| 66 |
|
| 67 |
begin
|
| 68 |
PUBLIC_CLOUD_EC2_CONF = YAML::load(File.read(EC2_DRIVER_CONF)) |
| 69 |
rescue Exception => e |
| 70 |
str_error="Unable to read '#{EC2_DRIVER_CONF}'. Invalid YAML syntax:\n" +
|
| 71 |
e.message + "\n********Stack trace from EC2 IM driver*********\n"
|
| 72 |
raise str_error |
| 73 |
end
|
| 74 |
|
| 75 |
# The main class for the EC2 driver
|
| 76 |
class EC2Driver |
| 77 |
ACTION = VirtualMachineDriver::ACTION |
| 78 |
POLL_ATTRIBUTE = VirtualMachineDriver::POLL_ATTRIBUTE |
| 79 |
VM_STATE = VirtualMachineDriver::VM_STATE |
| 80 |
|
| 81 |
# Key that will be used to store the monitoring information in the template
|
| 82 |
EC2_MONITOR_KEY = "EC2DRIVER_MONITOR" |
| 83 |
|
| 84 |
# EC2 commands constants
|
| 85 |
EC2 = {
|
| 86 |
:run => {
|
| 87 |
:cmd => :create, |
| 88 |
:args => {
|
| 89 |
"AKI" => {
|
| 90 |
:opt => 'kernel_id' |
| 91 |
}, |
| 92 |
"AMI" => {
|
| 93 |
:opt => 'image_id' |
| 94 |
}, |
| 95 |
"BLOCKDEVICEMAPPING" => {
|
| 96 |
:opt => 'block_device_mappings', |
| 97 |
:proc => lambda {|str|
|
| 98 |
str.split(' ').collect { |s|
|
| 99 |
dev, tmp = s.split('=')
|
| 100 |
hash = Hash.new
|
| 101 |
hash[:device_name] = dev
|
| 102 |
if tmp == "none" |
| 103 |
hash[:no_device] = dev
|
| 104 |
else
|
| 105 |
hash[:ebs] = Hash.new |
| 106 |
tmp_a = tmp.split(':')
|
| 107 |
hash[:ebs][:snapshot_id] = tmp_a[0] if tmp_a[0] && !tmp_a[0].empty? |
| 108 |
hash[:ebs][:volume_size] = tmp_a[1].to_i if tmp_a[1] && !tmp_a[1].empty? |
| 109 |
if tmp_a[2] == "false" |
| 110 |
hash[:ebs][:delete_on_termination] = false |
| 111 |
elsif tmp_a[2] == "true" |
| 112 |
hash[:ebs][:delete_on_termination] = true |
| 113 |
end
|
| 114 |
hash[:ebs][:volume_type] = tmp_a[3] if tmp_a[3] && !tmp_a[3].empty? |
| 115 |
hash[:ebs][:iops] = tmp_a[4].to_i if tmp_a[4] && !tmp_a[4].empty? |
| 116 |
end
|
| 117 |
hash |
| 118 |
} |
| 119 |
} |
| 120 |
}, |
| 121 |
"CLIENTTOKEN" => {
|
| 122 |
:opt => 'client_token' |
| 123 |
}, |
| 124 |
"INSTANCETYPE" => {
|
| 125 |
:opt => 'instance_type' |
| 126 |
}, |
| 127 |
"KEYPAIR" => {
|
| 128 |
:opt => 'key_name' |
| 129 |
}, |
| 130 |
"LICENSEPOOL" => {
|
| 131 |
:opt => 'license/pool' |
| 132 |
}, |
| 133 |
"PLACEMENTGROUP" => {
|
| 134 |
:opt => 'placement/group_name' |
| 135 |
}, |
| 136 |
"PRIVATEIP" => {
|
| 137 |
:opt => 'private_ip_address' |
| 138 |
}, |
| 139 |
"RAMDISK" => {
|
| 140 |
:opt => 'ramdisk_id' |
| 141 |
}, |
| 142 |
"SUBNETID" => {
|
| 143 |
:opt => 'subnet_id' |
| 144 |
}, |
| 145 |
"TENANCY" => {
|
| 146 |
:opt => 'placement/tenancy' |
| 147 |
}, |
| 148 |
"USERDATA" => {
|
| 149 |
:opt => 'user_data' |
| 150 |
}, |
| 151 |
#"USERDATAFILE" => {
|
| 152 |
# :opt => '-f'
|
| 153 |
#},
|
| 154 |
"SECURITYGROUPS" => {
|
| 155 |
:opt => 'security_groups', |
| 156 |
:proc => lambda {|str| str.split(/,\s*/)} |
| 157 |
}, |
| 158 |
"SECURITYGROUPIDS" => {
|
| 159 |
:opt => 'security_group_ids', |
| 160 |
:proc => lambda {|str| str.split(/,\s*/)} |
| 161 |
}, |
| 162 |
"AVAILABILITYZONE" => {
|
| 163 |
:opt => 'placement/availability_zone' |
| 164 |
}, |
| 165 |
"EBS_OPTIMIZED" => {
|
| 166 |
:opt => 'ebs_optimized', |
| 167 |
:proc => lambda {|str| str.downcase.eql? "true"} |
| 168 |
} |
| 169 |
} |
| 170 |
}, |
| 171 |
:terminate => {
|
| 172 |
:cmd => :terminate |
| 173 |
}, |
| 174 |
:describe => {
|
| 175 |
:cmd => :describe_instances |
| 176 |
}, |
| 177 |
:associate => {
|
| 178 |
:cmd => :associate_address, |
| 179 |
:args => {
|
| 180 |
#"SUBNETID" => {
|
| 181 |
# :opt => '-a',
|
| 182 |
# :proc => lambda {|str| ''}
|
| 183 |
#},
|
| 184 |
"ELASTICIP" => {
|
| 185 |
:opt => 'public_ip' |
| 186 |
} |
| 187 |
} |
| 188 |
}, |
| 189 |
:authorize => {
|
| 190 |
:cmd => :authorize, |
| 191 |
:args => {
|
| 192 |
"AUTHORIZEDPORTS" => {
|
| 193 |
:opt => '-p', |
| 194 |
:proc => lambda {|str| str.split(',').join(' -p ')} |
| 195 |
} |
| 196 |
} |
| 197 |
}, |
| 198 |
:reboot => {
|
| 199 |
:cmd => :reboot |
| 200 |
}, |
| 201 |
:stop => {
|
| 202 |
:cmd => :stop |
| 203 |
}, |
| 204 |
:start => {
|
| 205 |
:cmd => :start |
| 206 |
}, |
| 207 |
:tags => {
|
| 208 |
:cmd => :create_tags, |
| 209 |
:args => {
|
| 210 |
"TAGS" => {
|
| 211 |
:opt => 'tags', |
| 212 |
:proc => lambda {|str|
|
| 213 |
hash = {}
|
| 214 |
str.split(',').each {|s|
|
| 215 |
k,v = s.split('=')
|
| 216 |
hash[k] = v |
| 217 |
} |
| 218 |
hash |
| 219 |
} |
| 220 |
} |
| 221 |
} |
| 222 |
} |
| 223 |
} |
| 224 |
|
| 225 |
# EC2 attributes that will be retrieved in a polling action
|
| 226 |
EC2_POLL_ATTRS = [
|
| 227 |
:public_dns_name,
|
| 228 |
:private_dns_name,
|
| 229 |
:key_name,
|
| 230 |
# not available as a method, should get placement/availability_zone
|
| 231 |
# :availability_zone,
|
| 232 |
:platform,
|
| 233 |
:vpc_id,
|
| 234 |
:private_ip_address,
|
| 235 |
:public_ip_address,
|
| 236 |
:subnet_id,
|
| 237 |
:security_groups,
|
| 238 |
:instance_type,
|
| 239 |
:image_id
|
| 240 |
] |
| 241 |
|
| 242 |
# EC2 constructor, loads credentials and endpoint
|
| 243 |
def initialize(host, host_id=nil) |
| 244 |
@host = host
|
| 245 |
@host_id = host_id
|
| 246 |
|
| 247 |
@state_change_timeout = PUBLIC_CLOUD_EC2_CONF['state_wait_timeout_seconds'].to_i |
| 248 |
|
| 249 |
@instance_types = PUBLIC_CLOUD_EC2_CONF['instance_types'] |
| 250 |
|
| 251 |
conn_opts = get_connect_info(host) |
| 252 |
access_key = conn_opts[:access]
|
| 253 |
secret_key = conn_opts[:secret]
|
| 254 |
region_name = conn_opts[:region]
|
| 255 |
|
| 256 |
#sanitize region data
|
| 257 |
raise "access_key_id not defined for #{host}" if access_key.nil? |
| 258 |
raise "secret_access_key not defined for #{host}" if secret_key.nil? |
| 259 |
raise "region_name not defined for #{host}" if region_name.nil? |
| 260 |
|
| 261 |
Aws.config.merge!({
|
| 262 |
:access_key_id => access_key,
|
| 263 |
:secret_access_key => secret_key,
|
| 264 |
:region => region_name
|
| 265 |
}) |
| 266 |
|
| 267 |
if (proxy_uri = PUBLIC_CLOUD_EC2_CONF['proxy_uri']) |
| 268 |
Aws.config(:proxy_uri => proxy_uri) |
| 269 |
end
|
| 270 |
|
| 271 |
@ec2 = Aws::EC2::Resource.new |
| 272 |
end
|
| 273 |
|
| 274 |
# Check the current template of host
|
| 275 |
# to retrieve connection information
|
| 276 |
# needed for Amazon
|
| 277 |
def get_connect_info(host) |
| 278 |
conn_opts={}
|
| 279 |
|
| 280 |
client = OpenNebula::Client.new |
| 281 |
pool = OpenNebula::HostPool.new(client) |
| 282 |
pool.info |
| 283 |
objects=pool.select {|object| object.name==host }
|
| 284 |
xmlhost = objects.first |
| 285 |
|
| 286 |
system = OpenNebula::System.new(client) |
| 287 |
config = system.get_configuration |
| 288 |
raise "Error getting oned configuration : #{config.message}" if OpenNebula.is_error?(config) |
| 289 |
|
| 290 |
token = config["ONE_KEY"]
|
| 291 |
|
| 292 |
conn_opts = {
|
| 293 |
:access => xmlhost["TEMPLATE/EC2_ACCESS"], |
| 294 |
:secret => xmlhost["TEMPLATE/EC2_SECRET"] |
| 295 |
} |
| 296 |
|
| 297 |
begin
|
| 298 |
conn_opts = OpenNebula.decrypt(conn_opts, token)
|
| 299 |
conn_opts[:region] = xmlhost["TEMPLATE/REGION_NAME"] |
| 300 |
rescue
|
| 301 |
raise "HOST: #{host} must have ec2 credentials and region in order to work properly"
|
| 302 |
end
|
| 303 |
|
| 304 |
return conn_opts
|
| 305 |
end
|
| 306 |
|
| 307 |
# DEPLOY action, also sets ports and ip if needed
|
| 308 |
def deploy(id, host, xml_text, lcm_state, deploy_id) |
| 309 |
|
| 310 |
# Restore if we need to
|
| 311 |
if lcm_state != "BOOT" && lcm_state != "BOOT_FAILURE" |
| 312 |
restore(deploy_id) |
| 313 |
return deploy_id
|
| 314 |
end
|
| 315 |
|
| 316 |
# Otherwise deploy the VM
|
| 317 |
|
| 318 |
begin
|
| 319 |
ec2_info = get_deployment_info(host, xml_text) |
| 320 |
rescue Exception => e |
| 321 |
raise e |
| 322 |
end
|
| 323 |
|
| 324 |
load_default_template_values |
| 325 |
|
| 326 |
if !ec2_value(ec2_info, 'AMI') |
| 327 |
raise "Cannot find AMI in deployment file"
|
| 328 |
end
|
| 329 |
|
| 330 |
opts = generate_options(:run, ec2_info, {
|
| 331 |
:min_count => 1, |
| 332 |
:max_count => 1}) |
| 333 |
|
| 334 |
# The OpenNebula context will be only included if not USERDATA
|
| 335 |
# is provided by the user
|
| 336 |
if !ec2_value(ec2_info, 'USERDATA') |
| 337 |
xml = OpenNebula::XMLElement.new |
| 338 |
xml.initialize_xml(xml_text, 'VM')
|
| 339 |
end
|
| 340 |
|
| 341 |
if xml.has_elements?('TEMPLATE/CONTEXT') |
| 342 |
# Since there is only 1 level ',' will not be added
|
| 343 |
context_str = xml.template_like_str('TEMPLATE/CONTEXT')
|
| 344 |
|
| 345 |
if xml['TEMPLATE/CONTEXT/TOKEN'] == 'YES' |
| 346 |
# TODO use OneGate library
|
| 347 |
token_str = generate_onegate_token(xml) |
| 348 |
if token_str
|
| 349 |
context_str << "\nONEGATE_TOKEN=\"#{token_str}\""
|
| 350 |
end
|
| 351 |
end
|
| 352 |
|
| 353 |
userdata_key = EC2[:run][:args]["USERDATA"][:opt] |
| 354 |
opts[userdata_key] = Base64.encode64(context_str)
|
| 355 |
end
|
| 356 |
|
| 357 |
instances = @ec2.create_instances(opts)
|
| 358 |
instance = instances.first |
| 359 |
|
| 360 |
start_time = Time.now
|
| 361 |
|
| 362 |
while Time.now - start_time < @state_change_timeout |
| 363 |
begin
|
| 364 |
break if instance.exists? |
| 365 |
rescue => e
|
| 366 |
OpenNebula::log_error("RESCUE: #{e.inspect}") |
| 367 |
end
|
| 368 |
|
| 369 |
sleep 2
|
| 370 |
end
|
| 371 |
|
| 372 |
tags = generate_options(:tags, ec2_info)[:tags] || {} |
| 373 |
|
| 374 |
tag_array = [] |
| 375 |
tags.each{ |key,value|
|
| 376 |
tag_array << {
|
| 377 |
:key => key,
|
| 378 |
:value => value
|
| 379 |
} |
| 380 |
} |
| 381 |
|
| 382 |
instance.create_tags(:tags => tag_array) if tag_array.length > 0 |
| 383 |
|
| 384 |
elastic_ip = ec2_value(ec2_info, 'ELASTICIP')
|
| 385 |
|
| 386 |
wait_state('running', instance.id)
|
| 387 |
|
| 388 |
if elastic_ip
|
| 389 |
|
| 390 |
if elastic_ip.match(Resolv::IPv4::Regex) |
| 391 |
address_key = :public_ip
|
| 392 |
else
|
| 393 |
address_key = :allocation_id
|
| 394 |
end
|
| 395 |
|
| 396 |
address = {
|
| 397 |
:instance_id => instance.id,
|
| 398 |
address_key => elastic_ip |
| 399 |
} |
| 400 |
|
| 401 |
@ec2.client.associate_address(address)
|
| 402 |
end
|
| 403 |
|
| 404 |
|
| 405 |
instance.create_tags(tags: [{
|
| 406 |
key: 'ONE_ID', |
| 407 |
value: id
|
| 408 |
}]) |
| 409 |
|
| 410 |
puts(instance.id) |
| 411 |
end
|
| 412 |
|
| 413 |
# Shutdown a EC2 instance
|
| 414 |
def shutdown(deploy_id, lcm_state) |
| 415 |
case lcm_state
|
| 416 |
when "SHUTDOWN" |
| 417 |
ec2_action(deploy_id, :terminate)
|
| 418 |
when "SHUTDOWN_POWEROFF", "SHUTDOWN_UNDEPLOY" |
| 419 |
ec2_action(deploy_id, :stop)
|
| 420 |
end
|
| 421 |
end
|
| 422 |
|
| 423 |
# Reboot a EC2 instance
|
| 424 |
def reboot(deploy_id) |
| 425 |
ec2_action(deploy_id, :reboot)
|
| 426 |
end
|
| 427 |
|
| 428 |
# Cancel a EC2 instance
|
| 429 |
def cancel(deploy_id) |
| 430 |
ec2_action(deploy_id, :terminate)
|
| 431 |
end
|
| 432 |
|
| 433 |
# Save a EC2 instance
|
| 434 |
def save(deploy_id) |
| 435 |
wait_state('running', deploy_id)
|
| 436 |
ec2_action(deploy_id, :stop)
|
| 437 |
wait_state('stopped', deploy_id)
|
| 438 |
end
|
| 439 |
|
| 440 |
# Resumes a EC2 instance
|
| 441 |
def restore(deploy_id) |
| 442 |
wait_state('stopped', deploy_id)
|
| 443 |
ec2_action(deploy_id, :start)
|
| 444 |
end
|
| 445 |
|
| 446 |
# Get info (IP, and state) for a EC2 instance
|
| 447 |
def poll(id, deploy_id) |
| 448 |
i = get_instance(deploy_id) |
| 449 |
vm = OpenNebula::VirtualMachine.new_with_id(id, OpenNebula::Client.new) |
| 450 |
vm.info |
| 451 |
cw_mon_time = vm["LAST_POLL"] ? vm["LAST_POLL"].to_i : Time.now.to_i |
| 452 |
do_cw = (Time.now.to_i - cw_mon_time) >= 360 |
| 453 |
puts parse_poll(i, vm, do_cw, cw_mon_time) |
| 454 |
end
|
| 455 |
|
| 456 |
# Parse template instance type into
|
| 457 |
# Amazon ec2 format (M1SMALL => m1.small)
|
| 458 |
def parse_inst_type(type) |
| 459 |
return type.downcase.gsub("_", ".") |
| 460 |
end
|
| 461 |
|
| 462 |
# Get the info of all the EC2 instances. An EC2 instance must include
|
| 463 |
# the ONE_ID tag, otherwise it will be ignored
|
| 464 |
def monitor_all_vms |
| 465 |
totalmemory = 0
|
| 466 |
totalcpu = 0
|
| 467 |
|
| 468 |
# Get last cloudwatch monitoring time
|
| 469 |
host_obj = OpenNebula::Host.new_with_id(@host_id, |
| 470 |
OpenNebula::Client.new) |
| 471 |
host_obj.info |
| 472 |
cw_mon_time = host_obj["/HOST/TEMPLATE/CWMONTIME"]
|
| 473 |
capacity = host_obj.to_hash["HOST"]["TEMPLATE"]["CAPACITY"] |
| 474 |
if !capacity.nil? && Hash === capacity |
| 475 |
capacity.each{ |name, value|
|
| 476 |
name = parse_inst_type(name) |
| 477 |
cpu, mem = instance_type_capacity(name) |
| 478 |
totalmemory += mem * value.to_i |
| 479 |
totalcpu += cpu * value.to_i |
| 480 |
} |
| 481 |
else
|
| 482 |
raise "you must define CAPACITY section properly! check the template"
|
| 483 |
end
|
| 484 |
|
| 485 |
host_info = "HYPERVISOR=ec2\n"
|
| 486 |
host_info << "PUBLIC_CLOUD=YES\n"
|
| 487 |
host_info << "PRIORITY=-1\n"
|
| 488 |
host_info << "TOTALMEMORY=#{totalmemory.round}\n"
|
| 489 |
host_info << "TOTALCPU=#{totalcpu}\n"
|
| 490 |
host_info << "CPUSPEED=1000\n"
|
| 491 |
host_info << "HOSTNAME=\"#{@host}\"\n"
|
| 492 |
|
| 493 |
vms_info = "VM_POLL=YES\n"
|
| 494 |
|
| 495 |
#
|
| 496 |
# Add information for running VMs (running and pending).
|
| 497 |
#
|
| 498 |
usedcpu = 0
|
| 499 |
usedmemory = 0
|
| 500 |
|
| 501 |
# Build an array of VMs and last_polls for monitoring
|
| 502 |
vpool = OpenNebula::VirtualMachinePool.new(OpenNebula::Client.new, |
| 503 |
OpenNebula::VirtualMachinePool::INFO_ALL_VM) |
| 504 |
vpool.info |
| 505 |
onevm_info = {}
|
| 506 |
|
| 507 |
|
| 508 |
if !cw_mon_time
|
| 509 |
cw_mon_time = Time.now.to_i
|
| 510 |
else
|
| 511 |
cw_mon_time = cw_mon_time.to_i |
| 512 |
end
|
| 513 |
|
| 514 |
do_cw = (Time.now.to_i - cw_mon_time) >= 360 |
| 515 |
vpool.each{
|
| 516 |
|vm| onevm_info[vm.deploy_id] = vm |
| 517 |
} |
| 518 |
|
| 519 |
|
| 520 |
work_q = Queue.new
|
| 521 |
@ec2.instances.each{|i| work_q.push i }
|
| 522 |
workers = (0...20).map do |
| 523 |
Thread.new do |
| 524 |
begin
|
| 525 |
while i = work_q.pop(true) |
| 526 |
next if i.state.name != 'pending' && i.state.name != 'running' |
| 527 |
one_id = i.tags.find {|t| t.key == 'ONE_ID' }
|
| 528 |
one_id = one_id.value if one_id
|
| 529 |
poll_data=parse_poll(i, onevm_info[i.id], do_cw, cw_mon_time) |
| 530 |
vm_template_to_one = vm_to_one(i) |
| 531 |
vm_template_to_one = Base64.encode64(vm_template_to_one).gsub("\n","") |
| 532 |
vms_info << "VM=[\n"
|
| 533 |
vms_info << " ID=#{one_id || -1},\n"
|
| 534 |
vms_info << " DEPLOY_ID=#{i.instance_id},\n"
|
| 535 |
vms_info << " VM_NAME=#{i.instance_id},\n"
|
| 536 |
vms_info << " IMPORT_TEMPLATE=\"#{vm_template_to_one}\",\n"
|
| 537 |
vms_info << " POLL=\"#{poll_data}\" ]\n"
|
| 538 |
if one_id
|
| 539 |
name = i.instance_type |
| 540 |
cpu, mem = instance_type_capacity(name) |
| 541 |
usedcpu += cpu |
| 542 |
usedmemory += mem |
| 543 |
end
|
| 544 |
end
|
| 545 |
rescue Exception => e |
| 546 |
end
|
| 547 |
end
|
| 548 |
end; "ok" |
| 549 |
workers.map(&:join); "ok" |
| 550 |
|
| 551 |
host_info << "USEDMEMORY=#{usedmemory.round}\n"
|
| 552 |
host_info << "USEDCPU=#{usedcpu.round}\n"
|
| 553 |
host_info << "FREEMEMORY=#{(totalmemory - usedmemory).round}\n"
|
| 554 |
host_info << "FREECPU=#{(totalcpu - usedcpu).round}\n"
|
| 555 |
|
| 556 |
if do_cw
|
| 557 |
host_info << "CWMONTIME=#{Time.now.to_i}"
|
| 558 |
else
|
| 559 |
host_info << "CWMONTIME=#{cw_mon_time}"
|
| 560 |
end
|
| 561 |
|
| 562 |
puts host_info |
| 563 |
puts vms_info |
| 564 |
end
|
| 565 |
|
| 566 |
private |
| 567 |
|
| 568 |
#Get the associated capacity of the instance_type as cpu (in 100 percent
|
| 569 |
#e.g. 800) and memory (in KB)
|
| 570 |
def instance_type_capacity(name) |
| 571 |
return 0, 0 if @instance_types[name].nil? |
| 572 |
return (@instance_types[name]['cpu'].to_f * 100).to_i , |
| 573 |
(@instance_types[name]['memory'].to_f * 1024 * 1024).to_i |
| 574 |
end
|
| 575 |
|
| 576 |
# Get the EC2 section of the template. If more than one EC2 section
|
| 577 |
# the CLOUD element is used and matched with the host
|
| 578 |
def get_deployment_info(host, xml_text) |
| 579 |
xml = REXML::Document.new xml_text |
| 580 |
|
| 581 |
ec2 = nil
|
| 582 |
ec2_deprecated = nil
|
| 583 |
|
| 584 |
all_ec2_elements = xml.root.get_elements("//USER_TEMPLATE/PUBLIC_CLOUD")
|
| 585 |
|
| 586 |
# First, let's see if we have an EC2 site that matches
|
| 587 |
# our desired host name
|
| 588 |
all_ec2_elements.each { |element|
|
| 589 |
cloud=element.elements["HOST"]
|
| 590 |
if cloud && cloud.text.upcase == host.upcase
|
| 591 |
ec2 = element |
| 592 |
else
|
| 593 |
cloud=element.elements["CLOUD"]
|
| 594 |
if cloud && cloud.text.upcase == host.upcase
|
| 595 |
ec2_deprecated = element |
| 596 |
end
|
| 597 |
end
|
| 598 |
} |
| 599 |
|
| 600 |
ec2 ||= ec2_deprecated |
| 601 |
|
| 602 |
if !ec2
|
| 603 |
# If we don't find the EC2 site, and ONE just
|
| 604 |
# knows about one EC2 site, let's use that
|
| 605 |
if all_ec2_elements.size == 1 |
| 606 |
ec2 = all_ec2_elements[0]
|
| 607 |
else
|
| 608 |
raise RuntimeError.new("Cannot find PUBLIC_CLOUD element in deployment "\ |
| 609 |
" file or no HOST site matching the requested in the "\
|
| 610 |
" template.")
|
| 611 |
end
|
| 612 |
end
|
| 613 |
|
| 614 |
ec2 |
| 615 |
end
|
| 616 |
|
| 617 |
# Retrieve the vm information from the EC2 instance
|
| 618 |
def parse_poll(instance, onevm, do_cw, cw_mon_time) |
| 619 |
begin
|
| 620 |
if onevm
|
| 621 |
if do_cw
|
| 622 |
cloudwatch_str = cloudwatch_monitor_info(instance.instance_id, |
| 623 |
onevm, |
| 624 |
cw_mon_time) |
| 625 |
else
|
| 626 |
previous_cpu = onevm["MONITORING/CPU"] || 0 |
| 627 |
previous_netrx = onevm["MONITORING/NETRX"] || 0 |
| 628 |
previous_nettx = onevm["MONITORING/NETTX"] || 0 |
| 629 |
|
| 630 |
cloudwatch_str = "CPU=#{previous_cpu} NETTX=#{previous_nettx} NETRX=#{previous_netrx} "
|
| 631 |
end
|
| 632 |
else
|
| 633 |
cloudwatch_str = ""
|
| 634 |
end
|
| 635 |
|
| 636 |
mem = onevm["TEMPLATE/MEMORY"].to_s
|
| 637 |
mem=mem.to_i*1024
|
| 638 |
info = "#{POLL_ATTRIBUTE[:memory]}=#{mem} #{cloudwatch_str}"
|
| 639 |
|
| 640 |
state = ""
|
| 641 |
if !instance.exists?
|
| 642 |
state = VM_STATE[:deleted] |
| 643 |
else
|
| 644 |
state = case instance.state.name
|
| 645 |
when 'pending' |
| 646 |
VM_STATE[:active] |
| 647 |
when 'running' |
| 648 |
VM_STATE[:active] |
| 649 |
when 'shutting-down', 'terminated' |
| 650 |
VM_STATE[:deleted] |
| 651 |
else
|
| 652 |
VM_STATE[:unknown] |
| 653 |
end
|
| 654 |
end
|
| 655 |
info << "#{POLL_ATTRIBUTE[:state]}=#{state} "
|
| 656 |
|
| 657 |
EC2_POLL_ATTRS.map { |key|
|
| 658 |
value = instance.send(key) |
| 659 |
if !value.nil? && !value.empty?
|
| 660 |
if value.is_a?(Array) |
| 661 |
value = value.map {|v|
|
| 662 |
v.group_id if v.is_a?(Aws::EC2::Types::GroupIdentifier) |
| 663 |
}.join(",")
|
| 664 |
end
|
| 665 |
|
| 666 |
info << "AWS_#{key.to_s.upcase}=\\\"#{URI::encode(value)}\\\" "
|
| 667 |
end
|
| 668 |
} |
| 669 |
|
| 670 |
info |
| 671 |
rescue
|
| 672 |
# Unkown state if exception occurs retrieving information from
|
| 673 |
# an instance
|
| 674 |
"#{POLL_ATTRIBUTE[:state]}=#{VM_STATE[:unknown]} "
|
| 675 |
end
|
| 676 |
end
|
| 677 |
|
| 678 |
|
| 679 |
# Execute an EC2 command
|
| 680 |
# +deploy_id+: String, VM id in EC2
|
| 681 |
# +ec2_action+: Symbol, one of the keys of the EC2 hash constant (i.e :run)
|
| 682 |
def ec2_action(deploy_id, ec2_action) |
| 683 |
begin
|
| 684 |
i = get_instance(deploy_id) |
| 685 |
i.send(EC2[ec2_action][:cmd]) |
| 686 |
rescue => e
|
| 687 |
raise e |
| 688 |
end
|
| 689 |
end
|
| 690 |
|
| 691 |
# Generate the options for the given command from the xml provided in the
|
| 692 |
# template. The available options for each command are defined in the EC2
|
| 693 |
# constant
|
| 694 |
def generate_options(action, xml, extra_params={}) |
| 695 |
opts = extra_params || {}
|
| 696 |
|
| 697 |
if EC2[action][:args] |
| 698 |
EC2[action][:args].each {|k,v| |
| 699 |
str = ec2_value(xml, k, &v[:proc])
|
| 700 |
if str
|
| 701 |
tmp = opts |
| 702 |
last_key = nil
|
| 703 |
v[:opt].split('/').each { |key| |
| 704 |
k = key.to_sym |
| 705 |
tmp = tmp[last_key] if last_key
|
| 706 |
tmp[k] = {}
|
| 707 |
last_key = k |
| 708 |
} |
| 709 |
tmp[last_key] = str |
| 710 |
end
|
| 711 |
} |
| 712 |
end
|
| 713 |
|
| 714 |
opts |
| 715 |
end
|
| 716 |
|
| 717 |
# Returns the value of the xml specified by the name or the default
|
| 718 |
# one if it does not exist
|
| 719 |
# +xml+: REXML Document, containing EC2 information
|
| 720 |
# +name+: String, xpath expression to retrieve the value
|
| 721 |
# +block+: Block, block to be applied to the value before returning it
|
| 722 |
def ec2_value(xml, name, &block) |
| 723 |
value = value_from_xml(xml, name) || @defaults[name]
|
| 724 |
if block_given? && value
|
| 725 |
block.call(value) |
| 726 |
else
|
| 727 |
value |
| 728 |
end
|
| 729 |
end
|
| 730 |
|
| 731 |
def value_from_xml(xml, name) |
| 732 |
if xml
|
| 733 |
element = xml.elements[name] |
| 734 |
element.text.strip if element && element.text
|
| 735 |
end
|
| 736 |
end
|
| 737 |
|
| 738 |
# Waits until ec2 machine reach the desired state
|
| 739 |
# +state+: String, is the desired state, needs to be a real state of Amazon ec2: running, stopped, terminated, pending
|
| 740 |
# +deploy_id+: String, VM id in EC2
|
| 741 |
def wait_state(state, deploy_id) |
| 742 |
ready = (state == 'stopped') || (state == 'pending') || (state == 'running') || (state == 'terminated') |
| 743 |
raise "Waiting for an invalid state" if !ready |
| 744 |
t_init = Time.now
|
| 745 |
begin
|
| 746 |
wstate = get_instance(deploy_id).state.name rescue nil |
| 747 |
raise "Ended in invalid state" if Time.now - t_init > @state_change_timeout |
| 748 |
sleep 3
|
| 749 |
end while wstate != state |
| 750 |
end
|
| 751 |
|
| 752 |
# Load the default values that will be used to create a new instance, if
|
| 753 |
# not provided in the template. These values are defined in the EC2_CONF
|
| 754 |
# file
|
| 755 |
def load_default_template_values |
| 756 |
@defaults = Hash.new |
| 757 |
|
| 758 |
if File.exist?(EC2_DRIVER_DEFAULT) |
| 759 |
fd = File.new(EC2_DRIVER_DEFAULT) |
| 760 |
xml = REXML::Document.new fd |
| 761 |
fd.close() |
| 762 |
|
| 763 |
return if !xml || !xml.root |
| 764 |
|
| 765 |
ec2 = xml.root.elements["PUBLIC_CLOUD"]
|
| 766 |
|
| 767 |
return if !ec2 |
| 768 |
|
| 769 |
EC2.each {|action, hash|
|
| 770 |
if hash[:args] |
| 771 |
hash[:args].each { |key, value|
|
| 772 |
@defaults[key.to_sym] = value_from_xml(ec2, key)
|
| 773 |
} |
| 774 |
end
|
| 775 |
} |
| 776 |
end
|
| 777 |
end
|
| 778 |
|
| 779 |
# Retrieve the instance from EC2
|
| 780 |
def get_instance(id) |
| 781 |
begin
|
| 782 |
instance = @ec2.instance(id)
|
| 783 |
if instance.exists?
|
| 784 |
return instance
|
| 785 |
else
|
| 786 |
raise RuntimeError.new("Instance #{id} does not exist") |
| 787 |
end
|
| 788 |
rescue => e
|
| 789 |
raise e |
| 790 |
end
|
| 791 |
end
|
| 792 |
|
| 793 |
# Build template for importation
|
| 794 |
def vm_to_one(instance) |
| 795 |
cpu, mem = instance_type_capacity(instance.instance_type) |
| 796 |
|
| 797 |
cpu = cpu.to_f / 100
|
| 798 |
vcpu = cpu.ceil |
| 799 |
mem = mem.to_i / 1024 # Memory for templates expressed in MB |
| 800 |
|
| 801 |
str = "NAME = \"Instance from #{instance.id}\"\n"\
|
| 802 |
"CPU = \"#{cpu}\"\n"\
|
| 803 |
"VCPU = \"#{vcpu}\"\n"\
|
| 804 |
"MEMORY = \"#{mem}\"\n"\
|
| 805 |
"HYPERVISOR = \"ec2\"\n"\
|
| 806 |
"PUBLIC_CLOUD = [\n"\
|
| 807 |
" TYPE =\"ec2\",\n"\
|
| 808 |
" AMI =\"#{instance.image_id}\"\n"\
|
| 809 |
"]\n"\
|
| 810 |
"IMPORT_VM_ID = \"#{instance.id}\"\n"\
|
| 811 |
"SCHED_REQUIREMENTS=\"NAME=\\\"#{@host}\\\"\"\n"\
|
| 812 |
"DESCRIPTION = \"Instance imported from EC2, from instance"\
|
| 813 |
" #{instance.id}, AMI #{instance.image_id}\"\n"
|
| 814 |
|
| 815 |
str |
| 816 |
end
|
| 817 |
|
| 818 |
# Extract monitoring information from Cloud Watch
|
| 819 |
# CPU, NETTX and NETRX
|
| 820 |
def cloudwatch_monitor_info(id, onevm, cw_mon_time) |
| 821 |
cw=Aws::CloudWatch::Client.new |
| 822 |
|
| 823 |
# CPU
|
| 824 |
begin
|
| 825 |
cpu = get_cloudwatch_metric(cw, |
| 826 |
"CPUUtilization",
|
| 827 |
cw_mon_time, |
| 828 |
["Average"],
|
| 829 |
"Percent",
|
| 830 |
id) |
| 831 |
if cpu[:datapoints].size != 0 |
| 832 |
cpu = cpu[:datapoints][-1][:average] |
| 833 |
else
|
| 834 |
cpu = onevm["MONITORING/CPU"] || 0 |
| 835 |
end
|
| 836 |
cpu = cpu.to_f.round(2).to_s
|
| 837 |
rescue => e
|
| 838 |
OpenNebula::log_error(e.message)
|
| 839 |
end
|
| 840 |
|
| 841 |
# NETTX
|
| 842 |
nettx = 0
|
| 843 |
begin
|
| 844 |
nettx_dp = get_cloudwatch_metric(cw, |
| 845 |
"NetworkOut",
|
| 846 |
cw_mon_time, |
| 847 |
["Sum"],
|
| 848 |
"Bytes",
|
| 849 |
id)[:datapoints]
|
| 850 |
previous_nettx = onevm["/VM/MONITORING/NETTX"]
|
| 851 |
nettx = previous_nettx ? previous_nettx.to_i : 0
|
| 852 |
|
| 853 |
nettx_dp.each{|dp|
|
| 854 |
nettx += dp[:sum].to_i
|
| 855 |
} |
| 856 |
rescue => e
|
| 857 |
OpenNebula::log_error(e.message)
|
| 858 |
end
|
| 859 |
|
| 860 |
# NETRX
|
| 861 |
netrx = 0
|
| 862 |
begin
|
| 863 |
netrx_dp = get_cloudwatch_metric(cw, |
| 864 |
"NetworkIn",
|
| 865 |
cw_mon_time, |
| 866 |
["Sum"],
|
| 867 |
"Bytes",
|
| 868 |
id)[:datapoints]
|
| 869 |
previous_netrx = onevm["/VM/MONITORING/NETRX"]
|
| 870 |
netrx = previous_netrx ? previous_netrx.to_i : 0
|
| 871 |
|
| 872 |
netrx_dp.each{|dp|
|
| 873 |
netrx += dp[:sum].to_i
|
| 874 |
} |
| 875 |
rescue => e
|
| 876 |
OpenNebula::log_error(e.message)
|
| 877 |
end
|
| 878 |
|
| 879 |
"CPU=#{cpu.to_s} NETTX=#{nettx.to_s} NETRX=#{netrx.to_s} "
|
| 880 |
end
|
| 881 |
|
| 882 |
# Get metric from AWS/EC2 namespace from the last poll
|
| 883 |
def get_cloudwatch_metric(cw, metric_name, last_poll, statistics, units, id) |
| 884 |
dt = 60 # period |
| 885 |
t0 = (Time.at(last_poll.to_i)-65) # last poll time |
| 886 |
t = (Time.now-60) # actual time |
| 887 |
|
| 888 |
while ((t - t0)/dt >= 1440) do dt+=60 end |
| 889 |
|
| 890 |
options={:namespace=>"AWS/EC2",
|
| 891 |
:metric_name=>metric_name,
|
| 892 |
:start_time=> t0.iso8601,
|
| 893 |
:end_time=> t.iso8601,
|
| 894 |
:period=>dt,
|
| 895 |
:statistics=>statistics,
|
| 896 |
:unit=>units,
|
| 897 |
:dimensions=>[{:name=>"InstanceId", :value=>id}]} |
| 898 |
|
| 899 |
cw.get_metric_statistics(options) |
| 900 |
end
|
| 901 |
|
| 902 |
# TODO move this method to a OneGate library
|
| 903 |
def generate_onegate_token(xml) |
| 904 |
# Create the OneGate token string
|
| 905 |
vmid_str = xml["ID"]
|
| 906 |
stime_str = xml["STIME"]
|
| 907 |
str_to_encrypt = "#{vmid_str}:#{stime_str}"
|
| 908 |
|
| 909 |
user_id = xml['TEMPLATE/CREATED_BY']
|
| 910 |
|
| 911 |
if user_id.nil?
|
| 912 |
OpenNebula::log_error("VMID:#{vNid} CREATED_BY not present" \ |
| 913 |
" in the VM TEMPLATE")
|
| 914 |
return nil |
| 915 |
end
|
| 916 |
|
| 917 |
user = OpenNebula::User.new_with_id(user_id, |
| 918 |
OpenNebula::Client.new) |
| 919 |
rc = user.info |
| 920 |
|
| 921 |
if OpenNebula.is_error?(rc) |
| 922 |
OpenNebula::log_error("VMID:#{vmid} user.info" \ |
| 923 |
" error: #{rc.message}")
|
| 924 |
return nil |
| 925 |
end
|
| 926 |
|
| 927 |
token_password = user['TEMPLATE/TOKEN_PASSWORD']
|
| 928 |
|
| 929 |
if token_password.nil?
|
| 930 |
OpenNebula::log_error(VMID:#{vmid} TOKEN_PASSWORD not present"\ |
| 931 |
" in the USER:#{user_id} TEMPLATE")
|
| 932 |
return nil |
| 933 |
end
|
| 934 |
|
| 935 |
cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc") |
| 936 |
cipher.encrypt |
| 937 |
cipher.key = token_password |
| 938 |
onegate_token = cipher.update(str_to_encrypt) |
| 939 |
onegate_token << cipher.final |
| 940 |
|
| 941 |
onegate_token_64 = Base64.encode64(onegate_token).chop
|
| 942 |
end
|
| 943 |
end
|
| 944 |
|