wasp 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "aws-sdk", ">= 1.3.2"
4
+ gem "net-ssh", ">= 2.3.0"
5
+ gem "net-scp", ">= 1.0.4"
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Ji-Hun Seol
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,77 @@
1
+ WaspswithRainofStings
2
+ ====================
3
+
4
+ By arkemiq (arkemiq@gmail.com)
5
+
6
+ Description
7
+ -----------
8
+
9
+ Wasp is a distributed heavy load generator in Ruby inspired by beeswithmachineguns(https://github.com/newsapps/beeswithmachineguns).
10
+
11
+ This is a utility of arming(create and install load program) many wasps (micro EC2 instances)
12
+ from multiple regions(EC2 zones) to attack (load test) targets(web applications).
13
+
14
+ Installation
15
+ ------------
16
+
17
+ $ git clone https://github.com/arkemiq/wasp.git
18
+ $ cd wasp
19
+ $ bundle install
20
+
21
+ Configuring EC2 credentials
22
+ ---------------------------
23
+
24
+ Wasps uses aws-sdk gem to communicate with EC2 and supports all the same methods of storing credentials that it does.
25
+ Set access key and secret key for config/aws.yml to access your EC2 account.
26
+
27
+ access_key_id: xxxxxxxxxxxxxxxxx
28
+ secret_access_key: xxxxxxxxxxxxxxxxx
29
+
30
+ Running
31
+ -------
32
+
33
+ Usage:
34
+
35
+ To set credentials:
36
+ $ wasp set [AWS Credential file]
37
+
38
+ To launch 6 wasps:
39
+ - launch 6 instances in us-east zone with private key named wasps
40
+ $ wasp up -k wasps -s 6
41
+
42
+ - launch 5 instances in us-west-2 zone with ami-8cb33ebc AMI, username ubuntu and private key named wasps
43
+ $ wasp up -k wasps -z us-west-2 -a ami-8cb33ebc -s 5 -l ubuntu
44
+
45
+ To check status:
46
+ $ wasp status
47
+
48
+ To equip weapon(default: apachebench):
49
+ $ wasp equip
50
+
51
+ To attack target with 1000 requests and 100 concurrent users per each wasp:
52
+ $ wasp attack -n 1000 -c 100 -u http://target_site
53
+
54
+ To attack target with incrementally increase wasps from 1 to 10000 during 60 seconds:
55
+ $ wasp rattack -p 10000:60 -u http://target_site
56
+
57
+ To sleep wasps:
58
+ $ wasp down
59
+
60
+ To see options:
61
+ $ wasp help
62
+
63
+ *Note*: the default EC2 security group is called 'wasps' and by default it locks out SSH access.
64
+
65
+ *Note*: if you didn't specify a zone for EC2 instances, it use default zone 'us-east'.
66
+
67
+ *Please remember to do this*--we aren't responsible for your EC2 bills.
68
+
69
+ Notice! (PLEASE READ)
70
+ ---------------------
71
+ If you decide to use the Wasps, please keep in mind the following important notice: they are, more-or-less a distributed denial-of-service attack in a fancy package and, therefore, if you point them at any server you don’t own you will behaving *unethically*, have your Amazon Web Services account *locked-out*, and be *liable* in a court of law for any downtime you cause.
72
+
73
+ You have waspn warned.
74
+
75
+ License
76
+ -------
77
+ MIT.
@@ -0,0 +1,15 @@
1
+ require 'rake/testtask'
2
+
3
+ desc "Run rspec"
4
+ task :spec do
5
+ sh('bundle install')
6
+ require "rspec/core/rake_task"
7
+ RSpec::Core::RakeTask.new do |t|
8
+ t.rspec_opts = %w(-fs -c)
9
+ end
10
+ end
11
+ task :default => :spec
12
+
13
+ Rake::TestTask.new do |t|
14
+ t.libs << 'test'
15
+ end
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../../lib/wasp', __FILE__)
4
+
5
+ WASP::Nest.wakeup(ARGV.dup)
6
+
@@ -0,0 +1,4 @@
1
+ # Fill in your AWS Access Key ID and Secret Access Key
2
+ # http://aws.amazon.com/security-credentials
3
+ access_key_id:
4
+ secret_access_key:
@@ -0,0 +1,14 @@
1
+ ROOT = File.expand_path(File.dirname(__FILE__))
2
+
3
+ module WASP
4
+ autoload :Const, "#{ROOT}/wasp/const"
5
+ autoload :Wasp, "#{ROOT}/wasp/wasp"
6
+ autoload :QueenWasp, "#{ROOT}/wasp/queenwasp"
7
+ autoload :Nest, "#{ROOT}/wasp/nest"
8
+ autoload :Aws, "#{ROOT}/wasp/ec2"
9
+ autoload :Config, "#{ROOT}/wasp/config"
10
+ autoload :WeaponAB, "#{ROOT}/wasp/stingab"
11
+ end
12
+
13
+ require "#{ROOT}/wasp/core_ext"
14
+ require 'fileutils'
@@ -0,0 +1,11 @@
1
+
2
+ module WASP
3
+ class Config
4
+
5
+ class << self
6
+ attr_accessor :output
7
+ attr_accessor :colorize
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ module WASP
2
+ module Const
3
+ VERSION = '0.2.0'
4
+
5
+ DEFAULT_WASPS = 5
6
+ DEFAULT_GROUP = 'wasps'
7
+ DEFAULT_ZONE = 'us-east-1'
8
+ DEFAULT_AMI = 'ami-baba68d3'
9
+ DEFAULT_USER = 'ubuntu'
10
+ DEFAULT_KEY = 'wasp'
11
+ DEFAULT_NUMBER_OF_REQUESTS = 1000
12
+ DEFAULT_TIME = 10
13
+ DEFAULT_CONCURRENT_OF_CONNECTIONS = 100
14
+ DEFAULT_WEAPON = 'ab'
15
+ SSH_PORT = 22
16
+ NATS_PORT = 4222
17
+ DEFAULT_MAXIMUM_STINGS = 10000
18
+ DEFAULT_WAVE_TIME = 10
19
+ DEFAULT_COOLDOWN = 10
20
+ end
21
+ end
@@ -0,0 +1,113 @@
1
+ module WASPExtensions
2
+
3
+ def say(message)
4
+ WASP::Config.output.puts(message) if WASP::Config.output
5
+ end
6
+
7
+ def header(message, filler = '-')
8
+ say "\n"
9
+ say message
10
+ say filler.to_s * message.size
11
+ end
12
+
13
+ def banner(message)
14
+ say "\n"
15
+ say message
16
+ end
17
+
18
+ def display(message, nl=true)
19
+ if nl
20
+ say message
21
+ else
22
+ if WASP::Config.output
23
+ WASP::Config.output.print(message)
24
+ WASP::Config.output.flush
25
+ end
26
+ end
27
+ end
28
+
29
+ def clear(size=80)
30
+ return unless WASP::Config.output
31
+ WASP::Config.output.print("\r")
32
+ WASP::Config.output.print(" " * size)
33
+ WASP::Config.output.print("\r")
34
+ #WASP::Config.output.flush
35
+ end
36
+
37
+ def err(message, prefix='Error: ')
38
+ raise WASP::CliExit, "#{prefix}#{message}"
39
+ end
40
+
41
+ def quit(message = nil)
42
+ raise WASP::GracefulExit, message
43
+ end
44
+
45
+ def blank?
46
+ self.to_s.blank?
47
+ end
48
+
49
+ def uptime_string(delta)
50
+ num_seconds = delta.to_i
51
+ days = num_seconds / (60 * 60 * 24);
52
+ num_seconds -= days * (60 * 60 * 24);
53
+ hours = num_seconds / (60 * 60);
54
+ num_seconds -= hours * (60 * 60);
55
+ minutes = num_seconds / 60;
56
+ num_seconds -= minutes * 60;
57
+ "#{days}d:#{hours}h:#{minutes}m:#{num_seconds}s"
58
+ end
59
+
60
+ def pretty_size(size, prec=1)
61
+ return 'NA' unless size
62
+ return "#{size}B" if size < 1024
63
+ return sprintf("%.#{prec}fK", size/1024.0) if size < (1024*1024)
64
+ return sprintf("%.#{prec}fM", size/(1024.0*1024.0)) if size < (1024*1024*1024)
65
+ return sprintf("%.#{prec}fG", size/(1024.0*1024.0*1024.0))
66
+ end
67
+
68
+ end
69
+
70
+ module StringExtensions
71
+ def red
72
+ colorize("\e[0m\e[31m")
73
+ end
74
+
75
+ def green
76
+ colorize("\e[0m\e[32m")
77
+ end
78
+
79
+ def yellow
80
+ colorize("\e[0m\e[33m")
81
+ end
82
+
83
+ def bold
84
+ colorize("\e[0m\e[1m")
85
+ end
86
+
87
+ def colorize(color_code)
88
+ "#{color_code}#{self}\e[0m"
89
+ end
90
+
91
+ def blank?
92
+ self =~ /^\s*$/
93
+ end
94
+
95
+ def truncate(limit = 30)
96
+ return "" if self.blank?
97
+ etc = "..."
98
+ stripped = self.strip[0..limit]
99
+ if stripped.length > limit
100
+ stripped.gsub(/\s+?(\S+)?$/, "") + etc
101
+ else
102
+ stripped
103
+ end
104
+ end
105
+ end
106
+
107
+ class Object
108
+ include WASPExtensions
109
+ end
110
+
111
+ class String
112
+ include StringExtensions
113
+ end
@@ -0,0 +1,355 @@
1
+ require 'aws-sdk'
2
+
3
+ module WASP
4
+ class Aws
5
+ attr_reader :config_path
6
+ attr_reader :ec2
7
+ attr_reader :group
8
+ attr_reader :key
9
+ attr_reader :instances
10
+ attr_reader :regions
11
+ attr_reader :instance_list
12
+ attr_reader :threads
13
+ attr_reader :login
14
+
15
+ SLEEP_TIME = 1
16
+ LINE_LENGTH = 100
17
+
18
+ def initialize (args)
19
+ config_path = ENV["HOME"] + "/.waspaws.yml"
20
+ begin
21
+ AWS.config(YAML.load(File.read(config_path)))
22
+ @ec2 = AWS::EC2.new
23
+ rescue => ex
24
+ puts "[WARN]".yellow + " #{ex.message}"
25
+ puts "[WARN]".yellow + " Please set AWS credential file."
26
+ exit false
27
+ end
28
+
29
+ # evaluate AWS access_key and secret_access_key
30
+ begin
31
+ print "EC2".green + " Checking access key validation.."
32
+ @ec2.availability_zones.each do |av|
33
+ av.name
34
+ end
35
+ puts " OK".green
36
+ rescue => ex
37
+ puts "[WARN]".yellow + " #{ex.message}"
38
+ puts "[WARN]".yellow + " Please copy/paste correct AWS access_key and secret_access_key to config/aws.yml file"
39
+ exit false
40
+ end
41
+
42
+ @num_wasps = if args[:server].nil? then WASP::Const::DEFAULT_WASPS
43
+ else
44
+ args[:server].to_i
45
+ end
46
+ @group = if args[:group].nil? then WASP::Const::DEFAULT_GROUP
47
+ else
48
+ args[:group]
49
+ end
50
+ @zone = if args[:zone].nil? then WASP::Const::DEFAULT_ZONE
51
+ else
52
+ args[:zone]
53
+ end
54
+ @ami = if args[:ami].nil? then get_default_ami(@zone)
55
+ else
56
+ args[:ami]
57
+ end
58
+ @login = if args[:login].nil? then WASP::Const::DEFAULT_USER
59
+ else
60
+ args[:login]
61
+ end
62
+
63
+ @key = args[:key]
64
+
65
+
66
+ @regions = @ec2.regions
67
+ @instance_list = []
68
+ @instances = nil
69
+ end
70
+
71
+ def get_keypair
72
+ make_key = true
73
+ key = @regions[@zone].key_pairs.filter('key-name', @key + '-' + @zone).first
74
+
75
+ if key.nil? == false then
76
+ if not File.exists?("#{ENV['HOME']}/.ssh/#{@key}-#{@zone}.pem") then
77
+ key.delete
78
+ make_key = true
79
+ else
80
+ make_key = false
81
+ end
82
+ end
83
+
84
+ if make_key then
85
+ key = @regions[@zone].key_pairs.create(@key + '-' + @zone)
86
+ keyfile = "#{ENV['HOME']}/.ssh/#{@key}-#{@zone}.pem"
87
+ begin
88
+ File.delete(keyfile) if File.exists?(keyfile)
89
+ File.open(keyfile, "w") do |f|
90
+ f.write(key.private_key)
91
+ end
92
+ File.chmod(0400, keyfile)
93
+ rescue => ex
94
+ print "[WARN]".yellow + " #{ex.message}"
95
+ exit false
96
+ end
97
+ end
98
+
99
+ puts "EC2".green + " Private key is created in " + "~/.ssh/#{@key}-#{@zone}.pem".bold
100
+ end
101
+
102
+ def get_security_group
103
+ found = false
104
+ puts "EC2".green + " Look up existed security group [" + "#{@group}".bold + "].."
105
+ security_groups = @regions[@zone].security_groups
106
+
107
+ found = true if security_groups.filter('group-name', @group).first != nil
108
+
109
+ if not found then
110
+ puts "EC2".green + " Creating security group " + "#{@group}".bold + ".."
111
+ wasps = security_groups.create(@group)
112
+ puts "EC2".green + " Open inbound tcp port :#{WASP::Const::SSH_PORT}"
113
+ wasps.authorize_ingress(:tcp, WASP::Const::SSH_PORT)
114
+ puts "EC2".green + " Open inbound tcp port :#{WASP::Const::NATS_PORT}"
115
+ wasps.authorize_ingress(:tcp, WASP::Const::NATS_PORT)
116
+ puts "EC2".green + " Allow ping"
117
+ wasps.allow_ping
118
+ end
119
+
120
+ puts "EC2".green + " Security group [#{@group}] have set.."
121
+ end
122
+
123
+ def waiting_wasp (i)
124
+ banner = "EC2".green + " Launching wasps: "
125
+
126
+ display banner, false
127
+ while i.status == :pending do
128
+ print '.'
129
+ sleep SLEEP_TIME
130
+ end
131
+
132
+ if i.status == :running then
133
+ clear(LINE_LENGTH)
134
+ display "#{banner}#{'OK'.green}"
135
+ @instance_list.push(i.id)
136
+ puts "Wasp " + "#{i.id}".yellow + " is ready to attack"
137
+ i.tags.Name = 'a wasp!'
138
+ else
139
+ puts "EC2".green + " #{i.id} is going to wrong place"
140
+ end
141
+ end
142
+
143
+ def create
144
+ puts "EC2".green + " #{@num_wasps} wasps will be launched.."
145
+
146
+ begin
147
+ @instances = @ec2.regions[@zone].instances.create(:image_id=>@ami,
148
+ :security_groups=>[@group],
149
+ :key_name=>@key + '-' + @zone,
150
+ :instance_type=>'t1.micro',
151
+ :count=>@num_wasps)
152
+ num_wasps = 0
153
+ rescue => ex
154
+ puts "[WARN]".yellow + " #{ex.message}"
155
+ return false
156
+ end
157
+ if @instances.class == Array then
158
+ @instances.each do |i|
159
+ waiting_wasp(i)
160
+ end
161
+ num_wasps = @instances.count
162
+ else
163
+ waiting_wasp(@instances)
164
+ num_wasps = 1
165
+ end
166
+
167
+ _write_to_file
168
+
169
+ puts "The swarm has assembled " + "#{num_wasps}".yellow + " wasps.\n"
170
+ true
171
+ end
172
+
173
+ def _get_instances
174
+ status_of_wasps = {}
175
+ if @instance_list.count == 0 then
176
+ begin
177
+ File.open("#{ENV["HOME"]}/.nest", "r") do |file|
178
+ lines = file.readlines
179
+ lines.each do |line|
180
+ instance, key, zone, login = line.split(' ')
181
+
182
+ if status_of_wasps[zone].nil? then
183
+ status_of_wasps[zone] = {}
184
+ status_of_wasps[zone][:instances] = []
185
+ status_of_wasps[zone][:instances].push(instance)
186
+ status_of_wasps[zone][:login] = login
187
+ else
188
+ status_of_wasps[zone][:instances].push(instance)
189
+ end
190
+ end
191
+ end
192
+ rescue => ex
193
+ puts "There is no wasps in the air."
194
+
195
+ return nil
196
+ end
197
+ end
198
+
199
+ return status_of_wasps
200
+ end
201
+
202
+ def _write_to_file
203
+ begin
204
+ File.open("#{ENV["HOME"]}/.nest", "a+") do |file|
205
+ if @instances.class == Array then
206
+ @instances.each do |i|
207
+ file.puts("#{i.id} #{i.key_name} #{i.availability_zone} #{@login}\n")
208
+ end
209
+ else
210
+ file.puts("#{@instances.id} #{@instances.key_name} #{@instances.availability_zone} #{@login}\n")
211
+ end
212
+ end
213
+ rescue => ex
214
+ puts "[WARN]".yellow + " #{ex.message}"
215
+ end
216
+ end
217
+
218
+ def _delete_file
219
+ begin
220
+ File.delete("#{ENV["HOME"]}/.nest")
221
+ rescue => ex
222
+ puts "[WARN]".yellow + "#{ex.message}"
223
+ puts "There is no wasps in the air."
224
+ end
225
+ end
226
+
227
+ def terminate
228
+ count = 0
229
+
230
+ #if @instances.nil? then
231
+ ins = _get_instances
232
+
233
+ return nil if ins.nil?
234
+
235
+
236
+ ins.each do |region, info|
237
+ r = get_ec2_zone(region)
238
+ info[:instances].each do |i|
239
+ count += 1
240
+ puts "Wasp" + " #{i}".yellow + " from #{region} is going home"
241
+ begin
242
+ @ec2.regions[r].instances[i].terminate
243
+ rescue => e
244
+ puts "#{e.message}"
245
+ end
246
+ end
247
+ end
248
+
249
+ _delete_file
250
+
251
+ puts "EC2".green + " Stood down #{count} wasps.\n"
252
+ end
253
+
254
+ def get_wasps
255
+ wasps = []
256
+ ins = _get_instances
257
+
258
+ return nil if ins.nil?
259
+
260
+ count = ins.count
261
+ ins.each do |region, info|
262
+ r = get_ec2_zone(region)
263
+ login = info[:login]
264
+ info[:instances].each do |it|
265
+ i = @ec2.regions[r].instances[it]
266
+
267
+ wasps.push({:instance_id => i.id,
268
+ :instance_name => i.dns_name,
269
+ :region => region,
270
+ :login => login,
271
+ :key_name => i.key_pair.name,
272
+ :report => nil,
273
+ :wavereport => nil})
274
+ end
275
+ end
276
+
277
+ wasps
278
+ end
279
+
280
+ def status
281
+ return ins = _get_instances
282
+ end
283
+
284
+ def get_wasp_status(zone=WASP::Const::DEFAULT_ZONE , id)
285
+ begin
286
+ z = get_ec2_zone(zone)
287
+ @ec2.regions[z].instances[id].status
288
+ rescue => ex
289
+ puts "[WARN]".yellow + " #{ex.message}"
290
+ return nil
291
+ end
292
+ end
293
+
294
+ def get_wasp_domain(zone=WASP::Const::DEFAULT_ZONE , id)
295
+ begin
296
+ z = get_ec2_zone(zone)
297
+ @ec2.regions[z].instances[id].dns_name
298
+ rescue => ex
299
+ puts "[WARN]".yellow + " #{ex.message}"
300
+ return nil
301
+ end
302
+ end
303
+
304
+ def get_ec2_zone(zone)
305
+ case
306
+ when zone.match(/eu-west-1/)
307
+ "eu-west-1"
308
+ when zone.match(/sa-east-1/)
309
+ "sa-east-1"
310
+ when zone.match(/us-west-1/)
311
+ "us-west-1"
312
+ when zone.match(/us-west-2/)
313
+ "us-west-2"
314
+ when zone.match(/ap-northeast-1/)
315
+ "ap-northeast-1"
316
+ when zone.match(/us-east-1/)
317
+ "us-east-1"
318
+ when zone.match(/ap-southeast-1/)
319
+ "ap-southeast-1"
320
+ else
321
+ "us-east-1"
322
+ end
323
+ end
324
+
325
+ def get_default_ami(zone)
326
+ case
327
+ when zone.match(/eu-west-1/)
328
+ "ami-895069fd"
329
+ when zone.match(/sa-east-1/)
330
+ "ami-b673acab"
331
+ when zone.match(/us-west-1/)
332
+ "ami-6da8f128"
333
+ when zone.match(/us-west-2/)
334
+ "ami-ae05889e"
335
+ when zone.match(/ap-northeast-1/)
336
+ "ami-10299f11"
337
+ when zone.match(/us-east-1/)
338
+ "ami-baba68d3"
339
+ when zone.match(/ap-southeast-1/)
340
+ "ami-4296d210"
341
+ else
342
+ "ami-baba68d3"
343
+ end
344
+ end
345
+
346
+ def get_regions
347
+ regions = []
348
+ @ec2.regions.each do |r|
349
+ regions.push(r.name)
350
+ end
351
+
352
+ regions
353
+ end
354
+ end
355
+ end