Init commit for standalone enodebd
Change-Id: I88eeef5135dd7ba8551ddd9fb6a0695f5325337b
diff --git a/state_machines/__init__.py b/state_machines/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/state_machines/__init__.py
diff --git a/state_machines/acs_state_utils.py b/state_machines/acs_state_utils.py
new file mode 100644
index 0000000..e0e32cc
--- /dev/null
+++ b/state_machines/acs_state_utils.py
@@ -0,0 +1,328 @@
+"""
+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 typing import Any, Dict, List, Optional
+
+from data_models.data_model import DataModel
+from data_models.data_model_parameters import ParameterName
+from device_config.enodeb_configuration import EnodebConfiguration
+from devices.device_utils import EnodebDeviceName, get_device_name
+from exceptions import ConfigurationError
+from logger import EnodebdLogger as logger
+from tr069 import models
+
+
+def process_inform_message(
+ inform: Any,
+ data_model: DataModel,
+ device_cfg: EnodebConfiguration,
+) -> None:
+ """
+ Modifies the device configuration based on what is received in the Inform
+ message. Will raise an error if it turns out that the data model we are
+ using is incorrect. This is decided based on the device OUI and
+ software-version that is reported in the Inform message.
+
+ Args:
+ inform: Inform Tr069 message
+ device_handler: The state machine we are using for our device
+ """
+ param_values_by_path = _get_param_values_by_path(inform)
+ param_name_list = data_model.get_parameter_names()
+ name_to_val = {}
+ for name in param_name_list:
+ path = data_model.get_parameter(name).path
+ if path in param_values_by_path:
+ value = param_values_by_path[path]
+ name_to_val[name] = value
+
+ for name, val in name_to_val.items():
+ device_cfg.set_parameter(name, val)
+
+
+def get_device_name_from_inform(
+ inform: models.Inform,
+) -> EnodebDeviceName:
+ def _get_param_value_from_path_suffix(
+ suffix: str,
+ path_list: List[str],
+ param_values_by_path: Dict[str, Any],
+ ) -> Any:
+ for path in path_list:
+ if path.endswith(suffix):
+ return param_values_by_path[path]
+ raise ConfigurationError('Did not receive expected info in Inform')
+
+ param_values_by_path = _get_param_values_by_path(inform)
+
+ # Check the OUI and version number to see if the data model matches
+ path_list = list(param_values_by_path.keys())
+ if hasattr(inform, 'DeviceId') and \
+ hasattr(inform.DeviceId, 'OUI'):
+ device_oui = inform.DeviceId.OUI
+ else:
+ device_oui = _get_param_value_from_path_suffix(
+ 'DeviceInfo.ManufacturerOUI',
+ path_list,
+ param_values_by_path,
+ )
+ sw_version = _get_param_value_from_path_suffix(
+ 'DeviceInfo.SoftwareVersion',
+ path_list,
+ param_values_by_path,
+ )
+ return get_device_name(device_oui, sw_version)
+
+
+def does_inform_have_event(
+ inform: models.Inform,
+ event_code: str,
+) -> bool:
+ """ True if the Inform message contains the specified event code """
+ for event in inform.Event.EventStruct:
+ if event.EventCode == event_code:
+ return True
+ return False
+
+
+def _get_param_values_by_path(
+ inform: models.Inform,
+) -> Dict[str, Any]:
+ if not hasattr(inform, 'ParameterList') or \
+ not hasattr(inform.ParameterList, 'ParameterValueStruct'):
+ raise ConfigurationError('Did not receive ParamterList in Inform')
+ param_values_by_path = {}
+ for param_value in inform.ParameterList.ParameterValueStruct:
+ path = param_value.Name
+ value = param_value.Value.Data
+ logger.debug(
+ '(Inform msg) Received parameter: %s = %s', path,
+ value,
+ )
+ param_values_by_path[path] = value
+ return param_values_by_path
+
+
+def are_tr069_params_equal(param_a: Any, param_b: Any, type_: str) -> bool:
+ """
+ Compare two parameters in TR-069 format.
+ The following differences are ignored:
+ - Leading and trailing whitespace, commas and quotes
+ - Capitalization, for booleans (true, false)
+ Returns:
+ True if params are the same
+ """
+ # Cast booleans to integers
+ cmp_a, cmp_b = param_a, param_b
+ if type_ == 'boolean' and cmp_b in ('0', '1') or cmp_a in ('0', '1'):
+ cmp_a, cmp_b = map(int, (cmp_a, cmp_b))
+ cmp_a, cmp_b = map(str, (cmp_a, cmp_b))
+ cmp_a, cmp_b = map(lambda s: s.strip(', \'"'), (cmp_a, cmp_b))
+ if cmp_a.lower() in ['true', 'false']:
+ cmp_a, cmp_b = map(lambda s: s.lower(), (cmp_a, cmp_b))
+ return cmp_a == cmp_b
+
+
+def get_all_objects_to_add(
+ desired_cfg: EnodebConfiguration,
+ device_cfg: EnodebConfiguration,
+) -> List[ParameterName]:
+ """
+ Find a ParameterName that needs to be added to the eNB configuration,
+ if any
+
+ Note: This is the expected name of the parameter once it is added
+ but this is different than how to add it. For example,
+ enumerated objects of the form XX.YY.N. should be added
+ by calling AddObject to XX.YY. and having the CPE assign
+ the index.
+ """
+ desired = desired_cfg.get_object_names()
+ current = device_cfg.get_object_names()
+ return list(set(desired).difference(set(current)))
+
+
+def get_all_objects_to_delete(
+ desired_cfg: EnodebConfiguration,
+ device_cfg: EnodebConfiguration,
+) -> List[ParameterName]:
+ """
+ Find a ParameterName that needs to be deleted from the eNB configuration,
+ if any
+ """
+ desired = desired_cfg.get_object_names()
+ current = device_cfg.get_object_names()
+ return list(set(current).difference(set(desired)))
+
+
+def get_params_to_get(
+ device_cfg: EnodebConfiguration,
+ data_model: DataModel,
+ request_all_params: bool = False,
+) -> List[ParameterName]:
+ """
+ Returns the names of params not belonging to objects that are added/removed
+ """
+ desired_names = data_model.get_present_params()
+ if request_all_params:
+ return desired_names
+ known_names = device_cfg.get_parameter_names()
+ names = list(set(desired_names) - set(known_names))
+ return names
+
+
+def get_object_params_to_get(
+ desired_cfg: Optional[EnodebConfiguration],
+ device_cfg: EnodebConfiguration,
+ data_model: DataModel,
+) -> List[ParameterName]:
+ """
+ Returns a list of parameter names for object parameters we don't know the
+ current value of
+ """
+ names = []
+ # TODO: This might a string for some strange reason, investigate why
+ num_plmns = \
+ int(device_cfg.get_parameter(ParameterName.NUM_PLMNS))
+ for i in range(1, num_plmns + 1):
+ obj_name = ParameterName.PLMN_N % i
+ if not device_cfg.has_object(obj_name):
+ device_cfg.add_object(obj_name)
+ obj_to_params = data_model.get_numbered_param_names()
+ desired = obj_to_params[obj_name]
+ current = []
+ if desired_cfg is not None:
+ current = desired_cfg.get_parameter_names_for_object(obj_name)
+ names_to_add = list(set(desired) - set(current))
+ names = names + names_to_add
+ return names
+
+
+# We don't attempt to set these parameters on the eNB configuration
+READ_ONLY_PARAMETERS = [
+ ParameterName.OP_STATE,
+ ParameterName.RF_TX_STATUS,
+ ParameterName.GPS_STATUS,
+ ParameterName.PTP_STATUS,
+ ParameterName.MME_STATUS,
+ ParameterName.GPS_LAT,
+ ParameterName.GPS_LONG,
+]
+
+
+def get_param_values_to_set(
+ desired_cfg: EnodebConfiguration,
+ device_cfg: EnodebConfiguration,
+ data_model: DataModel,
+ exclude_admin: bool = False,
+) -> Dict[ParameterName, Any]:
+ """
+ Get a map of param names to values for parameters that we will
+ set on the eNB's configuration, excluding parameters for objects that can
+ be added/removed.
+
+ Also exclude special parameters like admin state, since it may be set at
+ a different time in the provisioning process than most parameters.
+ """
+ param_values = {}
+ # Get the parameters we might set
+ params = set(desired_cfg.get_parameter_names()) - set(READ_ONLY_PARAMETERS)
+ if exclude_admin:
+ params = set(params) - {ParameterName.ADMIN_STATE}
+ # Values of parameters
+ for name in params:
+ new = desired_cfg.get_parameter(name)
+ old = device_cfg.get_parameter(name)
+ _type = data_model.get_parameter(name).type
+ if not are_tr069_params_equal(new, old, _type):
+ param_values[name] = new
+
+ return param_values
+
+
+def get_obj_param_values_to_set(
+ desired_cfg: EnodebConfiguration,
+ device_cfg: EnodebConfiguration,
+ data_model: DataModel,
+) -> Dict[ParameterName, Dict[ParameterName, Any]]:
+ """ Returns a map from object name to (a map of param name to value) """
+ param_values = {}
+ objs = desired_cfg.get_object_names()
+ for obj_name in objs:
+ param_values[obj_name] = {}
+ params = desired_cfg.get_parameter_names_for_object(obj_name)
+ for name in params:
+ new = desired_cfg.get_parameter_for_object(name, obj_name)
+ old = device_cfg.get_parameter_for_object(name, obj_name)
+ _type = data_model.get_parameter(name).type
+ if not are_tr069_params_equal(new, old, _type):
+ param_values[obj_name][name] = new
+ return param_values
+
+
+def get_all_param_values_to_set(
+ desired_cfg: EnodebConfiguration,
+ device_cfg: EnodebConfiguration,
+ data_model: DataModel,
+ exclude_admin: bool = False,
+) -> Dict[ParameterName, Any]:
+ """ Returns a map of param names to values that we need to set """
+ param_values = get_param_values_to_set(
+ desired_cfg, device_cfg,
+ data_model, exclude_admin,
+ )
+ obj_param_values = get_obj_param_values_to_set(
+ desired_cfg, device_cfg,
+ data_model,
+ )
+ for _obj_name, param_map in obj_param_values.items():
+ for name, val in param_map.items():
+ param_values[name] = val
+ return param_values
+
+
+def parse_get_parameter_values_response(
+ data_model: DataModel,
+ message: models.GetParameterValuesResponse,
+) -> Dict[ParameterName, Any]:
+ """ Returns a map of ParameterName to the value read from the response """
+ param_values_by_path = {}
+ for param_value_struct in message.ParameterList.ParameterValueStruct:
+ param_values_by_path[param_value_struct.Name] = \
+ param_value_struct.Value.Data
+
+ param_name_list = data_model.get_parameter_names()
+ name_to_val = {}
+ for name in param_name_list:
+ path = data_model.get_parameter(name).path
+ if path in param_values_by_path:
+ value = param_values_by_path[path]
+ name_to_val[name] = value
+
+ return name_to_val
+
+
+def get_optional_param_to_check(
+ data_model: DataModel,
+) -> Optional[ParameterName]:
+ """
+ If there is a parameter which is optional in the data model, and we do not
+ know if it exists or not, then return it so we can check for its presence.
+ """
+ params = data_model.get_names_of_optional_params()
+ for param in params:
+ try:
+ data_model.is_parameter_present(param)
+ except KeyError:
+ return param
+ return None
diff --git a/state_machines/enb_acs.py b/state_machines/enb_acs.py
new file mode 100644
index 0000000..24e24e7
--- /dev/null
+++ b/state_machines/enb_acs.py
@@ -0,0 +1,220 @@
+"""
+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 asyncio import BaseEventLoop
+from time import time
+from typing import Any, Type
+
+from common.service import MagmaService
+from data_models.data_model import DataModel
+from data_models.data_model_parameters import ParameterName
+from device_config.enodeb_config_postprocessor import (
+ EnodebConfigurationPostProcessor,
+)
+from device_config.enodeb_configuration import EnodebConfiguration
+from devices.device_utils import EnodebDeviceName
+from state_machines.acs_state_utils import are_tr069_params_equal
+
+
+class EnodebAcsStateMachine(ABC):
+ """
+ Handles all TR-069 messages.
+ Acts as the Auto Configuration Server (ACS), as specified by TR-069.
+ A device/version specific ACS message handler.
+ Different devices have various idiosyncrasies.
+ Subclass BasicEnodebAcsStateMachine for a specific device/version
+ implementation.
+
+ This ACS class can only handle a single connected eNodeB device.
+ Multiple connected eNodeB devices will lead to undefined behavior.
+
+ This ABC is more of an interface definition.
+ """
+
+ def __init__(self, use_param_key: bool = False) -> None:
+ self._service = None
+ self._desired_cfg = None
+ self._device_cfg = None
+ self._data_model = None
+ self._are_invasive_changes_applied = True
+ # Flag to preseve backwards compatibility
+ self._use_param_key = use_param_key
+ self._param_version_key = None
+
+ def has_parameter(self, param: ParameterName) -> bool:
+ """
+ Return True if the data model has the parameter
+
+ Raise KeyError if the parameter is optional and we do not know yet
+ if this eNodeB has the parameter
+ """
+ return self.data_model.is_parameter_present(param)
+
+ def get_parameter(self, param: ParameterName) -> Any:
+ """
+ Return the value of the parameter
+ """
+ return self.device_cfg.get_parameter(param)
+
+ def set_parameter_asap(self, param: ParameterName, value: Any) -> None:
+ """
+ Set the parameter to the suggested value ASAP
+ """
+ self.desired_cfg.set_parameter(param, value)
+
+ def is_enodeb_configured(self) -> bool:
+ """
+ True if the desired configuration matches the device configuration
+ """
+ if self.desired_cfg is None:
+ return False
+ if not self.data_model.are_param_presences_known():
+ return False
+ desired = self.desired_cfg.get_parameter_names()
+
+ for name in desired:
+ val1 = self.desired_cfg.get_parameter(name)
+ val2 = self.device_cfg.get_parameter(name)
+ type_ = self.data_model.get_parameter(name).type
+ if not are_tr069_params_equal(val1, val2, type_):
+ return False
+
+ for obj_name in self.desired_cfg.get_object_names():
+ params = self.desired_cfg.get_parameter_names_for_object(obj_name)
+ for name in params:
+ val1 = self.device_cfg.get_parameter_for_object(name, obj_name)
+ val2 = self.desired_cfg.get_parameter_for_object(
+ name,
+ obj_name,
+ )
+ type_ = self.data_model.get_parameter(name).type
+ if not are_tr069_params_equal(val1, val2, type_):
+ return False
+ return True
+
+ @abstractmethod
+ def get_state(self) -> str:
+ """
+ Get info about the state of the ACS
+ """
+ pass
+
+ @abstractmethod
+ def handle_tr069_message(self, message: Any) -> Any:
+ """
+ Given a TR-069 message sent from the hardware, return an
+ appropriate response
+ """
+ pass
+
+ @abstractmethod
+ def transition(self, next_state: str) -> None:
+ pass
+
+ @property
+ def service(self) -> MagmaService:
+ return self._service
+
+ @service.setter
+ def service(self, service: MagmaService) -> None:
+ self._service = service
+
+ @property
+ def event_loop(self) -> BaseEventLoop:
+ return self._service.loop
+
+ @property
+ def mconfig(self) -> Any:
+ return self._service.mconfig
+
+ @property
+ def service_config(self) -> Any:
+ return self._service.config
+
+ @property
+ def desired_cfg(self) -> EnodebConfiguration:
+ return self._desired_cfg
+
+ @desired_cfg.setter
+ def desired_cfg(self, val: EnodebConfiguration) -> None:
+ if self.has_version_key:
+ self.parameter_version_inc()
+ self._desired_cfg = val
+
+ @property
+ def device_cfg(self) -> EnodebConfiguration:
+ return self._device_cfg
+
+ @device_cfg.setter
+ def device_cfg(self, val: EnodebConfiguration) -> None:
+ self._device_cfg = val
+
+ @property
+ def data_model(self) -> DataModel:
+ return self._data_model
+
+ @property
+ def has_version_key(self) -> bool:
+ """ Return if the ACS supports param version key """
+ return self._use_param_key
+
+ @property
+ def parameter_version_key(self) -> int:
+ """ Return the param version key """
+ return self._param_version_key
+
+ def parameter_version_inc(self):
+ """ Set the internal version key to the timestamp """
+ self._param_version_key = time()
+
+ @data_model.setter
+ def data_model(self, data_model) -> None:
+ self._data_model = data_model
+
+ @property
+ def are_invasive_changes_applied(self) -> bool:
+ return self._are_invasive_changes_applied
+
+ @are_invasive_changes_applied.setter
+ def are_invasive_changes_applied(self, is_applied: bool) -> None:
+ self._are_invasive_changes_applied = is_applied
+
+ @property
+ @abstractmethod
+ def data_model_class(self) -> Type[DataModel]:
+ pass
+
+ @property
+ @abstractmethod
+ def device_name(self) -> EnodebDeviceName:
+ pass
+
+ @property
+ @abstractmethod
+ def config_postprocessor(self) -> EnodebConfigurationPostProcessor:
+ pass
+
+ @abstractmethod
+ def reboot_asap(self) -> None:
+ """
+ Send a request to reboot the eNodeB ASAP
+ """
+ pass
+
+ @abstractmethod
+ def is_enodeb_connected(self) -> bool:
+ pass
+
+ @abstractmethod
+ def stop_state_machine(self) -> None:
+ pass
diff --git a/state_machines/enb_acs_impl.py b/state_machines/enb_acs_impl.py
new file mode 100644
index 0000000..c29690e
--- /dev/null
+++ b/state_machines/enb_acs_impl.py
@@ -0,0 +1,299 @@
+"""
+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.
+"""
+
+import traceback
+from abc import abstractmethod
+from typing import Any, Dict
+
+from common.service import MagmaService
+import metrics
+from data_models.data_model_parameters import ParameterName
+from device_config.enodeb_configuration import EnodebConfiguration
+from exceptions import ConfigurationError
+from logger import EnodebdLogger as logger
+from state_machines.enb_acs import EnodebAcsStateMachine
+from state_machines.enb_acs_states import EnodebAcsState
+from state_machines.timer import StateMachineTimer
+from tr069 import models
+from tr069.models import Tr069ComplexModel
+
+
+class BasicEnodebAcsStateMachine(EnodebAcsStateMachine):
+ """
+ Most of the EnodebAcsStateMachine classes for each device work about the
+ same way. Differences lie mainly in the data model, desired configuration,
+ and the state transition map.
+
+ This class specifies the shared implementation between them.
+ """
+
+ # eNodeB connection timeout is used to determine whether or not eNodeB is
+ # connected to enodebd based on time of last Inform message. By default,
+ # periodic inform interval is 30secs, so timeout should be larger than
+ # this.
+ # Also set timer longer than reboot time, so that an eNodeB reboot does not
+ # trigger a connection-timeout alarm.
+ ENB_CONNECTION_TIMEOUT = 600 # In seconds
+
+ # If eNodeB is disconnected from MME for an unknown reason for this time,
+ # then reboot it. Set to a long time to ensure this doesn't interfere with
+ # other enodebd configuration processes - it is just a measure of last
+ # resort for an unlikely error case
+ MME_DISCONNECT_ENODEB_REBOOT_TIMER = 15 * 60
+
+ # Check the MME connection status every 15 seconds
+ MME_CHECK_TIMER = 15
+
+ def __init__(
+ self,
+ service: MagmaService,
+ use_param_key: bool,
+ ) -> None:
+ super().__init__(use_param_key=use_param_key)
+ self.state = None
+ self.timeout_handler = None
+ self.mme_timeout_handler = None
+ self.mme_timer = None
+ self._start_state_machine(service)
+
+ def get_state(self) -> str:
+ if self.state is None:
+ logger.warning('ACS State machine is not in any state.')
+ return 'N/A'
+ return self.state.state_description()
+
+ def handle_tr069_message(
+ self,
+ message: Tr069ComplexModel,
+ ) -> Tr069ComplexModel:
+ """
+ Accept the tr069 message from the eNB and produce a reply.
+
+ States may transition after reading a message but BEFORE producing
+ a reply. Most steps in the provisioning process are represented as
+ beginning with enodebd sending a request to the eNB, and waiting for
+ the reply from the eNB.
+ """
+ # TransferComplete messages come at random times, and we ignore them
+ if isinstance(message, models.TransferComplete):
+ return models.TransferCompleteResponse()
+ try:
+ self._read_tr069_msg(message)
+ return self._get_tr069_msg(message)
+ except Exception: # pylint: disable=broad-except
+ logger.error('Failed to handle tr069 message')
+ logger.error(traceback.format_exc())
+ self._dump_debug_info()
+ self.transition(self.unexpected_fault_state_name)
+ return self._get_tr069_msg(message)
+
+ def transition(self, next_state: str) -> Any:
+ logger.debug('State transition to <%s>', next_state)
+ self.state.exit()
+ self.state = self.state_map[next_state]
+ self.state.enter()
+
+ def stop_state_machine(self) -> None:
+ """ Clean up anything the state machine is tracking or doing """
+ self.state.exit()
+ if self.timeout_handler is not None:
+ self.timeout_handler.cancel()
+ self.timeout_handler = None
+ if self.mme_timeout_handler is not None:
+ self.mme_timeout_handler.cancel()
+ self.mme_timeout_handler = None
+ self._service = None
+ self._desired_cfg = None
+ self._device_cfg = None
+ self._data_model = None
+
+ self.mme_timer = None
+
+ def _start_state_machine(
+ self,
+ service: MagmaService,
+ ):
+ self.service = service
+ self.data_model = self.data_model_class()
+ # The current known device config has few known parameters
+ # The desired configuration depends on what the current configuration
+ # is. This we don't know fully, yet.
+ self.device_cfg = EnodebConfiguration(self.data_model)
+
+ self._init_state_map()
+ self.state = self.state_map[self.disconnected_state_name]
+ self.state.enter()
+ self._reset_timeout()
+ self._periodic_check_mme_connection()
+
+ def _reset_state_machine(
+ self,
+ service: MagmaService,
+ ):
+ self.stop_state_machine()
+ self._start_state_machine(service)
+
+ def _read_tr069_msg(self, message: Any) -> None:
+ """ Process incoming message and maybe transition state """
+ self._reset_timeout()
+ msg_handled, next_state = self.state.read_msg(message)
+ if not msg_handled:
+ self._transition_for_unexpected_msg(message)
+ _msg_handled, next_state = self.state.read_msg(message)
+ if next_state is not None:
+ self.transition(next_state)
+
+ def _get_tr069_msg(self, message: Any) -> Any:
+ """ Get a new message to send, and maybe transition state """
+ msg_and_transition = self.state.get_msg(message)
+ if msg_and_transition.next_state:
+ self.transition(msg_and_transition.next_state)
+ msg = msg_and_transition.msg
+ return msg
+
+ def _transition_for_unexpected_msg(self, message: Any) -> None:
+ """
+ eNB devices may send an Inform message in the middle of a provisioning
+ session. To deal with this, transition to a state that expects an
+ Inform message, but also track the status of the eNB as not having
+ been disconnected.
+ """
+ if isinstance(message, models.Inform):
+ logger.debug(
+ 'ACS in (%s) state. Received an Inform message',
+ self.state.state_description(),
+ )
+ self._reset_state_machine(self.service)
+ elif isinstance(message, models.Fault):
+ logger.debug(
+ 'ACS in (%s) state. Received a Fault <%s>',
+ self.state.state_description(), message.FaultString,
+ )
+ self.transition(self.unexpected_fault_state_name)
+ else:
+ raise ConfigurationError('Cannot handle unexpected TR069 msg')
+
+ def _reset_timeout(self) -> None:
+ if self.timeout_handler is not None:
+ self.timeout_handler.cancel()
+
+ def timed_out():
+ self.transition(self.disconnected_state_name)
+
+ self.timeout_handler = self.event_loop.call_later(
+ self.ENB_CONNECTION_TIMEOUT,
+ timed_out,
+ )
+
+ def _periodic_check_mme_connection(self) -> None:
+ self._check_mme_connection()
+ self.mme_timeout_handler = self.event_loop.call_later(
+ self.MME_CHECK_TIMER,
+ self._periodic_check_mme_connection,
+ )
+
+ def _check_mme_connection(self) -> None:
+ """
+ Check if eNodeB should be connected to MME but isn't, and maybe reboot.
+
+ If the eNB doesn't report connection to MME within a timeout period,
+ get it to reboot in the hope that it will fix things.
+
+ Usually, enodebd polls the eNodeB for whether it is connected to MME.
+ This method checks the last polled MME connection status, and if
+ eNodeB should be connected to MME but it isn't.
+ """
+ if self.device_cfg.has_parameter(ParameterName.MME_STATUS) and \
+ self.device_cfg.get_parameter(ParameterName.MME_STATUS):
+ is_mme_connected = 1
+ else:
+ is_mme_connected = 0
+
+ # True if we would expect MME to be connected, but it isn't
+ is_mme_unexpectedly_dc = \
+ self.is_enodeb_connected() \
+ and self.is_enodeb_configured() \
+ and self.mconfig.allow_enodeb_transmit \
+ and not is_mme_connected
+
+ if is_mme_unexpectedly_dc:
+ logger.warning(
+ 'eNodeB is connected to AGw, is configured, '
+ 'and has AdminState enabled for transmit. '
+ 'MME connection to eNB is missing.',
+ )
+ if self.mme_timer is None:
+ logger.warning(
+ 'eNodeB will be rebooted if MME connection '
+ 'is not established in: %s seconds.',
+ self.MME_DISCONNECT_ENODEB_REBOOT_TIMER,
+ )
+ metrics.STAT_ENODEB_REBOOT_TIMER_ACTIVE.set(1)
+ self.mme_timer = \
+ StateMachineTimer(self.MME_DISCONNECT_ENODEB_REBOOT_TIMER)
+ elif self.mme_timer.is_done():
+ logger.warning(
+ 'eNodeB has not established MME connection '
+ 'within %s seconds - rebooting!',
+ self.MME_DISCONNECT_ENODEB_REBOOT_TIMER,
+ )
+ metrics.STAT_ENODEB_REBOOTS.labels(cause='MME disconnect').inc()
+ metrics.STAT_ENODEB_REBOOT_TIMER_ACTIVE.set(0)
+ self.mme_timer = None
+ self.reboot_asap()
+ else:
+ # eNB is not connected to MME, but we're still waiting to see
+ # if it will connect within the timeout period.
+ # Take no action for now.
+ pass
+ else:
+ if self.mme_timer is not None:
+ logger.info('eNodeB has established MME connection.')
+ self.mme_timer = None
+ metrics.STAT_ENODEB_REBOOT_TIMER_ACTIVE.set(0)
+
+ def _dump_debug_info(self) -> None:
+ if self.device_cfg is not None:
+ logger.error(
+ 'Device configuration: %s',
+ self.device_cfg.get_debug_info(),
+ )
+ else:
+ logger.error('Device configuration: None')
+ if self.desired_cfg is not None:
+ logger.error(
+ 'Desired configuration: %s',
+ self.desired_cfg.get_debug_info(),
+ )
+ else:
+ logger.error('Desired configuration: None')
+
+ @abstractmethod
+ def _init_state_map(self) -> None:
+ pass
+
+ @property
+ @abstractmethod
+ def state_map(self) -> Dict[str, EnodebAcsState]:
+ pass
+
+ @property
+ @abstractmethod
+ def disconnected_state_name(self) -> str:
+ pass
+
+ @property
+ @abstractmethod
+ def unexpected_fault_state_name(self) -> str:
+ """ State to handle unexpected Fault messages """
+ pass
diff --git a/state_machines/enb_acs_manager.py b/state_machines/enb_acs_manager.py
new file mode 100644
index 0000000..7ad6b02
--- /dev/null
+++ b/state_machines/enb_acs_manager.py
@@ -0,0 +1,253 @@
+"""
+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 typing import Any, List, Optional
+
+from common.service import MagmaService
+from device_config.configuration_util import is_enb_registered
+from devices.device_map import get_device_handler_from_name
+from devices.device_utils import EnodebDeviceName
+from exceptions import UnrecognizedEnodebError
+from logger import EnodebdLogger as logger
+from state_machines.acs_state_utils import (
+ get_device_name_from_inform,
+)
+from state_machines.enb_acs import EnodebAcsStateMachine
+from tr069 import models
+from spyne import ComplexModelBase
+from spyne.server.wsgi import WsgiMethodContext
+
+
+class StateMachineManager:
+ """
+ Delegates tr069 message handling to a dedicated state machine for the
+ device.
+ """
+
+ def __init__(
+ self,
+ service: MagmaService,
+ ):
+ self._ip_serial_mapping = IpToSerialMapping()
+ self._service = service
+ self._state_machine_by_ip = {}
+
+ def handle_tr069_message(
+ self,
+ ctx: WsgiMethodContext,
+ tr069_message: ComplexModelBase,
+ ) -> Any:
+ """ Delegate message handling to the appropriate eNB state machine """
+ client_ip = self._get_client_ip(ctx)
+ if isinstance(tr069_message, models.Inform):
+ try:
+ self._update_device_mapping(client_ip, tr069_message)
+ except UnrecognizedEnodebError as err:
+ logger.warning(
+ 'Received TR-069 Inform message from an '
+ 'unrecognized device. '
+ 'Ending TR-069 session with empty HTTP '
+ 'response. Error: (%s)', err,
+ )
+ return models.DummyInput()
+
+ handler = self._get_handler(client_ip)
+ if handler is None:
+ logger.warning(
+ 'Received non-Inform TR-069 message from unknown '
+ 'eNB. Ending session with empty HTTP response.',
+ )
+ return models.DummyInput()
+
+ return handler.handle_tr069_message(tr069_message)
+
+ def get_handler_by_ip(self, client_ip: str) -> EnodebAcsStateMachine:
+ return self._state_machine_by_ip[client_ip]
+
+ def get_handler_by_serial(self, enb_serial: str) -> EnodebAcsStateMachine:
+ client_ip = self._ip_serial_mapping.get_ip(enb_serial)
+ return self._state_machine_by_ip[client_ip]
+
+ def get_connected_serial_id_list(self) -> List[str]:
+ return self._ip_serial_mapping.get_serial_list()
+
+ def get_ip_of_serial(self, enb_serial: str) -> str:
+ return self._ip_serial_mapping.get_ip(enb_serial)
+
+ def get_serial_of_ip(self, client_ip: str) -> str:
+ serial = self._ip_serial_mapping.get_serial(client_ip)
+ return serial or 'default'
+
+ def _get_handler(
+ self,
+ client_ip: str,
+ ) -> EnodebAcsStateMachine:
+ return self._state_machine_by_ip[client_ip]
+
+ def _update_device_mapping(
+ self,
+ client_ip: str,
+ inform: models.Inform,
+ ) -> None:
+ """
+ When receiving an Inform message, we can figure out what device we
+ are talking to. We can also see if the IP has changed, and the
+ StateMachineManager must track this so that subsequent tr069
+ messages can be handled correctly.
+ """
+ enb_serial = self._parse_msg_for_serial(inform)
+ if enb_serial is None:
+ raise UnrecognizedEnodebError(
+ 'eNB does not have serial number '
+ 'under expected param path',
+ )
+ if not is_enb_registered(self._service.mconfig, enb_serial):
+ raise UnrecognizedEnodebError(
+ 'eNB not registered to this Access '
+ 'Gateway (serial #%s)' % enb_serial,
+ )
+ self._associate_serial_to_ip(client_ip, enb_serial)
+ handler = self._get_handler(client_ip)
+ if handler is None:
+ device_name = get_device_name_from_inform(inform)
+ handler = self._build_handler(device_name)
+ self._state_machine_by_ip[client_ip] = handler
+
+ def _associate_serial_to_ip(
+ self,
+ client_ip: str,
+ enb_serial: str,
+ ) -> None:
+ """
+ If a device/IP combination changes, then the StateMachineManager
+ must detect this, and update its mapping of what serial/IP corresponds
+ to which handler.
+ """
+ if self._ip_serial_mapping.has_ip(client_ip):
+ # Same IP, different eNB connected
+ prev_serial = self._ip_serial_mapping.get_serial(client_ip)
+ if enb_serial != prev_serial:
+ logger.info(
+ 'eNodeB change on IP <%s>, from %s to %s',
+ client_ip, prev_serial, enb_serial,
+ )
+ self._ip_serial_mapping.set_ip_and_serial(client_ip, enb_serial)
+ self._state_machine_by_ip[client_ip] = None
+ elif self._ip_serial_mapping.has_serial(enb_serial):
+ # Same eNB, different IP
+ prev_ip = self._ip_serial_mapping.get_ip(enb_serial)
+ if client_ip != prev_ip:
+ logger.info(
+ 'eNodeB <%s> changed IP from %s to %s',
+ enb_serial, prev_ip, client_ip,
+ )
+ self._ip_serial_mapping.set_ip_and_serial(client_ip, enb_serial)
+ handler = self._state_machine_by_ip[prev_ip]
+ self._state_machine_by_ip[client_ip] = handler
+ del self._state_machine_by_ip[prev_ip]
+ else:
+ # TR069 message is coming from a different IP, and a different
+ # serial ID. No need to change mapping
+ handler = None
+ self._ip_serial_mapping.set_ip_and_serial(client_ip, enb_serial)
+ self._state_machine_by_ip[client_ip] = handler
+
+ @staticmethod
+ def _parse_msg_for_serial(tr069_message: models.Inform) -> Optional[str]:
+ """ Return the eNodeB serial ID if it's found in the message """
+ if not isinstance(tr069_message, models.Inform):
+ return
+
+ # Mikrotik Intercell does not return serial in ParameterList
+ if hasattr(tr069_message, 'DeviceId') and \
+ hasattr(tr069_message.DeviceId, 'SerialNumber'):
+ return tr069_message.DeviceId.SerialNumber
+
+ if not hasattr(tr069_message, 'ParameterList') or \
+ not hasattr(tr069_message.ParameterList, 'ParameterValueStruct'):
+ return None
+
+ # Parse the parameters
+ param_values_by_path = {}
+ for param_value in tr069_message.ParameterList.ParameterValueStruct:
+ path = param_value.Name
+ value = param_value.Value.Data
+ param_values_by_path[path] = value
+
+ possible_sn_paths = [
+ 'Device.DeviceInfo.SerialNumber',
+ 'InternetGatewayDevice.DeviceInfo.SerialNumber',
+ ]
+ for path in possible_sn_paths:
+ if path in param_values_by_path:
+ return param_values_by_path[path]
+ return None
+
+ @staticmethod
+ def _get_client_ip(ctx: WsgiMethodContext) -> str:
+ return ctx.transport.req_env.get("REMOTE_ADDR", "unknown")
+
+ def _build_handler(
+ self,
+ device_name: EnodebDeviceName,
+ ) -> EnodebAcsStateMachine:
+ """
+ Create a new state machine based on the device type
+ """
+ device_handler_class = get_device_handler_from_name(device_name)
+ acs_state_machine = device_handler_class(self._service)
+ return acs_state_machine
+
+
+class IpToSerialMapping:
+ """ Bidirectional map between <eNodeB IP> and <eNodeB serial ID> """
+
+ def __init__(self) -> None:
+ self.ip_by_enb_serial = {}
+ self.enb_serial_by_ip = {}
+
+ def del_ip(self, ip: str) -> None:
+ if ip not in self.enb_serial_by_ip:
+ raise KeyError('Cannot delete missing IP')
+ serial = self.enb_serial_by_ip[ip]
+ del self.enb_serial_by_ip[ip]
+ del self.ip_by_enb_serial[serial]
+
+ def del_serial(self, serial: str) -> None:
+ if serial not in self.ip_by_enb_serial:
+ raise KeyError('Cannot delete missing eNodeB serial ID')
+ ip = self.ip_by_enb_serial[serial]
+ del self.ip_by_enb_serial[serial]
+ del self.enb_serial_by_ip[ip]
+
+ def set_ip_and_serial(self, ip: str, serial: str) -> None:
+ self.ip_by_enb_serial[serial] = ip
+ self.enb_serial_by_ip[ip] = serial
+
+ def get_ip(self, serial: str) -> str:
+ return self.ip_by_enb_serial[serial]
+
+ def get_serial(self, ip: str) -> Optional[str]:
+ return self.enb_serial_by_ip.get(ip, None)
+
+ def has_ip(self, ip: str) -> bool:
+ return ip in self.enb_serial_by_ip
+
+ def has_serial(self, serial: str) -> bool:
+ return serial in self.ip_by_enb_serial
+
+ def get_serial_list(self) -> List[str]:
+ return list(self.ip_by_enb_serial.keys())
+
+ def get_ip_list(self) -> List[str]:
+ return list(self.enb_serial_by_ip.keys())
diff --git a/state_machines/enb_acs_pointer.py b/state_machines/enb_acs_pointer.py
new file mode 100644
index 0000000..3ec19a7
--- /dev/null
+++ b/state_machines/enb_acs_pointer.py
@@ -0,0 +1,38 @@
+"""
+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 state_machines.enb_acs import EnodebAcsStateMachine
+
+
+class StateMachinePointer:
+ """
+ This is a hack to deal with the possibility that the specified data model
+ doesn't match the eNB device enodebd ends up connecting to.
+
+ When the data model doesn't match, the state machine is replaced with one
+ that matches the data model.
+ """
+
+ def __init__(self, acs_state_machine: EnodebAcsStateMachine):
+ self._acs_state_machine = acs_state_machine
+
+ @property
+ def state_machine(self):
+ return self._acs_state_machine
+
+ @state_machine.setter
+ def state_machine(
+ self,
+ acs_state_machine: EnodebAcsStateMachine,
+ ) -> None:
+ self._acs_state_machine = acs_state_machine
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'
diff --git a/state_machines/timer.py b/state_machines/timer.py
new file mode 100644
index 0000000..09f6b68
--- /dev/null
+++ b/state_machines/timer.py
@@ -0,0 +1,33 @@
+"""
+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 datetime import datetime, timedelta
+
+
+class StateMachineTimer():
+ def __init__(self, seconds_remaining: int) -> None:
+ self.start_time = datetime.now()
+ self.seconds = seconds_remaining
+
+ def is_done(self) -> bool:
+ time_elapsed = datetime.now() - self.start_time
+ if time_elapsed > timedelta(seconds=self.seconds):
+ return True
+ return False
+
+ def seconds_elapsed(self) -> int:
+ time_elapsed = datetime.now() - self.start_time
+ return int(time_elapsed.total_seconds())
+
+ def seconds_remaining(self) -> int:
+ return max(0, self.seconds - self.seconds_elapsed())