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