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)