Skip to content

Commit

Permalink
Adding server mode
Browse files Browse the repository at this point in the history
  • Loading branch information
spulec committed Mar 5, 2013
1 parent c6f5aff commit a728b25
Show file tree
Hide file tree
Showing 31 changed files with 489 additions and 66 deletions.
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[report]

exclude_lines =
if __name__ == .__main__.:
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,32 @@ It gets even better! Moto isn't just S3. Here's the status of the other AWS serv
* SES (@mock_ses) - core done
* SQS (@mock_sqs) - core done

This library has been tested on boto v2.5+.
For example, imagine you have a function that you use to launch new ec2 instances:

```python
import boto

def add_servers(ami_id, count):
conn = boto.connect_ec2('the_key', 'the_secret')
for index in range(count):
conn.run_instances(ami_id)
```

To test it:

```python
from . import add_servers

@mock_ec2
def test_add_servers():
add_servers('ami-1234abcd', 2)

conn = boto.connect_ec2('the_key', 'the_secret')
reservations = conn.get_all_instances()
assert len(reservations) == 2
instance1 = reservations[0].instances[0]
assert instance1.image_id == 'ami-1234abcd'
```

## Usage

Expand Down Expand Up @@ -108,8 +133,28 @@ def test_my_model_save():
mock.stop()
```

## Stand-alone Server Mode

Moto also comes with a stand-alone server mode. This allows you to utilize the backend structure of Moto even if you don't use Python.

To run a service:

```console
$ moto_server ec2
* Running on http://127.0.0.1:5000/
```

Then go to [localhost](http://localhost:5000/?Action=DescribeInstances) to see a list of running instances (it will be empty since you haven't added any yet).

## Install

```console
$ pip install moto
```

This library has been tested on boto v2.5+.


## Thanks

A huge thanks to [Gabriel Falcão](https://github.com/gabrielfalcao) and his [HTTPretty](https://github.com/gabrielfalcao/HTTPretty) library. Moto would not exist without it.

This comment has been minimized.

Copy link
@gabrielfalcao

gabrielfalcao Feb 11, 2015

❤️

50 changes: 47 additions & 3 deletions moto/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import re

from moto.packages.httpretty import HTTPretty
from .utils import convert_regex_to_flask_path


class MockAWS(object):
Expand Down Expand Up @@ -48,13 +49,56 @@ def reset(self):
self.__init__()

@property
def urls(self):
def _url_module(self):
backend_module = self.__class__.__module__
backend_urls_module_name = backend_module.replace("models", "urls")
backend_urls_module = __import__(backend_urls_module_name, fromlist=['urls'])
urls = backend_urls_module.urls
backend_urls_module = __import__(backend_urls_module_name, fromlist=['url_bases', 'url_paths'])
return backend_urls_module

@property
def urls(self):
"""
A dictionary of the urls to be mocked with this service and the handlers
that should be called in their place
"""
url_bases = self._url_module.url_bases
unformatted_paths = self._url_module.url_paths

urls = {}
for url_base in url_bases:
for url_path, handler in unformatted_paths.iteritems():
url = url_path.format(url_base)
urls[url] = handler

return urls

@property
def url_paths(self):
"""
A dictionary of the paths of the urls to be mocked with this service and
the handlers that should be called in their place
"""
unformatted_paths = self._url_module.url_paths

paths = {}
for unformatted_path, handler in unformatted_paths.iteritems():
path = unformatted_path.format("")
paths[path] = handler

return paths

@property
def flask_paths(self):
"""
The url paths that will be used for the flask server
"""
paths = {}
for url_path, handler in self.url_paths.iteritems():
url_path = convert_regex_to_flask_path(url_path)
paths[url_path] = handler

return paths

def decorator(self, func=None):
if func:
return MockAWS(self)(func)
Expand Down
5 changes: 4 additions & 1 deletion moto/core/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@


class BaseResponse(object):
def dispatch2(self, uri, body, headers):
return self.dispatch(uri, body, headers)

def dispatch(self, uri, body, headers):
if body:
querystring = parse_qs(body)
Expand All @@ -13,7 +16,7 @@ def dispatch(self, uri, body, headers):
self.path = uri.path
self.querystring = querystring

action = querystring['Action'][0]
action = querystring.get('Action', [""])[0]
action = camelcase_to_underscores(action)

method_names = method_names_from_class(self.__class__)
Expand Down
60 changes: 60 additions & 0 deletions moto/core/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
from collections import namedtuple
import inspect
import random
import re
from urlparse import parse_qs

from flask import request


def headers_to_dict(headers):
if isinstance(headers, dict):
# If already dict, return
return headers

result = {}
for index, header in enumerate(headers.split("\r\n")):
if not header:
Expand Down Expand Up @@ -51,3 +59,55 @@ def get_random_hex(length=8):

def get_random_message_id():
return '{}-{}-{}-{}-{}'.format(get_random_hex(8), get_random_hex(4), get_random_hex(4), get_random_hex(4), get_random_hex(12))


def convert_regex_to_flask_path(url_path):
"""
Converts a regex matching url to one that can be used with flask
"""
for token in ["$"]:
url_path = url_path.replace(token, "")

def caller(reg):
match_name, match_pattern = reg.groups()
return '<regex("{0}"):{1}>'.format(match_pattern, match_name)

url_path = re.sub("\(\?P<(.*?)>(.*?)\)", caller, url_path)
return url_path


class convert_flask_to_httpretty_response(object):
def __init__(self, callback):
self.callback = callback

@property
def __name__(self):
# For instance methods, use class and method names. Otherwise
# use module and method name
if inspect.ismethod(self.callback):
outer = self.callback.im_class.__name__
else:
outer = self.callback.__module__
return "{}.{}".format(outer, self.callback.__name__)

def __call__(self, args=None, **kwargs):
hostname = request.host_url
method = request.method
path = request.path
query = request.query_string

# Mimic the HTTPretty URIInfo class
URI = namedtuple('URI', 'hostname method path query')
uri = URI(hostname, method, path, query)

body = request.data or query
headers = dict(request.headers)
result = self.callback(uri, body, headers)
if isinstance(result, basestring):
# result is just the response
return result
else:
# result is a responce, headers tuple
response, headers = result
status = headers.pop('status', None)
return response, status, headers
14 changes: 9 additions & 5 deletions moto/dynamodb/responses.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import re
import json

from moto.core.utils import headers_to_dict
from .models import dynamodb_backend


Expand All @@ -17,12 +17,16 @@ def get_method_name(self, headers):
ie: X-Amz-Target: DynamoDB_20111205.ListTables -> ListTables
"""
match = re.search(r'X-Amz-Target: \w+\.(\w+)', headers)
return match.groups()[0]
match = headers.get('X-Amz-Target')
if match:
return match.split(".")[1]

def dispatch(self):
method = self.get_method_name(self.headers)
return getattr(self, method)(self.uri, self.body, self.headers)
if method:
return getattr(self, method)(self.uri, self.body, self.headers)
else:
return "", dict(status=404)

def ListTables(self, uri, body, headers):
tables = dynamodb_backend.tables.keys()
Expand All @@ -36,4 +40,4 @@ def DescribeTable(self, uri, body, headers):


def handler(uri, body, headers):
return DynamoHandler(uri, body, headers).dispatch()
return DynamoHandler(uri, body, headers_to_dict(headers)).dispatch()
10 changes: 7 additions & 3 deletions moto/dynamodb/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
def sts_handler(uri, body, headers):
return GET_SESSION_TOKEN_RESULT

urls = {
"https?://dynamodb.us-east-1.amazonaws.com/": handler,
"https?://sts.amazonaws.com/": sts_handler,
url_bases = [
"https?://dynamodb.us-east-1.amazonaws.com",
"https?://sts.amazonaws.com",
]

url_paths = {
"{0}/": handler,
}


Expand Down
13 changes: 7 additions & 6 deletions moto/ec2/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ def get_instance(self, instance_id):
if instance.id == instance_id:
return instance

def add_instances(self, count):
def add_instances(self, image_id, count):
new_reservation = Reservation()
new_reservation.id = random_reservation_id()
for index in range(count):
new_instance = Instance()
new_instance.id = random_instance_id()
new_instance.image_id = image_id
new_instance._state_name = "pending"
new_instance._state_code = 0
new_reservation.instances.append(new_instance)
Expand Down Expand Up @@ -226,11 +227,11 @@ def __init__(self, ip_protocol, from_port, to_port, ip_ranges, source_groups):
@property
def unique_representation(self):
return "{}-{}-{}-{}-{}".format(
self.ip_protocol,
self.from_port,
self.to_port,
self.ip_ranges,
self.source_groups
self.ip_protocol,
self.from_port,
self.to_port,
self.ip_ranges,
self.source_groups
)

def __eq__(self, other):
Expand Down
5 changes: 3 additions & 2 deletions moto/ec2/responses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ def dispatch(self, uri, body, headers):
else:
querystring = parse_qs(headers)

action = querystring['Action'][0]
action = camelcase_to_underscores(action)
action = querystring.get('Action', [None])[0]
if action:
action = camelcase_to_underscores(action)

for sub_response in self.sub_responses:
method_names = method_names_from_class(sub_response)
Expand Down
7 changes: 4 additions & 3 deletions moto/ec2/responses/instances.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ def describe_instances(self):

def run_instances(self):
min_count = int(self.querystring.get('MinCount', ['1'])[0])
new_reservation = ec2_backend.add_instances(min_count)
image_id = self.querystring.get('ImageId')[0]
new_reservation = ec2_backend.add_instances(image_id, min_count)
template = Template(EC2_RUN_INSTANCES)
return template.render(reservation=new_reservation)

Expand Down Expand Up @@ -75,7 +76,7 @@ def modify_instance_attribute(self):
{% for instance in reservation.instances %}
<item>
<instanceId>{{ instance.id }}</instanceId>
<imageId>ami-60a54009</imageId>
<imageId>{{ instance.image_id }}</imageId>
<instanceState>
<code>{{ instance._state_code }}</code>
<name>{{ instance._state_name }}</name>
Expand Down Expand Up @@ -127,7 +128,7 @@ def modify_instance_attribute(self):
{% for instance in reservation.instances %}
<item>
<instanceId>{{ instance.id }}</instanceId>
<imageId>ami-1a2b3c4d</imageId>
<imageId>{{ instance.image_id }}</imageId>
<instanceState>
<code>{{ instance._state_code }}</code>
<name>{{ instance._state_name }}</name>
Expand Down
9 changes: 7 additions & 2 deletions moto/ec2/urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
from .responses import EC2Response

urls = {
"https?://ec2.us-east-1.amazonaws.com/": EC2Response().dispatch,

url_bases = [
"https?://ec2.us-east-1.amazonaws.com",
]

url_paths = {
'{0}/': EC2Response().dispatch,
}
5 changes: 3 additions & 2 deletions moto/s3/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ def set_key(self, bucket_name, key_name, value):
return new_key

def get_key(self, bucket_name, key_name):
bucket = self.buckets[bucket_name]
return bucket.keys.get(key_name)
bucket = self.get_bucket(bucket_name)
if bucket:
return bucket.keys.get(key_name)

def prefix_query(self, bucket, prefix):
key_results = set()
Expand Down

0 comments on commit a728b25

Please sign in to comment.