Statistics
| Branch: | Tag: | Revision:

one / src / vmm_mad / remotes / ec2 / ec2_driver.rb @ 2911648e

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
        return type.downcase.gsub("_", ".")
458
    end
459

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

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

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

    
491
        vms_info = "VM_POLL=YES\n"
492

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

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

    
505

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

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

    
517

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

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

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

    
560
        puts host_info
561
        puts vms_info
562
    end
563

    
564
private
565

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

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

    
579
        ec2 = nil
580
        ec2_deprecated = nil
581

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

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

    
598
        ec2 ||= ec2_deprecated
599

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

    
612
        ec2
613
    end
614

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

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

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

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

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

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

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

    
676

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

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

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

    
712
        opts
713
    end
714

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

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

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

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

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

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

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

    
765
            return if !ec2
766

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

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

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

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

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

    
813
        str
814
    end
815

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

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

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

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

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

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

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

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

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

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

    
897
        cw.get_metric_statistics(options)
898
    end
899

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

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

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

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

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

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

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

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

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