blob: 51a42a7ca4c3499b717d030ff57c6bc6b2825d41 [file] [log] [blame]
# SPDX-FileCopyrightText: 2020 The Magma Authors.
# SPDX-FileCopyrightText: 2022 Open Networking Foundation <support@opennetworking.org>
#
# SPDX-License-Identifier: BSD-3-Clause
import json
import os
from collections import namedtuple
from typing import Any, Dict, List, NamedTuple, Optional, Tuple, Union
from lte.protos.enodebd_pb2 import SingleEnodebStatus
from lte.protos.mconfig import mconfigs_pb2
from common import serialization_utils
import metrics
from data_models.data_model_parameters import ParameterName
from device_config.configuration_util import (
find_enb_by_cell_id,
get_enb_rf_tx_desired,
)
from exceptions import ConfigurationError
from logger import EnodebdLogger as logger
from s1ap_client import get_all_enb_state
from state_machines.enb_acs import EnodebAcsStateMachine
from state_machines.enb_acs_manager import StateMachineManager
from orc8r.protos.service303_pb2 import State
# There are 2 levels of caching for GPS coordinates from the enodeB: module
# variables (in-memory) and on disk. In the event the enodeB stops reporting
# GPS, we will continue to report the cached coordinates from the in-memory
# cached coordinates. If enodebd is restarted, this in-memory cache will be
# populated by the file
CACHED_GPS_COORD_FILE_PATH = os.path.join(
'/var/opt/magma/enodebd',
'gps_coords.txt',
)
# Cache GPS coordinates in memory so we don't write to the file cache if the
# coordinates have not changed. We can read directly from here instead of the
# file cache when the enodeB goes down unless these are unintialized.
_gps_lat_cached = None
_gps_lon_cached = None
EnodebStatus = NamedTuple(
'EnodebStatus',
[
('enodeb_configured', bool),
('gps_latitude', str),
('gps_longitude', str),
('enodeb_connected', bool),
('opstate_enabled', bool),
('rf_tx_on', bool),
('rf_tx_desired', bool),
('gps_connected', bool),
('ptp_connected', bool),
('mme_connected', bool),
('fsm_state', str),
('cell_id', int),
],
)
# TODO: Remove after checkins support multiple eNB status
MagmaOldEnodebdStatus = namedtuple(
'MagmaOldEnodebdStatus',
[
'enodeb_serial',
'enodeb_configured',
'gps_latitude',
'gps_longitude',
'enodeb_connected',
'opstate_enabled',
'rf_tx_on',
'rf_tx_desired',
'gps_connected',
'ptp_connected',
'mme_connected',
'enodeb_state',
],
)
MagmaEnodebdStatus = NamedTuple(
'MagmaEnodebdStatus',
[
('n_enodeb_connected', str),
('all_enodeb_configured', str),
('all_enodeb_opstate_enabled', str),
('all_enodeb_rf_tx_configured', str),
('any_enodeb_gps_connected', str),
('all_enodeb_ptp_connected', str),
('all_enodeb_mme_connected', str),
('gateway_gps_longitude', str),
('gateway_gps_latitude', str),
],
)
def update_status_metrics(status: EnodebStatus) -> None:
""" Update metrics for eNodeB status """
# Call every second
metrics_by_stat_key = {
'enodeb_connected': metrics.STAT_ENODEB_CONNECTED,
'enodeb_configured': metrics.STAT_ENODEB_CONFIGURED,
'opstate_enabled': metrics.STAT_OPSTATE_ENABLED,
'rf_tx_on': metrics.STAT_RF_TX_ENABLED,
'rf_tx_desired': metrics.STAT_RF_TX_DESIRED,
'gps_connected': metrics.STAT_GPS_CONNECTED,
'ptp_connected': metrics.STAT_PTP_CONNECTED,
'mme_connected': metrics.STAT_MME_CONNECTED,
}
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
for stat_key, metric in metrics_by_stat_key.items():
metric.set(get_metric_value(status._asdict(), stat_key))
# TODO: Remove after checkins support multiple eNB status
def get_service_status_old(
enb_acs_manager: StateMachineManager,
) -> Dict[str, Any]:
""" Get service status compatible with older controller """
enb_status_by_serial = get_all_enb_status(enb_acs_manager)
# Since we only expect users to plug in a single eNB, generate service
# status with the first one we find that is connected
for enb_serial, enb_status in enb_status_by_serial.items():
if enb_status.enodeb_connected:
return MagmaOldEnodebdStatus(
enodeb_serial=enb_serial,
enodeb_configured=_bool_to_str(enb_status.enodeb_configured),
gps_latitude=enb_status.gps_latitude,
gps_longitude=enb_status.gps_longitude,
enodeb_connected=_bool_to_str(enb_status.enodeb_connected),
opstate_enabled=_bool_to_str(enb_status.opstate_enabled),
rf_tx_on=_bool_to_str(enb_status.rf_tx_on),
rf_tx_desired=_bool_to_str(enb_status.rf_tx_desired),
gps_connected=_bool_to_str(enb_status.gps_connected),
ptp_connected=_bool_to_str(enb_status.ptp_connected),
mme_connected=_bool_to_str(enb_status.mme_connected),
enodeb_state=enb_status.fsm_state,
)._asdict()
return MagmaOldEnodebdStatus(
enodeb_serial='N/A',
enodeb_configured='0',
gps_latitude='0.0',
gps_longitude='0.0',
enodeb_connected='0',
opstate_enabled='0',
rf_tx_on='0',
rf_tx_desired='N/A',
gps_connected='0',
ptp_connected='0',
mme_connected='0',
enodeb_state='N/A',
)._asdict()
def get_service_status(enb_acs_manager: StateMachineManager) -> Dict[str, Any]:
enodebd_status = _get_enodebd_status(enb_acs_manager)
return enodebd_status._asdict()
def _get_enodebd_status(
enb_acs_manager: StateMachineManager,
) -> MagmaEnodebdStatus:
enb_status_by_serial = get_all_enb_status(enb_acs_manager)
# Start from default values for enodebd status
n_enodeb_connected = 0
all_enodeb_configured = False
all_enodeb_opstate_enabled = False
all_enodeb_rf_tx_configured = False
any_enodeb_gps_connected = False
all_enodeb_ptp_connected = False
all_enodeb_mme_connected = False
gateway_gps_longitude = '0.0'
gateway_gps_latitude = '0.0'
def _is_rf_tx_configured(enb_status: EnodebStatus) -> bool:
return enb_status.rf_tx_on == enb_status.rf_tx_desired
if enb_status_by_serial:
enb_status_list = list(enb_status_by_serial.values())
# Aggregate all eNB status for enodebd status, repetitive but
# clearer for output purposes.
n_enodeb_connected = sum(
enb_status.enodeb_connected for enb_status in enb_status_list
)
all_enodeb_configured = all(
enb_status.enodeb_configured for enb_status in enb_status_list
)
all_enodeb_mme_connected = all(
enb_status.mme_connected for enb_status in enb_status_list
)
all_enodeb_opstate_enabled = all(
enb_status.opstate_enabled for enb_status in enb_status_list
)
all_enodeb_ptp_connected = all(
enb_status.ptp_connected for enb_status in enb_status_list
)
any_enodeb_gps_connected = any(
enb_status.gps_connected for enb_status in enb_status_list
)
all_enodeb_rf_tx_configured = all(
_is_rf_tx_configured(enb_status) for enb_status in enb_status_list
)
if n_enodeb_connected:
gateway_gps_longitude = enb_status_list[0].gps_longitude
gateway_gps_latitude = enb_status_list[0].gps_latitude
return MagmaEnodebdStatus(
n_enodeb_connected=str(n_enodeb_connected),
all_enodeb_configured=str(all_enodeb_configured),
all_enodeb_opstate_enabled=str(all_enodeb_opstate_enabled),
all_enodeb_rf_tx_configured=str(all_enodeb_rf_tx_configured),
any_enodeb_gps_connected=str(any_enodeb_gps_connected),
all_enodeb_ptp_connected=str(all_enodeb_ptp_connected),
all_enodeb_mme_connected=str(all_enodeb_mme_connected),
gateway_gps_longitude=str(gateway_gps_longitude),
gateway_gps_latitude=str(gateway_gps_latitude),
)
def get_all_enb_status(
enb_acs_manager: StateMachineManager,
) -> Dict[str, EnodebStatus]:
enb_status_by_serial = {}
serial_list = enb_acs_manager.get_connected_serial_id_list()
for enb_serial in serial_list:
handler = enb_acs_manager.get_handler_by_serial(enb_serial)
status = get_enb_status(handler)
enb_status_by_serial[enb_serial] = status
return enb_status_by_serial
def get_enb_status(enodeb: EnodebAcsStateMachine) -> EnodebStatus:
"""
Returns a dict representing the status of an enodeb
The returned dictionary will be a subset of the following keys:
- enodeb_connected
- enodeb_configured
- opstate_enabled
- rf_tx_on
- rf_tx_desired
- gps_connected
- ptp_connected
- mme_connected
- gps_latitude
- gps_longitude
- ip_address
- cell_id
The set of keys returned will depend on the connection status of the
enodeb. A missing key indicates that the value is unknown.
Returns:
Status dictionary for the enodeb state
"""
enodeb_configured = enodeb.is_enodeb_configured()
# We cache GPS coordinates so try to read them before the early return
# if the enB is not connected
gps_lat, gps_lon = _get_and_cache_gps_coords(enodeb)
enodeb_connected = enodeb.is_enodeb_connected()
opstate_enabled = _parse_param_as_bool(enodeb, ParameterName.OP_STATE)
rf_tx_on = _parse_param_as_bool(enodeb, ParameterName.RF_TX_STATUS)
rf_tx_on = rf_tx_on and enodeb_connected
try:
enb_serial = \
enodeb.device_cfg.get_parameter(ParameterName.SERIAL_NUMBER)
enb_cell_id = int(
enodeb.device_cfg.get_parameter(ParameterName.CELL_ID),
)
rf_tx_desired = get_enb_rf_tx_desired(enodeb.mconfig, enb_serial)
except (KeyError, ConfigurationError):
rf_tx_desired = False
enb_cell_id = 0
mme_connected = _parse_param_as_bool(enodeb, ParameterName.MME_STATUS)
gps_connected = _get_gps_status_as_bool(enodeb)
try:
ptp_connected = _parse_param_as_bool(enodeb, ParameterName.PTP_STATUS)
except ConfigurationError:
ptp_connected = False
return EnodebStatus(
enodeb_configured=enodeb_configured,
gps_latitude=gps_lat,
gps_longitude=gps_lon,
enodeb_connected=enodeb_connected,
opstate_enabled=opstate_enabled,
rf_tx_on=rf_tx_on,
rf_tx_desired=rf_tx_desired,
gps_connected=gps_connected,
ptp_connected=ptp_connected,
mme_connected=mme_connected,
fsm_state=enodeb.get_state(),
cell_id=enb_cell_id,
)
def get_single_enb_status(
device_serial: str,
state_machine_manager: StateMachineManager,
) -> SingleEnodebStatus:
try:
handler = state_machine_manager.get_handler_by_serial(device_serial)
except KeyError:
return _empty_enb_status()
# This namedtuple is missing IP and serial info
status = get_enb_status(handler)
# Get IP info
ip = state_machine_manager.get_ip_of_serial(device_serial)
def get_status_property(status: bool) -> SingleEnodebStatus.StatusProperty:
if status:
return SingleEnodebStatus.StatusProperty.Value('ON')
return SingleEnodebStatus.StatusProperty.Value('OFF')
# Build the message to return through gRPC
enb_status = SingleEnodebStatus()
enb_status.device_serial = device_serial
enb_status.ip_address = ip
enb_status.connected = get_status_property(status.enodeb_connected)
enb_status.configured = get_status_property(status.enodeb_configured)
enb_status.opstate_enabled = get_status_property(status.opstate_enabled)
enb_status.rf_tx_on = get_status_property(status.rf_tx_on)
enb_status.rf_tx_desired = get_status_property(status.rf_tx_desired)
enb_status.gps_connected = get_status_property(status.gps_connected)
enb_status.ptp_connected = get_status_property(status.ptp_connected)
enb_status.mme_connected = get_status_property(status.mme_connected)
enb_status.gps_longitude = status.gps_longitude
enb_status.gps_latitude = status.gps_latitude
enb_status.fsm_state = status.fsm_state
return enb_status
def get_operational_states(
enb_acs_manager: StateMachineManager,
mconfig: mconfigs_pb2.EnodebD,
) -> List[State]:
"""
Returns: A list of State with EnodebStatus encoded as JSON
"""
states = []
configured_serial_ids = []
enb_status_by_serial = get_all_enb_status(enb_acs_manager)
# Get S1 connected eNBs
enb_statuses = get_all_enb_state()
for serial_id in enb_status_by_serial:
enb_status_dict = enb_status_by_serial[serial_id]._asdict()
# Add IP address to state
enb_status_dict['ip_address'] = enb_acs_manager.get_ip_of_serial(
serial_id,
)
# Add num of UEs connected
num_ue_connected = enb_statuses.get(enb_status_dict['cell_id'], 0)
enb_status_dict['ues_connected'] = num_ue_connected
serialized = json.dumps(enb_status_dict)
state = State(
type="single_enodeb",
deviceID=serial_id,
value=serialized.encode('utf-8'),
)
configured_serial_ids.append(serial_id)
states.append(state)
# Get state for externally configured enodebs
s1_states = get_enb_s1_connected_states(
enb_statuses,
configured_serial_ids,
mconfig,
)
states.extend(s1_states)
return states
def get_enb_s1_connected_states(
enb_s1_state_map, configured_serial_ids,
mconfig,
) -> List[State]:
states = []
for enb_id in enb_s1_state_map:
enb = find_enb_by_cell_id(mconfig, enb_id)
if enb and enb.serial_num not in configured_serial_ids:
status = EnodebStatus(
enodeb_configured=False,
gps_latitude='N/A',
gps_longitude='N/A',
enodeb_connected=True,
opstate_enabled=False,
rf_tx_on=False,
rf_tx_desired=False,
gps_connected=False,
ptp_connected=False,
mme_connected=True,
fsm_state='N/A',
cell_id=enb_id,
)
status_dict = status._asdict()
# Add IP address to state
status_dict['ip_address'] = enb.config.ip_address
# Add num of UEs connected to state, use cellID from mconfig
status_dict['ues_connected'] = enb_s1_state_map.get(enb_id, 0)
serialized = json.dumps(status_dict)
state = State(
type="single_enodeb",
deviceID=enb.serial_num,
value=serialized.encode('utf-8'),
)
states.append(state)
return states
def _empty_enb_status() -> SingleEnodebStatus:
enb_status = SingleEnodebStatus()
enb_status.device_serial = 'N/A'
enb_status.ip_address = 'N/A'
enb_status.connected = '0'
enb_status.configured = '0'
enb_status.opstate_enabled = '0'
enb_status.rf_tx_on = '0'
enb_status.rf_tx_desired = 'N/A'
enb_status.gps_connected = '0'
enb_status.ptp_connected = '0'
enb_status.mme_connected = '0'
enb_status.gps_longitude = '0.0'
enb_status.gps_latitude = '0.0'
enb_status.fsm_state = 'N/A'
return enb_status
def _parse_param_as_bool(
enodeb: EnodebAcsStateMachine,
param_name: ParameterName,
) -> bool:
try:
return _format_as_bool(enodeb.get_parameter(param_name), param_name)
except (KeyError, ConfigurationError):
return False
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', 'enabled', 'Success'}:
return True
elif stripped_value in {'false', '0', 'disabled', 'InProgress', 'unsatisfied'}:
return False
else:
logger.warning(
'%s parameter not understood (%s)', param_name, param_value,
)
return False
def _get_gps_status_as_bool(enodeb: EnodebAcsStateMachine) -> bool:
try:
if not enodeb.has_parameter(ParameterName.GPS_STATUS):
return False
else:
param = enodeb.get_parameter(ParameterName.GPS_STATUS)
if isinstance(param, bool):
# No translation to do.
return param
stripped_value = param.lower().strip()
if stripped_value in ['0', '2', 'inprogress']:
# 2 = GPS locking
return False
elif stripped_value in ['1', 'Success']:
return True
else:
logger.warning(
'GPS status parameter not understood (%s)', param,
)
return False
except (KeyError, ConfigurationError, AttributeError):
return False
def _get_and_cache_gps_coords(enodeb: EnodebAcsStateMachine) -> Tuple[
str, str,
]:
"""
Read the GPS coordinates of the enB from its configuration or the
cached coordinate file if the preceding read fails. If reading from
enB configuration succeeds, this method will cache the new coordinates.
Returns:
(str, str): GPS latitude, GPS longitude
"""
lat, lon = '', ''
try:
lat = enodeb.get_parameter(ParameterName.GPS_LAT)
lon = enodeb.get_parameter(ParameterName.GPS_LONG)
if lat != _gps_lat_cached or lon != _gps_lon_cached:
_cache_new_gps_coords(lat, lon)
return lat, lon
except (KeyError, ConfigurationError):
return _get_cached_gps_coords()
except ValueError:
logger.warning('GPS lat/long not understood (%s/%s)', lat, lon)
return '0', '0'
def _get_cached_gps_coords() -> Tuple[str, str]:
"""
Returns cached GPS coordinates if enB is disconnected or otherwise not
reporting coordinates.
Returns:
(str, str): (GPS lat, GPS lon)
"""
# pylint: disable=global-statement
global _gps_lat_cached, _gps_lon_cached
if _gps_lat_cached is None or _gps_lon_cached is None:
_gps_lat_cached, _gps_lon_cached = _read_gps_coords_from_file()
return _gps_lat_cached, _gps_lon_cached
def _read_gps_coords_from_file():
try:
with open(CACHED_GPS_COORD_FILE_PATH, encoding="utf-8") 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 _cache_new_gps_coords(gps_lat, gps_lon):
"""
Cache GPS coordinates in the module-level variables here and write them
to a managed file on disk.
Args:
gps_lat (str): latitude as a string
gps_lon (str): longitude as a string
"""
# pylint: disable=global-statement
global _gps_lat_cached, _gps_lon_cached
_gps_lat_cached, _gps_lon_cached = gps_lat, gps_lon
_write_gps_coords_to_file(gps_lat, gps_lon)
def _write_gps_coords_to_file(gps_lat, gps_lon):
lines = '{lat}\n{lon}'.format(lat=gps_lat, lon=gps_lon)
try:
serialization_utils.write_to_file_atomically(
CACHED_GPS_COORD_FILE_PATH,
lines,
)
except OSError:
pass
def _bool_to_str(b: bool) -> str:
if b is True:
return "1"
return "0"