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
splunkctl/__init__.py
ADDED
splunkctl/__main__.py
ADDED
splunkctl/client.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""SDK wrapper — lazy connection and auth resolution."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import splunklib.client as splunk_client
|
|
8
|
+
|
|
9
|
+
from splunkctl import config as cfg_mod
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SplunkClient:
|
|
13
|
+
"""Lazy-initializing Splunk SDK client.
|
|
14
|
+
|
|
15
|
+
Connection is established on first access to ``.service``.
|
|
16
|
+
Help, config, and offline commands never trigger auth.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__( # noqa: D107
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
config_path: Path | None = None,
|
|
23
|
+
host: str | None = None,
|
|
24
|
+
port: int | None = None,
|
|
25
|
+
username: str | None = None,
|
|
26
|
+
password: str | None = None,
|
|
27
|
+
token: str | None = None,
|
|
28
|
+
scheme: str | None = None,
|
|
29
|
+
app: str | None = None,
|
|
30
|
+
owner: str | None = None,
|
|
31
|
+
verify: bool | None = None,
|
|
32
|
+
timeout: int = 30,
|
|
33
|
+
debug: bool = False,
|
|
34
|
+
) -> None:
|
|
35
|
+
self._config_path = config_path
|
|
36
|
+
self._overrides: dict[str, Any] = {
|
|
37
|
+
k: v
|
|
38
|
+
for k, v in {
|
|
39
|
+
"host": host,
|
|
40
|
+
"port": port,
|
|
41
|
+
"username": username,
|
|
42
|
+
"password": password,
|
|
43
|
+
"token": token,
|
|
44
|
+
"scheme": scheme,
|
|
45
|
+
"app": app,
|
|
46
|
+
"owner": owner,
|
|
47
|
+
"verify": verify,
|
|
48
|
+
}.items()
|
|
49
|
+
if v is not None
|
|
50
|
+
}
|
|
51
|
+
self._timeout = timeout
|
|
52
|
+
self._debug = debug
|
|
53
|
+
self._service: Any = None
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def service(self) -> Any:
|
|
57
|
+
"""Connect on first access."""
|
|
58
|
+
if self._service is None:
|
|
59
|
+
if self._debug:
|
|
60
|
+
import logging
|
|
61
|
+
|
|
62
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
63
|
+
logging.getLogger("splunklib").setLevel(logging.DEBUG)
|
|
64
|
+
|
|
65
|
+
cfg = cfg_mod.load(self._config_path)
|
|
66
|
+
cfg.update(self._overrides)
|
|
67
|
+
|
|
68
|
+
connect_args: dict[str, Any] = {
|
|
69
|
+
"host": cfg.get("host", "localhost"),
|
|
70
|
+
"port": int(cfg.get("port", 8089)),
|
|
71
|
+
"scheme": cfg.get("scheme", "https"),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if cfg.get("token"):
|
|
75
|
+
connect_args["splunkToken"] = cfg["token"]
|
|
76
|
+
else:
|
|
77
|
+
connect_args["username"] = cfg.get("username", "")
|
|
78
|
+
connect_args["password"] = cfg.get("password", "")
|
|
79
|
+
|
|
80
|
+
if cfg.get("app"):
|
|
81
|
+
connect_args["app"] = cfg["app"]
|
|
82
|
+
if cfg.get("owner"):
|
|
83
|
+
connect_args["owner"] = cfg["owner"]
|
|
84
|
+
|
|
85
|
+
if not cfg.get("verify", False):
|
|
86
|
+
connect_args["verify"] = False
|
|
87
|
+
|
|
88
|
+
if self._timeout:
|
|
89
|
+
connect_args["timeout"] = self._timeout
|
|
90
|
+
|
|
91
|
+
self._service = splunk_client.connect(**connect_args)
|
|
92
|
+
|
|
93
|
+
return self._service
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_client(ctx: click.Context) -> SplunkClient:
|
|
97
|
+
"""Build a SplunkClient from Click context. Does not connect."""
|
|
98
|
+
obj: dict[str, Any] = ctx.ensure_object(dict)
|
|
99
|
+
return SplunkClient(
|
|
100
|
+
config_path=Path(obj["config"]) if obj.get("config") else None,
|
|
101
|
+
timeout=obj.get("timeout", 30),
|
|
102
|
+
debug=obj.get("debug", False),
|
|
103
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Alerts commands — fired alerts, alert actions, suppression."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from splunkctl import output
|
|
9
|
+
from splunkctl.client import get_client
|
|
10
|
+
from splunkctl.guard import check
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group("alerts")
|
|
14
|
+
def alerts_group() -> None:
|
|
15
|
+
"""Manage fired alerts and alert actions."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@alerts_group.command("list")
|
|
19
|
+
@click.pass_context
|
|
20
|
+
def list_alerts(ctx: click.Context) -> None:
|
|
21
|
+
"""List fired alerts."""
|
|
22
|
+
client = get_client(ctx)
|
|
23
|
+
rows: list[dict[str, Any]] = []
|
|
24
|
+
for alert in client.service.fired_alerts:
|
|
25
|
+
if alert.name == "-":
|
|
26
|
+
continue
|
|
27
|
+
content: dict[str, Any] = dict(alert.content)
|
|
28
|
+
rows.append(
|
|
29
|
+
{
|
|
30
|
+
"name": alert.name,
|
|
31
|
+
"count": alert.count,
|
|
32
|
+
"triggered_time": content.get("triggered_time", ""),
|
|
33
|
+
"severity": content.get("severity", ""),
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
if not rows:
|
|
37
|
+
output.info("No fired alerts.")
|
|
38
|
+
return
|
|
39
|
+
output.render(ctx, rows)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@alerts_group.command("get")
|
|
43
|
+
@click.argument("name")
|
|
44
|
+
@click.pass_context
|
|
45
|
+
def get_alert(ctx: click.Context, name: str) -> None:
|
|
46
|
+
"""Get details of a specific fired alert group."""
|
|
47
|
+
client = get_client(ctx)
|
|
48
|
+
try:
|
|
49
|
+
alert = client.service.fired_alerts[name]
|
|
50
|
+
except KeyError:
|
|
51
|
+
output.error(f"Fired alert not found: {name}")
|
|
52
|
+
ctx.exit(1)
|
|
53
|
+
return
|
|
54
|
+
row: dict[str, Any] = {"name": alert.name, "count": alert.count}
|
|
55
|
+
row.update(dict(alert.content))
|
|
56
|
+
output.render(ctx, row)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@alerts_group.command("actions")
|
|
60
|
+
@click.pass_context
|
|
61
|
+
def list_actions(ctx: click.Context) -> None:
|
|
62
|
+
"""List available alert action types."""
|
|
63
|
+
client = get_client(ctx)
|
|
64
|
+
rows: list[dict[str, Any]] = []
|
|
65
|
+
for stanza in client.service.confs["alert_actions"]:
|
|
66
|
+
content: dict[str, Any] = dict(stanza.content)
|
|
67
|
+
rows.append(
|
|
68
|
+
{
|
|
69
|
+
"name": stanza.name,
|
|
70
|
+
"label": content.get("label", ""),
|
|
71
|
+
"description": content.get("description", ""),
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
output.render(ctx, rows)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@alerts_group.command("suppress")
|
|
78
|
+
@click.argument("name")
|
|
79
|
+
@click.option(
|
|
80
|
+
"--duration",
|
|
81
|
+
type=int,
|
|
82
|
+
default=3600,
|
|
83
|
+
help="Suppression duration in seconds.",
|
|
84
|
+
)
|
|
85
|
+
@click.pass_context
|
|
86
|
+
def suppress_alert(ctx: click.Context, name: str, duration: int) -> None:
|
|
87
|
+
"""Suppress a fired alert (guarded)."""
|
|
88
|
+
details = f"Suppress '{name}' for {duration}s"
|
|
89
|
+
if not check(ctx, "suppress fired alert", details=details):
|
|
90
|
+
return
|
|
91
|
+
client = get_client(ctx)
|
|
92
|
+
path = f"/services/alerts/fired_alerts/{quote(name, safe='')}"
|
|
93
|
+
client.service.post(path, suppress="1", expiration=str(duration))
|
|
94
|
+
output.info(f"Suppressed: {name} for {duration}s")
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""App management commands — list, get, install, uninstall, update, 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
|
+
_APP_FIELDS = ("label", "version", "visible", "disabled", "author", "description")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _app_row(app: Any) -> dict[str, Any]:
|
|
14
|
+
content: dict[str, Any] = dict(app.content)
|
|
15
|
+
return {
|
|
16
|
+
"name": app.name,
|
|
17
|
+
**{f: content.get(f, "") for f in _APP_FIELDS},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _list_fields(app: Any) -> dict[str, Any]:
|
|
22
|
+
content: dict[str, Any] = dict(app.content)
|
|
23
|
+
return {
|
|
24
|
+
"name": app.name,
|
|
25
|
+
"label": content.get("label", ""),
|
|
26
|
+
"version": content.get("version", ""),
|
|
27
|
+
"visible": content.get("visible", ""),
|
|
28
|
+
"disabled": content.get("disabled", ""),
|
|
29
|
+
"author": content.get("author", ""),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@click.group("apps")
|
|
34
|
+
def apps_group() -> None:
|
|
35
|
+
"""Manage Splunk apps."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@apps_group.command("list")
|
|
39
|
+
@click.pass_context
|
|
40
|
+
def list_apps(ctx: click.Context) -> None:
|
|
41
|
+
"""List installed apps."""
|
|
42
|
+
client = get_client(ctx)
|
|
43
|
+
svc = client.service
|
|
44
|
+
apps = svc.apps.list()
|
|
45
|
+
if not apps:
|
|
46
|
+
output.info("No apps found.")
|
|
47
|
+
return
|
|
48
|
+
rows = [_list_fields(a) for a in apps]
|
|
49
|
+
output.render(ctx, rows)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@apps_group.command("get")
|
|
53
|
+
@click.argument("name")
|
|
54
|
+
@click.pass_context
|
|
55
|
+
def get_app(ctx: click.Context, name: str) -> None:
|
|
56
|
+
"""Get app details."""
|
|
57
|
+
client = get_client(ctx)
|
|
58
|
+
svc = client.service
|
|
59
|
+
try:
|
|
60
|
+
app = svc.apps[name]
|
|
61
|
+
except KeyError:
|
|
62
|
+
output.error(f"App '{name}' not found.")
|
|
63
|
+
ctx.exit(1)
|
|
64
|
+
return
|
|
65
|
+
output.render(ctx, _app_row(app))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@apps_group.command("install")
|
|
69
|
+
@click.option("--name", required=True, help="App name.")
|
|
70
|
+
@click.option(
|
|
71
|
+
"--path",
|
|
72
|
+
"file_path",
|
|
73
|
+
required=True,
|
|
74
|
+
type=click.Path(exists=True),
|
|
75
|
+
help="Path to .tar.gz or .spl package.",
|
|
76
|
+
)
|
|
77
|
+
@click.pass_context
|
|
78
|
+
def install_app(ctx: click.Context, name: str, file_path: str) -> None:
|
|
79
|
+
"""Install an app from a .tar.gz or .spl file."""
|
|
80
|
+
details = f"Install app '{name}' from '{file_path}'"
|
|
81
|
+
if not guard.check(ctx, details):
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
client = get_client(ctx)
|
|
85
|
+
svc = client.service
|
|
86
|
+
try:
|
|
87
|
+
svc.post(
|
|
88
|
+
"/services/apps/local",
|
|
89
|
+
name=name,
|
|
90
|
+
update=True,
|
|
91
|
+
filename=True,
|
|
92
|
+
**{"appfile": file_path},
|
|
93
|
+
)
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
output.error(f"Install failed: {exc}")
|
|
96
|
+
ctx.exit(1)
|
|
97
|
+
return
|
|
98
|
+
output.info(f"Installed app '{name}'.")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@apps_group.command("uninstall")
|
|
102
|
+
@click.argument("name")
|
|
103
|
+
@click.pass_context
|
|
104
|
+
def uninstall_app(ctx: click.Context, name: str) -> None:
|
|
105
|
+
"""Uninstall an app."""
|
|
106
|
+
details = f"Uninstall app '{name}'"
|
|
107
|
+
if not guard.check(ctx, details):
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
client = get_client(ctx)
|
|
111
|
+
svc = client.service
|
|
112
|
+
try:
|
|
113
|
+
app = svc.apps[name]
|
|
114
|
+
app.delete()
|
|
115
|
+
except KeyError:
|
|
116
|
+
output.error(f"App '{name}' not found.")
|
|
117
|
+
ctx.exit(1)
|
|
118
|
+
return
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
output.error(f"Uninstall failed: {exc}")
|
|
121
|
+
ctx.exit(1)
|
|
122
|
+
return
|
|
123
|
+
output.info(f"Uninstalled app '{name}'.")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@apps_group.command("update")
|
|
127
|
+
@click.argument("name")
|
|
128
|
+
@click.option("--visible/--hidden", default=None, help="Set app visibility.")
|
|
129
|
+
@click.option("--enabled/--disabled", default=None, help="Enable or disable the app.")
|
|
130
|
+
@click.pass_context
|
|
131
|
+
def update_app(
|
|
132
|
+
ctx: click.Context,
|
|
133
|
+
name: str,
|
|
134
|
+
visible: bool | None,
|
|
135
|
+
enabled: bool | None,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Update app settings."""
|
|
138
|
+
kwargs: dict[str, Any] = {}
|
|
139
|
+
if visible is not None:
|
|
140
|
+
kwargs["visible"] = visible
|
|
141
|
+
if enabled is not None:
|
|
142
|
+
kwargs["disabled"] = not enabled
|
|
143
|
+
if not kwargs:
|
|
144
|
+
msg = "No settings specified. Use --visible/--hidden or --enabled/--disabled."
|
|
145
|
+
output.error(msg)
|
|
146
|
+
ctx.exit(1)
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
parts: list[str] = []
|
|
150
|
+
if visible is not None:
|
|
151
|
+
parts.append(f"visible={'true' if visible else 'false'}")
|
|
152
|
+
if enabled is not None:
|
|
153
|
+
parts.append(f"disabled={'false' if enabled else 'true'}")
|
|
154
|
+
details = f"Update app '{name}': {', '.join(parts)}"
|
|
155
|
+
if not guard.check(ctx, details):
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
client = get_client(ctx)
|
|
159
|
+
svc = client.service
|
|
160
|
+
try:
|
|
161
|
+
app = svc.apps[name]
|
|
162
|
+
app.update(**kwargs)
|
|
163
|
+
except KeyError:
|
|
164
|
+
output.error(f"App '{name}' not found.")
|
|
165
|
+
ctx.exit(1)
|
|
166
|
+
return
|
|
167
|
+
except Exception as exc:
|
|
168
|
+
output.error(f"Update failed: {exc}")
|
|
169
|
+
ctx.exit(1)
|
|
170
|
+
return
|
|
171
|
+
output.info(f"Updated app '{name}'.")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@apps_group.command("reload")
|
|
175
|
+
@click.pass_context
|
|
176
|
+
def reload_apps(ctx: click.Context) -> None:
|
|
177
|
+
"""Reload all apps."""
|
|
178
|
+
if not guard.check(ctx, "Reload all apps"):
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
client = get_client(ctx)
|
|
182
|
+
svc = client.service
|
|
183
|
+
try:
|
|
184
|
+
svc.get("/services/apps/local/_reload")
|
|
185
|
+
except Exception as exc:
|
|
186
|
+
output.error(f"Reload failed: {exc}")
|
|
187
|
+
ctx.exit(1)
|
|
188
|
+
return
|
|
189
|
+
output.info("Apps reloaded.")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Self-discovery — machine-readable command tree for agents."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from splunkctl import __version__, output
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _param_entry(p: click.Parameter) -> dict[str, Any]:
|
|
12
|
+
"""Build a single parameter descriptor."""
|
|
13
|
+
entry: dict[str, Any] = {
|
|
14
|
+
"name": p.name or "",
|
|
15
|
+
"type": p.type.name if hasattr(p.type, "name") else str(p.type),
|
|
16
|
+
}
|
|
17
|
+
if isinstance(p, click.Argument):
|
|
18
|
+
entry["kind"] = "argument"
|
|
19
|
+
elif isinstance(p, click.Option):
|
|
20
|
+
entry["kind"] = "option"
|
|
21
|
+
if p.opts:
|
|
22
|
+
entry["flags"] = p.opts
|
|
23
|
+
if p.is_flag:
|
|
24
|
+
entry["type"] = "flag"
|
|
25
|
+
if p.required:
|
|
26
|
+
entry["required"] = True
|
|
27
|
+
if p.help:
|
|
28
|
+
entry["help"] = p.help
|
|
29
|
+
if isinstance(p.type, click.Choice):
|
|
30
|
+
entry["choices"] = list(p.type.choices)
|
|
31
|
+
if not (isinstance(p, click.Option) and p.is_flag):
|
|
32
|
+
if isinstance(p.default, (str, int, float)) and p.default is not None:
|
|
33
|
+
entry["default"] = p.default
|
|
34
|
+
return entry
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _walk(group: click.Group) -> list[dict[str, Any]]:
|
|
38
|
+
"""Recursively build the command tree."""
|
|
39
|
+
nodes: list[dict[str, Any]] = []
|
|
40
|
+
for name, cmd in sorted(group.commands.items()):
|
|
41
|
+
node: dict[str, Any] = {
|
|
42
|
+
"name": name,
|
|
43
|
+
"help": (cmd.help or "").split("\n")[0],
|
|
44
|
+
}
|
|
45
|
+
if isinstance(cmd, click.Group):
|
|
46
|
+
node["subcommands"] = _walk(cmd)
|
|
47
|
+
else:
|
|
48
|
+
params: list[dict[str, Any]] = []
|
|
49
|
+
for p in cmd.params:
|
|
50
|
+
if p.name in ("help",):
|
|
51
|
+
continue
|
|
52
|
+
params.append(_param_entry(p))
|
|
53
|
+
if params:
|
|
54
|
+
node["params"] = params
|
|
55
|
+
nodes.append(node)
|
|
56
|
+
return nodes
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@click.command("commands")
|
|
60
|
+
@click.pass_context
|
|
61
|
+
def commands_meta(ctx: click.Context) -> None:
|
|
62
|
+
"""Print the command tree as JSON (for agent discovery)."""
|
|
63
|
+
root = ctx.parent
|
|
64
|
+
if root is None or not isinstance(root.command, click.Group):
|
|
65
|
+
output.error("Cannot resolve CLI root.")
|
|
66
|
+
ctx.exit(1)
|
|
67
|
+
return
|
|
68
|
+
result: dict[str, Any] = {
|
|
69
|
+
"version": __version__,
|
|
70
|
+
"commands": _walk(root.command),
|
|
71
|
+
}
|
|
72
|
+
click.echo(json.dumps(result, indent=2))
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Config commands — init, show, test."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from splunkctl import config as cfg_mod
|
|
9
|
+
from splunkctl import output
|
|
10
|
+
from splunkctl.client import SplunkClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group("config")
|
|
14
|
+
def config_group() -> None:
|
|
15
|
+
"""Manage splunkctl configuration."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@config_group.command()
|
|
19
|
+
@click.option("--host", prompt=True, default="localhost", help="Splunk host.")
|
|
20
|
+
@click.option("--port", prompt=True, default=8089, type=int, help="Splunk port.")
|
|
21
|
+
@click.option("--username", prompt=True, default="admin", help="Splunk username.")
|
|
22
|
+
@click.option("--password", prompt=True, hide_input=True, help="Splunk password.")
|
|
23
|
+
@click.option(
|
|
24
|
+
"--scheme",
|
|
25
|
+
type=click.Choice(["https", "http"]),
|
|
26
|
+
default="https",
|
|
27
|
+
help="Connection scheme.",
|
|
28
|
+
)
|
|
29
|
+
@click.option("--verify/--no-verify", default=False, help="Verify SSL certificate.")
|
|
30
|
+
@click.option(
|
|
31
|
+
"--path",
|
|
32
|
+
type=click.Path(),
|
|
33
|
+
default=None,
|
|
34
|
+
help="Config file path (default: ~/.splunkctl/config.yaml).",
|
|
35
|
+
)
|
|
36
|
+
def init(
|
|
37
|
+
host: str,
|
|
38
|
+
port: int,
|
|
39
|
+
username: str,
|
|
40
|
+
password: str,
|
|
41
|
+
scheme: str,
|
|
42
|
+
verify: bool,
|
|
43
|
+
path: str | None,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Interactive setup — create or overwrite config."""
|
|
46
|
+
cfg: dict[str, Any] = {
|
|
47
|
+
"host": host,
|
|
48
|
+
"port": port,
|
|
49
|
+
"username": username,
|
|
50
|
+
"password": password,
|
|
51
|
+
"scheme": scheme,
|
|
52
|
+
"verify": verify,
|
|
53
|
+
}
|
|
54
|
+
dest = Path(path) if path else None
|
|
55
|
+
saved = cfg_mod.save(cfg, dest)
|
|
56
|
+
output.info(f"Config saved to {saved}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@config_group.command()
|
|
60
|
+
@click.pass_context
|
|
61
|
+
def show(ctx: click.Context) -> None:
|
|
62
|
+
"""Display current config (secrets redacted)."""
|
|
63
|
+
cfg_path = ctx.obj.get("config")
|
|
64
|
+
cfg = cfg_mod.load(Path(cfg_path) if cfg_path else None)
|
|
65
|
+
output.render(ctx, cfg_mod.redact(cfg))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@config_group.command()
|
|
69
|
+
@click.pass_context
|
|
70
|
+
def test(ctx: click.Context) -> None:
|
|
71
|
+
"""Verify connectivity and auth against the Splunk instance."""
|
|
72
|
+
cfg_path = ctx.obj.get("config")
|
|
73
|
+
config_path = Path(cfg_path) if cfg_path else None
|
|
74
|
+
cfg = cfg_mod.load(config_path)
|
|
75
|
+
|
|
76
|
+
output.info(
|
|
77
|
+
f"Connecting to {cfg.get('scheme', 'https')}://"
|
|
78
|
+
f"{cfg.get('host', 'localhost')}:{cfg.get('port', 8089)} ..."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
client = SplunkClient(config_path=config_path)
|
|
83
|
+
svc_info = client.service.info
|
|
84
|
+
output.info(f"OK — {svc_info['serverName']} (Splunk {svc_info['version']})")
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
output.error(str(exc))
|
|
87
|
+
ctx.exit(1)
|