blob: a9b84a5334b57fd00e1c415290df1cdad6fca5b7 [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"""
13from abc import ABC, abstractmethod
14from collections import namedtuple
15from typing import Any, Optional
16
17from data_models.data_model import InvalidTrParamPath
18from data_models.data_model_parameters import ParameterName
19from device_config.configuration_init import build_desired_config
20from exceptions import ConfigurationError, Tr069Error
21from logger import EnodebdLogger as logger
22from state_machines.acs_state_utils import (
23 does_inform_have_event,
24 get_all_objects_to_add,
25 get_all_objects_to_delete,
26 get_all_param_values_to_set,
27 get_obj_param_values_to_set,
28 get_object_params_to_get,
29 get_optional_param_to_check,
30 get_param_values_to_set,
31 get_params_to_get,
32 parse_get_parameter_values_response,
33 process_inform_message,
34)
35from state_machines.enb_acs import EnodebAcsStateMachine
36from state_machines.timer import StateMachineTimer
37from tr069 import models
38
39AcsMsgAndTransition = namedtuple(
40 'AcsMsgAndTransition', ['msg', 'next_state'],
41)
42
43AcsReadMsgResult = namedtuple(
44 'AcsReadMsgResult', ['msg_handled', 'next_state'],
45)
46
47
48class EnodebAcsState(ABC):
49 """
50 State class for the Enodeb state machine
51
52 States can transition after reading a message from the eNB, sending a
53 message out to the eNB, or when a timer completes. As such, some states
54 are only responsible for message sending, and others are only responsible
55 for reading incoming messages.
56
57 In the constructor, set up state transitions.
58 """
59
60 def __init__(self):
61 self._acs = None
62
63 def enter(self) -> None:
64 """
65 Set up your timers here. Call transition(..) on the ACS when the timer
66 completes or throw an error
67 """
68 pass
69
70 def exit(self) -> None:
71 """Destroy timers here"""
72 pass
73
74 def read_msg(self, message: Any) -> AcsReadMsgResult:
75 """
76 Args: message: tr069 message
77 Returns: name of the next state, if transition required
78 """
79 raise ConfigurationError(
80 '%s should implement read_msg() if it '
81 'needs to handle message reading' % self.__class__.__name__,
82 )
83
84 def get_msg(self, message: Any) -> AcsMsgAndTransition:
85 """
86 Produce a message to send back to the eNB.
87
88 Args:
89 message: TR-069 message which was already processed by read_msg
90
91 Returns: Message and possible transition
92 """
93 raise ConfigurationError(
94 '%s should implement get_msg() if it '
95 'needs to produce messages' % self.__class__.__name__,
96 )
97
98 @property
99 def acs(self) -> EnodebAcsStateMachine:
100 return self._acs
101
102 @acs.setter
103 def acs(self, val: EnodebAcsStateMachine) -> None:
104 self._acs = val
105
106 @abstractmethod
107 def state_description(self) -> str:
108 """ Provide a few words about what the state represents """
109 pass
110
111
112class WaitInformState(EnodebAcsState):
113 """
114 This state indicates that no Inform message has been received yet, or
115 that no Inform message has been received for a long time.
116
117 This state is used to handle an Inform message that arrived when enodebd
118 already believes that the eNB is connected. As such, it is unclear to
119 enodebd whether the eNB is just sending another Inform, or if a different
120 eNB was plugged into the same interface.
121 """
122
123 def __init__(
124 self,
125 acs: EnodebAcsStateMachine,
126 when_done: str,
127 when_boot: Optional[str] = None,
128 ):
129 super().__init__()
130 self.acs = acs
131 self.done_transition = when_done
132 self.boot_transition = when_boot
133 self.has_enb_just_booted = False
134
135 def read_msg(self, message: Any) -> AcsReadMsgResult:
136 """
137 Args:
138 message: models.Inform Tr069 Inform message
139 """
140 if not isinstance(message, models.Inform):
141 return AcsReadMsgResult(False, None)
142 process_inform_message(
143 message, self.acs.data_model,
144 self.acs.device_cfg,
145 )
146 if does_inform_have_event(message, '1 BOOT'):
147 return AcsReadMsgResult(True, self.boot_transition)
148 return AcsReadMsgResult(True, None)
149
150 def get_msg(self, message: Any) -> AcsMsgAndTransition:
151 """ Reply with InformResponse """
152 response = models.InformResponse()
153 # Set maxEnvelopes to 1, as per TR-069 spec
154 response.MaxEnvelopes = 1
155 return AcsMsgAndTransition(response, self.done_transition)
156
157 def state_description(self) -> str:
158 return 'Waiting for an Inform'
159
160
161class GetRPCMethodsState(EnodebAcsState):
162 """
163 After the first Inform message from boot, it is expected that the eNB
164 will try to learn the RPC methods of the ACS.
165 """
166
167 def __init__(self, acs: EnodebAcsStateMachine, when_done: str, when_skip: str):
168 super().__init__()
169 self.acs = acs
170 self.done_transition = when_done
171 self.skip_transition = when_skip
172
173 def read_msg(self, message: Any) -> AcsReadMsgResult:
174 # If this is a regular Inform, not after a reboot we'll get an empty
175 if isinstance(message, models.DummyInput):
176 return AcsReadMsgResult(True, self.skip_transition)
177 if not isinstance(message, models.GetRPCMethods):
178 return AcsReadMsgResult(False, self.done_transition)
179 return AcsReadMsgResult(True, None)
180
181 def get_msg(self, message: Any) -> AcsMsgAndTransition:
182 resp = models.GetRPCMethodsResponse()
183 resp.MethodList = models.MethodList()
184 RPC_METHODS = ['Inform', 'GetRPCMethods', 'TransferComplete']
185 resp.MethodList.arrayType = 'xsd:string[%d]' \
186 % len(RPC_METHODS)
187 resp.MethodList.string = RPC_METHODS
188 return AcsMsgAndTransition(resp, self.done_transition)
189
190 def state_description(self) -> str:
191 return 'Waiting for incoming GetRPC Methods after boot'
192
193
194class BaicellsRemWaitState(EnodebAcsState):
195 """
196 We've already received an Inform message. This state is to handle a
197 Baicells eNodeB issue.
198
199 After eNodeB is rebooted, hold off configuring it for some time to give
200 time for REM to run. This is a BaiCells eNodeB issue that doesn't support
201 enabling the eNodeB during initial REM.
202
203 In this state, just hang at responding to Inform, and then ending the
204 TR-069 session.
205 """
206
207 CONFIG_DELAY_AFTER_BOOT = 600
208
209 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
210 super().__init__()
211 self.acs = acs
212 self.done_transition = when_done
213 self.rem_timer = None
214
215 def enter(self):
216 self.rem_timer = StateMachineTimer(self.CONFIG_DELAY_AFTER_BOOT)
217 logger.info(
218 'Holding off of eNB configuration for %s seconds. '
219 'Will resume after eNB REM process has finished. ',
220 self.CONFIG_DELAY_AFTER_BOOT,
221 )
222
223 def exit(self):
224 self.rem_timer = None
225
226 def read_msg(self, message: Any) -> AcsReadMsgResult:
227 if not isinstance(message, models.Inform):
228 return AcsReadMsgResult(False, None)
229 process_inform_message(
230 message, self.acs.data_model,
231 self.acs.device_cfg,
232 )
233 return AcsReadMsgResult(True, None)
234
235 def get_msg(self, message: Any) -> AcsMsgAndTransition:
236 if self.rem_timer.is_done():
237 return AcsMsgAndTransition(
238 models.DummyInput(),
239 self.done_transition,
240 )
241 return AcsMsgAndTransition(models.DummyInput(), None)
242
243 def state_description(self) -> str:
244 remaining = self.rem_timer.seconds_remaining()
245 return 'Waiting for eNB REM to run for %d more seconds before ' \
246 'resuming with configuration.' % remaining
247
248
249class WaitEmptyMessageState(EnodebAcsState):
250 def __init__(
251 self,
252 acs: EnodebAcsStateMachine,
253 when_done: str,
254 when_missing: Optional[str] = None,
255 ):
256 super().__init__()
257 self.acs = acs
258 self.done_transition = when_done
259 self.unknown_param_transition = when_missing
260
261 def read_msg(self, message: Any) -> AcsReadMsgResult:
262 """
263 It's expected that we transition into this state right after receiving
264 an Inform message and replying with an InformResponse. At that point,
265 the eNB sends an empty HTTP request (aka DummyInput) to initiate the
266 rest of the provisioning process
267 """
268 if not isinstance(message, models.DummyInput):
269 logger.debug("Ignoring message %s", str(type(message)))
270 return AcsReadMsgResult(msg_handled=False, next_state=None)
271 if self.unknown_param_transition:
272 if get_optional_param_to_check(self.acs.data_model):
273 return AcsReadMsgResult(
274 msg_handled=True,
275 next_state=self.unknown_param_transition,
276 )
277 return AcsReadMsgResult(
278 msg_handled=True,
279 next_state=self.done_transition,
280 )
281
282 def get_msg(self, message: Any) -> AcsReadMsgResult:
283 """
284 Return a dummy message waiting for the empty message from CPE
285 """
286 request = models.DummyInput()
287 return AcsMsgAndTransition(msg=request, next_state=None)
288
289 def state_description(self) -> str:
290 return 'Waiting for empty message from eNodeB'
291
292
293class CheckOptionalParamsState(EnodebAcsState):
294 def __init__(
295 self,
296 acs: EnodebAcsStateMachine,
297 when_done: str,
298 ):
299 super().__init__()
300 self.acs = acs
301 self.done_transition = when_done
302 self.optional_param = None
303
304 def get_msg(self, message: Any) -> AcsMsgAndTransition:
305 self.optional_param = get_optional_param_to_check(self.acs.data_model)
306 if self.optional_param is None:
307 raise Tr069Error('Invalid State')
308 # Generate the request
309 request = models.GetParameterValues()
310 request.ParameterNames = models.ParameterNames()
311 request.ParameterNames.arrayType = 'xsd:string[1]'
312 request.ParameterNames.string = []
313 path = self.acs.data_model.get_parameter(self.optional_param).path
314 request.ParameterNames.string.append(path)
315 return AcsMsgAndTransition(request, None)
316
317 def read_msg(self, message: Any) -> AcsReadMsgResult:
318 """ Process either GetParameterValuesResponse or a Fault """
319 if type(message) == models.Fault:
320 self.acs.data_model.set_parameter_presence(
321 self.optional_param,
322 False,
323 )
324 elif type(message) == models.GetParameterValuesResponse:
325 name_to_val = parse_get_parameter_values_response(
326 self.acs.data_model,
327 message,
328 )
329 logger.debug(
330 'Received CPE parameter values: %s',
331 str(name_to_val),
332 )
333 for name, val in name_to_val.items():
334 self.acs.data_model.set_parameter_presence(
335 self.optional_param,
336 True,
337 )
338 magma_val = self.acs.data_model.transform_for_magma(name, val)
339 self.acs.device_cfg.set_parameter(name, magma_val)
340 else:
341 return AcsReadMsgResult(False, None)
342
343 if get_optional_param_to_check(self.acs.data_model) is not None:
344 return AcsReadMsgResult(True, None)
345 return AcsReadMsgResult(True, self.done_transition)
346
347 def state_description(self) -> str:
348 return 'Checking if some optional parameters exist in data model'
349
350
351class SendGetTransientParametersState(EnodebAcsState):
352 """
353 Periodically read eNodeB status. Note: keep frequency low to avoid
354 backing up large numbers of read operations if enodebd is busy.
355 Some eNB parameters are read only and updated by the eNB itself.
356 """
357 PARAMETERS = [
358 ParameterName.OP_STATE,
359 ParameterName.RF_TX_STATUS,
360 ParameterName.GPS_STATUS,
361 ParameterName.PTP_STATUS,
362 ParameterName.MME_STATUS,
363 ParameterName.GPS_LAT,
364 ParameterName.GPS_LONG,
365 ]
366
367 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
368 super().__init__()
369 self.acs = acs
370 self.done_transition = when_done
371
372 def read_msg(self, message: Any) -> AcsReadMsgResult:
373 if not isinstance(message, models.DummyInput):
374 return AcsReadMsgResult(False, None)
375 return AcsReadMsgResult(True, None)
376
377 def get_msg(self, message: Any) -> AcsMsgAndTransition:
378 request = models.GetParameterValues()
379 request.ParameterNames = models.ParameterNames()
380 request.ParameterNames.string = []
381 for name in self.PARAMETERS:
382 # Not all data models have these parameters
383 if self.acs.data_model.is_parameter_present(name):
384 path = self.acs.data_model.get_parameter(name).path
385 request.ParameterNames.string.append(path)
386 request.ParameterNames.arrayType = \
387 'xsd:string[%d]' % len(request.ParameterNames.string)
388
389 return AcsMsgAndTransition(request, self.done_transition)
390
391 def state_description(self) -> str:
392 return 'Getting transient read-only parameters'
393
394
395class WaitGetTransientParametersState(EnodebAcsState):
396 """
397 Periodically read eNodeB status. Note: keep frequency low to avoid
398 backing up large numbers of read operations if enodebd is busy
399 """
400
401 def __init__(
402 self,
403 acs: EnodebAcsStateMachine,
404 when_get: str,
405 when_get_obj_params: str,
406 when_delete: str,
407 when_add: str,
408 when_set: str,
409 when_skip: str,
410 ):
411 super().__init__()
412 self.acs = acs
413 self.done_transition = when_get
414 self.get_obj_params_transition = when_get_obj_params
415 self.rm_obj_transition = when_delete
416 self.add_obj_transition = when_add
417 self.set_transition = when_set
418 self.skip_transition = when_skip
419
420 def read_msg(self, message: Any) -> AcsReadMsgResult:
421 if not isinstance(message, models.GetParameterValuesResponse):
422 return AcsReadMsgResult(False, None)
423 # Current values of the fetched parameters
424 name_to_val = parse_get_parameter_values_response(
425 self.acs.data_model,
426 message,
427 )
428 logger.debug('Fetched Transient Params: %s', str(name_to_val))
429
430 # Update device configuration
431 for name in name_to_val:
432 magma_val = \
433 self.acs.data_model.transform_for_magma(
434 name,
435 name_to_val[name],
436 )
437 self.acs.device_cfg.set_parameter(name, magma_val)
438
439 return AcsReadMsgResult(True, self.get_next_state())
440
441 def get_next_state(self) -> str:
442 should_get_params = \
443 len(
444 get_params_to_get(
445 self.acs.device_cfg,
446 self.acs.data_model,
447 ),
448 ) > 0
449 if should_get_params:
450 return self.done_transition
451 should_get_obj_params = \
452 len(
453 get_object_params_to_get(
454 self.acs.desired_cfg,
455 self.acs.device_cfg,
456 self.acs.data_model,
457 ),
458 ) > 0
459 if should_get_obj_params:
460 return self.get_obj_params_transition
461 elif len(
462 get_all_objects_to_delete(
463 self.acs.desired_cfg,
464 self.acs.device_cfg,
465 ),
466 ) > 0:
467 return self.rm_obj_transition
468 elif len(
469 get_all_objects_to_add(
470 self.acs.desired_cfg,
471 self.acs.device_cfg,
472 ),
473 ) > 0:
474 return self.add_obj_transition
475 return self.skip_transition
476
477 def state_description(self) -> str:
478 return 'Getting transient read-only parameters'
479
480
481class GetParametersState(EnodebAcsState):
482 """
483 Get the value of most parameters of the eNB that are defined in the data
484 model. Object parameters are excluded.
485 """
486
487 def __init__(
488 self,
489 acs: EnodebAcsStateMachine,
490 when_done: str,
491 request_all_params: bool = False,
492 ):
493 super().__init__()
494 self.acs = acs
495 self.done_transition = when_done
496 # Set to True if we want to request values of all parameters, even if
497 # the ACS state machine already has recorded values of them.
498 self.request_all_params = request_all_params
499
500 def read_msg(self, message: Any) -> AcsReadMsgResult:
501 """
502 It's expected that we transition into this state right after receiving
503 an Inform message and replying with an InformResponse. At that point,
504 the eNB sends an empty HTTP request (aka DummyInput) to initiate the
505 rest of the provisioning process
506 """
507 if not isinstance(message, models.DummyInput):
508 return AcsReadMsgResult(False, None)
509 return AcsReadMsgResult(True, None)
510
511 def get_msg(self, message: Any) -> AcsMsgAndTransition:
512 """
513 Respond with GetParameterValuesRequest
514
515 Get the values of all parameters defined in the data model.
516 Also check which addable objects are present, and what the values of
517 parameters for those objects are.
518 """
519
520 # Get the names of regular parameters
521 names = get_params_to_get(
522 self.acs.device_cfg, self.acs.data_model,
523 self.request_all_params,
524 )
525
526 # Generate the request
527 request = models.GetParameterValues()
528 request.ParameterNames = models.ParameterNames()
529 request.ParameterNames.arrayType = 'xsd:string[%d]' \
530 % len(names)
531 request.ParameterNames.string = []
532 for name in names:
533 path = self.acs.data_model.get_parameter(name).path
534 if path is not InvalidTrParamPath:
535 # Only get data elements backed by tr69 path
536 request.ParameterNames.string.append(path)
537
538 return AcsMsgAndTransition(request, self.done_transition)
539
540 def state_description(self) -> str:
541 return 'Getting non-object parameters'
542
543
544class WaitGetParametersState(EnodebAcsState):
545 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
546 super().__init__()
547 self.acs = acs
548 self.done_transition = when_done
549
550 def read_msg(self, message: Any) -> AcsReadMsgResult:
551 """ Process GetParameterValuesResponse """
552 if not isinstance(message, models.GetParameterValuesResponse):
553 return AcsReadMsgResult(False, None)
554 name_to_val = parse_get_parameter_values_response(
555 self.acs.data_model,
556 message,
557 )
558 logger.debug('Received CPE parameter values: %s', str(name_to_val))
559 for name, val in name_to_val.items():
560 magma_val = self.acs.data_model.transform_for_magma(name, val)
561 self.acs.device_cfg.set_parameter(name, magma_val)
562 return AcsReadMsgResult(True, self.done_transition)
563
564 def state_description(self) -> str:
565 return 'Getting non-object parameters'
566
567
568class GetObjectParametersState(EnodebAcsState):
569 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
570 super().__init__()
571 self.acs = acs
572 self.done_transition = when_done
573
574 def get_msg(self, message: Any) -> AcsMsgAndTransition:
575 """ Respond with GetParameterValuesRequest """
576 names = get_object_params_to_get(
577 self.acs.desired_cfg,
578 self.acs.device_cfg,
579 self.acs.data_model,
580 )
581
582 # Generate the request
583 request = models.GetParameterValues()
584 request.ParameterNames = models.ParameterNames()
585 request.ParameterNames.arrayType = 'xsd:string[%d]' \
586 % len(names)
587 request.ParameterNames.string = []
588 for name in names:
589 path = self.acs.data_model.get_parameter(name).path
590 request.ParameterNames.string.append(path)
591
592 return AcsMsgAndTransition(request, self.done_transition)
593
594 def state_description(self) -> str:
595 return 'Getting object parameters'
596
597
598class WaitGetObjectParametersState(EnodebAcsState):
599 def __init__(
600 self,
601 acs: EnodebAcsStateMachine,
602 when_delete: str,
603 when_add: str,
604 when_set: str,
605 when_skip: str,
606 ):
607 super().__init__()
608 self.acs = acs
609 self.rm_obj_transition = when_delete
610 self.add_obj_transition = when_add
611 self.set_params_transition = when_set
612 self.skip_transition = when_skip
613
614 def read_msg(self, message: Any) -> AcsReadMsgResult:
615 """ Process GetParameterValuesResponse """
616 if not isinstance(message, models.GetParameterValuesResponse):
617 return AcsReadMsgResult(False, None)
618
619 path_to_val = {}
620 if hasattr(message.ParameterList, 'ParameterValueStruct') and \
621 message.ParameterList.ParameterValueStruct is not None:
622 for param_value_struct in message.ParameterList.ParameterValueStruct:
623 path_to_val[param_value_struct.Name] = \
624 param_value_struct.Value.Data
625 logger.debug('Received object parameters: %s', str(path_to_val))
626
627 # Number of PLMN objects reported can be incorrect. Let's count them
628 num_plmns = 0
629 obj_to_params = self.acs.data_model.get_numbered_param_names()
630 while True:
631 obj_name = ParameterName.PLMN_N % (num_plmns + 1)
632 if obj_name not in obj_to_params or len(obj_to_params[obj_name]) == 0:
633 logger.warning(
634 "eNB has PLMN %s but not defined in model",
635 obj_name,
636 )
637 break
638 param_name_list = obj_to_params[obj_name]
639 obj_path = self.acs.data_model.get_parameter(param_name_list[0]).path
640 if obj_path not in path_to_val:
641 break
642 if not self.acs.device_cfg.has_object(obj_name):
643 self.acs.device_cfg.add_object(obj_name)
644 num_plmns += 1
645 for name in param_name_list:
646 path = self.acs.data_model.get_parameter(name).path
647 value = path_to_val[path]
648 magma_val = \
649 self.acs.data_model.transform_for_magma(name, value)
650 self.acs.device_cfg.set_parameter_for_object(
651 name, magma_val,
652 obj_name,
653 )
654 num_plmns_reported = \
655 int(self.acs.device_cfg.get_parameter(ParameterName.NUM_PLMNS))
656 if num_plmns != num_plmns_reported:
657 logger.warning(
658 "eNB reported %d PLMNs but found %d",
659 num_plmns_reported, num_plmns,
660 )
661 self.acs.device_cfg.set_parameter(
662 ParameterName.NUM_PLMNS,
663 num_plmns,
664 )
665
666 # Now we can have the desired state
667 if self.acs.desired_cfg is None:
668 self.acs.desired_cfg = build_desired_config(
669 self.acs.mconfig,
670 self.acs.service_config,
671 self.acs.device_cfg,
672 self.acs.data_model,
673 self.acs.config_postprocessor,
674 )
675
676 if len(
677 get_all_objects_to_delete(
678 self.acs.desired_cfg,
679 self.acs.device_cfg,
680 ),
681 ) > 0:
682 return AcsReadMsgResult(True, self.rm_obj_transition)
683 elif len(
684 get_all_objects_to_add(
685 self.acs.desired_cfg,
686 self.acs.device_cfg,
687 ),
688 ) > 0:
689 return AcsReadMsgResult(True, self.add_obj_transition)
690 elif len(
691 get_all_param_values_to_set(
692 self.acs.desired_cfg,
693 self.acs.device_cfg,
694 self.acs.data_model,
695 ),
696 ) > 0:
697 return AcsReadMsgResult(True, self.set_params_transition)
698 return AcsReadMsgResult(True, self.skip_transition)
699
700 def state_description(self) -> str:
701 return 'Getting object parameters'
702
703
704class DeleteObjectsState(EnodebAcsState):
705 def __init__(
706 self,
707 acs: EnodebAcsStateMachine,
708 when_add: str,
709 when_skip: str,
710 ):
711 super().__init__()
712 self.acs = acs
713 self.deleted_param = None
714 self.add_obj_transition = when_add
715 self.skip_transition = when_skip
716
717 def get_msg(self, message: Any) -> AcsMsgAndTransition:
718 """
719 Send DeleteObject message to TR-069 and poll for response(s).
720 Input:
721 - Object name (string)
722 """
723 request = models.DeleteObject()
724 self.deleted_param = get_all_objects_to_delete(
725 self.acs.desired_cfg,
726 self.acs.device_cfg,
727 )[0]
728 request.ObjectName = \
729 self.acs.data_model.get_parameter(self.deleted_param).path
730 return AcsMsgAndTransition(request, None)
731
732 def read_msg(self, message: Any) -> AcsReadMsgResult:
733 """
734 Send DeleteObject message to TR-069 and poll for response(s).
735 Input:
736 - Object name (string)
737 """
738 if type(message) == models.DeleteObjectResponse:
739 if message.Status != 0:
740 raise Tr069Error(
741 'Received DeleteObjectResponse with '
742 'Status=%d' % message.Status,
743 )
744 elif type(message) == models.Fault:
745 raise Tr069Error(
746 'Received Fault in response to DeleteObject '
747 '(faultstring = %s)' % message.FaultString,
748 )
749 else:
750 return AcsReadMsgResult(False, None)
751
752 self.acs.device_cfg.delete_object(self.deleted_param)
753 obj_list_to_delete = get_all_objects_to_delete(
754 self.acs.desired_cfg,
755 self.acs.device_cfg,
756 )
757 if len(obj_list_to_delete) > 0:
758 return AcsReadMsgResult(True, None)
759 if len(
760 get_all_objects_to_add(
761 self.acs.desired_cfg,
762 self.acs.device_cfg,
763 ),
764 ) == 0:
765 return AcsReadMsgResult(True, self.skip_transition)
766 return AcsReadMsgResult(True, self.add_obj_transition)
767
768 def state_description(self) -> str:
769 return 'Deleting objects'
770
771
772class AddObjectsState(EnodebAcsState):
773 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
774 super().__init__()
775 self.acs = acs
776 self.done_transition = when_done
777 self.added_param = None
778
779 def get_msg(self, message: Any) -> AcsMsgAndTransition:
780 request = models.AddObject()
781 self.added_param = get_all_objects_to_add(
782 self.acs.desired_cfg,
783 self.acs.device_cfg,
784 )[0]
785 desired_param = self.acs.data_model.get_parameter(self.added_param)
786 desired_path = desired_param.path
787 path_parts = desired_path.split('.')
788 # If adding enumerated object, ie. XX.N. we should add it to the
789 # parent object XX. so strip the index
790 if len(path_parts) > 2 and \
791 path_parts[-1] == '' and path_parts[-2].isnumeric():
792 logger.debug('Stripping index from path=%s', desired_path)
793 desired_path = '.'.join(path_parts[:-2]) + '.'
794 request.ObjectName = desired_path
795 return AcsMsgAndTransition(request, None)
796
797 def read_msg(self, message: Any) -> AcsReadMsgResult:
798 if type(message) == models.AddObjectResponse:
799 if message.Status != 0:
800 raise Tr069Error(
801 'Received AddObjectResponse with '
802 'Status=%d' % message.Status,
803 )
804 elif type(message) == models.Fault:
805 raise Tr069Error(
806 'Received Fault in response to AddObject '
807 '(faultstring = %s)' % message.FaultString,
808 )
809 else:
810 return AcsReadMsgResult(False, None)
811 instance_n = message.InstanceNumber
812 self.acs.device_cfg.add_object(self.added_param % instance_n)
813 obj_list_to_add = get_all_objects_to_add(
814 self.acs.desired_cfg,
815 self.acs.device_cfg,
816 )
817 if len(obj_list_to_add) > 0:
818 return AcsReadMsgResult(True, None)
819 return AcsReadMsgResult(True, self.done_transition)
820
821 def state_description(self) -> str:
822 return 'Adding objects'
823
824
825class SetParameterValuesState(EnodebAcsState):
826 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
827 super().__init__()
828 self.acs = acs
829 self.done_transition = when_done
830
831 def get_msg(self, message: Any) -> AcsMsgAndTransition:
832 request = models.SetParameterValues()
833 request.ParameterList = models.ParameterValueList()
834 param_values = get_all_param_values_to_set(
835 self.acs.desired_cfg,
836 self.acs.device_cfg,
837 self.acs.data_model,
838 )
839 request.ParameterList.arrayType = 'cwmp:ParameterValueStruct[%d]' \
840 % len(param_values)
841 request.ParameterList.ParameterValueStruct = []
842 logger.debug(
843 'Sending TR069 request to set CPE parameter values: %s',
844 str(param_values),
845 )
846 # TODO: Match key response when we support having multiple outstanding
847 # calls.
848 if self.acs.has_version_key:
849 request.ParameterKey = models.ParameterKeyType()
850 request.ParameterKey.Data =\
851 "SetParameter-{:10.0f}".format(self.acs.parameter_version_key)
852 request.ParameterKey.type = 'xsd:string'
853
854 for name, value in param_values.items():
855 param_info = self.acs.data_model.get_parameter(name)
856 type_ = param_info.type
857 name_value = models.ParameterValueStruct()
858 name_value.Value = models.anySimpleType()
859 name_value.Name = param_info.path
860 enb_value = self.acs.data_model.transform_for_enb(name, value)
861 if type_ in ('int', 'unsignedInt'):
862 name_value.Value.type = 'xsd:%s' % type_
863 name_value.Value.Data = str(enb_value)
864 elif type_ == 'boolean':
865 # Boolean values have integral representations in spec
866 name_value.Value.type = 'xsd:boolean'
867 name_value.Value.Data = str(int(enb_value))
868 elif type_ == 'string':
869 name_value.Value.type = 'xsd:string'
870 name_value.Value.Data = str(enb_value)
871 else:
872 raise Tr069Error(
873 'Unsupported type for %s: %s' %
874 (name, type_),
875 )
876 if param_info.is_invasive:
877 self.acs.are_invasive_changes_applied = False
878 request.ParameterList.ParameterValueStruct.append(name_value)
879
880 return AcsMsgAndTransition(request, self.done_transition)
881
882 def state_description(self) -> str:
883 return 'Setting parameter values'
884
885
886class SetParameterValuesNotAdminState(EnodebAcsState):
887 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
888 super().__init__()
889 self.acs = acs
890 self.done_transition = when_done
891
892 def get_msg(self, message: Any) -> AcsMsgAndTransition:
893 request = models.SetParameterValues()
894 request.ParameterList = models.ParameterValueList()
895 param_values = get_all_param_values_to_set(
896 self.acs.desired_cfg,
897 self.acs.device_cfg,
898 self.acs.data_model,
899 exclude_admin=True,
900 )
901 request.ParameterList.arrayType = 'cwmp:ParameterValueStruct[%d]' \
902 % len(param_values)
903 request.ParameterList.ParameterValueStruct = []
904 logger.debug(
905 'Sending TR069 request to set CPE parameter values: %s',
906 str(param_values),
907 )
908 for name, value in param_values.items():
909 param_info = self.acs.data_model.get_parameter(name)
910 type_ = param_info.type
911 name_value = models.ParameterValueStruct()
912 name_value.Value = models.anySimpleType()
913 name_value.Name = param_info.path
914 enb_value = self.acs.data_model.transform_for_enb(name, value)
915 if type_ in ('int', 'unsignedInt'):
916 name_value.Value.type = 'xsd:%s' % type_
917 name_value.Value.Data = str(enb_value)
918 elif type_ == 'boolean':
919 # Boolean values have integral representations in spec
920 name_value.Value.type = 'xsd:boolean'
921 name_value.Value.Data = str(int(enb_value))
922 elif type_ == 'string':
923 name_value.Value.type = 'xsd:string'
924 name_value.Value.Data = str(enb_value)
925 else:
926 raise Tr069Error(
927 'Unsupported type for %s: %s' %
928 (name, type_),
929 )
930 if param_info.is_invasive:
931 self.acs.are_invasive_changes_applied = False
932 request.ParameterList.ParameterValueStruct.append(name_value)
933
934 return AcsMsgAndTransition(request, self.done_transition)
935
936 def state_description(self) -> str:
937 return 'Setting parameter values excluding Admin Enable'
938
939
940class WaitSetParameterValuesState(EnodebAcsState):
941 def __init__(
942 self,
943 acs: EnodebAcsStateMachine,
944 when_done: str,
945 when_apply_invasive: str,
946 status_non_zero_allowed: bool = False,
947 ):
948 super().__init__()
949 self.acs = acs
950 self.done_transition = when_done
951 self.apply_invasive_transition = when_apply_invasive
952 # Set Params can legally return zero and non zero status
953 # Per tr-196, if there are errors the method should return a fault.
954 # Make flag optional to compensate for existing radios returning non
955 # zero on error.
956 self.status_non_zero_allowed = status_non_zero_allowed
957
958 def read_msg(self, message: Any) -> AcsReadMsgResult:
959 if type(message) == models.SetParameterValuesResponse:
960 if not self.status_non_zero_allowed:
961 if message.Status != 0:
962 raise Tr069Error(
963 'Received SetParameterValuesResponse with '
964 'Status=%d' % message.Status,
965 )
966 self._mark_as_configured()
967 if not self.acs.are_invasive_changes_applied:
968 return AcsReadMsgResult(True, self.apply_invasive_transition)
969 return AcsReadMsgResult(True, self.done_transition)
970 elif type(message) == models.Fault:
971 logger.error(
972 'Received Fault in response to SetParameterValues, '
973 'Code (%s), Message (%s)', message.FaultCode,
974 message.FaultString,
975 )
976 if message.SetParameterValuesFault is not None:
977 for fault in message.SetParameterValuesFault:
978 logger.error(
979 'SetParameterValuesFault Param: %s, '
980 'Code: %s, String: %s', fault.ParameterName,
981 fault.FaultCode, fault.FaultString,
982 )
983 return AcsReadMsgResult(False, None)
984
985 def _mark_as_configured(self) -> None:
986 """
987 A successful attempt at setting parameter values means that we need to
988 update what we think the eNB's configuration is to match what we just
989 set the parameter values to.
990 """
991 # Values of parameters
992 name_to_val = get_param_values_to_set(
993 self.acs.desired_cfg,
994 self.acs.device_cfg,
995 self.acs.data_model,
996 )
997 for name, val in name_to_val.items():
998 magma_val = self.acs.data_model.transform_for_magma(name, val)
999 self.acs.device_cfg.set_parameter(name, magma_val)
1000
1001 # Values of object parameters
1002 obj_to_name_to_val = get_obj_param_values_to_set(
1003 self.acs.desired_cfg,
1004 self.acs.device_cfg,
1005 self.acs.data_model,
1006 )
1007 for obj_name, name_to_val in obj_to_name_to_val.items():
1008 for name, val in name_to_val.items():
1009 logger.debug(
1010 'Set obj: %s, name: %s, val: %s', str(obj_name),
1011 str(name), str(val),
1012 )
1013 magma_val = self.acs.data_model.transform_for_magma(name, val)
1014 self.acs.device_cfg.set_parameter_for_object(
1015 name, magma_val,
1016 obj_name,
1017 )
1018 logger.info('Successfully configured CPE parameters!')
1019
1020 def state_description(self) -> str:
1021 return 'Setting parameter values'
1022
1023
1024class EndSessionState(EnodebAcsState):
1025 """ To end a TR-069 session, send an empty HTTP response """
1026
1027 def __init__(self, acs: EnodebAcsStateMachine):
1028 super().__init__()
1029 self.acs = acs
1030
1031 def read_msg(self, message: Any) -> AcsReadMsgResult:
1032 """
1033 No message is expected after enodebd sends the eNodeB
1034 an empty HTTP response.
1035
1036 If a device sends an empty HTTP request, we can just
1037 ignore it and send another empty response.
1038 """
1039 if isinstance(message, models.DummyInput):
1040 return AcsReadMsgResult(True, None)
1041 return AcsReadMsgResult(False, None)
1042
1043 def get_msg(self, message: Any) -> AcsMsgAndTransition:
1044 request = models.DummyInput()
1045 return AcsMsgAndTransition(request, None)
1046
1047 def state_description(self) -> str:
1048 return 'Completed provisioning eNB. Awaiting new Inform.'
1049
1050
1051class EnbSendRebootState(EnodebAcsState):
1052 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
1053 super().__init__()
1054 self.acs = acs
1055 self.done_transition = when_done
1056 self.prev_msg_was_inform = False
1057
1058 def read_msg(self, message: Any) -> AcsReadMsgResult:
1059 """
1060 This state can be transitioned into through user command.
1061 All messages received by enodebd will be ignored in this state.
1062 """
1063 if self.prev_msg_was_inform \
1064 and not isinstance(message, models.DummyInput):
1065 return AcsReadMsgResult(False, None)
1066 elif isinstance(message, models.Inform):
1067 self.prev_msg_was_inform = True
1068 process_inform_message(
1069 message, self.acs.data_model,
1070 self.acs.device_cfg,
1071 )
1072 return AcsReadMsgResult(True, None)
1073 self.prev_msg_was_inform = False
1074 return AcsReadMsgResult(True, None)
1075
1076 def get_msg(self, message: Any) -> AcsMsgAndTransition:
1077 if self.prev_msg_was_inform:
1078 response = models.InformResponse()
1079 # Set maxEnvelopes to 1, as per TR-069 spec
1080 response.MaxEnvelopes = 1
1081 return AcsMsgAndTransition(response, None)
1082 logger.info('Sending reboot request to eNB')
1083 request = models.Reboot()
1084 request.CommandKey = ''
1085 self.acs.are_invasive_changes_applied = True
1086 return AcsMsgAndTransition(request, self.done_transition)
1087
1088 def state_description(self) -> str:
1089 return 'Rebooting eNB'
1090
1091
1092class SendRebootState(EnodebAcsState):
1093 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
1094 super().__init__()
1095 self.acs = acs
1096 self.done_transition = when_done
1097 self.prev_msg_was_inform = False
1098
1099 def read_msg(self, message: Any) -> AcsReadMsgResult:
1100 """
1101 This state can be transitioned into through user command.
1102 All messages received by enodebd will be ignored in this state.
1103 """
1104 if self.prev_msg_was_inform \
1105 and not isinstance(message, models.DummyInput):
1106 return AcsReadMsgResult(False, None)
1107 elif isinstance(message, models.Inform):
1108 self.prev_msg_was_inform = True
1109 process_inform_message(
1110 message, self.acs.data_model,
1111 self.acs.device_cfg,
1112 )
1113 return AcsReadMsgResult(True, None)
1114 self.prev_msg_was_inform = False
1115 return AcsReadMsgResult(True, None)
1116
1117 def get_msg(self, message: Any) -> AcsMsgAndTransition:
1118 if self.prev_msg_was_inform:
1119 response = models.InformResponse()
1120 # Set maxEnvelopes to 1, as per TR-069 spec
1121 response.MaxEnvelopes = 1
1122 return AcsMsgAndTransition(response, None)
1123 logger.info('Sending reboot request to eNB')
1124 request = models.Reboot()
1125 request.CommandKey = ''
1126 return AcsMsgAndTransition(request, self.done_transition)
1127
1128 def state_description(self) -> str:
1129 return 'Rebooting eNB'
1130
1131
1132class WaitRebootResponseState(EnodebAcsState):
1133 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
1134 super().__init__()
1135 self.acs = acs
1136 self.done_transition = when_done
1137
1138 def read_msg(self, message: Any) -> AcsReadMsgResult:
1139 if not isinstance(message, models.RebootResponse):
1140 return AcsReadMsgResult(False, None)
1141 return AcsReadMsgResult(True, None)
1142
1143 def get_msg(self, message: Any) -> AcsMsgAndTransition:
1144 """ Reply with empty message """
1145 return AcsMsgAndTransition(models.DummyInput(), self.done_transition)
1146
1147 def state_description(self) -> str:
1148 return 'Rebooting eNB'
1149
1150
1151class WaitInformMRebootState(EnodebAcsState):
1152 """
1153 After sending a reboot request, we expect an Inform request with a
1154 specific 'inform event code'
1155 """
1156
1157 # Time to wait for eNodeB reboot. The measured time
1158 # (on BaiCells indoor eNodeB)
1159 # is ~110secs, so add healthy padding on top of this.
1160 REBOOT_TIMEOUT = 300 # In seconds
1161 # We expect that the Inform we receive tells us the eNB has rebooted
1162 INFORM_EVENT_CODE = 'M Reboot'
1163
1164 def __init__(
1165 self,
1166 acs: EnodebAcsStateMachine,
1167 when_done: str,
1168 when_timeout: str,
1169 ):
1170 super().__init__()
1171 self.acs = acs
1172 self.done_transition = when_done
1173 self.timeout_transition = when_timeout
1174 self.timeout_timer = None
1175 self.timer_handle = None
1176
1177 def enter(self):
1178 self.timeout_timer = StateMachineTimer(self.REBOOT_TIMEOUT)
1179
1180 def check_timer() -> None:
1181 if self.timeout_timer.is_done():
1182 self.acs.transition(self.timeout_transition)
1183 raise Tr069Error(
1184 'Did not receive Inform response after '
1185 'rebooting',
1186 )
1187
1188 self.timer_handle = \
1189 self.acs.event_loop.call_later(
1190 self.REBOOT_TIMEOUT,
1191 check_timer,
1192 )
1193
1194 def exit(self):
1195 self.timer_handle.cancel()
1196 self.timeout_timer = None
1197
1198 def read_msg(self, message: Any) -> AcsReadMsgResult:
1199 if not isinstance(message, models.Inform):
1200 return AcsReadMsgResult(False, None)
1201 if not does_inform_have_event(message, self.INFORM_EVENT_CODE):
1202 raise Tr069Error(
1203 'Did not receive M Reboot event code in '
1204 'Inform',
1205 )
1206 process_inform_message(
1207 message, self.acs.data_model,
1208 self.acs.device_cfg,
1209 )
1210 return AcsReadMsgResult(True, self.done_transition)
1211
1212 def state_description(self) -> str:
1213 return 'Waiting for M Reboot code from Inform'
1214
1215
1216class WaitRebootDelayState(EnodebAcsState):
1217 """
1218 After receiving the Inform notifying us that the eNodeB has successfully
1219 rebooted, wait a short duration to prevent unspecified race conditions
1220 that may occur w.r.t reboot
1221 """
1222
1223 # Short delay timer to prevent race conditions w.r.t. reboot
1224 SHORT_CONFIG_DELAY = 10
1225
1226 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
1227 super().__init__()
1228 self.acs = acs
1229 self.done_transition = when_done
1230 self.config_timer = None
1231 self.timer_handle = None
1232
1233 def enter(self):
1234 self.config_timer = StateMachineTimer(self.SHORT_CONFIG_DELAY)
1235
1236 def check_timer() -> None:
1237 if self.config_timer.is_done():
1238 self.acs.transition(self.done_transition)
1239
1240 self.timer_handle = \
1241 self.acs.event_loop.call_later(
1242 self.SHORT_CONFIG_DELAY,
1243 check_timer,
1244 )
1245
1246 def exit(self):
1247 self.timer_handle.cancel()
1248 self.config_timer = None
1249
1250 def read_msg(self, message: Any) -> AcsReadMsgResult:
1251 return AcsReadMsgResult(True, None)
1252
1253 def get_msg(self, message: Any) -> AcsMsgAndTransition:
1254 return AcsMsgAndTransition(models.DummyInput(), None)
1255
1256 def state_description(self) -> str:
1257 return 'Waiting after eNB reboot to prevent race conditions'
1258
1259
1260class ErrorState(EnodebAcsState):
1261 """
1262 The eNB handler will enter this state when an unhandled Fault is received.
1263
1264 If the inform_transition_target constructor parameter is non-null, this
1265 state will attempt to autoremediate by transitioning to the specified
1266 target state when an Inform is received.
1267 """
1268
1269 def __init__(
1270 self, acs: EnodebAcsStateMachine,
1271 inform_transition_target: Optional[str] = None,
1272 ):
1273 super().__init__()
1274 self.acs = acs
1275 self.inform_transition_target = inform_transition_target
1276
1277 def read_msg(self, message: Any) -> AcsReadMsgResult:
1278 return AcsReadMsgResult(True, None)
1279
1280 def get_msg(self, message: Any) -> AcsMsgAndTransition:
1281 if not self.inform_transition_target:
1282 return AcsMsgAndTransition(models.DummyInput(), None)
1283
1284 if isinstance(message, models.Inform):
1285 return AcsMsgAndTransition(
1286 models.DummyInput(),
1287 self.inform_transition_target,
1288 )
1289 return AcsMsgAndTransition(models.DummyInput(), None)
1290
1291 def state_description(self) -> str:
1292 return 'Error state - awaiting manual restart of enodebd service or ' \
1293 'an Inform to be received from the eNB'