mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
Rebuilt Splunk using custom library (#40123)
* Rebuilt Splunk using splunk_data_sender * Fixing lint issues * Apply suggestions from code review Recommended Fixes Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Moved to single send queue and fixed ssl verify * Using coroutine and Asyncio.lock * Changed to custom library hass_splunk * Fixed "use_ssl" parameter * Better error catching * Better error log Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
"""Tests for the splunk component."""
|
@ -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()
|
Reference in New Issue
Block a user