blob: 5adc691b013af291ace89c5a7f5d8d0804425904 [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
50logger = logging.getLogger(__name__)
51
52
53def sf_auth_retry(method):
54 def wrapper(self, *args, **kwargs):
55 try:
56 return method(self, *args, **kwargs)
57 except sf_exceptions.SalesforceExpiredSession:
58 logger.warning('Salesforce session expired.')
59 self._auth()
60 return method(self, *args, **kwargs)
61 return wrapper
62
63
64class SfNotifierError(Exception):
Michal Kobusee36c422018-11-26 15:02:31 +010065 pass
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010066
67
68class SalesforceClient(object):
69
70 def __init__(self, config):
Michal Kobusafbf4d02018-11-28 14:18:05 +010071 self.metrics = {
72 'sf_auth_ok': Gauge('sf_auth_ok', 'sf-notifier'),
73 'sf_error_count': Counter('sf_error_count', 'sf-notifier'),
74 'sf_request_count': Counter('sf_request_count', 'sf-notifier')
75 }
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010076 self.session = requests.Session()
77 self.config = self._validate_config(config)
78 self.environment = self.config.pop('environment_id')
79 self._auth()
80 self._registered_alerts = {}
81
Michal Kobusee36c422018-11-26 15:02:31 +010082 @staticmethod
83 def _validate_config(config):
84 kwargs = {}
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010085
86 for param, field in CONFIG_FIELD_MAP.iteritems():
87 setting_var = param.upper()
88 env_var = 'SFDC_{}'.format(setting_var)
89 kwargs[field] = os.environ.get(
Michal Kobusee36c422018-11-26 15:02:31 +010090 env_var, config.get(setting_var))
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010091
92 if field == 'domain':
Michal Kobus17726ae2018-11-27 12:59:55 +010093 if kwargs[field] in ['true', 'True', True]:
Michal Kobusee36c422018-11-26 15:02:31 +010094 kwargs[field] = 'test'
95 else:
96 del kwargs[field]
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +010097 continue
98
99 if kwargs[field] is None:
100 msg = ('Invalid config: missing "{}" field or "{}" environment'
101 ' variable.').format(param, env_var)
102 logger.error(msg)
103 raise SfNotifierError(msg)
104 return kwargs
105
106 def _auth(self):
Michal Kobusee36c422018-11-26 15:02:31 +0100107 kwargs = {'session': self.session}
108 kwargs.update(self.config)
Michal Kobus17726ae2018-11-27 12:59:55 +0100109 try:
110 self.sf = Salesforce(**kwargs)
111 except sf_exceptions.SalesforceAuthenticationFailed:
112 logger.error('Salesforce authentication failure.')
Michal Kobusafbf4d02018-11-28 14:18:05 +0100113 self.metrics['sf_auth_ok'].set(0)
Michal Kobus17726ae2018-11-27 12:59:55 +0100114 return
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100115 logger.info('Salesforce authentication successful.')
Michal Kobusafbf4d02018-11-28 14:18:05 +0100116 self.metrics['sf_auth_ok'].set(1)
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100117
Michal Kobusee36c422018-11-26 15:02:31 +0100118 @staticmethod
119 def _get_alert_id(labels):
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100120 alert_id_data = ''
121 for key in sorted(labels):
122 alert_id_data += labels[key].replace(".", "\\.")
123 return hashlib.sha256(alert_id_data).hexdigest()
124
125 @sf_auth_retry
126 def _create_case(self, subject, body, labels, alert_id):
127
128 if alert_id in self._registered_alerts:
Michal Kobusafbf4d02018-11-28 14:18:05 +0100129 logger.warning('Duplicate case for alert: {}.'.format(alert_id))
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100130 return 1, self._registered_alerts[alert_id]['Id']
131
132 severity = labels.get('severity', 'unknown').upper()
133 payload = {
134 'Subject': subject,
135 'Description': body,
136 'IsMosAlert__c': 'true',
137 'Alert_Priority__c': STATE_MAP.get(severity, '070 Unknown'),
138 'Alert_Host__c': labels.get('host') or labels.get(
139 'instance', 'UNKNOWN'
140 ),
141 'Alert_Service__c': labels.get('service', 'UNKNOWN'),
142 'Environment2__c': self.environment,
143 'Alert_ID__c': alert_id,
144 }
Michal Kobusafbf4d02018-11-28 14:18:05 +0100145 logger.info('Try to create case: {}.'.format(payload))
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100146 try:
Michal Kobusafbf4d02018-11-28 14:18:05 +0100147 self.metrics['sf_request_count'].inc()
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100148 case = self.sf.Case.create(payload)
Michal Kobusafbf4d02018-11-28 14:18:05 +0100149 logger.info('Created case: {}.'.format(case))
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100150 except sf_exceptions.SalesforceMalformedRequest as ex:
151 msg = ex.content[0]['message']
152 err_code = ex.content[0]['errorCode']
153
154 if err_code == 'DUPLICATE_VALUE':
Michal Kobus17726ae2018-11-27 12:59:55 +0100155 logger.warning('Duplicate case: {}.'.format(msg))
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100156 case_id = msg.split()[-1]
157 self._registered_alerts[alert_id] = {'Id': case_id}
158 return 1, case_id
159 else:
Michal Kobusafbf4d02018-11-28 14:18:05 +0100160 self.metrics['sf_error_count'].inc()
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100161 raise
162
163 self._registered_alerts[alert_id] = {'Id': case['id']}
164 return 0, case['id']
165
166 @sf_auth_retry
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100167 def _close_case(self, case_id):
168 logger.info('Try to close case: {}.'.format(case_id))
Michal Kobusafbf4d02018-11-28 14:18:05 +0100169 self.metrics['sf_request_count'].inc()
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100170 update = self.sf.Case.update(
171 case_id,
172 {'Status': 'Auto-solved', 'Alert_ID__c': uuid.uuid4().hex}
173 )
174 logger.info('Closed case: {}.'.format(case_id))
175 return update
176
177 @sf_auth_retry
178 def _create_feed_item(self, subject, body, case_id):
179 feed_item = {'Title': subject, 'ParentId': case_id, 'Body': body}
180 return self.sf.FeedItem.create(feed_item)
181
182 @sf_auth_retry
183 def _get_case_by_alert_id(self, alert_id):
184 logger.info('Try to get case by alert ID: {}.'.format(alert_id))
185
186 if alert_id in self._registered_alerts:
187 return self._registered_alerts[alert_id]
188 try:
189 return self.sf.Case.get_by_custom_id('Alert_ID__c', alert_id)
190 except sf_exceptions.SalesforceResourceNotFound:
Michal Kobusba987052018-11-30 13:01:08 +0100191 if self._registered_alerts.get(alert_id):
192 del self._registered_alerts[alert_id]
193
Mateusz Matuszkowiak2820c662018-11-21 12:07:25 +0100194 logger.warning('Alert ID: {} was already solved.'.format(alert_id))
195
196 def create_case(self, subject, body, status, labels):
197 alert_id = self._get_alert_id(labels)
198
199 error_code, case_id = self._create_case(subject, body,
200 labels, alert_id)
201
202 response = {'case_id': case_id, 'alert_id': alert_id,
203 'status': 'created'}
204
205 if error_code != 2:
206 self._create_feed_item(subject, body, case_id)
207 if error_code == 1:
208 response['status'] = 'duplicate'
209 return response
210
211 def close_case(self, labels):
212 alert_id = self._get_alert_id(labels)
213 case = self._get_case_by_alert_id(alert_id)
214
215 response = {'alert_id': alert_id, 'status': 'resolved'}
216
217 if case is None:
218 return response
219
220 if self._registered_alerts.get(alert_id):
221 del self._registered_alerts[alert_id]
222
223 response['case_id'] = case['Id']
224 response['closed'] = self._close_case(case['Id'])
225 return response