blob: 63d138fbe3a7c26e7ba8863830eb0b80228b6f27 [file] [log] [blame]
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +01001# Copyright 2018: Mirantis Inc.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16import hashlib
17import logging
18import os
19import uuid
20
Michal Kobus915f3cf2018-12-10 20:09:41 +010021from cachetools import TTLCache
22
Michal Kobusafbf4d02018-11-28 14:18:05 +010023from prometheus_client import Counter, Gauge
24
Michal Kobus915f3cf2018-12-10 20:09:41 +010025from requests import Session
26from requests import exceptions as req_exceptions
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010027
28from simple_salesforce import Salesforce
29from simple_salesforce import exceptions as sf_exceptions
30
31
32STATE_MAP = {
33 'OK': '060 Informational',
34 'UP': '060 Informational',
35 'UNKNOWN': '070 Unknown',
36 'WARNING': '080 Warning',
37 'MINOR': '080 Warning',
38 'MAJOR': '090 Critical',
39 'CRITICAL': '090 Critical',
40 'DOWN': '090 Critical',
41 'UNREACHABLE': '090 Critical',
42}
43
44CONFIG_FIELD_MAP = {
45 'auth_url': 'instance',
46 'username': 'username',
47 'password': 'password',
48 'organization_id': 'organizationId',
49 'environment_id': 'environment_id',
50 'sandbox_enabled': 'domain',
51}
52
Michal Kobus73d33522018-12-10 11:41:13 +010053ALLOWED_HASHING = ('md5', 'sha256')
54
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010055logger = logging.getLogger(__name__)
56
57
58def sf_auth_retry(method):
59 def wrapper(self, *args, **kwargs):
60 try:
61 return method(self, *args, **kwargs)
62 except sf_exceptions.SalesforceExpiredSession:
63 logger.warning('Salesforce session expired.')
64 self._auth()
Michal Kobus915f3cf2018-12-10 20:09:41 +010065 except req_exceptions.ConnectionError:
66 logger.error('Salesforce connection error.')
67 self._auth()
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010068 return method(self, *args, **kwargs)
69 return wrapper
70
71
72class SfNotifierError(Exception):
Michal Kobusee36c422018-11-26 15:02:31 +010073 pass
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010074
75
76class SalesforceClient(object):
77
78 def __init__(self, config):
Michal Kobusafbf4d02018-11-28 14:18:05 +010079 self.metrics = {
80 'sf_auth_ok': Gauge('sf_auth_ok', 'sf-notifier'),
81 'sf_error_count': Counter('sf_error_count', 'sf-notifier'),
82 'sf_request_count': Counter('sf_request_count', 'sf-notifier')
83 }
Michal Kobus915f3cf2018-12-10 20:09:41 +010084 self.session = Session()
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010085 self.config = self._validate_config(config)
Michal Kobus73d33522018-12-10 11:41:13 +010086 self.hash_func = self._hash_func()
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010087 self.environment = self.config.pop('environment_id')
88 self._auth()
Michal Kobus915f3cf2018-12-10 20:09:41 +010089 self._registered_alerts = TTLCache(maxsize=2048, ttl=300)
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010090
Michal Kobusee36c422018-11-26 15:02:31 +010091 @staticmethod
Michal Kobus73d33522018-12-10 11:41:13 +010092 def _hash_func():
93 name = os.environ.get('SF_NOTIFIER_ALERT_ID_HASH_FUNC', 'sha256')
94 if name in ALLOWED_HASHING:
95 return getattr(hashlib, name)
96 return hashlib.sha256
97
98 @staticmethod
Michal Kobusee36c422018-11-26 15:02:31 +010099 def _validate_config(config):
100 kwargs = {}
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100101
102 for param, field in CONFIG_FIELD_MAP.iteritems():
103 setting_var = param.upper()
104 env_var = 'SFDC_{}'.format(setting_var)
105 kwargs[field] = os.environ.get(
Michal Kobusee36c422018-11-26 15:02:31 +0100106 env_var, config.get(setting_var))
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100107
108 if field == 'domain':
Michal Kobus17726ae2018-11-27 12:59:55 +0100109 if kwargs[field] in ['true', 'True', True]:
Michal Kobusee36c422018-11-26 15:02:31 +0100110 kwargs[field] = 'test'
111 else:
112 del kwargs[field]
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100113 continue
114
115 if kwargs[field] is None:
116 msg = ('Invalid config: missing "{}" field or "{}" environment'
117 ' variable.').format(param, env_var)
118 logger.error(msg)
119 raise SfNotifierError(msg)
120 return kwargs
121
122 def _auth(self):
Michal Kobusee36c422018-11-26 15:02:31 +0100123 kwargs = {'session': self.session}
124 kwargs.update(self.config)
Michal Kobus17726ae2018-11-27 12:59:55 +0100125 try:
126 self.sf = Salesforce(**kwargs)
127 except sf_exceptions.SalesforceAuthenticationFailed:
128 logger.error('Salesforce authentication failure.')
Michal Kobusafbf4d02018-11-28 14:18:05 +0100129 self.metrics['sf_auth_ok'].set(0)
Michal Kobus17726ae2018-11-27 12:59:55 +0100130 return
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100131 logger.info('Salesforce authentication successful.')
Michal Kobusafbf4d02018-11-28 14:18:05 +0100132 self.metrics['sf_auth_ok'].set(1)
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100133
Michal Kobus73d33522018-12-10 11:41:13 +0100134 def _get_alert_id(self, labels):
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100135 alert_id_data = ''
136 for key in sorted(labels):
137 alert_id_data += labels[key].replace(".", "\\.")
Michal Kobus73d33522018-12-10 11:41:13 +0100138 return self.hash_func(alert_id_data).hexdigest()
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100139
140 @sf_auth_retry
141 def _create_case(self, subject, body, labels, alert_id):
142
143 if alert_id in self._registered_alerts:
Michal Kobusafbf4d02018-11-28 14:18:05 +0100144 logger.warning('Duplicate case for alert: {}.'.format(alert_id))
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100145 return 1, self._registered_alerts[alert_id]['Id']
146
147 severity = labels.get('severity', 'unknown').upper()
148 payload = {
149 'Subject': subject,
150 'Description': body,
151 'IsMosAlert__c': 'true',
152 'Alert_Priority__c': STATE_MAP.get(severity, '070 Unknown'),
153 'Alert_Host__c': labels.get('host') or labels.get(
154 'instance', 'UNKNOWN'
155 ),
156 'Alert_Service__c': labels.get('service', 'UNKNOWN'),
157 'Environment2__c': self.environment,
158 'Alert_ID__c': alert_id,
159 }
Michal Kobusafbf4d02018-11-28 14:18:05 +0100160 logger.info('Try to create case: {}.'.format(payload))
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100161 try:
Michal Kobusafbf4d02018-11-28 14:18:05 +0100162 self.metrics['sf_request_count'].inc()
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100163 case = self.sf.Case.create(payload)
Michal Kobusafbf4d02018-11-28 14:18:05 +0100164 logger.info('Created case: {}.'.format(case))
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100165 except sf_exceptions.SalesforceMalformedRequest as ex:
166 msg = ex.content[0]['message']
167 err_code = ex.content[0]['errorCode']
168
169 if err_code == 'DUPLICATE_VALUE':
Michal Kobus17726ae2018-11-27 12:59:55 +0100170 logger.warning('Duplicate case: {}.'.format(msg))
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100171 case_id = msg.split()[-1]
172 self._registered_alerts[alert_id] = {'Id': case_id}
173 return 1, case_id
174 else:
Michal Kobusafbf4d02018-11-28 14:18:05 +0100175 self.metrics['sf_error_count'].inc()
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100176 raise
177
178 self._registered_alerts[alert_id] = {'Id': case['id']}
179 return 0, case['id']
180
181 @sf_auth_retry
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100182 def _close_case(self, case_id):
183 logger.info('Try to close case: {}.'.format(case_id))
Michal Kobusafbf4d02018-11-28 14:18:05 +0100184 self.metrics['sf_request_count'].inc()
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100185 update = self.sf.Case.update(
186 case_id,
187 {'Status': 'Auto-solved', 'Alert_ID__c': uuid.uuid4().hex}
188 )
189 logger.info('Closed case: {}.'.format(case_id))
190 return update
191
192 @sf_auth_retry
193 def _create_feed_item(self, subject, body, case_id):
194 feed_item = {'Title': subject, 'ParentId': case_id, 'Body': body}
195 return self.sf.FeedItem.create(feed_item)
196
197 @sf_auth_retry
198 def _get_case_by_alert_id(self, alert_id):
199 logger.info('Try to get case by alert ID: {}.'.format(alert_id))
200
201 if alert_id in self._registered_alerts:
202 return self._registered_alerts[alert_id]
203 try:
204 return self.sf.Case.get_by_custom_id('Alert_ID__c', alert_id)
205 except sf_exceptions.SalesforceResourceNotFound:
Michal Kobusba987052018-11-30 13:01:08 +0100206 if self._registered_alerts.get(alert_id):
207 del self._registered_alerts[alert_id]
208
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100209 logger.warning('Alert ID: {} was already solved.'.format(alert_id))
210
Michal Kobus915f3cf2018-12-10 20:09:41 +0100211 def create_case(self, subject, body, labels):
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100212 alert_id = self._get_alert_id(labels)
213
214 error_code, case_id = self._create_case(subject, body,
215 labels, alert_id)
216
Michal Kobus915f3cf2018-12-10 20:09:41 +0100217 self._create_feed_item(subject, body, case_id)
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100218
Michal Kobus915f3cf2018-12-10 20:09:41 +0100219 response = {'case_id': case_id, 'alert_id': alert_id}
220
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100221 if error_code == 1:
222 response['status'] = 'duplicate'
Michal Kobus915f3cf2018-12-10 20:09:41 +0100223 else:
224 response['status'] = 'created'
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100225 return response
226
227 def close_case(self, labels):
228 alert_id = self._get_alert_id(labels)
229 case = self._get_case_by_alert_id(alert_id)
230
231 response = {'alert_id': alert_id, 'status': 'resolved'}
232
233 if case is None:
234 return response
235
236 if self._registered_alerts.get(alert_id):
237 del self._registered_alerts[alert_id]
238
239 response['case_id'] = case['Id']
240 response['closed'] = self._close_case(case['Id'])
241 return response