Chameleon self boot-strapping API
diff --git a/grpc_client/grpc_client.py b/grpc_client/grpc_client.py
index 2ce8d7c..5f373a7 100644
--- a/grpc_client/grpc_client.py
+++ b/grpc_client/grpc_client.py
@@ -21,25 +21,27 @@
semantics are derived from the recovered schema.
"""
import os
-
-import grpc
import sys
-from consul import Consul
from random import randint
-from structlog import get_logger
from zlib import decompress
-from chameleon.protos.schema_pb2 import NullMessage, Schema, SchemaServiceStub
+import grpc
+from consul import Consul
+from structlog import get_logger
+
+from chameleon.protos.schema_pb2 import NullMessage, SchemaServiceStub
log = get_logger()
class GrpcClient(object):
- def __init__(self, consul_endpoint, endpoint='localhost:50055'):
+ def __init__(self, consul_endpoint, work_dir, endpoint='localhost:50055'):
self.consul_endpoint = consul_endpoint
self.endpoint = endpoint
- self.work_dir = '/tmp/chameleon'
+ self.work_dir = work_dir
+ self.plugin_dir = os.path.abspath(os.path.join(
+ os.path.dirname(__file__), '../protoc_plugins'))
self.channel = None
self.schema = None
@@ -134,13 +136,22 @@
cmd = (
'cd %s && '
+ 'env PATH=%s '
'python -m grpc.tools.protoc '
'-I. '
'-I%s '
'--python_out=. '
'--grpc_python_out=. '
- '%s' % (self.work_dir, google_api_dir, fname)
+ '--plugin=protoc-gen-gw=%s/gw_gen.py '
+ '--gw_out=. '
+ '%s' % (
+ self.work_dir,
+ ':'.join([os.environ['PATH'], self.plugin_dir]),
+ google_api_dir,
+ self.plugin_dir,
+ fname)
)
+ log.debug('executing', cmd=cmd)
os.system(cmd)
# test-load each _pb2 file to see all is right
@@ -152,3 +163,7 @@
modname = fname[:-len('.py')]
log.debug('test-import', modname=modname)
_ = __import__(modname)
+
+ def invoke(self, stub, method_name, request):
+ response = getattr(stub(self.channel), method_name)(request)
+ return response
diff --git a/main.py b/main.py
index e75de77..470dc84 100755
--- a/main.py
+++ b/main.py
@@ -20,7 +20,7 @@
import argparse
import os
import sys
-import time
+
import yaml
from twisted.internet.defer import inlineCallbacks
@@ -32,6 +32,7 @@
from chameleon.nethelpers import get_my_primary_local_ipv4
from chameleon.dockerhelpers import get_my_containers_name
from chameleon.grpc_client.grpc_client import GrpcClient
+from chameleon.web_server.web_server import WebServer
defs = dict(
@@ -45,6 +46,7 @@
internal_host_address=os.environ.get('INTERNAL_HOST_ADDRESS',
get_my_primary_local_ipv4()),
rest_port=os.environ.get('REST_PORT', 8881),
+ work_dir=os.environ.get('WORK_DIR', '/tmp/chameleon')
)
@@ -140,6 +142,14 @@
action='count',
help=_help)
+ _help = ('work dir to compile and assemble generated files (default=%s)'
+ % defs['work_dir'])
+ parser.add_argument('-w', '--work-dir',
+ dest='work_dir',
+ action='store',
+ default=defs['work_dir'],
+ help=_help)
+
_help = ('use docker container name as Chameleon instance id'
' (overrides -i/--instance-id option)')
parser.add_argument('--instance-id-is-container-name',
@@ -207,14 +217,14 @@
def start(self):
self.start_reactor() # will not return except Keyboard interrupt
+ @inlineCallbacks
def startup_components(self):
self.log.info('starting-internal-components')
-
- self.grpc_client = \
- GrpcClient(self.args.consul, self.args.grpc_endpoint).run()
-
- # TODO init server
-
+ args = self.args
+ self.grpc_client = yield \
+ GrpcClient(args.consul, args.work_dir, args.grpc_endpoint).run()
+ self.web_server = yield \
+ WebServer(args.rest_port, args.work_dir, self.grpc_client).run()
self.log.info('started-internal-services')
@inlineCallbacks
diff --git a/protoc_plugins/__init__.py b/protoc_plugins/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/protoc_plugins/__init__.py
diff --git a/protoc_plugins/gw_gen.py b/protoc_plugins/gw_gen.py
new file mode 100755
index 0000000..aa2a3ce
--- /dev/null
+++ b/protoc_plugins/gw_gen.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import os
+import sys
+
+from google.protobuf.compiler import plugin_pb2 as plugin
+from google.protobuf.descriptor import FieldDescriptor
+from google.protobuf.descriptor_pb2 import ServiceDescriptorProto, MethodOptions
+from google.protobuf.message import Message
+from jinja2 import Template
+from simplejson import dumps
+
+# without this import, http method annotations would not be recognized:
+from google.api import annotations_pb2 as _, http_pb2
+
+
+template = Template("""
+# Generated file; please do not edit
+
+from simplejson import dumps, load
+from structlog import get_logger
+from protobuf_to_dict import protobuf_to_dict, dict_to_protobuf
+
+{% set package = file_name.replace('.proto', '') %}
+import {{ package + '_pb2' }} as {{ package }}
+
+log = get_logger()
+
+def add_routes(app, grpc_client):
+
+ {% for method in methods %}
+ {% set method_name = method['service'] + '_' + method['method'] %}
+ {% set path = method['path'].replace('{', '<string:').replace('}', '>') %}
+ @app.route('{{ path }}', methods=['{{ method['verb'].upper() }}'])
+ def {{ method_name }}(server, request, **kw):
+ log.debug('{{ method_name }}', request=request, server=server, **kw)
+ {% if method['body'] == '*' %}
+ data = load(request.content)
+ data.update(kw)
+ {% elif method['body'] == '' %}
+ data = kw
+ {% else %}
+ riase NotImplementedError('cannot handle specific body field list')
+ {% endif %}
+ req = dict_to_protobuf({{ method['input_type'] }}, data)
+ res = grpc_client.invoke(
+ {{ '.'.join([package, method['service']]) }}Stub,
+ '{{ method['method'] }}', req)
+ out_data = protobuf_to_dict(res, use_enum_labels=True)
+ request.setHeader('Content-Type', 'application/json')
+ log.debug('{{ method_name }}', **out_data)
+ return dumps(out_data)
+
+ {% endfor %}
+
+""", trim_blocks=True, lstrip_blocks=True)
+
+
+def traverse_methods(proto_file):
+
+ package = proto_file.name
+ for service in proto_file.service:
+ assert isinstance(service, ServiceDescriptorProto)
+
+ for method in service.method:
+ options = method.options
+ assert isinstance(options, MethodOptions)
+ for fd, http in options.ListFields():
+ if fd.full_name == 'google.api.http':
+ assert fd.name == 'http'
+ assert isinstance(http, http_pb2.HttpRule)
+
+ input_type = method.input_type
+ if input_type.startswith('.'):
+ input_type = input_type[1:]
+
+ output_type = method.output_type
+ if output_type.startswith('.'):
+ output_type = output_type[1:]
+
+ if http.delete:
+ verb = 'delete'
+ path = http.delete
+ elif http.get:
+ verb = 'get'
+ path = http.get
+ elif http.patch:
+ verb = 'patch'
+ path = http.patch
+ elif http.post:
+ verb = 'post'
+ path = http.post
+ elif http.put:
+ verb = 'put'
+ path = http.put
+ else:
+ raise AttributeError('No valid verb in method %s' %
+ method.name)
+
+ body = http.body
+
+ data = {
+ 'package': package,
+ 'filename': proto_file.name,
+ 'service': service.name,
+ 'method': method.name,
+ 'input_type': input_type,
+ 'output_type': output_type,
+ 'path': path,
+ 'verb': verb,
+ 'body': body
+ }
+
+ yield data
+
+
+def generate_gw_code(file_name, methods):
+ return template.render(file_name=file_name, methods=methods)
+
+
+def generate_code(request, response):
+
+ assert isinstance(request, plugin.CodeGeneratorRequest)
+ for proto_file in request.proto_file:
+ output = []
+
+ for data in traverse_methods(proto_file):
+ output.append(data)
+
+ # as a nice side-effect, generate a json file capturing the essence
+ # of the RPC method entries
+ f = response.file.add()
+ f.name = proto_file.name + '.json'
+ f.content = dumps(output, indent=4)
+
+ # generate the real Python code file
+ f = response.file.add()
+ assert proto_file.name.endswith('.proto')
+ f.name = proto_file.name.replace('.proto', '_gw.py')
+ f.content = generate_gw_code(proto_file.name, output)
+
+
+if __name__ == '__main__':
+
+ if len(sys.argv) >= 2:
+ # read input from file, to allow troubleshooting
+ with open(sys.argv[1], 'r') as f:
+ data = f.read()
+ else:
+ # read input from stdin
+ data = sys.stdin.read()
+
+ # parse request
+ request = plugin.CodeGeneratorRequest()
+ request.ParseFromString(data)
+
+ # create response object
+ response = plugin.CodeGeneratorResponse()
+
+ # generate the output and the response
+ generate_code(request, response)
+
+ # serialize the response
+ output = response.SerializeToString()
+
+ # write response to stdout
+ sys.stdout.write(output)
diff --git a/protos/schema.desc b/protos/schema.desc
new file mode 100644
index 0000000..3cb025f
--- /dev/null
+++ b/protos/schema.desc
Binary files differ
diff --git a/protos/schema_pb2.py b/protos/schema_pb2.py
new file mode 100644
index 0000000..eab43f5
--- /dev/null
+++ b/protos/schema_pb2.py
@@ -0,0 +1,300 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: schema.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='schema.proto',
+ package='schema',
+ syntax='proto3',
+ serialized_pb=_b('\n\x0cschema.proto\x12\x06schema\"\xcd\x01\n\x06Schema\x12*\n\x06protos\x18\x01 \x03(\x0b\x32\x1a.schema.Schema.ProtosEntry\x12\x34\n\x0b\x64\x65scriptors\x18\x02 \x03(\x0b\x32\x1f.schema.Schema.DescriptorsEntry\x1a-\n\x0bProtosEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x32\n\x10\x44\x65scriptorsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\"\r\n\x0bNullMessage2C\n\rSchemaService\x12\x32\n\tGetSchema\x12\x13.schema.NullMessage\x1a\x0e.schema.Schema\"\x00\x62\x06proto3')
+)
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+
+
+
+_SCHEMA_PROTOSENTRY = _descriptor.Descriptor(
+ name='ProtosEntry',
+ full_name='schema.Schema.ProtosEntry',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='key', full_name='schema.Schema.ProtosEntry.key', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='value', full_name='schema.Schema.ProtosEntry.value', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=133,
+ serialized_end=178,
+)
+
+_SCHEMA_DESCRIPTORSENTRY = _descriptor.Descriptor(
+ name='DescriptorsEntry',
+ full_name='schema.Schema.DescriptorsEntry',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='key', full_name='schema.Schema.DescriptorsEntry.key', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='value', full_name='schema.Schema.DescriptorsEntry.value', index=1,
+ number=2, type=12, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b(""),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=180,
+ serialized_end=230,
+)
+
+_SCHEMA = _descriptor.Descriptor(
+ name='Schema',
+ full_name='schema.Schema',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='protos', full_name='schema.Schema.protos', index=0,
+ number=1, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='descriptors', full_name='schema.Schema.descriptors', index=1,
+ number=2, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[_SCHEMA_PROTOSENTRY, _SCHEMA_DESCRIPTORSENTRY, ],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=25,
+ serialized_end=230,
+)
+
+
+_NULLMESSAGE = _descriptor.Descriptor(
+ name='NullMessage',
+ full_name='schema.NullMessage',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=232,
+ serialized_end=245,
+)
+
+_SCHEMA_PROTOSENTRY.containing_type = _SCHEMA
+_SCHEMA_DESCRIPTORSENTRY.containing_type = _SCHEMA
+_SCHEMA.fields_by_name['protos'].message_type = _SCHEMA_PROTOSENTRY
+_SCHEMA.fields_by_name['descriptors'].message_type = _SCHEMA_DESCRIPTORSENTRY
+DESCRIPTOR.message_types_by_name['Schema'] = _SCHEMA
+DESCRIPTOR.message_types_by_name['NullMessage'] = _NULLMESSAGE
+
+Schema = _reflection.GeneratedProtocolMessageType('Schema', (_message.Message,), dict(
+
+ ProtosEntry = _reflection.GeneratedProtocolMessageType('ProtosEntry', (_message.Message,), dict(
+ DESCRIPTOR = _SCHEMA_PROTOSENTRY,
+ __module__ = 'schema_pb2'
+ # @@protoc_insertion_point(class_scope:schema.Schema.ProtosEntry)
+ ))
+ ,
+
+ DescriptorsEntry = _reflection.GeneratedProtocolMessageType('DescriptorsEntry', (_message.Message,), dict(
+ DESCRIPTOR = _SCHEMA_DESCRIPTORSENTRY,
+ __module__ = 'schema_pb2'
+ # @@protoc_insertion_point(class_scope:schema.Schema.DescriptorsEntry)
+ ))
+ ,
+ DESCRIPTOR = _SCHEMA,
+ __module__ = 'schema_pb2'
+ # @@protoc_insertion_point(class_scope:schema.Schema)
+ ))
+_sym_db.RegisterMessage(Schema)
+_sym_db.RegisterMessage(Schema.ProtosEntry)
+_sym_db.RegisterMessage(Schema.DescriptorsEntry)
+
+NullMessage = _reflection.GeneratedProtocolMessageType('NullMessage', (_message.Message,), dict(
+ DESCRIPTOR = _NULLMESSAGE,
+ __module__ = 'schema_pb2'
+ # @@protoc_insertion_point(class_scope:schema.NullMessage)
+ ))
+_sym_db.RegisterMessage(NullMessage)
+
+
+_SCHEMA_PROTOSENTRY.has_options = True
+_SCHEMA_PROTOSENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+_SCHEMA_DESCRIPTORSENTRY.has_options = True
+_SCHEMA_DESCRIPTORSENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+import grpc
+from grpc.beta import implementations as beta_implementations
+from grpc.beta import interfaces as beta_interfaces
+from grpc.framework.common import cardinality
+from grpc.framework.interfaces.face import utilities as face_utilities
+
+
+class SchemaServiceStub(object):
+ """Schema services
+ """
+
+ def __init__(self, channel):
+ """Constructor.
+
+ Args:
+ channel: A grpc.Channel.
+ """
+ self.GetSchema = channel.unary_unary(
+ '/schema.SchemaService/GetSchema',
+ request_serializer=NullMessage.SerializeToString,
+ response_deserializer=Schema.FromString,
+ )
+
+
+class SchemaServiceServicer(object):
+ """Schema services
+ """
+
+ def GetSchema(self, request, context):
+ """Return active grpc schemas
+ """
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+
+def add_SchemaServiceServicer_to_server(servicer, server):
+ rpc_method_handlers = {
+ 'GetSchema': grpc.unary_unary_rpc_method_handler(
+ servicer.GetSchema,
+ request_deserializer=NullMessage.FromString,
+ response_serializer=Schema.SerializeToString,
+ ),
+ }
+ generic_handler = grpc.method_handlers_generic_handler(
+ 'schema.SchemaService', rpc_method_handlers)
+ server.add_generic_rpc_handlers((generic_handler,))
+
+
+class BetaSchemaServiceServicer(object):
+ """Schema services
+ """
+ def GetSchema(self, request, context):
+ """Return active grpc schemas
+ """
+ context.code(beta_interfaces.StatusCode.UNIMPLEMENTED)
+
+
+class BetaSchemaServiceStub(object):
+ """Schema services
+ """
+ def GetSchema(self, request, timeout, metadata=None, with_call=False, protocol_options=None):
+ """Return active grpc schemas
+ """
+ raise NotImplementedError()
+ GetSchema.future = None
+
+
+def beta_create_SchemaService_server(servicer, pool=None, pool_size=None, default_timeout=None, maximum_timeout=None):
+ request_deserializers = {
+ ('schema.SchemaService', 'GetSchema'): NullMessage.FromString,
+ }
+ response_serializers = {
+ ('schema.SchemaService', 'GetSchema'): Schema.SerializeToString,
+ }
+ method_implementations = {
+ ('schema.SchemaService', 'GetSchema'): face_utilities.unary_unary_inline(servicer.GetSchema),
+ }
+ server_options = beta_implementations.server_options(request_deserializers=request_deserializers, response_serializers=response_serializers, thread_pool=pool, thread_pool_size=pool_size, default_timeout=default_timeout, maximum_timeout=maximum_timeout)
+ return beta_implementations.server(method_implementations, options=server_options)
+
+
+def beta_create_SchemaService_stub(channel, host=None, metadata_transformer=None, pool=None, pool_size=None):
+ request_serializers = {
+ ('schema.SchemaService', 'GetSchema'): NullMessage.SerializeToString,
+ }
+ response_deserializers = {
+ ('schema.SchemaService', 'GetSchema'): Schema.FromString,
+ }
+ cardinalities = {
+ 'GetSchema': cardinality.Cardinality.UNARY_UNARY,
+ }
+ stub_options = beta_implementations.stub_options(host=host, metadata_transformer=metadata_transformer, request_serializers=request_serializers, response_deserializers=response_deserializers, thread_pool=pool, thread_pool_size=pool_size)
+ return beta_implementations.dynamic_stub(channel, 'schema.SchemaService', cardinalities, options=stub_options)
+# @@protoc_insertion_point(module_scope)
diff --git a/protos/third_party/google/api/annotations.desc b/protos/third_party/google/api/annotations.desc
new file mode 100644
index 0000000..d159f06
--- /dev/null
+++ b/protos/third_party/google/api/annotations.desc
Binary files differ
diff --git a/protos/third_party/google/api/annotations.proto b/protos/third_party/google/api/annotations.proto
new file mode 100644
index 0000000..cbd18b8
--- /dev/null
+++ b/protos/third_party/google/api/annotations.proto
@@ -0,0 +1,29 @@
+// Copyright (c) 2015, Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.api;
+
+import "google/api/http.proto";
+import "google/protobuf/descriptor.proto";
+
+option java_multiple_files = true;
+option java_outer_classname = "AnnotationsProto";
+option java_package = "com.google.api";
+
+extend google.protobuf.MethodOptions {
+ // See `HttpRule`.
+ HttpRule http = 72295728;
+}
diff --git a/protos/third_party/google/api/http.desc b/protos/third_party/google/api/http.desc
new file mode 100644
index 0000000..7b2513a
--- /dev/null
+++ b/protos/third_party/google/api/http.desc
Binary files differ
diff --git a/protos/third_party/google/api/http.proto b/protos/third_party/google/api/http.proto
new file mode 100644
index 0000000..ce07aa1
--- /dev/null
+++ b/protos/third_party/google/api/http.proto
@@ -0,0 +1,127 @@
+// Copyright (c) 2015, Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package google.api;
+
+option java_multiple_files = true;
+option java_outer_classname = "HttpProto";
+option java_package = "com.google.api";
+
+
+// `HttpRule` defines the mapping of an RPC method to one or more HTTP REST API
+// methods. The mapping determines what portions of the request message are
+// populated from the path, query parameters, or body of the HTTP request. The
+// mapping is typically specified as an `google.api.http` annotation, see
+// "google/api/annotations.proto" for details.
+//
+// The mapping consists of a mandatory field specifying a path template and an
+// optional `body` field specifying what data is represented in the HTTP request
+// body. The field name for the path indicates the HTTP method. Example:
+//
+// ```
+// package google.storage.v2;
+//
+// import "google/api/annotations.proto";
+//
+// service Storage {
+// rpc CreateObject(CreateObjectRequest) returns (Object) {
+// option (google.api.http) {
+// post: "/v2/{bucket_name=buckets/*}/objects"
+// body: "object"
+// };
+// };
+// }
+// ```
+//
+// Here `bucket_name` and `object` bind to fields of the request message
+// `CreateObjectRequest`.
+//
+// The rules for mapping HTTP path, query parameters, and body fields
+// to the request message are as follows:
+//
+// 1. The `body` field specifies either `*` or a field path, or is
+// omitted. If omitted, it assumes there is no HTTP body.
+// 2. Leaf fields (recursive expansion of nested messages in the
+// request) can be classified into three types:
+// (a) Matched in the URL template.
+// (b) Covered by body (if body is `*`, everything except (a) fields;
+// else everything under the body field)
+// (c) All other fields.
+// 3. URL query parameters found in the HTTP request are mapped to (c) fields.
+// 4. Any body sent with an HTTP request can contain only (b) fields.
+//
+// The syntax of the path template is as follows:
+//
+// Template = "/" Segments [ Verb ] ;
+// Segments = Segment { "/" Segment } ;
+// Segment = "*" | "**" | LITERAL | Variable ;
+// Variable = "{" FieldPath [ "=" Segments ] "}" ;
+// FieldPath = IDENT { "." IDENT } ;
+// Verb = ":" LITERAL ;
+//
+// `*` matches a single path component, `**` zero or more path components, and
+// `LITERAL` a constant. A `Variable` can match an entire path as specified
+// again by a template; this nested template must not contain further variables.
+// If no template is given with a variable, it matches a single path component.
+// The notation `{var}` is henceforth equivalent to `{var=*}`.
+//
+// Use CustomHttpPattern to specify any HTTP method that is not included in the
+// pattern field, such as HEAD, or "*" to leave the HTTP method unspecified for
+// a given URL path rule. The wild-card rule is useful for services that provide
+// content to Web (HTML) clients.
+message HttpRule {
+
+ // Determines the URL pattern is matched by this rules. This pattern can be
+ // used with any of the {get|put|post|delete|patch} methods. A custom method
+ // can be defined using the 'custom' field.
+ oneof pattern {
+ // Used for listing and getting information about resources.
+ string get = 2;
+
+ // Used for updating a resource.
+ string put = 3;
+
+ // Used for creating a resource.
+ string post = 4;
+
+ // Used for deleting a resource.
+ string delete = 5;
+
+ // Used for updating a resource.
+ string patch = 6;
+
+ // Custom pattern is used for defining custom verbs.
+ CustomHttpPattern custom = 8;
+ }
+
+ // The name of the request field whose value is mapped to the HTTP body, or
+ // `*` for mapping all fields not captured by the path pattern to the HTTP
+ // body.
+ string body = 7;
+
+ // Additional HTTP bindings for the selector. Nested bindings must not
+ // specify a selector and must not contain additional bindings.
+ repeated HttpRule additional_bindings = 11;
+}
+
+// A custom pattern is used for defining custom HTTP verb.
+message CustomHttpPattern {
+ // The name of this custom HTTP verb.
+ string kind = 1;
+
+ // The path matched by this custom verb.
+ string path = 2;
+}
diff --git a/swagger_ui/index.html b/swagger_ui/index.html
index adcda63..fd2e909 100644
--- a/swagger_ui/index.html
+++ b/swagger_ui/index.html
@@ -37,7 +37,7 @@
if (url && url.length > 1) {
url = decodeURIComponent(url[1]);
} else {
- url = "http://petstore.swagger.io/v2/swagger.json";
+ url = "/v1/swagger.json";
}
hljs.configure({
diff --git a/web_server/__init__.py b/web_server/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/web_server/__init__.py
diff --git a/web_server/web_server.py b/web_server/web_server.py
new file mode 100644
index 0000000..4452ccd
--- /dev/null
+++ b/web_server/web_server.py
@@ -0,0 +1,159 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import os
+
+from klein import Klein
+from simplejson import dumps, load
+from structlog import get_logger
+from twisted.internet import reactor, endpoints
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.tcp import Port
+from twisted.web.server import Site
+from twisted.web.static import File
+
+log = get_logger()
+
+'''
+## To be automated as part of the template
+from voltha.protos.voltha_pb2 import *
+from protobuf_to_dict import protobuf_to_dict, dict_to_protobuf
+
+def add_routes(app, grpc_client):
+
+ @app.route('/health', methods=['GET'])
+ def get_health(server, request):
+ log.debug('get-health-req', request=request, server=server)
+ req = NullMessage()
+ res = grpc_client.invoke(
+ HealthServiceStub, 'GetHealthStatus', req)
+ data = protobuf_to_dict(res, use_enum_labels=True)
+ request.setHeader('Content-Type', 'application/json')
+ log.debug('get-health-res', **data)
+ return dumps(data)
+
+ @app.route('/addresses', methods=['GET'])
+ def list_addresses(server, request):
+ log.debug('list-addresses-req', request=request, server=server)
+ req = NullMessage()
+ res = grpc_client.invoke(
+ ExampleServiceStub, 'ListAddresses', req)
+ data = protobuf_to_dict(res, use_enum_labels=True)
+ request.setHeader('Content-Type', 'application/json')
+ log.debug('list-addresses-res', **data)
+ return dumps(data)
+
+ @app.route('/addresses/<string:id>', methods=['GET'])
+ def get_address(server, request, id):
+ log.debug('get-address-req', request=request, server=server, id=id)
+ req = ID(id=id)
+ res = grpc_client.invoke(
+ ExampleServiceStub, 'GetAddress', req)
+ data = protobuf_to_dict(res, use_enum_labels=True)
+ request.setHeader('Content-Type', 'application/json')
+ log.debug('get-address-res', **data)
+ return dumps(data)
+
+ @app.route('/addresses/<string:id>', methods=['DELETE'])
+ def delete_address(server, request, id):
+ log.debug('delete-address-req', request=request, server=server, id=id)
+ req = ID(id=id)
+ res = grpc_client.invoke(
+ ExampleServiceStub, 'DeleteAddress', req)
+ data = protobuf_to_dict(res, use_enum_labels=True)
+ request.setHeader('Content-Type', 'application/json')
+ log.debug('delete-address-res', **data)
+ return dumps(data)
+
+ @app.route('/addresses', methods=['PATCH'])
+ def update_address(server, request):
+ log.debug('update-address-req', request=request, server=server)
+ data = load(request.content)
+ req = dict_to_protobuf(Address, data)
+ res = grpc_client.invoke(
+ ExampleServiceStub, 'UpdateAddress', req)
+ data = protobuf_to_dict(res, use_enum_labels=True)
+ request.setHeader('Content-Type', 'application/json')
+ log.debug('update-address-res', **data)
+ return dumps(data)
+
+## end
+'''
+
+class WebServer(object):
+
+ app = Klein()
+
+ def __init__(self, port, work_dir, grpc_client):
+ self.port = port
+ self.site = None
+ self.work_dir = work_dir
+ self.grpc_client = grpc_client
+
+ self.swagger_ui_root_dir = os.path.abspath(
+ os.path.join(os.path.dirname(__file__), '../swagger_ui'))
+
+ self.tcp_port = None
+
+ @inlineCallbacks
+ def run(self):
+ yield self._open_endpoint()
+ yield self._load_generated_routes()
+ returnValue(self)
+
+ def _load_generated_routes(self):
+ for fname in os.listdir(self.work_dir):
+ if fname.endswith('_gw.py'):
+ module_name = fname.replace('.py', '')
+ print 'module_name', module_name
+ m = __import__(module_name)
+ print dir(m)
+ assert hasattr(m, 'add_routes')
+ m.add_routes(self.app, self.grpc_client)
+
+ @inlineCallbacks
+ def _open_endpoint(self):
+ endpoint = endpoints.TCP4ServerEndpoint(reactor, self.port)
+ self.site = Site(self.app.resource())
+ self.tcp_port = yield endpoint.listen(self.site)
+ log.info('web-server-started', port=self.port)
+ self.endpoint = endpoint
+
+ @inlineCallbacks
+ def shutdown(self):
+ if self.tcp_porte is not None:
+ assert isinstance(self.tcp_port, Port)
+ yield self.tcp_port.socket.close()
+
+ # static swagger_ui website as landing page (for now)
+
+ @app.route('/', branch=True)
+ def static(self, request):
+ try:
+ log.debug(request=request)
+ return File(self.swagger_ui_root_dir)
+ except Exception, e:
+ log.exception('file-not-found', request=request)
+
+ # static swagger.json file to serve the schema
+
+ @app.route('/v1/swagger.json')
+ def swagger_json(self, request):
+ try:
+ return File(os.path.join(self.work_dir, 'swagger.json'))
+ except Exception, e:
+ log.exception('file-not-found', request=request)