First version of dashboard.
This commit is contained in:
parent
cf771d7f14
commit
1709208872
@ -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__
|
||||
|
@ -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."""
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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',
|
||||
|
169
moto/templates/dashboard.html
Normal file
169
moto/templates/dashboard.html
Normal 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>
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user