def get_all_enb_state( print_grpc_payload: bool = False) -> Optional[Dict[int, int]]: """ Make RPC call to 'GetENBState' method of s1ap service """ try: chan = ServiceRegistry.get_rpc_channel( S1AP_SERVICE_NAME, ServiceRegistry.LOCAL, ) except ValueError: logger.error('Cant get RPC channel to %s', S1AP_SERVICE_NAME) return {} client = S1apServiceStub(chan) try: request = Void() print_grpc( request, print_grpc_payload, "Get All eNB State Request:", ) res = client.GetENBState(request, DEFAULT_GRPC_TIMEOUT) print_grpc( res, print_grpc_payload, "Get All eNB State Response:", ) return res.enb_state_map except grpc.RpcError as err: logger.warning( "GetEnbState error: [%s] %s", err.code(), err.details(), ) return {}
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 postprocess( self, mconfig: Any, service_cfg: Any, desired_cfg: EnodebConfiguration, ) -> None: # TODO: Get this config from the domain proxy # TODO @amarpad, set these when DProxy integration is done. # For now the radio will directly talk to the SAS and get these # attributes. desired_cfg.delete_parameter(ParameterName.EARFCNDL) desired_cfg.delete_parameter(ParameterName.DL_BANDWIDTH) desired_cfg.delete_parameter(ParameterName.UL_BANDWIDTH) # go through misc parameters and set them to default. for name, val in FreedomFiOneMiscParameters.defaults.items(): desired_cfg.set_parameter(name, val) # Bump up the parameter key version self.acs.parameter_version_inc() # Workaround a bug in Sercomm firmware in release 3920, 3921 # where the meaning of CellReservedForOperatorUse is wrong. # Set to True to ensure the PLMN is not reserved num_plmns = self.acs.data_model.get_num_plmns() for i in range(1, num_plmns + 1): object_name = ParameterName.PLMN_N % i desired_cfg.set_parameter_for_object( param_name=ParameterName.PLMN_N_CELL_RESERVED % i, value=True, object_name=object_name, ) if self.WEB_UI_ENABLE_LIST_KEY in service_cfg: serial_nos = service_cfg.get(self.WEB_UI_ENABLE_LIST_KEY) if self.acs.device_cfg.has_parameter( ParameterName.SERIAL_NUMBER, ): if self.acs.get_parameter(ParameterName.SERIAL_NUMBER) in \ serial_nos: desired_cfg.set_parameter( FreedomFiOneMiscParameters.WEB_UI_ENABLE, True, ) else: # This should not happen EnodebdLogger.error("Serial number unknown for device") if self.SAS_KEY not in service_cfg: return sas_cfg = service_cfg[self.SAS_KEY] sas_param_names = self.acs.data_model.get_sas_param_names() for name, val in sas_cfg.items(): if name not in sas_param_names: EnodebdLogger.warning("Ignoring attribute %s", name) continue desired_cfg.set_parameter(name, val)
async def run(cmd): """Fork shell and run command NOTE: Popen is non-blocking""" cmd = shlex.split(cmd) proc = await asyncio.create_subprocess_shell(" ".join(cmd)) await proc.communicate() if proc.returncode != 0: # This can happen because the NAT prerouting rule didn't exist logger.error( 'Possible error running async subprocess: %s exited with ' 'return code [%d].', cmd, proc.returncode) return proc.returncode
def get_metric_value(enodeb_status: Dict[str, str], key: str): # Metrics are "sticky" when synced to the cloud - if we don't # receive a status update from enodeb, set the metric to 0 # to explicitly indicate that it was not received, otherwise the # metrics collector will continue to report the last value val = enodeb_status.get(key, None) if val is None: return 0 if type(val) is not bool: logger.error('Could not cast metric value %s to int', val) return 0 return int(val) # val should be either True or False
def _get_enb_label_from_request(self, request) -> str: label = 'default' ip = request.headers.get('X-Forwarded-For') if ip is None: ip = request.remote_addr if ip is None: return label try: label = self.enb_manager.get_serial(ip) except KeyError: logger.error("Couldn't find serial for ip", ip) return label
def tr069_server(state_machine_manager: StateMachineManager) -> None: """ TR-069 server Inputs: - acs_to_cpe_queue = instance of Queue containing messages from parent process/thread to be sent to CPE - cpe_to_acs_queue = instance of Queue containing messages from CPE to be sent to parent process/thread """ config = load_service_config("enodebd") AutoConfigServer.set_state_machine_manager(state_machine_manager) app = Tr069Application( [AutoConfigServer], CWMP_NS, in_protocol=Tr069Soap11(validator='soft'), out_protocol=Tr069Soap11(), ) wsgi_app = WsgiApplication(app) try: ip_address = get_ip_from_if(config['tr069']['interface']) except (ValueError, KeyError) as e: # Interrupt main thread since process should not continue without TR-069 _thread.interrupt_main() raise e socket.setdefaulttimeout(SOCKET_TIMEOUT) logger.info( 'Starting TR-069 server on %s:%s', ip_address, config['tr069']['port'], ) server = make_server( ip_address, config['tr069']['port'], wsgi_app, WSGIServer, tr069_WSGIRequestHandler, ) # Note: use single-thread server, to avoid state contention try: server.serve_forever() finally: # Log error and interrupt main thread, to ensure that entire process # is restarted if this thread exits logger.error('Hit error in TR-069 thread. Interrupting main thread.') _thread.interrupt_main()
def read_msg(self, message: Any) -> Optional[str]: if type(message) == models.Fault: logger.error('Received Fault in response to SetParameterValues') 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, ) raise Tr069Error( 'Received Fault in response to SetParameterValues ' '(faultstring = %s)' % message.FaultString, ) elif not isinstance(message, models.SetParameterValuesResponse): return AcsReadMsgResult(False, None) if message.Status != 0: raise Tr069Error( 'Received SetParameterValuesResponse with ' 'Status=%d' % message.Status, ) param_name = ParameterName.ADMIN_STATE desired_admin_value = \ self.acs.desired_cfg.get_parameter(param_name) \ and self.admin_value magma_value = \ self.acs.data_model.transform_for_magma( param_name, desired_admin_value, ) self.acs.device_cfg.set_parameter(param_name, magma_value) if len( get_all_objects_to_delete( self.acs.desired_cfg, self.acs.device_cfg, ), ) > 0: return AcsReadMsgResult(True, self.del_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) else: return AcsReadMsgResult(True, self.done_transition)
def get_all_enb_state() -> Optional[Dict[int, int]]: """ Make RPC call to 'GetENBState' method of s1ap service """ try: chan = ServiceRegistry.get_rpc_channel(S1AP_SERVICE_NAME, ServiceRegistry.LOCAL) except ValueError: logger.error('Cant get RPC channel to %s', S1AP_SERVICE_NAME) return {} client = S1apServiceStub(chan) try: res = client.GetENBState(Void(), DEFAULT_GRPC_TIMEOUT) return res.enb_state_map except grpc.RpcError as err: logger.warning("GetEnbState error: [%s] %s", err.code(), err.details()) return {}
def postprocess( self, mconfig: Any, service_cfg: Any, desired_cfg: EnodebConfiguration, ) -> None: # TODO: Get this config from the domain proxy # TODO @amarpad, set these when DProxy integration is done. # For now the radio will directly talk to the SAS and get these # attributes. desired_cfg.delete_parameter(ParameterName.EARFCNDL) desired_cfg.delete_parameter(ParameterName.DL_BANDWIDTH) desired_cfg.delete_parameter(ParameterName.UL_BANDWIDTH) # go through misc parameters and set them to default. for name, val in FreedomFiOneMiscParameters.defaults.items(): desired_cfg.set_parameter(name, val) # Bump up the parameter key version self.acs.parameter_version_inc() if self.WEB_UI_ENABLE_LIST_KEY in service_cfg: serial_nos = service_cfg.get(self.WEB_UI_ENABLE_LIST_KEY) if self.acs.device_cfg.has_parameter( ParameterName.SERIAL_NUMBER, ): if self.acs.get_parameter(ParameterName.SERIAL_NUMBER) in \ serial_nos: desired_cfg.set_parameter( FreedomFiOneMiscParameters.WEB_UI_ENABLE, True, ) else: # This should not happen EnodebdLogger.error("Serial number unknown for device") if self.SAS_KEY not in service_cfg: return sas_cfg = service_cfg[self.SAS_KEY] sas_param_names = self.acs.data_model.get_sas_param_names() for name, val in sas_cfg.items(): if name not in sas_param_names: EnodebdLogger.warning("Ignoring attribute %s", name) continue desired_cfg.set_parameter(name, val)
def _get_enb_label_from_request(self, request) -> str: label = 'default' ip = request.headers.get('X-Forwarded-For') if ip is None: peername = request.transport.get_extra_info('peername') if peername is not None: ip, _ = peername if ip is None: return label label = self.enb_manager.get_serial_of_ip(ip) if label: logger.debug('Found serial %s for ip %s', label, ip) else: logger.error("Couldn't find serial for ip", ip) return label
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')
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 enb_serial is None: # TR-069 message did not contain an eNodeB serial ID. logger.error('Cannot associate null eNB serial to a an IP') pass elif 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
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 _parse_tdd_counters(self, enb_label, names, data): """ Parse eNodeB performance management counters from TDD structure. Most of the logic is just to extract the correct counter based on the name of the statistic. Each counter is either of type 'V', which is a single integer value, or 'CV', which contains multiple integer sub-elements, named 'SV', which we add together. E.g: <V i="9">0</V> <CV i="10"> <SN>RRC.AttConnReestab.RECONF_FAIL</SN> <SV>0</SV> <SN>RRC.AttConnReestab.HO_FAIL</SN> <SV>0</SV> <SN>RRC.AttConnReestab.OTHER</SN> <SV>0</SV> </CV> See tests/stats_manager_tests.py for a more complete example. """ index_data_map = self._build_index_to_data_map(data) name_index_map = self._build_name_to_index_map(names) # For each performance metric, extract value from XML document and set # internal metric to that value. for pm_name, metric in self.PM_FILE_TO_METRIC_MAP.items(): elements = pm_name.split(':') counter = elements.pop(0) if len(elements) == 0: subcounter = None else: subcounter = elements.pop(0) index = name_index_map.get(counter) if index is None: logger.warning('PM counter %s not found in PmNames', counter) continue data_el = index_data_map.get(index) if data_el is None: logger.warning('PM counter %s not found in PmData', counter) continue if data_el.tag == 'V': if subcounter is not None: logger.warning('No subcounter in PM counter %s', counter) continue # Data is singular value try: value = int(data_el.text) except ValueError: logger.info('PM value (%s) of counter %s not integer', data_el.text, counter) continue elif data_el.tag == 'CV': # Check whether we want just one subcounter, or sum them all subcounter_index = None if subcounter is not None: index = 0 for sub_name_el in data_el.findall('SN'): if sub_name_el.text == subcounter: subcounter_index = index index = index + 1 if subcounter is not None and subcounter_index is None: logger.warning('PM subcounter (%s) not found', subcounter) continue # Data is multiple sub-elements. Sum them, or select the one # of interest value = 0 try: index = 0 for sub_data_el in data_el.findall('SV'): if subcounter_index is None or \ subcounter_index == index: value = value + int(sub_data_el.text) index = index + 1 except ValueError: logger.error('PM value (%s) of counter %s not integer', sub_data_el.text, pm_name) continue else: logger.warning('Unknown PM data type (%s) of counter %s', data_el.tag, pm_name) continue # Apply new value to metric if pm_name == 'PDCP.UpOctUl' or pm_name == 'PDCP.UpOctDl': metric.labels(enb_label).set(value) else: metric.set(value)
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 _assert_param_in_model(self, param_name: ParameterName) -> None: trparam_model = self.data_model tr_param = trparam_model.get_parameter(param_name) if tr_param is None: logger.error('Parameter <%s> not defined in model', param_name) raise ConfigurationError("Parameter not defined in model.")