Seperating usecase test suite dependencies
[integration/csit.git] / plans / usecases-pnf-sw-upgrade / pnf-sw-upgrade / simulators / pnfsim / pnf-sw-upgrade / subscriber.py
1 #!/usr/bin/env python3
2
3 # ============LICENSE_START=======================================================
4 #  Copyright (C) 2020 Nordix Foundation.
5 # ================================================================================
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
9 #
10 #      http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
17 #
18 # SPDX-License-Identifier: Apache-2.0
19 # ============LICENSE_END=========================================================
20
21 __author__ = "Eliezio Oliveira <eliezio.oliveira@est.tech>"
22 __copyright__ = "Copyright (C) 2020 Nordix Foundation"
23 __license__ = "Apache 2.0"
24
25 import os
26 import time
27 from threading import Timer
28
29 import sysrepo as sr
30 from loguru import logger
31
32 YANG_MODULE_NAME = 'pnf-sw-upgrade'
33
34 XPATH_CTX = sr.Xpath_Ctx()
35 PAUSE_TO_LOCK = 0.5
36
37 #
38 # ----- BEGIN Finite State Machine definitions -----
39 #
40
41 # Actions
42 ACT_PRE_CHECK = 'PRE_CHECK'
43 ACT_DOWNLOAD_NE_SW = 'DOWNLOAD_NE_SW'
44 ACT_ACTIVATE_NE_SW = 'ACTIVATE_NE_SW'
45 ACT_CANCEL = 'CANCEL'
46
47 # States
48 ST_CREATED = 'CREATED'
49 ST_INITIALIZED = 'INITIALIZED'
50 ST_DOWNLOAD_IN_PROGRESS = 'DOWNLOAD_IN_PROGRESS'
51 ST_DOWNLOAD_COMPLETED = 'DOWNLOAD_COMPLETED'
52 ST_ACTIVATION_IN_PROGRESS = 'ACTIVATION_IN_PROGRESS'
53 ST_ACTIVATION_COMPLETED = 'ACTIVATION_COMPLETED'
54
55 # Timeouts used for timed transitions
56 SWUG_TIMED_TRANSITION_TO = int(os.environ.get("SWUG_TIMED_TRANSITION_TO", "7"))
57 TO_DOWNLOAD = SWUG_TIMED_TRANSITION_TO
58 TO_ACTIVATION = SWUG_TIMED_TRANSITION_TO
59
60
61 def timestamper(sess, key_id):
62     xpath = xpath_of(key_id, 'state-change-time')
63     now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
64     state = sr.Val(now, sr.SR_STRING_T)
65     sess.set_item(xpath, state)
66
67
68 def xpath_of(key_id, leaf_id):
69     selector = "[id='{0}']".format(key_id) if key_id else ''
70     return "/%s:software-upgrade/upgrade-package%s/%s" % (YANG_MODULE_NAME, selector, leaf_id)
71
72
73 """
74 The finite state machine (FSM) is represented as a dictionary where the current state is the key, and its value is
75 an object (also represented as a dictionary) with the following optional attributes:
76
77 - on_enter: a function called when FSM enters this state;
78 - transitions: a dictionary mapping every acceptable action to the target state;
79 - timed_transition: a pair for a timed transition that will automatically occur after a given interval.
80 """
81 STATE_MACHINE = {
82     ST_CREATED: {
83         'transitions': {ACT_PRE_CHECK: ST_INITIALIZED}
84     },
85     ST_INITIALIZED: {
86         'on_enter': timestamper,
87         'transitions': {ACT_DOWNLOAD_NE_SW: ST_DOWNLOAD_IN_PROGRESS}
88     },
89     ST_DOWNLOAD_IN_PROGRESS: {
90         'on_enter': timestamper,
91         'timed_transition': (TO_DOWNLOAD, ST_DOWNLOAD_COMPLETED),
92         'transitions': {ACT_CANCEL: ST_INITIALIZED}
93     },
94     ST_DOWNLOAD_COMPLETED: {
95         'on_enter': timestamper,
96         'transitions': {ACT_ACTIVATE_NE_SW: ST_ACTIVATION_IN_PROGRESS}
97     },
98     ST_ACTIVATION_IN_PROGRESS: {
99         'on_enter': timestamper,
100         'timed_transition': (TO_ACTIVATION, ST_ACTIVATION_COMPLETED),
101         'transitions': {ACT_CANCEL: ST_DOWNLOAD_COMPLETED}
102     },
103     ST_ACTIVATION_COMPLETED: {
104         'on_enter': timestamper,
105         'transitions': {ACT_ACTIVATE_NE_SW: ST_ACTIVATION_IN_PROGRESS}
106     }
107 }
108
109
110 #
111 # ----- END Finite State Machine definitions -----
112 #
113
114
115 def main():
116     try:
117         conn = sr.Connection(YANG_MODULE_NAME)
118         sess = sr.Session(conn)
119         subscribe = sr.Subscribe(sess)
120
121         subscribe.module_change_subscribe(YANG_MODULE_NAME, module_change_cb, conn)
122
123         try:
124             print_current_config(sess, YANG_MODULE_NAME)
125         except Exception as e:
126             logger.error(e)
127
128         sr.global_loop()
129
130         logger.info("Application exit requested, exiting.")
131     except Exception as e:
132         logger.error(e)
133
134
135 # Function to be called for subscribed client of given session whenever configuration changes.
136 def module_change_cb(sess, module_name, event, private_ctx):
137     if event == sr.SR_EV_APPLY:
138         try:
139             conn = private_ctx
140             change_path = xpath_of(None, 'action')
141             it = sess.get_changes_iter(change_path)
142             while True:
143                 change = sess.get_change_next(it)
144                 if change is None:
145                     break
146                 op = change.oper()
147                 if op in (sr.SR_OP_CREATED, sr.SR_OP_MODIFIED):
148                     handle_trigger_action(conn, sess, change.new_val())
149         except Exception as e:
150             logger.error(e)
151     return sr.SR_ERR_OK
152
153
154 # Function to print current configuration state.
155 # It does so by loading all the items of a session and printing them out.
156 def print_current_config(session, module_name):
157     select_xpath = f"/{module_name}:*//*"
158     values = session.get_items(select_xpath)
159     if values:
160         logger.info("========== BEGIN CONFIG ==========")
161         for i in range(values.val_cnt()):
162             logger.info(values.val(i).to_string().strip())
163         logger.info("=========== END CONFIG ===========")
164
165
166 def handle_trigger_action(conn, sess, action_val):
167     """
168     Handle individual changes on the model.
169     """
170     logger.info("CREATED/MODIFIED: %s" % action_val.to_string())
171     xpath = action_val.xpath()
172     last_node = XPATH_CTX.last_node(xpath)
173     # Warning: 'key_value' modifies 'xpath'!
174     key_id = XPATH_CTX.key_value(xpath, 'upgrade-package', 'id')
175     if key_id and last_node == 'action':
176         action = action_val.data().get_enum()
177         cur_state = sess.get_item(xpath_of(key_id, 'current-status')).data().get_enum()
178         next_state_str = STATE_MACHINE[cur_state]['transitions'].get(action, None)
179         if next_state_str:
180             Timer(PAUSE_TO_LOCK, try_change_state, (conn, key_id, next_state_str)).start()
181
182
183 def try_change_state(conn, key_id, state_str):
184     sess = sr.Session(conn)
185     try:
186         try:
187             sess.lock_module(YANG_MODULE_NAME)
188         except RuntimeError:
189             logger.warning(f"Retrying after {PAUSE_TO_LOCK}s")
190             Timer(PAUSE_TO_LOCK, try_change_state, (conn, key_id, state_str)).start()
191             return
192         try:
193             state = sr.Val(state_str, sr.SR_ENUM_T)
194             sess.set_item(xpath_of(key_id, 'current-status'), state)
195             on_enter = STATE_MACHINE[state_str].get('on_enter', None)
196             if callable(on_enter):
197                 on_enter(sess, key_id)
198             sess.commit()
199         finally:
200             sess.unlock_module(YANG_MODULE_NAME)
201         delay, next_state_str = STATE_MACHINE[state_str].get('timed_transition', [0, None])
202         if delay:
203             Timer(delay, try_change_state, (conn, key_id, next_state_str)).start()
204     finally:
205         sess.session_stop()
206
207
208 if __name__ == '__main__':
209     main()