beaker 3.20.0 → 3.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +8 -8
  2. data/beaker.gemspec +4 -5
  3. data/docs/how_to/hypervisors/README.md +7 -3
  4. data/docs/tutorials/quick_start_rake_tasks.md +1 -1
  5. data/lib/beaker/command.rb +1 -1
  6. data/lib/beaker/host.rb +6 -4
  7. data/lib/beaker/host/windows/exec.rb +10 -0
  8. data/lib/beaker/host/windows/pkg.rb +1 -29
  9. data/lib/beaker/host_prebuilt_steps.rb +1 -0
  10. data/lib/beaker/hypervisor.rb +26 -37
  11. data/lib/beaker/ssh_connection.rb +8 -15
  12. data/lib/beaker/version.rb +1 -1
  13. data/spec/beaker/dsl/install_utils/windows_utils_spec.rb +1 -1
  14. data/spec/beaker/host/windows/exec_spec.rb +18 -0
  15. data/spec/beaker/host/windows/pkg_spec.rb +0 -7
  16. data/spec/beaker/host_prebuilt_steps_spec.rb +1 -0
  17. data/spec/beaker/host_spec.rb +31 -40
  18. data/spec/beaker/hypervisor/hypervisor_spec.rb +20 -34
  19. data/spec/beaker/ssh_connection_spec.rb +18 -19
  20. data/spec/spec_helper.rb +0 -1
  21. metadata +23 -57
  22. data/docs/how_to/hypervisors/aws.md +0 -149
  23. data/docs/how_to/hypervisors/ec2.md +0 -81
  24. data/docs/how_to/hypervisors/google_compute_engine.md +0 -41
  25. data/docs/how_to/hypervisors/vagrant.md +0 -165
  26. data/docs/how_to/hypervisors/vagrant_hosts_file_examples.md +0 -60
  27. data/docs/how_to/hypervisors/vagrant_libvirt.md +0 -58
  28. data/docs/how_to/hypervisors/vmware_fusion.md +0 -36
  29. data/docs/how_to/hypervisors/vsphere.md +0 -54
  30. data/lib/beaker/hypervisor/aws_sdk.rb +0 -989
  31. data/lib/beaker/hypervisor/ec2_helper.rb +0 -41
  32. data/lib/beaker/hypervisor/fusion.rb +0 -65
  33. data/lib/beaker/hypervisor/google_compute.rb +0 -164
  34. data/lib/beaker/hypervisor/google_compute_helper.rb +0 -577
  35. data/lib/beaker/hypervisor/vagrant.rb +0 -286
  36. data/lib/beaker/hypervisor/vagrant_custom.rb +0 -11
  37. data/lib/beaker/hypervisor/vagrant_fusion.rb +0 -17
  38. data/lib/beaker/hypervisor/vagrant_libvirt.rb +0 -41
  39. data/lib/beaker/hypervisor/vagrant_parallels.rb +0 -18
  40. data/lib/beaker/hypervisor/vagrant_virtualbox.rb +0 -76
  41. data/lib/beaker/hypervisor/vagrant_workstation.rb +0 -13
  42. data/lib/beaker/hypervisor/vsphere.rb +0 -85
  43. data/lib/beaker/hypervisor/vsphere_helper.rb +0 -204
  44. data/spec/beaker/hypervisor/aws_sdk_spec.rb +0 -980
  45. data/spec/beaker/hypervisor/ec2_helper_spec.rb +0 -44
  46. data/spec/beaker/hypervisor/fusion_spec.rb +0 -41
  47. data/spec/beaker/hypervisor/vagrant_custom_spec.rb +0 -46
  48. data/spec/beaker/hypervisor/vagrant_fusion_spec.rb +0 -32
  49. data/spec/beaker/hypervisor/vagrant_libvirt_spec.rb +0 -61
  50. data/spec/beaker/hypervisor/vagrant_parallels_spec.rb +0 -44
  51. data/spec/beaker/hypervisor/vagrant_spec.rb +0 -479
  52. data/spec/beaker/hypervisor/vagrant_virtualbox_spec.rb +0 -44
  53. data/spec/beaker/hypervisor/vagrant_workstation_spec.rb +0 -32
  54. data/spec/beaker/hypervisor/vsphere_helper_spec.rb +0 -163
  55. data/spec/beaker/hypervisor/vsphere_spec.rb +0 -90
@@ -1,989 +0,0 @@
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
- end
40
-
41
- # Provision all hosts on EC2 using the AWS::EC2 API
42
- #
43
- # @return [void]
44
- def provision
45
- start_time = Time.now
46
-
47
- # Perform the main launch work
48
- launch_all_nodes()
49
-
50
- wait_for_status_netdev()
51
-
52
- # Add metadata tags to each instance
53
- add_tags()
54
-
55
- # Grab the ip addresses and dns from EC2 for each instance to use for ssh
56
- populate_dns()
57
-
58
- #enable root if user is not root
59
- enable_root_on_hosts()
60
-
61
- # Set the hostname for each box
62
- set_hostnames()
63
-
64
- # Configure /etc/hosts on each host
65
- configure_hosts()
66
-
67
- @logger.notify("aws-sdk: Provisioning complete in #{Time.now - start_time} seconds")
68
-
69
- nil #void
70
- end
71
-
72
- # Kill all instances.
73
- #
74
- # @param instances [Enumerable<EC2::Instance>]
75
- # @return [void]
76
- def kill_instances(instances)
77
- instances.each do |instance|
78
- if !instance.nil? and instance.exists?
79
- @logger.notify("aws-sdk: killing EC2 instance #{instance.id}")
80
- instance.terminate
81
- end
82
- end
83
- nil
84
- end
85
-
86
- # Cleanup all earlier provisioned hosts on EC2 using the AWS::EC2 library
87
- #
88
- # It goes without saying, but a #cleanup does nothing without a #provision
89
- # method call first.
90
- #
91
- # @return [void]
92
- def cleanup
93
- # Provisioning should have set the host 'instance' values.
94
- kill_instances(@hosts.map{|h| h['instance']}.select{|x| !x.nil?})
95
- delete_key_pair_all_regions()
96
- nil
97
- end
98
-
99
- # Print instances to the logger. Instances will be from all regions
100
- # associated with provided key name and limited by regex compared to
101
- # instance status. Defaults to running instances.
102
- #
103
- # @param [String] key The key_name to match for
104
- # @param [Regex] status The regular expression to match against the instance's status
105
- def log_instances(key = key_name, status = /running/)
106
- instances = []
107
- @ec2.regions.each do |region|
108
- @logger.debug "Reviewing: #{region.name}"
109
- @ec2.regions[region.name].instances.each do |instance|
110
- if (instance.key_name =~ /#{key}/) and (instance.status.to_s =~ status)
111
- instances << instance
112
- end
113
- end
114
- end
115
- output = ""
116
- instances.each do |instance|
117
- 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"
118
- end
119
- @logger.notify("aws-sdk: List instances (keyname: #{key})")
120
- @logger.notify("#{output}")
121
- end
122
-
123
- # Provided an id return an instance object.
124
- # Instance object will respond to methods described here: {http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/EC2/Instance.html AWS Instance Object}.
125
- # @param [String] id The id of the instance to return
126
- # @return [AWS::EC2::Instance] An AWS::EC2 instance object
127
- def instance_by_id(id)
128
- @ec2.instances[id]
129
- end
130
-
131
- # Return all instances currently on ec2.
132
- # @see AwsSdk#instance_by_id
133
- # @return [AWS::EC2::InstanceCollection] An array of AWS::EC2 instance objects
134
- def instances
135
- @ec2.instances
136
- end
137
-
138
- # Provided an id return a VPC object.
139
- # VPC object will respond to methods described here: {http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/EC2/VPC.html AWS VPC Object}.
140
- # @param [String] id The id of the VPC to return
141
- # @return [AWS::EC2::VPC] An AWS::EC2 vpc object
142
- def vpc_by_id(id)
143
- @ec2.vpcs[id]
144
- end
145
-
146
- # Return all VPCs currently on ec2.
147
- # @see AwsSdk#vpc_by_id
148
- # @return [AWS::EC2::VPCCollection] An array of AWS::EC2 vpc objects
149
- def vpcs
150
- @ec2.vpcs
151
- end
152
-
153
- # Provided an id return a security group object
154
- # Security object will respond to methods described here: {http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/EC2/SecurityGroup.html AWS SecurityGroup Object}.
155
- # @param [String] id The id of the security group to return
156
- # @return [AWS::EC2::SecurityGroup] An AWS::EC2 security group object
157
- def security_group_by_id(id)
158
- @ec2.security_groups[id]
159
- end
160
-
161
- # Return all security groups currently on ec2.
162
- # @see AwsSdk#security_goup_by_id
163
- # @return [AWS::EC2::SecurityGroupCollection] An array of AWS::EC2 security group objects
164
- def security_groups
165
- @ec2.security_groups
166
- end
167
-
168
- # Shutdown and destroy ec2 instances idenfitied by key that have been alive
169
- # longer than ZOMBIE hours.
170
- #
171
- # @param [Integer] max_age The age in hours that a machine needs to be older than to be considered a zombie
172
- # @param [String] key The key_name to match for
173
- def kill_zombies(max_age = ZOMBIE, key = key_name)
174
- @logger.notify("aws-sdk: Kill Zombies! (keyname: #{key}, age: #{max_age} hrs)")
175
- #examine all available regions
176
- kill_count = 0
177
- time_now = Time.now.getgm #ec2 uses GM time
178
- @ec2.regions.each do |region|
179
- @logger.debug "Reviewing: #{region.name}"
180
- # Note: don't use instances.each here as that funtion doesn't allow proper rescue from error states
181
- instances = @ec2.regions[region.name].instances
182
- instances.each do |instance|
183
- begin
184
- if (instance.key_name =~ /#{key}/)
185
- @logger.debug "Examining #{instance.id} (keyname: #{instance.key_name}, launch time: #{instance.launch_time}, status: #{instance.status})"
186
- if ((time_now - instance.launch_time) > max_age*60*60) and instance.status.to_s !~ /terminated/
187
- @logger.debug "Kill! #{instance.id}: #{instance.key_name} (Current status: #{instance.status})"
188
- instance.terminate()
189
- kill_count += 1
190
- end
191
- end
192
- rescue AWS::Core::Resource::NotFound, AWS::EC2::Errors => e
193
- @logger.debug "Failed to remove instance: #{instance.id}, #{e}"
194
- end
195
- end
196
- end
197
- delete_key_pair_all_regions(key_name_prefix)
198
-
199
- @logger.notify "#{key}: Killed #{kill_count} instance(s)"
200
- end
201
-
202
- # Destroy any volumes marked 'available', INCLUDING THOSE YOU DON'T OWN! Use with care.
203
- def kill_zombie_volumes
204
- # Occasionaly, tearing down ec2 instances leaves orphaned EBS volumes behind -- these stack up quickly.
205
- # This simply looks for EBS volumes that are not in use
206
- # Note: don't use volumes.each here as that funtion doesn't allow proper rescue from error states
207
- @logger.notify("aws-sdk: Kill Zombie Volumes!")
208
- volume_count = 0
209
- @ec2.regions.each do |region|
210
- @logger.debug "Reviewing: #{region.name}"
211
- volumes = @ec2.regions[region.name].volumes.map { |vol| vol.id }
212
- volumes.each do |vol|
213
- begin
214
- vol = @ec2.regions[region.name].volumes[vol]
215
- if ( vol.status.to_s =~ /available/ )
216
- @logger.debug "Tear down available volume: #{vol.id}"
217
- vol.delete()
218
- volume_count += 1
219
- end
220
- rescue AWS::EC2::Errors::InvalidVolume::NotFound => e
221
- @logger.debug "Failed to remove volume: #{vol.id}, #{e}"
222
- end
223
- end
224
- end
225
- @logger.notify "Freed #{volume_count} volume(s)"
226
-
227
- end
228
-
229
- # Create an EC2 instance for host, tag it, and return it.
230
- #
231
- # @return [void]
232
- # @api private
233
- def create_instance(host, ami_spec, subnet_id)
234
- amitype = host['vmname'] || host['platform']
235
- amisize = host['amisize'] || 'm1.small'
236
- vpc_id = host['vpc_id'] || @options['vpc_id'] || nil
237
-
238
- if vpc_id and !subnet_id
239
- raise RuntimeError, "A subnet_id must be provided with a vpc_id"
240
- end
241
-
242
- # Use snapshot provided for this host
243
- image_type = host['snapshot']
244
- if not image_type
245
- raise RuntimeError, "No snapshot/image_type provided for EC2 provisioning"
246
- end
247
- ami = ami_spec[amitype]
248
- ami_region = ami[:region]
249
-
250
- # Main region object for ec2 operations
251
- region = @ec2.regions[ami_region]
252
-
253
- # If we haven't defined a vpc_id then we use the default vpc for the provided region
254
- if !vpc_id
255
- @logger.notify("aws-sdk: filtering available vpcs in region by 'isDefault")
256
- filtered_vpcs = region.client.describe_vpcs(:filters => [{:name => 'isDefault', :values => ['true']}])
257
- if !filtered_vpcs[:vpc_set].empty?
258
- vpc_id = filtered_vpcs[:vpc_set].first[:vpc_id]
259
- else #there's no default vpc, use nil
260
- vpc_id = nil
261
- end
262
- end
263
-
264
- # Grab the vpc object based upon provided id
265
- vpc = vpc_id ? region.vpcs[vpc_id] : nil
266
-
267
- # Grab image object
268
- image_id = ami[:image][image_type.to_sym]
269
- @logger.notify("aws-sdk: Checking image #{image_id} exists and getting its root device")
270
- image = region.images[image_id]
271
- if image.nil? and not image.exists?
272
- raise RuntimeError, "Image not found: #{image_id}"
273
- end
274
-
275
- @logger.notify("Image Storage Type: #{image.root_device_type}")
276
-
277
- # Transform the images block_device_mappings output into a format
278
- # ready for a create.
279
- block_device_mappings = []
280
- if image.root_device_type == :ebs
281
- orig_bdm = image.block_device_mappings()
282
- @logger.notify("aws-sdk: Image block_device_mappings: #{orig_bdm.to_hash}")
283
- orig_bdm.each do |device_name, rest|
284
- block_device_mappings << {
285
- :device_name => device_name,
286
- :ebs => {
287
- # Change the default size of the root volume.
288
- :volume_size => host['volume_size'] || rest[:volume_size],
289
- # This is required to override the images default for
290
- # delete_on_termination, forcing all volumes to be deleted once the
291
- # instance is terminated.
292
- :delete_on_termination => true,
293
- }
294
- }
295
- end
296
- end
297
-
298
- security_group = ensure_group(vpc || region, Beaker::EC2Helper.amiports(host))
299
- #check if ping is enabled
300
- ping_security_group = ensure_ping_group(vpc || region)
301
-
302
- msg = "aws-sdk: launching %p on %p using %p/%p%s" %
303
- [host.name, amitype, amisize, image_type,
304
- subnet_id ? ("in %p" % subnet_id) : '']
305
- @logger.notify(msg)
306
- config = {
307
- :count => 1,
308
- :image_id => image_id,
309
- :monitoring_enabled => true,
310
- :key_pair => ensure_key_pair(region),
311
- :security_groups => [security_group, ping_security_group],
312
- :instance_type => amisize,
313
- :disable_api_termination => false,
314
- :instance_initiated_shutdown_behavior => "terminate",
315
- :subnet => subnet_id,
316
- }
317
- config[:block_device_mappings] = block_device_mappings if image.root_device_type == :ebs
318
- region.instances.create(config)
319
- end
320
-
321
- # For each host, create an EC2 instance in one of the specified
322
- # subnets and push it onto instances_created. Each subnet will be
323
- # tried at most once for each host, and more than one subnet may
324
- # be tried if capacity constraints are encountered. Each Hash in
325
- # instances_created will contain an :instance and :host value.
326
- #
327
- # @param hosts [Enumerable<Host>]
328
- # @param subnets [Enumerable<String>]
329
- # @param ami_spec [Hash]
330
- # @param instances_created Enumerable<Hash{Symbol=>EC2::Instance,Host}>
331
- # @return [void]
332
- # @api private
333
- def launch_nodes_on_some_subnet(hosts, subnets, ami_spec, instances_created)
334
- # Shuffle the subnets so we don't always hit the same one
335
- # first, and cycle though the subnets independently of the
336
- # host, so we stick with one that's working. Try each subnet
337
- # once per-host.
338
- if subnets.nil? or subnets.empty?
339
- return
340
- end
341
- subnet_i = 0
342
- shuffnets = subnets.shuffle
343
- hosts.each do |host|
344
- instance = nil
345
- shuffnets.length.times do
346
- begin
347
- subnet_id = shuffnets[subnet_i]
348
- instance = create_instance(host, ami_spec, subnet_id)
349
- instances_created.push({:instance => instance, :host => host})
350
- break
351
- rescue AWS::EC2::Errors::InsufficientInstanceCapacity => ex
352
- @logger.notify("aws-sdk: hit #{subnet_id} capacity limit; moving on")
353
- subnet_i = (subnet_i + 1) % shuffnets.length
354
- end
355
- end
356
- if instance.nil?
357
- raise RuntimeError, "unable to launch host in any requested subnet"
358
- end
359
- end
360
- end
361
-
362
- # Create EC2 instances for all hosts, tag them, and wait until
363
- # they're running. When a host provides a subnet_id, create the
364
- # instance in that subnet, otherwise prefer a CONFIG subnet_id.
365
- # If neither are set but there is a CONFIG subnet_ids list,
366
- # attempt to create the host in each specified subnet, which might
367
- # fail due to capacity constraints, for example. Specifying both
368
- # a CONFIG subnet_id and subnet_ids will provoke an error.
369
- #
370
- # @return [void]
371
- # @api private
372
- def launch_all_nodes
373
- @logger.notify("aws-sdk: launch all hosts in configuration")
374
- ami_spec = YAML.load_file(@options[:ec2_yaml])["AMI"]
375
- global_subnet_id = @options['subnet_id']
376
- global_subnets = @options['subnet_ids']
377
- if global_subnet_id and global_subnets
378
- raise RuntimeError, 'Config specifies both subnet_id and subnet_ids'
379
- end
380
- no_subnet_hosts = []
381
- specific_subnet_hosts = []
382
- some_subnet_hosts = []
383
- @hosts.each do |host|
384
- if global_subnet_id or host['subnet_id']
385
- specific_subnet_hosts.push(host)
386
- elsif global_subnets
387
- some_subnet_hosts.push(host)
388
- else
389
- no_subnet_hosts.push(host)
390
- end
391
- end
392
- instances = [] # Each element is {:instance => i, :host => h}
393
- begin
394
- @logger.notify("aws-sdk: launch instances not particular about subnet")
395
- launch_nodes_on_some_subnet(some_subnet_hosts, global_subnets, ami_spec,
396
- instances)
397
- @logger.notify("aws-sdk: launch instances requiring a specific subnet")
398
- specific_subnet_hosts.each do |host|
399
- subnet_id = host['subnet_id'] || global_subnet_id
400
- instance = create_instance(host, ami_spec, subnet_id)
401
- instances.push({:instance => instance, :host => host})
402
- end
403
- @logger.notify("aws-sdk: launch instances requiring no subnet")
404
- no_subnet_hosts.each do |host|
405
- instance = create_instance(host, ami_spec, nil)
406
- instances.push({:instance => instance, :host => host})
407
- end
408
- wait_for_status(:running, instances)
409
- rescue Exception => ex
410
- @logger.notify("aws-sdk: exception #{ex.class}: #{ex}")
411
- kill_instances(instances.map{|x| x[:instance]})
412
- raise ex
413
- end
414
- # At this point, all instances should be running since wait
415
- # either returns on success or throws an exception.
416
- if instances.empty?
417
- raise RuntimeError, "Didn't manage to launch any EC2 instances"
418
- end
419
- # Assign the now known running instances to their hosts.
420
- instances.each {|x| x[:host]['instance'] = x[:instance]}
421
- nil
422
- end
423
-
424
- # Wait until all instances reach the desired state. Each Hash in
425
- # instances must contain an :instance and :host value.
426
- #
427
- # @param status [Symbol] EC2 state to wait for, :running :stopped etc.
428
- # @param instances Enumerable<Hash{Symbol=>EC2::Instance,Host}>
429
- # @param block [Proc] more complex checks can be made by passing a
430
- # block in. This overrides the status parameter.
431
- # EC2::Instance objects from the hosts will be
432
- # yielded to the passed block
433
- # @return [void]
434
- # @api private
435
- def wait_for_status(status, instances, &block)
436
- # Wait for each node to reach status :running
437
- @logger.notify("aws-sdk: Waiting for all hosts to be #{status}")
438
- instances.each do |x|
439
- name = x[:name]
440
- instance = x[:instance]
441
- @logger.notify("aws-sdk: Wait for node #{name} to be #{status}")
442
- # Here we keep waiting for the machine state to reach ':running' with an
443
- # exponential backoff for each poll.
444
- # TODO: should probably be a in a shared method somewhere
445
- for tries in 1..10
446
- begin
447
- if block_given?
448
- test_result = yield instance
449
- else
450
- test_result = instance.status == status
451
- end
452
- if test_result
453
- # Always sleep, so the next command won't cause a throttle
454
- backoff_sleep(tries)
455
- break
456
- elsif tries == 10
457
- raise "Instance never reached state #{status}"
458
- end
459
- rescue AWS::EC2::Errors::InvalidInstanceID::NotFound => e
460
- @logger.debug("Instance #{name} not yet available (#{e})")
461
- end
462
- backoff_sleep(tries)
463
- end
464
- end
465
- end
466
-
467
- # Handles special checks needed for netdev platforms.
468
- #
469
- # @note if any host is an netdev one, these checks will happen once across all
470
- # of the hosts, and then we'll exit
471
- #
472
- # @return [void]
473
- # @api private
474
- def wait_for_status_netdev()
475
- @hosts.each do |host|
476
- if host['platform'] =~ /f5-|netscaler/
477
- wait_for_status(:running, @hosts)
478
-
479
- wait_for_status(nil, @hosts) do |instance|
480
- instance_status_collection = instance.client.describe_instance_status({:instance_ids => [instance.id]})
481
- first_instance = instance_status_collection[:instance_status_set].first
482
- first_instance[:system_status][:status] == "ok"
483
- end
484
-
485
- break
486
- end
487
- end
488
- end
489
-
490
- # Add metadata tags to all instances
491
- #
492
- # @return [void]
493
- # @api private
494
- def add_tags
495
- @hosts.each do |host|
496
- instance = host['instance']
497
-
498
- # Define tags for the instance
499
- @logger.notify("aws-sdk: Add tags for #{host.name}")
500
- instance.add_tag("jenkins_build_url", :value => @options[:jenkins_build_url])
501
- instance.add_tag("Name", :value => host.name)
502
- instance.add_tag("department", :value => @options[:department])
503
- instance.add_tag("project", :value => @options[:project])
504
- instance.add_tag("created_by", :value => @options[:created_by])
505
-
506
- host[:host_tags].each do |name, val|
507
- instance.add_tag(name.to_s, :value => val)
508
- end
509
- end
510
-
511
- nil
512
- end
513
-
514
- # Populate the hosts IP address from the EC2 dns_name
515
- #
516
- # @return [void]
517
- # @api private
518
- def populate_dns
519
- # Obtain the IP addresses and dns_name for each host
520
- @hosts.each do |host|
521
- @logger.notify("aws-sdk: Populate DNS for #{host.name}")
522
- instance = host['instance']
523
- host['ip'] = instance.ip_address ? instance.ip_address : instance.private_ip_address
524
- host['private_ip'] = instance.private_ip_address
525
- host['dns_name'] = instance.dns_name
526
- @logger.notify("aws-sdk: name: #{host.name} ip: #{host['ip']} private_ip: #{host['private_ip']} dns_name: #{instance.dns_name}")
527
- end
528
-
529
- nil
530
- end
531
-
532
- # Return a valid /etc/hosts line for a given host
533
- #
534
- # @param [Beaker::Host] host Beaker::Host object for generating /etc/hosts entry
535
- # @param [Symbol] interface Symbol identifies which ip should be used for host
536
- # @return [String] formatted hosts entry for host
537
- # @api private
538
- def etc_hosts_entry(host, interface = :ip)
539
- name = host.name
540
- domain = get_domain_name(host)
541
- ip = host[interface.to_s]
542
- "#{ip}\t#{name} #{name}.#{domain} #{host['dns_name']}\n"
543
- end
544
-
545
- # Configure /etc/hosts for each node
546
- #
547
- # @note f5 hosts are skipped since this isn't a valid step there
548
- #
549
- # @return [void]
550
- # @api private
551
- def configure_hosts
552
- non_netdev_hosts = @hosts.select{ |h| !(h['platform'] =~ /f5-|netscaler/) }
553
- non_netdev_hosts.each do |host|
554
- host_entries = non_netdev_hosts.map do |h|
555
- h == host ? etc_hosts_entry(h, :private_ip) : etc_hosts_entry(h)
556
- end
557
- host_entries.unshift "127.0.0.1\tlocalhost localhost.localdomain\n"
558
- set_etc_hosts(host, host_entries.join(''))
559
- end
560
- nil
561
- end
562
-
563
- # Enables root for instances with custom username like ubuntu-amis
564
- #
565
- # @return [void]
566
- # @api private
567
- def enable_root_on_hosts
568
- @hosts.each do |host|
569
- enable_root(host)
570
- end
571
- end
572
-
573
- # Enables root access for a host when username is not root
574
- #
575
- # @return [void]
576
- # @api private
577
- def enable_root(host)
578
- if host['user'] != 'root'
579
- if host['platform'] =~ /f5-/
580
- enable_root_f5(host)
581
- elsif host['platform'] =~ /netscaler/
582
- enable_root_netscaler(host)
583
- else
584
- copy_ssh_to_root(host, @options)
585
- enable_root_login(host, @options)
586
- host['user'] = 'root'
587
- end
588
- host.close
589
- end
590
- end
591
-
592
- # Enables root access for a host on an f5 platform
593
- # @note This method does not support other platforms
594
- #
595
- # @return nil
596
- # @api private
597
- def enable_root_f5(host)
598
- for tries in 1..10
599
- begin
600
- #This command is problematic as the F5 is not always done loading
601
- if host.exec(Command.new("modify sys db systemauth.disablerootlogin value false"), :acceptable_exit_codes => [0,1]).exit_code == 0 \
602
- and host.exec(Command.new("modify sys global-settings gui-setup disabled"), :acceptable_exit_codes => [0,1]).exit_code == 0 \
603
- and host.exec(Command.new("save sys config"), :acceptable_exit_codes => [0,1]).exit_code == 0
604
- backoff_sleep(tries)
605
- break
606
- elsif tries == 10
607
- raise "Instance was unable to be configured"
608
- end
609
- rescue Beaker::Host::CommandFailure => e
610
- @logger.debug("Instance not yet configured (#{e})")
611
- end
612
- backoff_sleep(tries)
613
- end
614
- host['user'] = 'root'
615
- host.close
616
- sha256 = Digest::SHA256.new
617
- password = sha256.hexdigest((1..50).map{(rand(86)+40).chr}.join.gsub(/\\/,'\&\&'))
618
- host['ssh'] = {:password => password}
619
- host.exec(Command.new("echo -e '#{password}\\n#{password}' | tmsh modify auth password admin"))
620
- @logger.notify("f5: Configured admin password to be #{password}")
621
- end
622
-
623
- # Enables root access for a host on an netscaler platform
624
- # @note This method does not support other platforms
625
- #
626
- # @return nil
627
- # @api private
628
- def enable_root_netscaler(host)
629
- host['ssh'] = {:password => host['instance'].id}
630
- @logger.notify("netscaler: nsroot password is #{host['instance'].id}")
631
- end
632
-
633
- # Set the :vmhostname for each host object to be the dns_name, which is accessible
634
- # publicly. Then configure each ec2 machine to that dns_name, so that when facter
635
- # is installed the facts for hostname and domain match the dns_name.
636
- #
637
- # if :use_beaker_hostnames: is true, set the :vmhostname and hostname of each ec2
638
- # machine to the host[:name] from the beaker hosts file.
639
- #
640
- # @return [@hosts]
641
- # @api private
642
- def set_hostnames
643
- if @options[:use_beaker_hostnames]
644
- @hosts.each do |host|
645
- host[:vmhostname] = host[:name]
646
- if host['platform'] =~ /el-7/
647
- # on el-7 hosts, the hostname command doesn't "stick" randomly
648
- host.exec(Command.new("hostnamectl set-hostname #{host.name}"))
649
- else
650
- next if host['platform'] =~ /netscaler/
651
- host.exec(Command.new("hostname #{host.name}"))
652
- if host['vmname'] =~ /^amazon/
653
- # Amazon Linux requires this to preserve host name changes across reboots.
654
- # http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/set-hostname.html
655
- # Also note that without an elastic ip set, while this will
656
- # preserve the hostname across a full shutdown/startup of the vm
657
- # (as opposed to a reboot) -- the ip address will have changed.
658
- host.exec(Command.new("sed -ie '/^HOSTNAME/ s/=.*/=#{host.name}/' /etc/sysconfig/network"))
659
- end
660
- end
661
- end
662
- else
663
- @hosts.each do |host|
664
- host[:vmhostname] = host[:dns_name]
665
- if host['platform'] =~ /el-7/
666
- # on el-7 hosts, the hostname command doesn't "stick" randomly
667
- host.exec(Command.new("hostnamectl set-hostname #{host.hostname}"))
668
- else
669
- next if host['platform'] =~ /netscaler/
670
- host.exec(Command.new("hostname #{host.hostname}"))
671
- if host['vmname'] =~ /^amazon/
672
- # See note above
673
- host.exec(Command.new("sed -ie '/^HOSTNAME/ s/=.*/=#{host.hostname}/' /etc/sysconfig/network"))
674
- end
675
- end
676
- end
677
- end
678
- end
679
-
680
- # Calculates and waits a back-off period based on the number of tries
681
- #
682
- # Logs each backupoff time and retry value to the console.
683
- #
684
- # @param tries [Number] number of tries to calculate back-off period
685
- # @return [void]
686
- # @api private
687
- def backoff_sleep(tries)
688
- # Exponential with some randomization
689
- sleep_time = 2 ** tries
690
- @logger.notify("aws-sdk: Sleeping #{sleep_time} seconds for attempt #{tries}.")
691
- sleep sleep_time
692
- nil
693
- end
694
-
695
- # Retrieve the public key locally from the executing users ~/.ssh directory
696
- #
697
- # @return [String] contents of public key
698
- # @api private
699
- def public_key
700
- keys = Array(@options[:ssh][:keys])
701
- keys << '~/.ssh/id_rsa'
702
- keys << '~/.ssh/id_dsa'
703
- key_file = nil
704
- keys.each do |key|
705
- key_filename = File.expand_path(key + '.pub')
706
- key_file = key_filename if File.exists?(key_filename)
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(key_file)
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
- end
989
- end