ec2_driver.rb

OpenNebula Systems Support Team, 11/15/2017 03:22 PM

Download (30.8 KB)

 
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