beaker-aws 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1001 @@
1
+ require 'aws/ec2'
2
+ require 'set'
3
+ require 'zlib'
4
+ require 'beaker/hypervisor/ec2_helper'
5
+
6
+ module Beaker
7
+ # This is an alternate EC2 driver that implements direct API access using
8
+ # Amazon's AWS-SDK library: {http://aws.amazon.com/documentation/sdkforruby/ SDK For Ruby}
9
+ #
10
+ # It is built for full control, to reduce any other layers beyond the pure
11
+ # vendor API.
12
+ class AwsSdk < Beaker::Hypervisor
13
+ ZOMBIE = 3 #anything older than 3 hours is considered a zombie
14
+ PING_SECURITY_GROUP_NAME = 'beaker-ping'
15
+
16
+ # Initialize AwsSdk hypervisor driver
17
+ #
18
+ # @param [Array<Beaker::Host>] hosts Array of Beaker::Host objects
19
+ # @param [Hash<String, String>] options Options hash
20
+ def initialize(hosts, options)
21
+ @hosts = hosts
22
+ @options = options
23
+ @logger = options[:logger]
24
+
25
+ # Get AWS credentials
26
+ creds = load_credentials()
27
+
28
+ config = {
29
+ :access_key_id => creds[:access_key],
30
+ :secret_access_key => creds[:secret_key],
31
+ :logger => Logger.new($stdout),
32
+ :log_level => :debug,
33
+ :log_formatter => AWS::Core::LogFormatter.colored,
34
+ :max_retries => 12,
35
+ }
36
+ AWS.config(config)
37
+
38
+ @ec2 = AWS::EC2.new()
39
+ test_split_install()
40
+ end
41
+
42
+ # Provision all hosts on EC2 using the AWS::EC2 API
43
+ #
44
+ # @return [void]
45
+ def provision
46
+ start_time = Time.now
47
+
48
+ # Perform the main launch work
49
+ launch_all_nodes()
50
+
51
+ wait_for_status_netdev()
52
+
53
+ # Add metadata tags to each instance
54
+ add_tags()
55
+
56
+ # Grab the ip addresses and dns from EC2 for each instance to use for ssh
57
+ populate_dns()
58
+
59
+ #enable root if user is not root
60
+ enable_root_on_hosts()
61
+
62
+ # Set the hostname for each box
63
+ set_hostnames()
64
+
65
+ # Configure /etc/hosts on each host
66
+ configure_hosts()
67
+
68
+ @logger.notify("aws-sdk: Provisioning complete in #{Time.now - start_time} seconds")
69
+
70
+ nil #void
71
+ end
72
+
73
+ # Kill all instances.
74
+ #
75
+ # @param instances [Enumerable<EC2::Instance>]
76
+ # @return [void]
77
+ def kill_instances(instances)
78
+ instances.each do |instance|
79
+ if !instance.nil? and instance.exists?
80
+ @logger.notify("aws-sdk: killing EC2 instance #{instance.id}")
81
+ instance.terminate
82
+ end
83
+ end
84
+ nil
85
+ end
86
+
87
+ # Cleanup all earlier provisioned hosts on EC2 using the AWS::EC2 library
88
+ #
89
+ # It goes without saying, but a #cleanup does nothing without a #provision
90
+ # method call first.
91
+ #
92
+ # @return [void]
93
+ def cleanup
94
+ # Provisioning should have set the host 'instance' values.
95
+ kill_instances(@hosts.map{|h| h['instance']}.select{|x| !x.nil?})
96
+ delete_key_pair_all_regions()
97
+ nil
98
+ end
99
+
100
+ # Print instances to the logger. Instances will be from all regions
101
+ # associated with provided key name and limited by regex compared to
102
+ # instance status. Defaults to running instances.
103
+ #
104
+ # @param [String] key The key_name to match for
105
+ # @param [Regex] status The regular expression to match against the instance's status
106
+ def log_instances(key = key_name, status = /running/)
107
+ instances = []
108
+ @ec2.regions.each do |region|
109
+ @logger.debug "Reviewing: #{region.name}"
110
+ @ec2.regions[region.name].instances.each do |instance|
111
+ if (instance.key_name =~ /#{key}/) and (instance.status.to_s =~ status)
112
+ instances << instance
113
+ end
114
+ end
115
+ end
116
+ output = ""
117
+ instances.each do |instance|
118
+ output << "#{instance.id} keyname: #{instance.key_name}, dns name: #{instance.dns_name}, private ip: #{instance.private_ip_address}, ip: #{instance.ip_address}, launch time #{instance.launch_time}, status: #{instance.status}\n"
119
+ end
120
+ @logger.notify("aws-sdk: List instances (keyname: #{key})")
121
+ @logger.notify("#{output}")
122
+ end
123
+
124
+ # Provided an id return an instance object.
125
+ # Instance object will respond to methods described here: {http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/EC2/Instance.html AWS Instance Object}.
126
+ # @param [String] id The id of the instance to return
127
+ # @return [AWS::EC2::Instance] An AWS::EC2 instance object
128
+ def instance_by_id(id)
129
+ @ec2.instances[id]
130
+ end
131
+
132
+ # Return all instances currently on ec2.
133
+ # @see AwsSdk#instance_by_id
134
+ # @return [AWS::EC2::InstanceCollection] An array of AWS::EC2 instance objects
135
+ def instances
136
+ @ec2.instances
137
+ end
138
+
139
+ # Provided an id return a VPC object.
140
+ # VPC object will respond to methods described here: {http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/EC2/VPC.html AWS VPC Object}.
141
+ # @param [String] id The id of the VPC to return
142
+ # @return [AWS::EC2::VPC] An AWS::EC2 vpc object
143
+ def vpc_by_id(id)
144
+ @ec2.vpcs[id]
145
+ end
146
+
147
+ # Return all VPCs currently on ec2.
148
+ # @see AwsSdk#vpc_by_id
149
+ # @return [AWS::EC2::VPCCollection] An array of AWS::EC2 vpc objects
150
+ def vpcs
151
+ @ec2.vpcs
152
+ end
153
+
154
+ # Provided an id return a security group object
155
+ # Security object will respond to methods described here: {http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/EC2/SecurityGroup.html AWS SecurityGroup Object}.
156
+ # @param [String] id The id of the security group to return
157
+ # @return [AWS::EC2::SecurityGroup] An AWS::EC2 security group object
158
+ def security_group_by_id(id)
159
+ @ec2.security_groups[id]
160
+ end
161
+
162
+ # Return all security groups currently on ec2.
163
+ # @see AwsSdk#security_goup_by_id
164
+ # @return [AWS::EC2::SecurityGroupCollection] An array of AWS::EC2 security group objects
165
+ def security_groups
166
+ @ec2.security_groups
167
+ end
168
+
169
+ # Shutdown and destroy ec2 instances idenfitied by key that have been alive
170
+ # longer than ZOMBIE hours.
171
+ #
172
+ # @param [Integer] max_age The age in hours that a machine needs to be older than to be considered a zombie
173
+ # @param [String] key The key_name to match for
174
+ def kill_zombies(max_age = ZOMBIE, key = key_name)
175
+ @logger.notify("aws-sdk: Kill Zombies! (keyname: #{key}, age: #{max_age} hrs)")
176
+ #examine all available regions
177
+ kill_count = 0
178
+ time_now = Time.now.getgm #ec2 uses GM time
179
+ @ec2.regions.each do |region|
180
+ @logger.debug "Reviewing: #{region.name}"
181
+ # Note: don't use instances.each here as that funtion doesn't allow proper rescue from error states
182
+ instances = @ec2.regions[region.name].instances
183
+ instances.each do |instance|
184
+ begin
185
+ if (instance.key_name =~ /#{key}/)
186
+ @logger.debug "Examining #{instance.id} (keyname: #{instance.key_name}, launch time: #{instance.launch_time}, status: #{instance.status})"
187
+ if ((time_now - instance.launch_time) > max_age*60*60) and instance.status.to_s !~ /terminated/
188
+ @logger.debug "Kill! #{instance.id}: #{instance.key_name} (Current status: #{instance.status})"
189
+ instance.terminate()
190
+ kill_count += 1
191
+ end
192
+ end
193
+ rescue AWS::Core::Resource::NotFound, AWS::EC2::Errors => e
194
+ @logger.debug "Failed to remove instance: #{instance.id}, #{e}"
195
+ end
196
+ end
197
+ end
198
+ delete_key_pair_all_regions(key_name_prefix)
199
+
200
+ @logger.notify "#{key}: Killed #{kill_count} instance(s)"
201
+ end
202
+
203
+ # Destroy any volumes marked 'available', INCLUDING THOSE YOU DON'T OWN! Use with care.
204
+ def kill_zombie_volumes
205
+ # Occasionaly, tearing down ec2 instances leaves orphaned EBS volumes behind -- these stack up quickly.
206
+ # This simply looks for EBS volumes that are not in use
207
+ # Note: don't use volumes.each here as that funtion doesn't allow proper rescue from error states
208
+ @logger.notify("aws-sdk: Kill Zombie Volumes!")
209
+ volume_count = 0
210
+ @ec2.regions.each do |region|
211
+ @logger.debug "Reviewing: #{region.name}"
212
+ volumes = @ec2.regions[region.name].volumes.map { |vol| vol.id }
213
+ volumes.each do |vol|
214
+ begin
215
+ vol = @ec2.regions[region.name].volumes[vol]
216
+ if ( vol.status.to_s =~ /available/ )
217
+ @logger.debug "Tear down available volume: #{vol.id}"
218
+ vol.delete()
219
+ volume_count += 1
220
+ end
221
+ rescue AWS::EC2::Errors::InvalidVolume::NotFound => e
222
+ @logger.debug "Failed to remove volume: #{vol.id}, #{e}"
223
+ end
224
+ end
225
+ end
226
+ @logger.notify "Freed #{volume_count} volume(s)"
227
+
228
+ end
229
+
230
+ # Create an EC2 instance for host, tag it, and return it.
231
+ #
232
+ # @return [void]
233
+ # @api private
234
+ def create_instance(host, ami_spec, subnet_id)
235
+ amitype = host['vmname'] || host['platform']
236
+ amisize = host['amisize'] || 'm1.small'
237
+ vpc_id = host['vpc_id'] || @options['vpc_id'] || nil
238
+
239
+ if vpc_id and !subnet_id
240
+ raise RuntimeError, "A subnet_id must be provided with a vpc_id"
241
+ end
242
+
243
+ # Use snapshot provided for this host
244
+ image_type = host['snapshot']
245
+ if not image_type
246
+ raise RuntimeError, "No snapshot/image_type provided for EC2 provisioning"
247
+ end
248
+ ami = ami_spec[amitype]
249
+ ami_region = ami[:region]
250
+
251
+ # Main region object for ec2 operations
252
+ region = @ec2.regions[ami_region]
253
+
254
+ # If we haven't defined a vpc_id then we use the default vpc for the provided region
255
+ if !vpc_id
256
+ @logger.notify("aws-sdk: filtering available vpcs in region by 'isDefault")
257
+ filtered_vpcs = region.client.describe_vpcs(:filters => [{:name => 'isDefault', :values => ['true']}])
258
+ if !filtered_vpcs[:vpc_set].empty?
259
+ vpc_id = filtered_vpcs[:vpc_set].first[:vpc_id]
260
+ else #there's no default vpc, use nil
261
+ vpc_id = nil
262
+ end
263
+ end
264
+
265
+ # Grab the vpc object based upon provided id
266
+ vpc = vpc_id ? region.vpcs[vpc_id] : nil
267
+
268
+ # Grab image object
269
+ image_id = ami[:image][image_type.to_sym]
270
+ @logger.notify("aws-sdk: Checking image #{image_id} exists and getting its root device")
271
+ image = region.images[image_id]
272
+ if image.nil? and not image.exists?
273
+ raise RuntimeError, "Image not found: #{image_id}"
274
+ end
275
+
276
+ @logger.notify("Image Storage Type: #{image.root_device_type}")
277
+
278
+ # Transform the images block_device_mappings output into a format
279
+ # ready for a create.
280
+ block_device_mappings = []
281
+ if image.root_device_type == :ebs
282
+ orig_bdm = image.block_device_mappings()
283
+ @logger.notify("aws-sdk: Image block_device_mappings: #{orig_bdm.to_hash}")
284
+ orig_bdm.each do |device_name, rest|
285
+ block_device_mappings << {
286
+ :device_name => device_name,
287
+ :ebs => {
288
+ # Change the default size of the root volume.
289
+ :volume_size => host['volume_size'] || rest[:volume_size],
290
+ # This is required to override the images default for
291
+ # delete_on_termination, forcing all volumes to be deleted once the
292
+ # instance is terminated.
293
+ :delete_on_termination => true,
294
+ }
295
+ }
296
+ end
297
+ end
298
+
299
+ security_group = ensure_group(vpc || region, Beaker::EC2Helper.amiports(host))
300
+ #check if ping is enabled
301
+ ping_security_group = ensure_ping_group(vpc || region)
302
+
303
+ msg = "aws-sdk: launching %p on %p using %p/%p%s" %
304
+ [host.name, amitype, amisize, image_type,
305
+ subnet_id ? ("in %p" % subnet_id) : '']
306
+ @logger.notify(msg)
307
+ config = {
308
+ :count => 1,
309
+ :image_id => image_id,
310
+ :monitoring_enabled => true,
311
+ :key_pair => ensure_key_pair(region),
312
+ :security_groups => [security_group, ping_security_group],
313
+ :instance_type => amisize,
314
+ :disable_api_termination => false,
315
+ :instance_initiated_shutdown_behavior => "terminate",
316
+ :subnet => subnet_id,
317
+ }
318
+ config[:block_device_mappings] = block_device_mappings if image.root_device_type == :ebs
319
+ region.instances.create(config)
320
+ end
321
+
322
+ # For each host, create an EC2 instance in one of the specified
323
+ # subnets and push it onto instances_created. Each subnet will be
324
+ # tried at most once for each host, and more than one subnet may
325
+ # be tried if capacity constraints are encountered. Each Hash in
326
+ # instances_created will contain an :instance and :host value.
327
+ #
328
+ # @param hosts [Enumerable<Host>]
329
+ # @param subnets [Enumerable<String>]
330
+ # @param ami_spec [Hash]
331
+ # @param instances_created Enumerable<Hash{Symbol=>EC2::Instance,Host}>
332
+ # @return [void]
333
+ # @api private
334
+ def launch_nodes_on_some_subnet(hosts, subnets, ami_spec, instances_created)
335
+ # Shuffle the subnets so we don't always hit the same one
336
+ # first, and cycle though the subnets independently of the
337
+ # host, so we stick with one that's working. Try each subnet
338
+ # once per-host.
339
+ if subnets.nil? or subnets.empty?
340
+ return
341
+ end
342
+ subnet_i = 0
343
+ shuffnets = subnets.shuffle
344
+ hosts.each do |host|
345
+ instance = nil
346
+ shuffnets.length.times do
347
+ begin
348
+ subnet_id = shuffnets[subnet_i]
349
+ instance = create_instance(host, ami_spec, subnet_id)
350
+ instances_created.push({:instance => instance, :host => host})
351
+ break
352
+ rescue AWS::EC2::Errors::InsufficientInstanceCapacity => ex
353
+ @logger.notify("aws-sdk: hit #{subnet_id} capacity limit; moving on")
354
+ subnet_i = (subnet_i + 1) % shuffnets.length
355
+ end
356
+ end
357
+ if instance.nil?
358
+ raise RuntimeError, "unable to launch host in any requested subnet"
359
+ end
360
+ end
361
+ end
362
+
363
+ # Create EC2 instances for all hosts, tag them, and wait until
364
+ # they're running. When a host provides a subnet_id, create the
365
+ # instance in that subnet, otherwise prefer a CONFIG subnet_id.
366
+ # If neither are set but there is a CONFIG subnet_ids list,
367
+ # attempt to create the host in each specified subnet, which might
368
+ # fail due to capacity constraints, for example. Specifying both
369
+ # a CONFIG subnet_id and subnet_ids will provoke an error.
370
+ #
371
+ # @return [void]
372
+ # @api private
373
+ def launch_all_nodes
374
+ @logger.notify("aws-sdk: launch all hosts in configuration")
375
+ ami_spec = YAML.load_file(@options[:ec2_yaml])["AMI"]
376
+ global_subnet_id = @options['subnet_id']
377
+ global_subnets = @options['subnet_ids']
378
+ if global_subnet_id and global_subnets
379
+ raise RuntimeError, 'Config specifies both subnet_id and subnet_ids'
380
+ end
381
+ no_subnet_hosts = []
382
+ specific_subnet_hosts = []
383
+ some_subnet_hosts = []
384
+ @hosts.each do |host|
385
+ if global_subnet_id or host['subnet_id']
386
+ specific_subnet_hosts.push(host)
387
+ elsif global_subnets
388
+ some_subnet_hosts.push(host)
389
+ else
390
+ no_subnet_hosts.push(host)
391
+ end
392
+ end
393
+ instances = [] # Each element is {:instance => i, :host => h}
394
+ begin
395
+ @logger.notify("aws-sdk: launch instances not particular about subnet")
396
+ launch_nodes_on_some_subnet(some_subnet_hosts, global_subnets, ami_spec,
397
+ instances)
398
+ @logger.notify("aws-sdk: launch instances requiring a specific subnet")
399
+ specific_subnet_hosts.each do |host|
400
+ subnet_id = host['subnet_id'] || global_subnet_id
401
+ instance = create_instance(host, ami_spec, subnet_id)
402
+ instances.push({:instance => instance, :host => host})
403
+ end
404
+ @logger.notify("aws-sdk: launch instances requiring no subnet")
405
+ no_subnet_hosts.each do |host|
406
+ instance = create_instance(host, ami_spec, nil)
407
+ instances.push({:instance => instance, :host => host})
408
+ end
409
+ wait_for_status(:running, instances)
410
+ rescue Exception => ex
411
+ @logger.notify("aws-sdk: exception #{ex.class}: #{ex}")
412
+ kill_instances(instances.map{|x| x[:instance]})
413
+ raise ex
414
+ end
415
+ # At this point, all instances should be running since wait
416
+ # either returns on success or throws an exception.
417
+ if instances.empty?
418
+ raise RuntimeError, "Didn't manage to launch any EC2 instances"
419
+ end
420
+ # Assign the now known running instances to their hosts.
421
+ instances.each {|x| x[:host]['instance'] = x[:instance]}
422
+ nil
423
+ end
424
+
425
+ # Wait until all instances reach the desired state. Each Hash in
426
+ # instances must contain an :instance and :host value.
427
+ #
428
+ # @param status [Symbol] EC2 state to wait for, :running :stopped etc.
429
+ # @param instances Enumerable<Hash{Symbol=>EC2::Instance,Host}>
430
+ # @param block [Proc] more complex checks can be made by passing a
431
+ # block in. This overrides the status parameter.
432
+ # EC2::Instance objects from the hosts will be
433
+ # yielded to the passed block
434
+ # @return [void]
435
+ # @api private
436
+ def wait_for_status(status, instances, &block)
437
+ # Wait for each node to reach status :running
438
+ @logger.notify("aws-sdk: Waiting for all hosts to be #{status}")
439
+ instances.each do |x|
440
+ name = x[:name]
441
+ instance = x[:instance]
442
+ @logger.notify("aws-sdk: Wait for node #{name} to be #{status}")
443
+ # Here we keep waiting for the machine state to reach ':running' with an
444
+ # exponential backoff for each poll.
445
+ # TODO: should probably be a in a shared method somewhere
446
+ for tries in 1..10
447
+ begin
448
+ if block_given?
449
+ test_result = yield instance
450
+ else
451
+ test_result = instance.status == status
452
+ end
453
+ if test_result
454
+ # Always sleep, so the next command won't cause a throttle
455
+ backoff_sleep(tries)
456
+ break
457
+ elsif tries == 10
458
+ raise "Instance never reached state #{status}"
459
+ end
460
+ rescue AWS::EC2::Errors::InvalidInstanceID::NotFound => e
461
+ @logger.debug("Instance #{name} not yet available (#{e})")
462
+ end
463
+ backoff_sleep(tries)
464
+ end
465
+ end
466
+ end
467
+
468
+ # Handles special checks needed for netdev platforms.
469
+ #
470
+ # @note if any host is an netdev one, these checks will happen once across all
471
+ # of the hosts, and then we'll exit
472
+ #
473
+ # @return [void]
474
+ # @api private
475
+ def wait_for_status_netdev()
476
+ @hosts.each do |host|
477
+ if host['platform'] =~ /f5-|netscaler/
478
+ wait_for_status(:running, @hosts)
479
+
480
+ wait_for_status(nil, @hosts) do |instance|
481
+ instance_status_collection = instance.client.describe_instance_status({:instance_ids => [instance.id]})
482
+ first_instance = instance_status_collection[:instance_status_set].first
483
+ first_instance[:system_status][:status] == "ok"
484
+ end
485
+
486
+ break
487
+ end
488
+ end
489
+ end
490
+
491
+ # Add metadata tags to all instances
492
+ #
493
+ # @return [void]
494
+ # @api private
495
+ def add_tags
496
+ @hosts.each do |host|
497
+ instance = host['instance']
498
+
499
+ # Define tags for the instance
500
+ @logger.notify("aws-sdk: Add tags for #{host.name}")
501
+ instance.add_tag("jenkins_build_url", :value => @options[:jenkins_build_url])
502
+ instance.add_tag("Name", :value => host.name)
503
+ instance.add_tag("department", :value => @options[:department])
504
+ instance.add_tag("project", :value => @options[:project])
505
+ instance.add_tag("created_by", :value => @options[:created_by])
506
+
507
+ host[:host_tags].each do |name, val|
508
+ instance.add_tag(name.to_s, :value => val)
509
+ end
510
+ end
511
+
512
+ nil
513
+ end
514
+
515
+ # Populate the hosts IP address from the EC2 dns_name
516
+ #
517
+ # @return [void]
518
+ # @api private
519
+ def populate_dns
520
+ # Obtain the IP addresses and dns_name for each host
521
+ @hosts.each do |host|
522
+ @logger.notify("aws-sdk: Populate DNS for #{host.name}")
523
+ instance = host['instance']
524
+ host['ip'] = instance.ip_address ? instance.ip_address : instance.private_ip_address
525
+ host['private_ip'] = instance.private_ip_address
526
+ host['dns_name'] = instance.dns_name
527
+ @logger.notify("aws-sdk: name: #{host.name} ip: #{host['ip']} private_ip: #{host['private_ip']} dns_name: #{instance.dns_name}")
528
+ end
529
+
530
+ nil
531
+ end
532
+
533
+ # Return a valid /etc/hosts line for a given host
534
+ #
535
+ # @param [Beaker::Host] host Beaker::Host object for generating /etc/hosts entry
536
+ # @param [Symbol] interface Symbol identifies which ip should be used for host
537
+ # @return [String] formatted hosts entry for host
538
+ # @api private
539
+ def etc_hosts_entry(host, interface = :ip)
540
+ name = host.name
541
+ domain = get_domain_name(host)
542
+ ip = host[interface.to_s]
543
+ "#{ip}\t#{name} #{name}.#{domain} #{host['dns_name']}\n"
544
+ end
545
+
546
+ # Configure /etc/hosts for each node
547
+ #
548
+ # @note f5 hosts are skipped since this isn't a valid step there
549
+ #
550
+ # @return [void]
551
+ # @api private
552
+ def configure_hosts
553
+ non_netdev_hosts = @hosts.select{ |h| !(h['platform'] =~ /f5-|netscaler/) }
554
+ non_netdev_hosts.each do |host|
555
+ host_entries = non_netdev_hosts.map do |h|
556
+ h == host ? etc_hosts_entry(h, :private_ip) : etc_hosts_entry(h)
557
+ end
558
+ host_entries.unshift "127.0.0.1\tlocalhost localhost.localdomain\n"
559
+ set_etc_hosts(host, host_entries.join(''))
560
+ end
561
+ nil
562
+ end
563
+
564
+ # Enables root for instances with custom username like ubuntu-amis
565
+ #
566
+ # @return [void]
567
+ # @api private
568
+ def enable_root_on_hosts
569
+ @hosts.each do |host|
570
+ enable_root(host)
571
+ end
572
+ end
573
+
574
+ # Enables root access for a host when username is not root
575
+ #
576
+ # @return [void]
577
+ # @api private
578
+ def enable_root(host)
579
+ if host['user'] != 'root'
580
+ if host['platform'] =~ /f5-/
581
+ enable_root_f5(host)
582
+ elsif host['platform'] =~ /netscaler/
583
+ enable_root_netscaler(host)
584
+ else
585
+ copy_ssh_to_root(host, @options)
586
+ enable_root_login(host, @options)
587
+ host['user'] = 'root'
588
+ end
589
+ host.close
590
+ end
591
+ end
592
+
593
+ # Enables root access for a host on an f5 platform
594
+ # @note This method does not support other platforms
595
+ #
596
+ # @return nil
597
+ # @api private
598
+ def enable_root_f5(host)
599
+ for tries in 1..10
600
+ begin
601
+ #This command is problematic as the F5 is not always done loading
602
+ if host.exec(Command.new("modify sys db systemauth.disablerootlogin value false"), :acceptable_exit_codes => [0,1]).exit_code == 0 \
603
+ and host.exec(Command.new("modify sys global-settings gui-setup disabled"), :acceptable_exit_codes => [0,1]).exit_code == 0 \
604
+ and host.exec(Command.new("save sys config"), :acceptable_exit_codes => [0,1]).exit_code == 0
605
+ backoff_sleep(tries)
606
+ break
607
+ elsif tries == 10
608
+ raise "Instance was unable to be configured"
609
+ end
610
+ rescue Beaker::Host::CommandFailure => e
611
+ @logger.debug("Instance not yet configured (#{e})")
612
+ end
613
+ backoff_sleep(tries)
614
+ end
615
+ host['user'] = 'root'
616
+ host.close
617
+ sha256 = Digest::SHA256.new
618
+ password = sha256.hexdigest((1..50).map{(rand(86)+40).chr}.join.gsub(/\\/,'\&\&'))
619
+ host['ssh'] = {:password => password}
620
+ host.exec(Command.new("echo -e '#{password}\\n#{password}' | tmsh modify auth password admin"))
621
+ @logger.notify("f5: Configured admin password to be #{password}")
622
+ end
623
+
624
+ # Enables root access for a host on an netscaler platform
625
+ # @note This method does not support other platforms
626
+ #
627
+ # @return nil
628
+ # @api private
629
+ def enable_root_netscaler(host)
630
+ host['ssh'] = {:password => host['instance'].id}
631
+ @logger.notify("netscaler: nsroot password is #{host['instance'].id}")
632
+ end
633
+
634
+ # Set the :vmhostname for each host object to be the dns_name, which is accessible
635
+ # publicly. Then configure each ec2 machine to that dns_name, so that when facter
636
+ # is installed the facts for hostname and domain match the dns_name.
637
+ #
638
+ # if :use_beaker_hostnames: is true, set the :vmhostname and hostname of each ec2
639
+ # machine to the host[:name] from the beaker hosts file.
640
+ #
641
+ # @return [@hosts]
642
+ # @api private
643
+ def set_hostnames
644
+ if @options[:use_beaker_hostnames]
645
+ @hosts.each do |host|
646
+ host[:vmhostname] = host[:name]
647
+ if host['platform'] =~ /el-7/
648
+ # on el-7 hosts, the hostname command doesn't "stick" randomly
649
+ host.exec(Command.new("hostnamectl set-hostname #{host.name}"))
650
+ else
651
+ next if host['platform'] =~ /netscaler/
652
+ host.exec(Command.new("hostname #{host.name}"))
653
+ if host['vmname'] =~ /^amazon/
654
+ # Amazon Linux requires this to preserve host name changes across reboots.
655
+ # http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/set-hostname.html
656
+ # Also note that without an elastic ip set, while this will
657
+ # preserve the hostname across a full shutdown/startup of the vm
658
+ # (as opposed to a reboot) -- the ip address will have changed.
659
+ host.exec(Command.new("sed -ie '/^HOSTNAME/ s/=.*/=#{host.name}/' /etc/sysconfig/network"))
660
+ end
661
+ end
662
+ end
663
+ else
664
+ @hosts.each do |host|
665
+ host[:vmhostname] = host[:dns_name]
666
+ if host['platform'] =~ /el-7/
667
+ # on el-7 hosts, the hostname command doesn't "stick" randomly
668
+ host.exec(Command.new("hostnamectl set-hostname #{host.hostname}"))
669
+ else
670
+ next if host['platform'] =~ /netscaler/
671
+ host.exec(Command.new("hostname #{host.hostname}"))
672
+ if host['vmname'] =~ /^amazon/
673
+ # See note above
674
+ host.exec(Command.new("sed -ie '/^HOSTNAME/ s/=.*/=#{host.hostname}/' /etc/sysconfig/network"))
675
+ end
676
+ end
677
+ end
678
+ end
679
+ end
680
+
681
+ # Calculates and waits a back-off period based on the number of tries
682
+ #
683
+ # Logs each backupoff time and retry value to the console.
684
+ #
685
+ # @param tries [Number] number of tries to calculate back-off period
686
+ # @return [void]
687
+ # @api private
688
+ def backoff_sleep(tries)
689
+ # Exponential with some randomization
690
+ sleep_time = 2 ** tries
691
+ @logger.notify("aws-sdk: Sleeping #{sleep_time} seconds for attempt #{tries}.")
692
+ sleep sleep_time
693
+ nil
694
+ end
695
+
696
+ # Retrieve the public key locally from the executing users ~/.ssh directory
697
+ #
698
+ # @return [String] contents of public key
699
+ # @api private
700
+ def public_key
701
+ keys = Array(@options[:ssh][:keys])
702
+ keys << '~/.ssh/id_rsa'
703
+ keys << '~/.ssh/id_dsa'
704
+ key_file = keys.find do |key|
705
+ key_pub = key + '.pub'
706
+ File.exist?(File.expand_path(key_pub)) && File.exist?(File.expand_path(key))
707
+ end
708
+
709
+ if key_file
710
+ @logger.debug("Using public key: #{key_file}")
711
+ else
712
+ raise RuntimeError, "Expected to find a public key, but couldn't in #{keys}"
713
+ end
714
+ File.read(File.expand_path(key_file + '.pub'))
715
+ end
716
+
717
+ # Generate a key prefix for key pair names
718
+ #
719
+ # @note This is the part of the key that will stay static between Beaker
720
+ # runs on the same host.
721
+ #
722
+ # @return [String] Beaker key pair name based on sanitized hostname
723
+ def key_name_prefix
724
+ safe_hostname = Socket.gethostname.gsub('.', '-')
725
+ "Beaker-#{local_user}-#{safe_hostname}"
726
+ end
727
+
728
+ # Generate a reusable key name from the local hosts hostname
729
+ #
730
+ # @return [String] safe key name for current host
731
+ # @api private
732
+ def key_name
733
+ "#{key_name_prefix}-#{@options[:aws_keyname_modifier]}-#{@options[:timestamp].strftime("%F_%H_%M_%S_%N")}"
734
+ end
735
+
736
+ # Returns the local user running this tool
737
+ #
738
+ # @return [String] username of local user
739
+ # @api private
740
+ def local_user
741
+ ENV['USER']
742
+ end
743
+
744
+ # Creates the KeyPair for this test run
745
+ #
746
+ # @param region [AWS::EC2::Region] region to create the key pair in
747
+ # @return [AWS::EC2::KeyPair] created key_pair
748
+ # @api private
749
+ def ensure_key_pair(region)
750
+ pair_name = key_name()
751
+ delete_key_pair(region, pair_name)
752
+ create_new_key_pair(region, pair_name)
753
+ end
754
+
755
+ # Deletes key pairs from all regions
756
+ #
757
+ # @param [String] keypair_name_filter if given, will get all keypairs that match
758
+ # a simple {::String#start_with?} filter. If no filter is given, the basic key
759
+ # name returned by {#key_name} will be used.
760
+ #
761
+ # @return nil
762
+ # @api private
763
+ def delete_key_pair_all_regions(keypair_name_filter=nil)
764
+ region_keypairs_hash = my_key_pairs(keypair_name_filter)
765
+ region_keypairs_hash.each_pair do |region, keypair_name_array|
766
+ keypair_name_array.each do |keypair_name|
767
+ delete_key_pair(region, keypair_name)
768
+ end
769
+ end
770
+ end
771
+
772
+ # Gets the Beaker user's keypairs by region
773
+ #
774
+ # @param [String] name_filter if given, will get all keypairs that match
775
+ # a simple {::String#start_with?} filter. If no filter is given, the basic key
776
+ # name returned by {#key_name} will be used.
777
+ #
778
+ # @return [Hash{AWS::EC2::Region=>Array[String]}] a hash of region instance to
779
+ # an array of the keypair names that match for the filter
780
+ # @api private
781
+ def my_key_pairs(name_filter=nil)
782
+ keypairs_by_region = {}
783
+ keyname_default = key_name()
784
+ keyname_filtered = "#{name_filter}-*"
785
+ @ec2.regions.each do |region|
786
+ if name_filter
787
+ aws_name_filter = keyname_filtered
788
+ else
789
+ aws_name_filter = keyname_default
790
+ end
791
+ keypair_collection = region.key_pairs.filter('key-name', aws_name_filter)
792
+ keypair_collection.each do |keypair|
793
+ keypairs_by_region[region] ||= []
794
+ keypairs_by_region[region] << keypair.name
795
+ end
796
+ end
797
+ keypairs_by_region
798
+ end
799
+
800
+ # Deletes a given key pair
801
+ #
802
+ # @param [AWS::EC2::Region] region the region the key belongs to
803
+ # @param [String] pair_name the name of the key to be deleted
804
+ #
805
+ # @api private
806
+ def delete_key_pair(region, pair_name)
807
+ kp = region.key_pairs[pair_name]
808
+ if kp.exists?
809
+ @logger.debug("aws-sdk: delete key pair in region: #{region.name}")
810
+ kp.delete()
811
+ end
812
+ end
813
+
814
+ # Create a new key pair for a given Beaker run
815
+ #
816
+ # @param [AWS::EC2::Region] region the region the key pair will be imported into
817
+ # @param [String] pair_name the name of the key to be created
818
+ #
819
+ # @return [AWS::EC2::KeyPair] key pair created
820
+ # @raise [RuntimeError] raised if AWS keypair not created
821
+ def create_new_key_pair(region, pair_name)
822
+ @logger.debug("aws-sdk: importing new key pair: #{pair_name}")
823
+ ssh_string = public_key()
824
+ region.key_pairs.import(pair_name, ssh_string)
825
+ kp = region.key_pairs[pair_name]
826
+
827
+ exists = false
828
+ for tries in 1..5
829
+ if kp.exists?
830
+ exists = true
831
+ break
832
+ end
833
+ @logger.debug("AWS key pair doesn't appear to exist yet, sleeping before retry ")
834
+ backoff_sleep(tries)
835
+ end
836
+
837
+ if exists
838
+ @logger.debug("aws-sdk: key pair #{pair_name} imported")
839
+ kp
840
+ else
841
+ raise RuntimeError, "AWS key pair #{pair_name} can not be queried, even after import"
842
+ end
843
+ end
844
+
845
+ # Return a reproducable security group identifier based on input ports
846
+ #
847
+ # @param ports [Array<Number>] array of port numbers
848
+ # @return [String] group identifier
849
+ # @api private
850
+ def group_id(ports)
851
+ if ports.nil? or ports.empty?
852
+ raise ArgumentError, "Ports list cannot be nil or empty"
853
+ end
854
+
855
+ unless ports.is_a? Set
856
+ ports = Set.new(ports)
857
+ end
858
+
859
+ # Lolwut, #hash is inconsistent between ruby processes
860
+ "Beaker-#{Zlib.crc32(ports.inspect)}"
861
+ end
862
+
863
+ # Return an existing group, or create new one
864
+ #
865
+ # Accepts a VPC as input for checking & creation.
866
+ #
867
+ # @param vpc [AWS::EC2::VPC] the AWS vpc control object
868
+ # @return [AWS::EC2::SecurityGroup] created security group
869
+ # @api private
870
+ def ensure_ping_group(vpc)
871
+ @logger.notify("aws-sdk: Ensure security group exists that enables ping, create if not")
872
+
873
+ group = vpc.security_groups.filter('group-name', PING_SECURITY_GROUP_NAME).first
874
+
875
+ if group.nil?
876
+ group = create_ping_group(vpc)
877
+ end
878
+
879
+ group
880
+ end
881
+
882
+ # Return an existing group, or create new one
883
+ #
884
+ # Accepts a VPC as input for checking & creation.
885
+ #
886
+ # @param vpc [AWS::EC2::VPC] the AWS vpc control object
887
+ # @param ports [Array<Number>] an array of port numbers
888
+ # @return [AWS::EC2::SecurityGroup] created security group
889
+ # @api private
890
+ def ensure_group(vpc, ports)
891
+ @logger.notify("aws-sdk: Ensure security group exists for ports #{ports.to_s}, create if not")
892
+ name = group_id(ports)
893
+
894
+ group = vpc.security_groups.filter('group-name', name).first
895
+
896
+ if group.nil?
897
+ group = create_group(vpc, ports)
898
+ end
899
+
900
+ group
901
+ end
902
+
903
+ # Create a new ping enabled security group
904
+ #
905
+ # Accepts a region or VPC for group creation.
906
+ #
907
+ # @param rv [AWS::EC2::Region, AWS::EC2::VPC] the AWS region or vpc control object
908
+ # @return [AWS::EC2::SecurityGroup] created security group
909
+ # @api private
910
+ def create_ping_group(rv)
911
+ @logger.notify("aws-sdk: Creating group #{PING_SECURITY_GROUP_NAME}")
912
+ group = rv.security_groups.create(PING_SECURITY_GROUP_NAME,
913
+ :description => "Custom Beaker security group to enable ping")
914
+
915
+ group.allow_ping
916
+
917
+ group
918
+ end
919
+
920
+ # Create a new security group
921
+ #
922
+ # Accepts a region or VPC for group creation.
923
+ #
924
+ # @param rv [AWS::EC2::Region, AWS::EC2::VPC] the AWS region or vpc control object
925
+ # @param ports [Array<Number>] an array of port numbers
926
+ # @return [AWS::EC2::SecurityGroup] created security group
927
+ # @api private
928
+ def create_group(rv, ports)
929
+ name = group_id(ports)
930
+ @logger.notify("aws-sdk: Creating group #{name} for ports #{ports.to_s}")
931
+ group = rv.security_groups.create(name,
932
+ :description => "Custom Beaker security group for #{ports.to_a}")
933
+
934
+ unless ports.is_a? Set
935
+ ports = Set.new(ports)
936
+ end
937
+
938
+ ports.each do |port|
939
+ group.authorize_ingress(:tcp, port)
940
+ end
941
+
942
+ group
943
+ end
944
+
945
+ # Return a hash containing AWS credentials
946
+ #
947
+ # @return [Hash<Symbol, String>] AWS credentials
948
+ # @api private
949
+ def load_credentials
950
+ return load_env_credentials unless load_env_credentials.empty?
951
+ load_fog_credentials(@options[:dot_fog])
952
+ end
953
+
954
+ # Return AWS credentials loaded from environment variables
955
+ #
956
+ # @param prefix [String] environment variable prefix
957
+ # @return [Hash<Symbol, String>] ec2 credentials
958
+ # @api private
959
+ def load_env_credentials(prefix='AWS')
960
+ provider = AWS::Core::CredentialProviders::ENVProvider.new prefix
961
+
962
+ if provider.set?
963
+ {
964
+ :access_key => provider.access_key_id,
965
+ :secret_key => provider.secret_access_key,
966
+ }
967
+ else
968
+ {}
969
+ end
970
+ end
971
+ # Return a hash containing the fog credentials for EC2
972
+ #
973
+ # @param dot_fog [String] dot fog path
974
+ # @return [Hash<Symbol, String>] ec2 credentials
975
+ # @api private
976
+ def load_fog_credentials(dot_fog = '.fog')
977
+ fog = YAML.load_file( dot_fog )
978
+ default = fog[:default]
979
+
980
+ raise "You must specify an aws_access_key_id in your .fog file (#{dot_fog}) for ec2 instances!" unless default[:aws_access_key_id]
981
+ raise "You must specify an aws_secret_access_key in your .fog file (#{dot_fog}) for ec2 instances!" unless default[:aws_secret_access_key]
982
+
983
+ {
984
+ :access_key => default[:aws_access_key_id],
985
+ :secret_key => default[:aws_secret_access_key],
986
+ }
987
+ end
988
+
989
+ # Adds port 8143 to host[:additional_ports]
990
+ # if master, database and dashboard are not on same instance
991
+ def test_split_install
992
+ @hosts.each do |host|
993
+ mono_roles = ['master', 'database', 'dashboard']
994
+ roles_intersection = host[:roles] & mono_roles
995
+ if roles_intersection.size != 3 && roles_intersection.any?
996
+ host[:additional_ports] ? host[:additional_ports].push(8143) : host[:additional_ports] = [8143]
997
+ end
998
+ end
999
+ end
1000
+ end
1001
+ end