Init commit for standalone enodebd
Change-Id: I88eeef5135dd7ba8551ddd9fb6a0695f5325337b
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