...
 
Commits (5)
...@@ -11,3 +11,6 @@ __all__ = [ ...@@ -11,3 +11,6 @@ __all__ = [
] ]
__version__ = '0.1' __version__ = '0.1'
# put it elsewhere?
api_version = 1
...@@ -50,7 +50,7 @@ def create_argparser(): ...@@ -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("--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'") 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-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') 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): ...@@ -26,3 +26,6 @@ class NotificationNotDismissibleException(NotifylibError):
class NotificationStorageException(NotifylibError): class NotificationStorageException(NotifylibError):
pass pass
class VersionMismatchException(NotifylibError):
pass
...@@ -6,8 +6,9 @@ from datetime import datetime ...@@ -6,8 +6,9 @@ from datetime import datetime
from jinja2 import TemplateError from jinja2 import TemplateError
from types import SimpleNamespace from types import SimpleNamespace
from . import api_version
from .config import config from .config import config
from .exceptions import CreateNotificationError, NotificationTemplatingError from .exceptions import CreateNotificationError, NotificationTemplatingError, VersionMismatchException
from .notificationskeleton import NotificationSkeleton from .notificationskeleton import NotificationSkeleton
from .supervisor import Supervisor from .supervisor import Supervisor
...@@ -15,12 +16,13 @@ logger = logging.getLogger(__name__) ...@@ -15,12 +16,13 @@ logger = logging.getLogger(__name__)
class Notification: 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? # TODO: better name?
META_ATTRS = ['persistent', 'timestamp', 'severity', 'default_action'] 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.notif_id = notif_id
self.api_version = api_version
self.timestamp = timestamp self.timestamp = timestamp
self.skeleton = skeleton self.skeleton = skeleton
...@@ -51,13 +53,18 @@ class Notification: ...@@ -51,13 +53,18 @@ class Notification:
nid = cls._generate_id() nid = cls._generate_id()
ts = int(datetime.utcnow().timestamp()) ts = int(datetime.utcnow().timestamp())
n = cls(nid, ts, skel, **opts) n = cls(nid, api_version, ts, skel, **opts)
return n return n
@classmethod @classmethod
def from_file(cls, path, plugin_storage): 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: try:
with open(path, 'r') as f: with open(path, 'r') as f:
json_data = json.load(f) json_data = json.load(f)
...@@ -68,21 +75,37 @@ class Notification: ...@@ -68,21 +75,37 @@ class Notification:
logger.warning("Failed to deserialize json file: %s", e) logger.warning("Failed to deserialize json file: %s", e)
return None return None
# Very simple validation based on API version
if not cls.validate_version(json_data):
raise VersionMismatchException
skel_args = json_data['skeleton'] skel_args = json_data['skeleton']
plug = plugin_storage.get_plugin(skel_args['plugin_name']) plug = plugin_storage.get_plugin(skel_args['plugin_name'])
skel_args['jinja_env'] = plug.get_jinja_env() skel_args['jinja_env'] = plug.get_jinja_env()
# quick and dirty hack
# TODO: Use json schema or another validation method # TODO: Use json schema or another validation method
try: skel_obj = NotificationSkeleton(**skel_args)
skel_obj = NotificationSkeleton(**skel_args) json_data['skeleton'] = skel_obj # replace json data with skeleton instance
json_data['skeleton'] = skel_obj # replace json data with skeleton instance
return cls(**json_data) return cls(**json_data)
except TypeError:
logger.warning("Malformed notification file - skipping") @classmethod
return None 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): def is_valid(self, timestamp=None):
"""If notification is still valid based on multiple conditions""" """If notification is still valid based on multiple conditions"""
......
...@@ -5,6 +5,7 @@ import logging ...@@ -5,6 +5,7 @@ import logging
from datetime import datetime from datetime import datetime
from functools import lru_cache from functools import lru_cache
from .exceptions import VersionMismatchException
from .notification import Notification from .notification import Notification
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -57,12 +58,22 @@ class NotificationStorage: ...@@ -57,12 +58,22 @@ class NotificationStorage:
def load(self, storage_dir): def load(self, storage_dir):
"""Deserialize all notifications from FS""" """Deserialize all notifications from FS"""
logger.debug("Deserializing notifications from '%s'", storage_dir) logger.debug("Deserializing notifications from '%s'", storage_dir)
to_delete = []
for filepath in glob.glob(os.path.join(storage_dir, '*.json')): 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: for path in to_delete:
self.notifications[n.notif_id] = n self.remove_file(path)
self.shortid_map[n.notif_id[:self.SHORTID_LENGTH]] = n.notif_id
def valid_id(self, msgid): def valid_id(self, msgid):
"""Check if msgid is valid and message with that id exists""" """Check if msgid is valid and message with that id exists"""
...@@ -142,19 +153,23 @@ class NotificationStorage: ...@@ -142,19 +153,23 @@ class NotificationStorage:
storage_dir = self.storage_dirs['volatile'] storage_dir = self.storage_dirs['volatile']
filepath = os.path.join(storage_dir, "{}.json".format(msgid)) filepath = os.path.join(storage_dir, "{}.json".format(msgid))
tmp_filepath = "{}.tmp".format(filepath) self.remove_file(filepath)
try: def remove_file(self, filepath):
os.rename(filepath, tmp_filepath) """Remove file from FS"""
except FileNotFoundError as e: logger.debug("Removing file %s", filepath)
logger.error(e) tmp_filepath = "{}.tmp".format(filepath)
return try:
except IsADirectoryError: os.rename(filepath, tmp_filepath)
logger.error("Cannot rename file. There already is a directory with the same name!") except FileNotFoundError as e:
return logger.error(e)
return
except IsADirectoryError:
logger.error("Cannot rename file. There already is a directory with the same name!")
return
try: try:
os.unlink(tmp_filepath) os.unlink(tmp_filepath)
except OSError as e: except OSError as e:
logger.error("Cannot remove tempfile '%s'. Reason: %s", tmp_filepath, e) logger.error("Cannot remove tempfile '%s'. Reason: %s", tmp_filepath, e)
return return