mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
New base server.
This commit is contained in:
1 parent
5e665cc67a
commit
61344a1669
51 files changed
+2632
-1835
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,131 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
import os.path
|
||||
|
||||
from .route import Route
|
||||
|
||||
|
||||
class Documentation(object):
|
||||
"""Extract API documentation as Sphinx compatible files"""
|
||||
def __init__(self, route):
|
||||
self._documentation = route.get_documentation()
|
||||
|
||||
def write(self):
|
||||
for path in sorted(self._documentation):
|
||||
filename = self._file_path(path)
|
||||
handler_doc = self._documentation[path]
|
||||
with open("docs/api/{}.rst".format(filename), 'w+') as f:
|
||||
f.write('{}\n------------------------------\n\n'.format(path))
|
||||
f.write('.. contents::\n')
|
||||
for method in handler_doc["methods"]:
|
||||
f.write('\n{} {}\n'.format(method["method"], path))
|
||||
f.write('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n')
|
||||
f.write('{}\n\n'.format(method["description"]))
|
||||
|
||||
if len(method["parameters"]) > 0:
|
||||
f.write("Parameters\n**********\n")
|
||||
for parameter in method["parameters"]:
|
||||
desc = method["parameters"][parameter]
|
||||
f.write("- **{}**: {}\n".format(parameter, desc))
|
||||
f.write("\n")
|
||||
|
||||
f.write("Response status codes\n*******************\n")
|
||||
for code in method["status_codes"]:
|
||||
desc = method["status_codes"][code]
|
||||
f.write("- **{}**: {}\n".format(code, desc))
|
||||
f.write("\n")
|
||||
|
||||
if "properties" in method["input_schema"]:
|
||||
f.write("Input\n*******\n")
|
||||
self._write_definitions(f, method["input_schema"])
|
||||
self.__write_json_schema(f, method["input_schema"])
|
||||
|
||||
if "properties" in method["output_schema"]:
|
||||
f.write("Output\n*******\n")
|
||||
self.__write_json_schema(f, method["output_schema"])
|
||||
|
||||
self._include_query_example(f, method, path)
|
||||
|
||||
def _include_query_example(self, f, method, path):
|
||||
"""If a sample session is available we include it in documentation"""
|
||||
m = method["method"].lower()
|
||||
query_path = "examples/{}_{}.txt".format(m, self._file_path(path))
|
||||
if os.path.isfile("docs/api/{}".format(query_path)):
|
||||
f.write("Sample session\n***************\n")
|
||||
f.write("\n\n.. literalinclude:: {}\n\n".format(query_path))
|
||||
|
||||
def _file_path(self, path):
|
||||
return re.sub('[^a-z0-9]', '', path)
|
||||
|
||||
def _write_definitions(self, f, schema):
|
||||
if "definitions" in schema:
|
||||
f.write("Types\n+++++++++\n")
|
||||
for definition in sorted(schema['definitions']):
|
||||
desc = schema['definitions'][definition].get("description")
|
||||
f.write("{}\n^^^^^^^^^^^^^^^^\n{}\n\n".format(definition, desc))
|
||||
self._write_json_schema(f, schema['definitions'][definition])
|
||||
f.write("Body\n+++++++++\n")
|
||||
|
||||
def _write_json_schema_object(self, f, obj, schema):
|
||||
"""
|
||||
obj is current object in JSON schema
|
||||
schema is the whole schema including definitions
|
||||
"""
|
||||
for name in sorted(obj.get("properties", {})):
|
||||
prop = obj["properties"][name]
|
||||
mandatory = " "
|
||||
if name in obj.get("required", []):
|
||||
mandatory = "✔"
|
||||
|
||||
if "enum" in prop:
|
||||
field_type = "enum"
|
||||
prop['description'] = "Possible values: {}".format(', '.join(prop['enum']))
|
||||
else:
|
||||
field_type = prop.get("type", "")
|
||||
|
||||
# Resolve oneOf relation to their human type.
|
||||
if field_type == 'object' and 'oneOf' in prop:
|
||||
field_type = ', '.join(map(lambda p: p['$ref'].split('/').pop(), prop['oneOf']))
|
||||
|
||||
f.write(" <tr><td>{}</td>\
|
||||
<td>{}</td> \
|
||||
<td>{}</td> \
|
||||
<td>{}</td> \
|
||||
</tr>\n".format(
|
||||
name,
|
||||
mandatory,
|
||||
field_type,
|
||||
prop.get("description", "")
|
||||
))
|
||||
|
||||
def _write_json_schema(self, f, schema):
|
||||
# TODO: rewrite this using RST for portability
|
||||
f.write(".. raw:: html\n\n <table>\n")
|
||||
f.write(" <tr> \
|
||||
<th>Name</th> \
|
||||
<th>Mandatory</th> \
|
||||
<th>Type</th> \
|
||||
<th>Description</th> \
|
||||
</tr>\n")
|
||||
self._write_json_schema_object(f, schema, schema)
|
||||
f.write(" </table>\n\n")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
Documentation(Route).write()
|
||||
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
import jsonschema
|
||||
import aiohttp.web
|
||||
|
||||
|
||||
class Response(aiohttp.web.Response):
|
||||
|
||||
def __init__(self, route=None, output_schema=None, headers={}, **kwargs):
|
||||
|
||||
self._route = route
|
||||
self._output_schema = output_schema
|
||||
headers['X-Route'] = self._route
|
||||
super().__init__(headers=headers, **kwargs)
|
||||
|
||||
def json(self, answer):
|
||||
"""Pass a Python object and return a JSON as answer"""
|
||||
|
||||
self.content_type = "application/json"
|
||||
if self._output_schema is not None:
|
||||
try:
|
||||
jsonschema.validate(answer, self._output_schema)
|
||||
except jsonschema.ValidationError as e:
|
||||
raise aiohttp.web.HTTPBadRequest(text="{}".format(e))
|
||||
self.body = json.dumps(answer, indent=4, sort_keys=True).encode('utf-8')
|
||||
@@ -0,0 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
import jsonschema
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
from .response import Response
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def parse_request(request, input_schema):
|
||||
"""Parse body of request and raise HTTP errors in case of problems"""
|
||||
content_length = request.content_length
|
||||
if content_length is not None and content_length > 0:
|
||||
body = yield from request.read()
|
||||
try:
|
||||
request.json = json.loads(body.decode('utf-8'))
|
||||
except ValueError as e:
|
||||
raise aiohttp.web.HTTPBadRequest(text="Invalid JSON {}".format(e))
|
||||
try:
|
||||
jsonschema.validate(request.json, input_schema)
|
||||
except jsonschema.ValidationError as e:
|
||||
raise aiohttp.web.HTTPBadRequest(text="Request is not {} '{}' in schema: {}".format(
|
||||
e.validator,
|
||||
e.validator_value,
|
||||
json.dumps(e.schema)))
|
||||
return request
|
||||
|
||||
|
||||
class Route(object):
|
||||
""" Decorator adding:
|
||||
* json schema verification
|
||||
* routing inside handlers
|
||||
* documentation information about endpoints
|
||||
"""
|
||||
|
||||
_routes = []
|
||||
_documentation = {}
|
||||
|
||||
@classmethod
|
||||
def get(cls, path, *args, **kw):
|
||||
return cls._route('GET', path, *args, **kw)
|
||||
|
||||
@classmethod
|
||||
def post(cls, path, *args, **kw):
|
||||
return cls._route('POST', path, *args, **kw)
|
||||
|
||||
@classmethod
|
||||
def put(cls, path, *args, **kw):
|
||||
return cls._route('PUT', path, *args, **kw)
|
||||
|
||||
@classmethod
|
||||
def _route(cls, method, path, *args, **kw):
|
||||
# This block is executed only the first time
|
||||
output_schema = kw.get("output", {})
|
||||
input_schema = kw.get("input", {})
|
||||
cls._path = path
|
||||
cls._documentation.setdefault(cls._path, {"methods": []})
|
||||
|
||||
def register(func):
|
||||
route = cls._path
|
||||
|
||||
cls._documentation[route]["methods"].append({
|
||||
"method": method,
|
||||
"status_codes": kw.get("status_codes", {200: "OK"}),
|
||||
"parameters": kw.get("parameters", {}),
|
||||
"output_schema": output_schema,
|
||||
"input_schema": input_schema,
|
||||
"description": kw.get("description", "")
|
||||
})
|
||||
func = asyncio.coroutine(func)
|
||||
|
||||
@asyncio.coroutine
|
||||
def control_schema(request):
|
||||
# This block is executed at each method call
|
||||
try:
|
||||
request = yield from parse_request(request, input_schema)
|
||||
response = Response(route=route, output_schema=output_schema)
|
||||
yield from func(request, response)
|
||||
except aiohttp.web.HTTPException as e:
|
||||
response = Response(route=route)
|
||||
response.set_status(e.status)
|
||||
response.json({"message": e.text, "status": e.status})
|
||||
return response
|
||||
|
||||
cls._routes.append((method, cls._path, control_schema))
|
||||
|
||||
return control_schema
|
||||
return register
|
||||
|
||||
@classmethod
|
||||
def get_routes(cls):
|
||||
return cls._routes
|
||||
|
||||
@classmethod
|
||||
def get_documentation(cls):
|
||||
return cls._documentation
|
||||
Reference in new issue
Block a user