Skip to content

Commit 486ad19

Browse files
author
Sam Stelle
committed
Merge remote-tracking branch 'origin/feature/storage_passwords' into develop
2 parents 4c2ae3b + f611372 commit 486ad19

File tree

5 files changed

+361
-10
lines changed

5 files changed

+361
-10
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ examples/*/local
1717
examples/*/metadata
1818
tests/searchcommands_data/log/
1919
tests/searchcommands_data/output/
20+
examples/searchcommands_app/searchcommand_app.log
21+
Test Results*.html

splunklib/binding.py

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

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

splunklib/client.py

Lines changed: 126 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.
@@ -736,11 +760,8 @@ def post(self, path_segment="", owner=None, app=None, sharing=None, **query):
736760
if path_segment.startswith('/'):
737761
path = path_segment
738762
else:
739-
path = self.service._abspath(self.path + path_segment, owner=owner,
740-
app=app, sharing=sharing)
741-
return self.service.post(path,
742-
owner=owner, app=app, sharing=sharing,
743-
**query)
763+
path = self.service._abspath(self.path + path_segment, owner=owner, app=app, sharing=sharing)
764+
return self.service.post(path, owner=owner, app=app, sharing=sharing, **query)
744765

745766

746767
# kwargs: path, app, owner, sharing, state
@@ -819,7 +840,8 @@ def __init__(self, service, path, **kwargs):
819840
Endpoint.__init__(self, service, path)
820841
self._state = None
821842
if not kwargs.get('skip_refresh', False):
822-
self.refresh(kwargs.get('state', None)) # "Prefresh"
843+
self.refresh(kwargs.get('state', None)) # "Prefresh"
844+
return
823845

824846
def __contains__(self, item):
825847
try:
@@ -1273,6 +1295,7 @@ def _load_list(self, response):
12731295
self._entity_path(state),
12741296
state=state)
12751297
entities.append(entity)
1298+
12761299
return entities
12771300

12781301
def itemmeta(self):
@@ -1475,7 +1498,7 @@ def create(self, name, **params):
14751498
new_app = applications.create("my_fake_app")
14761499
"""
14771500
if not isinstance(name, basestring):
1478-
raise InvalidNameException("%s is not a valid name for an entity." % name)
1501+
raise InvalidNameException("%s is not a valid name for an entity." % name)
14791502
if 'namespace' in params:
14801503
namespace = params.pop('namespace')
14811504
params['owner'] = namespace.owner
@@ -1701,6 +1724,102 @@ def __len__(self):
17011724
if not x.startswith('eai') and x != 'disabled'])
17021725

17031726

1727+
class StoragePassword(Entity):
1728+
"""This class contains a storage password.
1729+
"""
1730+
def __init__(self, service, path, **kwargs):
1731+
state = kwargs.get('state', None)
1732+
kwargs['skip_refresh'] = kwargs.get('skip_refresh', state is not None)
1733+
super(StoragePassword, self).__init__(service, path, **kwargs)
1734+
self._state = state
1735+
1736+
@property
1737+
def clear_password(self):
1738+
return self.content.get('clear_password')
1739+
1740+
@property
1741+
def encrypted_password(self):
1742+
return self.content.get('encr_password')
1743+
1744+
@property
1745+
def realm(self):
1746+
return self.content.get('realm')
1747+
1748+
@property
1749+
def username(self):
1750+
return self.content.get('username')
1751+
1752+
1753+
class StoragePasswords(Collection):
1754+
"""This class provides access to the storage passwords from this Splunk
1755+
instance. Retrieve this collection using :meth:`Service.storage_passwords`.
1756+
"""
1757+
def __init__(self, service):
1758+
if service.namespace.owner == '-' or service.namespace.app == '-':
1759+
raise ValueError("StoragePasswords cannot have wildcards in namespace.")
1760+
super(StoragePasswords, self).__init__(service, PATH_STORAGE_PASSWORDS, item=StoragePassword)
1761+
1762+
def create(self, password, username, realm=None):
1763+
""" Creates a storage password.
1764+
1765+
A `StoragePassword` can be identified by <username>, or by <realm>:<username> if the
1766+
optional realm parameter is also provided.
1767+
1768+
:param password: The password for the credentials - this is the only part of the credentials that will be stored securely.
1769+
:type name: ``string``
1770+
:param username: The username for the credentials.
1771+
:type name: ``string``
1772+
:param realm: The credential realm. (optional)
1773+
:type name: ``string``
1774+
1775+
:return: The :class:`StoragePassword` object created.
1776+
"""
1777+
if not isinstance(username, basestring):
1778+
raise ValueError("Invalid name: %s" % repr(username))
1779+
1780+
if realm is None:
1781+
response = self.post(password=password, name=username)
1782+
else:
1783+
response = self.post(password=password, realm=realm, name=username)
1784+
1785+
if response.status != 201:
1786+
raise ValueError("Unexpected status code %s returned from creating a stanza" % response.status)
1787+
1788+
entries = _load_atom_entries(response)
1789+
state = _parse_atom_entry(entries[0])
1790+
storage_password = StoragePassword(self.service, self._entity_path(state), state=state, skip_refresh=True)
1791+
1792+
return storage_password
1793+
1794+
def delete(self, username, realm=None):
1795+
"""Delete a storage password by username and/or realm.
1796+
1797+
The identifier can be passed in through the username parameter as
1798+
<username> or <realm>:<username>, but the preferred way is by
1799+
passing in the username and realm parameters.
1800+
1801+
:param username: The username for the credentials, or <realm>:<username> if the realm parameter is omitted.
1802+
:type name: ``string``
1803+
:param realm: The credential realm. (optional)
1804+
:type name: ``string``
1805+
:return: The `StoragePassword` collection.
1806+
:rtype: ``self``
1807+
"""
1808+
if realm is None:
1809+
# This case makes the username optional, so
1810+
# the full name can be passed in as realm.
1811+
# Assume it's already encoded.
1812+
name = username
1813+
else:
1814+
# Encode each component separately
1815+
name = UrlEncoded(realm, encode_slash=True) + ":" + UrlEncoded(username, encode_slash=True)
1816+
1817+
# Append the : expected at the end of the name
1818+
if name[-1] is not ":":
1819+
name = name + ":"
1820+
return Collection.delete(self, name)
1821+
1822+
17041823
class AlertGroup(Entity):
17051824
"""This class represents a group of fired alerts for a saved search. Access
17061825
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)