diff options
Diffstat (limited to 'libs/apprise/plugins/NotifyOpsgenie.py')
-rw-r--r-- | libs/apprise/plugins/NotifyOpsgenie.py | 601 |
1 files changed, 601 insertions, 0 deletions
diff --git a/libs/apprise/plugins/NotifyOpsgenie.py b/libs/apprise/plugins/NotifyOpsgenie.py new file mode 100644 index 000000000..da63a1d8a --- /dev/null +++ b/libs/apprise/plugins/NotifyOpsgenie.py @@ -0,0 +1,601 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2020 Chris Caron <[email protected]> +# All rights reserved. +# +# This code is licensed under the MIT License. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions : +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# 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 +# THE SOFTWARE. + +# Signup @ https://www.opsgenie.com +# +# Generate your Integration API Key +# https://app.opsgenie.com/settings/integration/add/API/ + +# Knowing this, you can build your Opsgenie URL as follows: +# opsgenie://{apikey}/ +# opsgenie://{apikey}/@{user} +# opsgenie://{apikey}/*{schedule} +# opsgenie://{apikey}/^{escalation} +# opsgenie://{apikey}/#{team} +# +# You can mix and match what you want to notify freely +# opsgenie://{apikey}/@{user}/#{team}/*{schedule}/^{escalation} +# +# If no target prefix is specified, then it is assumed to be a user. +# +# API Documentation: https://docs.opsgenie.com/docs/alert-api +# API Integration Docs: https://docs.opsgenie.com/docs/api-integration + +import requests +from json import dumps + +from .NotifyBase import NotifyBase +from ..common import NotifyType +from ..utils import validate_regex +from ..utils import is_uuid +from ..utils import parse_list +from ..utils import parse_bool +from ..AppriseLocale import gettext_lazy as _ + + +class OpsgenieCategory(NotifyBase): + """ + We define the different category types that we can notify + """ + USER = 'user' + SCHEDULE = 'schedule' + ESCALATION = 'escalation' + TEAM = 'team' + + +OPSGENIE_CATEGORIES = ( + OpsgenieCategory.USER, + OpsgenieCategory.SCHEDULE, + OpsgenieCategory.ESCALATION, + OpsgenieCategory.TEAM, +) + + +# Regions +class OpsgenieRegion(object): + US = 'us' + EU = 'eu' + + +# Opsgenie APIs +OPSGENIE_API_LOOKUP = { + OpsgenieRegion.US: 'https://api.opsgenie.com/v2/alerts', + OpsgenieRegion.EU: 'https://api.eu.opsgenie.com/v2/alerts', +} + +# A List of our regions we can use for verification +OPSGENIE_REGIONS = ( + OpsgenieRegion.US, + OpsgenieRegion.EU, +) + + +# Priorities +class OpsgeniePriority(object): + LOW = 1 + MODERATE = 2 + NORMAL = 3 + HIGH = 4 + EMERGENCY = 5 + + +OPSGENIE_PRIORITIES = ( + OpsgeniePriority.LOW, + OpsgeniePriority.MODERATE, + OpsgeniePriority.NORMAL, + OpsgeniePriority.HIGH, + OpsgeniePriority.EMERGENCY, +) + + +class NotifyOpsgenie(NotifyBase): + """ + A wrapper for Opsgenie Notifications + """ + + # The default descriptive name associated with the Notification + service_name = 'Opsgenie' + + # The services URL + service_url = 'https://opsgenie.com/' + + # All notification requests are secure + secure_protocol = 'opsgenie' + + # A URL that takes you to the setup/help of the specific protocol + setup_url = 'https://github.com/caronc/apprise/wiki/Notify_opsgenie' + + # The maximum length of the body + body_maxlen = 15000 + + # If we don't have the specified min length, then we don't bother using + # the body directive + opsgenie_body_minlen = 130 + + # The default region to use if one isn't otherwise specified + opsgenie_default_region = OpsgenieRegion.US + + # The maximum allowable targets within a notification + maximum_batch_size = 50 + + # Define object templates + templates = ( + '{schema}://{apikey}', + '{schema}://{apikey}/{targets}', + ) + + # Define our template tokens + template_tokens = dict(NotifyBase.template_tokens, **{ + 'apikey': { + 'name': _('API Key'), + 'type': 'string', + 'private': True, + 'required': True, + }, + 'target_escalation': { + 'name': _('Target Escalation'), + 'prefix': '^', + 'type': 'string', + 'map_to': 'targets', + }, + 'target_schedule': { + 'name': _('Target Schedule'), + 'type': 'string', + 'prefix': '*', + 'map_to': 'targets', + }, + 'target_user': { + 'name': _('Target User'), + 'type': 'string', + 'prefix': '@', + 'map_to': 'targets', + }, + 'target_team': { + 'name': _('Target Team'), + 'type': 'string', + 'prefix': '#', + 'map_to': 'targets', + }, + 'targets': { + 'name': _('Targets '), + 'type': 'list:string', + }, + }) + + # Define our template arguments + template_args = dict(NotifyBase.template_args, **{ + 'region': { + 'name': _('Region Name'), + 'type': 'choice:string', + 'values': OPSGENIE_REGIONS, + 'default': OpsgenieRegion.US, + 'map_to': 'region_name', + }, + 'batch': { + 'name': _('Batch Mode'), + 'type': 'bool', + 'default': False, + }, + 'priority': { + 'name': _('Priority'), + 'type': 'choice:int', + 'values': OPSGENIE_PRIORITIES, + 'default': OpsgeniePriority.NORMAL, + }, + 'entity': { + 'name': _('Entity'), + 'type': 'string', + }, + 'alias': { + 'name': _('Alias'), + 'type': 'string', + }, + 'tags': { + 'name': _('Tags'), + 'type': 'string', + }, + 'to': { + 'alias_of': 'targets', + }, + }) + + # Map of key-value pairs to use as custom properties of the alert. + template_kwargs = { + 'details': { + 'name': _('Details'), + 'prefix': '+', + }, + } + + def __init__(self, apikey, targets, region_name=None, details=None, + priority=None, alias=None, entity=None, batch=False, + tags=None, **kwargs): + """ + Initialize Opsgenie Object + """ + super(NotifyOpsgenie, self).__init__(**kwargs) + + # API Key (associated with project) + self.apikey = validate_regex(apikey) + if not self.apikey: + msg = 'An invalid Opsgenie API Key ' \ + '({}) was specified.'.format(apikey) + self.logger.warning(msg) + raise TypeError(msg) + + # The Priority of the message + if priority not in OPSGENIE_PRIORITIES: + self.priority = OpsgeniePriority.NORMAL + + else: + self.priority = priority + + # Store our region + try: + self.region_name = self.opsgenie_default_region \ + if region_name is None else region_name.lower() + + if self.region_name not in OPSGENIE_REGIONS: + # allow the outer except to handle this common response + raise + except: + # Invalid region specified + msg = 'The Opsgenie region specified ({}) is invalid.' \ + .format(region_name) + self.logger.warning(msg) + raise TypeError(msg) + + self.details = {} + if details: + # Store our extra details + self.details.update(details) + + # Prepare Batch Mode Flag + self.batch_size = self.maximum_batch_size if batch else 1 + + # Assign our tags (if defined) + self.__tags = parse_list(tags) + + # Assign our entity (if defined) + self.entity = entity + + # Assign our alias (if defined) + self.alias = alias + + # Initialize our Targets + self.targets = [] + + # Sort our targets + for _target in parse_list(targets): + target = _target.strip() + if len(target) < 2: + self.logger.debug('Ignoring Opsgenie Entry: %s' % target) + continue + + if target.startswith(NotifyOpsgenie.template_tokens + ['target_team']['prefix']): + + self.targets.append( + {'type': OpsgenieCategory.TEAM, 'id': target[1:]} + if is_uuid(target[1:]) else + {'type': OpsgenieCategory.TEAM, 'name': target[1:]}) + + elif target.startswith(NotifyOpsgenie.template_tokens + ['target_schedule']['prefix']): + + self.targets.append( + {'type': OpsgenieCategory.SCHEDULE, 'id': target[1:]} + if is_uuid(target[1:]) else + {'type': OpsgenieCategory.SCHEDULE, 'name': target[1:]}) + + elif target.startswith(NotifyOpsgenie.template_tokens + ['target_escalation']['prefix']): + + self.targets.append( + {'type': OpsgenieCategory.ESCALATION, 'id': target[1:]} + if is_uuid(target[1:]) else + {'type': OpsgenieCategory.ESCALATION, 'name': target[1:]}) + + elif target.startswith(NotifyOpsgenie.template_tokens + ['target_user']['prefix']): + + self.targets.append( + {'type': OpsgenieCategory.USER, 'id': target[1:]} + if is_uuid(target[1:]) else + {'type': OpsgenieCategory.USER, 'username': target[1:]}) + + else: + # Ambiguious entry; treat it as a user but not before + # displaying a warning to the end user first: + self.logger.debug( + 'Treating ambigious Opsgenie target %s as a user', target) + self.targets.append( + {'type': OpsgenieCategory.USER, 'id': target} + if is_uuid(target) else + {'type': OpsgenieCategory.USER, 'username': target}) + + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): + """ + Perform Opsgenie Notification + """ + + headers = { + 'User-Agent': self.app_id, + 'Content-Type': 'application/json', + 'Authorization': 'GenieKey {}'.format(self.apikey), + } + + # Prepare our URL as it's based on our hostname + notify_url = OPSGENIE_API_LOOKUP[self.region_name] + + # Initialize our has_error flag + has_error = False + + # We want to manually set the title onto the body if specified + title_body = body if not title else '{}: {}'.format(title, body) + + # Create a copy ouf our details object + details = self.details.copy() + if 'type' not in details: + details['type'] = notify_type + + # Prepare our payload + payload = { + 'source': self.app_desc, + 'message': title_body, + 'description': body, + 'details': details, + 'priority': 'P{}'.format(self.priority), + } + + # Use our body directive if we exceed the minimum message + # limitation + if len(payload['message']) > self.opsgenie_body_minlen: + payload['message'] = '{}...'.format( + body[:self.opsgenie_body_minlen - 3]) + + if self.__tags: + payload['tags'] = self.__tags + + if self.entity: + payload['entity'] = self.entity + + if self.alias: + payload['alias'] = self.alias + + length = len(self.targets) if self.targets else 1 + for index in range(0, length, self.batch_size): + if self.targets: + # If there were no targets identified, then we simply + # just iterate once without the responders set + payload['responders'] = \ + self.targets[index:index + self.batch_size] + + # Some Debug Logging + self.logger.debug( + 'Opsgenie POST URL: {} (cert_verify={})'.format( + notify_url, self.verify_certificate)) + self.logger.debug('Opsgenie Payload: {}' .format(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + try: + r = requests.post( + notify_url, + data=dumps(payload), + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + ) + + if r.status_code not in ( + requests.codes.accepted, requests.codes.ok): + status_str = \ + NotifyBase.http_response_code_lookup( + r.status_code) + + self.logger.warning( + 'Failed to send Opsgenie notification:' + '{}{}error={}.'.format( + status_str, + ', ' if status_str else '', + r.status_code)) + + self.logger.debug( + 'Response Details:\r\n{}'.format(r.content)) + + # Mark our failure + has_error = True + continue + + # If we reach here; the message was sent + self.logger.info('Sent Opsgenie notification') + self.logger.debug( + 'Response Details:\r\n{}'.format(r.content)) + + except requests.RequestException as e: + self.logger.warning( + 'A Connection error occurred sending Opsgenie ' + 'notification.') + self.logger.debug('Socket Exception: %s' % str(e)) + # Mark our failure + has_error = True + + return not has_error + + def url(self, privacy=False, *args, **kwargs): + """ + Returns the URL built dynamically based on specified arguments. + """ + + _map = { + OpsgeniePriority.LOW: 'low', + OpsgeniePriority.MODERATE: 'moderate', + OpsgeniePriority.NORMAL: 'normal', + OpsgeniePriority.HIGH: 'high', + OpsgeniePriority.EMERGENCY: 'emergency', + } + + # Define any URL parameters + params = { + 'region': self.region_name, + 'priority': + _map[OpsgeniePriority.NORMAL] if self.priority not in _map + else _map[self.priority], + 'batch': 'yes' if self.batch_size > 1 else 'no', + } + + # Assign our entity value (if defined) + if self.entity: + params['entity'] = self.entity + + # Assign our alias value (if defined) + if self.alias: + params['alias'] = self.alias + + # Assign our tags (if specifed) + if self.__tags: + params['tags'] = ','.join(self.__tags) + + # Append our details into our parameters + params.update({'+{}'.format(k): v for k, v in self.details.items()}) + + # Extend our parameters + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + # A map allows us to map our target types so they can be correctly + # placed back into your URL below. Hence map the 'user' -> '@' + __map = { + OpsgenieCategory.USER: + NotifyOpsgenie.template_tokens['target_user']['prefix'], + OpsgenieCategory.SCHEDULE: + NotifyOpsgenie.template_tokens['target_schedule']['prefix'], + OpsgenieCategory.ESCALATION: + NotifyOpsgenie.template_tokens['target_escalation']['prefix'], + OpsgenieCategory.TEAM: + NotifyOpsgenie.template_tokens['target_team']['prefix'], + } + + return '{schema}://{apikey}/{targets}/?{params}'.format( + schema=self.secure_protocol, + apikey=self.pprint(self.apikey, privacy, safe=''), + targets='/'.join( + [NotifyOpsgenie.quote('{}{}'.format( + __map[x['type']], + x.get('id', x.get('name', x.get('username'))))) + for x in self.targets]), + params=NotifyOpsgenie.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 + + # The API Key is stored in the hostname + results['apikey'] = NotifyOpsgenie.unquote(results['host']) + + # Get our Targets + results['targets'] = NotifyOpsgenie.split_path(results['fullpath']) + + # Add our Meta Detail keys + results['details'] = {NotifyBase.unquote(x): NotifyBase.unquote(y) + for x, y in results['qsd+'].items()} + + if 'priority' in results['qsd'] and len(results['qsd']['priority']): + _map = { + # Letter Assignnments + 'l': OpsgeniePriority.LOW, + 'm': OpsgeniePriority.MODERATE, + 'n': OpsgeniePriority.NORMAL, + 'h': OpsgeniePriority.HIGH, + 'e': OpsgeniePriority.EMERGENCY, + 'lo': OpsgeniePriority.LOW, + 'me': OpsgeniePriority.MODERATE, + 'no': OpsgeniePriority.NORMAL, + 'hi': OpsgeniePriority.HIGH, + 'em': OpsgeniePriority.EMERGENCY, + # Support 3rd Party API Documented Scale + '1': OpsgeniePriority.LOW, + '2': OpsgeniePriority.MODERATE, + '3': OpsgeniePriority.NORMAL, + '4': OpsgeniePriority.HIGH, + '5': OpsgeniePriority.EMERGENCY, + 'p1': OpsgeniePriority.LOW, + 'p2': OpsgeniePriority.MODERATE, + 'p3': OpsgeniePriority.NORMAL, + 'p4': OpsgeniePriority.HIGH, + 'p5': OpsgeniePriority.EMERGENCY, + } + try: + results['priority'] = \ + _map[results['qsd']['priority'][0:2].lower()] + + except KeyError: + # No priority was set + pass + + # Get Batch Boolean (if set) + results['batch'] = \ + parse_bool( + results['qsd'].get( + 'batch', + NotifyOpsgenie.template_args['batch']['default'])) + + if 'apikey' in results['qsd'] and len(results['qsd']['apikey']): + results['apikey'] = \ + NotifyOpsgenie.unquote(results['qsd']['apikey']) + + if 'tags' in results['qsd'] and len(results['qsd']['tags']): + # Extract our tags + results['tags'] = \ + parse_list(NotifyOpsgenie.unquote(results['qsd']['tags'])) + + if 'region' in results['qsd'] and len(results['qsd']['region']): + # Extract our region + results['region_name'] = \ + NotifyOpsgenie.unquote(results['qsd']['region']) + + if 'entity' in results['qsd'] and len(results['qsd']['entity']): + # Extract optional entity field + results['entity'] = \ + NotifyOpsgenie.unquote(results['qsd']['entity']) + + if 'alias' in results['qsd'] and len(results['qsd']['alias']): + # Extract optional alias field + results['alias'] = \ + NotifyOpsgenie.unquote(results['qsd']['alias']) + + # Handle 'to' email address + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'].append(results['qsd']['to']) + + return results |