evilhornets 0.0.1

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f2eb8b072d3546ff3b3252cec33ada70b49e7279
4
+ data.tar.gz: d8cad9948b5b5affda0e054f1ef594b17dc0770c
5
+ SHA512:
6
+ metadata.gz: a8e5645179e857ab39824dcf742b81f75c9c4c67051a1ce6d027102ffaaab38ff983d9644ad9b73e990b8683715442a2426b1d99093e62b9e91a1d61c24b2973
7
+ data.tar.gz: 18d0774defbaed47e8b39a4e542cf1f628be92537747bd11d793188d3c9b4151f3290244ad3ed5e15ba82c63eaa8657e8ba8d8cb1ab6a9ca6b7713a33528657a
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'hornet'
4
+ options = Hornet.parse ARGV
5
+ Hornet.go ARGV, options
@@ -0,0 +1,126 @@
1
+ require 'optparse'
2
+ require 'ostruct'
3
+ require 'hornet/headquarter'
4
+
5
+ Commands = {
6
+ 'up' => 'Spin up multiple EC2 micro instances to host bees.',
7
+ 'attack' => 'Launch an attack against a target.',
8
+ 'down' => 'Terminate load testing servers.',
9
+ 'scale' => 'Adjust the number of load testing servers.',
10
+ 'report' => 'Report the load testing result.',
11
+ 'help' => 'Print the help document.',
12
+ }
13
+
14
+ class Hornet
15
+
16
+ def self.parse(args)
17
+ command = args.first
18
+ options = OpenStruct.new
19
+ parser = OptionParser.new do |opt|
20
+
21
+ if ['--help', '-h', 'help'].include? command
22
+ print_help = ['--help', '-h', 'help'].include? command
23
+ command = 'help'
24
+ end
25
+ if not Commands.keys.include? command
26
+ opt.banner = "Usage: hornet (%s) [options]" % Commands.keys.join('|')
27
+ else
28
+ opt.banner = "Usage: hornet %s [options]" % command
29
+
30
+ if ['up', 'attack', 'scale'].include? command
31
+ opt.separator Commands[command]
32
+ end
33
+
34
+ # build options depends on command
35
+
36
+ if command == 'attack' or print_help
37
+ opt.separator 'attack:'
38
+ opt.on('-n', '--number [NUMBER]', 'Number of total attacks to launch (default: 1000).') do |value|
39
+ options.number = value
40
+ end
41
+ opt.on('-c', '--concurrent [CONCURRENT]', 'The number of concurrent connections to make to the target (default: 100).') do |value|
42
+ options.concurrent = value
43
+ end
44
+ opt.on('-b', '--bees [BEES]', 'Number of containers to create (default: 1).') do |value|
45
+ options.bees = value
46
+ end
47
+ opt.on('-u', '--url [URL]', 'URL of the target to attack.') do |value|
48
+ options.url = value
49
+ end
50
+ end
51
+
52
+ if command == 'up' or print_help
53
+ opt.separator 'up:'
54
+ opt.on('-r', '--region [REGION]', 'Region the server will be built (default: us-east-1d).') do |value|
55
+ options.region = value
56
+ end
57
+ opt.on('-n', '--number [NUMBER]', 'Number of servers to start (default: 1).') do |value|
58
+ options.number = value
59
+ end
60
+ opt.on('-u', '--username [USERNAME]', 'The ssh username name to use to connect to the servers (default: ubuntu).') do |value|
61
+ options.username = value
62
+ end
63
+ opt.on('-k', '--key [KEY]', 'The ssh key pair name to use to connect to the servers.') do |value|
64
+ options.key_name = value
65
+ end
66
+ opt.on('-i', '--image_id [IMAGE_ID]', 'The ID of the AMI.') do |value|
67
+ options.image_id = value
68
+ end
69
+ end
70
+
71
+ if command == 'scale' or print_help
72
+ opt.separator 'scale:'
73
+ opt.on('-n', '--number [NUMBER]', 'Number of servers to scale to (default: 1).') do |value|
74
+ options.number = value
75
+ end
76
+ end
77
+
78
+ opt.on('-h', '--help', 'Print this help document.') do |value|
79
+ abort parser.to_s
80
+ end
81
+ end
82
+
83
+ end
84
+
85
+ # don't parse anything if no command sepcified
86
+ if command.nil? or ['--help', '-h', 'help'].include? command
87
+ abort parser.to_s
88
+ end
89
+ parser.parse!
90
+ options
91
+ end
92
+
93
+ # validate options, abort if required options is missing
94
+ def self.validate_options(command, options)
95
+ ops = {}
96
+ begin
97
+ case command
98
+ when 'attack'
99
+ ops = {:number => 1000, :concurrent => 100, :bees => 1}.merge options.to_h
100
+ if not ops.has_key? :url
101
+ raise ArgumentError.new 'Missing argument: --url'
102
+ end
103
+ when 'up'
104
+ ops = {:region => 'us-east-1', :username => 'ubuntu', :number => 1, :image_id => 'ami-c3e997f9'}.merge options.to_h
105
+ if not ops.has_key? :key_name
106
+ raise ArgumentError.new 'Missing argument: --key'
107
+ end
108
+ when 'scale'
109
+ ops = {:number => 1}.merge options.to_h
110
+ end
111
+ rescue ArgumentError => msg
112
+ abort msg.to_s
113
+ end
114
+ OpenStruct.new ops
115
+ end
116
+
117
+ # move on to the next step.
118
+ def self.go(args, options)
119
+ command = args.first
120
+ if not command.nil?
121
+ options = Hornet.validate_options command, options
122
+ headquarter = Fleet::Headquarter.new(command, options.to_h)
123
+ headquarter.dispatch
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,74 @@
1
+ module Fleet
2
+
3
+ Headlines = [
4
+ 'Time taken for tests',
5
+ 'Complete requests',
6
+ 'Failed requests',
7
+ 'Non-2xx responses',
8
+ 'Total transferred',
9
+ 'HTML transferred',
10
+ 'Requests per second',
11
+ 'Time per request',
12
+ 'Transfer rate'
13
+ ]
14
+
15
+ def Fleet.print_report(result)
16
+ result.each do |key, value|
17
+ puts '%s: %s' % [key, value.join(' ')]
18
+ end
19
+ end
20
+
21
+ # merge all results together and with certain calculations
22
+ def Fleet.report(data)
23
+ result = {}
24
+
25
+ # hash
26
+ data.each_value do |entries|
27
+ entries.each do |key, value|
28
+
29
+ if result.has_key? key
30
+ result[key][0] << value.first.to_f
31
+ else
32
+ result[key] = value.size == 1 ? [[value.first.to_f]] : [[value.first.to_f], value.last]
33
+ end
34
+
35
+ end
36
+ end
37
+
38
+ # process the result data, e.g, sum and avg
39
+ result.each do |key, value|
40
+ #puts value.to_s
41
+ if ['Time taken for tests'].include? key
42
+ value[0] = value[0].sort.last
43
+ elsif ['Complete requests', 'Failed requests', 'Total transferred', 'HTML transferred', 'Non-2xx responses'].include? key
44
+ value[0] = value[0].inject{|sum, x| sum + x}
45
+ else
46
+ count = value[0].count
47
+ value[0] = (value[0].inject{|sum, x| sum + x} / count).round(2)
48
+ end
49
+ result[key] = value
50
+ end
51
+ result
52
+ end
53
+
54
+ def Fleet.parse_ab_data(data)
55
+ result = {}
56
+
57
+ # parse each line with the matched heading
58
+ data.each_line do |line|
59
+ if line.start_with?(*Headlines)
60
+ parts = line.partition(':')
61
+ head = parts.first
62
+ body = parts.last.strip.split(' ', 2)
63
+ result[head] = body
64
+ end
65
+ end
66
+
67
+ if not result.has_key? 'Time per request'
68
+ return {}
69
+ end
70
+
71
+ result
72
+ end
73
+
74
+ end
@@ -0,0 +1,220 @@
1
+ require 'aws-sdk'
2
+ require 'ostruct'
3
+ require 'hornet/fleet'
4
+ require 'hornet/hive'
5
+
6
+ module Fleet
7
+ class Headquarter
8
+
9
+ STATE_FILE = File.expand_path('~/.hives')
10
+
11
+ def initialize(command, options={})
12
+ @command = command
13
+ @options = options
14
+
15
+ @state = OpenStruct.new options
16
+ @state.loaded = false
17
+
18
+ readServerState
19
+
20
+ if @state.region
21
+ Aws.config.update({:region => @state.region})
22
+ @ec2_resource = Aws::EC2::Resource.new
23
+ @ec2_client = Aws::EC2::Client.new
24
+ end
25
+ end
26
+
27
+ def dispatch
28
+ case @command
29
+ when 'up'
30
+ createHives @options
31
+ when 'attack'
32
+ hivesAttack @options
33
+ when 'scale'
34
+ scaleHives @options
35
+ when 'report'
36
+ hivesReport
37
+ when 'down'
38
+ destroyHives
39
+ end
40
+ end
41
+
42
+ # create a number of hives using user options
43
+ def createHives(options)
44
+ number_of_hive = options.has_key?(:number) ? options[:number].to_i : 1
45
+ hive_options = {
46
+ :key_name => nil,
47
+ :image_id => nil,
48
+ :min_count => number_of_hive,
49
+ :max_count => number_of_hive,
50
+ :instance_type => 't2.micro'
51
+ }
52
+ hive_options.merge!(options.select {|k,v| hive_options.has_key?(k)})
53
+ hives = @ec2_resource.create_instances hive_options
54
+ puts "%i hives are being built" % number_of_hive
55
+
56
+ # write the current state to the file
57
+ @state.hives = hives.map(&:id) + @state.hives.to_a
58
+ writeServerList
59
+
60
+ checkHivesStatus hives
61
+
62
+ # tagging happens after the instance is ready
63
+ @ec2_resource.create_tags({:tags => [{:key => 'Name', :value => 'hive'}], :resources => hives.map(&:id)})
64
+ end
65
+
66
+ # start the attack simultaneously.
67
+ def hivesAttack(options)
68
+ hives = []
69
+ attack_threads = []
70
+ attack_options = []
71
+
72
+ puts "Preparing the attack:"
73
+ puts "%s bees will attack %s times, %s at a time" % [options[:bees], options[:bees].to_i * options[:number].to_i, options[:concurrent]]
74
+ remains = options[:bees].to_i % @state.hives.count
75
+ options[:bees] = options[:bees].to_i / @state.hives.count
76
+
77
+ puts "Hive Bees"
78
+ @state.hives.each_with_index do |instance_id, index|
79
+ if index == @state.hives.size - 1
80
+ options[:bees] += remains
81
+ end
82
+ attack_options << options.clone
83
+ puts '%s %s' % [instance_id, options[:bees]]
84
+
85
+ hive = Hive.new @state.username, @state.key_name, instance_id
86
+ hives << hive
87
+ attack_threads << Thread.new do
88
+ hive.attack attack_options[index]
89
+ end
90
+ end
91
+
92
+ puts "\n"
93
+ attack_threads.each {|t| t.join}
94
+ puts "\n"
95
+
96
+ hivesReport hives
97
+ end
98
+
99
+ # collect report from every hive
100
+ def hivesReport(hives=[])
101
+ data = {}
102
+ report_threads = []
103
+ if not hives.any?
104
+ @state.hives.each_with_index do |instance_id, index|
105
+ hives << Hive.new(@state.username, @state.key_name, instance_id)
106
+ end
107
+ end
108
+
109
+ # create the report threads
110
+ hives.each do |hive|
111
+ report_threads << Thread.new do
112
+ data[hive.instance_id] = hive.report
113
+ end
114
+ end
115
+
116
+ report_threads.each {|t| t.join}
117
+ Fleet.print_report Fleet.report(data)
118
+ end
119
+
120
+ # scale hives up and down
121
+ def scaleHives(options)
122
+ if not @state.loaded
123
+ abort 'Perhaps build some hives first?'
124
+ end
125
+ number_of_hive = options.has_key?(:number) ? options[:number].to_i : 1
126
+ if @state.hives.count == number_of_hive
127
+ abort 'No hives scaled'
128
+ elsif @state.hives.count > number_of_hive
129
+ destroyHives number_of_hive > 0 ? @state.hives[number_of_hive..-1] : {}
130
+ else
131
+ options = {:number => number_of_hive - @state.hives.count, :image_id => @state.image_id}
132
+ createHives @state.to_h.merge options
133
+ end
134
+ end
135
+
136
+ # tear down all running hives
137
+ def destroyHives instances = []
138
+ instances = instances.empty? ? @state.hives : instances
139
+ if not instances.empty?
140
+
141
+ # attemp the terminate ec2 instances
142
+ begin
143
+ @ec2_client.terminate_instances instance_ids: instances
144
+ rescue Aws::EC2::Errors::InvalidInstanceIDNotFound => e
145
+ # for mismatches, terminate what we can
146
+ instances_2b_removed = instances - e.to_s.match(/\'[^']*\'/)[0].split(',').map! {|x| x.strip.tr_s("'", "")}
147
+ @ec2_client.terminate_instances instance_ids: instances_2b_removed
148
+ rescue Aws::EC2::Errors::InvalidInstanceIDMalformed
149
+ end
150
+
151
+ if instances.count == @state.hives.count
152
+ removeServerList
153
+ else
154
+ @state.hives.reject! {|item| instances.include? item}
155
+ writeServerList
156
+ end
157
+ else
158
+ abord 'Perhaps build some hives first?'
159
+ end
160
+ puts '%i hives are teared down!' % instances.count
161
+ end
162
+
163
+ private
164
+
165
+ def readServerState
166
+ if not File.exist? STATE_FILE
167
+ return false
168
+ end
169
+ server_state = IO.readlines(STATE_FILE).map! {|l| l.strip}
170
+ begin
171
+ @state.username = server_state[0]
172
+ @state.key_name = server_state[1]
173
+ @state.region = server_state[2]
174
+ @state.image_id = server_state[3]
175
+ @state.hives = server_state[4..-1]
176
+ rescue
177
+ abort 'A problem occured when reading hives'
178
+ end
179
+ @state.loaded = true
180
+ end
181
+
182
+ def writeServerList
183
+ begin
184
+ File.open(STATE_FILE, 'w') do |f|
185
+ f.write("%s\n" % @state.username)
186
+ f.write("%s\n" % @state.key_name)
187
+ f.write("%s\n" % @state.region)
188
+ f.write("%s\n" % @state.image_id)
189
+ f.write(@state.hives.join("\n"))
190
+ end
191
+ rescue
192
+ abort 'Failed to written down hives details'
193
+ end
194
+ end
195
+
196
+ def removeServerList
197
+ File.delete STATE_FILE
198
+ end
199
+
200
+ # check over status of hives
201
+ def checkHivesStatus(hives)
202
+ hives_built = []
203
+ filters = [{:name => 'instance-state-name', :values => ['pending', 'running']}]
204
+ while hives_built.count != hives.count do
205
+ statuses = @ec2_client.describe_instance_status instance_ids: hives.map(&:id), include_all_instances: true, filters: filters
206
+ statuses.each do |response|
207
+ response[:instance_statuses].each do |instance|
208
+ building = instance[:instance_state].name == 'running' ? false : true
209
+ instance_id = instance[:instance_id]
210
+ if not building and not hives_built.include? instance_id
211
+ puts 'Hive %s is ready!' % instance_id
212
+ hives_built << instance_id
213
+ end
214
+ end
215
+ end
216
+ sleep(1)
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,110 @@
1
+ require 'aws-sdk'
2
+ require 'net/ssh'
3
+ require 'hornet/fleet'
4
+
5
+ module Fleet
6
+ class Hive
7
+ attr_accessor :instance_id
8
+ HOME_DIR = '/home/ubuntu'
9
+
10
+ def initialize(username, key, instance_id)
11
+ @username = username
12
+ @key = File.expand_path('~/.ssh/%s.pem' % key)
13
+
14
+ # grab the instance ip and id
15
+ instance = Aws::EC2::Instance.new instance_id
16
+ @instance_id = instance_id
17
+ @ip = instance.public_ip_address
18
+ end
19
+
20
+ # attack the target, clean the previous attack result, preapre the attack and then start the attack
21
+ def attack(option)
22
+ Net::SSH.start(@ip, @username, :keys => [@key]) do |ssh|
23
+ # remove all exited containers
24
+ clean_cmd = _clean ssh
25
+ clean_cmd.wait
26
+
27
+ # prepare the attack
28
+ create_cmd = _prepare ssh, option
29
+ create_cmd.wait
30
+
31
+ # build the command
32
+ #open a new channel and run the container
33
+ attack_cmd = _execute ssh, option
34
+ attack_cmd.wait
35
+ end
36
+ end
37
+
38
+ # connect to the instance and collect the results
39
+ def report
40
+ result = {}
41
+
42
+ Net::SSH.start(@ip, @username, :keys => [@key]) do |ssh|
43
+
44
+ data = ""
45
+ collection_cmd = _collection_info ssh, data
46
+ collection_cmd.wait
47
+
48
+ # parse the result
49
+ index = 1
50
+ data.split('Connection Times (ms)').each do |d|
51
+ data = Fleet.parse_ab_data d
52
+ if data.any?
53
+ result[index] = data
54
+ index += 1
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+ Fleet.report result
61
+ end
62
+
63
+ private
64
+ # add new ab command to ab.sh
65
+ def _prepare(ssh, option)
66
+ benchmark_command = 'ab -s 60 -r -n %{number} -c %{concurrent} "%{url}" >> /root/${HOSTNAME}.out' % option
67
+ ssh.open_channel do |cha|
68
+ cha.exec "touch %{path}/ab.sh && echo '%{cmd}' > %{path}/ab.sh" % {:cmd => benchmark_command, :path => HOME_DIR} do |ch, success|
69
+ raise "could not execute command" unless success
70
+ end
71
+ end
72
+ end
73
+
74
+ # start the attack
75
+ def _execute(ssh, option)
76
+ puts "Hive %s is starting it's attack" % @instance_id
77
+ ssh.open_channel do |cha|
78
+ # 'for i in {1..%s}; do nohup docker run -v /home/ubuntu:/root andizzle/debian bash /root/ab.sh; done'
79
+ cmd = ['docker run -v /home/ubuntu:/root andizzle/debian bash /root/ab.sh'] * option[:bees]
80
+ cha.exec cmd.join ' & ' do |ch, success|
81
+ raise "could not execute command" unless success
82
+ ch.on_data do |c, data|
83
+ puts data
84
+ end
85
+ ch.on_close { puts "Hive %s has finished it's attack!" % @instance_id}
86
+ end
87
+ end
88
+ end
89
+
90
+ def _collection_info(ssh, data_pool)
91
+ ssh.open_channel do |cha|
92
+ cha.exec 'find %s -name "*.out" -exec cat {} \;' % HOME_DIR do |ch, success|
93
+ raise "could not execute command" unless success
94
+ ch.on_data do |c, data|
95
+ data_pool << data
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ # clean up the battlefield
102
+ def _clean(ssh)
103
+ ssh.open_channel do |cha|
104
+ cha.exec 'docker ps -aq -f status=exited | xargs docker rm && find /home/ubuntu -name "*.out" -exec rm {} \;' % HOME_DIR do |ch, success|
105
+ raise "could not execute command" unless success
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: evilhornets
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Andy Zhang
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-ssh
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.9'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.9'
41
+ description: Stress test your web apps.
42
+ email: andizzle.zhang@gmail.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - bin/hornet
48
+ - lib/hornet.rb
49
+ - lib/hornet/fleet.rb
50
+ - lib/hornet/headquarter.rb
51
+ - lib/hornet/hive.rb
52
+ homepage: https://github.com/andizzle/evilhornets
53
+ licenses:
54
+ - MIT
55
+ metadata: {}
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubyforge_project:
72
+ rubygems_version: 2.4.5
73
+ signing_key:
74
+ specification_version: 4
75
+ summary: Launch EC2 micro instances, each instance creates multiple docker containers
76
+ to stress test your web applications.
77
+ test_files: []