Init commit for standalone enodebd
Change-Id: I88eeef5135dd7ba8551ddd9fb6a0695f5325337b
diff --git a/state_machines/enb_acs_states.py b/state_machines/enb_acs_states.py
new file mode 100644
index 0000000..a9b84a5
--- /dev/null
+++ b/state_machines/enb_acs_states.py
@@ -0,0 +1,1293 @@
+"""
+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 abc import ABC, abstractmethod
+from collections import namedtuple
+from typing import Any, Optional
+
+from data_models.data_model import InvalidTrParamPath
+from data_models.data_model_parameters import ParameterName
+from device_config.configuration_init import build_desired_config
+from exceptions import ConfigurationError, Tr069Error
+from logger import EnodebdLogger as logger
+from state_machines.acs_state_utils import (
+ does_inform_have_event,
+ get_all_objects_to_add,
+ get_all_objects_to_delete,
+ get_all_param_values_to_set,
+ get_obj_param_values_to_set,
+ get_object_params_to_get,
+ get_optional_param_to_check,
+ get_param_values_to_set,
+ get_params_to_get,
+ parse_get_parameter_values_response,
+ process_inform_message,
+)
+from state_machines.enb_acs import EnodebAcsStateMachine
+from state_machines.timer import StateMachineTimer
+from tr069 import models
+
+AcsMsgAndTransition = namedtuple(
+ 'AcsMsgAndTransition', ['msg', 'next_state'],
+)
+
+AcsReadMsgResult = namedtuple(
+ 'AcsReadMsgResult', ['msg_handled', 'next_state'],
+)
+
+
+class EnodebAcsState(ABC):
+ """
+ State class for the Enodeb state machine
+
+ States can transition after reading a message from the eNB, sending a
+ message out to the eNB, or when a timer completes. As such, some states
+ are only responsible for message sending, and others are only responsible
+ for reading incoming messages.
+
+ In the constructor, set up state transitions.
+ """
+
+ def __init__(self):
+ self._acs = None
+
+ def enter(self) -> None:
+ """
+ Set up your timers here. Call transition(..) on the ACS when the timer
+ completes or throw an error
+ """
+ pass
+
+ def exit(self) -> None:
+ """Destroy timers here"""
+ pass
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ """
+ Args: message: tr069 message
+ Returns: name of the next state, if transition required
+ """
+ raise ConfigurationError(
+ '%s should implement read_msg() if it '
+ 'needs to handle message reading' % self.__class__.__name__,
+ )
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ """
+ Produce a message to send back to the eNB.
+
+ Args:
+ message: TR-069 message which was already processed by read_msg
+
+ Returns: Message and possible transition
+ """
+ raise ConfigurationError(
+ '%s should implement get_msg() if it '
+ 'needs to produce messages' % self.__class__.__name__,
+ )
+
+ @property
+ def acs(self) -> EnodebAcsStateMachine:
+ return self._acs
+
+ @acs.setter
+ def acs(self, val: EnodebAcsStateMachine) -> None:
+ self._acs = val
+
+ @abstractmethod
+ def state_description(self) -> str:
+ """ Provide a few words about what the state represents """
+ pass
+
+
+class WaitInformState(EnodebAcsState):
+ """
+ This state indicates that no Inform message has been received yet, or
+ that no Inform message has been received for a long time.
+
+ This state is used to handle an Inform message that arrived when enodebd
+ already believes that the eNB is connected. As such, it is unclear to
+ enodebd whether the eNB is just sending another Inform, or if a different
+ eNB was plugged into the same interface.
+ """
+
+ def __init__(
+ self,
+ acs: EnodebAcsStateMachine,
+ when_done: str,
+ when_boot: Optional[str] = None,
+ ):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+ self.boot_transition = when_boot
+ self.has_enb_just_booted = False
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ """
+ Args:
+ message: models.Inform Tr069 Inform message
+ """
+ if not isinstance(message, models.Inform):
+ return AcsReadMsgResult(False, None)
+ process_inform_message(
+ message, self.acs.data_model,
+ self.acs.device_cfg,
+ )
+ if does_inform_have_event(message, '1 BOOT'):
+ return AcsReadMsgResult(True, self.boot_transition)
+ return AcsReadMsgResult(True, None)
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ """ Reply with InformResponse """
+ response = models.InformResponse()
+ # Set maxEnvelopes to 1, as per TR-069 spec
+ response.MaxEnvelopes = 1
+ return AcsMsgAndTransition(response, self.done_transition)
+
+ def state_description(self) -> str:
+ return 'Waiting for an Inform'
+
+
+class GetRPCMethodsState(EnodebAcsState):
+ """
+ After the first Inform message from boot, it is expected that the eNB
+ will try to learn the RPC methods of the ACS.
+ """
+
+ def __init__(self, acs: EnodebAcsStateMachine, when_done: str, when_skip: str):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+ self.skip_transition = when_skip
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ # If this is a regular Inform, not after a reboot we'll get an empty
+ if isinstance(message, models.DummyInput):
+ return AcsReadMsgResult(True, self.skip_transition)
+ if not isinstance(message, models.GetRPCMethods):
+ return AcsReadMsgResult(False, self.done_transition)
+ return AcsReadMsgResult(True, None)
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ resp = models.GetRPCMethodsResponse()
+ resp.MethodList = models.MethodList()
+ RPC_METHODS = ['Inform', 'GetRPCMethods', 'TransferComplete']
+ resp.MethodList.arrayType = 'xsd:string[%d]' \
+ % len(RPC_METHODS)
+ resp.MethodList.string = RPC_METHODS
+ return AcsMsgAndTransition(resp, self.done_transition)
+
+ def state_description(self) -> str:
+ return 'Waiting for incoming GetRPC Methods after boot'
+
+
+class BaicellsRemWaitState(EnodebAcsState):
+ """
+ We've already received an Inform message. This state is to handle a
+ Baicells eNodeB issue.
+
+ After eNodeB is rebooted, hold off configuring it for some time to give
+ time for REM to run. This is a BaiCells eNodeB issue that doesn't support
+ enabling the eNodeB during initial REM.
+
+ In this state, just hang at responding to Inform, and then ending the
+ TR-069 session.
+ """
+
+ CONFIG_DELAY_AFTER_BOOT = 600
+
+ def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+ self.rem_timer = None
+
+ def enter(self):
+ self.rem_timer = StateMachineTimer(self.CONFIG_DELAY_AFTER_BOOT)
+ logger.info(
+ 'Holding off of eNB configuration for %s seconds. '
+ 'Will resume after eNB REM process has finished. ',
+ self.CONFIG_DELAY_AFTER_BOOT,
+ )
+
+ def exit(self):
+ self.rem_timer = None
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ if not isinstance(message, models.Inform):
+ return AcsReadMsgResult(False, None)
+ process_inform_message(
+ message, self.acs.data_model,
+ self.acs.device_cfg,
+ )
+ return AcsReadMsgResult(True, None)
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ if self.rem_timer.is_done():
+ return AcsMsgAndTransition(
+ models.DummyInput(),
+ self.done_transition,
+ )
+ return AcsMsgAndTransition(models.DummyInput(), None)
+
+ def state_description(self) -> str:
+ remaining = self.rem_timer.seconds_remaining()
+ return 'Waiting for eNB REM to run for %d more seconds before ' \
+ 'resuming with configuration.' % remaining
+
+
+class WaitEmptyMessageState(EnodebAcsState):
+ def __init__(
+ self,
+ acs: EnodebAcsStateMachine,
+ when_done: str,
+ when_missing: Optional[str] = None,
+ ):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+ self.unknown_param_transition = when_missing
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ """
+ It's expected that we transition into this state right after receiving
+ an Inform message and replying with an InformResponse. At that point,
+ the eNB sends an empty HTTP request (aka DummyInput) to initiate the
+ rest of the provisioning process
+ """
+ if not isinstance(message, models.DummyInput):
+ logger.debug("Ignoring message %s", str(type(message)))
+ return AcsReadMsgResult(msg_handled=False, next_state=None)
+ if self.unknown_param_transition:
+ if get_optional_param_to_check(self.acs.data_model):
+ return AcsReadMsgResult(
+ msg_handled=True,
+ next_state=self.unknown_param_transition,
+ )
+ return AcsReadMsgResult(
+ msg_handled=True,
+ next_state=self.done_transition,
+ )
+
+ def get_msg(self, message: Any) -> AcsReadMsgResult:
+ """
+ Return a dummy message waiting for the empty message from CPE
+ """
+ request = models.DummyInput()
+ return AcsMsgAndTransition(msg=request, next_state=None)
+
+ def state_description(self) -> str:
+ return 'Waiting for empty message from eNodeB'
+
+
+class CheckOptionalParamsState(EnodebAcsState):
+ def __init__(
+ self,
+ acs: EnodebAcsStateMachine,
+ when_done: str,
+ ):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+ self.optional_param = None
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ self.optional_param = get_optional_param_to_check(self.acs.data_model)
+ if self.optional_param is None:
+ raise Tr069Error('Invalid State')
+ # Generate the request
+ request = models.GetParameterValues()
+ request.ParameterNames = models.ParameterNames()
+ request.ParameterNames.arrayType = 'xsd:string[1]'
+ request.ParameterNames.string = []
+ path = self.acs.data_model.get_parameter(self.optional_param).path
+ request.ParameterNames.string.append(path)
+ return AcsMsgAndTransition(request, None)
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ """ Process either GetParameterValuesResponse or a Fault """
+ if type(message) == models.Fault:
+ self.acs.data_model.set_parameter_presence(
+ self.optional_param,
+ False,
+ )
+ elif type(message) == models.GetParameterValuesResponse:
+ name_to_val = parse_get_parameter_values_response(
+ self.acs.data_model,
+ message,
+ )
+ logger.debug(
+ 'Received CPE parameter values: %s',
+ str(name_to_val),
+ )
+ for name, val in name_to_val.items():
+ self.acs.data_model.set_parameter_presence(
+ self.optional_param,
+ True,
+ )
+ magma_val = self.acs.data_model.transform_for_magma(name, val)
+ self.acs.device_cfg.set_parameter(name, magma_val)
+ else:
+ return AcsReadMsgResult(False, None)
+
+ if get_optional_param_to_check(self.acs.data_model) is not None:
+ return AcsReadMsgResult(True, None)
+ return AcsReadMsgResult(True, self.done_transition)
+
+ def state_description(self) -> str:
+ return 'Checking if some optional parameters exist in data model'
+
+
+class SendGetTransientParametersState(EnodebAcsState):
+ """
+ Periodically read eNodeB status. Note: keep frequency low to avoid
+ backing up large numbers of read operations if enodebd is busy.
+ Some eNB parameters are read only and updated by the eNB itself.
+ """
+ PARAMETERS = [
+ ParameterName.OP_STATE,
+ ParameterName.RF_TX_STATUS,
+ ParameterName.GPS_STATUS,
+ ParameterName.PTP_STATUS,
+ ParameterName.MME_STATUS,
+ ParameterName.GPS_LAT,
+ ParameterName.GPS_LONG,
+ ]
+
+ def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ if not isinstance(message, models.DummyInput):
+ return AcsReadMsgResult(False, None)
+ return AcsReadMsgResult(True, None)
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ request = models.GetParameterValues()
+ request.ParameterNames = models.ParameterNames()
+ request.ParameterNames.string = []
+ for name in self.PARAMETERS:
+ # Not all data models have these parameters
+ if self.acs.data_model.is_parameter_present(name):
+ path = self.acs.data_model.get_parameter(name).path
+ request.ParameterNames.string.append(path)
+ request.ParameterNames.arrayType = \
+ 'xsd:string[%d]' % len(request.ParameterNames.string)
+
+ return AcsMsgAndTransition(request, self.done_transition)
+
+ def state_description(self) -> str:
+ return 'Getting transient read-only parameters'
+
+
+class WaitGetTransientParametersState(EnodebAcsState):
+ """
+ Periodically read eNodeB status. Note: keep frequency low to avoid
+ backing up large numbers of read operations if enodebd is busy
+ """
+
+ def __init__(
+ self,
+ acs: EnodebAcsStateMachine,
+ when_get: str,
+ when_get_obj_params: str,
+ when_delete: str,
+ when_add: str,
+ when_set: str,
+ when_skip: str,
+ ):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_get
+ self.get_obj_params_transition = when_get_obj_params
+ self.rm_obj_transition = when_delete
+ self.add_obj_transition = when_add
+ self.set_transition = when_set
+ self.skip_transition = when_skip
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ if not isinstance(message, models.GetParameterValuesResponse):
+ return AcsReadMsgResult(False, None)
+ # Current values of the fetched parameters
+ name_to_val = parse_get_parameter_values_response(
+ self.acs.data_model,
+ message,
+ )
+ logger.debug('Fetched Transient Params: %s', str(name_to_val))
+
+ # Update device configuration
+ for name in name_to_val:
+ magma_val = \
+ self.acs.data_model.transform_for_magma(
+ name,
+ name_to_val[name],
+ )
+ self.acs.device_cfg.set_parameter(name, magma_val)
+
+ return AcsReadMsgResult(True, self.get_next_state())
+
+ def get_next_state(self) -> str:
+ should_get_params = \
+ len(
+ get_params_to_get(
+ self.acs.device_cfg,
+ self.acs.data_model,
+ ),
+ ) > 0
+ if should_get_params:
+ return self.done_transition
+ should_get_obj_params = \
+ len(
+ get_object_params_to_get(
+ self.acs.desired_cfg,
+ self.acs.device_cfg,
+ self.acs.data_model,
+ ),
+ ) > 0
+ if should_get_obj_params:
+ return self.get_obj_params_transition
+ elif len(
+ get_all_objects_to_delete(
+ self.acs.desired_cfg,
+ self.acs.device_cfg,
+ ),
+ ) > 0:
+ return self.rm_obj_transition
+ elif len(
+ get_all_objects_to_add(
+ self.acs.desired_cfg,
+ self.acs.device_cfg,
+ ),
+ ) > 0:
+ return self.add_obj_transition
+ return self.skip_transition
+
+ def state_description(self) -> str:
+ return 'Getting transient read-only parameters'
+
+
+class GetParametersState(EnodebAcsState):
+ """
+ Get the value of most parameters of the eNB that are defined in the data
+ model. Object parameters are excluded.
+ """
+
+ def __init__(
+ self,
+ acs: EnodebAcsStateMachine,
+ when_done: str,
+ request_all_params: bool = False,
+ ):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+ # Set to True if we want to request values of all parameters, even if
+ # the ACS state machine already has recorded values of them.
+ self.request_all_params = request_all_params
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ """
+ It's expected that we transition into this state right after receiving
+ an Inform message and replying with an InformResponse. At that point,
+ the eNB sends an empty HTTP request (aka DummyInput) to initiate the
+ rest of the provisioning process
+ """
+ if not isinstance(message, models.DummyInput):
+ return AcsReadMsgResult(False, None)
+ return AcsReadMsgResult(True, None)
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ """
+ Respond with GetParameterValuesRequest
+
+ Get the values of all parameters defined in the data model.
+ Also check which addable objects are present, and what the values of
+ parameters for those objects are.
+ """
+
+ # Get the names of regular parameters
+ names = get_params_to_get(
+ self.acs.device_cfg, self.acs.data_model,
+ self.request_all_params,
+ )
+
+ # Generate the request
+ request = models.GetParameterValues()
+ request.ParameterNames = models.ParameterNames()
+ request.ParameterNames.arrayType = 'xsd:string[%d]' \
+ % len(names)
+ request.ParameterNames.string = []
+ for name in names:
+ path = self.acs.data_model.get_parameter(name).path
+ if path is not InvalidTrParamPath:
+ # Only get data elements backed by tr69 path
+ request.ParameterNames.string.append(path)
+
+ return AcsMsgAndTransition(request, self.done_transition)
+
+ def state_description(self) -> str:
+ return 'Getting non-object parameters'
+
+
+class WaitGetParametersState(EnodebAcsState):
+ def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ """ Process GetParameterValuesResponse """
+ if not isinstance(message, models.GetParameterValuesResponse):
+ return AcsReadMsgResult(False, None)
+ name_to_val = parse_get_parameter_values_response(
+ self.acs.data_model,
+ message,
+ )
+ logger.debug('Received CPE parameter values: %s', str(name_to_val))
+ for name, val in name_to_val.items():
+ magma_val = self.acs.data_model.transform_for_magma(name, val)
+ self.acs.device_cfg.set_parameter(name, magma_val)
+ return AcsReadMsgResult(True, self.done_transition)
+
+ def state_description(self) -> str:
+ return 'Getting non-object parameters'
+
+
+class GetObjectParametersState(EnodebAcsState):
+ def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ """ Respond with GetParameterValuesRequest """
+ names = get_object_params_to_get(
+ self.acs.desired_cfg,
+ self.acs.device_cfg,
+ self.acs.data_model,
+ )
+
+ # Generate the request
+ request = models.GetParameterValues()
+ request.ParameterNames = models.ParameterNames()
+ request.ParameterNames.arrayType = 'xsd:string[%d]' \
+ % len(names)
+ request.ParameterNames.string = []
+ for name in names:
+ path = self.acs.data_model.get_parameter(name).path
+ request.ParameterNames.string.append(path)
+
+ return AcsMsgAndTransition(request, self.done_transition)
+
+ def state_description(self) -> str:
+ return 'Getting object parameters'
+
+
+class WaitGetObjectParametersState(EnodebAcsState):
+ def __init__(
+ self,
+ acs: EnodebAcsStateMachine,
+ when_delete: str,
+ when_add: str,
+ when_set: str,
+ when_skip: str,
+ ):
+ super().__init__()
+ self.acs = acs
+ self.rm_obj_transition = when_delete
+ self.add_obj_transition = when_add
+ self.set_params_transition = when_set
+ self.skip_transition = when_skip
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ """ Process GetParameterValuesResponse """
+ if not isinstance(message, models.GetParameterValuesResponse):
+ return AcsReadMsgResult(False, None)
+
+ path_to_val = {}
+ if hasattr(message.ParameterList, 'ParameterValueStruct') and \
+ message.ParameterList.ParameterValueStruct is not None:
+ for param_value_struct in message.ParameterList.ParameterValueStruct:
+ path_to_val[param_value_struct.Name] = \
+ param_value_struct.Value.Data
+ logger.debug('Received object parameters: %s', str(path_to_val))
+
+ # Number of PLMN objects reported can be incorrect. Let's count them
+ num_plmns = 0
+ obj_to_params = self.acs.data_model.get_numbered_param_names()
+ while True:
+ obj_name = ParameterName.PLMN_N % (num_plmns + 1)
+ if obj_name not in obj_to_params or len(obj_to_params[obj_name]) == 0:
+ logger.warning(
+ "eNB has PLMN %s but not defined in model",
+ obj_name,
+ )
+ break
+ param_name_list = obj_to_params[obj_name]
+ obj_path = self.acs.data_model.get_parameter(param_name_list[0]).path
+ if obj_path not in path_to_val:
+ break
+ if not self.acs.device_cfg.has_object(obj_name):
+ self.acs.device_cfg.add_object(obj_name)
+ num_plmns += 1
+ for name in param_name_list:
+ path = self.acs.data_model.get_parameter(name).path
+ value = path_to_val[path]
+ magma_val = \
+ self.acs.data_model.transform_for_magma(name, value)
+ self.acs.device_cfg.set_parameter_for_object(
+ name, magma_val,
+ obj_name,
+ )
+ num_plmns_reported = \
+ int(self.acs.device_cfg.get_parameter(ParameterName.NUM_PLMNS))
+ if num_plmns != num_plmns_reported:
+ logger.warning(
+ "eNB reported %d PLMNs but found %d",
+ num_plmns_reported, num_plmns,
+ )
+ self.acs.device_cfg.set_parameter(
+ ParameterName.NUM_PLMNS,
+ num_plmns,
+ )
+
+ # Now we can have the desired state
+ if self.acs.desired_cfg is None:
+ self.acs.desired_cfg = build_desired_config(
+ self.acs.mconfig,
+ self.acs.service_config,
+ self.acs.device_cfg,
+ self.acs.data_model,
+ self.acs.config_postprocessor,
+ )
+
+ if len(
+ get_all_objects_to_delete(
+ self.acs.desired_cfg,
+ self.acs.device_cfg,
+ ),
+ ) > 0:
+ return AcsReadMsgResult(True, self.rm_obj_transition)
+ elif len(
+ get_all_objects_to_add(
+ self.acs.desired_cfg,
+ self.acs.device_cfg,
+ ),
+ ) > 0:
+ return AcsReadMsgResult(True, self.add_obj_transition)
+ elif len(
+ get_all_param_values_to_set(
+ self.acs.desired_cfg,
+ self.acs.device_cfg,
+ self.acs.data_model,
+ ),
+ ) > 0:
+ return AcsReadMsgResult(True, self.set_params_transition)
+ return AcsReadMsgResult(True, self.skip_transition)
+
+ def state_description(self) -> str:
+ return 'Getting object parameters'
+
+
+class DeleteObjectsState(EnodebAcsState):
+ def __init__(
+ self,
+ acs: EnodebAcsStateMachine,
+ when_add: str,
+ when_skip: str,
+ ):
+ super().__init__()
+ self.acs = acs
+ self.deleted_param = None
+ self.add_obj_transition = when_add
+ self.skip_transition = when_skip
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ """
+ Send DeleteObject message to TR-069 and poll for response(s).
+ Input:
+ - Object name (string)
+ """
+ request = models.DeleteObject()
+ self.deleted_param = get_all_objects_to_delete(
+ self.acs.desired_cfg,
+ self.acs.device_cfg,
+ )[0]
+ request.ObjectName = \
+ self.acs.data_model.get_parameter(self.deleted_param).path
+ return AcsMsgAndTransition(request, None)
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ """
+ Send DeleteObject message to TR-069 and poll for response(s).
+ Input:
+ - Object name (string)
+ """
+ if type(message) == models.DeleteObjectResponse:
+ if message.Status != 0:
+ raise Tr069Error(
+ 'Received DeleteObjectResponse with '
+ 'Status=%d' % message.Status,
+ )
+ elif type(message) == models.Fault:
+ raise Tr069Error(
+ 'Received Fault in response to DeleteObject '
+ '(faultstring = %s)' % message.FaultString,
+ )
+ else:
+ return AcsReadMsgResult(False, None)
+
+ self.acs.device_cfg.delete_object(self.deleted_param)
+ obj_list_to_delete = get_all_objects_to_delete(
+ self.acs.desired_cfg,
+ self.acs.device_cfg,
+ )
+ if len(obj_list_to_delete) > 0:
+ return AcsReadMsgResult(True, None)
+ if len(
+ get_all_objects_to_add(
+ self.acs.desired_cfg,
+ self.acs.device_cfg,
+ ),
+ ) == 0:
+ return AcsReadMsgResult(True, self.skip_transition)
+ return AcsReadMsgResult(True, self.add_obj_transition)
+
+ def state_description(self) -> str:
+ return 'Deleting objects'
+
+
+class AddObjectsState(EnodebAcsState):
+ def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+ self.added_param = None
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ request = models.AddObject()
+ self.added_param = get_all_objects_to_add(
+ self.acs.desired_cfg,
+ self.acs.device_cfg,
+ )[0]
+ desired_param = self.acs.data_model.get_parameter(self.added_param)
+ desired_path = desired_param.path
+ path_parts = desired_path.split('.')
+ # If adding enumerated object, ie. XX.N. we should add it to the
+ # parent object XX. so strip the index
+ if len(path_parts) > 2 and \
+ path_parts[-1] == '' and path_parts[-2].isnumeric():
+ logger.debug('Stripping index from path=%s', desired_path)
+ desired_path = '.'.join(path_parts[:-2]) + '.'
+ request.ObjectName = desired_path
+ return AcsMsgAndTransition(request, None)
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ if type(message) == models.AddObjectResponse:
+ if message.Status != 0:
+ raise Tr069Error(
+ 'Received AddObjectResponse with '
+ 'Status=%d' % message.Status,
+ )
+ elif type(message) == models.Fault:
+ raise Tr069Error(
+ 'Received Fault in response to AddObject '
+ '(faultstring = %s)' % message.FaultString,
+ )
+ else:
+ return AcsReadMsgResult(False, None)
+ instance_n = message.InstanceNumber
+ self.acs.device_cfg.add_object(self.added_param % instance_n)
+ obj_list_to_add = get_all_objects_to_add(
+ self.acs.desired_cfg,
+ self.acs.device_cfg,
+ )
+ if len(obj_list_to_add) > 0:
+ return AcsReadMsgResult(True, None)
+ return AcsReadMsgResult(True, self.done_transition)
+
+ def state_description(self) -> str:
+ return 'Adding objects'
+
+
+class SetParameterValuesState(EnodebAcsState):
+ def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ request = models.SetParameterValues()
+ request.ParameterList = models.ParameterValueList()
+ param_values = get_all_param_values_to_set(
+ self.acs.desired_cfg,
+ self.acs.device_cfg,
+ self.acs.data_model,
+ )
+ request.ParameterList.arrayType = 'cwmp:ParameterValueStruct[%d]' \
+ % len(param_values)
+ request.ParameterList.ParameterValueStruct = []
+ logger.debug(
+ 'Sending TR069 request to set CPE parameter values: %s',
+ str(param_values),
+ )
+ # TODO: Match key response when we support having multiple outstanding
+ # calls.
+ if self.acs.has_version_key:
+ request.ParameterKey = models.ParameterKeyType()
+ request.ParameterKey.Data =\
+ "SetParameter-{:10.0f}".format(self.acs.parameter_version_key)
+ request.ParameterKey.type = 'xsd:string'
+
+ for name, value in param_values.items():
+ param_info = self.acs.data_model.get_parameter(name)
+ type_ = param_info.type
+ name_value = models.ParameterValueStruct()
+ name_value.Value = models.anySimpleType()
+ name_value.Name = param_info.path
+ enb_value = self.acs.data_model.transform_for_enb(name, value)
+ if type_ in ('int', 'unsignedInt'):
+ name_value.Value.type = 'xsd:%s' % type_
+ name_value.Value.Data = str(enb_value)
+ elif type_ == 'boolean':
+ # Boolean values have integral representations in spec
+ name_value.Value.type = 'xsd:boolean'
+ name_value.Value.Data = str(int(enb_value))
+ elif type_ == 'string':
+ name_value.Value.type = 'xsd:string'
+ name_value.Value.Data = str(enb_value)
+ else:
+ raise Tr069Error(
+ 'Unsupported type for %s: %s' %
+ (name, type_),
+ )
+ if param_info.is_invasive:
+ self.acs.are_invasive_changes_applied = False
+ request.ParameterList.ParameterValueStruct.append(name_value)
+
+ return AcsMsgAndTransition(request, self.done_transition)
+
+ def state_description(self) -> str:
+ return 'Setting parameter values'
+
+
+class SetParameterValuesNotAdminState(EnodebAcsState):
+ def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ request = models.SetParameterValues()
+ request.ParameterList = models.ParameterValueList()
+ param_values = get_all_param_values_to_set(
+ self.acs.desired_cfg,
+ self.acs.device_cfg,
+ self.acs.data_model,
+ exclude_admin=True,
+ )
+ request.ParameterList.arrayType = 'cwmp:ParameterValueStruct[%d]' \
+ % len(param_values)
+ request.ParameterList.ParameterValueStruct = []
+ logger.debug(
+ 'Sending TR069 request to set CPE parameter values: %s',
+ str(param_values),
+ )
+ for name, value in param_values.items():
+ param_info = self.acs.data_model.get_parameter(name)
+ type_ = param_info.type
+ name_value = models.ParameterValueStruct()
+ name_value.Value = models.anySimpleType()
+ name_value.Name = param_info.path
+ enb_value = self.acs.data_model.transform_for_enb(name, value)
+ if type_ in ('int', 'unsignedInt'):
+ name_value.Value.type = 'xsd:%s' % type_
+ name_value.Value.Data = str(enb_value)
+ elif type_ == 'boolean':
+ # Boolean values have integral representations in spec
+ name_value.Value.type = 'xsd:boolean'
+ name_value.Value.Data = str(int(enb_value))
+ elif type_ == 'string':
+ name_value.Value.type = 'xsd:string'
+ name_value.Value.Data = str(enb_value)
+ else:
+ raise Tr069Error(
+ 'Unsupported type for %s: %s' %
+ (name, type_),
+ )
+ if param_info.is_invasive:
+ self.acs.are_invasive_changes_applied = False
+ request.ParameterList.ParameterValueStruct.append(name_value)
+
+ return AcsMsgAndTransition(request, self.done_transition)
+
+ def state_description(self) -> str:
+ return 'Setting parameter values excluding Admin Enable'
+
+
+class WaitSetParameterValuesState(EnodebAcsState):
+ def __init__(
+ self,
+ acs: EnodebAcsStateMachine,
+ when_done: str,
+ when_apply_invasive: str,
+ status_non_zero_allowed: bool = False,
+ ):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+ self.apply_invasive_transition = when_apply_invasive
+ # Set Params can legally return zero and non zero status
+ # Per tr-196, if there are errors the method should return a fault.
+ # Make flag optional to compensate for existing radios returning non
+ # zero on error.
+ self.status_non_zero_allowed = status_non_zero_allowed
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ if type(message) == models.SetParameterValuesResponse:
+ if not self.status_non_zero_allowed:
+ if message.Status != 0:
+ raise Tr069Error(
+ 'Received SetParameterValuesResponse with '
+ 'Status=%d' % message.Status,
+ )
+ self._mark_as_configured()
+ if not self.acs.are_invasive_changes_applied:
+ return AcsReadMsgResult(True, self.apply_invasive_transition)
+ return AcsReadMsgResult(True, self.done_transition)
+ elif type(message) == models.Fault:
+ logger.error(
+ 'Received Fault in response to SetParameterValues, '
+ 'Code (%s), Message (%s)', message.FaultCode,
+ message.FaultString,
+ )
+ if message.SetParameterValuesFault is not None:
+ for fault in message.SetParameterValuesFault:
+ logger.error(
+ 'SetParameterValuesFault Param: %s, '
+ 'Code: %s, String: %s', fault.ParameterName,
+ fault.FaultCode, fault.FaultString,
+ )
+ return AcsReadMsgResult(False, None)
+
+ def _mark_as_configured(self) -> None:
+ """
+ A successful attempt at setting parameter values means that we need to
+ update what we think the eNB's configuration is to match what we just
+ set the parameter values to.
+ """
+ # Values of parameters
+ name_to_val = get_param_values_to_set(
+ self.acs.desired_cfg,
+ self.acs.device_cfg,
+ self.acs.data_model,
+ )
+ for name, val in name_to_val.items():
+ magma_val = self.acs.data_model.transform_for_magma(name, val)
+ self.acs.device_cfg.set_parameter(name, magma_val)
+
+ # Values of object parameters
+ obj_to_name_to_val = get_obj_param_values_to_set(
+ self.acs.desired_cfg,
+ self.acs.device_cfg,
+ self.acs.data_model,
+ )
+ for obj_name, name_to_val in obj_to_name_to_val.items():
+ for name, val in name_to_val.items():
+ logger.debug(
+ 'Set obj: %s, name: %s, val: %s', str(obj_name),
+ str(name), str(val),
+ )
+ magma_val = self.acs.data_model.transform_for_magma(name, val)
+ self.acs.device_cfg.set_parameter_for_object(
+ name, magma_val,
+ obj_name,
+ )
+ logger.info('Successfully configured CPE parameters!')
+
+ def state_description(self) -> str:
+ return 'Setting parameter values'
+
+
+class EndSessionState(EnodebAcsState):
+ """ To end a TR-069 session, send an empty HTTP response """
+
+ def __init__(self, acs: EnodebAcsStateMachine):
+ super().__init__()
+ self.acs = acs
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ """
+ No message is expected after enodebd sends the eNodeB
+ an empty HTTP response.
+
+ If a device sends an empty HTTP request, we can just
+ ignore it and send another empty response.
+ """
+ if isinstance(message, models.DummyInput):
+ return AcsReadMsgResult(True, None)
+ return AcsReadMsgResult(False, None)
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ request = models.DummyInput()
+ return AcsMsgAndTransition(request, None)
+
+ def state_description(self) -> str:
+ return 'Completed provisioning eNB. Awaiting new Inform.'
+
+
+class EnbSendRebootState(EnodebAcsState):
+ def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+ self.prev_msg_was_inform = False
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ """
+ This state can be transitioned into through user command.
+ All messages received by enodebd will be ignored in this state.
+ """
+ if self.prev_msg_was_inform \
+ and not isinstance(message, models.DummyInput):
+ return AcsReadMsgResult(False, None)
+ elif isinstance(message, models.Inform):
+ self.prev_msg_was_inform = True
+ process_inform_message(
+ message, self.acs.data_model,
+ self.acs.device_cfg,
+ )
+ return AcsReadMsgResult(True, None)
+ self.prev_msg_was_inform = False
+ return AcsReadMsgResult(True, None)
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ if self.prev_msg_was_inform:
+ response = models.InformResponse()
+ # Set maxEnvelopes to 1, as per TR-069 spec
+ response.MaxEnvelopes = 1
+ return AcsMsgAndTransition(response, None)
+ logger.info('Sending reboot request to eNB')
+ request = models.Reboot()
+ request.CommandKey = ''
+ self.acs.are_invasive_changes_applied = True
+ return AcsMsgAndTransition(request, self.done_transition)
+
+ def state_description(self) -> str:
+ return 'Rebooting eNB'
+
+
+class SendRebootState(EnodebAcsState):
+ def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+ self.prev_msg_was_inform = False
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ """
+ This state can be transitioned into through user command.
+ All messages received by enodebd will be ignored in this state.
+ """
+ if self.prev_msg_was_inform \
+ and not isinstance(message, models.DummyInput):
+ return AcsReadMsgResult(False, None)
+ elif isinstance(message, models.Inform):
+ self.prev_msg_was_inform = True
+ process_inform_message(
+ message, self.acs.data_model,
+ self.acs.device_cfg,
+ )
+ return AcsReadMsgResult(True, None)
+ self.prev_msg_was_inform = False
+ return AcsReadMsgResult(True, None)
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ if self.prev_msg_was_inform:
+ response = models.InformResponse()
+ # Set maxEnvelopes to 1, as per TR-069 spec
+ response.MaxEnvelopes = 1
+ return AcsMsgAndTransition(response, None)
+ logger.info('Sending reboot request to eNB')
+ request = models.Reboot()
+ request.CommandKey = ''
+ return AcsMsgAndTransition(request, self.done_transition)
+
+ def state_description(self) -> str:
+ return 'Rebooting eNB'
+
+
+class WaitRebootResponseState(EnodebAcsState):
+ def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ if not isinstance(message, models.RebootResponse):
+ return AcsReadMsgResult(False, None)
+ return AcsReadMsgResult(True, None)
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ """ Reply with empty message """
+ return AcsMsgAndTransition(models.DummyInput(), self.done_transition)
+
+ def state_description(self) -> str:
+ return 'Rebooting eNB'
+
+
+class WaitInformMRebootState(EnodebAcsState):
+ """
+ After sending a reboot request, we expect an Inform request with a
+ specific 'inform event code'
+ """
+
+ # Time to wait for eNodeB reboot. The measured time
+ # (on BaiCells indoor eNodeB)
+ # is ~110secs, so add healthy padding on top of this.
+ REBOOT_TIMEOUT = 300 # In seconds
+ # We expect that the Inform we receive tells us the eNB has rebooted
+ INFORM_EVENT_CODE = 'M Reboot'
+
+ def __init__(
+ self,
+ acs: EnodebAcsStateMachine,
+ when_done: str,
+ when_timeout: str,
+ ):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+ self.timeout_transition = when_timeout
+ self.timeout_timer = None
+ self.timer_handle = None
+
+ def enter(self):
+ self.timeout_timer = StateMachineTimer(self.REBOOT_TIMEOUT)
+
+ def check_timer() -> None:
+ if self.timeout_timer.is_done():
+ self.acs.transition(self.timeout_transition)
+ raise Tr069Error(
+ 'Did not receive Inform response after '
+ 'rebooting',
+ )
+
+ self.timer_handle = \
+ self.acs.event_loop.call_later(
+ self.REBOOT_TIMEOUT,
+ check_timer,
+ )
+
+ def exit(self):
+ self.timer_handle.cancel()
+ self.timeout_timer = None
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ if not isinstance(message, models.Inform):
+ return AcsReadMsgResult(False, None)
+ if not does_inform_have_event(message, self.INFORM_EVENT_CODE):
+ raise Tr069Error(
+ 'Did not receive M Reboot event code in '
+ 'Inform',
+ )
+ process_inform_message(
+ message, self.acs.data_model,
+ self.acs.device_cfg,
+ )
+ return AcsReadMsgResult(True, self.done_transition)
+
+ def state_description(self) -> str:
+ return 'Waiting for M Reboot code from Inform'
+
+
+class WaitRebootDelayState(EnodebAcsState):
+ """
+ After receiving the Inform notifying us that the eNodeB has successfully
+ rebooted, wait a short duration to prevent unspecified race conditions
+ that may occur w.r.t reboot
+ """
+
+ # Short delay timer to prevent race conditions w.r.t. reboot
+ SHORT_CONFIG_DELAY = 10
+
+ def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
+ super().__init__()
+ self.acs = acs
+ self.done_transition = when_done
+ self.config_timer = None
+ self.timer_handle = None
+
+ def enter(self):
+ self.config_timer = StateMachineTimer(self.SHORT_CONFIG_DELAY)
+
+ def check_timer() -> None:
+ if self.config_timer.is_done():
+ self.acs.transition(self.done_transition)
+
+ self.timer_handle = \
+ self.acs.event_loop.call_later(
+ self.SHORT_CONFIG_DELAY,
+ check_timer,
+ )
+
+ def exit(self):
+ self.timer_handle.cancel()
+ self.config_timer = None
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ return AcsReadMsgResult(True, None)
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ return AcsMsgAndTransition(models.DummyInput(), None)
+
+ def state_description(self) -> str:
+ return 'Waiting after eNB reboot to prevent race conditions'
+
+
+class ErrorState(EnodebAcsState):
+ """
+ The eNB handler will enter this state when an unhandled Fault is received.
+
+ If the inform_transition_target constructor parameter is non-null, this
+ state will attempt to autoremediate by transitioning to the specified
+ target state when an Inform is received.
+ """
+
+ def __init__(
+ self, acs: EnodebAcsStateMachine,
+ inform_transition_target: Optional[str] = None,
+ ):
+ super().__init__()
+ self.acs = acs
+ self.inform_transition_target = inform_transition_target
+
+ def read_msg(self, message: Any) -> AcsReadMsgResult:
+ return AcsReadMsgResult(True, None)
+
+ def get_msg(self, message: Any) -> AcsMsgAndTransition:
+ if not self.inform_transition_target:
+ return AcsMsgAndTransition(models.DummyInput(), None)
+
+ if isinstance(message, models.Inform):
+ return AcsMsgAndTransition(
+ models.DummyInput(),
+ self.inform_transition_target,
+ )
+ return AcsMsgAndTransition(models.DummyInput(), None)
+
+ def state_description(self) -> str:
+ return 'Error state - awaiting manual restart of enodebd service or ' \
+ 'an Inform to be received from the eNB'