Skip to content

Commit 38adcb0

Browse files
harshilgajera-crestmkolasinski-splunkdvarasani-crestmbruzda-splunksrv-rr-github-token
committed
feat: add dual stack support for sc4s ingestor (#922)
Update sc4s ingestor to support dual stack connection Runs IPV4: https://github.com/splunk/splunk-add-on-for-symantec-endpoint-protection/actions/runs/18777336078 IPV6: https://cd.splunkdev.com/taautomation/ta-automation-compatibility-tests/-/pipelines/30916895 --------- Co-authored-by: mkolasinski-splunk <[email protected]> Co-authored-by: dvarasani-crest <[email protected]> Co-authored-by: Marcin Bruzda <[email protected]> Co-authored-by: srv-rr-github-token <[email protected]>
1 parent e4b055d commit 38adcb0

File tree

2 files changed

+66
-65
lines changed

2 files changed

+66
-65
lines changed

pytest_splunk_addon/event_ingestors/sc4s_event_ingestor.py

Lines changed: 52 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -15,76 +15,87 @@
1515
#
1616
import socket
1717
from time import sleep
18-
import os
19-
import re
20-
import concurrent.futures
21-
from .base_event_ingestor import EventIngestor
2218
import logging
2319

20+
from typing import Dict
21+
22+
from .base_event_ingestor import EventIngestor
23+
2424
LOGGER = logging.getLogger("pytest-splunk-addon")
2525

2626

2727
class SC4SEventIngestor(EventIngestor):
2828
"""
29-
Class to Ingest Events via SC4S
30-
31-
The format for required_configs is::
32-
33-
{
34-
sc4s_host (str): Address of the Splunk Server. Do not provide http scheme in the host.
35-
sc4s_port (int): Port number of the above host address
36-
}
29+
Class to ingest events via SC4S (supports both IPv4 and IPv6)
3730
3831
Args:
3932
required_configs (dict): Dictionary containing splunk host and sc4s port
4033
"""
4134

42-
def __init__(self, required_configs):
35+
def __init__(self, required_configs: Dict[str, str]) -> None:
4336
self.sc4s_host = required_configs["sc4s_host"]
4437
self.sc4s_port = required_configs["sc4s_port"]
45-
self.server_address = (
46-
required_configs["sc4s_host"],
47-
required_configs["sc4s_port"],
48-
)
38+
39+
def _create_socket(self):
40+
"""Try all addresses (IPv4 and IPv6) and return a connected socket."""
41+
last_exc = None
42+
for res in socket.getaddrinfo(
43+
self.sc4s_host, self.sc4s_port, socket.AF_UNSPEC, socket.SOCK_STREAM
44+
):
45+
af, socktype, proto, _, sa = res
46+
try:
47+
sock = socket.socket(af, socktype, proto)
48+
if af == socket.AF_INET6:
49+
# Attempt dual-stack if supported
50+
try:
51+
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
52+
except (AttributeError, OSError):
53+
pass
54+
sock.connect(sa)
55+
return sock
56+
except Exception as e:
57+
last_exc = e
58+
LOGGER.debug(f"Failed to connect to {sa}: {e}")
59+
try:
60+
sock.close()
61+
except Exception:
62+
pass
63+
continue
64+
raise ConnectionError(
65+
f"Could not connect to SC4S at {self.sc4s_host}:{self.sc4s_port} via IPv4 or IPv6"
66+
) from last_exc
4967

5068
def ingest(self, events, thread_count):
5169
"""
52-
Ingests events in the splunk via sc4s (Single/Batch of Events)
70+
Ingests events in Splunk via SC4S (single/batch of events)
5371
5472
Args:
5573
events (list): Events with newline character or LineBreaker as separator
56-
5774
"""
5875

59-
# This loop just checks for a viable remote connection
76+
# Retry loop to establish connection
6077
tried = 0
61-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
6278
while True:
6379
try:
64-
sock.connect(self.server_address)
80+
sock = self._create_socket()
6581
break
6682
except Exception as e:
6783
tried += 1
68-
LOGGER.debug("Attempt {} to ingest data with SC4S".format(str(tried)))
84+
LOGGER.debug(f"Attempt {tried} to ingest data with SC4S")
6985
if tried > 90:
70-
LOGGER.error(
71-
"Failed to ingest event with SC4S {} times".format(str(tried))
72-
)
86+
LOGGER.error(f"Failed to ingest event with SC4S {tried} times")
7387
raise e
7488
sleep(1)
75-
finally:
76-
sock.close()
7789

78-
raw_events = list()
79-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
80-
sock.connect(self.server_address)
81-
for event in events:
82-
# raw_events.extend()
83-
for se in event.event.splitlines():
84-
try:
85-
sock.sendall(str.encode(se + "\n"))
86-
except Exception as e:
87-
LOGGER.debug("Attempt ingest data with SC4S=".format(se))
88-
LOGGER.exception(e)
89-
sleep(1)
90-
sock.close()
90+
# Send events
91+
try:
92+
for event in events:
93+
for se in event.event.splitlines():
94+
try:
95+
sock.sendall(str.encode(se + "\n"))
96+
except Exception as e:
97+
LOGGER.debug(f"Attempt ingest data with SC4S: {se}")
98+
LOGGER.exception(e)
99+
sleep(1)
100+
finally:
101+
sock.close()

tests/unit/tests_standard_lib/test_event_ingestors/test_sc4s_event_ingestor.py

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,6 @@ def socket_mock(monkeypatch):
2525
"socket.socket",
2626
socket_mock,
2727
)
28-
monkeypatch.setattr(
29-
"socket.AF_INET",
30-
"AF_INET",
31-
)
32-
monkeypatch.setattr(
33-
"socket.SOCK_STREAM",
34-
"SOCK_STREAM",
35-
)
3628
return socket_mock
3729

3830

@@ -46,31 +38,29 @@ def sleep_mock(monkeypatch):
4638

4739
def test_sc4s_data_can_be_ingested(socket_mock, sc4s_ingestor, sc4s_events):
4840
sc4s_ingestor.ingest(sc4s_events, 20)
49-
assert socket_mock.call_count == 2
50-
socket_mock.assert_has_calls(
51-
[call("AF_INET", "SOCK_STREAM"), call("AF_INET", "SOCK_STREAM")], any_order=True
52-
)
53-
assert socket_mock.connect.call_count == 2
54-
socket_mock.connect.assert_has_calls(
55-
[call(("127.0.0.1", 55730)), call(("127.0.0.1", 55730))]
56-
)
57-
assert socket_mock.sendall.call_count == 2
58-
assert socket_mock.close.call_count == 2
41+
assert socket_mock.call_count == 1 # Socket created once
42+
assert socket_mock.connect.call_count == 1
43+
socket_mock.connect.assert_called_with(("127.0.0.1", 55730))
44+
assert socket_mock.sendall.call_count == len(sc4s_events)
45+
assert socket_mock.close.call_count == 1
5946

6047

6148
def test_exception_raised_when_sc4s_socket_can_not_be_opened(
6249
socket_mock, sleep_mock, sc4s_ingestor, sc4s_events, caplog
6350
):
64-
socket_mock.connect.side_effect = Exception
65-
pytest.raises(Exception, sc4s_ingestor.ingest, *(sc4s_events, 20))
66-
assert "Failed to ingest event with SC4S 91 times" in caplog.messages
67-
assert socket_mock.connect.call_count == socket_mock.close.call_count == 91
51+
socket_mock.connect.side_effect = Exception("Connection failed")
52+
with pytest.raises(ConnectionError):
53+
sc4s_ingestor.ingest(sc4s_events, 20)
54+
assert "Failed to ingest event with SC4S 91 times" in caplog.text
55+
assert socket_mock.connect.call_count == 91
56+
assert socket_mock.close.call_count == 91
6857

6958

7059
def test_exception_raised_when_sc4s_event_sent(
7160
socket_mock, sleep_mock, sc4s_ingestor, sc4s_events, caplog
7261
):
7362
socket_mock.sendall.side_effect = Exception("Send data fail")
7463
sc4s_ingestor.ingest(sc4s_events, 20)
75-
assert "Send data fail" in caplog.messages
76-
assert socket_mock.connect.call_count == socket_mock.close.call_count == 2
64+
assert "Send data fail" in caplog.text
65+
assert socket_mock.connect.call_count == 1
66+
assert socket_mock.close.call_count == 1

0 commit comments

Comments
 (0)