Skip to content

Commit d765036

Browse files
authored
Merge pull request #113 from Helene/automatic_config_refresh
Trigger metadata refresh by zimon sensors configuration changes automtically
2 parents 0e4f1b9 + 50e43a6 commit d765036

File tree

5 files changed

+176
-4
lines changed

5 files changed

+176
-4
lines changed

source/bridgeLogger.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ def configureLogging(logPath, logfile, loglevel=logging.INFO):
5454

5555
logToFile = True if logfile else False
5656

57-
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)-8s - %(message)s')
58-
formatter1 = logging.Formatter('%(asctime)s - %(levelname)-8s - %(message)s', datefmt='%Y-%m-%d %H:%M')
57+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(threadName)-20s - %(levelname)-8s - %(message)s')
58+
formatter1 = logging.Formatter('%(asctime)s - %(threadName)-20s - %(levelname)-8s - %(message)s', datefmt='%Y-%m-%d %H:%M')
5959

6060
# prepare the logger
6161
logging.setLoggerClass(MyLogger)
@@ -85,3 +85,7 @@ def configureLogging(logPath, logfile, loglevel=logging.INFO):
8585

8686
logger.propagate = False # prevent propagation to default (console) logger
8787
return logger
88+
89+
90+
def getBridgeLogger():
91+
return logging.getLogger(__name__)

source/messages.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,11 @@
5555
'Query2port': 'For better bridge performance multithreaded port {} will be used',
5656
'CollectorConnInfo': 'Connection to the collector server established successfully',
5757
'BridgeVersionInfo': ' *** IBM Spectrum Scale bridge for Grafana - Version: {} ***',
58-
'FileNotFound': 'The file {} not found.'
58+
'FileNotFound': 'The file {} not found.',
59+
'FileChanged': 'The file {} has been changed.',
60+
'FileAddedToWatch': 'The file {} added to watching files.',
61+
'StartWatchingFiles': 'Start watching file changes in the path {}.',
62+
'StopWatchingFiles': 'Stop watching file changes in the path {}.',
63+
'PathNoCfgFiles': 'The path does not contain any configuration files.',
64+
'UnhandledError': 'Unhandled error: {}'
5965
}

source/watcher.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'''
2+
##############################################################################
3+
# Copyright 2023 IBM Corp.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
##############################################################################
17+
18+
Created on Sep 22, 2023
19+
20+
@author: HWASSMAN
21+
'''
22+
23+
import os
24+
import time
25+
from bridgeLogger import getBridgeLogger
26+
from messages import MSG
27+
28+
29+
class ConfigWatcher(object):
30+
running = False
31+
refresh_delay_secs = 30
32+
33+
def __init__(self, watch_paths, call_func_on_change=None, *args, **kwargs):
34+
self._cached_stamp = {}
35+
self.logger = getBridgeLogger()
36+
self.paths = watch_paths
37+
self.filenames = set()
38+
self.call_func_on_change = call_func_on_change
39+
self.args = args
40+
self.kwargs = kwargs
41+
42+
def update_files_list(self):
43+
oldfiles = self.filenames.copy()
44+
for path in self.paths:
45+
if os.path.isfile(path):
46+
self.filenames.add(path)
47+
elif os.path.isdir(path):
48+
for root, _, files in os.walk(path):
49+
for file in files:
50+
if file.endswith(".cfg"):
51+
self.filenames.add(os.path.join(root, file))
52+
else:
53+
self.logger.trace(MSG['PathNoCfgFiles'].format(path))
54+
for file in self.filenames.difference(oldfiles):
55+
self.logger.debug(MSG['FileAddedToWatch'].format(file))
56+
57+
def look(self):
58+
""" Function to check if a file timestamp has changed"""
59+
for filename in self.filenames:
60+
stamp = os.stat(filename).st_mtime
61+
if filename not in self._cached_stamp:
62+
self._cached_stamp[filename] = stamp
63+
elif stamp != self._cached_stamp[filename]:
64+
self._cached_stamp[filename] = stamp
65+
# File has changed, so do something...
66+
self.logger.info(MSG['FileChanged'].format(filename))
67+
if self.call_func_on_change is not None:
68+
self.call_func_on_change(*self.args, **self.kwargs)
69+
70+
def watch(self):
71+
""" Function to keep watching in a loop """
72+
self.running = True
73+
self.logger.debug(MSG['StartWatchingFiles'].format(self.paths))
74+
while self.running:
75+
try:
76+
# Look for changes
77+
time.sleep(self.refresh_delay_secs)
78+
self.update_files_list()
79+
self.look()
80+
except KeyboardInterrupt:
81+
self.logger.details(MSG['StopWatchingFiles'].format(self.paths))
82+
break
83+
except FileNotFoundError as e:
84+
# Action on file not found
85+
self.logger.warning(MSG['FileNotFound'].format(e.filename))
86+
pass
87+
except Exception as e:
88+
self.logger.warning(MSG['StopWatchingFiles'].format(self.paths))
89+
self.logger.details(MSG['UnhandledError'].format(type(e).__name__))
90+
break
91+
92+
def stop_watch(self):
93+
""" Function to break watching """
94+
self.running = False

source/zimonGrafanaIntf.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@
3737
from metaclasses import Singleton
3838
from bridgeLogger import configureLogging
3939
from confParser import getSettings
40+
from watcher import ConfigWatcher
4041
from collections import defaultdict
4142
from timeit import default_timer as timer
4243
from time import sleep
44+
from threading import Thread
4345

4446

4547
class MetadataHandler(metaclass=Singleton):
@@ -108,11 +110,14 @@ def __initializeTables(self):
108110
return
109111
raise ValueError(MSG['NoData'])
110112

111-
def update(self):
113+
def update(self, refresh_all=False):
112114
'''Read the topology from ZIMon and update
113115
the tables for metrics, keys, key elements (tag keys)
114116
and key values (tag values)'''
115117

118+
if refresh_all:
119+
self.__sensorsConf = SensorConfig.readSensorsConfigFromMMSDRFS(self.logger)
120+
116121
tstart = timer()
117122
self.__metaData = Topo(self.qh.getTopology())
118123
tend = timer()
@@ -606,6 +611,22 @@ def resolveAPIKeyValue(storedKey):
606611
return storedKey
607612

608613

614+
def refresh_metadata(refresh_all=False):
615+
md = MetadataHandler()
616+
md.update(refresh_all)
617+
618+
619+
def watch_config():
620+
files_to_watch = []
621+
if os.path.isfile(SensorConfig.mmsdrfsFile):
622+
files_to_watch.append(SensorConfig.mmsdrfsFile)
623+
else:
624+
files_to_watch.append(SensorConfig.zimonFile)
625+
626+
watcher = ConfigWatcher(files_to_watch, refresh_metadata, refresh_all=True)
627+
watcher.watch()
628+
629+
609630
def main(argv):
610631
# parse input arguments
611632
args, msg = getSettings(argv)
@@ -695,6 +716,9 @@ def main(argv):
695716
try:
696717
cherrypy.engine.start()
697718
logger.info("server started")
719+
t = Thread(name='ConfigWatchThread', target=watch_config)
720+
t.start()
721+
# t.join()
698722
with open("/proc/{}/stat".format(os.getpid())) as f:
699723
data = f.read()
700724
foreground_pid_of_group = data.rsplit(" ", 45)[1]

tests/test_configWatcher.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import os
2+
import time
3+
from source.watcher import ConfigWatcher
4+
from source.bridgeLogger import configureLogging
5+
from nose2.tools.decorators import with_setup
6+
from threading import Thread
7+
8+
9+
def my_setup():
10+
global path, logger, mainSensorsConfig, wrongSensorsConfig, zimonFile, sensorsCount
11+
path = os.getcwd()
12+
logger = configureLogging(path, None)
13+
mainSensorsConfig = 'ZIMonSensors.cfg'
14+
wrongSensorsConfig = 'ZIMonSensors-protocols-wrong.cfg'
15+
sensorsCount = 0
16+
17+
18+
@with_setup(my_setup)
19+
def test_case01():
20+
dummyFile = os.path.join(path, wrongSensorsConfig)
21+
cw = ConfigWatcher([dummyFile])
22+
assert len(cw.paths) == 1
23+
assert len(cw.filenames) == 0
24+
t = Thread(name='ConfigWatchThread', target=cw.watch)
25+
t.start()
26+
time.sleep(3)
27+
cw.stop_watch()
28+
t.join()
29+
assert len(cw.paths) == 1
30+
assert len(cw.filenames) == 0
31+
32+
33+
@with_setup(my_setup)
34+
def test_case02():
35+
cw = ConfigWatcher([path])
36+
assert len(cw.paths) > 0
37+
assert len(cw.filenames) == 0
38+
t = Thread(name='ConfigWatchThread', target=cw.watch)
39+
t.start()
40+
time.sleep(3)
41+
cw.stop_watch()
42+
t.join()
43+
assert len(cw.paths) > 0
44+
assert len(cw.filenames) > 1

0 commit comments

Comments
 (0)