diff --git a/.coveragerc b/.coveragerc index e35695fb8b2..c1fe30c6f42 100644 --- a/.coveragerc +++ b/.coveragerc @@ -806,6 +806,7 @@ omit = homeassistant/components/spc/* homeassistant/components/speedtestdotnet/* homeassistant/components/spider/* + homeassistant/components/splunk/* homeassistant/components/spotcrime/sensor.py homeassistant/components/spotify/__init__.py homeassistant/components/spotify/media_player.py diff --git a/CODEOWNERS b/CODEOWNERS index 613617e9942..5b7f0c1ae3f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -404,6 +404,7 @@ homeassistant/components/sonos/* @cgtobi homeassistant/components/spaceapi/* @fabaff homeassistant/components/speedtestdotnet/* @rohankapoorcom @engrbm87 homeassistant/components/spider/* @peternijssen +homeassistant/components/splunk/* @Bre77 homeassistant/components/spotify/* @frenck homeassistant/components/sql/* @dgomes homeassistant/components/squeezebox/* @rajlaud diff --git a/homeassistant/components/splunk/__init__.py b/homeassistant/components/splunk/__init__.py index bbff510db14..ec1471a2272 100644 --- a/homeassistant/components/splunk/__init__.py +++ b/homeassistant/components/splunk/__init__.py @@ -1,9 +1,10 @@ -"""Support to send data to an Splunk instance.""" +"""Support to send data to a Splunk instance.""" import json import logging +import time -from aiohttp.hdrs import AUTHORIZATION -import requests +from aiohttp import ClientConnectionError, ClientResponseError +from hass_splunk import SplunkPayloadError, hass_splunk import voluptuous as vol from homeassistant.const import ( @@ -16,14 +17,15 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, ) from homeassistant.helpers import state as state_helper +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.helpers.json import JSONEncoder _LOGGER = logging.getLogger(__name__) -CONF_FILTER = "filter" DOMAIN = "splunk" +CONF_FILTER = "filter" DEFAULT_HOST = "localhost" DEFAULT_PORT = 8088 @@ -48,23 +50,7 @@ CONFIG_SCHEMA = vol.Schema( ) -def post_request(event_collector, body, headers, verify_ssl): - """Post request to Splunk.""" - try: - payload = {"host": event_collector, "event": body} - requests.post( - event_collector, - data=json.dumps(payload, cls=JSONEncoder), - headers=headers, - timeout=10, - verify=verify_ssl, - ) - - except requests.exceptions.RequestException as error: - _LOGGER.exception("Error saving event to Splunk: %s", error) - - -def setup(hass, config): +async def async_setup(hass, config): """Set up the Splunk component.""" conf = config[DOMAIN] host = conf.get(CONF_HOST) @@ -75,18 +61,33 @@ def setup(hass, config): name = conf.get(CONF_NAME) entity_filter = conf[CONF_FILTER] - if use_ssl: - uri_scheme = "https://" - else: - uri_scheme = "http://" + event_collector = hass_splunk( + session=async_get_clientsession(hass), + host=host, + port=port, + token=token, + use_ssl=use_ssl, + verify_ssl=verify_ssl, + ) - event_collector = f"{uri_scheme}{host}:{port}/services/collector/event" - headers = {AUTHORIZATION: f"Splunk {token}"} + if not await event_collector.check(connectivity=False, token=True, busy=False): + return False - def splunk_event_listener(event): + payload = { + "time": time.time(), + "host": name, + "event": { + "domain": DOMAIN, + "meta": "Splunk integration has started", + }, + } + + await event_collector.queue(json.dumps(payload, cls=JSONEncoder), send=False) + + async def splunk_event_listener(event): """Listen for new messages on the bus and sends them to Splunk.""" - state = event.data.get("new_state") + state = event.data.get("new_state") if state is None or not entity_filter(state.entity_id): return @@ -95,19 +96,29 @@ def setup(hass, config): except ValueError: _state = state.state - json_body = [ - { + payload = { + "time": event.time_fired.timestamp(), + "host": name, + "event": { "domain": state.domain, "entity_id": state.object_id, "attributes": dict(state.attributes), - "time": str(event.time_fired), "value": _state, - "host": name, - } - ] + }, + } - post_request(event_collector, json_body, headers, verify_ssl) + try: + await event_collector.queue(json.dumps(payload, cls=JSONEncoder), send=True) + except SplunkPayloadError as err: + if err.status == 401: + _LOGGER.error(err) + else: + _LOGGER.warning(err) + except ClientConnectionError as err: + _LOGGER.warning(err) + except ClientResponseError as err: + _LOGGER.error(err.message) - hass.bus.listen(EVENT_STATE_CHANGED, splunk_event_listener) + hass.bus.async_listen(EVENT_STATE_CHANGED, splunk_event_listener) return True diff --git a/homeassistant/components/splunk/manifest.json b/homeassistant/components/splunk/manifest.json index 337458b4c3f..aaddac2609d 100644 --- a/homeassistant/components/splunk/manifest.json +++ b/homeassistant/components/splunk/manifest.json @@ -2,5 +2,10 @@ "domain": "splunk", "name": "Splunk", "documentation": "https://www.home-assistant.io/integrations/splunk", - "codeowners": [] -} + "requirements": [ + "hass_splunk==0.1.0" + ], + "codeowners": [ + "@Bre77" + ] +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 5ec5ccd8784..73e33aa80ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -722,6 +722,9 @@ hangups==0.4.11 # homeassistant.components.cloud hass-nabucasa==0.37.0 +# homeassistant.components.splunk +hass_splunk==0.1.0 + # homeassistant.components.jewish_calendar hdate==0.9.5 diff --git a/tests/components/splunk/__init__.py b/tests/components/splunk/__init__.py deleted file mode 100644 index 709483291e3..00000000000 --- a/tests/components/splunk/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the splunk component.""" diff --git a/tests/components/splunk/test_init.py b/tests/components/splunk/test_init.py deleted file mode 100644 index 86de865bc0d..00000000000 --- a/tests/components/splunk/test_init.py +++ /dev/null @@ -1,182 +0,0 @@ -"""The tests for the Splunk component.""" -import json -import unittest -from unittest import mock - -import homeassistant.components.splunk as splunk -from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON -from homeassistant.core import State -from homeassistant.helpers import state as state_helper -from homeassistant.setup import setup_component -import homeassistant.util.dt as dt_util - -from tests.common import get_test_home_assistant, mock_state_change_event - - -class TestSplunk(unittest.TestCase): - """Test the Splunk component.""" - - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.addCleanup(self.tear_down_cleanup) - - def tear_down_cleanup(self): - """Stop everything that was started.""" - self.hass.stop() - - def test_setup_config_full(self): - """Test setup with all data.""" - config = { - "splunk": { - "host": "host", - "port": 123, - "token": "secret", - "ssl": "False", - "verify_ssl": "True", - "name": "hostname", - "filter": { - "exclude_domains": ["fake"], - "exclude_entities": ["fake.entity"], - }, - } - } - - self.hass.bus.listen = mock.MagicMock() - assert setup_component(self.hass, splunk.DOMAIN, config) - assert self.hass.bus.listen.called - assert EVENT_STATE_CHANGED == self.hass.bus.listen.call_args_list[0][0][0] - - def test_setup_config_defaults(self): - """Test setup with defaults.""" - config = {"splunk": {"host": "host", "token": "secret"}} - - self.hass.bus.listen = mock.MagicMock() - assert setup_component(self.hass, splunk.DOMAIN, config) - assert self.hass.bus.listen.called - assert EVENT_STATE_CHANGED == self.hass.bus.listen.call_args_list[0][0][0] - - def _setup(self, mock_requests): - """Test the setup.""" - # pylint: disable=attribute-defined-outside-init - self.mock_post = mock_requests.post - self.mock_request_exception = Exception - mock_requests.exceptions.RequestException = self.mock_request_exception - config = {"splunk": {"host": "host", "token": "secret", "port": 8088}} - - self.hass.bus.listen = mock.MagicMock() - setup_component(self.hass, splunk.DOMAIN, config) - self.handler_method = self.hass.bus.listen.call_args_list[0][0][1] - - @mock.patch.object(splunk, "requests") - def test_event_listener(self, mock_requests): - """Test event listener.""" - self._setup(mock_requests) - - now = dt_util.now() - valid = {"1": 1, "1.0": 1.0, STATE_ON: 1, STATE_OFF: 0, "foo": "foo"} - - for in_, out in valid.items(): - state = mock.MagicMock( - state=in_, - domain="fake", - object_id="entity", - attributes={"datetime_attr": now}, - ) - event = mock.MagicMock(data={"new_state": state}, time_fired=12345) - - try: - out = state_helper.state_as_number(state) - except ValueError: - out = state.state - - body = [ - { - "domain": "fake", - "entity_id": "entity", - "attributes": {"datetime_attr": now.isoformat()}, - "time": "12345", - "value": out, - "host": "HASS", - } - ] - - payload = { - "host": "http://host:8088/services/collector/event", - "event": body, - } - self.handler_method(event) - assert self.mock_post.call_count == 1 - assert self.mock_post.call_args == mock.call( - payload["host"], - data=json.dumps(payload), - headers={"Authorization": "Splunk secret"}, - timeout=10, - verify=True, - ) - self.mock_post.reset_mock() - - def _setup_with_filter(self, addl_filters=None): - """Test the setup.""" - config = { - "splunk": { - "host": "host", - "token": "secret", - "port": 8088, - "filter": { - "exclude_domains": ["excluded_domain"], - "exclude_entities": ["other_domain.excluded_entity"], - }, - } - } - if addl_filters: - config["splunk"]["filter"].update(addl_filters) - - setup_component(self.hass, splunk.DOMAIN, config) - - @mock.patch.object(splunk, "post_request") - def test_splunk_entityfilter(self, mock_requests): - """Test event listener.""" - # pylint: disable=no-member - self._setup_with_filter() - - testdata = [ - {"entity_id": "other_domain.other_entity", "filter_expected": False}, - {"entity_id": "other_domain.excluded_entity", "filter_expected": True}, - {"entity_id": "excluded_domain.other_entity", "filter_expected": True}, - ] - - for test in testdata: - mock_state_change_event(self.hass, State(test["entity_id"], "on")) - self.hass.block_till_done() - - if test["filter_expected"]: - assert not splunk.post_request.called - else: - assert splunk.post_request.called - - splunk.post_request.reset_mock() - - @mock.patch.object(splunk, "post_request") - def test_splunk_entityfilter_with_glob_filter(self, mock_requests): - """Test event listener.""" - # pylint: disable=no-member - self._setup_with_filter({"exclude_entity_globs": ["*.skip_*"]}) - - testdata = [ - {"entity_id": "other_domain.other_entity", "filter_expected": False}, - {"entity_id": "other_domain.excluded_entity", "filter_expected": True}, - {"entity_id": "excluded_domain.other_entity", "filter_expected": True}, - {"entity_id": "test.skip_me", "filter_expected": True}, - ] - - for test in testdata: - mock_state_change_event(self.hass, State(test["entity_id"], "on")) - self.hass.block_till_done() - - if test["filter_expected"]: - assert not splunk.post_request.called - else: - assert splunk.post_request.called - - splunk.post_request.reset_mock()