blob: ae265bcdbada09f5eec6d1b3f72e14a296e9918c [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 Kobusafbf4d02018-11-28 14:18:05 +010021from prometheus_client import Counter, Gauge
22
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010023import requests
24
25from simple_salesforce import Salesforce
26from simple_salesforce import exceptions as sf_exceptions
27
28
29STATE_MAP = {
30 'OK': '060 Informational',
31 'UP': '060 Informational',
32 'UNKNOWN': '070 Unknown',
33 'WARNING': '080 Warning',
34 'MINOR': '080 Warning',
35 'MAJOR': '090 Critical',
36 'CRITICAL': '090 Critical',
37 'DOWN': '090 Critical',
38 'UNREACHABLE': '090 Critical',
39}
40
41CONFIG_FIELD_MAP = {
42 'auth_url': 'instance',
43 'username': 'username',
44 'password': 'password',
45 'organization_id': 'organizationId',
46 'environment_id': 'environment_id',
47 'sandbox_enabled': 'domain',
48}
49
Michal Kobus73d33522018-12-10 11:41:13 +010050ALLOWED_HASHING = ('md5', 'sha256')
51
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010052logger = logging.getLogger(__name__)
53
54
55def sf_auth_retry(method):
56 def wrapper(self, *args, **kwargs):
57 try:
58 return method(self, *args, **kwargs)
59 except sf_exceptions.SalesforceExpiredSession:
60 logger.warning('Salesforce session expired.')
61 self._auth()
62 return method(self, *args, **kwargs)
63 return wrapper
64
65
66class SfNotifierError(Exception):
Michal Kobusee36c422018-11-26 15:02:31 +010067 pass
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010068
69
70class SalesforceClient(object):
71
72 def __init__(self, config):
Michal Kobusafbf4d02018-11-28 14:18:05 +010073 self.metrics = {
74 'sf_auth_ok': Gauge('sf_auth_ok', 'sf-notifier'),
75 'sf_error_count': Counter('sf_error_count', 'sf-notifier'),
76 'sf_request_count': Counter('sf_request_count', 'sf-notifier')
77 }
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010078 self.session = requests.Session()
79 self.config = self._validate_config(config)
Michal Kobus73d33522018-12-10 11:41:13 +010080 self.hash_func = self._hash_func()
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010081 self.environment = self.config.pop('environment_id')
82 self._auth()
83 self._registered_alerts = {}
84
Michal Kobusee36c422018-11-26 15:02:31 +010085 @staticmethod
Michal Kobus73d33522018-12-10 11:41:13 +010086 def _hash_func():
87 name = os.environ.get('SF_NOTIFIER_ALERT_ID_HASH_FUNC', 'sha256')
88 if name in ALLOWED_HASHING:
89 return getattr(hashlib, name)
90 return hashlib.sha256
91
92 @staticmethod
Michal Kobusee36c422018-11-26 15:02:31 +010093 def _validate_config(config):
94 kwargs = {}
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010095
96 for param, field in CONFIG_FIELD_MAP.iteritems():
97 setting_var = param.upper()
98 env_var = 'SFDC_{}'.format(setting_var)
99 kwargs[field] = os.environ.get(
Michal Kobusee36c422018-11-26 15:02:31 +0100100 env_var, config.get(setting_var))
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100101
102 if field == 'domain':
Michal Kobus17726ae2018-11-27 12:59:55 +0100103 if kwargs[field] in ['true', 'True', True]:
Michal Kobusee36c422018-11-26 15:02:31 +0100104 kwargs[field] = 'test'
105 else:
106 del kwargs[field]
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100107 continue
108
109 if kwargs[field] is None:
110 msg = ('Invalid config: missing "{}" field or "{}" environment'
111 ' variable.').format(param, env_var)
112 logger.error(msg)
113 raise SfNotifierError(msg)
114 return kwargs
115
116 def _auth(self):
Michal Kobusee36c422018-11-26 15:02:31 +0100117 kwargs = {'session': self.session}
118 kwargs.update(self.config)
Michal Kobus17726ae2018-11-27 12:59:55 +0100119 try:
120 self.sf = Salesforce(**kwargs)
121 except sf_exceptions.SalesforceAuthenticationFailed:
122 logger.error('Salesforce authentication failure.')
Michal Kobusafbf4d02018-11-28 14:18:05 +0100123 self.metrics['sf_auth_ok'].set(0)
Michal Kobus17726ae2018-11-27 12:59:55 +0100124 return
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100125 logger.info('Salesforce authentication successful.')
Michal Kobusafbf4d02018-11-28 14:18:05 +0100126 self.metrics['sf_auth_ok'].set(1)
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100127
Michal Kobus73d33522018-12-10 11:41:13 +0100128 def _get_alert_id(self, labels):
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100129 alert_id_data = ''
130 for key in sorted(labels):
131 alert_id_data += labels[key].replace(".", "\\.")
Michal Kobus73d33522018-12-10 11:41:13 +0100132 return self.hash_func(alert_id_data).hexdigest()
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100133
134 @sf_auth_retry
135 def _create_case(self, subject, body, labels, alert_id):
136
137 if alert_id in self._registered_alerts:
Michal Kobusafbf4d02018-11-28 14:18:05 +0100138 logger.warning('Duplicate case for alert: {}.'.format(alert_id))
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100139 return 1, self._registered_alerts[alert_id]['Id']
140
141 severity = labels.get('severity', 'unknown').upper()
142 payload = {
143 'Subject': subject,
144 'Description': body,
145 'IsMosAlert__c': 'true',
146 'Alert_Priority__c': STATE_MAP.get(severity, '070 Unknown'),
147 'Alert_Host__c': labels.get('host') or labels.get(
148 'instance', 'UNKNOWN'
149 ),
150 'Alert_Service__c': labels.get('service', 'UNKNOWN'),
151 'Environment2__c': self.environment,
152 'Alert_ID__c': alert_id,
153 }
Michal Kobusafbf4d02018-11-28 14:18:05 +0100154 logger.info('Try to create case: {}.'.format(payload))
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100155 try:
Michal Kobusafbf4d02018-11-28 14:18:05 +0100156 self.metrics['sf_request_count'].inc()
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100157 case = self.sf.Case.create(payload)
Michal Kobusafbf4d02018-11-28 14:18:05 +0100158 logger.info('Created case: {}.'.format(case))
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100159 except sf_exceptions.SalesforceMalformedRequest as ex:
160 msg = ex.content[0]['message']
161 err_code = ex.content[0]['errorCode']
162
163 if err_code == 'DUPLICATE_VALUE':
Michal Kobus17726ae2018-11-27 12:59:55 +0100164 logger.warning('Duplicate case: {}.'.format(msg))
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100165 case_id = msg.split()[-1]
166 self._registered_alerts[alert_id] = {'Id': case_id}
167 return 1, case_id
168 else:
Michal Kobusafbf4d02018-11-28 14:18:05 +0100169 self.metrics['sf_error_count'].inc()
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100170 raise
171
172 self._registered_alerts[alert_id] = {'Id': case['id']}
173 return 0, case['id']
174
175 @sf_auth_retry
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100176 def _close_case(self, case_id):
177 logger.info('Try to close case: {}.'.format(case_id))
Michal Kobusafbf4d02018-11-28 14:18:05 +0100178 self.metrics['sf_request_count'].inc()
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100179 update = self.sf.Case.update(
180 case_id,
181 {'Status': 'Auto-solved', 'Alert_ID__c': uuid.uuid4().hex}
182 )
183 logger.info('Closed case: {}.'.format(case_id))
184 return update
185
186 @sf_auth_retry
187 def _create_feed_item(self, subject, body, case_id):
188 feed_item = {'Title': subject, 'ParentId': case_id, 'Body': body}
189 return self.sf.FeedItem.create(feed_item)
190
191 @sf_auth_retry
192 def _get_case_by_alert_id(self, alert_id):
193 logger.info('Try to get case by alert ID: {}.'.format(alert_id))
194
195 if alert_id in self._registered_alerts:
196 return self._registered_alerts[alert_id]
197 try:
198 return self.sf.Case.get_by_custom_id('Alert_ID__c', alert_id)
199 except sf_exceptions.SalesforceResourceNotFound:
Michal Kobusba987052018-11-30 13:01:08 +0100200 if self._registered_alerts.get(alert_id):
201 del self._registered_alerts[alert_id]
202
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100203 logger.warning('Alert ID: {} was already solved.'.format(alert_id))
204
205 def create_case(self, subject, body, status, labels):
206 alert_id = self._get_alert_id(labels)
207
208 error_code, case_id = self._create_case(subject, body,
209 labels, alert_id)
210
211 response = {'case_id': case_id, 'alert_id': alert_id,
212 'status': 'created'}
213
214 if error_code != 2:
215 self._create_feed_item(subject, body, case_id)
216 if error_code == 1:
217 response['status'] = 'duplicate'
218 return response
219
220 def close_case(self, labels):
221 alert_id = self._get_alert_id(labels)
222 case = self._get_case_by_alert_id(alert_id)
223
224 response = {'alert_id': alert_id, 'status': 'resolved'}
225
226 if case is None:
227 return response
228
229 if self._registered_alerts.get(alert_id):
230 del self._registered_alerts[alert_id]
231
232 response['case_id'] = case['Id']
233 response['closed'] = self._close_case(case['Id'])
234 return response