def _set_tdd_subframe_config( device_cfg: EnodebConfiguration, cfg: EnodebConfiguration, subframe_assignment: Any, special_subframe_pattern: Any, ) -> None: """ Set the following parameters: - Subframe assignment - Special subframe pattern """ # Don't try to set if this is not TDD mode if (device_cfg.has_parameter(ParameterName.DUPLEX_MODE_CAPABILITY) and device_cfg.get_parameter( ParameterName.DUPLEX_MODE_CAPABILITY) != 'TDDMode'): return config_assert( subframe_assignment in range(0, 6 + 1), 'Invalid TDD subframe assignment (%d)' % subframe_assignment, ) config_assert( special_subframe_pattern in range(0, 9 + 1), 'Invalid TDD special subframe pattern (%d)' % special_subframe_pattern, ) cfg.set_parameter( ParameterName.SUBFRAME_ASSIGNMENT, subframe_assignment, ) cfg.set_parameter( ParameterName.SPECIAL_SUBFRAME_PATTERN, special_subframe_pattern, )
def postprocess(self, mconfig: Any, service_cfg: Any, desired_cfg: EnodebConfiguration) -> None: """ Add some params to the desired config Args: mconfig (Any): mconfig service_cfg (Any): service config desired_cfg (EnodebConfiguration): desired config """ desired_cfg.set_parameter(ParameterName.SAS_ENABLED, 1) desired_cfg.set_parameter_for_object( ParameterName.PLMN_N_CELL_RESERVED % 1, True, # noqa: WPS345,WPS425 ParameterName.PLMN_N % 1, # noqa: WPS345 ) parameters_to_delete = [ ParameterName.RADIO_ENABLE, ParameterName.POWER_SPECTRAL_DENSITY, ParameterName.EARFCNDL, ParameterName.EARFCNUL, ParameterName.BAND, ParameterName.DL_BANDWIDTH, ParameterName.UL_BANDWIDTH, ParameterName.SAS_RADIO_ENABLE, ] for p in parameters_to_delete: if desired_cfg.has_parameter(p): desired_cfg.delete_parameter(p)
def postprocess(self, mconfig: Any, service_cfg: Any, desired_cfg: EnodebConfiguration) -> None: """ Add some params to the desired config Args: mconfig (Any): mconfig service_cfg (Any): service config desired_cfg (EnodebConfiguration): desired config """ desired_cfg.set_parameter(ParameterName.SAS_ENABLED, 1) # Set Cell reservation for both cells desired_cfg.set_parameter_for_object( ParameterName.PLMN_N_CELL_RESERVED % 1, True, # noqa: WPS345,WPS425 ParameterName.PLMN_N % 1, # noqa: WPS345 ) desired_cfg.set_parameter( CarrierAggregationParameters.CA_PLMN_CELL_RESERVED, True, ) # Make sure FAPService.1. is Primary desired_cfg.set_parameter_for_object( ParameterName.PLMN_N_PRIMARY % 1, True, # noqa: WPS345,WPS425 ParameterName.PLMN_N % 1, # noqa: WPS345 ) desired_cfg.set_parameter( CarrierAggregationParameters.CA_PLMN_PRIMARY, False, ) # Enable both cells desired_cfg.set_parameter_for_object( ParameterName.PLMN_N_ENABLE % 1, True, # noqa: WPS345,WPS425 ParameterName.PLMN_N % 1, # noqa: WPS345 ) desired_cfg.set_parameter( CarrierAggregationParameters.CA_PLMN_ENABLE, True, ) parameters_to_delete = [ ParameterName.RADIO_ENABLE, ParameterName.POWER_SPECTRAL_DENSITY, ParameterName.EARFCNDL, ParameterName.EARFCNUL, ParameterName.BAND, ParameterName.DL_BANDWIDTH, ParameterName.UL_BANDWIDTH, ParameterName.SAS_RADIO_ENABLE, ] for p in parameters_to_delete: if desired_cfg.has_parameter(p): desired_cfg.delete_parameter(p)
class EnodebConfigurationTest(TestCase): def setUp(self): self.config = EnodebConfiguration(CaviumTrDataModel) def tearDown(self): self.config = None def test_data_model(self) -> None: data_model = self.config.data_model expected = CaviumTrDataModel self.assertEqual(data_model, expected, 'Data model fetch incorrect') def test_get_has_set_parameter(self) -> None: param = ParameterName.ADMIN_STATE self.config.set_parameter(param, True) self.assertTrue(self.config.has_parameter(param), 'Expected to have parameter') param_value = self.config.get_parameter(param) expected = True self.assertEqual(param_value, expected, 'Parameter value does not match what was set') def test_add_has_delete_object(self) -> None: object_name = ParameterName.PLMN_N % 1 self.assertFalse(self.config.has_object(object_name)) self.config.add_object(object_name) self.assertTrue(self.config.has_object(object_name)) self.config.delete_object(object_name) self.assertFalse(self.config.has_object(object_name)) def test_get_parameter_names(self) -> None: # Should start off as an empty list names_list = self.config.get_parameter_names() self.assertEqual(len(names_list), 0, 'Expected 0 names') # Should grow as we set parameters self.config.set_parameter(ParameterName.ADMIN_STATE, True) names_list = self.config.get_parameter_names() self.assertEqual(len(names_list), 1, 'Expected 1 name') # Parameter names should not include objects self.config.add_object(ParameterName.PLMN) names_list = self.config.get_parameter_names() self.assertEqual(len(names_list), 1, 'Expected 1 name') def test_get_object_names(self) -> None: # Should start off as an empty list obj_list = self.config.get_object_names() self.assertEqual(len(obj_list), 0, 'Expected 0 names') # Should grow as we set parameters self.config.add_object(ParameterName.PLMN) obj_list = self.config.get_object_names() self.assertEqual(len(obj_list), 1, 'Expected 1 names') def test_get_set_parameter_for_object(self) -> None: self.config.add_object(ParameterName.PLMN_N % 1) self.config.set_parameter_for_object( ParameterName.PLMN_N_CELL_RESERVED % 1, True, ParameterName.PLMN_N % 1) param_value = self.config.get_parameter_for_object( ParameterName.PLMN_N_CELL_RESERVED % 1, ParameterName.PLMN_N % 1) self.assertTrue( param_value, 'Expected that the param for object was set correctly') def test_get_parameter_names_for_object(self) -> None: # Should start off empty self.config.add_object(ParameterName.PLMN_N % 1) param_list = self.config.get_parameter_names_for_object( ParameterName.PLMN_N % 1) self.assertEqual(len(param_list), 0, 'Should be an empty param list') # Should increment as we set parameters self.config.set_parameter_for_object( ParameterName.PLMN_N_CELL_RESERVED % 1, True, ParameterName.PLMN_N % 1) param_list = self.config.get_parameter_names_for_object( ParameterName.PLMN_N % 1) self.assertEqual(len(param_list), 1, 'Should not be an empty list')
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, ) -> None: super().__init__() 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
def set_magma_device_cfg( cls, name_to_val: Dict, device_cfg: EnodebConfiguration, ): """ Convert FreedomFiOne name_to_val representation to magma device_cfg """ success_str = "SUCCESS" # String constant returned by radio insync_str = "INSYNC" if (name_to_val.get(cls.DEFAULT_GW) and name_to_val[cls.DEFAULT_GW].upper() != success_str): # Nothing will proceed if the eNB doesn't have an IP on the WAN serial_num = "unknown" if device_cfg.has_parameter(ParameterName.SERIAL_NUMBER): serial_num = device_cfg.get_parameter( ParameterName.SERIAL_NUMBER, ) EnodebdLogger.error( "Radio with serial number %s doesn't have IP address " "on WAN", serial_num, ) device_cfg.set_parameter( param_name=ParameterName.RF_TX_STATUS, value=False, ) device_cfg.set_parameter( param_name=ParameterName.GPS_STATUS, value=False, ) device_cfg.set_parameter( param_name=ParameterName.PTP_STATUS, value=False, ) device_cfg.set_parameter( param_name=ParameterName.MME_STATUS, value=False, ) device_cfg.set_parameter( param_name=ParameterName.OP_STATE, value=False, ) return if (name_to_val.get(cls.SAS_STATUS) and name_to_val[cls.SAS_STATUS].upper() == success_str): device_cfg.set_parameter( param_name=ParameterName.RF_TX_STATUS, value=True, ) else: # No sas grant so not transmitting. There is no explicit node for Tx # in FreedomFiOne device_cfg.set_parameter( param_name=ParameterName.RF_TX_STATUS, value=False, ) if (name_to_val.get(cls.GPS_SCAN_STATUS) and name_to_val[cls.GPS_SCAN_STATUS].upper() == success_str): device_cfg.set_parameter( param_name=ParameterName.GPS_STATUS, value=True, ) # Time comes through GPS so can only be insync with GPS is # in sync, we use PTP_STATUS field to overload timer is in Sync. if (name_to_val.get(cls.SYNC_STATUS) and name_to_val[cls.SYNC_STATUS].upper() == insync_str): device_cfg.set_parameter( param_name=ParameterName.PTP_STATUS, value=True, ) else: device_cfg.set_parameter( param_name=ParameterName.PTP_STATUS, value=False, ) else: device_cfg.set_parameter( param_name=ParameterName.GPS_STATUS, value=False, ) device_cfg.set_parameter( param_name=ParameterName.PTP_STATUS, value=False, ) if (name_to_val.get(cls.DEFAULT_GW) and name_to_val[cls.DEFAULT_GW].upper() == success_str): device_cfg.set_parameter( param_name=ParameterName.MME_STATUS, value=True, ) else: device_cfg.set_parameter( param_name=ParameterName.MME_STATUS, value=False, ) if (name_to_val.get(cls.ENB_STATUS) and name_to_val[cls.ENB_STATUS].upper() == success_str): device_cfg.set_parameter( param_name=ParameterName.OP_STATE, value=True, ) else: device_cfg.set_parameter( param_name=ParameterName.OP_STATE, value=False, ) pass_through_params = [ParameterName.GPS_LAT, ParameterName.GPS_LONG] for name in pass_through_params: device_cfg.set_parameter(name, name_to_val[name])
def _set_earfcn_freq_band_mode( device_cfg: EnodebConfiguration, cfg: EnodebConfiguration, data_model: DataModel, earfcndl: int, ) -> None: """ Set the following parameters: - EARFCNDL - EARFCNUL - Band """ # Note: validation of EARFCNDL done by mapping function. If invalid # EARFCN, raise ConfigurationError try: band, duplex_mode, earfcnul = map_earfcndl_to_band_earfcnul_mode( earfcndl, ) except ValueError as err: raise ConfigurationError(err) # Verify capabilities if device_cfg.has_parameter(ParameterName.DUPLEX_MODE_CAPABILITY): duplex_capability = \ device_cfg.get_parameter(ParameterName.DUPLEX_MODE_CAPABILITY) if duplex_mode == DuplexMode.TDD and duplex_capability != 'TDDMode': raise ConfigurationError( ('eNodeB duplex mode capability is <{0}>, ' 'but earfcndl is <{1}>, giving duplex ' 'mode <{2}> instead').format( duplex_capability, str(earfcndl), str(duplex_mode), )) elif duplex_mode == DuplexMode.FDD and duplex_capability != 'FDDMode': raise ConfigurationError( ('eNodeB duplex mode capability is <{0}>, ' 'but earfcndl is <{1}>, giving duplex ' 'mode <{2}> instead').format( duplex_capability, str(earfcndl), str(duplex_mode), )) elif duplex_mode not in {DuplexMode.TDD, DuplexMode.FDD}: raise ConfigurationError( 'Invalid duplex mode (%s)' % str(duplex_mode), ) if device_cfg.has_parameter(ParameterName.BAND_CAPABILITY): # Baicells indicated that they no longer use the band capability list, # so it may not be populated correctly band_capability_list = device_cfg.get_parameter( ParameterName.BAND_CAPABILITY, ) band_capabilities = band_capability_list.split(',') if str(band) not in band_capabilities: logger.warning( 'Band %d not in capabilities list (%s). Continuing' ' with config because capabilities list may not be' ' correct', band, band_capabilities, ) cfg.set_parameter(ParameterName.EARFCNDL, earfcndl) if duplex_mode == DuplexMode.FDD: _set_param_if_present( cfg, data_model, ParameterName.EARFCNUL, earfcnul, ) else: logger.debug('Not setting EARFCNUL - duplex mode is not FDD') _set_param_if_present(cfg, data_model, ParameterName.BAND, band) if duplex_mode == DuplexMode.TDD: logger.debug('Set EARFCNDL=%d, Band=%d', earfcndl, band) elif duplex_mode == DuplexMode.FDD: logger.debug( 'Set EARFCNDL=%d, EARFCNUL=%d, Band=%d', earfcndl, earfcnul, band, )