Skip to content

Commit 60aedb6

Browse files
author
David Noble
committed
Storage Passwords implementation + decorators.py bug fix
* Added Service.storage_passwords method and StoragePassword/s classes. Verified CRUD operations, but left one TODO: url encode {realm}:{name} making sure to encode slash and backslash characters. * Option.Item.__str__ now correctly calls the validation formatter, if there is one
1 parent fb27bca commit 60aedb6

File tree

3 files changed

+96
-10
lines changed

3 files changed

+96
-10
lines changed

splunklib/binding.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -650,8 +650,7 @@ def post(self, path_segment, owner=None, app=None, sharing=None, headers=None, *
650650
if headers is None:
651651
headers = []
652652

653-
path = self.authority + self._abspath(path_segment, owner=owner,
654-
app=app, sharing=sharing)
653+
path = self.authority + self._abspath(path_segment, owner=owner, app=app, sharing=sharing)
655654
logging.debug("POST request to %s (body: %s)", path, repr(query))
656655
all_headers = headers + self._auth_headers
657656
response = self.http.post(path, all_headers, **query)

splunklib/client.py

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,44 +102,53 @@
102102
PATH_USERS = "authentication/users/"
103103
PATH_RECEIVERS_STREAM = "receivers/stream"
104104
PATH_RECEIVERS_SIMPLE = "receivers/simple"
105+
PATH_STORAGE_PASSWORDS = "storage/passwords"
105106

106107
XNAMEF_ATOM = "{http://www.w3.org/2005/Atom}%s"
107108
XNAME_ENTRY = XNAMEF_ATOM % "entry"
108109
XNAME_CONTENT = XNAMEF_ATOM % "content"
109110

110111
MATCH_ENTRY_CONTENT = "%s/%s/*" % (XNAME_ENTRY, XNAME_CONTENT)
111112

113+
112114
class IllegalOperationException(Exception):
113115
"""Thrown when an operation is not possible on the Splunk instance that a
114116
:class:`Service` object is connected to."""
115117
pass
116118

119+
117120
class IncomparableException(Exception):
118121
"""Thrown when trying to compare objects (using ``==``, ``<``, ``>``, and
119122
so on) of a type that doesn't support it."""
120123
pass
121124

125+
122126
class AmbiguousReferenceException(ValueError):
123127
"""Thrown when the name used to fetch an entity matches more than one entity."""
124128
pass
125129

130+
126131
class InvalidNameException(Exception):
127132
"""Thrown when the specified name contains characters that are not allowed
128133
in Splunk entity names."""
129134
pass
130135

136+
131137
class NoSuchCapability(Exception):
132138
"""Thrown when the capability that has been referred to doesn't exist."""
133139
pass
134140

141+
135142
class OperationError(Exception):
136143
"""Raised for a failed operation, such as a time out."""
137144
pass
138145

146+
139147
class NotSupportedError(Exception):
140148
"""Raised for operations that are not supported on a given object."""
141149
pass
142150

151+
143152
def _trailing(template, *targets):
144153
"""Substring of *template* following all *targets*.
145154
@@ -168,6 +177,7 @@ def _trailing(template, *targets):
168177
s = s[n + len(t):]
169178
return s
170179

180+
171181
# Filter the given state content record according to the given arg list.
172182
def _filter_content(content, *args):
173183
if len(args) > 0:
@@ -180,10 +190,12 @@ def _path(base, name):
180190
if not base.endswith('/'): base = base + '/'
181191
return base + name
182192

193+
183194
# Load an atom record from the body of the given response
184195
def _load_atom(response, match=None):
185196
return data.load(response.body.read(), match)
186197

198+
187199
# Load an array of atom entries from the body of the given response
188200
def _load_atom_entries(response):
189201
r = _load_atom(response)
@@ -203,10 +215,12 @@ def _load_atom_entries(response):
203215
if entries is None: return None
204216
return entries if isinstance(entries, list) else [entries]
205217

218+
206219
# Load the sid from the body of the given response
207220
def _load_sid(response):
208221
return _load_atom(response).response.sid
209222

223+
210224
# Parse the given atom entry record into a generic entity state record
211225
def _parse_atom_entry(entry):
212226
title = entry.get('title', None)
@@ -233,6 +247,7 @@ def _parse_atom_entry(entry):
233247
'content': content
234248
})
235249

250+
236251
# Parse the metadata fields out of the given atom entry content record
237252
def _parse_atom_metadata(content):
238253
# Hoist access metadata
@@ -247,6 +262,7 @@ def _parse_atom_metadata(content):
247262

248263
return record({'access': access, 'fields': fields})
249264

265+
250266
# kwargs: scheme, host, port, app, owner, username, password
251267
def connect(**kwargs):
252268
"""This function connects and logs in to a Splunk instance.
@@ -455,6 +471,14 @@ def modular_input_kinds(self):
455471
else:
456472
raise IllegalOperationException("Modular inputs are not supported before Splunk version 5.")
457473

474+
@property
475+
def storage_passwords(self):
476+
"""Returns the collection of the modular input kinds on this Splunk instance.
477+
478+
:return: A :class:`ReadOnlyCollection` of :class:`ModularInputKind` entities.
479+
"""
480+
return StoragePasswords(self)
481+
458482
# kwargs: enable_lookups, reload_macros, parse_only, output_mode
459483
def parse(self, query, **kwargs):
460484
"""Parses a search query and returns a semantic map of the search.
@@ -739,11 +763,8 @@ def post(self, path_segment="", owner=None, app=None, sharing=None, **query):
739763
if path_segment.startswith('/'):
740764
path = path_segment
741765
else:
742-
path = self.service._abspath(self.path + path_segment, owner=owner,
743-
app=app, sharing=sharing)
744-
return self.service.post(path,
745-
owner=owner, app=app, sharing=sharing,
746-
**query)
766+
path = self.service._abspath(self.path + path_segment, owner=owner, app=app, sharing=sharing)
767+
return self.service.post(path, owner=owner, app=app, sharing=sharing, **query)
747768

748769

749770
# kwargs: path, app, owner, sharing, state
@@ -822,7 +843,8 @@ def __init__(self, service, path, **kwargs):
822843
Endpoint.__init__(self, service, path)
823844
self._state = None
824845
if not kwargs.get('skip_refresh', False):
825-
self.refresh(kwargs.get('state', None)) # "Prefresh"
846+
self.refresh(kwargs.get('state', None)) # "Prefresh"
847+
return
826848

827849
def __contains__(self, item):
828850
try:
@@ -1473,7 +1495,7 @@ def create(self, name, **params):
14731495
new_app = applications.create("my_fake_app")
14741496
"""
14751497
if not isinstance(name, basestring):
1476-
raise InvalidNameException("%s is not a valid name for an entity." % name)
1498+
raise InvalidNameException("%s is not a valid name for an entity." % name)
14771499
if 'namespace' in params:
14781500
namespace = params.pop('namespace')
14791501
params['owner'] = namespace.owner
@@ -1648,6 +1670,70 @@ def __len__(self):
16481670
if not x.startswith('eai') and x != 'disabled'])
16491671

16501672

1673+
class StoragePassword(Entity):
1674+
"""This class contains a storage password.
1675+
1676+
"""
1677+
def __init__(self, service, path, **kwargs):
1678+
state = kwargs.get('state', None)
1679+
kwargs['skip_refresh'] = kwargs.get('skip_refresh', state is not None)
1680+
super(StoragePassword, self).__init__(service, path, **kwargs)
1681+
self._state = state
1682+
1683+
@property
1684+
def clear_password(self):
1685+
return self.content.get('clear_password')
1686+
1687+
@property
1688+
def encrypted_password(self):
1689+
return self.content.get('encr_password')
1690+
1691+
@property
1692+
def realm(self):
1693+
return self.content.get('realm')
1694+
1695+
@property
1696+
def username(self):
1697+
return self.content.get('username')
1698+
1699+
1700+
class StoragePasswords(Collection):
1701+
"""This class provides access to the storage passwords from this Splunk
1702+
instance. Retrieve this collection using :meth:`Service.storage_passwords`.
1703+
1704+
"""
1705+
def __init__(self, service):
1706+
if service.namespace.owner == '-' or service.namespace.app == '-':
1707+
raise ValueError("StoragePasswords cannot have wildcards in namespace.")
1708+
super(StoragePasswords, self).__init__(service, PATH_STORAGE_PASSWORDS, item=StoragePassword)
1709+
1710+
def create(self, name, password):
1711+
""" Creates or edits a storage password by *name*.
1712+
1713+
:param name: A name of the form "username" or "realm:username".
1714+
:type name: ``string``
1715+
1716+
:return: The :class:`StoragePassword` object created.
1717+
1718+
"""
1719+
if not isinstance(name, basestring):
1720+
raise ValueError('Invalid name: %s' % repr(name))
1721+
1722+
identity = name.split(':', 1)
1723+
realm, name = identity if len(identity) == 2 else ('', identity)
1724+
1725+
response = self.post(password=password, realm=realm, name=name)
1726+
1727+
if response.status != 201:
1728+
raise ValueError("Unexpected status code %s returned from creating a stanza" % response.status)
1729+
1730+
entries = _load_atom_entries(response)
1731+
state = _parse_atom_entry(entries[0])
1732+
storage_password = StoragePassword(self.service, self._entity_path(state), state=state, skip_refresh=True)
1733+
1734+
return storage_password
1735+
1736+
16511737
class AlertGroup(Entity):
16521738
"""This class represents a group of fired alerts for a saved search. Access
16531739
it using the :meth:`alerts` property."""

splunklib/searchcommands/decorators.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,9 @@ def __repr__(self):
222222
return str(self)
223223

224224
def __str__(self):
225+
value = self.validator.format(self.value) if self.validator is not None else str(self.value)
225226
encoder = Option.Encoder(self)
226-
text = '='.join([self.name, encoder.encode(self.value)])
227+
text = '='.join([self.name, encoder.encode(value)])
227228
return text
228229

229230
#region Properties

0 commit comments

Comments
 (0)