mirror of
https://github.com/caronc/apprise.git
synced 2024-11-21 15:43:27 +01:00
Split rsyslog:// and syslog:// into their own services (#930)
This commit is contained in:
parent
f55032a2ec
commit
e46f8fec1d
1
KEYWORDS
1
KEYWORDS
@ -68,6 +68,7 @@ Pushy
|
||||
PushDeer
|
||||
Reddit
|
||||
Rocket.Chat
|
||||
RSyslog
|
||||
Ryver
|
||||
SendGrid
|
||||
ServerChan
|
||||
|
@ -108,6 +108,7 @@ The table below identifies the services this tool supports and some example serv
|
||||
| [PushDeer](https://github.com/caronc/apprise/wiki/Notify_pushdeer) | pushdeer:// or pushdeers:// | (TCP) 80 or 443 | pushdeer://pushKey<br />pushdeer://hostname/pushKey<br />pushdeer://hostname:port/pushKey
|
||||
| [Reddit](https://github.com/caronc/apprise/wiki/Notify_reddit) | reddit:// | (TCP) 443 | reddit://user:password@app_id/app_secret/subreddit<br />reddit://user:password@app_id/app_secret/sub1/sub2/subN
|
||||
| [Rocket.Chat](https://github.com/caronc/apprise/wiki/Notify_rocketchat) | rocket:// or rockets:// | (TCP) 80 or 443 | rocket://user:password@hostname/RoomID/Channel<br />rockets://user:password@hostname:443/#Channel1/#Channel1/RoomID<br />rocket://user:password@hostname/#Channel<br />rocket://webhook@hostname<br />rockets://webhook@hostname/@User/#Channel
|
||||
| [RSyslog](https://github.com/caronc/apprise/wiki/Notify_rsyslog) | rsyslog:// | (UDP) 514 | rsyslog://hostname<br />rsyslog://hostname/Facility
|
||||
| [Ryver](https://github.com/caronc/apprise/wiki/Notify_ryver) | ryver:// | (TCP) 443 | ryver://Organization/Token<br />ryver://botname@Organization/Token
|
||||
| [SendGrid](https://github.com/caronc/apprise/wiki/Notify_sendgrid) | sendgrid:// | (TCP) 443 | sendgrid://APIToken:FromEmail/<br />sendgrid://APIToken:FromEmail/ToEmail<br />sendgrid://APIToken:FromEmail/ToEmail1/ToEmail2/ToEmailN/
|
||||
| [ServerChan](https://github.com/caronc/apprise/wiki/Notify_serverchan) | schan:// | (TCP) 443 | schan://sendkey/
|
||||
@ -118,7 +119,7 @@ The table below identifies the services this tool supports and some example serv
|
||||
| [Streamlabs](https://github.com/caronc/apprise/wiki/Notify_streamlabs) | strmlabs:// | (TCP) 443 | strmlabs://AccessToken/<br/>strmlabs://AccessToken/?name=name&identifier=identifier&amount=0¤cy=USD
|
||||
| [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
|
||||
| [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
|
||||
| [Syslog](https://github.com/caronc/apprise/wiki/Notify_syslog) | syslog:// | n/a | syslog://<br />syslog://Facility
|
||||
| [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
|
||||
| [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
|
||||
|
380
apprise/plugins/NotifyRSyslog.py
Normal file
380
apprise/plugins/NotifyRSyslog.py
Normal file
@ -0,0 +1,380 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import os
|
||||
import socket
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_bool
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class syslog:
|
||||
"""
|
||||
Extrapoloated information from the syslog library so that this plugin
|
||||
would not be dependent on it.
|
||||
"""
|
||||
# Notification Categories
|
||||
LOG_KERN = 0
|
||||
LOG_USER = 8
|
||||
LOG_MAIL = 16
|
||||
LOG_DAEMON = 24
|
||||
LOG_AUTH = 32
|
||||
LOG_SYSLOG = 40
|
||||
LOG_LPR = 48
|
||||
LOG_NEWS = 56
|
||||
LOG_UUCP = 64
|
||||
LOG_CRON = 72
|
||||
LOG_LOCAL0 = 128
|
||||
LOG_LOCAL1 = 136
|
||||
LOG_LOCAL2 = 144
|
||||
LOG_LOCAL3 = 152
|
||||
LOG_LOCAL4 = 160
|
||||
LOG_LOCAL5 = 168
|
||||
LOG_LOCAL6 = 176
|
||||
LOG_LOCAL7 = 184
|
||||
|
||||
# Notification Types
|
||||
LOG_INFO = 6
|
||||
LOG_NOTICE = 5
|
||||
LOG_WARNING = 4
|
||||
LOG_CRIT = 2
|
||||
|
||||
|
||||
class SyslogFacility:
|
||||
"""
|
||||
All of the supported facilities
|
||||
"""
|
||||
KERN = 'kern'
|
||||
USER = 'user'
|
||||
MAIL = 'mail'
|
||||
DAEMON = 'daemon'
|
||||
AUTH = 'auth'
|
||||
SYSLOG = 'syslog'
|
||||
LPR = 'lpr'
|
||||
NEWS = 'news'
|
||||
UUCP = 'uucp'
|
||||
CRON = 'cron'
|
||||
LOCAL0 = 'local0'
|
||||
LOCAL1 = 'local1'
|
||||
LOCAL2 = 'local2'
|
||||
LOCAL3 = 'local3'
|
||||
LOCAL4 = 'local4'
|
||||
LOCAL5 = 'local5'
|
||||
LOCAL6 = 'local6'
|
||||
LOCAL7 = 'local7'
|
||||
|
||||
|
||||
SYSLOG_FACILITY_MAP = {
|
||||
SyslogFacility.KERN: syslog.LOG_KERN,
|
||||
SyslogFacility.USER: syslog.LOG_USER,
|
||||
SyslogFacility.MAIL: syslog.LOG_MAIL,
|
||||
SyslogFacility.DAEMON: syslog.LOG_DAEMON,
|
||||
SyslogFacility.AUTH: syslog.LOG_AUTH,
|
||||
SyslogFacility.SYSLOG: syslog.LOG_SYSLOG,
|
||||
SyslogFacility.LPR: syslog.LOG_LPR,
|
||||
SyslogFacility.NEWS: syslog.LOG_NEWS,
|
||||
SyslogFacility.UUCP: syslog.LOG_UUCP,
|
||||
SyslogFacility.CRON: syslog.LOG_CRON,
|
||||
SyslogFacility.LOCAL0: syslog.LOG_LOCAL0,
|
||||
SyslogFacility.LOCAL1: syslog.LOG_LOCAL1,
|
||||
SyslogFacility.LOCAL2: syslog.LOG_LOCAL2,
|
||||
SyslogFacility.LOCAL3: syslog.LOG_LOCAL3,
|
||||
SyslogFacility.LOCAL4: syslog.LOG_LOCAL4,
|
||||
SyslogFacility.LOCAL5: syslog.LOG_LOCAL5,
|
||||
SyslogFacility.LOCAL6: syslog.LOG_LOCAL6,
|
||||
SyslogFacility.LOCAL7: syslog.LOG_LOCAL7,
|
||||
}
|
||||
|
||||
SYSLOG_FACILITY_RMAP = {
|
||||
syslog.LOG_KERN: SyslogFacility.KERN,
|
||||
syslog.LOG_USER: SyslogFacility.USER,
|
||||
syslog.LOG_MAIL: SyslogFacility.MAIL,
|
||||
syslog.LOG_DAEMON: SyslogFacility.DAEMON,
|
||||
syslog.LOG_AUTH: SyslogFacility.AUTH,
|
||||
syslog.LOG_SYSLOG: SyslogFacility.SYSLOG,
|
||||
syslog.LOG_LPR: SyslogFacility.LPR,
|
||||
syslog.LOG_NEWS: SyslogFacility.NEWS,
|
||||
syslog.LOG_UUCP: SyslogFacility.UUCP,
|
||||
syslog.LOG_CRON: SyslogFacility.CRON,
|
||||
syslog.LOG_LOCAL0: SyslogFacility.LOCAL0,
|
||||
syslog.LOG_LOCAL1: SyslogFacility.LOCAL1,
|
||||
syslog.LOG_LOCAL2: SyslogFacility.LOCAL2,
|
||||
syslog.LOG_LOCAL3: SyslogFacility.LOCAL3,
|
||||
syslog.LOG_LOCAL4: SyslogFacility.LOCAL4,
|
||||
syslog.LOG_LOCAL5: SyslogFacility.LOCAL5,
|
||||
syslog.LOG_LOCAL6: SyslogFacility.LOCAL6,
|
||||
syslog.LOG_LOCAL7: SyslogFacility.LOCAL7,
|
||||
}
|
||||
|
||||
# Used as a lookup when handling the Apprise -> Syslog Mapping
|
||||
SYSLOG_PUBLISH_MAP = {
|
||||
NotifyType.INFO: syslog.LOG_INFO,
|
||||
NotifyType.SUCCESS: syslog.LOG_NOTICE,
|
||||
NotifyType.FAILURE: syslog.LOG_CRIT,
|
||||
NotifyType.WARNING: syslog.LOG_WARNING,
|
||||
}
|
||||
|
||||
|
||||
class NotifyRSyslog(NotifyBase):
|
||||
"""
|
||||
A wrapper for Remote Syslog Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'Remote Syslog'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://tools.ietf.org/html/rfc5424'
|
||||
|
||||
# The default protocol
|
||||
protocol = 'rsyslog'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_rsyslog'
|
||||
|
||||
# Disable throttle rate for RSyslog requests
|
||||
request_rate_per_sec = 0
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{host}',
|
||||
'{schema}://{host}:{port}',
|
||||
'{schema}://{host}/{facility}',
|
||||
'{schema}://{host}:{port}/{facility}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'facility': {
|
||||
'name': _('Facility'),
|
||||
'type': 'choice:string',
|
||||
'values': [k for k in SYSLOG_FACILITY_MAP.keys()],
|
||||
'default': SyslogFacility.USER,
|
||||
'required': True,
|
||||
},
|
||||
'host': {
|
||||
'name': _('Hostname'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
},
|
||||
'port': {
|
||||
'name': _('Port'),
|
||||
'type': 'int',
|
||||
'min': 1,
|
||||
'max': 65535,
|
||||
'default': 514,
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'facility': {
|
||||
# We map back to the same element defined in template_tokens
|
||||
'alias_of': 'facility',
|
||||
},
|
||||
'logpid': {
|
||||
'name': _('Log PID'),
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
'map_to': 'log_pid',
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, facility=None, log_pid=True, **kwargs):
|
||||
"""
|
||||
Initialize RSyslog Object
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if facility:
|
||||
try:
|
||||
self.facility = SYSLOG_FACILITY_MAP[facility]
|
||||
|
||||
except KeyError:
|
||||
msg = 'An invalid syslog facility ' \
|
||||
'({}) was specified.'.format(facility)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
else:
|
||||
self.facility = \
|
||||
SYSLOG_FACILITY_MAP[
|
||||
self.template_tokens['facility']['default']]
|
||||
|
||||
# Include PID with each message.
|
||||
self.log_pid = log_pid
|
||||
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform RSyslog Notification
|
||||
"""
|
||||
|
||||
if title:
|
||||
# Format title
|
||||
body = '{}: {}'.format(title, body)
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
host = self.host
|
||||
port = self.port if self.port \
|
||||
else self.template_tokens['port']['default']
|
||||
|
||||
if self.log_pid:
|
||||
payload = '<%d>- %d - %s' % (
|
||||
SYSLOG_PUBLISH_MAP[notify_type] + self.facility * 8,
|
||||
os.getpid(), body)
|
||||
|
||||
else:
|
||||
payload = '<%d>- %s' % (
|
||||
SYSLOG_PUBLISH_MAP[notify_type] + self.facility * 8, body)
|
||||
|
||||
# send UDP packet to upstream server
|
||||
self.logger.debug(
|
||||
'RSyslog Host: %s:%d/%s',
|
||||
host, port, SYSLOG_FACILITY_RMAP[self.facility])
|
||||
self.logger.debug('RSyslog 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 RSyslog '
|
||||
'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 RSyslog '
|
||||
'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(
|
||||
'RSyslog sent %d byte(s) but intended to send %d byte(s)',
|
||||
sent, len(payload))
|
||||
return False
|
||||
|
||||
self.logger.info('Sent RSyslog notification.')
|
||||
|
||||
return True
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'logpid': 'yes' if self.log_pid else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{hostname}{port}/{facility}/?{params}'.format(
|
||||
schema=self.protocol,
|
||||
hostname=NotifyRSyslog.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=NotifyRSyslog.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
||||
tokens = []
|
||||
|
||||
# Get our path values
|
||||
tokens.extend(NotifyRSyslog.split_path(results['fullpath']))
|
||||
|
||||
# Initialization
|
||||
facility = None
|
||||
|
||||
if tokens:
|
||||
# Store the last entry as the facility
|
||||
facility = tokens[-1].lower()
|
||||
|
||||
# However if specified on the URL, that will over-ride what was
|
||||
# identified
|
||||
if 'facility' in results['qsd'] and len(results['qsd']['facility']):
|
||||
facility = results['qsd']['facility'].lower()
|
||||
|
||||
if facility and facility not in SYSLOG_FACILITY_MAP:
|
||||
# Find first match; if no match is found we set the result
|
||||
# to the matching key. This allows us to throw a TypeError
|
||||
# during the __init__() call. The benifit of doing this
|
||||
# check here is if we do have a valid match, we can support
|
||||
# short form matches like 'u' which will match against user
|
||||
facility = next((f for f in SYSLOG_FACILITY_MAP.keys()
|
||||
if f.startswith(facility)), facility)
|
||||
|
||||
# Save facility if set
|
||||
if facility:
|
||||
results['facility'] = facility
|
||||
|
||||
# Include PID as part of the message logged
|
||||
results['log_pid'] = parse_bool(
|
||||
results['qsd'].get(
|
||||
'logpid',
|
||||
NotifyRSyslog.template_args['logpid']['default']))
|
||||
|
||||
return results
|
@ -30,14 +30,11 @@
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import os
|
||||
import syslog
|
||||
import socket
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_bool
|
||||
from ..utils import is_hostname
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
@ -107,20 +104,13 @@ SYSLOG_FACILITY_RMAP = {
|
||||
syslog.LOG_LOCAL7: SyslogFacility.LOCAL7,
|
||||
}
|
||||
|
||||
|
||||
class SyslogMode:
|
||||
# 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,
|
||||
)
|
||||
# Used as a lookup when handling the Apprise -> Syslog Mapping
|
||||
SYSLOG_PUBLISH_MAP = {
|
||||
NotifyType.INFO: syslog.LOG_INFO,
|
||||
NotifyType.SUCCESS: syslog.LOG_NOTICE,
|
||||
NotifyType.FAILURE: syslog.LOG_CRIT,
|
||||
NotifyType.WARNING: syslog.LOG_WARNING,
|
||||
}
|
||||
|
||||
|
||||
class NotifySyslog(NotifyBase):
|
||||
@ -134,8 +124,8 @@ class NotifySyslog(NotifyBase):
|
||||
# The services URL
|
||||
service_url = 'https://tools.ietf.org/html/rfc5424'
|
||||
|
||||
# The default secure protocol
|
||||
secure_protocol = 'syslog'
|
||||
# The default protocol
|
||||
protocol = 'syslog'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_syslog'
|
||||
@ -148,10 +138,6 @@ class NotifySyslog(NotifyBase):
|
||||
templates = (
|
||||
'{schema}://',
|
||||
'{schema}://{facility}',
|
||||
'{schema}://{host}',
|
||||
'{schema}://{host}:{port}',
|
||||
'{schema}://{host}/{facility}',
|
||||
'{schema}://{host}:{port}/{facility}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
@ -162,17 +148,6 @@ class NotifySyslog(NotifyBase):
|
||||
'values': [k for k in SYSLOG_FACILITY_MAP.keys()],
|
||||
'default': SyslogFacility.USER,
|
||||
},
|
||||
'host': {
|
||||
'name': _('Hostname'),
|
||||
'type': 'string',
|
||||
},
|
||||
'port': {
|
||||
'name': _('Port'),
|
||||
'type': 'int',
|
||||
'min': 1,
|
||||
'max': 65535,
|
||||
'default': 514,
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
@ -181,12 +156,6 @@ class NotifySyslog(NotifyBase):
|
||||
# We map back to the same element defined in template_tokens
|
||||
'alias_of': 'facility',
|
||||
},
|
||||
'mode': {
|
||||
'name': _('Syslog Mode'),
|
||||
'type': 'choice:string',
|
||||
'values': SYSLOG_MODES,
|
||||
'default': SyslogMode.LOCAL,
|
||||
},
|
||||
'logpid': {
|
||||
'name': _('Log PID'),
|
||||
'type': 'bool',
|
||||
@ -201,8 +170,8 @@ class NotifySyslog(NotifyBase):
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, facility=None, mode=None, log_pid=True,
|
||||
log_perror=False, **kwargs):
|
||||
def __init__(self, facility=None, log_pid=True, log_perror=False,
|
||||
**kwargs):
|
||||
"""
|
||||
Initialize Syslog Object
|
||||
"""
|
||||
@ -222,14 +191,6 @@ class NotifySyslog(NotifyBase):
|
||||
SYSLOG_FACILITY_MAP[
|
||||
self.template_tokens['facility']['default']]
|
||||
|
||||
self.mode = self.template_args['mode']['default'] \
|
||||
if not isinstance(mode, str) 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
|
||||
self.logoptions = 0
|
||||
|
||||
@ -248,7 +209,7 @@ class NotifySyslog(NotifyBase):
|
||||
if log_perror:
|
||||
self.logoptions |= syslog.LOG_PERROR
|
||||
|
||||
# Initialize our loggig
|
||||
# Initialize our logging
|
||||
syslog.openlog(
|
||||
self.app_id, logoption=self.logoptions, facility=self.facility)
|
||||
return
|
||||
@ -258,7 +219,7 @@ class NotifySyslog(NotifyBase):
|
||||
Perform Syslog Notification
|
||||
"""
|
||||
|
||||
_pmap = {
|
||||
SYSLOG_PUBLISH_MAP = {
|
||||
NotifyType.INFO: syslog.LOG_INFO,
|
||||
NotifyType.SUCCESS: syslog.LOG_NOTICE,
|
||||
NotifyType.FAILURE: syslog.LOG_CRIT,
|
||||
@ -271,70 +232,17 @@ class NotifySyslog(NotifyBase):
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
if self.mode == SyslogMode.LOCAL:
|
||||
try:
|
||||
syslog.syslog(_pmap[notify_type], body)
|
||||
try:
|
||||
syslog.syslog(SYSLOG_PUBLISH_MAP[notify_type], body)
|
||||
|
||||
except KeyError:
|
||||
# An invalid notification type was specified
|
||||
self.logger.warning(
|
||||
'An invalid notification type '
|
||||
'({}) was specified.'.format(notify_type))
|
||||
return False
|
||||
except KeyError:
|
||||
# An invalid notification type was specified
|
||||
self.logger.warning(
|
||||
'An invalid notification type '
|
||||
'({}) was specified.'.format(notify_type))
|
||||
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)
|
||||
self.logger.info('Sent Syslog notification.')
|
||||
|
||||
return True
|
||||
|
||||
@ -347,31 +255,16 @@ class NotifySyslog(NotifyBase):
|
||||
params = {
|
||||
'logperror': 'yes' if self.log_perror else 'no',
|
||||
'logpid': 'yes' if self.log_pid else 'no',
|
||||
'mode': self.mode,
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
if self.mode == SyslogMode.LOCAL:
|
||||
return '{schema}://{facility}/?{params}'.format(
|
||||
facility=self.template_tokens['facility']['default']
|
||||
if self.facility not in SYSLOG_FACILITY_RMAP
|
||||
else SYSLOG_FACILITY_RMAP[self.facility],
|
||||
schema=self.secure_protocol,
|
||||
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),
|
||||
return '{schema}://{facility}/?{params}'.format(
|
||||
facility=self.template_tokens['facility']['default']
|
||||
if self.facility not in SYSLOG_FACILITY_RMAP
|
||||
else SYSLOG_FACILITY_RMAP[self.facility],
|
||||
schema=self.protocol,
|
||||
params=NotifySyslog.urlencode(params),
|
||||
)
|
||||
|
||||
@ -394,21 +287,12 @@ class NotifySyslog(NotifyBase):
|
||||
# Get our path values
|
||||
tokens.extend(NotifySyslog.split_path(results['fullpath']))
|
||||
|
||||
# Initialization
|
||||
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]
|
||||
if tokens:
|
||||
# Store the last entry as the facility
|
||||
facility = tokens[-1].lower()
|
||||
|
||||
# However if specified on the URL, that will over-ride what was
|
||||
# identified
|
||||
@ -424,20 +308,6 @@ class NotifySyslog(NotifyBase):
|
||||
facility = next((f for f in SYSLOG_FACILITY_MAP.keys()
|
||||
if f.startswith(facility)), 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
|
||||
|
@ -51,9 +51,9 @@ Microsoft Windows, Microsoft Teams, Misskey, MQTT, MSG91, MyAndroid, Nexmo,
|
||||
Nextcloud, NextcloudTalk, Notica, Notifico, ntfy, Office365, OneSignal,
|
||||
Opsgenie, PagerDuty, PagerTree, ParsePlatform, PopcornNotify, Prowl, Pushalot,
|
||||
PushBullet, Pushjet, PushMe, Pushover, PushSafer, Pushy, PushDeer, Reddit,
|
||||
Rocket.Chat, SendGrid, ServerChan, Signal, SimplePush, Sinch, Slack, SMSEagle,
|
||||
SMTP2Go, Spontit, SparkPost, Super Toasty, Streamlabs, Stride, Syslog,
|
||||
Techulus Push, Telegram, Twilio, Twitter, Twist, XBMC, Voipms, Vonage,
|
||||
Rocket.Chat, RSyslog, SendGrid, ServerChan, Signal, SimplePush, Sinch, Slack,
|
||||
SMSEagle, SMTP2Go, Spontit, SparkPost, Super Toasty, Streamlabs, Stride,
|
||||
Syslog, Techulus Push, Telegram, Twilio, Twitter, Twist, XBMC, Voipms, Vonage,
|
||||
WhatsApp, Webex Teams}
|
||||
|
||||
Name: python-%{pypi_name}
|
||||
|
@ -1060,8 +1060,8 @@ def test_plugin_matrix_attachments_api_v2(mock_post, mock_get):
|
||||
|
||||
# Throw an exception on the second call to requests.post()
|
||||
for side_effect in (requests.RequestException(), OSError(), bad_response):
|
||||
mock_post.side_effect = [response, side_effect]
|
||||
mock_get.side_effect = [side_effect]
|
||||
mock_post.side_effect = [response, side_effect, side_effect]
|
||||
mock_get.side_effect = [side_effect, side_effect]
|
||||
|
||||
# We'll fail now because of our error handling
|
||||
assert obj.send(body="test", attach=attach) is False
|
||||
@ -1069,8 +1069,10 @@ def test_plugin_matrix_attachments_api_v2(mock_post, mock_get):
|
||||
# handle a bad response
|
||||
bad_response = mock.Mock()
|
||||
bad_response.status_code = requests.codes.internal_server_error
|
||||
mock_post.side_effect = [response, bad_response]
|
||||
mock_get.side_effect = [response, bad_response]
|
||||
mock_post.side_effect = \
|
||||
[response, bad_response, response, response, response]
|
||||
mock_get.side_effect = \
|
||||
[response, bad_response, response, response, response]
|
||||
|
||||
# We'll fail now because of an internal exception
|
||||
assert obj.send(body="test", attach=attach) is False
|
||||
|
180
test/test_plugin_rsyslog.py
Normal file
180
test/test_plugin_rsyslog.py
Normal file
@ -0,0 +1,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import re
|
||||
from unittest import mock
|
||||
import pytest
|
||||
|
||||
import apprise
|
||||
import socket
|
||||
|
||||
# Disable logging for a cleaner testing output
|
||||
import logging
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
||||
from apprise.plugins.NotifyRSyslog import NotifyRSyslog # noqa E402
|
||||
|
||||
|
||||
@mock.patch('socket.socket')
|
||||
@mock.patch('os.getpid')
|
||||
def test_plugin_rsyslog_by_url(mock_getpid, mock_socket):
|
||||
"""
|
||||
NotifyRSyslog() Apprise URLs
|
||||
|
||||
"""
|
||||
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
|
||||
|
||||
# an invalid URL
|
||||
assert NotifyRSyslog.parse_url(object) is None
|
||||
assert NotifyRSyslog.parse_url(42) is None
|
||||
assert NotifyRSyslog.parse_url(None) is None
|
||||
|
||||
# localhost does not lookup to any of the facility codes so this
|
||||
# gets interpreted as a host
|
||||
obj = apprise.Apprise.instantiate('rsyslog://localhost')
|
||||
assert isinstance(obj, NotifyRSyslog)
|
||||
assert obj.url().startswith('rsyslog://localhost') is True
|
||||
assert re.search(r'logpid=yes', obj.url()) is not None
|
||||
assert obj.notify(body=payload) is True
|
||||
|
||||
mock_connection.sendto.return_value = 18
|
||||
obj = apprise.Apprise.instantiate('rsyslog://localhost/?facility=local5')
|
||||
assert isinstance(obj, NotifyRSyslog)
|
||||
assert obj.url().startswith('rsyslog://localhost/local5') is True
|
||||
assert re.search(r'logpid=yes', obj.url()) is not None
|
||||
assert obj.notify(body=payload) is True
|
||||
|
||||
# Invalid instantiation
|
||||
assert apprise.Apprise.instantiate(
|
||||
'rsyslog://localhost/?facility=invalid') is None
|
||||
|
||||
mock_connection.sendto.return_value = 17
|
||||
# j will cause a search to take place and match to daemon
|
||||
obj = apprise.Apprise.instantiate('rsyslog://localhost/?facility=d')
|
||||
assert isinstance(obj, NotifyRSyslog)
|
||||
assert obj.url().startswith('rsyslog://localhost/daemon') is True
|
||||
assert re.search(r'logpid=yes', obj.url()) is not None
|
||||
assert obj.notify(body=payload) is True
|
||||
|
||||
# Test bad return count
|
||||
mock_connection.sendto.return_value = 0
|
||||
assert obj.notify(body=payload) is False
|
||||
|
||||
# Test with port
|
||||
mock_connection.sendto.return_value = 17
|
||||
obj = apprise.Apprise.instantiate('rsyslog://localhost:518')
|
||||
assert isinstance(obj, NotifyRSyslog)
|
||||
assert obj.url().startswith('rsyslog://localhost:518') is True
|
||||
assert re.search(r'logpid=yes', obj.url()) is not None
|
||||
assert obj.notify(body=payload) is True
|
||||
|
||||
# Set length to include title (for test)
|
||||
mock_connection.sendto.return_value = 39
|
||||
assert obj.notify(body=payload, title="Testing a title entry") is True
|
||||
|
||||
# Return length back to where it was
|
||||
mock_connection.sendto.return_value = 16
|
||||
|
||||
# Test with default port
|
||||
obj = apprise.Apprise.instantiate('rsyslog://localhost:514')
|
||||
assert isinstance(obj, NotifyRSyslog)
|
||||
assert obj.url().startswith('rsyslog://localhost') is True
|
||||
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('rsyslog://localhost/kern')
|
||||
assert isinstance(obj, NotifyRSyslog)
|
||||
assert obj.url().startswith('rsyslog://localhost/kern') is True
|
||||
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('rsyslog://localhost:514/d')
|
||||
assert isinstance(obj, NotifyRSyslog)
|
||||
assert obj.url().startswith('rsyslog://localhost/daemon') is True
|
||||
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
|
||||
|
||||
obj = apprise.Apprise.instantiate('rsyslog://localhost:9000/d?logpid=no')
|
||||
assert isinstance(obj, NotifyRSyslog)
|
||||
assert obj.url().startswith('rsyslog://localhost:9000/daemon') is True
|
||||
assert re.search(r'logpid=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
|
||||
|
||||
|
||||
def test_plugin_rsyslog_edge_cases():
|
||||
"""
|
||||
NotifyRSyslog() Edge Cases
|
||||
|
||||
"""
|
||||
|
||||
# Default
|
||||
obj = NotifyRSyslog(host="localhost", facility=None)
|
||||
assert isinstance(obj, NotifyRSyslog)
|
||||
assert obj.url().startswith('rsyslog://localhost/user') is True
|
||||
assert re.search(r'logpid=yes', obj.url()) is not None
|
||||
|
||||
# Exception should be thrown about the fact no bot token was specified
|
||||
with pytest.raises(TypeError):
|
||||
NotifyRSyslog(host="localhost", facility='invalid')
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
NotifyRSyslog(host="localhost", facility=object)
|
@ -36,7 +36,6 @@ import pytest
|
||||
from unittest import mock
|
||||
|
||||
import apprise
|
||||
import socket
|
||||
|
||||
# Disable logging for a cleaner testing output
|
||||
import logging
|
||||
@ -65,7 +64,6 @@ def test_plugin_syslog_by_url(openlog, syslog):
|
||||
assert obj.url().startswith('syslog://user') is True
|
||||
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'syslog://.*mode=local', obj.url())
|
||||
|
||||
assert isinstance(
|
||||
apprise.Apprise.instantiate(
|
||||
@ -76,7 +74,6 @@ def test_plugin_syslog_by_url(openlog, syslog):
|
||||
assert obj.url().startswith('syslog://user') is True
|
||||
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'syslog://.*mode=local', obj.url())
|
||||
|
||||
# Test sending a notification
|
||||
assert obj.notify("body") is True
|
||||
@ -90,7 +87,6 @@ def test_plugin_syslog_by_url(openlog, syslog):
|
||||
assert obj.url().startswith('syslog://local5') is True
|
||||
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'syslog://.*mode=local', obj.url())
|
||||
|
||||
# Invalid instantiation
|
||||
assert apprise.Apprise.instantiate('syslog://_/?facility=invalid') is None
|
||||
@ -101,7 +97,6 @@ def test_plugin_syslog_by_url(openlog, syslog):
|
||||
assert obj.url().startswith('syslog://daemon') is True
|
||||
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'syslog://.*mode=local', obj.url())
|
||||
|
||||
# Facility can also be specified on the url as a hostname
|
||||
obj = apprise.Apprise.instantiate('syslog://kern?logpid=no&logperror=y')
|
||||
@ -109,13 +104,11 @@ def test_plugin_syslog_by_url(openlog, syslog):
|
||||
assert obj.url().startswith('syslog://kern') is True
|
||||
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'syslog://.*mode=local', obj.url())
|
||||
|
||||
# Facilities specified as an argument always over-ride host
|
||||
obj = apprise.Apprise.instantiate('syslog://kern?facility=d')
|
||||
assert isinstance(obj, NotifySyslog)
|
||||
assert obj.url().startswith('syslog://daemon') is True
|
||||
assert re.search(r'syslog://.*mode=local', obj.url())
|
||||
|
||||
|
||||
@mock.patch('syslog.syslog')
|
||||
@ -132,7 +125,6 @@ def test_plugin_syslog_edge_cases(openlog, syslog):
|
||||
assert obj.url().startswith('syslog://user') is True
|
||||
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'syslog://.*mode=local', obj.url())
|
||||
|
||||
# Exception should be thrown about the fact no bot token was specified
|
||||
with pytest.raises(TypeError):
|
||||
@ -140,100 +132,3 @@ def test_plugin_syslog_edge_cases(openlog, syslog):
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
NotifySyslog(facility=object)
|
||||
|
||||
|
||||
@mock.patch('syslog.syslog')
|
||||
@mock.patch('syslog.openlog')
|
||||
@mock.patch('socket.socket')
|
||||
@mock.patch('os.getpid')
|
||||
def test_plugin_syslog_remote(
|
||||
mock_getpid, mock_socket, mock_openlog, mock_syslog):
|
||||
"""
|
||||
NotifySyslog() 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, 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, 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, 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, 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, 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, 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
Block a user