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