wasp 0.2.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.
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