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 _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!')
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 _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 _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 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)
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.info( 'Possible error running async subprocess: %s exited with ' 'return code [%d].', cmd, proc.returncode) return proc.returncode
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 _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 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 should_transition_to_firmware_upgrade_download(acs): device_sw_version = "" device_serial_number = "" if acs.device_cfg.has_parameter(ParameterName.SW_VERSION): device_sw_version = acs.device_cfg.get_parameter( ParameterName.SW_VERSION, ) if acs.device_cfg.has_parameter(ParameterName.SERIAL_NUMBER): device_serial_number = acs.device_cfg.get_parameter( ParameterName.SERIAL_NUMBER, ) if not device_sw_version or not device_serial_number: logger.debug( f'Skipping FW Download for eNB, missing device config: {device_sw_version=}, {device_serial_number=}.', ) return False if acs.is_fw_upgrade_in_progress(): logger.debug( 'Skipping FW Download for eNB [%s], firmware upgrade in progress.', device_serial_number, ) return False fw_upgrade_config = get_firmware_upgrade_download_config(acs) if not fw_upgrade_config: logger.debug( 'Skipping FW Download for eNB [%s], missing firmware upgrade config in enodebd.yml.', device_serial_number, ) return False target_software_version = fw_upgrade_config.get('version', '') if device_sw_version == target_software_version: logger.debug( 'Skipping FW Download for eNB [%s], eNB Software Version [%s] up to date with firmware upgrade config.', device_serial_number, target_software_version, ) acs.stop_fw_upgrade_timeout() return False logger.info( 'Initiate FW Download for eNB [%s], eNB SW Version [%s], target SW Version [%s]', device_serial_number, device_sw_version, target_software_version, ) return True
def get_firmware_upgrade_download_config(acs): device_serial_number = '' if acs.device_cfg.has_parameter(ParameterName.SERIAL_NUMBER): device_serial_number = acs.device_cfg.get_parameter( ParameterName.SERIAL_NUMBER, ) fw_upgrade_config = _get_firmware_upgrade_download_config_for_serial( acs, device_serial_number, ) if fw_upgrade_config: logger.info(f'Found {fw_upgrade_config=} for {device_serial_number=}') return fw_upgrade_config device_model = acs.device_name fw_upgrade_config = _get_firmware_upgrade_download_config_for_model( acs, device_model, ) if fw_upgrade_config: logger.info(f'Found {fw_upgrade_config=} for {device_model=}') return fw_upgrade_config
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)