splunkctl 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- splunkctl/__init__.py +3 -0
- splunkctl/__main__.py +5 -0
- splunkctl/client.py +103 -0
- splunkctl/commands/__init__.py +0 -0
- splunkctl/commands/alerts.py +94 -0
- splunkctl/commands/apps.py +189 -0
- splunkctl/commands/commands_meta.py +72 -0
- splunkctl/commands/config_cmd.py +87 -0
- splunkctl/commands/dashboards.py +216 -0
- splunkctl/commands/indexes.py +199 -0
- splunkctl/commands/info.py +29 -0
- splunkctl/commands/inputs.py +204 -0
- splunkctl/commands/lookups.py +217 -0
- splunkctl/commands/parsers.py +177 -0
- splunkctl/commands/rules.py +266 -0
- splunkctl/commands/search.py +270 -0
- splunkctl/commands/skill_cmd.py +43 -0
- splunkctl/commands/users.py +211 -0
- splunkctl/config.py +82 -0
- splunkctl/guard.py +22 -0
- splunkctl/main.py +149 -0
- splunkctl/output.py +75 -0
- splunkctl/skill/SKILL.md +325 -0
- splunkctl-0.1.0.dist-info/METADATA +118 -0
- splunkctl-0.1.0.dist-info/RECORD +29 -0
- splunkctl-0.1.0.dist-info/WHEEL +5 -0
- splunkctl-0.1.0.dist-info/entry_points.txt +2 -0
- splunkctl-0.1.0.dist-info/licenses/LICENSE +190 -0
- splunkctl-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Dashboard CRUD — raw REST (SDK gap)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import urllib.parse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from splunkctl import guard, output
|
|
12
|
+
from splunkctl.client import get_client
|
|
13
|
+
|
|
14
|
+
_READ_BASE = "/servicesNS/-/{app}/data/ui/views"
|
|
15
|
+
_WRITE_BASE = "/servicesNS/nobody/{app}/data/ui/views"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _read_path(app: str, name: str | None = None) -> str:
|
|
19
|
+
base = _READ_BASE.format(app=quote(app, safe=""))
|
|
20
|
+
return f"{base}/{quote(name, safe='')}" if name else base
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _write_path(app: str, name: str | None = None) -> str:
|
|
24
|
+
base = _WRITE_BASE.format(app=quote(app, safe=""))
|
|
25
|
+
return f"{base}/{quote(name, safe='')}" if name else base
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _rest_get(service: Any, path: str) -> dict[str, Any]:
|
|
29
|
+
"""GET JSON from Splunk REST API."""
|
|
30
|
+
resp = service.get(path, output_mode="json", count=0)
|
|
31
|
+
body: dict[str, Any] = json.loads(resp.body.read())
|
|
32
|
+
return body
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@click.group("dashboards")
|
|
36
|
+
def dashboards_group() -> None:
|
|
37
|
+
"""Dashboard management (raw REST)."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dashboards_group.command("list")
|
|
41
|
+
@click.option("--app", default="-", help="Splunk app context.")
|
|
42
|
+
@click.pass_context
|
|
43
|
+
def list_dashboards(ctx: click.Context, *, app: str) -> None:
|
|
44
|
+
"""List dashboards."""
|
|
45
|
+
client = get_client(ctx)
|
|
46
|
+
body = _rest_get(client.service, _read_path(app))
|
|
47
|
+
rows: list[dict[str, Any]] = [
|
|
48
|
+
{
|
|
49
|
+
"name": e["name"],
|
|
50
|
+
"app": e.get("acl", {}).get("app", ""),
|
|
51
|
+
"label": e.get("content", {}).get("label", ""),
|
|
52
|
+
"isDashboard": e.get("content", {}).get("isDashboard", False),
|
|
53
|
+
"isVisible": e.get("content", {}).get("isVisible", False),
|
|
54
|
+
}
|
|
55
|
+
for e in body.get("entry", [])
|
|
56
|
+
]
|
|
57
|
+
output.render(ctx, rows)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dashboards_group.command("get")
|
|
61
|
+
@click.argument("name")
|
|
62
|
+
@click.option("--app", default="-", help="Splunk app context.")
|
|
63
|
+
@click.pass_context
|
|
64
|
+
def get_dashboard(ctx: click.Context, name: str, *, app: str) -> None:
|
|
65
|
+
"""Get dashboard details including XML source."""
|
|
66
|
+
client = get_client(ctx)
|
|
67
|
+
try:
|
|
68
|
+
body = _rest_get(client.service, _read_path(app, name))
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
output.error(f"Dashboard '{name}' not found: {exc}")
|
|
71
|
+
ctx.exit(1)
|
|
72
|
+
return
|
|
73
|
+
entries: list[dict[str, Any]] = body.get("entry", [])
|
|
74
|
+
if not entries:
|
|
75
|
+
output.error(f"Dashboard '{name}' not found.")
|
|
76
|
+
ctx.exit(1)
|
|
77
|
+
return
|
|
78
|
+
e = entries[0]
|
|
79
|
+
row: dict[str, Any] = {
|
|
80
|
+
"name": e["name"],
|
|
81
|
+
"app": e.get("acl", {}).get("app", ""),
|
|
82
|
+
"label": e.get("content", {}).get("label", ""),
|
|
83
|
+
"isDashboard": e.get("content", {}).get("isDashboard", False),
|
|
84
|
+
"isVisible": e.get("content", {}).get("isVisible", False),
|
|
85
|
+
"eai:data": e.get("content", {}).get("eai:data", ""),
|
|
86
|
+
}
|
|
87
|
+
output.render(ctx, row)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dashboards_group.command("create")
|
|
91
|
+
@click.option("--name", required=True, help="Dashboard name.")
|
|
92
|
+
@click.option(
|
|
93
|
+
"--file",
|
|
94
|
+
"filepath",
|
|
95
|
+
required=True,
|
|
96
|
+
type=click.Path(exists=True),
|
|
97
|
+
help="XML file path.",
|
|
98
|
+
)
|
|
99
|
+
@click.option("--app", default="search", help="Splunk app context.")
|
|
100
|
+
@click.option("--label", default=None, help="Dashboard label.")
|
|
101
|
+
@click.pass_context
|
|
102
|
+
def create_dashboard(
|
|
103
|
+
ctx: click.Context,
|
|
104
|
+
name: str,
|
|
105
|
+
filepath: str,
|
|
106
|
+
*,
|
|
107
|
+
app: str,
|
|
108
|
+
label: str | None,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Create a dashboard from XML file."""
|
|
111
|
+
xml_content = Path(filepath).read_text(encoding="utf-8")
|
|
112
|
+
details = f" name: {name}\n app: {app}\n file: {filepath}"
|
|
113
|
+
if label:
|
|
114
|
+
details += f"\n label: {label}"
|
|
115
|
+
if not guard.check(ctx, f"Create dashboard '{name}'", details=details):
|
|
116
|
+
return
|
|
117
|
+
client = get_client(ctx)
|
|
118
|
+
params: dict[str, str] = {"name": name, "eai:data": xml_content}
|
|
119
|
+
if label:
|
|
120
|
+
params["label"] = label
|
|
121
|
+
try:
|
|
122
|
+
client.service.post(_write_path(app), body=urllib.parse.urlencode(params))
|
|
123
|
+
except Exception as exc:
|
|
124
|
+
output.error(f"Create failed: {exc}")
|
|
125
|
+
ctx.exit(1)
|
|
126
|
+
return
|
|
127
|
+
output.info(f"Dashboard '{name}' created in app '{app}'.")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dashboards_group.command("update")
|
|
131
|
+
@click.argument("name")
|
|
132
|
+
@click.option(
|
|
133
|
+
"--file",
|
|
134
|
+
"filepath",
|
|
135
|
+
required=True,
|
|
136
|
+
type=click.Path(exists=True),
|
|
137
|
+
help="XML file path.",
|
|
138
|
+
)
|
|
139
|
+
@click.option("--app", default="search", help="Splunk app context.")
|
|
140
|
+
@click.pass_context
|
|
141
|
+
def update_dashboard(
|
|
142
|
+
ctx: click.Context,
|
|
143
|
+
name: str,
|
|
144
|
+
filepath: str,
|
|
145
|
+
*,
|
|
146
|
+
app: str,
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Update dashboard XML."""
|
|
149
|
+
xml_content = Path(filepath).read_text(encoding="utf-8")
|
|
150
|
+
details = f" name: {name}\n app: {app}\n file: {filepath}"
|
|
151
|
+
if not guard.check(ctx, f"Update dashboard '{name}'", details=details):
|
|
152
|
+
return
|
|
153
|
+
client = get_client(ctx)
|
|
154
|
+
try:
|
|
155
|
+
client.service.post(
|
|
156
|
+
_write_path(app, name),
|
|
157
|
+
body=urllib.parse.urlencode({"eai:data": xml_content}),
|
|
158
|
+
)
|
|
159
|
+
except Exception as exc:
|
|
160
|
+
output.error(f"Update failed: {exc}")
|
|
161
|
+
ctx.exit(1)
|
|
162
|
+
return
|
|
163
|
+
output.info(f"Dashboard '{name}' updated.")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dashboards_group.command("delete")
|
|
167
|
+
@click.argument("name")
|
|
168
|
+
@click.option("--app", default="search", help="Splunk app context.")
|
|
169
|
+
@click.pass_context
|
|
170
|
+
def delete_dashboard(ctx: click.Context, name: str, *, app: str) -> None:
|
|
171
|
+
"""Delete a dashboard."""
|
|
172
|
+
if not guard.check(ctx, f"Delete dashboard '{name}'", details=f" app: {app}"):
|
|
173
|
+
return
|
|
174
|
+
client = get_client(ctx)
|
|
175
|
+
try:
|
|
176
|
+
client.service.delete(_write_path(app, name))
|
|
177
|
+
except Exception as exc:
|
|
178
|
+
output.error(f"Delete failed: {exc}")
|
|
179
|
+
ctx.exit(1)
|
|
180
|
+
return
|
|
181
|
+
output.info(f"Dashboard '{name}' deleted.")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dashboards_group.command("export")
|
|
185
|
+
@click.argument("name")
|
|
186
|
+
@click.option("--app", default="-", help="Splunk app context.")
|
|
187
|
+
@click.option(
|
|
188
|
+
"--out",
|
|
189
|
+
"out_file",
|
|
190
|
+
type=click.Path(),
|
|
191
|
+
default=None,
|
|
192
|
+
help="Output file.",
|
|
193
|
+
)
|
|
194
|
+
@click.pass_context
|
|
195
|
+
def export_dashboard(
|
|
196
|
+
ctx: click.Context, name: str, *, app: str, out_file: str | None
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Export dashboard XML to file or stdout."""
|
|
199
|
+
client = get_client(ctx)
|
|
200
|
+
try:
|
|
201
|
+
body = _rest_get(client.service, _read_path(app, name))
|
|
202
|
+
except Exception as exc:
|
|
203
|
+
output.error(f"Dashboard '{name}' not found: {exc}")
|
|
204
|
+
ctx.exit(1)
|
|
205
|
+
return
|
|
206
|
+
entries: list[dict[str, Any]] = body.get("entry", [])
|
|
207
|
+
if not entries:
|
|
208
|
+
output.error(f"Dashboard '{name}' not found.")
|
|
209
|
+
ctx.exit(1)
|
|
210
|
+
return
|
|
211
|
+
xml: str = entries[0].get("content", {}).get("eai:data", "")
|
|
212
|
+
if out_file:
|
|
213
|
+
Path(out_file).write_text(xml, encoding="utf-8")
|
|
214
|
+
output.info(f"Exported to {out_file}")
|
|
215
|
+
else:
|
|
216
|
+
click.echo(xml)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Index management — list, get, create, update, delete, clean, reload."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from splunkctl import guard, output
|
|
8
|
+
from splunkctl.client import get_client
|
|
9
|
+
|
|
10
|
+
_LIST_FIELDS = (
|
|
11
|
+
"name",
|
|
12
|
+
"datatype",
|
|
13
|
+
"totalEventCount",
|
|
14
|
+
"currentDBSizeMB",
|
|
15
|
+
"maxDataSizeMB",
|
|
16
|
+
"frozenTimePeriodInSecs",
|
|
17
|
+
"disabled",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _index_row(idx: Any, fields: tuple[str, ...] = _LIST_FIELDS) -> dict[str, Any]:
|
|
22
|
+
content: dict[str, Any] = dict(idx.content)
|
|
23
|
+
row: dict[str, Any] = {"name": idx.name}
|
|
24
|
+
for f in fields:
|
|
25
|
+
if f != "name":
|
|
26
|
+
row[f] = content.get(f, "")
|
|
27
|
+
return row
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@click.group("indexes")
|
|
31
|
+
def indexes_group() -> None:
|
|
32
|
+
"""Manage Splunk indexes."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@indexes_group.command("list")
|
|
36
|
+
@click.pass_context
|
|
37
|
+
def list_indexes(ctx: click.Context) -> None:
|
|
38
|
+
"""List all indexes."""
|
|
39
|
+
client = get_client(ctx)
|
|
40
|
+
rows = [_index_row(idx) for idx in client.service.indexes.list()]
|
|
41
|
+
output.render(ctx, rows)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@indexes_group.command("get")
|
|
45
|
+
@click.argument("name")
|
|
46
|
+
@click.pass_context
|
|
47
|
+
def get_index(ctx: click.Context, name: str) -> None:
|
|
48
|
+
"""Get full index details."""
|
|
49
|
+
client = get_client(ctx)
|
|
50
|
+
try:
|
|
51
|
+
idx = client.service.indexes[name]
|
|
52
|
+
except KeyError:
|
|
53
|
+
output.error(f"Index '{name}' not found.")
|
|
54
|
+
ctx.exit(1)
|
|
55
|
+
return
|
|
56
|
+
row: dict[str, Any] = {"name": idx.name, **dict(idx.content)}
|
|
57
|
+
output.render(ctx, row)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@indexes_group.command("create")
|
|
61
|
+
@click.option("--name", required=True, help="Index name.")
|
|
62
|
+
@click.option(
|
|
63
|
+
"--datatype",
|
|
64
|
+
type=click.Choice(["event", "metric"]),
|
|
65
|
+
default=None,
|
|
66
|
+
help="Index datatype.",
|
|
67
|
+
)
|
|
68
|
+
@click.option("--max-size", type=int, default=None, help="Max data size in MB.")
|
|
69
|
+
@click.option(
|
|
70
|
+
"--frozen-period", type=int, default=None, help="Frozen time period in seconds."
|
|
71
|
+
)
|
|
72
|
+
@click.option("--home-path", default=None, help="Home path for hot/warm buckets.")
|
|
73
|
+
@click.option("--cold-path", default=None, help="Cold path for cold buckets.")
|
|
74
|
+
@click.pass_context
|
|
75
|
+
def create_index(
|
|
76
|
+
ctx: click.Context,
|
|
77
|
+
name: str,
|
|
78
|
+
datatype: str | None,
|
|
79
|
+
max_size: int | None,
|
|
80
|
+
frozen_period: int | None,
|
|
81
|
+
home_path: str | None,
|
|
82
|
+
cold_path: str | None,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Create a new index."""
|
|
85
|
+
kwargs: dict[str, Any] = {}
|
|
86
|
+
if datatype is not None:
|
|
87
|
+
kwargs["datatype"] = datatype
|
|
88
|
+
if max_size is not None:
|
|
89
|
+
kwargs["maxDataSizeMB"] = max_size
|
|
90
|
+
if frozen_period is not None:
|
|
91
|
+
kwargs["frozenTimePeriodInSecs"] = frozen_period
|
|
92
|
+
if home_path is not None:
|
|
93
|
+
kwargs["homePath"] = home_path
|
|
94
|
+
if cold_path is not None:
|
|
95
|
+
kwargs["coldPath"] = cold_path
|
|
96
|
+
|
|
97
|
+
details = f" name={name}"
|
|
98
|
+
for k, v in kwargs.items():
|
|
99
|
+
details += f"\n {k}={v}"
|
|
100
|
+
|
|
101
|
+
if not guard.check(ctx, f"Create index '{name}'", details=details):
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
client = get_client(ctx)
|
|
105
|
+
client.service.indexes.create(name, **kwargs)
|
|
106
|
+
output.info(f"Index '{name}' created.")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@indexes_group.command("update")
|
|
110
|
+
@click.argument("name")
|
|
111
|
+
@click.option("--max-size", type=int, default=None, help="Max data size in MB.")
|
|
112
|
+
@click.option(
|
|
113
|
+
"--frozen-period", type=int, default=None, help="Frozen time period in seconds."
|
|
114
|
+
)
|
|
115
|
+
@click.pass_context
|
|
116
|
+
def update_index(
|
|
117
|
+
ctx: click.Context,
|
|
118
|
+
name: str,
|
|
119
|
+
max_size: int | None,
|
|
120
|
+
frozen_period: int | None,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Update index settings."""
|
|
123
|
+
kwargs: dict[str, Any] = {}
|
|
124
|
+
if max_size is not None:
|
|
125
|
+
kwargs["maxDataSizeMB"] = max_size
|
|
126
|
+
if frozen_period is not None:
|
|
127
|
+
kwargs["frozenTimePeriodInSecs"] = frozen_period
|
|
128
|
+
|
|
129
|
+
if not kwargs:
|
|
130
|
+
output.error("No settings to update. Provide --max-size or --frozen-period.")
|
|
131
|
+
ctx.exit(1)
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
details = f" index={name}"
|
|
135
|
+
for k, v in kwargs.items():
|
|
136
|
+
details += f"\n {k}={v}"
|
|
137
|
+
|
|
138
|
+
if not guard.check(ctx, f"Update index '{name}'", details=details):
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
client = get_client(ctx)
|
|
142
|
+
try:
|
|
143
|
+
idx = client.service.indexes[name]
|
|
144
|
+
except KeyError:
|
|
145
|
+
output.error(f"Index '{name}' not found.")
|
|
146
|
+
ctx.exit(1)
|
|
147
|
+
return
|
|
148
|
+
idx.update(**kwargs).refresh()
|
|
149
|
+
output.info(f"Index '{name}' updated.")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@indexes_group.command("delete")
|
|
153
|
+
@click.argument("name")
|
|
154
|
+
@click.pass_context
|
|
155
|
+
def delete_index(ctx: click.Context, name: str) -> None:
|
|
156
|
+
"""Delete an index."""
|
|
157
|
+
if not guard.check(ctx, f"Delete index '{name}'"):
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
client = get_client(ctx)
|
|
161
|
+
try:
|
|
162
|
+
idx = client.service.indexes[name]
|
|
163
|
+
except KeyError:
|
|
164
|
+
output.error(f"Index '{name}' not found.")
|
|
165
|
+
ctx.exit(1)
|
|
166
|
+
return
|
|
167
|
+
idx.delete()
|
|
168
|
+
output.info(f"Index '{name}' deleted.")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@indexes_group.command("clean")
|
|
172
|
+
@click.argument("name")
|
|
173
|
+
@click.pass_context
|
|
174
|
+
def clean_index(ctx: click.Context, name: str) -> None:
|
|
175
|
+
"""Remove all events from an index."""
|
|
176
|
+
if not guard.check(ctx, f"Clean index '{name}' (remove all events)"):
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
client = get_client(ctx)
|
|
180
|
+
try:
|
|
181
|
+
idx = client.service.indexes[name]
|
|
182
|
+
except KeyError:
|
|
183
|
+
output.error(f"Index '{name}' not found.")
|
|
184
|
+
ctx.exit(1)
|
|
185
|
+
return
|
|
186
|
+
idx.clean(timeout=60)
|
|
187
|
+
output.info(f"Index '{name}' cleaned.")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@indexes_group.command("reload")
|
|
191
|
+
@click.pass_context
|
|
192
|
+
def reload_indexes(ctx: click.Context) -> None:
|
|
193
|
+
"""Reload all index configurations."""
|
|
194
|
+
if not guard.check(ctx, "Reload all index configurations"):
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
client = get_client(ctx)
|
|
198
|
+
client.service.post("/services/data/indexes/_reload")
|
|
199
|
+
output.info("Index configurations reloaded.")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Info command — server info, license, version."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from splunkctl import output
|
|
8
|
+
from splunkctl.client import get_client
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@click.pass_context
|
|
13
|
+
def info(ctx: click.Context) -> None:
|
|
14
|
+
"""Show Splunk server info."""
|
|
15
|
+
client = get_client(ctx)
|
|
16
|
+
svc = client.service
|
|
17
|
+
si: dict[str, Any] = dict(svc.info)
|
|
18
|
+
|
|
19
|
+
row: dict[str, Any] = {
|
|
20
|
+
"server_name": si.get("serverName", ""),
|
|
21
|
+
"version": si.get("version", ""),
|
|
22
|
+
"build": si.get("build", ""),
|
|
23
|
+
"os": si.get("os_name", ""),
|
|
24
|
+
"cpu_arch": si.get("cpu_arch", ""),
|
|
25
|
+
"license_state": si.get("licenseState", ""),
|
|
26
|
+
"mode": si.get("mode", ""),
|
|
27
|
+
"guid": si.get("guid", ""),
|
|
28
|
+
}
|
|
29
|
+
output.render(ctx, row)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Data input commands — list, create, update, delete, enable, disable."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from splunkctl import guard, output
|
|
8
|
+
from splunkctl.client import get_client
|
|
9
|
+
|
|
10
|
+
VALID_KINDS = ("monitor", "tcp", "udp", "script", "http")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _input_row(inp: Any) -> dict[str, Any]:
|
|
14
|
+
content: dict[str, Any] = dict(inp.content)
|
|
15
|
+
return {
|
|
16
|
+
"name": inp.name,
|
|
17
|
+
"kind": inp.kind,
|
|
18
|
+
"disabled": content.get("disabled", ""),
|
|
19
|
+
"index": content.get("index", ""),
|
|
20
|
+
"sourcetype": content.get("sourcetype", ""),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _find_input(service: Any, name: str) -> Any:
|
|
25
|
+
for inp in service.inputs.list():
|
|
26
|
+
if inp.name == name:
|
|
27
|
+
return inp
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.group("inputs")
|
|
32
|
+
def inputs_group() -> None:
|
|
33
|
+
"""Manage data inputs."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@inputs_group.command("list")
|
|
37
|
+
@click.option(
|
|
38
|
+
"--kind",
|
|
39
|
+
type=click.Choice(VALID_KINDS),
|
|
40
|
+
default=None,
|
|
41
|
+
help="Filter by input kind.",
|
|
42
|
+
)
|
|
43
|
+
@click.pass_context
|
|
44
|
+
def list_inputs(ctx: click.Context, *, kind: str | None) -> None:
|
|
45
|
+
"""List data inputs."""
|
|
46
|
+
client = get_client(ctx)
|
|
47
|
+
rows = [_input_row(i) for i in client.service.inputs.list()]
|
|
48
|
+
if kind:
|
|
49
|
+
rows = [r for r in rows if r["kind"] == kind]
|
|
50
|
+
output.render(ctx, rows)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@inputs_group.command()
|
|
54
|
+
@click.argument("name")
|
|
55
|
+
@click.pass_context
|
|
56
|
+
def get(ctx: click.Context, *, name: str) -> None:
|
|
57
|
+
"""Show details for a specific input."""
|
|
58
|
+
client = get_client(ctx)
|
|
59
|
+
inp = _find_input(client.service, name)
|
|
60
|
+
if inp is None:
|
|
61
|
+
output.error(f"Input not found: {name}")
|
|
62
|
+
ctx.exit(1)
|
|
63
|
+
return
|
|
64
|
+
row: dict[str, Any] = {"name": inp.name, "kind": inp.kind}
|
|
65
|
+
row.update(dict(inp.content))
|
|
66
|
+
output.render(ctx, row)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@inputs_group.command()
|
|
70
|
+
@click.option("--name", required=True, help="Input name/path.")
|
|
71
|
+
@click.option(
|
|
72
|
+
"--kind",
|
|
73
|
+
required=True,
|
|
74
|
+
type=click.Choice(VALID_KINDS),
|
|
75
|
+
help="Input kind.",
|
|
76
|
+
)
|
|
77
|
+
@click.option("--index", default=None, help="Target index.")
|
|
78
|
+
@click.option("--sourcetype", default=None, help="Source type.")
|
|
79
|
+
@click.option("--disabled", is_flag=True, default=False, help="Create disabled.")
|
|
80
|
+
@click.pass_context
|
|
81
|
+
def create(
|
|
82
|
+
ctx: click.Context,
|
|
83
|
+
*,
|
|
84
|
+
name: str,
|
|
85
|
+
kind: str,
|
|
86
|
+
index: str | None,
|
|
87
|
+
sourcetype: str | None,
|
|
88
|
+
disabled: bool,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Create a new data input."""
|
|
91
|
+
kwargs: dict[str, Any] = {}
|
|
92
|
+
if index:
|
|
93
|
+
kwargs["index"] = index
|
|
94
|
+
if sourcetype:
|
|
95
|
+
kwargs["sourcetype"] = sourcetype
|
|
96
|
+
if disabled:
|
|
97
|
+
kwargs["disabled"] = True
|
|
98
|
+
|
|
99
|
+
details = f"kind={kind}"
|
|
100
|
+
if kwargs:
|
|
101
|
+
details += ", " + ", ".join(f"{k}={v}" for k, v in kwargs.items())
|
|
102
|
+
|
|
103
|
+
if not guard.check(ctx, f"Create input '{name}'", details=details):
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
client = get_client(ctx)
|
|
107
|
+
client.service.inputs.create(name, kind, **kwargs)
|
|
108
|
+
output.info(f"Created input: {name} ({kind})")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@inputs_group.command()
|
|
112
|
+
@click.argument("name")
|
|
113
|
+
@click.option("--index", default=None, help="Target index.")
|
|
114
|
+
@click.option("--sourcetype", default=None, help="Source type.")
|
|
115
|
+
@click.option("--enabled/--disabled", default=None, help="Enable or disable.")
|
|
116
|
+
@click.pass_context
|
|
117
|
+
def update(
|
|
118
|
+
ctx: click.Context,
|
|
119
|
+
*,
|
|
120
|
+
name: str,
|
|
121
|
+
index: str | None,
|
|
122
|
+
sourcetype: str | None,
|
|
123
|
+
enabled: bool | None,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Update an existing data input."""
|
|
126
|
+
kwargs: dict[str, Any] = {}
|
|
127
|
+
if index is not None:
|
|
128
|
+
kwargs["index"] = index
|
|
129
|
+
if sourcetype is not None:
|
|
130
|
+
kwargs["sourcetype"] = sourcetype
|
|
131
|
+
if enabled is not None:
|
|
132
|
+
kwargs["disabled"] = not enabled
|
|
133
|
+
|
|
134
|
+
if not kwargs:
|
|
135
|
+
output.error("No update options provided.")
|
|
136
|
+
ctx.exit(1)
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
details = ", ".join(f"{k}={v}" for k, v in kwargs.items())
|
|
140
|
+
if not guard.check(ctx, f"Update input '{name}'", details=details):
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
client = get_client(ctx)
|
|
144
|
+
inp = _find_input(client.service, name)
|
|
145
|
+
if inp is None:
|
|
146
|
+
output.error(f"Input not found: {name}")
|
|
147
|
+
ctx.exit(1)
|
|
148
|
+
return
|
|
149
|
+
inp.update(**kwargs).refresh()
|
|
150
|
+
output.info(f"Updated input: {name}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@inputs_group.command()
|
|
154
|
+
@click.argument("name")
|
|
155
|
+
@click.pass_context
|
|
156
|
+
def delete(ctx: click.Context, *, name: str) -> None:
|
|
157
|
+
"""Delete a data input."""
|
|
158
|
+
if not guard.check(ctx, f"Delete input '{name}'"):
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
client = get_client(ctx)
|
|
162
|
+
inp = _find_input(client.service, name)
|
|
163
|
+
if inp is None:
|
|
164
|
+
output.error(f"Input not found: {name}")
|
|
165
|
+
ctx.exit(1)
|
|
166
|
+
return
|
|
167
|
+
inp.delete()
|
|
168
|
+
output.info(f"Deleted input: {name}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@inputs_group.command()
|
|
172
|
+
@click.argument("name")
|
|
173
|
+
@click.pass_context
|
|
174
|
+
def enable(ctx: click.Context, *, name: str) -> None:
|
|
175
|
+
"""Enable a disabled input."""
|
|
176
|
+
if not guard.check(ctx, f"Enable input '{name}'"):
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
client = get_client(ctx)
|
|
180
|
+
inp = _find_input(client.service, name)
|
|
181
|
+
if inp is None:
|
|
182
|
+
output.error(f"Input not found: {name}")
|
|
183
|
+
ctx.exit(1)
|
|
184
|
+
return
|
|
185
|
+
inp.enable()
|
|
186
|
+
output.info(f"Enabled input: {name}")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@inputs_group.command()
|
|
190
|
+
@click.argument("name")
|
|
191
|
+
@click.pass_context
|
|
192
|
+
def disable(ctx: click.Context, *, name: str) -> None:
|
|
193
|
+
"""Disable an input."""
|
|
194
|
+
if not guard.check(ctx, f"Disable input '{name}'"):
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
client = get_client(ctx)
|
|
198
|
+
inp = _find_input(client.service, name)
|
|
199
|
+
if inp is None:
|
|
200
|
+
output.error(f"Input not found: {name}")
|
|
201
|
+
ctx.exit(1)
|
|
202
|
+
return
|
|
203
|
+
inp.disable()
|
|
204
|
+
output.info(f"Disabled input: {name}")
|