Skip to content

Commit 30d2088

Browse files
author
Fred Ross
committed
Merge pull request #54 from splunk/fross/restrictToHost
Add support for restrictToHost for TCP inputs
2 parents b26b3dc + 9c6c51a commit 30d2088

File tree

8 files changed

+232
-39
lines changed

8 files changed

+232
-39
lines changed

splunklib/client.py

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,9 @@ def _proper_namespace(self, owner=None, app=None, sharing=None):
797797
sharing = self._state.access.sharing
798798
return (owner, app, sharing)
799799

800+
def delete(self):
801+
owner, app, sharing = self._proper_namespace()
802+
return self.service.delete(self.path, owner=owner, app=app, sharing=sharing)
800803

801804
def get(self, path_segment="", owner=None, app=None, sharing=None, **query):
802805
owner, app, sharing = self._proper_namespace(owner, app, sharing)
@@ -1667,17 +1670,62 @@ def __init__(self, service, path, kind=None, **kwargs):
16671670
Entity.__init__(self, service, path, **kwargs)
16681671
if kind is None:
16691672
path_segments = path.split('/')
1670-
i = path_segments.index('inputs')
1671-
self.kind = '/'.join(path_segments[i+1:-1])
1672-
assert self.kind is not None
1673+
i = path_segments.index('inputs') + 1
1674+
if path_segments[i] == 'tcp':
1675+
self.kind = path_segments[i] + '/' + path_segments[i+1]
1676+
else:
1677+
self.kind = path_segments[i]
16731678
else:
16741679
self.kind = kind
16751680

1676-
# Maintain compatibility with older kind labels
1677-
if self.kind == 'tcp/raw':
1678-
self.kind = 'tcp'
1679-
if self.kind == 'tcp/cooked':
1680-
self.kind = 'splunktcp'
1681+
# Handle old input kind names.
1682+
if self.kind == 'tcp':
1683+
self.kind = 'tcp/raw'
1684+
if self.kind == 'splunktcp':
1685+
self.kind = 'tcp/cooked'
1686+
1687+
@property
1688+
def restrictToHost(self):
1689+
if 'restrictToHost' in self._state.content:
1690+
return self._state.content['restrictToHost']
1691+
else:
1692+
return ''
1693+
1694+
def update(self, **kwargs):
1695+
if self.kind not in ['tcp', 'splunktcp', 'tcp/raw', 'tcp/cooked']:
1696+
result = super(Input, self).update(**kwargs)
1697+
return result
1698+
else:
1699+
# TCP inputs have a property 'restrictToHost' which requires special care.
1700+
1701+
# There is a bug in Splunk < 5.0. Don't bother trying to update restrictToHost.
1702+
if 'restrictToHost' in kwargs and self.service.splunk_version < (5,):
1703+
raise IllegalOperationException("Updating restrictToHost has no effect before Splunk 5.0")
1704+
1705+
# In Splunk 4.x, if you update without restrictToHost as one of the fields,
1706+
# restrictToHost keeps its previous state. In 5.0, it is set to empty string.
1707+
# Thus, we must pass it every time. This doesn't actually introduce a race
1708+
# condition because if someone else has set restrictToHost to a new value on this
1709+
# TCP input, our update request will fail, since our reference to it still uses
1710+
# the old path.
1711+
to_update = kwargs.copy()
1712+
to_update['restrictToHost'] = kwargs.get('restrictToHost', self['restrictToHost'])
1713+
1714+
# Do the actual update operation.
1715+
result = super(Input, self).update(**to_update)
1716+
1717+
# Now we must update the path in case it changed.
1718+
# The pieces we break it into are:
1719+
# https://localhost:8089/services/data/inputs/tcp/raw/ boris: 10000
1720+
# |------------------ base_path -----------------------| | host || port|
1721+
assert self.path.endswith('/')
1722+
base_path = self.path.rsplit('/', 2)[0]
1723+
host = to_update['restrictToHost'] + ':' if to_update['restrictToHost'] != '' else ''
1724+
port = self.name.split(':', 1)[-1]
1725+
self.path = base_path + '/' + host + port
1726+
1727+
return result
1728+
16811729

16821730
# Inputs is a "kinded" collection, which is a heterogenous collection where
16831731
# each item is tagged with a kind, that provides a single merged view of all
@@ -1777,7 +1825,15 @@ def create(self, kind, name, **kwargs):
17771825
"""
17781826
kindpath = self.kindpath(kind)
17791827
self.post(kindpath, name=name, **kwargs)
1780-
path = _path(self.path + kindpath, name)
1828+
1829+
# If we created an input with restrictToHost set, then
1830+
# its path will be <restrictToHost>:<name>, not just <name>,
1831+
# and we have to adjust accordingly.
1832+
path = _path(
1833+
self.path + kindpath,
1834+
'%s:%s' % (kwargs['restrictToHost'], name) \
1835+
if kwargs.has_key('restrictToHost') else name
1836+
)
17811837
return Input(self.service, path, kind)
17821838

17831839
def delete(self, kind, name=None):
@@ -1807,7 +1863,8 @@ def _get_kind_list(self, subpath=[]):
18071863
if entry.title == 'all' or this_subpath == ['tcp','ssl']:
18081864
continue
18091865
elif 'create' in [x.rel for x in entry.link]:
1810-
kinds.append('/'.join(subpath + [entry.title]))
1866+
path = '/'.join(subpath + [entry.title])
1867+
kinds.append(path)
18111868
else:
18121869
subkinds = self._get_kind_list(subpath + [entry.title])
18131870
kinds.extend(subkinds)
@@ -1864,7 +1921,7 @@ def list(self, *kinds, **kwargs):
18641921
path = self.kindpath(kind)
18651922
logging.debug("Path for inputs: %s", path)
18661923
try:
1867-
response = self.service.get(path, **kwargs)
1924+
response = self.get(path, **kwargs)
18681925
except HTTPError, he:
18691926
if he.status == 404: # No inputs of this kind
18701927
return []

tests/test_app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def tearDown(self):
5050
if app_name.startswith('delete-me'):
5151
self.service.apps.delete(app_name)
5252
self.assertEventuallyTrue(lambda: app_name not in self.service.apps)
53-
self.clearRestartMessage()
53+
self.clear_restart_message()
5454

5555
def test_app_integrity(self):
5656
self.check_entity(self.app)
@@ -86,7 +86,7 @@ def test_delete(self):
8686
self.assertTrue(name in self.service.apps)
8787
self.service.apps.delete(name)
8888
self.assertFalse(name in self.service.apps)
89-
self.clearRestartMessage() # We don't actually have to restart here.
89+
self.clear_restart_message() # We don't actually have to restart here.
9090

9191
def test_package(self):
9292
p = self.app.package()

tests/test_index.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def test_submit_via_attached_socket(self):
108108
self.assertEventuallyTrue(lambda: self.totalEventCount() == eventCount+1, timeout=60)
109109

110110
def test_upload(self):
111-
self.installAppFromCollection("file_to_upload")
111+
self.install_app_from_collection("file_to_upload")
112112

113113
eventCount = int(self.index['totalEventCount'])
114114

tests/test_input.py

Lines changed: 140 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,128 @@
1919

2020
import splunklib.client as client
2121

22+
def highest_port(service, base_port, *kinds):
23+
"""Find the first port >= base_port not in use by any input in kinds."""
24+
highest_port = base_port
25+
for input in service.inputs.list(*kinds):
26+
port = int(input.name.split(':')[-1])
27+
highest_port = max(port, highest_port)
28+
return highest_port
29+
30+
class TestTcpInputNameHandling(testlib.SDKTestCase):
31+
def setUp(self):
32+
super(TestTcpInputNameHandling, self).setUp()
33+
self.base_port = highest_port(self.service, 10000, 'tcp', 'splunktcp') + 1
34+
35+
def tearDown(self):
36+
for input in self.service.inputs.list('tcp', 'splunktcp'):
37+
port = int(input.name.split(':')[-1])
38+
if port >= self.base_port:
39+
input.delete()
40+
super(TestTcpInputNameHandling, self).tearDown()
41+
42+
def test_create_tcp_port(self):
43+
for kind in ['tcp', 'splunktcp']:
44+
input = self.service.inputs.create(kind, str(self.base_port))
45+
self.check_entity(input)
46+
input.delete()
47+
48+
def test_cannot_create_with_restrictToHost_in_name(self):
49+
self.assertRaises(
50+
client.HTTPError,
51+
lambda: self.service.inputs.create('tcp', 'boris:10000')
52+
)
53+
54+
def test_remove_host_restriction(self):
55+
if self.service.splunk_version < (5,):
56+
# We can't set restrictToHost before 5.0 due to a bug in splunkd.
57+
return
58+
input = self.service.inputs.create('tcp', str(self.base_port), restrictToHost='boris')
59+
input.update(restrictToHost='')
60+
input.refresh()
61+
self.check_entity(input)
62+
input.delete()
63+
64+
def test_create_tcp_ports_with_restrictToHost(self):
65+
for kind in ['tcp', 'splunktcp']:
66+
# Make sure we can create two restricted inputs on the same port
67+
boris = self.service.inputs.create(kind, str(self.base_port), restrictToHost='boris')
68+
natasha = self.service.inputs.create(kind, str(self.base_port), restrictToHost='natasha')
69+
# And that they both function
70+
boris.refresh()
71+
natasha.refresh()
72+
self.check_entity(boris)
73+
self.check_entity(natasha)
74+
boris.delete()
75+
natasha.delete()
76+
77+
def test_restricted_to_unrestricted_collision(self):
78+
for kind in ['tcp', 'splunktcp']:
79+
restricted = self.service.inputs.create(kind, str(self.base_port), restrictToHost='boris')
80+
self.assertRaises(
81+
client.HTTPError,
82+
lambda: self.service.inputs.create(kind, str(self.base_port))
83+
)
84+
restricted.delete()
85+
86+
def test_unrestricted_to_restricted_collision(self):
87+
for kind in ['tcp', 'splunktcp']:
88+
unrestricted = self.service.inputs.create(kind, str(self.base_port))
89+
self.assertRaises(
90+
client.HTTPError,
91+
lambda: self.service.inputs.create(kind, str(self.base_port), restrictToHos='boris')
92+
)
93+
unrestricted.delete()
94+
95+
def test_update_restrictToHost(self):
96+
for kind in ['tcp', 'splunktcp']:
97+
port = self.base_port
98+
while True: # Find the next unbound port
99+
try:
100+
boris = self.service.inputs.create(kind, str(port), restrictToHost='boris')
101+
except client.HTTPError as he:
102+
if he.status == 400:
103+
port += 1
104+
else:
105+
break
106+
107+
# No matter what version we're actually running against,
108+
# we can check that on Splunk < 5.0, we get an exception
109+
# from trying to update restrictToHost.
110+
with self.fake_splunk_version((4,3)):
111+
self.assertRaises(
112+
client.IllegalOperationException,
113+
lambda: boris.update(restrictToHost='hilda')
114+
)
115+
116+
# And now back to real tests...
117+
if self.service.splunk_version >= (5,):
118+
boris.update(restrictToHost='hilda')
119+
boris.refresh()
120+
self.assertEqual('hilda:' + str(self.base_port), boris.name)
121+
boris.refresh()
122+
self.check_entity(boris)
123+
boris.delete()
124+
125+
def test_update_nonrestrictToHost(self):
126+
for kind in ['tcp', 'splunktcp']:
127+
port = self.base_port
128+
while True: # Find the next unbound port
129+
try:
130+
input = self.service.inputs.create(kind, str(port), restrictToHost='boris')
131+
except client.HTTPError as he:
132+
if he.status == 400:
133+
port += 1
134+
else:
135+
break
136+
try:
137+
input.update(host='meep')
138+
input.refresh()
139+
self.assertTrue(input.name.startswith('boris'))
140+
except:
141+
input.delete()
142+
raise
143+
22144
class TestRead(testlib.SDKTestCase):
23145
def test_read(self):
24146
inputs = self.service.inputs
@@ -59,7 +181,7 @@ def test_inputs_list_on_one_kind_with_search(self):
59181
self.assertEqual(expected, found)
60182

61183
def test_oneshot(self):
62-
self.installAppFromCollection('file_to_upload')
184+
self.install_app_from_collection('file_to_upload')
63185

64186
index_name = testlib.tmpname()
65187
index = self.service.indexes.create(index_name)
@@ -80,26 +202,23 @@ def test_oneshot_on_nonexistant_file(self):
80202
self.assertRaises(client.OperationFailedException,
81203
self.service.inputs.oneshot, name)
82204

83-
84205
class TestInput(testlib.SDKTestCase):
85206
def setUp(self):
86207
super(TestInput, self).setUp()
87208
inputs = self.service.inputs
88-
test_inputs = [{'kind': 'tcp', 'name': '9999', 'host': 'sdk-test'},
89-
{'kind': 'udp', 'name': '9999', 'host': 'sdk-test'}]
209+
unrestricted_port = str(highest_port(self.service, 10000, 'tcp', 'splunktcp', 'udp')+1)
210+
restricted_port = str(highest_port(self.service, int(unrestricted_port)+1, 'tcp', 'splunktcp')+1)
211+
test_inputs = [{'kind': 'tcp', 'name': unrestricted_port, 'host': 'sdk-test'},
212+
{'kind': 'udp', 'name': unrestricted_port, 'host': 'sdk-test'},
213+
{'kind': 'tcp', 'name': 'boris:' + restricted_port, 'host': 'sdk-test'}]
90214
self._test_entities = {}
91215

92-
base_port = 10000
93-
while True:
94-
if str(base_port) in inputs:
95-
base_port += 1
96-
else:
97-
break
98-
99216
self._test_entities['tcp'] = \
100-
inputs.create('tcp', str(base_port), host='sdk-test')
217+
inputs.create('tcp', unrestricted_port, host='sdk-test')
101218
self._test_entities['udp'] = \
102-
inputs.create('udp', str(base_port), host='sdk-test')
219+
inputs.create('udp', unrestricted_port, host='sdk-test')
220+
self._test_entities['restrictedTcp'] = \
221+
inputs.create('tcp', restricted_port, restrictToHost='boris')
103222

104223
def tearDown(self):
105224
super(TestInput, self).tearDown()
@@ -124,7 +243,7 @@ def test_lists_modular_inputs(self):
124243
else:
125244
# Install modular inputs to list, and restart
126245
# so they'll show up.
127-
self.installAppFromCollection("modular-inputs")
246+
self.install_app_from_collection("modular-inputs")
128247
self.uncheckedRestartSplunk()
129248

130249
inputs = self.service.inputs
@@ -159,11 +278,10 @@ def test_update(self):
159278
inputs = self.service.inputs
160279
for entity in self._test_entities.itervalues():
161280
kind, name = entity.kind, entity.name
162-
kwargs = {'host': 'foo', 'sourcetype': 'bar'}
281+
kwargs = {'host': 'foo'}
163282
entity.update(**kwargs)
164283
entity.refresh()
165284
self.assertEqual(entity.host, kwargs['host'])
166-
self.assertEqual(entity.sourcetype, kwargs['sourcetype'])
167285

168286
def test_delete(self):
169287
inputs = self.service.inputs
@@ -177,14 +295,17 @@ def test_delete(self):
177295
inputs.delete(name)
178296
self.assertFalse(name in inputs)
179297
else:
180-
self.assertRaises(client.AmbiguousReferenceException,
181-
inputs.delete, name)
298+
if not name.startswith('boris'):
299+
self.assertRaises(client.AmbiguousReferenceException,
300+
inputs.delete, name)
182301
self.service.inputs.delete(kind, name)
183302
self.assertFalse((kind,name) in inputs)
184303
self.assertRaises(client.EntityDeletedException,
185304
input_entity.refresh)
186305
remaining -= 1
187-
306+
307+
308+
188309

189310
if __name__ == "__main__":
190311
import unittest

tests/test_job.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def test_read_jobs(self):
114114
class TestJobWithDelayedDone(testlib.SDKTestCase):
115115
def setUp(self):
116116
super(TestJobWithDelayedDone, self).setUp()
117-
self.installAppFromCollection("sleep_command")
117+
self.install_app_from_collection("sleep_command")
118118
self.query = "search index=_internal | sleep done=100"
119119
self.job = self.service.jobs.create(
120120
query=self.query,

tests/test_modular_input_kinds.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
class ModularInputKindTestCase(testlib.SDKTestCase):
2121
def setUp(self):
2222
super(ModularInputKindTestCase, self).setUp()
23-
self.installAppFromCollection("modular-inputs")
23+
self.install_app_from_collection("modular-inputs")
2424
self.uncheckedRestartSplunk()
2525

2626
def test_list_arguments(self):

tests/test_service.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ def test_splunk_version(self):
101101
for p in v:
102102
self.assertTrue(isinstance(p, int) and p >= 0)
103103

104+
for version in [(4,3,3), (5,), (5,0,1)]:
105+
with self.fake_splunk_version(version):
106+
self.assertEqual(version, self.service.splunk_version)
107+
104108
class TestSettings(testlib.SDKTestCase):
105109
def test_read_settings(self):
106110
settings = self.service.settings

0 commit comments

Comments
 (0)