blob: 6b4b6216d737b3e5654d8ba74b3a9a429eadd897 [file] [log] [blame]
Wei-Yu Chenad55cb82022-02-15 20:07:01 +08001# SPDX-FileCopyrightText: 2020 The Magma Authors.
2# SPDX-FileCopyrightText: 2022 Open Networking Foundation <support@opennetworking.org>
3#
4# SPDX-License-Identifier: BSD-3-Clause
Wei-Yu Chen49950b92021-11-08 19:19:18 +08005
6import asyncio
7from xml.etree import ElementTree
8
9from aiohttp import web
10from common.misc_utils import get_ip_from_if
11from configuration.service_configs import load_service_config
12from enodeb_status import get_enb_status, update_status_metrics
13from logger import EnodebdLogger as logger
14from state_machines.enb_acs import EnodebAcsStateMachine
15from state_machines.enb_acs_manager import StateMachineManager
16
17import metrics
18
19
20class StatsManager:
21 """ HTTP server to receive performance management uploads from eNodeB and
22 translate to metrics """
23 # Dict to map performance counter names (from eNodeB) to metrics
24 # For eNodeB sub-counters, the counter name is shown as
25 # '<counter>:<sub-counter>'
26 PM_FILE_TO_METRIC_MAP = {
27 'RRC.AttConnEstab': metrics.STAT_RRC_ESTAB_ATT,
28 'RRC.SuccConnEstab': metrics.STAT_RRC_ESTAB_SUCC,
29 'RRC.AttConnReestab': metrics.STAT_RRC_REESTAB_ATT,
30 'RRC.AttConnReestab._Cause:RRC.AttConnReestab.RECONF_FAIL':
31 metrics.STAT_RRC_REESTAB_ATT_RECONF_FAIL,
32 'RRC.AttConnReestab._Cause:RRC.AttConnReestab.HO_FAIL':
33 metrics.STAT_RRC_REESTAB_ATT_HO_FAIL,
34 'RRC.AttConnReestab._Cause:RRC.AttConnReestab.OTHER':
35 metrics.STAT_RRC_REESTAB_ATT_OTHER,
36 'RRC.SuccConnReestab': metrics.STAT_RRC_REESTAB_SUCC,
37 'ERAB.NbrAttEstab': metrics.STAT_ERAB_ESTAB_ATT,
38 'ERAB.NbrSuccEstab': metrics.STAT_ERAB_ESTAB_SUCC,
39 'ERAB.NbrFailEstab': metrics.STAT_ERAB_ESTAB_FAIL,
40 'ERAB.NbrReqRelEnb': metrics.STAT_ERAB_REL_REQ,
41 'ERAB.NbrReqRelEnb.CauseUserInactivity':
42 metrics.STAT_ERAB_REL_REQ_USER_INAC,
43 'ERAB.NbrReqRelEnb.Normal': metrics.STAT_ERAB_REL_REQ_NORMAL,
44 'ERAB.NbrReqRelEnb._Cause:ERAB.NbrReqRelEnb.CauseRADIORESOURCESNOTAVAILABLE':
45 metrics.STAT_ERAB_REL_REQ_RES_NOT_AVAIL,
46 'ERAB.NbrReqRelEnb._Cause:ERAB.NbrReqRelEnb.CauseREDUCELOADINSERVINGCELL':
47 metrics.STAT_ERAB_REL_REQ_REDUCE_LOAD,
48 'ERAB.NbrReqRelEnb._Cause:ERAB.NbrReqRelEnb.CauseFAILUREINTHERADIOINTERFACEPROCEDURE':
49 metrics.STAT_ERAB_REL_REQ_FAIL_IN_RADIO,
50 'ERAB.NbrReqRelEnb._Cause:ERAB.NbrReqRelEnb.CauseRELEASEDUETOEUTRANGENERATEDREASONS':
51 metrics.STAT_ERAB_REL_REQ_EUTRAN_REAS,
52 'ERAB.NbrReqRelEnb._Cause:ERAB.NbrReqRelEnb.CauseRADIOCONNECTIONWITHUELOST':
53 metrics.STAT_ERAB_REL_REQ_RADIO_CONN_LOST,
54 'ERAB.NbrReqRelEnb._Cause:ERAB.NbrReqRelEnb.CauseOAMINTERVENTION':
55 metrics.STAT_ERAB_REL_REQ_OAM_INTV,
56 'PDCP.UpOctUl': metrics.STAT_PDCP_USER_PLANE_BYTES_UL,
57 'PDCP.UpOctDl': metrics.STAT_PDCP_USER_PLANE_BYTES_DL,
58 }
59
60 # Check if radio transmit is turned on every 10 seconds.
61 CHECK_RF_TX_PERIOD = 10
62
63 def __init__(self, enb_acs_manager: StateMachineManager):
64 self.enb_manager = enb_acs_manager
65 self.loop = asyncio.get_event_loop()
66 self._prev_rf_tx = False
67 self.mme_timeout_handler = None
68
69 def run(self) -> None:
70 """ Create and start HTTP server """
71 svc_config = load_service_config("enodebd")
72
73 app = web.Application()
74 app.router.add_route(
75 'POST', "/{something}",
76 self._post_and_put_handler,
77 )
78 app.router.add_route(
79 'PUT', "/{something}",
80 self._post_and_put_handler,
81 )
82
83 handler = app.make_handler()
84 create_server_func = self.loop.create_server(
85 handler,
86 host=get_ip_from_if(svc_config['tr069']['interface']),
87 port=svc_config['tr069']['perf_mgmt_port'],
88 )
89
90 self._periodic_check_rf_tx()
91 self.loop.run_until_complete(create_server_func)
92
93 def _periodic_check_rf_tx(self) -> None:
94 self._check_rf_tx()
95 self.mme_timeout_handler = self.loop.call_later(
96 self.CHECK_RF_TX_PERIOD,
97 self._periodic_check_rf_tx,
98 )
99
100 def _check_rf_tx(self) -> None:
101 """
102 Check if eNodeB should be connected to MME but isn't, and maybe reboot.
103
104 If the eNB doesn't report connection to MME within a timeout period,
105 get it to reboot in the hope that it will fix things.
106
107 Usually, enodebd polls the eNodeB for whether it is connected to MME.
108 This method checks the last polled MME connection status, and if
109 eNodeB should be connected to MME but it isn't.
110 """
111 # Clear stats when eNodeB stops radiating. This is
112 # because eNodeB stops sending performance metrics at this point.
113 serial_list = self.enb_manager.get_connected_serial_id_list()
114 for enb_serial in serial_list:
115 handler = self.enb_manager.get_handler_by_serial(enb_serial)
116 if handler:
117 self._check_rf_tx_for_handler(handler)
118
119 def _check_rf_tx_for_handler(self, handler: EnodebAcsStateMachine) -> None:
120 status = get_enb_status(handler)
121 if self._prev_rf_tx and not status.rf_tx_on:
122 self._clear_stats()
123 self._prev_rf_tx = status.rf_tx_on
124
125 # Update status metrics
126 update_status_metrics(status)
127
128 def _get_enb_label_from_request(self, request) -> str:
129 label = 'default'
130 ip = request.headers.get('X-Forwarded-For')
131
132 if ip is None:
133 peername = request.transport.get_extra_info('peername')
134 if peername is not None:
135 ip, _ = peername
136
137 if ip is None:
138 return label
139
140 label = self.enb_manager.get_serial_of_ip(ip)
141 if label:
142 logger.debug('Found serial %s for ip %s', label, ip)
143 else:
144 logger.error("Couldn't find serial for ip", ip)
145 return label
146
147 @asyncio.coroutine
148 def _post_and_put_handler(self, request) -> web.Response:
149 """ HTTP POST handler """
150 # Read request body and convert to XML tree
151 body = yield from request.read()
152
153 root = ElementTree.fromstring(body)
154 label = self._get_enb_label_from_request(request)
155 if label:
156 self._parse_pm_xml(label, root)
157
158 # Return success response
159 return web.Response()
160
161 def _parse_pm_xml(self, enb_label, xml_root) -> None:
162 """
163 Parse performance management XML from eNodeB and populate metrics.
164 The schema for this XML document, along with an example, is shown in
165 tests/stats_manager_tests.py.
166 """
167 for measurement in xml_root.findall('Measurements'):
168 object_type = measurement.findtext('ObjectType')
169 names = measurement.find('PmName')
170 data = measurement.find('PmData')
171 if object_type == 'EutranCellTdd':
172 self._parse_tdd_counters(enb_label, names, data)
173 elif object_type == 'ManagedElement':
174 # Currently no counters to parse
175 pass
176 elif object_type == 'SctpAssoc':
177 # Currently no counters to parse
178 pass
179
180 def _parse_tdd_counters(self, enb_label, names, data):
181 """
182 Parse eNodeB performance management counters from TDD structure.
183 Most of the logic is just to extract the correct counter based on the
184 name of the statistic. Each counter is either of type 'V', which is a
185 single integer value, or 'CV', which contains multiple integer
186 sub-elements, named 'SV', which we add together. E.g:
187 <V i="9">0</V>
188 <CV i="10">
189 <SN>RRC.AttConnReestab.RECONF_FAIL</SN>
190 <SV>0</SV>
191 <SN>RRC.AttConnReestab.HO_FAIL</SN>
192 <SV>0</SV>
193 <SN>RRC.AttConnReestab.OTHER</SN>
194 <SV>0</SV>
195 </CV>
196 See tests/stats_manager_tests.py for a more complete example.
197 """
198 index_data_map = self._build_index_to_data_map(data)
199 name_index_map = self._build_name_to_index_map(names)
200
201 # For each performance metric, extract value from XML document and set
202 # internal metric to that value.
203 for pm_name, metric in self.PM_FILE_TO_METRIC_MAP.items():
204
205 elements = pm_name.split(':')
206 counter = elements.pop(0)
207 if len(elements) == 0:
208 subcounter = None
209 else:
210 subcounter = elements.pop(0)
211
212 index = name_index_map.get(counter)
213 if index is None:
214 logger.warning('PM counter %s not found in PmNames', counter)
215 continue
216
217 data_el = index_data_map.get(index)
218 if data_el is None:
219 logger.warning('PM counter %s not found in PmData', counter)
220 continue
221
222 if data_el.tag == 'V':
223 if subcounter is not None:
224 logger.warning('No subcounter in PM counter %s', counter)
225 continue
226
227 # Data is singular value
228 try:
229 value = int(data_el.text)
230 except ValueError:
231 logger.info(
232 'PM value (%s) of counter %s not integer',
233 data_el.text, counter,
234 )
235 continue
236 elif data_el.tag == 'CV':
237 # Check whether we want just one subcounter, or sum them all
238 subcounter_index = None
239 if subcounter is not None:
240 index = 0
241 for sub_name_el in data_el.findall('SN'):
242 if sub_name_el.text == subcounter:
243 subcounter_index = index
244 index = index + 1
245
246 if subcounter is not None and subcounter_index is None:
247 logger.warning('PM subcounter (%s) not found', subcounter)
248 continue
249
250 # Data is multiple sub-elements. Sum them, or select the one
251 # of interest
252 value = 0
253 try:
254 index = 0
255 for sub_data_el in data_el.findall('SV'):
256 if subcounter_index is None or \
257 subcounter_index == index:
258 value = value + int(sub_data_el.text)
259 index = index + 1
260 except ValueError:
261 logger.error(
262 'PM value (%s) of counter %s not integer',
263 sub_data_el.text, pm_name,
264 )
265 continue
266 else:
267 logger.warning(
268 'Unknown PM data type (%s) of counter %s',
269 data_el.tag, pm_name,
270 )
271 continue
272
273 # Apply new value to metric
274 if pm_name == 'PDCP.UpOctUl' or pm_name == 'PDCP.UpOctDl':
275 metric.labels(enb_label).set(value)
276 else:
277 metric.set(value)
278
279 def _build_index_to_data_map(self, data_etree):
280 """
281 Parse XML ElementTree and build a dict mapping index to data XML
282 element. The relevant part of XML schema being parsed is:
283 <xs:element name="PmData">
284 <xs:complexType>
285 <xs:sequence minOccurs="0" maxOccurs="unbounded">
286 <xs:element name="Pm">
287 <xs:complexType>
288 <xs:choice minOccurs="0" maxOccurs="unbounded">
289 <xs:element name="V">
290 <xs:complexType>
291 <xs:simpleContent>
292 <xs:extension base="xs:string">
293 <xs:attribute name="i" type="xs:integer" use="required"/>
294 </xs:extension>
295 </xs:simpleContent>
296 </xs:complexType>
297 </xs:element>
298 <xs:element name="CV">
299 <xs:complexType>
300 <xs:sequence minOccurs="0" maxOccurs="unbounded">
301 <xs:element name="SN" type="xs:string"/>
302 <xs:element name="SV" type="xs:string"/>
303 </xs:sequence>
304 <xs:attribute name="i" type="xs:integer" use="required"/>
305 </xs:complexType>
306 </xs:element>
307 </xs:choice>
308 <xs:attribute name="Dn" type="xs:string" use="required"/>
309 <xs:attribute name="UserLabel" type="xs:string" use="required"/>
310 </xs:complexType>
311 </xs:element>
312 </xs:sequence>
313 </xs:complexType>
314 </xs:element>
315
316 Inputs:
317 - XML elementree element corresponding to 'PmData' in above schema
318 Outputs:
319 - Dict mapping index ('i' in above schema) to data elementree
320 elements ('V' and 'CV' in above schema)
321 """
322 # Construct map of index to pm_data XML element
323 index_data_map = {}
324 for pm_el in data_etree.findall('Pm'):
325 for data_el in pm_el.findall('V'):
326 index = data_el.get('i')
327 if index is not None:
328 index_data_map[index] = data_el
329 for data_el in pm_el.findall('CV'):
330 index = data_el.get('i')
331 if index is not None:
332 index_data_map[index] = data_el
333
334 return index_data_map
335
336 def _build_name_to_index_map(self, name_etree):
337 """
338 Parse XML ElementTree and build a dict mapping name to index. The
339 relevant part of XML schema being parsed is:
340 <xs:element name="PmName">
341 <xs:complexType>
342 <xs:sequence minOccurs="0" maxOccurs="unbounded">
343 <xs:element name="N">
344 <xs:complexType>
345 <xs:simpleContent>
346 <xs:extension base="xs:string">
347 <xs:attribute name="i" type="xs:integer" use="required"/>
348 </xs:extension>
349 </xs:simpleContent>
350 </xs:complexType>
351 </xs:element>
352 </xs:sequence>
353 </xs:complexType>
354 </xs:element>
355
356 Inputs:
357 - XML elementree element corresponding to 'PmName' in above schema
358 Outputs:
359 - Dict mapping name ('N' in above schema) to index ('i' in above
360 schema)
361 """
362 # Construct map of pm_name to index
363 name_index_map = {}
364 for name_el in name_etree.findall('N'):
365 name_index_map[name_el.text] = name_el.get('i')
366
367 return name_index_map
368
369 def _clear_stats(self) -> None:
370 """
371 Clear statistics. Called when eNodeB management plane disconnects
372 """
373 logger.info('Clearing performance counter statistics')
374 # Set all metrics to 0 if eNodeB not connected
375 for pm_name, metric in self.PM_FILE_TO_METRIC_MAP.items():
376 # eNB data usage metrics will not be cleared
377 if pm_name not in ('PDCP.UpOctUl', 'PDCP.UpOctDl'):
378 metric.set(0)