evilhornets 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []