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)
async def check_and_apply_iptables_rules( port: str, enodebd_public_ip: str, enodebd_ip: str, ) -> None: command = 'sudo iptables -t nat -L' output = subprocess.run(command, shell=True, stdout=subprocess.PIPE, check=True) command_output = output.stdout.decode('utf-8').strip() prerouting_rules = _get_prerouting_rules(command_output) if not prerouting_rules: logger.info('Configuring Iptables rule') await run( get_iptables_rule( port, enodebd_public_ip, enodebd_ip, add=True, ), ) else: # Checks each rule in PREROUTING Chain expected_rules_present = check_rules(prerouting_rules, port, enodebd_public_ip, enodebd_ip) if not expected_rules_present: logger.info('Configuring Iptables rule') await run( get_iptables_rule( port, enodebd_public_ip, enodebd_ip, add=True, ), )
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 _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 bandwidth(bandwidth_rbs: Union[str, int, float]) -> float: """ Map bandwidth in number of RBs to MHz TODO: TR-196 spec says this should be '6' rather than 'n6', but BaiCells eNodeB uses 'n6'. Need to resolve this. Args: bandwidth_rbs (str): Bandwidth in number of RBs Returns: str: Bandwidth in MHz """ if bandwidth_rbs in BANDWIDTH_RBS_TO_MHZ_MAP: return BANDWIDTH_RBS_TO_MHZ_MAP[bandwidth_rbs] logger.warning('Unknown bandwidth_rbs (%s)', str(bandwidth_rbs)) if bandwidth_rbs in BANDWIDTH_MHZ_LIST: return bandwidth_rbs elif isinstance(bandwidth_rbs, str): mhz = None if bandwidth_rbs.isdigit(): mhz = int(bandwidth_rbs) elif bandwidth_rbs.replace('.', '', 1).isdigit(): mhz = float(bandwidth_rbs) if mhz in BANDWIDTH_MHZ_LIST: return mhz raise ConfigurationError('Unknown bandwidth specification (%s)' % str(bandwidth_rbs))
def decompose_incoming_envelope(self, ctx, message=XmlDocument.REQUEST): """ For TR-069, the SOAP fault message (CPE->ACS) contains useful information, and should not result in another fault response (ACS->CPE). Strip the outer SOAP fault structure, so that the CWMP fault structure is treated as a normal RPC call (to the 'Fault' function). """ super(Tr069Soap11, self).decompose_incoming_envelope(ctx, message) if ctx.in_body_doc.tag == '{%s}Fault' % self.ns_soap_env: faultstring = ctx.in_body_doc.findtext('faultstring') if not faultstring or 'CWMP fault' not in faultstring: # Not a CWMP fault return # Strip SOAP fault structure, leaving inner CWMP fault structure detail_elem = ctx.in_body_doc.find('detail') if detail_elem is not None: detail_children = list(detail_elem) if len(detail_children): if len(detail_children) > 1: logger.warning("Multiple detail elements found in SOAP" " fault - using first one") ctx.in_body_doc = detail_children[0] ctx.method_request_string = ctx.in_body_doc.tag self.validate_body(ctx, message)
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 _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.warning('Parameter <%s> not defined in model', param_name) raise ConfigurationError( f"Parameter not defined in model: {param_name}")
async def set_enodebd_iptables_rule(): """ Remove & Set iptable rules for exposing public IP for enobeb instead of private IP.. """ # Remove & Set iptable rules for exposing public ip # for enobeb instead of private cfg = load_service_config('enodebd') port, interface = cfg['tr069']['port'], cfg['tr069']['interface'] enodebd_public_ip = cfg['tr069']['public_ip'] # IPv4 only as iptables only works for IPv4. TODO: Investigate ip6tables? enodebd_ip = get_ip_from_if(interface, preference=IpPreference.IPV4_ONLY) # Incoming data from 192.88.99.142 -> enodebd address (eg 192.168.60.142) enodebd_netmask = get_if_ip_with_netmask( interface, preference=IpPreference.IPV4_ONLY, )[1] verify_config = does_iface_config_match_expected( enodebd_ip, enodebd_netmask, ) if not verify_config: logger.warning( 'The IP address of the %s interface is %s. The ' 'expected IP addresses are %s', interface, enodebd_ip, str(EXPECTED_IP4)) await check_and_apply_iptables_rules( port, enodebd_public_ip, enodebd_ip, )
def read_msg(self, message: Any) -> AcsReadMsgResult: """ Process GetParameterValuesResponse Object parameters that have a reported value of None indicate that the object is not in the eNB's configuration. Most eNB devices will reply with a Fault message if we try to get values of parameters that don't exist on the data model, so this is an idiosyncrasy of Baicells QAFB. """ if not isinstance(message, models.GetParameterValuesResponse): return AcsReadMsgResult(False, None) path_to_val = {} 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)) num_plmns = self.acs.data_model.get_num_plmns() for i in range(1, num_plmns + 1): obj_name = ParameterName.PLMN_N % i obj_to_params = self.acs.data_model.get_numbered_param_names() param_name_list = obj_to_params[obj_name] for name in param_name_list: path = self.acs.data_model.get_parameter(name).path if path in path_to_val: value = path_to_val[path] if value is None: continue if obj_name not in self.acs.device_cfg.get_object_names(): self.acs.device_cfg.add_object(obj_name) magma_value = \ self.acs.data_model.transform_for_magma(name, value) self.acs.device_cfg.set_parameter_for_object(name, magma_value, obj_name) # Now we have enough information to build the desired configuration 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 _handle_tr069_message( cls, ctx: WsgiMethodContext, message: ComplexModelBase, ) -> ComplexModelBase: # Log incoming msg logger.debug('Handling TR069 message: %s %s', message.__class__.__name__, str(as_dict(message))) req = cls._get_tr069_response_from_sm(ctx, message) # Log outgoing msg logger.debug('Sending TR069 message: %s %s', req.__class__.__name__, str(as_dict(req))) # Set header ctx.out_header = models.ID(mustUnderstand='1') ctx.out_header.Data = 'null' # Set return message name if isinstance(req, models.DummyInput): # Generate 'empty' request to CPE using empty message name ctx.descriptor.out_message.Attributes.sub_name = 'EmptyHttp' return models.AcsToCpeRequests() ctx.descriptor.out_message.Attributes.sub_name = req.__class__.__name__ return cls._generate_acs_to_cpe_request_copy(req)
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 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)) 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 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 start_fw_upgrade_timeout(self) -> None: """ Start a firmware upgrade timeout timer. When initialing a firmware upgrade download, the eNB can take an unknown amount of time for the download to finish. This process is indicated by a TransferComplete TR069 message, but the enB can still operate and can apply the firmware at any time and reboot. Since we do not want to re-issue a download request (eNB hasn't updated, its SW version is still 'old' and the firmware version check will still detect and older FW version still present on the eNB) we want to hold with the download flow for some time - which is what this timer is for. """ if self.fw_upgrade_timeout_handler is not None: return logger.debug( 'ACS starting fw upgrade timeout for %d seconds', self.FW_UPGRADE_TIMEOUT, ) self.fw_upgrade_timeout_handler = self.event_loop.call_later( self.FW_UPGRADE_TIMEOUT, self.stop_fw_upgrade_timeout, )
def main(): """ Top-level function for enodebd """ service = MagmaService('enodebd', mconfigs_pb2.EnodebD()) logger.init() # Optionally pipe errors to Sentry sentry_init(service_name=service.name, sentry_mconfig=service.shared_mconfig.sentry_config) # State machine manager for tracking multiple connected eNB devices. state_machine_manager = StateMachineManager(service) # Statistics manager stats_mgr = StatsManager(state_machine_manager) stats_mgr.run() # Start TR-069 thread server_thread = Thread( target=tr069_server, args=(state_machine_manager, ), daemon=True, ) server_thread.start() print_grpc_payload = service.config.get('print_grpc_payload', False) # Add all servicers to the server enodebd_servicer = EnodebdRpcServicer( state_machine_manager, print_grpc_payload, ) enodebd_servicer.add_to_server(service.rpc_server) # Register function to get service status def get_enodebd_status(): return get_service_status_old(state_machine_manager) service.register_get_status_callback(get_enodebd_status) # Register a callback function for GetOperationalStates service303 function def get_enodeb_operational_states() -> List[State]: return get_operational_states( state_machine_manager, service.mconfig, print_grpc_payload, ) service.register_operational_states_callback(get_enodeb_operational_states) # Set eNodeBD iptables rules due to exposing public IP to eNodeB service.loop.create_task(set_enodebd_iptables_rule()) # Run the service loop service.run() # Cleanup the service service.close()
def _clear_stats(self) -> None: """ Clear statistics. Called when eNodeB management plane disconnects """ logger.info('Clearing performance counter statistics') # Set all metrics to 0 if eNodeB not connected for metric in self.PM_FILE_TO_METRIC_MAP.values(): metric.set(0)
def transition(self, next_state: str) -> Any: logger.debug( 'State transition from <%s> to <%s>', self.state.__class__.__name__, next_state, ) self.state.exit() self.state = self.state_map[next_state] self.state.enter()
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)
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 _clear_stats(self) -> None: """ Clear statistics. Called when eNodeB management plane disconnects """ logger.info('Clearing performance counter statistics') # Set all metrics to 0 if eNodeB not connected for pm_name, metric in self.PM_FILE_TO_METRIC_MAP: # eNB data usage metrics will not be cleared if pm_name not in ('PDCP.UpOctUl', 'PDCP.UpOctDl'): metric.set(0)
def get_msg(self) -> 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 stop_fw_upgrade_timeout(self) -> None: """ Stop firmware upgrade timeout timer. Invoking this method will re-enable firmware software version checking in the Download states. """ if self.fw_upgrade_timeout_handler is not None: logger.debug('ACS stopping fw upgrade timeout.') self.fw_upgrade_timeout_handler.cancel() self.fw_upgrade_timeout_handler = None
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_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 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 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 _read_gps_coords_from_file(): try: with open(CACHED_GPS_COORD_FILE_PATH) as f: lines = f.readlines() if len(lines) != 2: logger.warning( 'Expected to find 2 lines in GPS ' 'coordinate file but only found %d', len(lines)) return '0', '0' return tuple(map(lambda l: l.strip(), lines)) except OSError: logger.warning('Could not open cached GPS coordinate file') return '0', '0'
def _format_as_bool( param_value: Union[bool, str, int], param_name: Optional[Union[ParameterName, str]] = None, ) -> bool: """ Returns '1' for true, and '0' for false """ stripped_value = str(param_value).lower().strip() if stripped_value in {'true', '1'}: return True elif stripped_value in {'false', '0'}: return False else: logger.warning('%s parameter not understood (%s)', param_name, param_value) return False
def _parse_sw_version(version_str): """ Parse SW version string. Expects format: BaiStation_V100R001C00B110SPC003 For the above version string, returns: [100, 1, 0, 110, 3] Note: trailing characters (for dev builds) are ignored. Null is returned for version strings that don't match the above format. """ logger.debug('Got firmware version: %s', version_str) version = re.findall( r'BaiStation_V(\d{3})R(\d{3})C(\d{2})B(\d{3})SPC(\d{3})', version_str) if not version: return None elif len(version) > 1: logger.warning('SW version (%s) not formatted as expected', version_str) version_int = [] for num in version[0]: try: version_int.append(int(num)) except ValueError: logger.warning('SW version (%s) not formatted as expected', version_str) return None logger.debug('Parsed firmware version: %s', version_int) return version_int