Init commit for standalone enodebd

Change-Id: I88eeef5135dd7ba8551ddd9fb6a0695f5325337b
diff --git a/tr069/rpc_methods.py b/tr069/rpc_methods.py
new file mode 100644
index 0000000..aff0f30
--- /dev/null
+++ b/tr069/rpc_methods.py
@@ -0,0 +1,371 @@
+"""
+Copyright 2020 The Magma Authors.
+
+This source code is licensed under the BSD-style license found in the
+LICENSE file in the root directory of this source tree.
+
+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.
+"""
+
+from logger import EnodebdLogger as logger
+from state_machines.enb_acs_manager import StateMachineManager
+from spyne.decorator import rpc
+from spyne.model.complex import ComplexModelBase
+from spyne.server.wsgi import WsgiMethodContext
+from spyne.service import ServiceBase
+
+from . import models
+
+# Allow methods without 'self' as first input. Required by spyne
+# pylint: disable=no-self-argument
+
+# RPC methods supported by ACS
+RPC_METHODS = ['Inform', 'GetRPCMethods', 'TransferComplete']
+RPC_RESPONSES = [method + 'Response' for method in RPC_METHODS]
+# RPC methods supported by CPE
+CPE_RPC_METHODS = [
+    'SetParameterValues',
+    'GetParameterValues',
+    'GetParameterNames',
+    'SetParameterAttributes',
+    'GetParameterAttributes',
+    'AddObject',
+    'DeleteObject',
+    'Download',
+    'Reboot',
+]
+CPE_RPC_RESPONSES = [method + 'Response' for method in CPE_RPC_METHODS]
+# ACS RPC methods that are not explicitly described by the spec (hence shouldn't
+# be advertised by GetRPCMethods). Note: No responses for these
+PSEUDO_RPC_METHODS = ['Fault']
+# Top-level CWMP header elements. Namespaces should be preserved on these (since
+# they are not within other CWMP elements)
+TOP_LEVEL_HEADER_ELEMENTS = ['ID', 'HoldRequests']
+
+
+def fill_response_header(ctx):
+    """ Echo message ID from input header to output header, when responding to
+        CPE->ACS RPC calls """
+    ctx.out_header = models.ID(mustUnderstand='1')
+    ctx.out_header.Data = ctx.in_header.Data
+
+
+class AutoConfigServer(ServiceBase):
+    """ TR-069 ACS implementation. The TR-069/CWMP RPC messages are defined, as
+        per cwmp-1-0.xsd schema definition, in the RPC decorators below. These
+        RPC methods are intended to be called by TR-069-compliant customer
+        premesis equipment (CPE), over the SOAP/HTTP interface defined by
+        TR-069.
+
+        Per spyne documentation, this class is never instantiated, so all RPC
+        functions are implicitly staticmethods. Hence use static class variables
+        to hold state.
+        This also means that only a single thread can be used (since there are
+        no locks).
+        Note that staticmethod decorator can't be used in conjunction with rpc
+        decorator.
+    """
+    __out_header__ = models.ID
+    __in_header__ = models.ID
+    _acs_to_cpe_queue = None
+    _cpe_to_acs_queue = None
+
+    """ Set maxEnvelopes to 1, as per TR-069 spec """
+    _max_envelopes = 1
+
+    @classmethod
+    def set_state_machine_manager(
+        cls,
+        state_machine_manager: StateMachineManager,
+    ) -> None:
+        cls.state_machine_manager = state_machine_manager
+
+    @classmethod
+    def _handle_tr069_message(
+        cls,
+        ctx: WsgiMethodContext,
+        message: ComplexModelBase,
+    ) -> ComplexModelBase:
+        # Log incoming msg
+        if hasattr(message, 'as_dict'):
+            logger.debug('Handling TR069 message: %s', str(type(message)))
+        else:
+            logger.debug('Handling TR069 message.')
+
+        req = cls._get_tr069_response_from_sm(ctx, message)
+
+        # Log outgoing msg
+        if hasattr(req, 'as_dict'):
+            logger.debug('Sending TR069 message: %s', str(req.as_dict()))
+        else:
+            logger.debug('Sending TR069 message.')
+
+        # Set header
+        ctx.out_header = models.ID(mustUnderstand='1')
+        ctx.out_header.Data = 'null'
+
+        # Set return message name
+        if isinstance(req, models.DummyInput):
+            # Generate 'empty' request to CPE using empty message name
+            ctx.descriptor.out_message.Attributes.sub_name = 'EmptyHttp'
+            return models.AcsToCpeRequests()
+        ctx.descriptor.out_message.Attributes.sub_name = req.__class__.__name__
+        return cls._generate_acs_to_cpe_request_copy(req)
+
+    @classmethod
+    def _get_tr069_response_from_sm(
+            cls,
+            ctx: WsgiMethodContext,
+            message: ComplexModelBase,
+    ) -> ComplexModelBase:
+        # We want to blanket-catch all exceptions because a problem with one
+        # tr-069 session shouldn't tank the service for all other enodeB's
+        # being managed
+        try:
+            return cls.state_machine_manager.handle_tr069_message(ctx, message)
+        except Exception:   # pylint: disable=broad-except
+            logger.exception(
+                'Unexpected exception from state machine manager, returning '
+                'empty request',
+            )
+            return models.DummyInput()
+
+    @staticmethod
+    def _generate_acs_to_cpe_request_copy(request):
+        """ Create an AcsToCpeRequests instance with all the appropriate
+            members set from the input request. AcsToCpeRequests is a union of
+            all request messages, so field names match.
+        """
+        request_out = models.AcsToCpeRequests()
+        for parameter in request.get_flat_type_info(request.__class__):
+            try:
+                setattr(request_out, parameter, getattr(request, parameter))
+            except AttributeError:
+                # Allow un-set parameters. If CPE can't handle this, it will
+                # respond with an error message
+                pass
+        return request_out
+
+    # CPE->ACS RPC calls
+
+    @rpc(
+        models.GetRPCMethods,
+        _returns=models.GetRPCMethodsResponse,
+        _body_style="bare",
+        _operation_name="GetRPCMethods",
+        _out_message_name="GetRPCMethodsResponse",
+    )
+    def get_rpc_methods(ctx, request):
+        """ GetRPCMethods RPC call is terminated here. No need to pass to higher
+            layer """
+        fill_response_header(ctx)
+        resp = AutoConfigServer._handle_tr069_message(ctx, request)
+        return resp
+
+    @rpc(
+        models.Inform,
+        _returns=models.InformResponse,
+        _body_style="bare",
+        _operation_name="Inform",
+        _out_message_name="InformResponse",
+    )
+    def inform(ctx, request):
+        """ Inform response generated locally """
+        fill_response_header(ctx)
+        resp = AutoConfigServer._handle_tr069_message(ctx, request)
+        resp.MaxEnvelopes = AutoConfigServer._max_envelopes
+        return resp
+
+    @rpc(
+        models.TransferComplete,
+        _returns=models.TransferCompleteResponse,
+        _body_style="bare",
+        _operation_name="TransferComplete",
+        _out_message_name="TransferCompleteResponse",
+    )
+    def transfer_complete(ctx, request):
+        fill_response_header(ctx)
+        resp = AutoConfigServer._handle_tr069_message(ctx, request)
+        resp.MaxEnvelopes = AutoConfigServer._max_envelopes
+        return resp
+
+    # Spyne does not handle no input or SimpleModel input for 'bare' function
+    # DummyInput is unused
+    # pylint: disable=unused-argument
+    @rpc(
+        models.DummyInput,
+        _returns=models.AcsToCpeRequests,
+        _out_message_name="EmptyHttp",
+        _body_style='bare',
+        _operation_name="EmptyHttp",
+    )
+    def empty_http(ctx, dummy):
+        # Function to handle empty HTTP request
+        return AutoConfigServer._handle_tr069_message(ctx, dummy)
+
+    # CPE->ACS responses to ACS->CPE RPC calls
+
+    @rpc(
+        models.SetParameterValuesResponse,
+        _returns=models.AcsToCpeRequests,
+        _out_message_name="MessageNameToBeReplaced",
+        _body_style='bare',
+        _operation_name="SetParameterValuesResponse",
+    )
+    def set_parameter_values_response(ctx, response):
+        return AutoConfigServer._handle_tr069_message(ctx, response)
+
+    @rpc(
+        models.GetParameterValuesResponse,
+        _returns=models.AcsToCpeRequests,
+        _out_message_name="MessageNameToBeReplaced",
+        _body_style='bare',
+        _operation_name="GetParameterValuesResponse",
+    )
+    def get_parameter_values_response(ctx, response):
+        return AutoConfigServer._handle_tr069_message(ctx, response)
+
+    @rpc(
+        models.GetParameterNamesResponse,
+        _returns=models.AcsToCpeRequests,
+        _out_message_name="MessageNameToBeReplaced",
+        _body_style='bare',
+        _operation_name="GetParameterNamesResponse",
+    )
+    def get_parameter_names_response(ctx, response):
+        return AutoConfigServer._handle_tr069_message(ctx, response)
+
+    @rpc(
+        models.SetParameterAttributesResponse,
+        _returns=models.AcsToCpeRequests,
+        _out_message_name="MessageNameToBeReplaced",
+        _body_style='bare',
+        _operation_name="SetParameterAttributesResponse",
+    )
+    def set_parameter_attributes_response(ctx, response):
+        return AutoConfigServer._handle_tr069_message(ctx, response)
+
+    @rpc(
+        models.GetParameterAttributesResponse,
+        _returns=models.AcsToCpeRequests,
+        _out_message_name="MessageNameToBeReplaced",
+        _body_style='bare',
+        _operation_name="GetParameterAttributesResponse",
+    )
+    def get_parameter_attributes_response(ctx, response):
+        return AutoConfigServer._handle_tr069_message(ctx, response)
+
+    @rpc(
+        models.AddObjectResponse,
+        _returns=models.AcsToCpeRequests,
+        _out_message_name="MessageNameToBeReplaced",
+        _body_style='bare',
+        _operation_name="AddObjectResponse",
+    )
+    def add_object_response(ctx, response):
+        return AutoConfigServer._handle_tr069_message(ctx, response)
+
+    @rpc(
+        models.DeleteObjectResponse,
+        _returns=models.AcsToCpeRequests,
+        _out_message_name="MessageNameToBeReplaced",
+        _body_style='bare',
+        _operation_name="DeleteObjectResponse",
+    )
+    def delete_object_response(ctx, response):
+        return AutoConfigServer._handle_tr069_message(ctx, response)
+
+    @rpc(
+        models.DownloadResponse,
+        _returns=models.AcsToCpeRequests,
+        _out_message_name="MessageNameToBeReplaced",
+        _body_style='bare',
+        _operation_name="DownloadResponse",
+    )
+    def download_response(ctx, response):
+        return AutoConfigServer._handle_tr069_message(ctx, response)
+
+    @rpc(
+        models.RebootResponse,
+        _returns=models.AcsToCpeRequests,
+        _out_message_name="MessageNameToBeReplaced",
+        _body_style='bare',
+        _operation_name="RebootResponse",
+    )
+    def reboot_response(ctx, response):
+        return AutoConfigServer._handle_tr069_message(ctx, response)
+
+    @rpc(
+        models.Fault,
+        _returns=models.AcsToCpeRequests,
+        _out_message_name="MessageNameToBeReplaced",
+        _body_style='bare',
+        _operation_name="Fault",
+    )
+    def fault(ctx, response):
+        return AutoConfigServer._handle_tr069_message(ctx, response)
+
+
+def on_method_return_string(ctx):
+    """
+    By default, spyne adds a namespace to every single XML element.
+    There isn't a way to change this behavior, and the spyne-recommended way
+    to fix this is by doing string manipulation. The TR-069 spec mandates that
+    only the top-level CWMP elements contain namespaces. Hence this
+    function is to remove namespaces from all elements except top-level CWMP
+    elements (e.g. RPC request/response names, header elements).
+    """
+    # Format strings for XML tags, corresponding to:
+    # 1) Normal start or end tag (without attribute)
+    # 2) Open and close tag (when no attributes or sub-structures exist)
+    # 3) Tag containing attributes
+    # We don't just look for 'cwmp:%s' (with no character after %s) because this
+    # would pick up all tags that start with the tag of interest (e.g
+    # cwmp:SetParameterAttributes would also match
+    # cwmp:SetParameterAttributesStruct)
+    XML_FORMAT_STRS = [
+        ["cwmp:%s>", "!!!TEMP_MOD!!!:%s>"],
+        ["cwmp:%s/>", "!!!TEMP_MOD!!!:%s/>"],
+        ["cwmp:%s ", "!!!TEMP_MOD!!!:%s "],
+    ]
+    fields_to_preserve_ns = list(RPC_METHODS) + list(RPC_RESPONSES) + \
+        list(CPE_RPC_METHODS) + list(CPE_RPC_RESPONSES) + \
+        list(PSEUDO_RPC_METHODS) + list(TOP_LEVEL_HEADER_ELEMENTS)
+    for field in fields_to_preserve_ns:
+        for formats in XML_FORMAT_STRS:
+            orig_str = formats[0] % field
+            temp_str = formats[1] % field
+            ctx.out_string[0] = ctx.out_string[0].replace(
+                orig_str.encode('ascii'), temp_str.encode('ascii'),
+            )
+
+    # Also preserve namespace inside strings, e.g. for arrayType="cwmp:..."
+    orig_str = "=\"cwmp:"
+    temp_str = "=\"!!!TEMP_MOD!!!:"
+    ctx.out_string[0] = ctx.out_string[0].replace(
+        orig_str.encode('ascii'), temp_str.encode('ascii'),
+    )
+    orig_str = "=\'cwmp:"
+    temp_str = "=\'!!!TEMP_MOD!!!:"
+    ctx.out_string[0] = ctx.out_string[0].replace(
+        orig_str.encode('ascii'), temp_str.encode('ascii'),
+    )
+
+    ctx.out_string[0] = ctx.out_string[0].replace(b'cwmp:', b'')
+    ctx.out_string[0] = ctx.out_string[0].replace(b'!!!TEMP_MOD!!!:', b'cwmp:')
+
+    # Special-case handling so that 'EmptyHttp' RPC will be called using
+    # completely empty HTTP request (not even containing a SOAP envelope), as
+    # per TR-069 spec.
+    if(ctx.descriptor.out_message.Attributes.sub_name == 'EmptyHttp'):
+        ctx.out_string = [b'']
+
+
+AutoConfigServer.event_manager.add_listener(
+    'method_return_string',
+    on_method_return_string,
+)