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 absolute_import
from collections import defaultdict
import functools
import inspect
import re
import six
from moto import settings
from moto.packages.responses import responses
@ -208,12 +210,38 @@ class Model(type):
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):
def reset(self):
self.__dict__ = {}
self.__init__()
def get_models(self):
import pdb;pdb.set_trace()
models = getattr(backend.__class__, '__models__', {})
@property
def _url_module(self):
backend_module = self.__class__.__module__

View File

@ -12,6 +12,7 @@ from jinja2 import Environment, DictLoader, TemplateNotFound
import six
from six.moves.urllib.parse import parse_qs, urlparse
from flask import render_template
import xmltodict
from pkg_resources import resource_filename
from werkzeug.exceptions import HTTPException
@ -350,6 +351,32 @@ class MotoAPIResponse(BaseResponse):
return 200, {}, json.dumps({"status": "ok"})
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):
"""Store a recursive reference to dict."""

View File

@ -8,5 +8,7 @@ url_bases = [
response_instance = MotoAPIResponse()
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,
}

View File

@ -122,7 +122,10 @@ class convert_flask_to_httpretty_response(object):
result = self.callback(request, request.url, {})
# 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)
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 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 .exceptions import (
EC2ClientError,
@ -129,7 +129,7 @@ class StateReason(object):
self.code = code
class TaggedEC2Resource(object):
class TaggedEC2Resource(BaseModel):
def get_tags(self, *args, **kwargs):
tags = self.ec2_backend.describe_tags(
@ -2612,7 +2612,7 @@ class InternetGatewayBackend(object):
return self.describe_internet_gateways(internet_gateway_ids=igw_ids)[0]
class VPCGatewayAttachment(object):
class VPCGatewayAttachment(BaseModel):
def __init__(self, gateway_id, vpc_id):
self.gateway_id = gateway_id

View File

@ -47,7 +47,7 @@ class DomainDispatcherApplication(object):
def get_application(self, environ):
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"
elif path_info.startswith("/latest/meta-data/"):
host = "instance_metadata"

View File

@ -7,6 +7,7 @@ from xml.sax.saxutils import escape
import boto.sqs
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 .utils import generate_receipt_handle
from .exceptions import (
@ -18,7 +19,7 @@ DEFAULT_ACCOUNT_ID = 123456789012
DEFAULT_SENDER_ID = "AIDAIT2UOQQY3AUEKVGXU"
class Message(object):
class Message(BaseModel):
def __init__(self, message_id, body):
self.id = message_id
@ -93,7 +94,7 @@ class Message(object):
return False
class Queue(object):
class Queue(BaseModel):
camelcase_attributes = ['ApproximateNumberOfMessages',
'ApproximateNumberOfMessagesDelayed',
'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"}')
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")