Statistics
| Branch: | Tag: | Revision:

one / src / vmm_mad / remotes / ec2 / ec2_driver.rb @ 8cd7d0df

History | View | Annotate | Download (30.7 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
    STDERR.puts "********* STACK TRACE *********"
60
    STDERR.puts ex.backtrace
61
    STDERR.puts "*******************************"
62
    exit (-1)
63
end
64

    
65
begin
66
    PUBLIC_CLOUD_EC2_CONF = YAML::load(File.read(EC2_DRIVER_CONF))
67
rescue Exception => e
68
    str_error="Unable to read '#{EC2_DRIVER_CONF}'. Invalid YAML syntax:\n" +
69
                e.message + "\n********Stack trace from EC2 IM driver*********\n"
70
    raise str_error
71
end
72

    
73
# The main class for the EC2 driver
74
class EC2Driver
75
    ACTION          = VirtualMachineDriver::ACTION
76
    POLL_ATTRIBUTE  = VirtualMachineDriver::POLL_ATTRIBUTE
77
    VM_STATE        = VirtualMachineDriver::VM_STATE
78

    
79
    # Key that will be used to store the monitoring information in the template
80
    EC2_MONITOR_KEY = "EC2DRIVER_MONITOR"
81

    
82
    # EC2 commands constants
83
    EC2 = {
84
        :run => {
85
            :cmd => :create,
86
            :args => {
87
                "AKI" => {
88
                    :opt => 'kernel_id'
89
                },
90
                "AMI" => {
91
                    :opt => 'image_id'
92
                },
93
                "BLOCKDEVICEMAPPING" => {
94
                    :opt => 'block_device_mappings',
95
                    :proc => lambda {|str|
96
                        str.split(' ').collect { |s|
97
                            dev, tmp = s.split('=')
98
                            hash = Hash.new
99
                            hash[:device_name] = dev
100
                            if tmp == "none"
101
                                hash[:no_device] = dev
102
                            else
103
                                hash[:ebs] = Hash.new
104
                                tmp_a = tmp.split(':')
105
                                hash[:ebs][:snapshot_id] = tmp_a[0] if tmp_a[0] && !tmp_a[0].empty?
106
                                hash[:ebs][:volume_size] = tmp_a[1].to_i if tmp_a[1] && !tmp_a[1].empty?
107
                                if tmp_a[2] == "false"
108
                                    hash[:ebs][:delete_on_termination] = false
109
                                elsif tmp_a[2] == "true"
110
                                    hash[:ebs][:delete_on_termination] = true
111
                                end
112
                                hash[:ebs][:volume_type] = tmp_a[3] if tmp_a[3] && !tmp_a[3].empty?
113
                                hash[:ebs][:iops] = tmp_a[4].to_i if tmp_a[4] && !tmp_a[4].empty?
114
                            end
115
                            hash
116
                        }
117
                    }
118
                },
119
                "CLIENTTOKEN" => {
120
                    :opt => 'client_token'
121
                },
122
                "INSTANCETYPE" => {
123
                    :opt => 'instance_type'
124
                },
125
                "KEYPAIR" => {
126
                    :opt => 'key_name'
127
                },
128
                "LICENSEPOOL" => {
129
                    :opt => 'license/pool'
130
                },
131
                "PLACEMENTGROUP" => {
132
                    :opt => 'placement/group_name'
133
                },
134
                "PRIVATEIP" => {
135
                    :opt => 'private_ip_address'
136
                },
137
                "RAMDISK" => {
138
                    :opt => 'ramdisk_id'
139
                },
140
                "SUBNETID" => {
141
                    :opt => 'subnet_id'
142
                },
143
                "TENANCY" => {
144
                    :opt => 'placement/tenancy'
145
                },
146
                "USERDATA" => {
147
                    :opt => 'user_data'
148
                },
149
                #"USERDATAFILE" => {
150
                #    :opt => '-f'
151
                #},
152
                "SECURITYGROUPS" => {
153
                    :opt => 'security_groups',
154
                    :proc => lambda {|str| str.split(/,\s*/)}
155
                },
156
                "SECURITYGROUPIDS" => {
157
                    :opt => 'security_group_ids',
158
                    :proc => lambda {|str| str.split(/,\s*/)}
159
                },
160
                "AVAILABILITYZONE" => {
161
                    :opt => 'placement/availability_zone'
162
                },
163
                "EBS_OPTIMIZED" => {
164
                    :opt => 'ebs_optimized',
165
                    :proc => lambda {|str| str.downcase.eql? "true"}
166
                }
167
            }
168
        },
169
        :terminate => {
170
            :cmd => :terminate
171
        },
172
        :describe => {
173
            :cmd => :describe_instances
174
        },
175
        :associate => {
176
            :cmd => :associate_address,
177
            :args => {
178
                #"SUBNETID"  => {
179
                #    :opt  => '-a',
180
                #    :proc => lambda {|str| ''}
181
                #},
182
                "ELASTICIP" => {
183
                    :opt => 'public_ip'
184
                }
185
            }
186
        },
187
        :authorize => {
188
            :cmd => :authorize,
189
            :args => {
190
                "AUTHORIZEDPORTS" => {
191
                    :opt => '-p',
192
                    :proc => lambda {|str| str.split(',').join(' -p ')}
193
                }
194
            }
195
        },
196
        :reboot => {
197
            :cmd => :reboot
198
        },
199
        :stop => {
200
            :cmd => :stop
201
        },
202
        :start => {
203
            :cmd => :start
204
        },
205
        :tags => {
206
            :cmd => :create_tags,
207
            :args => {
208
                "TAGS" => {
209
                    :opt  => 'tags',
210
                    :proc => lambda {|str|
211
                        hash = {}
212
                        str.split(',').each {|s|
213
                            k,v = s.split('=')
214
                            hash[k] = v
215
                        }
216
                        hash
217
                    }
218
                }
219
            }
220
        }
221
    }
222

    
223
    # EC2 attributes that will be retrieved in a polling action
224
    EC2_POLL_ATTRS = [
225
        :public_dns_name,
226
        :private_dns_name,
227
        :key_name,
228
        # not available as a method, should get placement/availability_zone
229
        # :availability_zone,
230
        :platform,
231
        :vpc_id,
232
        :private_ip_address,
233
        :public_ip_address,
234
        :subnet_id,
235
        :security_groups,
236
        :instance_type,
237
        :image_id
238
    ]
239

    
240
    # EC2 constructor, loads credentials and endpoint
241
    def initialize(host, host_id=nil)
242
        @host    = host
243
        @host_id = host_id
244

    
245
        @state_change_timeout = PUBLIC_CLOUD_EC2_CONF['state_wait_timeout_seconds'].to_i
246

    
247
        @instance_types = PUBLIC_CLOUD_EC2_CONF['instance_types']
248

    
249
        conn_opts = get_connect_info(host)
250
        access_key = conn_opts[:access]
251
        secret_key = conn_opts[:secret]
252
        region_name = conn_opts[:region]
253

    
254
        #sanitize region data
255
        raise "access_key_id not defined for #{host}" if access_key.nil?
256
        raise "secret_access_key not defined for #{host}" if secret_key.nil?
257
        raise "region_name not defined for #{host}" if region_name.nil?
258

    
259
        Aws.config.merge!({
260
            :access_key_id      => access_key,
261
            :secret_access_key  => secret_key,
262
            :region             => region_name
263
        })
264

    
265
        if (proxy_uri = PUBLIC_CLOUD_EC2_CONF['proxy_uri'])
266
            Aws.config(:proxy_uri => proxy_uri)
267
        end
268

    
269
        @ec2 = Aws::EC2::Resource.new
270
    end
271

    
272
    # Check the current template of host
273
    # to retrieve connection information
274
    # needed for Amazon
275
    def get_connect_info(host)
276
        conn_opts={}
277

    
278
        client   = OpenNebula::Client.new
279
        pool = OpenNebula::HostPool.new(client)
280
        pool.info
281
        objects=pool.select {|object| object.name==host }
282
        xmlhost = objects.first
283

    
284
        system = OpenNebula::System.new(client)
285
        config = system.get_configuration
286
        raise "Error getting oned configuration : #{config.message}" if OpenNebula.is_error?(config)
287

    
288
        token = config["ONE_KEY"]
289

    
290
        conn_opts = {
291
            :access => xmlhost["TEMPLATE/EC2_ACCESS"],
292
            :secret => xmlhost["TEMPLATE/EC2_SECRET"]
293
        }
294

    
295
        begin
296
            conn_opts = OpenNebula.decrypt(conn_opts, token)
297
            conn_opts[:region] = xmlhost["TEMPLATE/REGION_NAME"]
298
        rescue
299
            raise "HOST: #{host} must have ec2 credentials and region in order to work properly"
300
        end
301

    
302
        return conn_opts
303
    end
304

    
305
    # DEPLOY action, also sets ports and ip if needed
306
    def deploy(id, host, xml_text, lcm_state, deploy_id)
307

    
308
        # Restore if we need to
309
        if lcm_state != "BOOT" && lcm_state != "BOOT_FAILURE"
310
            restore(deploy_id)
311
            return deploy_id
312
        end
313

    
314
        # Otherwise deploy the VM
315

    
316
        begin
317
            ec2_info = get_deployment_info(host, xml_text)
318
        rescue Exception => e
319
            raise e
320
        end
321

    
322
        load_default_template_values
323

    
324
        if !ec2_value(ec2_info, 'AMI')
325
            raise "Cannot find AMI in deployment file"
326
        end
327

    
328
        opts = generate_options(:run, ec2_info, {
329
            :min_count => 1,
330
            :max_count => 1})
331

    
332
        # The OpenNebula context will be only included if not USERDATA
333
        #   is provided by the user
334
        if !ec2_value(ec2_info, 'USERDATA')
335
            xml = OpenNebula::XMLElement.new
336
            xml.initialize_xml(xml_text, 'VM')
337
        end
338

    
339
        if xml.has_elements?('TEMPLATE/CONTEXT')
340
            # Since there is only 1 level ',' will not be added
341
            context_str = xml.template_like_str('TEMPLATE/CONTEXT')
342

    
343
            if xml['TEMPLATE/CONTEXT/TOKEN'] == 'YES'
344
                # TODO use OneGate library
345
                token_str = generate_onegate_token(xml)
346
                if token_str
347
                    context_str << "\nONEGATE_TOKEN=\"#{token_str}\""
348
                end
349
            end
350

    
351
            userdata_key = EC2[:run][:args]["USERDATA"][:opt]
352
            opts[userdata_key] = Base64.encode64(context_str)
353
        end
354

    
355
        instances = @ec2.create_instances(opts)
356
        instance = instances.first
357

    
358
        start_time = Time.now
359

    
360
        while Time.now - start_time < @state_change_timeout
361
            begin
362
                break if instance.exists?
363
            rescue => e
364
                OpenNebula::log_error("RESCUE: #{e.inspect}")
365
            end
366

    
367
            sleep 2
368
        end
369

    
370
        tags = generate_options(:tags, ec2_info)[:tags] || {}
371

    
372
        tag_array = []
373
        tags.each{ |key,value|
374
            tag_array << {
375
                :key => key,
376
                :value => value
377
            }
378
        }
379

    
380
        instance.create_tags(:tags => tag_array) if tag_array.length > 0
381

    
382
        elastic_ip = ec2_value(ec2_info, 'ELASTICIP')
383

    
384
        wait_state('running', instance.id)
385

    
386
        if elastic_ip
387

    
388
            if elastic_ip.match(Resolv::IPv4::Regex)
389
                address_key = :public_ip
390
            else
391
                address_key = :allocation_id
392
            end
393

    
394
            address = {
395
                :instance_id    => instance.id,
396
                address_key     => elastic_ip
397
            }
398

    
399
            @ec2.client.associate_address(address)
400
        end
401

    
402

    
403
        instance.create_tags(tags: [{
404
            key: 'ONE_ID',
405
            value: id
406
        }])
407

    
408
        puts(instance.id)
409
    end
410

    
411
    # Shutdown a EC2 instance
412
    def shutdown(deploy_id, lcm_state)
413
        case lcm_state
414
            when "SHUTDOWN"
415
                ec2_action(deploy_id, :terminate)
416
            when "SHUTDOWN_POWEROFF", "SHUTDOWN_UNDEPLOY"
417
                ec2_action(deploy_id, :stop)
418
        end
419
    end
420

    
421
    # Reboot a EC2 instance
422
    def reboot(deploy_id)
423
        ec2_action(deploy_id, :reboot)
424
    end
425

    
426
    # Cancel a EC2 instance
427
    def cancel(deploy_id)
428
        ec2_action(deploy_id, :terminate)
429
    end
430

    
431
    # Save a EC2 instance
432
    def save(deploy_id)
433
        wait_state('running', deploy_id)
434
        ec2_action(deploy_id, :stop)
435
        wait_state('stopped', deploy_id)
436
    end
437

    
438
    # Resumes a EC2 instance
439
    def restore(deploy_id)
440
        wait_state('stopped', deploy_id)
441
        ec2_action(deploy_id, :start)
442
    end
443

    
444
    # Get info (IP, and state) for a EC2 instance
445
    def poll(id, deploy_id)
446
        i = get_instance(deploy_id)
447
        vm = OpenNebula::VirtualMachine.new_with_id(id, OpenNebula::Client.new)
448
        vm.info
449
        cw_mon_time = vm["LAST_POLL"] ? vm["LAST_POLL"].to_i : Time.now.to_i
450
        do_cw = (Time.now.to_i - cw_mon_time) >= 360
451
        puts parse_poll(i, vm, do_cw, cw_mon_time)
452
    end
453

    
454
    # Parse template instance type into
455
    # Amazon ec2 format (M1SMALL => m1.small)
456
    def parse_inst_type(type)
457
        fixed_type = type[0..1]<< '.' << type[2..type.length+1]
458
        return fixed_type.downcase
459
    end
460

    
461
    # Get the info of all the EC2 instances. An EC2 instance must include
462
    #   the ONE_ID tag, otherwise it will be ignored
463
    def monitor_all_vms
464
        totalmemory = 0
465
        totalcpu = 0
466

    
467
        # Get last cloudwatch monitoring time
468
        host_obj    = OpenNebula::Host.new_with_id(@host_id,
469
                                                  OpenNebula::Client.new)
470
        host_obj.info
471
        cw_mon_time = host_obj["/HOST/TEMPLATE/CWMONTIME"]
472
        capacity = host_obj.to_hash["HOST"]["TEMPLATE"]["CAPACITY"]
473
        if !capacity.nil? && Hash === capacity
474
            capacity.each{ |name, value|
475
                name = parse_inst_type(name)
476
                cpu, mem = instance_type_capacity(name)
477
                totalmemory += mem * value.to_i
478
                totalcpu    += cpu * value.to_i
479
            }
480
        else
481
            raise "you must define CAPACITY section properly! check the template"
482
        end
483

    
484
        host_info =  "HYPERVISOR=ec2\n"
485
        host_info << "PUBLIC_CLOUD=YES\n"
486
        host_info << "PRIORITY=-1\n"
487
        host_info << "TOTALMEMORY=#{totalmemory.round}\n"
488
        host_info << "TOTALCPU=#{totalcpu}\n"
489
        host_info << "CPUSPEED=1000\n"
490
        host_info << "HOSTNAME=\"#{@host}\"\n"
491

    
492
        vms_info = "VM_POLL=YES\n"
493

    
494
        #
495
        # Add information for running VMs (running and pending).
496
        #
497
        usedcpu    = 0
498
        usedmemory = 0
499

    
500
        # Build an array of VMs and last_polls for monitoring
501
        vpool      = OpenNebula::VirtualMachinePool.new(OpenNebula::Client.new,
502
                                    OpenNebula::VirtualMachinePool::INFO_ALL_VM)
503
        vpool.info
504
        onevm_info = {}
505

    
506

    
507
        if !cw_mon_time
508
            cw_mon_time = Time.now.to_i
509
        else
510
            cw_mon_time = cw_mon_time.to_i
511
        end
512

    
513
        do_cw = (Time.now.to_i - cw_mon_time) >= 360
514
        vpool.each{
515
            |vm| onevm_info[vm.deploy_id] = vm
516
        }
517

    
518

    
519
        work_q = Queue.new
520
        @ec2.instances.each{|i| work_q.push i }
521
                workers = (0...20).map do
522
            Thread.new do
523
                begin
524
                    while i = work_q.pop(true)
525
                        next if i.state.name != 'pending' && i.state.name != 'running'
526
                        one_id = i.tags.find {|t| t.key == 'ONE_ID' }
527
                        one_id = one_id.value if one_id
528
                        poll_data=parse_poll(i, onevm_info[i.id], do_cw, cw_mon_time)
529
                        vm_template_to_one = vm_to_one(i)
530
                        vm_template_to_one = Base64.encode64(vm_template_to_one).gsub("\n","")
531
                        vms_info << "VM=[\n"
532
                        vms_info << "  ID=#{one_id || -1},\n"
533
                        vms_info << "  DEPLOY_ID=#{i.instance_id},\n"
534
                        vms_info << "  VM_NAME=#{i.instance_id},\n"
535
                        vms_info << "  IMPORT_TEMPLATE=\"#{vm_template_to_one}\",\n"
536
                        vms_info << "  POLL=\"#{poll_data}\" ]\n"
537
                        if one_id
538
                            name = i.instance_type
539
                            cpu, mem = instance_type_capacity(name)
540
                            usedcpu += cpu
541
                            usedmemory += mem
542
                        end
543
                    end
544
                rescue Exception => e
545
                end
546
            end
547
        end; "ok"
548
        workers.map(&:join); "ok"
549

    
550
        host_info << "USEDMEMORY=#{usedmemory.round}\n"
551
        host_info << "USEDCPU=#{usedcpu.round}\n"
552
        host_info << "FREEMEMORY=#{(totalmemory - usedmemory).round}\n"
553
        host_info << "FREECPU=#{(totalcpu - usedcpu).round}\n"
554

    
555
        if do_cw
556
            host_info << "CWMONTIME=#{Time.now.to_i}"
557
        else
558
            host_info << "CWMONTIME=#{cw_mon_time}"
559
        end
560

    
561
        puts host_info
562
        puts vms_info
563
    end
564

    
565
private
566

    
567
    #Get the associated capacity of the instance_type as cpu (in 100 percent
568
    #e.g. 800) and memory (in KB)
569
    def instance_type_capacity(name)
570
        return 0, 0 if @instance_types[name].nil?
571
        return (@instance_types[name]['cpu'].to_f * 100).to_i ,
572
               (@instance_types[name]['memory'].to_f * 1024 * 1024).to_i
573
    end
574

    
575
    # Get the EC2 section of the template. If more than one EC2 section
576
    # the CLOUD element is used and matched with the host
577
    def get_deployment_info(host, xml_text)
578
        xml = REXML::Document.new xml_text
579

    
580
        ec2 = nil
581
        ec2_deprecated = nil
582

    
583
        all_ec2_elements = xml.root.get_elements("//USER_TEMPLATE/PUBLIC_CLOUD")
584

    
585
        # First, let's see if we have an EC2 site that matches
586
        # our desired host name
587
        all_ec2_elements.each { |element|
588
            cloud=element.elements["HOST"]
589
            if cloud && cloud.text.upcase == host.upcase
590
                ec2 = element
591
            else
592
                cloud=element.elements["CLOUD"]
593
                if cloud && cloud.text.upcase == host.upcase
594
                    ec2_deprecated = element
595
                end
596
            end
597
        }
598

    
599
        ec2 ||= ec2_deprecated
600

    
601
        if !ec2
602
            # If we don't find the EC2 site, and ONE just
603
            # knows about one EC2 site, let's use that
604
            if all_ec2_elements.size == 1
605
                ec2 = all_ec2_elements[0]
606
            else
607
        raise RuntimeError.new("Cannot find PUBLIC_CLOUD element in deployment "\
608
                    " file or no HOST site matching the requested in the "\
609
                    " template.")
610
            end
611
        end
612

    
613
        ec2
614
    end
615

    
616
    # Retrieve the vm information from the EC2 instance
617
    def parse_poll(instance, onevm, do_cw, cw_mon_time)
618
        begin
619
            if onevm
620
                if do_cw
621
                    cloudwatch_str = cloudwatch_monitor_info(instance.instance_id,
622
                                                           onevm,
623
                                                           cw_mon_time)
624
                else
625
                    previous_cpu   = onevm["MONITORING/CPU"]  || 0
626
                    previous_netrx = onevm["MONITORING/NETRX"] || 0
627
                    previous_nettx = onevm["MONITORING/NETTX"] || 0
628

    
629
                    cloudwatch_str = "CPU=#{previous_cpu} NETTX=#{previous_nettx} NETRX=#{previous_netrx} "
630
                end
631
            else
632
                cloudwatch_str = ""
633
            end
634

    
635
            mem = onevm["TEMPLATE/MEMORY"].to_s
636
            mem=mem.to_i*1024
637
            info =  "#{POLL_ATTRIBUTE[:memory]}=#{mem} #{cloudwatch_str}"
638

    
639
            state = ""
640
            if !instance.exists?
641
                state = VM_STATE[:deleted]
642
            else
643
                state = case instance.state.name
644
                when 'pending'
645
                    VM_STATE[:active]
646
                when 'running'
647
                    VM_STATE[:active]
648
                when 'shutting-down', 'terminated'
649
                    VM_STATE[:deleted]
650
                else
651
                    VM_STATE[:unknown]
652
                end
653
            end
654
            info << "#{POLL_ATTRIBUTE[:state]}=#{state} "
655

    
656
            EC2_POLL_ATTRS.map { |key|
657
                value = instance.send(key)
658
                if !value.nil? && !value.empty?
659
                    if value.is_a?(Array)
660
                        value = value.map {|v|
661
                            v.group_id if v.is_a?(Aws::EC2::Types::GroupIdentifier)
662
                        }.join(",")
663
                    end
664

    
665
                    info << "AWS_#{key.to_s.upcase}=\\\"#{URI::encode(value)}\\\" "
666
                end
667
            }
668

    
669
            info
670
        rescue
671
            # Unkown state if exception occurs retrieving information from
672
            # an instance
673
            "#{POLL_ATTRIBUTE[:state]}=#{VM_STATE[:unknown]} "
674
        end
675
    end
676

    
677

    
678
    # Execute an EC2 command
679
    # +deploy_id+: String, VM id in EC2
680
    # +ec2_action+: Symbol, one of the keys of the EC2 hash constant (i.e :run)
681
    def ec2_action(deploy_id, ec2_action)
682
        begin
683
        i = get_instance(deploy_id)
684
        i.send(EC2[ec2_action][:cmd])
685
        rescue => e
686
                raise e
687
        end
688
    end
689

    
690
    # Generate the options for the given command from the xml provided in the
691
    #   template. The available options for each command are defined in the EC2
692
    #   constant
693
    def generate_options(action, xml, extra_params={})
694
        opts = extra_params || {}
695

    
696
        if EC2[action][:args]
697
            EC2[action][:args].each {|k,v|
698
                str = ec2_value(xml, k, &v[:proc])
699
                if str
700
                    tmp = opts
701
                    last_key = nil
702
                    v[:opt].split('/').each { |key|
703
                        k = key.to_sym
704
                        tmp = tmp[last_key] if last_key
705
                        tmp[k] = {}
706
                        last_key = k
707
                    }
708
                    tmp[last_key] = str
709
                end
710
            }
711
        end
712

    
713
        opts
714
    end
715

    
716
    # Returns the value of the xml specified by the name or the default
717
    # one if it does not exist
718
    # +xml+: REXML Document, containing EC2 information
719
    # +name+: String, xpath expression to retrieve the value
720
    # +block+: Block, block to be applied to the value before returning it
721
    def ec2_value(xml, name, &block)
722
        value = value_from_xml(xml, name) || @defaults[name]
723
        if block_given? && value
724
            block.call(value)
725
        else
726
            value
727
        end
728
    end
729

    
730
    def value_from_xml(xml, name)
731
        if xml
732
            element = xml.elements[name]
733
            element.text.strip if element && element.text
734
        end
735
    end
736

    
737
    # Waits until ec2 machine reach the desired state
738
    # +state+: String, is the desired state, needs to be a real state of Amazon ec2:  running, stopped, terminated, pending  
739
    # +deploy_id+: String, VM id in EC2
740
    def wait_state(state, deploy_id)
741
        ready = (state == 'stopped') || (state == 'pending') || (state == 'running') || (state == 'terminated')
742
        raise "Waiting for an invalid state" if !ready
743
        t_init = Time.now
744
        begin
745
            wstate = get_instance(deploy_id).state.name rescue nil
746
            raise "Ended in invalid state" if Time.now - t_init > @state_change_timeout
747
            sleep 3
748
        end while wstate != state
749
    end
750

    
751
    # Load the default values that will be used to create a new instance, if
752
    #   not provided in the template. These values are defined in the EC2_CONF
753
    #   file
754
    def load_default_template_values
755
        @defaults = Hash.new
756

    
757
        if File.exist?(EC2_DRIVER_DEFAULT)
758
            fd  = File.new(EC2_DRIVER_DEFAULT)
759
            xml = REXML::Document.new fd
760
            fd.close()
761

    
762
            return if !xml || !xml.root
763

    
764
            ec2 = xml.root.elements["PUBLIC_CLOUD"]
765

    
766
            return if !ec2
767

    
768
            EC2.each {|action, hash|
769
                if hash[:args]
770
                    hash[:args].each { |key, value|
771
                        @defaults[key.to_sym] = value_from_xml(ec2, key)
772
                    }
773
                end
774
            }
775
        end
776
    end
777

    
778
    # Retrieve the instance from EC2
779
    def get_instance(id)
780
        begin
781
            instance = @ec2.instance(id)
782
            if instance.exists?
783
                return instance
784
            else
785
                raise RuntimeError.new("Instance #{id} does not exist")
786
            end
787
        rescue => e
788
            raise e
789
        end
790
    end
791

    
792
    # Build template for importation
793
    def vm_to_one(instance)
794
        cpu, mem = instance_type_capacity(instance.instance_type)
795

    
796
        cpu  = cpu.to_f / 100
797
        vcpu = cpu.ceil
798
        mem  = mem.to_i / 1024 # Memory for templates expressed in MB
799

    
800
        str = "NAME   = \"Instance from #{instance.id}\"\n"\
801
              "CPU    = \"#{cpu}\"\n"\
802
              "VCPU   = \"#{vcpu}\"\n"\
803
              "MEMORY = \"#{mem}\"\n"\
804
              "HYPERVISOR = \"ec2\"\n"\
805
              "PUBLIC_CLOUD = [\n"\
806
              "  TYPE  =\"ec2\",\n"\
807
              "  AMI   =\"#{instance.image_id}\"\n"\
808
              "]\n"\
809
              "IMPORT_VM_ID    = \"#{instance.id}\"\n"\
810
              "SCHED_REQUIREMENTS=\"NAME=\\\"#{@host}\\\"\"\n"\
811
              "DESCRIPTION = \"Instance imported from EC2, from instance"\
812
              " #{instance.id}, AMI #{instance.image_id}\"\n"
813

    
814
        str
815
    end
816

    
817
    # Extract monitoring information from Cloud Watch
818
    # CPU, NETTX and NETRX
819
    def cloudwatch_monitor_info(id, onevm, cw_mon_time)
820
        cw=Aws::CloudWatch::Client.new
821

    
822
        # CPU
823
        begin
824
            cpu = get_cloudwatch_metric(cw,
825
                                        "CPUUtilization",
826
                                        cw_mon_time,
827
                                        ["Average"],
828
                                         "Percent",
829
                                         id)
830
            if cpu[:datapoints].size != 0
831
                cpu = cpu[:datapoints][-1][:average]
832
            else
833
                cpu = 0
834
            end
835
            cpu = cpu.to_f.round(2).to_s
836
        rescue => e
837
            OpenNebula::log_error(e.message)
838
        end
839

    
840
        # NETTX
841
        nettx = 0
842
        begin
843
            nettx_dp = get_cloudwatch_metric(cw,
844
                                             "NetworkOut",
845
                                             cw_mon_time,
846
                                             ["Sum"],
847
                                             "Bytes",
848
                                             id)[:datapoints]
849
            previous_nettx = onevm["/VM/MONITORING/NETTX"]
850
            nettx = previous_nettx ? previous_nettx.to_i : 0
851

    
852
            nettx_dp.each{|dp|
853
                nettx += dp[:sum].to_i
854
            }
855
        rescue => e
856
            OpenNebula::log_error(e.message)
857
        end
858

    
859
        # NETRX
860
        netrx = 0
861
        begin
862
            netrx_dp = get_cloudwatch_metric(cw,
863
                                             "NetworkIn",
864
                                             cw_mon_time,
865
                                             ["Sum"],
866
                                             "Bytes",
867
                                             id)[:datapoints]
868
            previous_netrx = onevm["/VM/MONITORING/NETRX"]
869
            netrx = previous_netrx ? previous_netrx.to_i : 0
870

    
871
            netrx_dp.each{|dp|
872
                netrx += dp[:sum].to_i
873
            }
874
        rescue => e
875
            OpenNebula::log_error(e.message)
876
        end
877

    
878
        "CPU=#{cpu.to_s} NETTX=#{nettx.to_s} NETRX=#{netrx.to_s} "
879
    end
880

    
881
    # Get metric from AWS/EC2 namespace from the last poll
882
    def get_cloudwatch_metric(cw, metric_name, last_poll, statistics, units, id)
883
        dt = 60                              # period
884
        t0 = (Time.at(last_poll.to_i)-65)    # last poll time
885
        t = (Time.now-60)                    # actual time
886

    
887
        while ((t - t0)/dt >= 1440) do dt+=60 end
888

    
889
        options={:namespace=>"AWS/EC2",
890
                 :metric_name=>metric_name,
891
                 :start_time=> t0.iso8601,
892
                 :end_time=> t.iso8601,
893
                 :period=>dt,
894
                 :statistics=>statistics,
895
                 :unit=>units,
896
                 :dimensions=>[{:name=>"InstanceId", :value=>id}]}
897

    
898
        cw.get_metric_statistics(options)
899
    end
900

    
901
    # TODO move this method to a OneGate library
902
    def generate_onegate_token(xml)
903
        # Create the OneGate token string
904
        vmid_str  = xml["ID"]
905
        stime_str = xml["STIME"]
906
        str_to_encrypt = "#{vmid_str}:#{stime_str}"
907

    
908
        user_id = xml['TEMPLATE/CREATED_BY']
909

    
910
        if user_id.nil?
911
            OpenNebula::log_error("VMID:#{vNid} CREATED_BY not present" \
912
                " in the VM TEMPLATE")
913
            return nil
914
        end
915

    
916
        user = OpenNebula::User.new_with_id(user_id,
917
                                            OpenNebula::Client.new)
918
        rc   = user.info
919

    
920
        if OpenNebula.is_error?(rc)
921
            OpenNebula::log_error("VMID:#{vmid} user.info" \
922
                " error: #{rc.message}")
923
            return nil
924
        end
925

    
926
        token_password = user['TEMPLATE/TOKEN_PASSWORD']
927

    
928
        if token_password.nil?
929
            OpenNebula::log_error(VMID:#{vmid} TOKEN_PASSWORD not present"\
930
                " in the USER:#{user_id} TEMPLATE")
931
            return nil
932
        end
933

    
934
        cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
935
        cipher.encrypt
936
        cipher.key = token_password
937
        onegate_token = cipher.update(str_to_encrypt)
938
        onegate_token << cipher.final
939

    
940
        onegate_token_64 = Base64.encode64(onegate_token).chop
941
    end
942
end
943