sensu handler for salesforce
diff --git a/README.rst b/README.rst
index cd086a8..475c1db 100644
--- a/README.rst
+++ b/README.rst
@@ -104,6 +104,30 @@
password: pwd
virtual_host: '/monitor'
+Sensu SalesForce handler
+
+.. code-block:: yaml
+
+ sensu:
+ server:
+ enabled: true
+ handler:
+ default:
+ enabled: true
+ set:
+ - sfdc
+ stdout:
+ enabled: true
+ sfdc:
+ enabled: true
+ sfdc_client_id: "3MVG9Oe7T3Ol0ea4MKj"
+ sfdc_client_secret: 11482216293059
+ sfdc_username: test@test1.test
+ sfdc_password: passTemp
+ sfdc_auth_url: https://mysite--scloudqa.cs12.my.salesforce.com
+ environment: a2XV0000001
+ sfdc_organization_id: 00DV00000
+
Read more
=========
diff --git a/sensu/files/handlers/sfdc.json b/sensu/files/handlers/sfdc.json
new file mode 100644
index 0000000..87d2019
--- /dev/null
+++ b/sensu/files/handlers/sfdc.json
@@ -0,0 +1,27 @@
+{%- set handler = pillar.sensu.server.handler[handler_name] %}
+{%- if handler_setting == "handler" %}
+{
+ "handlers": {
+ "sfdc": {
+ "type": "pipe",
+ {%- if handler.mutator is defined %}
+ "mutator": "{{ handler.mutator }}",
+ {%- endif %}
+ "command": "/etc/sensu/handlers/sfdc.py"
+ }
+ }
+}
+{%- endif %}
+{%- if handler_setting == "config" %}
+{
+ "sfdc": {
+ "sfdc_client_id": "{{ handler.sfdc_client_id }}",
+ "sfdc_client_secret": "{{ handler.sfdc_client_secret }}",
+ "sfdc_username": "{{ handler.sfdc_username }}",
+ "sfdc_password": "{{ handler.sfdc_password }}",
+ "sfdc_auth_url": "{{ handler.sfdc_auth_url }}",
+ "environment": "{{ handler.environment }}",
+ "sfdc_organization_id": "{{ handler.sfdc_organization_id }}"
+ }
+}
+{%- endif %}
\ No newline at end of file
diff --git a/sensu/files/plugins/handlers/notification/salesforce.py b/sensu/files/plugins/handlers/notification/salesforce.py
new file mode 100644
index 0000000..dc31a1c
--- /dev/null
+++ b/sensu/files/plugins/handlers/notification/salesforce.py
@@ -0,0 +1,264 @@
+# Copyright 2016 Mirantis, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+#
+
+
+import requests
+import json
+import xml.dom.minidom
+import logging
+
+#requests.packages.urllib3.disable_warnings()
+
+LOG = logging.getLogger()
+
+class OAuth2(object):
+ def __init__(self, client_id, client_secret, username, password, auth_url=None, organizationId=None):
+ if not auth_url:
+ auth_url = 'https://login.salesforce.com'
+
+ self.auth_url = auth_url
+ self.client_id = client_id
+ self.client_secret = client_secret
+ self.username = username
+ self.password = password
+ self.organizationId = organizationId
+
+ def getUniqueElementValueFromXmlString(self, xmlString, elementName):
+ """
+ Extracts an element value from an XML string.
+
+ For example, invoking
+ getUniqueElementValueFromXmlString('<?xml version="1.0" encoding="UTF-8"?><foo>bar</foo>', 'foo')
+ should return the value 'bar'.
+ """
+ xmlStringAsDom = xml.dom.minidom.parseString(xmlString)
+ elementsByName = xmlStringAsDom.getElementsByTagName(elementName)
+ elementValue = None
+ if len(elementsByName) > 0:
+ elementValue = elementsByName[0].toxml().replace('<' + elementName + '>', '').replace('</' + elementName + '>', '')
+ return elementValue
+
+
+
+ def authenticate_soap(self):
+
+ soap_url = '{}/services/Soap/u/36.0'.format(self.auth_url)
+
+ login_soap_request_body = """<?xml version="1.0" encoding="utf-8" ?>
+ <soapenv:Envelope
+ xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
+ xmlns:urn="urn:partner.soap.sforce.com">
+ <soapenv:Header>
+ <urn:CallOptions>
+ <urn:client>RestForce</urn:client>
+ <urn:defaultNamespace>sf</urn:defaultNamespace>
+ </urn:CallOptions>
+ <urn:LoginScopeHeader>
+ <urn:organizationId>{organizationId}</urn:organizationId>
+ </urn:LoginScopeHeader>
+ </soapenv:Header>
+ <soapenv:Body>
+ <urn:login>
+ <urn:username>{username}</urn:username>
+ <urn:password>{password}</urn:password>
+ </urn:login>
+ </soapenv:Body>
+ </soapenv:Envelope>""".format(
+ username=self.username, password=self.password, organizationId=self.organizationId)
+
+ login_soap_request_headers = {
+ 'content-type': 'text/xml',
+ 'charset': 'UTF-8',
+ 'SOAPAction': 'login'
+ }
+
+ response = requests.post(soap_url,
+ login_soap_request_body,
+ headers=login_soap_request_headers)
+ LOG.debug(response)
+ LOG.debug(response.status_code)
+ LOG.debug(response.text)
+
+
+ session_id = self.getUniqueElementValueFromXmlString(response.content, 'sessionId')
+ server_url = self.getUniqueElementValueFromXmlString(response.content, 'serverUrl')
+
+ response_json = {
+ 'access_token': session_id,
+ 'instance_url': self.auth_url
+ }
+
+ session_id = self.getUniqueElementValueFromXmlString(response.content, 'sessionId')
+ response.raise_for_status()
+ return response_json
+
+ def authenticate_rest(self):
+ data = {
+ 'grant_type': 'password',
+ 'client_id': self.client_id,
+ 'client_secret': self.client_secret,
+ 'username': self.username,
+ 'password': self.password,
+ }
+
+ url = '{}/services/oauth2/token'.format(self.auth_url)
+ response = requests.post(url, data=data)
+ response.raise_for_status()
+ return response.json()
+
+
+ def authenticate(self, **kwargs):
+ if self.organizationId:
+ LOG.debug('self.organizationId={}'.format(self.organizationId))
+ LOG.debug('Auth method = SOAP')
+ return self.authenticate_soap( **kwargs )
+ else:
+ LOG.debug('Auth method = REST')
+ return self.authenticate_rest( **kwargs )
+
+
+
+
+class Client(object):
+ def __init__(self, oauth2):
+ self.oauth2 = oauth2
+
+ self.access_token = None
+ self.instance_url = None
+
+ def ticket(self, id):
+ try:
+ return self.get('/services/data/v36.0/sobjects/proxyTicket__c/{}'.format(id)).json()
+ except requests.HTTPError:
+ return False
+
+ def create_mos_alert(self, data):
+ return self.post('/services/data/v36.0/sobjects/MOS_Alerts__c', data=json.dumps(data), headers={"content-type": "application/json"})
+
+ def create_mos_alert_comment(self, data):
+ return self.post('/services/data/v36.0/sobjects/MOS_Alert_Comment__c', data=json.dumps(data), headers={"content-type": "application/json"})
+
+ def get_mos_alert_comment(self, id):
+ return self.get('/services/data/v36.0/sobjects/MOS_Alert_Comment__c/{}'.format(id))
+
+
+ def del_mos_alert_comment(self, id):
+ return self.delete('/services/data/v36.0/sobjects/MOS_Alert_Comment__c/{}'.format(id))
+
+
+ def create_feeditem(self, data):
+ return self.post('/services/data/v36.0/sobjects/FeedItem', data=json.dumps(data), headers={"content-type": "application/json"})
+
+
+ def create_case(self, data):
+ return self.post('/services/data/v36.0/sobjects/Case', data=json.dumps(data), headers={"content-type": "application/json"})
+
+
+ def create_ticket(self, data):
+ return self.post('/services/data/v36.0/sobjects/Case', data=json.dumps(data), headers={"content-type": "application/json"}).json()
+
+ def get_case(self, id):
+ return self.get('/services/data/v36.0/sobjects/Case/{}'.format(id))
+
+ def get_mos_alert(self, id):
+ return self.get('/services/data/v36.0/sobjects/MOS_Alerts__c/{}'.format(id))
+
+ def del_mos_alert(self, id):
+ return self.delete('/services/data/v36.0/sobjects/MOS_Alerts__c/{}'.format(id))
+
+
+ def update_ticket(self, id, data):
+ return self.patch('/services/data/v36.0/sobjects/proxyTicket__c/{}'.format(id), data=json.dumps(data), headers={"content-type": "application/json"})
+
+ def update_mos_alert(self, id, data):
+ return self.patch('/services/data/v36.0/sobjects/MOS_Alerts__c/{}'.format(id), data=json.dumps(data), headers={"content-type": "application/json"})
+
+ def update_case(self, id, data):
+ return self.patch('/services/data/v36.0/sobjects/Case/{}'.format(id), data=json.dumps(data), headers={"content-type": "application/json"})
+
+
+ def update_comment(self, id, data):
+ return self.patch('/services/data/v36.0/sobjects/proxyTicketComment__c/{}'.format(id), data=json.dumps(data), headers={"content-type": "application/json"})
+
+ def create_ticket_comment(self, data):
+ return self.post('/services/data/v36.0/sobjects/proxyTicketComment__c', data=json.dumps(data), headers={"content-type": "application/json"}).json()
+
+ def environment(self, id):
+ return self.get('/services/data/v36.0/sobjects/Environment__c/{}'.format(id)).json()
+
+ def ticket_comments(self, ticket_id):
+ return self.search("SELECT Comment__c, CreatedById, external_id__c, Id, CreatedDate, createdby.name "
+ "FROM proxyTicketComment__c "
+ "WHERE related_id__c='{}'".format(ticket_id))
+
+ def ticket_comment(self, comment_id):
+ return self.get('/services/data/v36.0/query',
+ params=dict(q="SELECT Comment__c, CreatedById, Id "
+ "FROM proxyTicketComment__c "
+ "WHERE external_id__c='{}'".format(comment_id))).json()
+ def search(self, query):
+ response = self.get('/services/data/v36.0/query', params=dict(q=query)).json()
+ while True:
+ for record in response['records']:
+ yield record
+
+ if response['done']:
+ return
+
+ response = self.get(response['nextRecordsUrl']).json()
+
+ def get(self, url, **kwargs):
+ return self._request('get', url, **kwargs)
+
+ def patch(self, url, **kwargs):
+ return self._request('patch', url, **kwargs)
+
+ def post(self, url, **kwargs):
+ return self._request('post', url, **kwargs)
+
+ def delete(self, url, **kwargs):
+ return self._request('delete', url, **kwargs)
+
+
+ def delete1(self, url, **kwargs):
+ return self._request('post', url, **kwargs)
+
+
+
+ def _request(self, method, url, headers=None, **kwargs):
+ if not headers:
+ headers = {}
+
+ if not self.access_token or not self.instance_url:
+ result = self.oauth2.authenticate()
+ self.access_token = result['access_token']
+ self.instance_url = result['instance_url']
+
+ headers['Authorization'] = 'Bearer {}'.format(self.access_token)
+
+ url = self.instance_url + url
+ print "URL", url
+ print "KWARGS", kwargs
+ response = requests.request(method, url, headers=headers, **kwargs)
+ print "RESPONSE", response
+# Debug only
+ LOG.debug("Response code: {}".format(response.status_code))
+ try:
+ LOG.debug("Response content: {}".format(json.dumps(response.json(),sort_keys=True, indent=4, separators=(',', ': '))))
+ except Exception:
+ LOG.debug("Response content: {}".format(response.content))
+
+ return response
diff --git a/sensu/files/plugins/handlers/notification/sfdc.py b/sensu/files/plugins/handlers/notification/sfdc.py
new file mode 100644
index 0000000..c813c1f
--- /dev/null
+++ b/sensu/files/plugins/handlers/notification/sfdc.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python
+
+import logging
+import os
+import sys
+import yaml
+import json
+import sys
+import smtplib
+import requests
+import json
+import socket
+from optparse import OptionParser
+from email.mime.text import MIMEText
+from datetime import datetime
+import dateutil.parser
+from argparse import ArgumentParser
+from salesforce import OAuth2, Client
+
+try:
+ from sensu import Handler
+except ImportError:
+ print('You must have the sensu Python module i.e.: pip install sensu')
+ sys.exit(1)
+
+DELTA_SECONDS=3000000000
+
+LOG = logging.getLogger()
+#LOG.setLevel(INFO)
+
+class SfdcHandler(Handler):
+
+ def handle(self):
+ client_id = self.settings.get('sfdc', {}).get('sfdc_client_id')
+ client_secret = self.settings.get('sfdc', {}).get('sfdc_client_secret')
+ username = self.settings.get('sfdc', {}).get('sfdc_username')
+ password = self.settings.get('sfdc', {}).get('sfdc_password')
+ auth_url = self.settings.get('sfdc', {}).get('sfdc_auth_url')
+ organization_id = self.settings.get('sfdc', {}).get('sfdc_organization_id')
+ environment = self.settings.get('sfdc', {}).get('environment')
+ print self.event
+ print "client_id: ", client_id
+ print "client_secrete: ", client_secret
+ print "auth_url: ", auth_url
+ print "organization: ", organization_id
+ print "username: ", username
+# print "password", password
+ sfdc_oauth2 = OAuth2(client_id, client_secret, username, password, auth_url, organization_id)
+
+ data = self.event
+ client_host = data.get('client', {}).get('name')
+ check_name = data.get('check', {}).get('name')
+ check_action = data.get('action')
+ timestamp = data.get('check', {}).get('issued')
+ check_date = datetime.fromtimestamp(int(timestamp)).strftime('%Y-%m-%d %H:%M:%S')
+ description = data.get('check', {}).get('output')
+ status = data.get('check', {}).get('status')
+ severity_map = {
+ 0: '060 Informational',
+ 1: '080 Warning',
+ 2: '090 Critical',
+ 3: '070 Unknown'
+ }
+ notification_map = {
+ 'create': 'PROBLEM',
+ 'resolve': 'RECOVERY'
+ }
+ if isinstance(status, int):
+ severity = severity_map[status]
+ else:
+ severity = "none"
+
+ if isinstance(check_action, str):
+ notification = notification_map[check_action]
+ else:
+ notification = "CUSTOM"
+
+ Alert_ID = '{}--{}--{}'.format(environment,client_host, check_name)
+
+ print 'Alert_Id: {} '.format(Alert_ID)
+ LOG.debug('Alert_Id: {} '.format(Alert_ID))
+
+ sfdc_client = Client(sfdc_oauth2)
+
+ print "severity", severity
+ print "check_action", check_action #resolve, create
+ print "description", description
+ print "long_date_time", check_date
+ print "environment", environment
+ print "NOTIFICATION", notification
+# severity = "CRITICAL"
+# notification = "PROBLEM"
+# check_date = "Wed Sep 7 14:09:58 CEST 2016"
+ payload = {
+ 'notification_type': notification,
+ 'description': description,
+ 'long_date_time': check_date,
+ }
+
+ data = {
+ 'IsMosAlert__c': 'true',
+ 'Description': json.dumps(payload, sort_keys=True, indent=4),
+ 'Alert_ID__c': Alert_ID,
+ 'Subject': Alert_ID,
+ 'Environment2__c': environment,
+ 'Alert_Priority__c': severity,
+ 'Alert_Host__c': client_host,
+ 'Alert_Service__c': check_name,
+
+ # 'sort_marker__c': sort_marker,
+ }
+
+ feed_data_body = {
+ 'Description': payload,
+ 'Alert_Id': Alert_ID,
+ 'Cloud_ID': environment,
+ 'Alert_Priority': severity,
+ 'Status': "New",
+ }
+
+ try:
+ new_case = sfdc_client.create_case(data)
+ except Exception as E:
+ print "new case exception", E
+ sys.exit(1)
+
+
+
+ # If Case exist
+ if (new_case.status_code == 400) and (new_case.json()[0]['errorCode'] == 'DUPLICATE_VALUE'):
+ LOG.debug('Code: {}, Error message: {} '.format(new_case.status_code, new_case.text))
+ # Find Case ID
+ ExistingCaseId = new_case.json()[0]['message'].split(" ")[-1]
+ LOG.debug('ExistingCaseId: {} '.format(ExistingCaseId))
+ # Get Case
+ current_case = sfdc_client.get_case(ExistingCaseId).json()
+ LOG.debug("Existing Case: \n {}".format(json.dumps(current_case,sort_keys=True, indent=4)))
+
+ LastModifiedDate=current_case['LastModifiedDate']
+ Now=datetime.now().replace(tzinfo=None)
+ delta = Now - dateutil.parser.parse(LastModifiedDate).replace(tzinfo=None)
+
+ LOG.debug("Check if Case should be marked as OUTDATED. Case modification date is: {} , Now: {} , Delta(sec): {}, OutdateDelta(sec): {}".format(LastModifiedDate, Now, delta.seconds, DELTA_SECONDS))
+ if (delta.seconds > DELTA_SECONDS):
+ # Old Case is outdated
+ new_data = {
+ 'Alert_Id__c': '{}_closed_at_{}'.format(current_case['Alert_ID__c'],datetime.strftime(datetime.now(), "%Y.%m.%d-%H:%M:%S")),
+ 'Alert_Priority__c': '000 OUTDATED',
+ }
+ u = sfdc_client.update_case(id=ExistingCaseId, data=new_data)
+ LOG.debug('Upate status code: {} \n\nUpate content: {}\n\n Upate headers: {}\n\n'.format(u.status_code,u.content, u.headers))
+
+ # Try to create new caset again
+ try:
+ new_case = sfdc_client.create_case(data)
+ except Exception as E:
+ LOG.debug(E)
+ sys.exit(1)
+ else:
+ # Case was outdated an new was created
+ CaseId = new_case.json()['id']
+ LOG.debug("Case was just created, old one was marked as Outdated")
+ # Add commnet, because Case head should conains LAST data overriden on any update
+ CaseId = new_case.json()['id']
+
+ feeditem_data = {
+ 'ParentId': CaseId,
+ 'Visibility': 'AllUsers',
+ 'Body': json.dumps(feed_data_body, sort_keys=True, indent=4),
+ }
+ LOG.debug("FeedItem Data: {}".format(json.dumps(feeditem_data, sort_keys=True, indent=4)))
+ add_feed_item = sfdc_client.create_feeditem(feeditem_data)
+ LOG.debug('Add FeedItem status code: {} \n Add FeedItem reply: {} '.format(add_feed_item.status_code, add_feed_item.text))
+
+ else:
+ # Update Case
+ u = sfdc_client.update_case(id=ExistingCaseId, data=data)
+ LOG.debug('Upate status code: {} '.format(u.status_code))
+
+ feeditem_data = {
+ 'ParentId': ExistingCaseId,
+ 'Visibility': 'AllUsers',
+ 'Body': json.dumps(feed_data_body, sort_keys=True, indent=4),
+ }
+
+ LOG.debug("FeedItem Data: {}".format(json.dumps(feeditem_data, sort_keys=True, indent=4)))
+ add_feed_item = sfdc_client.create_feeditem(feeditem_data)
+ LOG.debug('Add FeedItem status code: {} \n Add FeedItem reply: {} '.format(add_feed_item.status_code, add_feed_item.text))
+
+ # Else If Case did not exist before and was just created
+ elif (new_case.status_code == 201):
+ LOG.debug("Case was just created")
+ # Add commnet, because Case head should conains LAST data overriden on any update
+ CaseId = new_case.json()['id']
+ feeditem_data = {
+ 'ParentId': CaseId,
+ 'Visibility': 'AllUsers',
+ 'Body': json.dumps(feed_data_body, sort_keys=True, indent=4),
+ }
+ LOG.debug("FeedItem Data: {}".format(json.dumps(feeditem_data, sort_keys=True, indent=4)))
+ add_feed_item = sfdc_client.create_feeditem(feeditem_data)
+ LOG.debug('Add FeedItem status code: {} \n Add FeedItem reply: {} '.format(add_feed_item.status_code, add_feed_item.text))
+
+ else:
+ LOG.debug("Unexpected error: Case was not created (code !=201) and Case does not exist (code != 400)")
+ sys.exit(1)
+
+ def check_kedb(self):
+ host = self.settings.get('sfdc', {}).get('kedb_host', 'localhost')
+ port = self.settings.get('sfdc', {}).get('kedb_port', 25)
+ url = 'http://%s:%s/handle/' % (host, port)
+ print 'URL============================='
+ print url
+ payload = {
+ 'event': self.event,
+ }
+ print 'PAYLOAD============================='
+ print payload
+ response = requests.post(url, data=json.dumps(payload))
+ print 'RESPONSE============================='
+ print response
+ print 'RESPONSE-DATA=========================='
+ self.event = response.json()
+ print self.event
+# return data
+
+if __name__=='__main__':
+ m = SfdcHandler()
+ sys.exit(0)
diff --git a/sensu/server.sls b/sensu/server.sls
index 2c3c781..237a6ca 100644
--- a/sensu/server.sls
+++ b/sensu/server.sls
@@ -86,7 +86,7 @@
{%- for handler_name, handler in server.get('handler', {}).iteritems() %}
-{%- if handler_name in ['default', 'flapjack', 'mail', 'sccd', 'stdout', 'statsd', 'slack', 'pipe'] %}
+{%- if handler_name in ['default', 'flapjack', 'mail', 'sccd', 'stdout', 'statsd', 'slack', 'pipe', 'sfdc'] %}
{%- include "sensu/server/_handler_"+handler_name+".sls" %}
diff --git a/sensu/server/_handler_sfdc.sls b/sensu/server/_handler_sfdc.sls
new file mode 100644
index 0000000..249d3ec
--- /dev/null
+++ b/sensu/server/_handler_sfdc.sls
@@ -0,0 +1,42 @@
+
+/etc/sensu/conf.d/sfdc.json:
+ file.managed:
+ - source: salt://sensu/files/handlers/sfdc.json
+ - template: jinja
+ - defaults:
+ handler_name: "{{ handler_name }}"
+ handler_setting: "config"
+ - watch_in:
+ - service: service_sensu_server
+ - service: service_sensu_api
+ - require_in:
+ - file: sensu_conf_dir_clean
+
+/etc/sensu/conf.d/handler_sfdc.json:
+ file.managed:
+ - source: salt://sensu/files/handlers/sfdc.json
+ - template: jinja
+ - defaults:
+ handler_name: "{{ handler_name }}"
+ handler_setting: "handler"
+ - watch_in:
+ - service: service_sensu_server
+ - service: service_sensu_api
+ - require_in:
+ - file: sensu_conf_dir_clean
+
+/etc/sensu/handlers/sfdc.py:
+ file.managed:
+ - source: salt://sensu/files/plugins/handlers/notification/sfdc.py
+ - mode: 755
+ - watch_in:
+ - service: service_sensu_server
+ - service: service_sensu_api
+
+/etc/sensu/handlers/salesforce.py:
+ file.managed:
+ - source: salt://sensu/files/plugins/handlers/notification/salesforce.py
+ - mode: 644
+ - watch_in:
+ - service: service_sensu_server
+ - service: service_sensu_api