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.
@@ -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}")