beaker-aws 0.1.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.
@@ -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