Skip to content

Replace homecooked cache #82

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 19 additions & 80 deletions lib/vsc/utils/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,15 @@

@author: Andy Georges (Ghent University)
"""
import gzip
import jsonpickle
import os
import diskcache as dc
import time
import pickle

from vsc.utils import fancylogger

class FileCache(object):
class FileCache(dc.Cache):
"""File cache with a timestamp safety.

Wrapper around diskcache to retain the old API until all usage can be replaced.

Stores data (something that can be pickled) into a dictionary
indexed by the data.key(). The value stored is a tuple consisting
of (the time of addition to the dictionary, the complete
Expand All @@ -65,52 +63,16 @@ def __init__(self, filename, retain_old=True, raise_unpickable=False):

@param filename: (absolute) path to the cache file.
"""
del raise_unpickable

super().__init__(filename)

self.log = fancylogger.getLogger(self.__class__.__name__, fname=False)
self.filename = filename
self.retain_old = retain_old
self.retain_old = retain_old # this is no longer used

self.new_shelf = {}
if not retain_old:
self.log.info("Starting with a new empty cache, not retaining previous info if any.")
self.shelf = {}
return

try:
with open(self.filename, 'rb') as f:
try:
g = gzip.GzipFile(mode='rb', fileobj=f) # no context manager available in python 26 yet
s = g.read()
except (IOError) as err:
self.log.error("Cannot load data from cache file %s as gzipped json", self.filename)
try:
f.seek(0)
self.shelf = pickle.load(f)
except pickle.UnpicklingError as err:
msg = "Problem loading pickle data from %s (corrupt data)" % (self.filename,)
if raise_unpickable:
self.log.raiseException(msg)
else:
self.log.error("%s. Continue with empty shelf: %s", msg, err)
self.shelf = {}
except (OSError, IOError):
self.log.raiseException("Could not load pickle data from %s", self.filename)
else:
try:
self.shelf = jsonpickle.decode(s)
except ValueError as err:
self.log.error("Cannot decode JSON from %s [%s]", self.filename, err)
self.log.info("Cache in %s starts with an empty shelf", self.filename)
self.shelf = {}
finally:
g.close()

except (OSError, IOError, ValueError, FileNotFoundError) as err:
self.log.warning("Could not access the file cache at %s [%s]", self.filename, err)
self.shelf = {}
self.log.info("Cache in %s starts with an empty shelf", (self.filename,))

def update(self, key, data, threshold):
self.clear()

def update(self, key, data, threshold=None):
"""Update the given data if the existing data is older than the given threshold.

@type key: something that can serve as a dictionary key (and thus can be pickled)
Expand All @@ -122,18 +84,12 @@ def update(self, key, data, threshold):
@param threshold: time in seconds
"""
now = time.time()
old = self.load(key)
if old:
(ts, _) = old
if now - ts > threshold:
self.new_shelf[key] = (now, data)
return True
else:
self.new_shelf[key] = old
return False
stored = self.set(key=key, value=(now, data), expire=threshold)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does this do if threshold is None?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it will not set an expiration time on the key.


if stored:
return (now, data)
else:
self.new_shelf[key] = (now, data)
return True
return (None, None)

def load(self, key):
"""Load the stored data for the given key along with the timestamp it was stored.
Expand All @@ -142,32 +98,15 @@ def load(self, key):

@returns: (timestamp, data) if there is data for the given key, None otherwise.
"""
return self.new_shelf.get(key, None) or self.shelf.get(key, None)
return self.get(key, default=(None, None))

@DeprecationWarning
def retain(self):
"""Retain non-updated data on close."""
self.retain_old = True

@DeprecationWarning
def discard(self):
"""Discard non-updated data on close."""
self.retain_old = False

def close(self):
"""Close the cache."""
dirname = os.path.dirname(self.filename)
if not os.path.exists(dirname):
os.makedirs(dirname)
with open(self.filename, 'wb') as fih:
if not fih:
self.log.error('cannot open the file cache at %s for writing', self.filename)
else:
if self.retain_old:
self.shelf.update(self.new_shelf)
self.new_shelf = self.shelf

with gzip.GzipFile(mode='wb', fileobj=fih) as zipf:
pickled = jsonpickle.encode(self.new_shelf)
# .encode() is required in Python 3, since we need to pass a bytestring
zipf.write(pickled.encode())

self.log.info('closing the file cache at %s', self.filename)
88 changes: 51 additions & 37 deletions lib/vsc/utils/nagios.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
@author: Luis Fernando Muñoz Mejías (Ghent University)
"""

import logging
import operator
import os
import pwd
Expand All @@ -50,12 +51,10 @@
import time

from vsc.utils.cache import FileCache
from vsc.utils.fancylogger import getLogger

log = getLogger(__name__)

NAGIOS_CACHE_DIR = '/var/cache'
NAGIOS_CACHE_FILENAME_TEMPLATE = '%s.nagios.json.gz'
NAGIOS_CACHE_FILENAME_TEMPLATE = '%s.nagios'

NAGIOS_OK = 'OK'
NAGIOS_WARNING = 'WARNING'
Expand Down Expand Up @@ -87,7 +86,7 @@ def _real_exit(message, code, metrics=''):
metrics = '|%s' % message[1]
if len(msg) > NAGIOS_MAX_MESSAGE_LENGTH:
# log long message but print truncated message
log.info("Nagios report %s: %s%s", exit_text, msg, metrics)
logging.info("Nagios report %s: %s%s", exit_text, msg, metrics)
msg = msg[:NAGIOS_MAX_MESSAGE_LENGTH-3] + '...'

print("%s %s%s" % (exit_text, msg, metrics))
Expand Down Expand Up @@ -151,16 +150,17 @@ def __init__(self, nrange):
@param nrange: nrange in [@][start:][end] format. If it is not a string, it is converted to
string and that string should allow conversion to float.
"""
self.log = getLogger(self.__class__.__name__, fname=False)

if not isinstance(nrange, str):
newnrange = str(nrange)
self.log.debug("nrange %s of type %s, converting to string (%s)", str(nrange), type(nrange), newnrange)
logging.debug("nrange %s of type %s, converting to string (%s)", str(nrange), type(nrange), newnrange)
try:
float(newnrange)
except ValueError:
self.log.raiseException("nrange %s (type %s) is not valid after conversion to string (newnrange %s)" %
(str(nrange), type(nrange), newnrange))
logging.exception(
"nrange %s (type %s) is not valid after conversion to string (newnrange %s)",
str(nrange), type(nrange), newnrange
)
raise
nrange = newnrange

self.range_fn = self.parse(nrange)
Expand All @@ -173,7 +173,7 @@ def parse(self, nrange):
r = reg.search(nrange)
if r:
res = r.groupdict()
self.log.debug("parse: nrange %s gave %s", nrange, res)
logging.debug("parse: nrange %s gave %s", nrange, res)

start_txt = res['start']
if start_txt is None:
Expand All @@ -184,26 +184,30 @@ def parse(self, nrange):
try:
start = float(start_txt)
except ValueError:
self.log.raiseException("Invalid start txt value %s" % start_txt)
logging.exception("Invalid start txt value %s", start_txt)
raise

end = res['end']
if end is not None:
try:
end = float(end)
except ValueError:
self.log.raiseException("Invalid end value %s" % end)
logging.exception("Invalid end value %s", end)
raise

neg = res['neg'] is not None
self.log.debug("parse: start %s end %s neg %s", start, end, neg)
logging.debug("parse: start %s end %s neg %s", start, end, neg)
else:
self.log.raiseException('parse: invalid nrange %s.' % nrange)
logging.exception('parse: invalid nrange %s.', nrange)
raise

def range_fn(test):
# test inside nrange?
try:
test = float(test)
except ValueError:
self.log.raiseException("range_fn: can't convert test %s (type %s) to float" % (test, type(test)))
logging.exception("range_fn: can't convert test %s (type %s) to float", test, type(test))
raise

start_res = True # default: -inf < test
if start is not None:
Expand All @@ -219,7 +223,7 @@ def range_fn(test):
if neg:
tmp_res = operator.not_(tmp_res)

self.log.debug("range_fn: test %s start_res %s end_res %s result %s (neg %s)",
logging.debug("range_fn: test %s start_res %s end_res %s result %s (neg %s)",
test, start_res, end_res, tmp_res, neg)
return tmp_res

Expand Down Expand Up @@ -261,27 +265,27 @@ def __init__(self, header, filename, threshold, nagios_username="nagios", world_

self.nagios_username = nagios_username

self.log = getLogger(self.__class__.__name__, fname=False)

def report_and_exit(self):
"""Unzips the cache file and reads the JSON data back in, prints the data and exits accordingly.
"""Reads the cache, prints the data and exits accordingly.

If the cache data is too old (now - cache timestamp > self.threshold), a critical exit is produced.
"""
try:
nagios_cache = FileCache(self.filename, True)
nagios_cache = FileCache(self.filename)
except (IOError, OSError):
self.log.critical("Error opening file %s for reading", self.filename)
unknown_exit("%s nagios gzipped JSON file unavailable (%s)" % (self.header, self.filename))
logging.critical("Error opening file %s for reading", self.filename)
unknown_exit("%s nagios cache unavailable (%s)" % (self.header, self.filename))

(timestamp, ((nagios_exit_code, nagios_exit_string), nagios_message)) = nagios_cache.load('nagios')
(_, nagios_exit_info) = nagios_cache.load('nagios')

if nagios_exit_info is None:
unknown_exit("%s nagios exit info expired" % self.header)

((nagios_exit_code, nagios_exit_string), nagios_message) = nagios_exit_info

print("%s %s" % (nagios_exit_string, nagios_message))
sys.exit(nagios_exit_code)

if self.threshold <= 0 or time.time() - timestamp < self.threshold:
self.log.info("Nagios check cache file %s contents delivered: %s", self.filename, nagios_message)
print("%s %s" % (nagios_exit_string, nagios_message))
sys.exit(nagios_exit_code)
else:
unknown_exit("%s gzipped JSON file too old (timestamp = %s)" % (self.header, time.ctime(timestamp)))

def cache(self, nagios_exit, nagios_message):
"""Store the result in the cache file with a timestamp.
Expand All @@ -294,28 +298,38 @@ def cache(self, nagios_exit, nagios_message):
"""
try:
nagios_cache = FileCache(self.filename)
nagios_cache.update('nagios', (nagios_exit, nagios_message), 0) # always update
nagios_cache.update('nagios', (nagios_exit, nagios_message), threshold=self.threshold)
nagios_cache.close()
self.log.info("Wrote nagios check cache file %s at about %s", self.filename, time.ctime(time.time()))
logging.info("Wrote nagios check cache file %s at about %s", self.filename, time.ctime(time.time()))
except (IOError, OSError):
# raising an error is ok, since we usually do this as the very last thing in the script
self.log.raiseException("Cannot save to the nagios gzipped JSON file (%s)" % self.filename)
logging.error("Cannot save to the nagios cache (%s)", self.filename)
raise

try:
p = pwd.getpwnam(self.nagios_username)
if self.world_readable:
os.chmod(self.filename, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH)
os.chmod(
self.filename,
stat.S_IRUSR | stat.S_IWUSR |
stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH |
stat.S_IXUSR | stat.S_IXGRP
)
else:
os.chmod(self.filename, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP)
os.chmod(
self.filename,
stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IXUSR | stat.S_IXGRP
)

# only change owner/group when run as root
if os.geteuid() == 0:
os.chown(self.filename, p.pw_uid, p.pw_gid)
else:
self.log.warn("Not running as root: Cannot chown the nagios check file %s to %s",
logging.warning("Not running as root: Cannot chown the nagios check file %s to %s",
self.filename, self.nagios_username)
except (OSError, FileNotFoundError):
self.log.raiseException("Cannot chown the nagios check file %s to the nagios user" % (self.filename))
logging.error("Cannot chown the nagios check file %s to the nagios user", self.filename)
raise

return True

Expand Down Expand Up @@ -441,7 +455,7 @@ def __init__(self, **kwargs):
self._final = None
self._final_state = None

self._threshold = 0
self._threshold = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the implication of this?

self._report_and_exit = False

self._world_readable = False
Expand Down
Loading