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