Skip to content

Commit 4456df2

Browse files
committed
Added automatic model reloading
1 parent e3106a5 commit 4456df2

11 files changed

Lines changed: 133 additions & 68 deletions

File tree

docs/AssetsConfigurations.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11

22
# Asset Configurations
33

4-
## UPS
5-
6-
### Agents
4+
## Agents
75

86
You can check if ipmi/snmp simulators are up & running by issuing status command:
97

108
`simengine-cli status --asset-key={key} --agent`
119

10+
## UPS
1211

1312
### SNMP Configurations
1413

docs/Installation.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Simengine uses OpenIPMI lanserv simulator for its BMC emulations and libvirt for
2020
```
2121
dnf install libvirt OpenIPMI OpenIPMI-lanserv OpenIPMI-libs OpenIPMI-devel python3-libvirt -y
2222
dnf install gcc redis -y
23+
dnf install ipmitool -y #for testing
2324
```
2425

2526
## Neo4J
@@ -62,6 +63,29 @@ Change location to `dashboard/fronend`
6263

6364
Run `npm install` and then `npm start`
6465

66+
## MIBs
67+
68+
Vendor-specific mibs may need to be installed for the testing purposes.
69+
70+
`dnf install net-snmp net-snmp-utils`
71+
72+
Add mib definitions:
73+
74+
`mkdir /usr/share/snmp/mibs/apc`
75+
76+
`cp data/ups/powernet426.mib /usr/share/snmp/mibs/apc/ # copy from simengine project`
77+
78+
Create configuration file:
79+
80+
`vi /etc/snmp/snmp.conf`
81+
82+
Paste `mibdirs` reference:
83+
84+
```
85+
mibdirs /usr/share/snmp/mibs:/usr/share/snmp/mibs/apc
86+
mibs ALL
87+
```
88+
6589
## Running
6690

6791
`simengine-cli` will need to be put into `$PATH` as `export PATH="$PATH:/path/to/simengine/enginecore"`

enginecore/app.py

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
import tempfile
88

99
from enginecore.state.state_listener import StateListener
10-
from enginecore.state.state_initializer import initialize
11-
1210

1311
def configure_env(relative=False):
1412
"""Set-up defaults for the env vars if not defined
@@ -33,20 +31,6 @@ def configure_env(relative=False):
3331
)
3432

3533

36-
def clear_temp():
37-
"""All app data is stored in /tmp/simengine (which is cleared on restart)"""
38-
sys_temp = tempfile.gettempdir()
39-
simengine_temp = os.path.join(sys_temp, 'simengine')
40-
if os.path.exists(simengine_temp):
41-
for the_file in os.listdir(simengine_temp):
42-
file_path = os.path.join(simengine_temp, the_file)
43-
if os.path.isfile(file_path):
44-
os.unlink(file_path)
45-
elif os.path.isdir(file_path):
46-
shutil.rmtree(file_path)
47-
else:
48-
os.makedirs(simengine_temp)
49-
5034
def run():
5135
"""
5236
Initilize compnents' states in redis based on a reference model
@@ -64,17 +48,11 @@ def run():
6448

6549
args = vars(argparser.parse_args())
6650

67-
# set-up temp app space
68-
clear_temp()
69-
7051
# env space configuration
7152
configure_env(relative=args['develop'])
7253

73-
# init state
74-
initialize(force_snmp_init=args['reload_data'])
75-
7654
# run daemon
77-
StateListener(debug=args['verbose']).run()
55+
StateListener(debug=args['verbose'], force_snmp_init=args['reload_data']).run()
7856

7957
if __name__ == '__main__':
8058
run()

enginecore/enginecore/state/redis_channels.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ class RedisChannels():
1010
load_update_channel = 'load-upd'
1111
state_update_channel = 'state-upd'
1212
oid_update_channel = 'oid-upd'
13+
model_update_channel = 'model-upd'
14+
15+
# battery states
1316
battery_update_channel = 'battery-upd'
1417
battery_conf_drain_channel = 'battery-drain-upd'
1518
battery_conf_charge_channel = 'battery-charge-upd'

enginecore/enginecore/state/state_initializer.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,29 @@
11
"""Initialize redis state based on reference model """
22
import os
3+
import tempfile
4+
import shutil
5+
36
import redis
7+
48
from enginecore.model.graph_reference import GraphReference
59
from enginecore.state.utils import format_as_redis_key, get_asset_type
610

11+
12+
def clear_temp():
13+
"""All app data is stored in /tmp/simengine (which is cleared on restart)"""
14+
sys_temp = tempfile.gettempdir()
15+
simengine_temp = os.path.join(sys_temp, 'simengine')
16+
if os.path.exists(simengine_temp):
17+
for the_file in os.listdir(simengine_temp):
18+
file_path = os.path.join(simengine_temp, the_file)
19+
if os.path.isfile(file_path):
20+
os.unlink(file_path)
21+
elif os.path.isdir(file_path):
22+
shutil.rmtree(file_path)
23+
else:
24+
os.makedirs(simengine_temp)
25+
26+
727
def initialize(force_snmp_init=False):
828
""" Initialize redis state using topology defined in the graph db """
929

enginecore/enginecore/state/state_listener.py

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from enginecore.state.web_socket import WebSocket
2020
from enginecore.state.redis_channels import RedisChannels
2121
from enginecore.model.graph_reference import GraphReference
22+
from enginecore.state.state_initializer import initialize, clear_temp
2223

2324
class NotifyClient(Event):
2425
"""Notify websocket clients of any data updates"""
@@ -27,36 +28,26 @@ class StateListener(Component):
2728
"""Top-level component that instantiates assets & maps redis events to circuit events"""
2829

2930

30-
def __init__(self, debug=False):
31+
def __init__(self, debug=False, force_snmp_init=False):
3132
super(StateListener, self).__init__()
3233

3334
### Set-up WebSocket & Redis listener ###
3435

35-
# subscribe to redis key events
36-
self.redis_store = redis.StrictRedis(host='localhost', port=6379)
36+
# Use redis pub/sub communication
37+
self._redis_store = redis.StrictRedis(host='localhost', port=6379)
3738

38-
# State Channels
39-
self.pubsub = self.redis_store.pubsub()
40-
self.pubsub.psubscribe(
41-
RedisChannels.oid_update_channel, # snmp oid updates
42-
RedisChannels.state_update_channel # power state changes
43-
)
44-
45-
# Battery Channel
46-
self.bat_pubsub = self.redis_store.pubsub()
47-
self.bat_pubsub.psubscribe(
48-
RedisChannels.battery_update_channel, # battery level updates
49-
RedisChannels.battery_conf_drain_channel, # update drain speed (factor)
50-
RedisChannels.battery_conf_charge_channel # update charge speed (factor)
51-
)
39+
self._bat_pubsub = self._redis_store.pubsub()
40+
self._state_pubsub = self._redis_store.pubsub()
5241

5342
# assets will store all the devices/items including PDUs, switches etc.
5443
self._assets = {}
44+
45+
# init graph db instance
5546
self._graph_ref = GraphReference()
56-
self._graph_db = self._graph_ref.get_session()
5747

58-
# set up a web socket
59-
self._server = Server(("0.0.0.0", 8000)).register(self)
48+
# set up a web socket server
49+
self._server = Server(("0.0.0.0", 8000)).register(self)
50+
6051
# Worker(process=False).register(self)
6152
Static().register(self._server)
6253
Logger().register(self._server)
@@ -65,10 +56,39 @@ def __init__(self, debug=False):
6556
Debugger(events=False).register(self)
6657

6758
self._ws = WebSocket().register(self._server)
68-
6959
WebSocketsDispatcher("/simengine").register(self._server)
7060

7161
### Register Assets ###
62+
self._subscribe_to_channels()
63+
self._reload_model(force_snmp_init)
64+
65+
66+
def _subscribe_to_channels(self):
67+
"""Subscribe to redis channels"""
68+
69+
# State Channels
70+
self._state_pubsub.psubscribe(
71+
RedisChannels.oid_update_channel, # snmp oid updates
72+
RedisChannels.state_update_channel, # power state changes
73+
RedisChannels.model_update_channel # model changes
74+
)
75+
76+
# Battery Channel
77+
self._bat_pubsub.psubscribe(
78+
RedisChannels.battery_update_channel, # battery level updates
79+
RedisChannels.battery_conf_drain_channel, # update drain speed (factor)
80+
RedisChannels.battery_conf_charge_channel # update charge speed (factor)
81+
)
82+
83+
84+
def _reload_model(self, force_snmp_init=True):
85+
"""Re-create system topology (instantiate assets based on graph ref)"""
86+
87+
self._assets = {}
88+
89+
# init state
90+
clear_temp()
91+
initialize(force_snmp_init)
7292

7393
# instantiate assets based on graph records
7494
leaf_nodes = []
@@ -147,10 +167,6 @@ def _handle_state_update(self, asset_key):
147167
self._chain_power_update(PowerEventResult(asset_key=asset_key, new_state=asset_status))
148168

149169

150-
def __exit__(self, exc_type, exc_value, traceback):
151-
self._graph_db.close()
152-
153-
154170
def _chain_load_update(self, event_result, increased=True):
155171
"""React to load update event by propogating the load changes up the power stream
156172
@@ -317,7 +333,7 @@ def _notify_client(self, asset_key, data):
317333

318334
def monitor_battery(self):
319335
"""Monitor battery in a separate pub/sub stream"""
320-
message = self.bat_pubsub.get_message()
336+
message = self._bat_pubsub.get_message()
321337

322338
# validate message
323339
if ((not message) or ('data' not in message) or (not isinstance(message['data'], bytes))):
@@ -346,7 +362,7 @@ def monitor_state(self):
346362
""" listens to redis events """
347363

348364
print("...")
349-
message = self.pubsub.get_message()
365+
message = self._state_pubsub.get_message()
350366

351367
# validate message
352368
if ((not message) or ('data' not in message) or (not isinstance(message['data'], bytes))):
@@ -364,14 +380,22 @@ def monitor_state(self):
364380
self._handle_state_update(int(asset_key))
365381

366382
elif message['channel'] == str.encode(RedisChannels.oid_update_channel):
367-
value = (self.redis_store.get(data)).decode()
383+
value = (self._redis_store.get(data)).decode()
368384
asset_key, oid = data.split('-')
369385
self._handle_oid_update(int(asset_key), oid, value)
370386

371387
elif message['channel'] == str.encode(RedisChannels.battery_update_channel):
372388
asset_key, _ = data.split('-')
373389
self._notify_client(int(asset_key), {'battery': self._assets[int(asset_key)].state.battery_level})
374390

391+
elif message['channel'] == str.encode(RedisChannels.model_update_channel):
392+
print('RELOAD REQUESTED')
393+
self._state_pubsub.unsubscribe()
394+
self._bat_pubsub.unsubscribe()
395+
396+
self._reload_model()
397+
self._subscribe_to_channels()
398+
375399
except KeyError as error:
376400
print("Detected unregistered asset under key [{}]".format(error), file=sys.stderr)
377401

enginecore/enginecore/state/state_managers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,16 @@ def get_system_status(cls, flatten=True):
295295
return assets
296296

297297

298+
@classmethod
299+
def reload_model(cls):
300+
"""Request daemon reloading"""
301+
StateManager.get_store().publish(RedisChannels.model_update_channel, 'reload')
302+
303+
298304
@classmethod
299305
def get_state_manager_by_key(cls, key, supported_assets, notify=True):
306+
"""Infer asset manager from key"""
307+
300308
graph_ref = GraphReference()
301309

302310
with graph_ref.get_session() as session:

enginecore/simengine-cli

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@ import json
77
import time
88
import curses
99
from enginecore.state.assets import Asset
10-
from enginecore.model.graph_reference import GraphReference
1110
import enginecore.model.system_modeler as sm
1211
from enginecore.state.state_managers import StateManager
13-
from enginecore.state.utils import get_asset_type
1412

1513

1614
def manage_state(asset_key, mng_action):
@@ -48,10 +46,16 @@ def status_table_format(assets, stdscr=False):
4846
for i, asset_key in enumerate(assets):
4947
asset = assets[asset_key]
5048
children = str(asset['children'] if 'children' in asset else "none")
51-
row = row_format.format(str(i), *[str(asset_key), asset['type'], str(asset['status']), children, "{0:.2f}".format(asset['load'])], end='')
49+
row = row_format.format(
50+
str(i),
51+
*[str(asset_key), asset['type'], str(asset['status']), children, "{0:.2f}".format(asset['load'])],
52+
end=''
53+
)
5254

5355
if stdscr:
54-
stdscr.addstr(i+1, 0, row, curses.color_pair(bcolors.ERROR if int(asset['status']) == 0 else bcolors.OKGREEN))
56+
stdscr.addstr(
57+
i+1, 0, row, curses.color_pair(bcolors.ERROR if int(asset['status']) == 0 else bcolors.OKGREEN)
58+
)
5559
else:
5660
print(row)
5761

@@ -132,10 +136,10 @@ def get_status(**kwargs):
132136

133137
def configure_battery(key, kwargs):
134138
"""Udpate runtime battery status"""
135-
if kwargs['drain_speed'] != None:
139+
if kwargs['drain_speed'] is not None:
136140
state_manager = Asset.get_state_manager_by_key(key, notify=True)
137141
state_manager.set_drain_speed_factor(kwargs['drain_speed'])
138-
if kwargs['charge_speed'] != None:
142+
if kwargs['charge_speed'] is not None:
139143
state_manager = Asset.get_state_manager_by_key(key, notify=True)
140144
state_manager.set_charge_speed_factor(kwargs['charge_speed'])
141145

@@ -203,6 +207,8 @@ asset_group = subparsers.add_parser('model', help="Manage system model: create n
203207
model_subp = asset_group.add_subparsers()
204208
create_asset_group = model_subp.add_parser('create', help="Create new asset")
205209

210+
reload_asset_action = model_subp.add_parser('reload', help="Reload the system topology (notify daemon of model changes)")
211+
206212
# detach & delete an asset by key
207213
delete_asset_action = model_subp.add_parser('delete', help="Remove individual asset by key")
208214
delete_asset_action.add_argument('-k', '--asset-key', type=int, required=True)
@@ -572,6 +578,10 @@ create_static_action.set_defaults(
572578
for action in update_actions:
573579
action.set_defaults(func=lambda args: sm.configure_asset(args['asset_key'], args))
574580

581+
reload_asset_action.set_defaults(
582+
func=lambda args: StateManager.reload_model()
583+
)
584+
575585
delete_asset_action.set_defaults(
576586
func=lambda args: sm.delete_asset(args['asset_key'])
577587
)

enginecore/tests/server_load_m1.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ class ServerLoadTest(unittest.TestCase):
3333
def setUpClass(cls):
3434
cls.redis_store = redis.StrictRedis(host='localhost', port=6379)
3535

36-
def setUp(self):
37-
3836
server_attr = {
3937
'domain_name': 'an-a01n01',
4038
'psu_num': 2,
@@ -64,6 +62,9 @@ def setUp(self):
6462
sm.link_assets(33, 5)
6563
sm.link_assets(2, 42)
6664

65+
StateManager.reload_model()
66+
time.sleep(3)
67+
6768

6869
def check_redis_values(self, expected_kv):
6970
for key, value in expected_kv.items():

0 commit comments

Comments
 (0)