...
 
Commits (5)
......@@ -11,3 +11,6 @@ __all__ = [
]
__version__ = '0.1'
# put it elsewhere?
api_version = 1
......@@ -50,7 +50,7 @@ def create_argparser():
parser_action.add_argument("--nodismiss", help="Disable explicit dismiss of message", action="store_false")
parser_action.add_argument("--default-action", help="Set action which will be used as 'default'")
group_add = parser_action.add_mutually_exclusive_group()
group_add = parser_action.add_mutually_exclusive_group(required=True)
group_add.add_argument('--from-json', metavar='JSON', help='Json string with template variables')
group_add.add_argument('--from-env', metavar='ENV_VAR', help='ENV variable which will template variables be read from')
......
......@@ -26,3 +26,6 @@ class NotificationNotDismissibleException(NotifylibError):
class NotificationStorageException(NotifylibError):
pass
class VersionMismatchException(NotifylibError):
pass
......@@ -6,8 +6,9 @@ from datetime import datetime
from jinja2 import TemplateError
from types import SimpleNamespace
from . import api_version
from .config import config
from .exceptions import CreateNotificationError, NotificationTemplatingError
from .exceptions import CreateNotificationError, NotificationTemplatingError, VersionMismatchException
from .notificationskeleton import NotificationSkeleton
from .supervisor import Supervisor
......@@ -15,12 +16,13 @@ logger = logging.getLogger(__name__)
class Notification:
ATTRS = ['notif_id', 'timestamp', 'skeleton', 'persistent', 'timeout', 'severity', 'data', 'fallback', 'valid', 'explicit_dismiss', 'default_action']
ATTRS = ['notif_id', 'api_version', 'timestamp', 'skeleton', 'persistent', 'timeout', 'severity', 'data', 'fallback', 'valid', 'explicit_dismiss', 'default_action']
# TODO: better name?
META_ATTRS = ['persistent', 'timestamp', 'severity', 'default_action']
def __init__(self, notif_id, timestamp, skeleton, data, persistent, timeout, severity, fallback=None, valid=True, explicit_dismiss=True, default_action='dismiss'):
def __init__(self, notif_id, api_version, timestamp, skeleton, data, persistent, timeout, severity, fallback=None, valid=True, explicit_dismiss=True, default_action='dismiss'):
self.notif_id = notif_id
self.api_version = api_version
self.timestamp = timestamp
self.skeleton = skeleton
......@@ -51,13 +53,18 @@ class Notification:
nid = cls._generate_id()
ts = int(datetime.utcnow().timestamp())
n = cls(nid, ts, skel, **opts)
n = cls(nid, api_version, ts, skel, **opts)
return n
@classmethod
def from_file(cls, path, plugin_storage):
"""Load notification from it's file and return new instance"""
"""
Load notification from it's file and return new instance
If there is failure to open/deserialize i.e. get file, return None
If there is invalid content, raise exception
"""
try:
with open(path, 'r') as f:
json_data = json.load(f)
......@@ -68,21 +75,37 @@ class Notification:
logger.warning("Failed to deserialize json file: %s", e)
return None
# Very simple validation based on API version
if not cls.validate_version(json_data):
raise VersionMismatchException
skel_args = json_data['skeleton']
plug = plugin_storage.get_plugin(skel_args['plugin_name'])
skel_args['jinja_env'] = plug.get_jinja_env()
# quick and dirty hack
# TODO: Use json schema or another validation method
try:
skel_obj = NotificationSkeleton(**skel_args)
json_data['skeleton'] = skel_obj # replace json data with skeleton instance
skel_obj = NotificationSkeleton(**skel_args)
json_data['skeleton'] = skel_obj # replace json data with skeleton instance
return cls(**json_data)
except TypeError:
logger.warning("Malformed notification file - skipping")
return None
return cls(**json_data)
@classmethod
def validate_version(cls, data):
"""Validate version of stored notification based on API version"""
if 'api_version' not in data:
logger.warning("Notification doesn't contain API version")
return False
if not isinstance(data['api_version'], int):
logger.warning("API version is not number")
return False
if data['api_version'] < api_version:
logger.warning("Notification was created using older API")
return False
return True
def is_valid(self, timestamp=None):
"""If notification is still valid based on multiple conditions"""
......
......@@ -5,6 +5,7 @@ import logging
from datetime import datetime
from functools import lru_cache
from .exceptions import VersionMismatchException
from .notification import Notification
logger = logging.getLogger(__name__)
......@@ -57,12 +58,22 @@ class NotificationStorage:
def load(self, storage_dir):
"""Deserialize all notifications from FS"""
logger.debug("Deserializing notifications from '%s'", storage_dir)
to_delete = []
for filepath in glob.glob(os.path.join(storage_dir, '*.json')):
n = Notification.from_file(filepath, self.plugin_storage)
try:
n = Notification.from_file(filepath, self.plugin_storage)
if n:
self.notifications[n.notif_id] = n
self.shortid_map[n.notif_id[:self.SHORTID_LENGTH]] = n.notif_id
except VersionMismatchException:
logger.debug("Notification version mismatch - marking to delete")
to_delete.append(filepath)
continue
if n:
self.notifications[n.notif_id] = n
self.shortid_map[n.notif_id[:self.SHORTID_LENGTH]] = n.notif_id
for path in to_delete:
self.remove_file(path)
def valid_id(self, msgid):
"""Check if msgid is valid and message with that id exists"""
......@@ -142,19 +153,23 @@ class NotificationStorage:
storage_dir = self.storage_dirs['volatile']
filepath = os.path.join(storage_dir, "{}.json".format(msgid))
tmp_filepath = "{}.tmp".format(filepath)
self.remove_file(filepath)
try:
os.rename(filepath, tmp_filepath)
except FileNotFoundError as e:
logger.error(e)
return
except IsADirectoryError:
logger.error("Cannot rename file. There already is a directory with the same name!")
return
def remove_file(self, filepath):
"""Remove file from FS"""
logger.debug("Removing file %s", filepath)
tmp_filepath = "{}.tmp".format(filepath)
try:
os.rename(filepath, tmp_filepath)
except FileNotFoundError as e:
logger.error(e)
return
except IsADirectoryError:
logger.error("Cannot rename file. There already is a directory with the same name!")
return
try:
os.unlink(tmp_filepath)
except OSError as e:
logger.error("Cannot remove tempfile '%s'. Reason: %s", tmp_filepath, e)
return
try:
os.unlink(tmp_filepath)
except OSError as e:
logger.error("Cannot remove tempfile '%s'. Reason: %s", tmp_filepath, e)
return