First version of dashboard.

This commit is contained in:
Steve Pulec 2017-03-11 22:45:42 -05:00
parent cf771d7f14
commit 1709208872
9 changed files with 249 additions and 7 deletions

View File

@ -1,9 +1,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from __future__ import absolute_import from __future__ import absolute_import
from collections import defaultdict
import functools import functools
import inspect import inspect
import re import re
import six
from moto import settings from moto import settings
from moto.packages.responses import responses from moto.packages.responses import responses
@ -208,12 +210,38 @@ class Model(type):
return dec return dec
model_data = defaultdict(dict)
class InstanceTrackerMeta(type):
def __new__(meta, name, bases, dct):
cls = super(InstanceTrackerMeta, meta).__new__(meta, name, bases, dct)
if name == 'BaseModel':
return cls
service = cls.__module__.split(".")[1]
if name not in model_data[service]:
model_data[service][name] = cls
cls.instances = []
return cls
@six.add_metaclass(InstanceTrackerMeta)
class BaseModel(object):
def __new__(cls, *args, **kwargs):
instance = super(BaseModel, cls).__new__(cls, *args, **kwargs)
cls.instances.append(instance)
return instance
class BaseBackend(object): class BaseBackend(object):
def reset(self): def reset(self):
self.__dict__ = {} self.__dict__ = {}
self.__init__() self.__init__()
def get_models(self):
import pdb;pdb.set_trace()
models = getattr(backend.__class__, '__models__', {})
@property @property
def _url_module(self): def _url_module(self):
backend_module = self.__class__.__module__ backend_module = self.__class__.__module__

View File

@ -12,6 +12,7 @@ from jinja2 import Environment, DictLoader, TemplateNotFound
import six import six
from six.moves.urllib.parse import parse_qs, urlparse from six.moves.urllib.parse import parse_qs, urlparse
from flask import render_template
import xmltodict import xmltodict
from pkg_resources import resource_filename from pkg_resources import resource_filename
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
@ -350,6 +351,32 @@ class MotoAPIResponse(BaseResponse):
return 200, {}, json.dumps({"status": "ok"}) return 200, {}, json.dumps({"status": "ok"})
return 400, {}, json.dumps({"Error": "Need to POST to reset Moto"}) return 400, {}, json.dumps({"Error": "Need to POST to reset Moto"})
def model_data(self, request, full_url, headers):
from moto.core.models import model_data
results = {}
for service in sorted(model_data):
models = model_data[service]
results[service] = {}
for name in sorted(models):
model = models[name]
results[service][name] = []
for instance in model.instances:
inst_result = {}
for attr in dir(instance):
if not attr.startswith("_"):
try:
json.dumps(getattr(instance, attr))
except TypeError:
pass
else:
inst_result[attr] = getattr(instance, attr)
results[service][name].append(inst_result)
return 200, {"Content-Type": "application/javascript"}, json.dumps(results)
def dashboard(self, request, full_url, headers):
return render_template('dashboard.html')
class _RecursiveDictRef(object): class _RecursiveDictRef(object):
"""Store a recursive reference to dict.""" """Store a recursive reference to dict."""

View File

@ -8,5 +8,7 @@ url_bases = [
response_instance = MotoAPIResponse() response_instance = MotoAPIResponse()
url_paths = { url_paths = {
'{0}/moto-api/$': response_instance.dashboard,
'{0}/moto-api/data.json': response_instance.model_data,
'{0}/moto-api/reset': response_instance.reset_response, '{0}/moto-api/reset': response_instance.reset_response,
} }

View File

@ -122,7 +122,10 @@ class convert_flask_to_httpretty_response(object):
result = self.callback(request, request.url, {}) result = self.callback(request, request.url, {})
# result is a status, headers, response tuple # result is a status, headers, response tuple
status, headers, content = result if len(result) == 3:
status, headers, content = result
else:
status, headers, content = 200, {}, result
response = Response(response=content, status=status, headers=headers) response = Response(response=content, status=status, headers=headers)
if request.method == "HEAD" and 'content-length' in headers: if request.method == "HEAD" and 'content-length' in headers:

View File

@ -13,7 +13,7 @@ from boto.ec2.spotinstancerequest import SpotInstanceRequest as BotoSpotRequest
from boto.ec2.launchspecification import LaunchSpecification from boto.ec2.launchspecification import LaunchSpecification
from moto.core import BaseBackend from moto.core import BaseBackend
from moto.core.models import Model from moto.core.models import Model, BaseModel
from moto.core.utils import iso_8601_datetime_with_milliseconds, camelcase_to_underscores from moto.core.utils import iso_8601_datetime_with_milliseconds, camelcase_to_underscores
from .exceptions import ( from .exceptions import (
EC2ClientError, EC2ClientError,
@ -129,7 +129,7 @@ class StateReason(object):
self.code = code self.code = code
class TaggedEC2Resource(object): class TaggedEC2Resource(BaseModel):
def get_tags(self, *args, **kwargs): def get_tags(self, *args, **kwargs):
tags = self.ec2_backend.describe_tags( tags = self.ec2_backend.describe_tags(
@ -2612,7 +2612,7 @@ class InternetGatewayBackend(object):
return self.describe_internet_gateways(internet_gateway_ids=igw_ids)[0] return self.describe_internet_gateways(internet_gateway_ids=igw_ids)[0]
class VPCGatewayAttachment(object): class VPCGatewayAttachment(BaseModel):
def __init__(self, gateway_id, vpc_id): def __init__(self, gateway_id, vpc_id):
self.gateway_id = gateway_id self.gateway_id = gateway_id

View File

@ -47,7 +47,7 @@ class DomainDispatcherApplication(object):
def get_application(self, environ): def get_application(self, environ):
path_info = environ.get('PATH_INFO', '') path_info = environ.get('PATH_INFO', '')
if path_info.startswith("/moto-api"): if path_info.startswith("/moto-api") or path_info == "/favicon.ico":
host = "moto_api" host = "moto_api"
elif path_info.startswith("/latest/meta-data/"): elif path_info.startswith("/latest/meta-data/"):
host = "instance_metadata" host = "instance_metadata"

View File

@ -7,6 +7,7 @@ from xml.sax.saxutils import escape
import boto.sqs import boto.sqs
from moto.core import BaseBackend from moto.core import BaseBackend
from moto.core.models import BaseModel
from moto.core.utils import camelcase_to_underscores, get_random_message_id, unix_time, unix_time_millis from moto.core.utils import camelcase_to_underscores, get_random_message_id, unix_time, unix_time_millis
from .utils import generate_receipt_handle from .utils import generate_receipt_handle
from .exceptions import ( from .exceptions import (
@ -18,7 +19,7 @@ DEFAULT_ACCOUNT_ID = 123456789012
DEFAULT_SENDER_ID = "AIDAIT2UOQQY3AUEKVGXU" DEFAULT_SENDER_ID = "AIDAIT2UOQQY3AUEKVGXU"
class Message(object): class Message(BaseModel):
def __init__(self, message_id, body): def __init__(self, message_id, body):
self.id = message_id self.id = message_id
@ -93,7 +94,7 @@ class Message(object):
return False return False
class Queue(object): class Queue(BaseModel):
camelcase_attributes = ['ApproximateNumberOfMessages', camelcase_attributes = ['ApproximateNumberOfMessages',
'ApproximateNumberOfMessagesDelayed', 'ApproximateNumberOfMessagesDelayed',
'ApproximateNumberOfMessagesNotVisible', 'ApproximateNumberOfMessagesNotVisible',

View File

@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Moto</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" rel="stylesheet">
<style>
body {
padding-top: 70px;
padding-bottom: 30px;
}
.theme-dropdown .dropdown-menu {
position: static;
display: block;
margin-bottom: 20px;
}
.theme-showcase > p > .btn {
margin: 5px 0;
}
.theme-showcase .navbar .container {
width: auto;
}
</style>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Moto</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Home</a></li>
<li><a href="#about">About</a></li>
<!-- <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Region <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#">us-east-1</a></li>
<li><a href="#">us-west-1</a></li>
<li><a href="#">us-west-2</a></li>
</ul>
</li>
--> </ul>
</div>
</div>
</nav>
<div class="container theme-showcase" role="main" id="main">
<div class="jumbotron">
<h1>Moto Dashboard</h1>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.6/handlebars.js"></script>
{% raw %}
<script id="template" type="text/x-handlebars-template">
<ul id="myTab" class="nav nav-tabs">
{{#each data}}
<li {{#equal @index 0}}class="active"{{/equal}}><a href="#{{@key}}" data-toggle="tab">{{@key}}</a></li>
{{/each}}
</ul>
<div id="myTabContent" class="tab-content">
{{#each data}}
<div class="tab-pane fade {{#equal @index 0}}in active{{/equal}}" id="{{@key}}">
{{#each this}}
<div class="page-header">
<h1>{{@key}}</h1>
</div>
<div class="row">
<div class="col-md-12">
<table class="table table-striped table-bordered table-condensed">
{{#each this}}
<tr>
{{#each this}}
<td>{{@key}}: {{this}}</td>
{{/each}}
</tr>
{{/each}}
</table>
</div>
</div>
{{/each}}
</div>
{{/each}}
</div>
</script>
<script>
Handlebars.registerHelper('equal', function(lvalue, rvalue, options) {
if (arguments.length < 3)
throw new Error("Handlebars Helper equal needs 2 parameters");
if( lvalue!=rvalue ) {
return options.inverse(this);
} else {
return options.fn(this);
}
});
$(document).ready(function (){
$.getJSON("/moto-api/data.json", function(data) {
var source = $('#template').html();
var template = Handlebars.compile(source);
$('#main').append(template({"data": data}));
// $.each(data, function(model_type, instances) {
// $.each(instances, function(index) {
// instance = instances[index];
// if (index == 0) {
// var row = "<thead>";
// $.each(instance, function(attr, attr_val) {
// row += "<th>" + attr + "</th>";
// })
// row += "</thead><tbody>";
// $("#my_table").append(row);
// }
// var row = "<tr>";
// $.each(instance, function(attr, attr_val) {
// row += "<td>" + attr_val + "</td>";
// });
// row += "</tr>";
// $("#my_table").append(row);
// });
// $("#my_table").append("</tbody>");
// });
});
})
</script>
{% endraw %}
</body>
</html>

View File

@ -19,3 +19,15 @@ def test_reset_api():
res.content.should.equal(b'{"status": "ok"}') res.content.should.equal(b'{"status": "ok"}')
conn.list_queues().shouldnt.contain('QueueUrls') # No more queues conn.list_queues().shouldnt.contain('QueueUrls') # No more queues
@mock_sqs
def test_data_api():
conn = boto3.client("sqs", region_name='us-west-1')
conn.create_queue(QueueName="queue1")
res = requests.post("{base_url}/moto-api/data.json".format(base_url=base_url))
queues = res.json()['sqs']['Queue']
len(queues).should.equal(1)
queue = queues[0]
queue['name'].should.equal("queue1")