mirror of https://github.com/caronc/apprise
Added support for remote syslog servers (#442)
parent
109841d72e
commit
0c7e32390e
|
@ -80,7 +80,7 @@ The table below identifies the services this tool supports and some example serv
|
||||||
| [SMTP2Go](https://github.com/caronc/apprise/wiki/Notify_smtp2go) | smtp2go:// | (TCP) 443 | smtp2go://user@hostname/apikey<br />smtp2go://user@hostname/apikey/email<br />smtp2go://user@hostname/apikey/email1/email2/emailN<br />smtp2go://user@hostname/apikey/?name="From%20User"
|
| [SMTP2Go](https://github.com/caronc/apprise/wiki/Notify_smtp2go) | smtp2go:// | (TCP) 443 | smtp2go://user@hostname/apikey<br />smtp2go://user@hostname/apikey/email<br />smtp2go://user@hostname/apikey/email1/email2/emailN<br />smtp2go://user@hostname/apikey/?name="From%20User"
|
||||||
| [SparkPost](https://github.com/caronc/apprise/wiki/Notify_sparkpost) | sparkpost:// | (TCP) 443 | sparkpost://user@hostname/apikey<br />sparkpost://user@hostname/apikey/email<br />sparkpost://user@hostname/apikey/email1/email2/emailN<br />sparkpost://user@hostname/apikey/?name="From%20User"
|
| [SparkPost](https://github.com/caronc/apprise/wiki/Notify_sparkpost) | sparkpost:// | (TCP) 443 | sparkpost://user@hostname/apikey<br />sparkpost://user@hostname/apikey/email<br />sparkpost://user@hostname/apikey/email1/email2/emailN<br />sparkpost://user@hostname/apikey/?name="From%20User"
|
||||||
| [Spontit](https://github.com/caronc/apprise/wiki/Notify_spontit) | spontit:// | (TCP) 443 | spontit://UserID@APIKey/<br />spontit://UserID@APIKey/Channel<br />spontit://UserID@APIKey/Channel1/Channel2/ChannelN
|
| [Spontit](https://github.com/caronc/apprise/wiki/Notify_spontit) | spontit:// | (TCP) 443 | spontit://UserID@APIKey/<br />spontit://UserID@APIKey/Channel<br />spontit://UserID@APIKey/Channel1/Channel2/ChannelN
|
||||||
| [Syslog](https://github.com/caronc/apprise/wiki/Notify_syslog) | syslog:// | n/a | syslog://<br />syslog://Facility
|
| [Syslog](https://github.com/caronc/apprise/wiki/Notify_syslog) | syslog:// | (UDP) 514 (_if hostname specified_) | syslog://<br />syslog://Facility<br />syslog://hostname<br />syslog://hostname/Facility
|
||||||
| [Telegram](https://github.com/caronc/apprise/wiki/Notify_telegram) | tgram:// | (TCP) 443 | tgram://bottoken/ChatID<br />tgram://bottoken/ChatID1/ChatID2/ChatIDN
|
| [Telegram](https://github.com/caronc/apprise/wiki/Notify_telegram) | tgram:// | (TCP) 443 | tgram://bottoken/ChatID<br />tgram://bottoken/ChatID1/ChatID2/ChatIDN
|
||||||
| [Twitter](https://github.com/caronc/apprise/wiki/Notify_twitter) | twitter:// | (TCP) 443 | twitter://CKey/CSecret/AKey/ASecret<br/>twitter://user@CKey/CSecret/AKey/ASecret<br/>twitter://CKey/CSecret/AKey/ASecret/User1/User2/User2<br/>twitter://CKey/CSecret/AKey/ASecret?mode=tweet
|
| [Twitter](https://github.com/caronc/apprise/wiki/Notify_twitter) | twitter:// | (TCP) 443 | twitter://CKey/CSecret/AKey/ASecret<br/>twitter://user@CKey/CSecret/AKey/ASecret<br/>twitter://CKey/CSecret/AKey/ASecret/User1/User2/User2<br/>twitter://CKey/CSecret/AKey/ASecret?mode=tweet
|
||||||
| [Twist](https://github.com/caronc/apprise/wiki/Notify_twist) | twist:// | (TCP) 443 | twist://pasword:login<br/>twist://password:login/#channel<br/>twist://password:login/#team:channel<br/>twist://password:login/#team:channel1/channel2/#team3:channel
|
| [Twist](https://github.com/caronc/apprise/wiki/Notify_twist) | twist:// | (TCP) 443 | twist://pasword:login<br/>twist://password:login/#channel<br/>twist://password:login/#team:channel<br/>twist://password:login/#team:channel1/channel2/#team3:channel
|
||||||
|
|
|
@ -22,12 +22,15 @@
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
import os
|
||||||
|
import six
|
||||||
import syslog
|
import syslog
|
||||||
|
import socket
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .NotifyBase import NotifyBase
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
|
from ..utils import is_hostname
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
@ -98,6 +101,21 @@ SYSLOG_FACILITY_RMAP = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SyslogMode(object):
|
||||||
|
# A local query
|
||||||
|
LOCAL = "local"
|
||||||
|
|
||||||
|
# A remote query
|
||||||
|
REMOTE = "remote"
|
||||||
|
|
||||||
|
|
||||||
|
# webhook modes are placed ito this list for validation purposes
|
||||||
|
SYSLOG_MODES = (
|
||||||
|
SyslogMode.LOCAL,
|
||||||
|
SyslogMode.REMOTE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NotifySyslog(NotifyBase):
|
class NotifySyslog(NotifyBase):
|
||||||
"""
|
"""
|
||||||
A wrapper for Syslog Notifications
|
A wrapper for Syslog Notifications
|
||||||
|
@ -119,13 +137,14 @@ class NotifySyslog(NotifyBase):
|
||||||
# local anyway
|
# local anyway
|
||||||
request_rate_per_sec = 0
|
request_rate_per_sec = 0
|
||||||
|
|
||||||
# Title to be added to body if present
|
|
||||||
title_maxlen = 0
|
|
||||||
|
|
||||||
# Define object templates
|
# Define object templates
|
||||||
templates = (
|
templates = (
|
||||||
'{schema}://',
|
'{schema}://',
|
||||||
'{schema}://{facility}',
|
'{schema}://{facility}',
|
||||||
|
'{schema}://{host}',
|
||||||
|
'{schema}://{host}:{port}',
|
||||||
|
'{schema}://{host}/{facility}',
|
||||||
|
'{schema}://{host}:{port}/{facility}',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Define our template tokens
|
# Define our template tokens
|
||||||
|
@ -136,6 +155,18 @@ class NotifySyslog(NotifyBase):
|
||||||
'values': [k for k in SYSLOG_FACILITY_MAP.keys()],
|
'values': [k for k in SYSLOG_FACILITY_MAP.keys()],
|
||||||
'default': SyslogFacility.USER,
|
'default': SyslogFacility.USER,
|
||||||
},
|
},
|
||||||
|
'host': {
|
||||||
|
'name': _('Hostname'),
|
||||||
|
'type': 'string',
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
'port': {
|
||||||
|
'name': _('Port'),
|
||||||
|
'type': 'int',
|
||||||
|
'min': 1,
|
||||||
|
'max': 65535,
|
||||||
|
'default': 514,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
# Define our template arguments
|
# Define our template arguments
|
||||||
|
@ -144,6 +175,12 @@ class NotifySyslog(NotifyBase):
|
||||||
# We map back to the same element defined in template_tokens
|
# We map back to the same element defined in template_tokens
|
||||||
'alias_of': 'facility',
|
'alias_of': 'facility',
|
||||||
},
|
},
|
||||||
|
'mode': {
|
||||||
|
'name': _('Syslog Mode'),
|
||||||
|
'type': 'choice:string',
|
||||||
|
'values': SYSLOG_MODES,
|
||||||
|
'default': SyslogMode.LOCAL,
|
||||||
|
},
|
||||||
'logpid': {
|
'logpid': {
|
||||||
'name': _('Log PID'),
|
'name': _('Log PID'),
|
||||||
'type': 'bool',
|
'type': 'bool',
|
||||||
|
@ -158,8 +195,8 @@ class NotifySyslog(NotifyBase):
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
def __init__(self, facility=None, log_pid=True, log_perror=False,
|
def __init__(self, facility=None, mode=None, log_pid=True,
|
||||||
**kwargs):
|
log_perror=False, **kwargs):
|
||||||
"""
|
"""
|
||||||
Initialize Syslog Object
|
Initialize Syslog Object
|
||||||
"""
|
"""
|
||||||
|
@ -179,6 +216,14 @@ class NotifySyslog(NotifyBase):
|
||||||
SYSLOG_FACILITY_MAP[
|
SYSLOG_FACILITY_MAP[
|
||||||
self.template_tokens['facility']['default']]
|
self.template_tokens['facility']['default']]
|
||||||
|
|
||||||
|
self.mode = self.template_args['mode']['default'] \
|
||||||
|
if not isinstance(mode, six.string_types) else mode.lower()
|
||||||
|
|
||||||
|
if self.mode not in SYSLOG_MODES:
|
||||||
|
msg = 'The mode specified ({}) is invalid.'.format(mode)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
# Logging Options
|
# Logging Options
|
||||||
self.logoptions = 0
|
self.logoptions = 0
|
||||||
|
|
||||||
|
@ -214,8 +259,13 @@ class NotifySyslog(NotifyBase):
|
||||||
NotifyType.WARNING: syslog.LOG_WARNING,
|
NotifyType.WARNING: syslog.LOG_WARNING,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if title:
|
||||||
|
# Format title
|
||||||
|
body = '{}: {}'.format(title, body)
|
||||||
|
|
||||||
# Always call throttle before any remote server i/o is made
|
# Always call throttle before any remote server i/o is made
|
||||||
self.throttle()
|
self.throttle()
|
||||||
|
if self.mode == SyslogMode.LOCAL:
|
||||||
try:
|
try:
|
||||||
syslog.syslog(_pmap[notify_type], body)
|
syslog.syslog(_pmap[notify_type], body)
|
||||||
|
|
||||||
|
@ -226,6 +276,60 @@ class NotifySyslog(NotifyBase):
|
||||||
'({}) was specified.'.format(notify_type))
|
'({}) was specified.'.format(notify_type))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
else: # SyslogMode.REMOTE
|
||||||
|
|
||||||
|
host = self.host
|
||||||
|
port = self.port if self.port \
|
||||||
|
else self.template_tokens['port']['default']
|
||||||
|
if self.log_pid:
|
||||||
|
payload = '<%d>- %d - %s' % (
|
||||||
|
_pmap[notify_type] + self.facility * 8, os.getpid(), body)
|
||||||
|
|
||||||
|
else:
|
||||||
|
payload = '<%d>- %s' % (
|
||||||
|
_pmap[notify_type] + self.facility * 8, body)
|
||||||
|
|
||||||
|
# send UDP packet to upstream server
|
||||||
|
self.logger.debug(
|
||||||
|
'Syslog Host: %s:%d/%s',
|
||||||
|
host, port, SYSLOG_FACILITY_RMAP[self.facility])
|
||||||
|
self.logger.debug('Syslog Payload: %s' % str(payload))
|
||||||
|
|
||||||
|
# our sent bytes
|
||||||
|
sent = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.settimeout(self.socket_connect_timeout)
|
||||||
|
sent = sock.sendto(payload.encode('utf-8'), (host, port))
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
except socket.gaierror as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'A connection error occurred sending Syslog '
|
||||||
|
'notification to %s:%d/%s', host, port,
|
||||||
|
SYSLOG_FACILITY_RMAP[self.facility]
|
||||||
|
)
|
||||||
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
except socket.timeout as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'A connection timeout occurred sending Syslog '
|
||||||
|
'notification to %s:%d/%s', host, port,
|
||||||
|
SYSLOG_FACILITY_RMAP[self.facility]
|
||||||
|
)
|
||||||
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
if sent < len(payload):
|
||||||
|
self.logger.warning(
|
||||||
|
'Syslog sent %d byte(s) but intended to send %d byte(s)',
|
||||||
|
sent, len(payload))
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.logger.info('Sent Syslog (%s) notification.', self.mode)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def url(self, privacy=False, *args, **kwargs):
|
def url(self, privacy=False, *args, **kwargs):
|
||||||
|
@ -237,11 +341,13 @@ class NotifySyslog(NotifyBase):
|
||||||
params = {
|
params = {
|
||||||
'logperror': 'yes' if self.log_perror else 'no',
|
'logperror': 'yes' if self.log_perror else 'no',
|
||||||
'logpid': 'yes' if self.log_pid else 'no',
|
'logpid': 'yes' if self.log_pid else 'no',
|
||||||
|
'mode': self.mode,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extend our parameters
|
# Extend our parameters
|
||||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||||
|
|
||||||
|
if self.mode == SyslogMode.LOCAL:
|
||||||
return '{schema}://{facility}/?{params}'.format(
|
return '{schema}://{facility}/?{params}'.format(
|
||||||
facility=self.template_tokens['facility']['default']
|
facility=self.template_tokens['facility']['default']
|
||||||
if self.facility not in SYSLOG_FACILITY_RMAP
|
if self.facility not in SYSLOG_FACILITY_RMAP
|
||||||
|
@ -250,6 +356,19 @@ class NotifySyslog(NotifyBase):
|
||||||
params=NotifySyslog.urlencode(params),
|
params=NotifySyslog.urlencode(params),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Remote mode:
|
||||||
|
return '{schema}://{hostname}{port}/{facility}/?{params}'.format(
|
||||||
|
schema=self.secure_protocol,
|
||||||
|
hostname=NotifySyslog.quote(self.host, safe=''),
|
||||||
|
port='' if self.port is None
|
||||||
|
or self.port == self.template_tokens['port']['default']
|
||||||
|
else ':{}'.format(self.port),
|
||||||
|
facility=self.template_tokens['facility']['default']
|
||||||
|
if self.facility not in SYSLOG_FACILITY_RMAP
|
||||||
|
else SYSLOG_FACILITY_RMAP[self.facility],
|
||||||
|
params=NotifySyslog.urlencode(params),
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_url(url):
|
def parse_url(url):
|
||||||
"""
|
"""
|
||||||
|
@ -262,9 +381,28 @@ class NotifySyslog(NotifyBase):
|
||||||
# We're done early as we couldn't load the results
|
# We're done early as we couldn't load the results
|
||||||
return results
|
return results
|
||||||
|
|
||||||
# if specified; save hostname into facility
|
tokens = []
|
||||||
facility = None if not results['host'] \
|
if results['host']:
|
||||||
else NotifySyslog.unquote(results['host'])
|
tokens.append(NotifySyslog.unquote(results['host']))
|
||||||
|
|
||||||
|
# Get our path values
|
||||||
|
tokens.extend(NotifySyslog.split_path(results['fullpath']))
|
||||||
|
|
||||||
|
facility = None
|
||||||
|
if len(tokens) > 1 and is_hostname(tokens[0]):
|
||||||
|
# syslog://hostname/facility
|
||||||
|
results['mode'] = SyslogMode.REMOTE
|
||||||
|
|
||||||
|
# Store our facility as the first path entry
|
||||||
|
facility = tokens[-1]
|
||||||
|
|
||||||
|
elif tokens:
|
||||||
|
# This is a bit ambigious... it could be either:
|
||||||
|
# syslog://facility -or- syslog://hostname
|
||||||
|
|
||||||
|
# First lets test it as a facility; we'll correct this
|
||||||
|
# later on if nessisary
|
||||||
|
facility = tokens[-1]
|
||||||
|
|
||||||
# However if specified on the URL, that will over-ride what was
|
# However if specified on the URL, that will over-ride what was
|
||||||
# identified
|
# identified
|
||||||
|
@ -280,15 +418,34 @@ class NotifySyslog(NotifyBase):
|
||||||
facility = next((f for f in SYSLOG_FACILITY_MAP.keys()
|
facility = next((f for f in SYSLOG_FACILITY_MAP.keys()
|
||||||
if f.startswith(facility)), facility)
|
if f.startswith(facility)), facility)
|
||||||
|
|
||||||
# Save facility
|
# Attempt to solve our ambiguity
|
||||||
|
if len(tokens) == 1 and is_hostname(tokens[0]) and (
|
||||||
|
results['port'] or facility not in SYSLOG_FACILITY_MAP):
|
||||||
|
|
||||||
|
# facility is likely hostname; update our guessed mode
|
||||||
|
results['mode'] = SyslogMode.REMOTE
|
||||||
|
|
||||||
|
# Reset our facility value
|
||||||
|
facility = None
|
||||||
|
|
||||||
|
# Set mode if not otherwise set
|
||||||
|
if 'mode' in results['qsd'] and len(results['qsd']['mode']):
|
||||||
|
results['mode'] = NotifySyslog.unquote(results['qsd']['mode'])
|
||||||
|
|
||||||
|
# Save facility if set
|
||||||
|
if facility:
|
||||||
results['facility'] = facility
|
results['facility'] = facility
|
||||||
|
|
||||||
# Include PID as part of the message logged
|
# Include PID as part of the message logged
|
||||||
results['log_pid'] = \
|
results['log_pid'] = parse_bool(
|
||||||
parse_bool(results['qsd'].get('logpid', True))
|
results['qsd'].get(
|
||||||
|
'logpid',
|
||||||
|
NotifySyslog.template_args['logpid']['default']))
|
||||||
|
|
||||||
# Print to stderr as well.
|
# Print to stderr as well.
|
||||||
results['log_perror'] = \
|
results['log_perror'] = parse_bool(
|
||||||
parse_bool(results['qsd'].get('logperror', False))
|
results['qsd'].get(
|
||||||
|
'logperror',
|
||||||
|
NotifySyslog.template_args['logperror']['default']))
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
|
@ -27,6 +27,7 @@ import re
|
||||||
import pytest
|
import pytest
|
||||||
import mock
|
import mock
|
||||||
import apprise
|
import apprise
|
||||||
|
import socket
|
||||||
|
|
||||||
# Disable logging for a cleaner testing output
|
# Disable logging for a cleaner testing output
|
||||||
import logging
|
import logging
|
||||||
|
@ -49,6 +50,7 @@ def test_notify_syslog_by_url(openlog, syslog):
|
||||||
assert obj.url().startswith('syslog://user') is True
|
assert obj.url().startswith('syslog://user') is True
|
||||||
assert re.search(r'logpid=yes', obj.url()) is not None
|
assert re.search(r'logpid=yes', obj.url()) is not None
|
||||||
assert re.search(r'logperror=no', obj.url()) is not None
|
assert re.search(r'logperror=no', obj.url()) is not None
|
||||||
|
assert re.search(r'syslog://.*mode=local', obj.url())
|
||||||
|
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
apprise.Apprise.instantiate(
|
apprise.Apprise.instantiate(
|
||||||
|
@ -59,9 +61,11 @@ def test_notify_syslog_by_url(openlog, syslog):
|
||||||
assert obj.url().startswith('syslog://user') is True
|
assert obj.url().startswith('syslog://user') is True
|
||||||
assert re.search(r'logpid=no', obj.url()) is not None
|
assert re.search(r'logpid=no', obj.url()) is not None
|
||||||
assert re.search(r'logperror=yes', obj.url()) is not None
|
assert re.search(r'logperror=yes', obj.url()) is not None
|
||||||
|
assert re.search(r'syslog://.*mode=local', obj.url())
|
||||||
|
|
||||||
# Test sending a notification
|
# Test sending a notification
|
||||||
assert obj.notify("body") is True
|
assert obj.notify("body") is True
|
||||||
|
assert obj.notify(title="title", body="body") is True
|
||||||
|
|
||||||
# Invalid Notification Type
|
# Invalid Notification Type
|
||||||
assert obj.notify("body", notify_type='invalid') is False
|
assert obj.notify("body", notify_type='invalid') is False
|
||||||
|
@ -71,6 +75,7 @@ def test_notify_syslog_by_url(openlog, syslog):
|
||||||
assert obj.url().startswith('syslog://local5') is True
|
assert obj.url().startswith('syslog://local5') is True
|
||||||
assert re.search(r'logpid=yes', obj.url()) is not None
|
assert re.search(r'logpid=yes', obj.url()) is not None
|
||||||
assert re.search(r'logperror=no', obj.url()) is not None
|
assert re.search(r'logperror=no', obj.url()) is not None
|
||||||
|
assert re.search(r'syslog://.*mode=local', obj.url())
|
||||||
|
|
||||||
# Invalid instantiation
|
# Invalid instantiation
|
||||||
assert apprise.Apprise.instantiate('syslog://_/?facility=invalid') is None
|
assert apprise.Apprise.instantiate('syslog://_/?facility=invalid') is None
|
||||||
|
@ -81,6 +86,7 @@ def test_notify_syslog_by_url(openlog, syslog):
|
||||||
assert obj.url().startswith('syslog://daemon') is True
|
assert obj.url().startswith('syslog://daemon') is True
|
||||||
assert re.search(r'logpid=yes', obj.url()) is not None
|
assert re.search(r'logpid=yes', obj.url()) is not None
|
||||||
assert re.search(r'logperror=no', obj.url()) is not None
|
assert re.search(r'logperror=no', obj.url()) is not None
|
||||||
|
assert re.search(r'syslog://.*mode=local', obj.url())
|
||||||
|
|
||||||
# Facility can also be specified on the url as a hostname
|
# Facility can also be specified on the url as a hostname
|
||||||
obj = apprise.Apprise.instantiate('syslog://kern?logpid=no&logperror=y')
|
obj = apprise.Apprise.instantiate('syslog://kern?logpid=no&logperror=y')
|
||||||
|
@ -88,11 +94,13 @@ def test_notify_syslog_by_url(openlog, syslog):
|
||||||
assert obj.url().startswith('syslog://kern') is True
|
assert obj.url().startswith('syslog://kern') is True
|
||||||
assert re.search(r'logpid=no', obj.url()) is not None
|
assert re.search(r'logpid=no', obj.url()) is not None
|
||||||
assert re.search(r'logperror=yes', obj.url()) is not None
|
assert re.search(r'logperror=yes', obj.url()) is not None
|
||||||
|
assert re.search(r'syslog://.*mode=local', obj.url())
|
||||||
|
|
||||||
# Facilities specified as an argument always over-ride host
|
# Facilities specified as an argument always over-ride host
|
||||||
obj = apprise.Apprise.instantiate('syslog://kern?facility=d')
|
obj = apprise.Apprise.instantiate('syslog://kern?facility=d')
|
||||||
assert isinstance(obj, apprise.plugins.NotifySyslog)
|
assert isinstance(obj, apprise.plugins.NotifySyslog)
|
||||||
assert obj.url().startswith('syslog://daemon') is True
|
assert obj.url().startswith('syslog://daemon') is True
|
||||||
|
assert re.search(r'syslog://.*mode=local', obj.url())
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('syslog.syslog')
|
@mock.patch('syslog.syslog')
|
||||||
|
@ -109,6 +117,7 @@ def test_notify_syslog_by_class(openlog, syslog):
|
||||||
assert obj.url().startswith('syslog://user') is True
|
assert obj.url().startswith('syslog://user') is True
|
||||||
assert re.search(r'logpid=yes', obj.url()) is not None
|
assert re.search(r'logpid=yes', obj.url()) is not None
|
||||||
assert re.search(r'logperror=no', obj.url()) is not None
|
assert re.search(r'logperror=no', obj.url()) is not None
|
||||||
|
assert re.search(r'syslog://.*mode=local', obj.url())
|
||||||
|
|
||||||
# Exception should be thrown about the fact no bot token was specified
|
# Exception should be thrown about the fact no bot token was specified
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
|
@ -116,3 +125,100 @@ def test_notify_syslog_by_class(openlog, syslog):
|
||||||
|
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
apprise.plugins.NotifySyslog(facility=object)
|
apprise.plugins.NotifySyslog(facility=object)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('syslog.syslog')
|
||||||
|
@mock.patch('syslog.openlog')
|
||||||
|
@mock.patch('socket.socket')
|
||||||
|
@mock.patch('os.getpid')
|
||||||
|
def test_notify_syslog_remote(
|
||||||
|
mock_getpid, mock_socket, mock_openlog, mock_syslog):
|
||||||
|
"""
|
||||||
|
API: Syslog Remote Testing
|
||||||
|
|
||||||
|
"""
|
||||||
|
payload = "test"
|
||||||
|
mock_connection = mock.Mock()
|
||||||
|
|
||||||
|
# Fix pid response since it can vary in length and this impacts the
|
||||||
|
# sendto() payload response
|
||||||
|
mock_getpid.return_value = 123
|
||||||
|
|
||||||
|
# our payload length
|
||||||
|
mock_connection.sendto.return_value = 16
|
||||||
|
mock_socket.return_value = mock_connection
|
||||||
|
|
||||||
|
# localhost does not lookup to any of the facility codes so this
|
||||||
|
# gets interpreted as a host
|
||||||
|
obj = apprise.Apprise.instantiate('syslog://localhost')
|
||||||
|
assert isinstance(obj, apprise.plugins.NotifySyslog)
|
||||||
|
assert obj.url().startswith('syslog://localhost') is True
|
||||||
|
assert re.search(r'syslog://.*mode=remote', obj.url())
|
||||||
|
assert re.search(r'logpid=yes', obj.url()) is not None
|
||||||
|
assert obj.notify(body=payload) is True
|
||||||
|
|
||||||
|
# Test with port
|
||||||
|
obj = apprise.Apprise.instantiate('syslog://localhost:518')
|
||||||
|
assert isinstance(obj, apprise.plugins.NotifySyslog)
|
||||||
|
assert obj.url().startswith('syslog://localhost:518') is True
|
||||||
|
assert re.search(r'syslog://.*mode=remote', obj.url())
|
||||||
|
assert re.search(r'logpid=yes', obj.url()) is not None
|
||||||
|
assert obj.notify(body=payload) is True
|
||||||
|
|
||||||
|
# Test with default port
|
||||||
|
obj = apprise.Apprise.instantiate('syslog://localhost:514')
|
||||||
|
assert isinstance(obj, apprise.plugins.NotifySyslog)
|
||||||
|
assert obj.url().startswith('syslog://localhost') is True
|
||||||
|
assert re.search(r'syslog://.*mode=remote', obj.url())
|
||||||
|
assert re.search(r'logpid=yes', obj.url()) is not None
|
||||||
|
assert obj.notify(body=payload) is True
|
||||||
|
|
||||||
|
# Specify a facility
|
||||||
|
obj = apprise.Apprise.instantiate('syslog://localhost/kern')
|
||||||
|
assert isinstance(obj, apprise.plugins.NotifySyslog)
|
||||||
|
assert obj.url().startswith('syslog://localhost/kern') is True
|
||||||
|
assert re.search(r'syslog://.*mode=remote', obj.url())
|
||||||
|
assert re.search(r'logpid=yes', obj.url()) is not None
|
||||||
|
assert obj.notify(body=payload) is True
|
||||||
|
|
||||||
|
# Specify a facility requiring a lookup and having the port identified
|
||||||
|
# resolves any ambiguity
|
||||||
|
obj = apprise.Apprise.instantiate('syslog://kern:514/d')
|
||||||
|
assert isinstance(obj, apprise.plugins.NotifySyslog)
|
||||||
|
assert obj.url().startswith('syslog://kern/daemon') is True
|
||||||
|
assert re.search(r'syslog://.*mode=remote', obj.url())
|
||||||
|
assert re.search(r'logpid=yes', obj.url()) is not None
|
||||||
|
mock_connection.sendto.return_value = 17 # daemon is one more byte in size
|
||||||
|
assert obj.notify(body=payload) is True
|
||||||
|
|
||||||
|
# We can attempt to exclusively set the mode as well without a port
|
||||||
|
# to also remove ambiguity; this falls back to sending as the 'user'
|
||||||
|
obj = apprise.Apprise.instantiate('syslog://kern/d?mode=remote&logpid=no')
|
||||||
|
assert isinstance(obj, apprise.plugins.NotifySyslog)
|
||||||
|
assert obj.url().startswith('syslog://kern/daemon') is True
|
||||||
|
assert re.search(r'syslog://.*mode=remote', obj.url())
|
||||||
|
assert re.search(r'logpid=no', obj.url()) is not None
|
||||||
|
assert re.search(r'logperror=no', obj.url()) is not None
|
||||||
|
|
||||||
|
# Test notifications
|
||||||
|
# + 1 byte in size due to user
|
||||||
|
# + length of pid returned
|
||||||
|
mock_connection.sendto.return_value = len(payload) + 5 \
|
||||||
|
+ len(str(mock_getpid.return_value))
|
||||||
|
assert obj.notify(body=payload) is True
|
||||||
|
# This only fails because the underlining sendto() will return a
|
||||||
|
# length different then what was expected
|
||||||
|
assert obj.notify(body="a different payload size") is False
|
||||||
|
|
||||||
|
# Test timeouts and errors that can occur
|
||||||
|
mock_connection.sendto.return_value = None
|
||||||
|
mock_connection.sendto.side_effect = socket.gaierror
|
||||||
|
assert obj.notify(body=payload) is False
|
||||||
|
|
||||||
|
mock_connection.sendto.side_effect = socket.timeout
|
||||||
|
assert obj.notify(body=payload) is False
|
||||||
|
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
# Handle an invalid mode
|
||||||
|
obj = apprise.Apprise.instantiate(
|
||||||
|
'syslog://user/?mode=invalid', suppress_exceptions=False)
|
||||||
|
|
Loading…
Reference in New Issue