Skip to content

Commit 6303bab

Browse files
harshilgajera-crestmkolasinski-splunkdvarasani-crestmbruzda-splunksrv-rr-github-token
authored
feat: add dual stack support for sc4s ingestor (#925)
Test runs: IPV4: https://github.com/splunk/splunk-add-on-for-symantec-endpoint-protection/actions/runs/18781386812 IPV6: https://cd.splunkdev.com/taautomation/ta-automation-compatibility-tests/-/pipelines/30981356 --------- 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 3480757 commit 6303bab

File tree

5 files changed

+74
-73
lines changed

5 files changed

+74
-73
lines changed

NOTICE

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
The following 3rd-party software packages may be used by or distributed with pytest-splunk-addon. Any information relevant to third-party vendors listed below are collected using common, reasonable means.
99

10-
Date generated: 2025-9-10
10+
Date generated: 2025-10-24
1111

12-
Revision ID: 467ba9ffed5261c0cc829cc791e1640f51a03879
12+
Revision ID: e90eb69fe293864434062ac2fac28699acc0db3c
1313

1414
================================================================================
1515
================================================================================
@@ -2120,7 +2120,7 @@ BSD-3-Clause, MIT-CMU, GPL-1.0-only
21202120
* BSD-3-Clause *
21212121
--------------------------------------------------------------------------------
21222122
File matches:
2123-
examples/searchparser.py
2123+
examples/booleansearchparser.py
21242124
--------------------------------------------------------------------------------
21252125

21262126

@@ -5903,10 +5903,10 @@ Copyright (c) 2016-2020 SISSA (International School for Advanced Studies).
59035903

59045904
Copyright (c) 2021 SISSA (International School for Advanced Studies).
59055905

5906-
Copyright (c) 2018-2020 SISSA (International School for Advanced Studies).
5907-
59085906
Copyright (c) 2016-2021 SISSA (International School for Advanced Studies).
59095907

5908+
Copyright (c) 2018-2020 SISSA (International School for Advanced Studies).
5909+
59105910
Copyright (c) 2016-2022 SISSA (International School for Advanced Studies).
59115911

59125912
Copyright (c) 2016-2023 SISSA (International School for Advanced Studies).
@@ -6338,4 +6338,4 @@ This formulation of W3C's notice and license became active on August 14 1998 so
63386338
--------------------------------------------------------------------------------
63396339
--------------------------------------------------------------------------------
63406340

6341-
Report Generated by FOSSA on 2025-9-10
6341+
Report Generated by FOSSA on 2025-10-24

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
[tool.poetry]
1818
name = "pytest-splunk-addon"
19-
version = "6.1.1"
19+
version = "6.2.0-beta.1"
2020
description = "A Dynamic test tool for Splunk Apps and Add-ons"
2121
authors = ["Splunk <[email protected]>"]
2222
license = "APACHE-2.0"

pytest_splunk_addon/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@
1818

1919
__author__ = """Splunk Inc."""
2020
__email__ = "[email protected]"
21-
__version__ = "6.1.1"
21+
__version__ = "6.2.0-beta.1"

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)