Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Beginnings of a Command Line Interface #5

Closed
wants to merge 1 commit into from

2 participants

Jeff Larson Chris Amico
Jeff Larson

Here's a quick hack to remove the fabric dependency and start in on a command line interface. My thinking is that it'd be a bit more unix-y to have a command structure like:

$ bees up 
$ bees attack -c 5
$ bees down

Word to the wise: my python's rusty, so there may be weirdo rubyisms in here. Also, it's irresponsible of me, but I haven't actually tested this, so it's really meant more as a gist than anything else.

Chris Amico

Have you looked at argparse for command line parsing? Might work better with subcommands. It does add a dependency for python < 2.7, though.

Yeah I wrote it first with argparse but wanted to keep the bar low. It sucks though b/c of that gnarly conditional. Honestly Im not sure what the best practice is here --- it's been many moons since I played with snakes.

Ruby somehow manages to make this way easier than Python, sadly. Maybe I'll take a crack at it after the election.

Yeah, for what it's worth chris has merged this into a refactor branch:

http://github.com/newsapps/beeswithmachineguns/tree/refactor

This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Oct 27, 2010
  1. Jeff Larson
This page is out of date. Refresh to see the latest.
Showing with 115 additions and 67 deletions.
  1. +65 −67 bees.py
  2. +50 −0 main.py
132 bees.py
View
@@ -30,7 +30,6 @@
import time
import boto
-from fabric.api import *
import paramiko
EC2_INSTANCE_TYPE = 'm1.small'
@@ -39,39 +38,38 @@
# Load state from file
+instance_ids = []
if os.path.isfile(STATE_FILENAME):
with open(STATE_FILENAME, 'r') as f:
text = f.read()
- env.instance_ids = text.split('\n')
-
- print 'Read %i bees from the roster.' % len(env.instance_ids)
-else:
- env.instance_ids = []
+ instance_ids = text.split('\n')
+
+ print 'Read %i bees from the roster.' % len(instance_ids)
# Utilities
def _write_server_list(instances):
with open(STATE_FILENAME, 'w') as f:
f.write('\n'.join([instance.id for instance in instances]))
-
+
# Methods
def up(count=5, group='staging', zone='us-east-1d'):
"""
Startup the load testing server.
"""
- if env.instance_ids:
+ if instance_ids:
print 'Bees are already assembled and awaiting orders.'
return
-
+
count = int(count)
-
+
print 'Connecting to the hive.'
-
+
ec2_connection = boto.connect_ec2()
-
+
print 'Attempting to call up %i bees.' % count
-
+
reservation = ec2_connection.run_instances(
image_id='ami-ff17fb96',
min_count=count,
@@ -80,113 +78,113 @@ def up(count=5, group='staging', zone='us-east-1d'):
security_groups=[group],
instance_type=EC2_INSTANCE_TYPE,
placement=zone)
-
+
print 'Waiting for bees to load their machine guns...'
-
+
for instance in reservation.instances:
while instance.state != 'running':
print '.'
time.sleep(5)
instance.update()
-
+
print 'Bee %s is ready for the attack.' % instance.id
-
+
_write_server_list(reservation.instances)
-
+
print 'The swarm has assembled %i bees.' % len(reservation.instances)
-
+
def report():
"""
Report the status of the load testing servers.
- """
- if not env.instance_ids:
+ """
+ if not instance_ids:
print 'No bees have been mobilized.'
return
ec2_connection = boto.connect_ec2()
-
- reservations = ec2_connection.get_all_instances(instance_ids=env.instance_ids)
+
+ reservations = ec2_connection.get_all_instances(instance_ids=instance_ids)
instances = []
for reservation in reservations:
instances.extend(reservation.instances)
-
+
for instance in instances:
print 'Bee %s: %s' % (instance.id, instance.state)
-
+
def down():
"""
Shutdown the load testing server.
"""
- if not env.instance_ids:
+ if not instance_ids:
print 'No bees have been mobilized.'
return
-
+
print 'Connecting to the hive.'
ec2_connection = boto.connect_ec2()
-
+
print 'Calling off the swarm.'
-
+
terminated_instance_ids = ec2_connection.terminate_instances(
- instance_ids=env.instance_ids)
-
+ instance_ids=instance_ids)
+
print 'Stood down %i bees.' % len(terminated_instance_ids)
-
+
os.remove(STATE_FILENAME)
-
+
def _attack(params):
"""
Test the target URL with requests.
-
+
Intended for use with multiprocessing.
"""
print 'Bee %i is joining the swarm.' % params['i']
-
+
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
params['instance_name'],
username='newsapps',
key_filename='/Users/sk/.ssh/frakkingtoasters.pem')
-
+
print 'Bee %i is firing his machine gun. Bang bang!' % params['i']
-
+
stdin, stdout, stderr = client.exec_command('ab -r -n %(num_requests)s -c %(concurrent_requests)s -C "sessionid=NotARealSessionID" %(url)s' % params)
-
+
response = {}
-
+
ab_results = stdout.read()
ms_per_request_search = re.search('Time\ per\ request:\s+([0-9.]+)\ \[ms\]\ \(mean\)', ab_results)
-
+
if not ms_per_request_search:
print 'Bee %i lost sight of the target (connection timed out).' % params['i']
return None
-
- requests_per_second_search = re.search('Requests\ per\ second:\s+([0-9.]+)\ \[#\/sec\]\ \(mean\)', ab_results)
+
+ requests_per_second_search = re.search('Requests\ per\ second:\s+([0-9.]+)\ \[#\/sec\]\ \(mean\)', ab_results)
fifty_percent_search = re.search('\s+50\%\s+([0-9]+)', ab_results)
ninety_percent_search = re.search('\s+90\%\s+([0-9]+)', ab_results)
complete_requests_search = re.search('Complete\ requests:\s+([0-9]+)', ab_results)
-
+
response['ms_per_request'] = float(ms_per_request_search.group(1))
response['requests_per_second'] = float(requests_per_second_search.group(1))
response['fifty_percent'] = float(fifty_percent_search.group(1))
response['ninety_percent'] = float(ninety_percent_search.group(1))
response['complete_requests'] = float(complete_requests_search.group(1))
-
+
print 'Bee %i is out of ammo.' % params['i']
-
+
client.close()
-
+
return response
-
+
def _print_results(results):
"""
Print summarized load-testing results.
"""
incomplete_results = [r for r in results if r is None]
-
+
if incomplete_results:
print ' Target failed to fully respond to %i bees.' % len(incomplete_results)
@@ -197,19 +195,19 @@ def _print_results(results):
complete_results = [r['requests_per_second'] for r in results if r is not None]
mean_requests = sum(complete_results)
print ' Requests per second:\t%f [#/sec] (mean)' % mean_requests
-
+
complete_results = [r['ms_per_request'] for r in results if r is not None]
mean_response = sum(complete_results) / len(complete_results)
print ' Time per request:\t\t%f [ms] (mean)' % mean_response
-
+
complete_results = [r['fifty_percent'] for r in results if r is not None]
mean_fifty = sum(complete_results) / len(complete_results)
print ' 50%% response time:\t\t%f [ms] (mean)' % mean_fifty
-
+
complete_results = [r['ninety_percent'] for r in results if r is not None]
mean_ninety = sum(complete_results) / len(complete_results)
print ' 90%% response time:\t\t%f [ms] (mean)' % mean_ninety
-
+
if mean_response < 500:
print 'Mission Assessment: Target crushed bee offensive.'
elif mean_response < 1000:
@@ -225,31 +223,31 @@ def attack(url, n=10000, c=100):
"""
Test the root url of this site.
"""
- if not env.instance_ids:
+ if not instance_ids:
print 'No bees are ready to attack.'
return
-
+
print 'Connecting to the hive.'
ec2_connection = boto.connect_ec2()
print 'Assembling bees.'
- reservations = ec2_connection.get_all_instances(instance_ids=env.instance_ids)
-
+ reservations = ec2_connection.get_all_instances(instance_ids=instance_ids)
+
instances = []
-
+
for reservation in reservations:
instances.extend(reservation.instances)
-
+
instance_count = len(instances)
requests_per_instance = int(n) / instance_count
connections_per_instance = int(c) / instance_count
-
+
print 'Each of %i bees will fire %s rounds, %s at a time.' % (instance_count, requests_per_instance, connections_per_instance)
-
+
params = []
-
+
for i, instance in enumerate(instances):
params.append({
'i': i,
@@ -259,20 +257,20 @@ def attack(url, n=10000, c=100):
'concurrent_requests': connections_per_instance,
'num_requests': requests_per_instance,
})
-
+
print 'Stinging URL so it will be cached for the attack.'
-
+
# Ping url so it will be cached for testing
local('curl %s >> /dev/null' % url)
-
+
print 'Organizing the swarm.'
-
+
# Spin up processes for connecting to EC2 instances
pool = Pool(len(params))
results = pool.map(_attack, params)
-
+
print 'Offensive complete.'
-
+
_print_results(results)
print 'The swarm is awaiting new orders.'
50 main.py
View
@@ -0,0 +1,50 @@
+import bees
+import sys
+from optparse import OptionParser
+
+def parse_options():
+ """
+ Handle the command line arguments for spinning up bees
+ """
+ command = sys,
+ parser = OptionParser(usage="""
+bees COMMAND URL [options]
+
+Bees With Machine Guns
+
+A utility for arming (creating) many bees (small EC2 instances) to attack
+(load test) targets (web applications).
+
+commands:
+ up start a batch of load testing servers
+ attack begin the attack on a specific url
+ down shutdown and deactivate the load testing servers
+ """)
+
+ parser.add_option('-c', '--count', metavar="COUNT", nargs=1,
+ help="number of instance to start")
+ parser.add_option('-g', '--group', metavar="GROUP", nargs=1,
+ help="the security group to run the instances under")
+ parser.add_option('-z', '--zone', metavar="ZONE", nargs=1,
+ help="the availability zone to start the instances in")
+
+ (options, args) = parser.parse_args()
+
+ if not args > 0:
+ parser.error("please enter a command")
+ command = args[0]
+ if command[0] is "attack" and len(args) == 1:
+ parser.error("to run an attack you need to present a url")
+ url = args[-1].split(",")
+
+ if command == "up":
+ bees.up(count=options.count, group=options.group, zone=options.zone)
+ elif command == "attack":
+ bees.attack(url[0], url[1], url[2])
+ elif command == "down":
+ bees.down()
+
+
+def main():
+ parse_options()
+
Something went wrong with that request. Please try again.