blob: 2a7367f21f02eda3d1dee7b74248b90e9ef82701 [file] [log] [blame]
"""
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