salt-api-cli 1.1.0__tar.gz → 1.3.0__tar.gz

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,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: salt-api-cli
3
+ Version: 1.3.0
4
+ Summary: CLI to access salt-api
5
+ Author-email: Pradish Bijukchhe <pradish@sandbox.com.np>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/sandbox-pokhara/saltapi-cli
8
+ Project-URL: Issues, https://github.com/sandbox-pokhara/saltapi-cli/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: rich>=15.0.0
13
+ Requires-Dist: typeguard>=4.5.2
14
+ Provides-Extra: pre-commit
15
+ Requires-Dist: pre-commit; extra == "pre-commit"
16
+
17
+ # salt-api-cli
18
+
19
+ Thin Python CLI for [salt-api](https://docs.saltproject.io/en/latest/ref/netapi/all/salt.netapi.rest_cherrypy.html).
20
+ Depends only on the standard library plus [`rich`](https://github.com/Textualize/rich)
21
+ (readable output) and [`typeguard`](https://github.com/agronholm/typeguard)
22
+ (JSON validation).
23
+
24
+ Logs in once with PAM credentials, caches the token in
25
+ `~/.cache/salt-api-cli/token.json`, then invokes salt-api's `local`,
26
+ `runner`, and `wheel` clients over HTTPS. The cached token self-heals:
27
+ it is refreshed proactively when its stored expiry has passed, and
28
+ reactively when the server rejects it (e.g. after the salt-master
29
+ container restarts and wipes its session store) — on rejection the CLI
30
+ discards the token, logs in again, and retries the request once.
31
+
32
+ Commands come in two layers:
33
+
34
+ - **Low-level** (`local`, `runner`, `wheel`) map directly to the salt-api
35
+ clients and print **raw JSON**.
36
+ - **High-level** (`state`, `keys`) wrap those clients and render
37
+ **readable, colorized output** with `rich`.
38
+
39
+ ## Installation
40
+
41
+ ```
42
+ pip install salt-api-cli
43
+ ```
44
+
45
+ ## Configuration
46
+
47
+ Configuration is resolved in this order (later sources override earlier):
48
+
49
+ 1. `~/.saltapiclirc` — INI file, `[salt-api-cli]` section
50
+ 2. Environment variables — `SALT_API_URL`, `SALT_API_USER`, `SALT_API_PASS`, `SALT_API_INSECURE`
51
+ 3. Command-line flags — `--url`, `--user`, `--password`, `--insecure`, `--relogin`, `--no-token-cache`
52
+
53
+ Example `~/.saltapiclirc`:
54
+
55
+ ```ini
56
+ [salt-api-cli]
57
+ url = https://salt.example.com
58
+ user = salt_api
59
+ password = secret
60
+ insecure = false
61
+ ```
62
+
63
+ `SALT_API_INSECURE=1` (or `insecure = true` in the config) skips TLS
64
+ certificate verification.
65
+
66
+ Token cache control: `--relogin` ignores any cached token and logs in
67
+ fresh (re-caching the new token); `--no-token-cache` neither reads nor
68
+ writes the cache for that run; `salt logout` discards the cached token.
69
+
70
+ ## Usage
71
+
72
+ ### Low-level commands (raw JSON)
73
+
74
+ These map one-to-one to the salt-api clients and print the response
75
+ verbatim as indented JSON.
76
+
77
+ ```
78
+ # Local client — fan out to minions
79
+ salt local '*' test.ping
80
+ salt local 'bml*' cmd.run 'whoami'
81
+ salt local 'bml1' cmd.run 'Get-Date' shell=powershell
82
+
83
+ # Runner client (master-side: manage.status, jobs.list_jobs, ...)
84
+ salt runner manage.status
85
+ salt runner jobs.list_jobs
86
+
87
+ # Wheel client (master-side, low-level)
88
+ salt wheel key.list_all
89
+ ```
90
+
91
+ ### High-level commands (readable, colorized)
92
+
93
+ These wrap the low-level clients and render their output with `rich`.
94
+
95
+ ```
96
+ # State runs — a colored table of states, one row each, with a summary.
97
+ # Driven by the local client + state.* functions.
98
+ salt state highstate 'bml1' # apply the highstate
99
+ salt state test 'bml1' # dry-run the highstate (forces test=True)
100
+ salt state apply 'bml1' veyon # apply specific sls module(s)
101
+ salt state apply 'bml1' veyon.ldap test=True
102
+
103
+ # Key management — wraps the wheel client's key.* functions.
104
+ # `keys list` shows one colored panel per status (Accepted/Pending/Denied/Rejected).
105
+ salt keys list
106
+ salt keys accept <id-or-glob>
107
+ salt keys accept-all
108
+ salt keys reject <id-or-glob>
109
+ salt keys delete <id-or-glob>
110
+ ```
111
+
112
+ Color and panels appear when writing to a terminal; output is plain when
113
+ piped to a file or pager.
114
+
115
+ Any `key=value` argument is parsed as a kwarg to the salt function;
116
+ anything else is positional.
117
+
118
+ You can also invoke the CLI as a module: `python -m salt_api_cli ...`.
119
+
120
+ ## License
121
+
122
+ This project is licensed under the terms of the MIT license.
@@ -0,0 +1,106 @@
1
+ # salt-api-cli
2
+
3
+ Thin Python CLI for [salt-api](https://docs.saltproject.io/en/latest/ref/netapi/all/salt.netapi.rest_cherrypy.html).
4
+ Depends only on the standard library plus [`rich`](https://github.com/Textualize/rich)
5
+ (readable output) and [`typeguard`](https://github.com/agronholm/typeguard)
6
+ (JSON validation).
7
+
8
+ Logs in once with PAM credentials, caches the token in
9
+ `~/.cache/salt-api-cli/token.json`, then invokes salt-api's `local`,
10
+ `runner`, and `wheel` clients over HTTPS. The cached token self-heals:
11
+ it is refreshed proactively when its stored expiry has passed, and
12
+ reactively when the server rejects it (e.g. after the salt-master
13
+ container restarts and wipes its session store) — on rejection the CLI
14
+ discards the token, logs in again, and retries the request once.
15
+
16
+ Commands come in two layers:
17
+
18
+ - **Low-level** (`local`, `runner`, `wheel`) map directly to the salt-api
19
+ clients and print **raw JSON**.
20
+ - **High-level** (`state`, `keys`) wrap those clients and render
21
+ **readable, colorized output** with `rich`.
22
+
23
+ ## Installation
24
+
25
+ ```
26
+ pip install salt-api-cli
27
+ ```
28
+
29
+ ## Configuration
30
+
31
+ Configuration is resolved in this order (later sources override earlier):
32
+
33
+ 1. `~/.saltapiclirc` — INI file, `[salt-api-cli]` section
34
+ 2. Environment variables — `SALT_API_URL`, `SALT_API_USER`, `SALT_API_PASS`, `SALT_API_INSECURE`
35
+ 3. Command-line flags — `--url`, `--user`, `--password`, `--insecure`, `--relogin`, `--no-token-cache`
36
+
37
+ Example `~/.saltapiclirc`:
38
+
39
+ ```ini
40
+ [salt-api-cli]
41
+ url = https://salt.example.com
42
+ user = salt_api
43
+ password = secret
44
+ insecure = false
45
+ ```
46
+
47
+ `SALT_API_INSECURE=1` (or `insecure = true` in the config) skips TLS
48
+ certificate verification.
49
+
50
+ Token cache control: `--relogin` ignores any cached token and logs in
51
+ fresh (re-caching the new token); `--no-token-cache` neither reads nor
52
+ writes the cache for that run; `salt logout` discards the cached token.
53
+
54
+ ## Usage
55
+
56
+ ### Low-level commands (raw JSON)
57
+
58
+ These map one-to-one to the salt-api clients and print the response
59
+ verbatim as indented JSON.
60
+
61
+ ```
62
+ # Local client — fan out to minions
63
+ salt local '*' test.ping
64
+ salt local 'bml*' cmd.run 'whoami'
65
+ salt local 'bml1' cmd.run 'Get-Date' shell=powershell
66
+
67
+ # Runner client (master-side: manage.status, jobs.list_jobs, ...)
68
+ salt runner manage.status
69
+ salt runner jobs.list_jobs
70
+
71
+ # Wheel client (master-side, low-level)
72
+ salt wheel key.list_all
73
+ ```
74
+
75
+ ### High-level commands (readable, colorized)
76
+
77
+ These wrap the low-level clients and render their output with `rich`.
78
+
79
+ ```
80
+ # State runs — a colored table of states, one row each, with a summary.
81
+ # Driven by the local client + state.* functions.
82
+ salt state highstate 'bml1' # apply the highstate
83
+ salt state test 'bml1' # dry-run the highstate (forces test=True)
84
+ salt state apply 'bml1' veyon # apply specific sls module(s)
85
+ salt state apply 'bml1' veyon.ldap test=True
86
+
87
+ # Key management — wraps the wheel client's key.* functions.
88
+ # `keys list` shows one colored panel per status (Accepted/Pending/Denied/Rejected).
89
+ salt keys list
90
+ salt keys accept <id-or-glob>
91
+ salt keys accept-all
92
+ salt keys reject <id-or-glob>
93
+ salt keys delete <id-or-glob>
94
+ ```
95
+
96
+ Color and panels appear when writing to a terminal; output is plain when
97
+ piped to a file or pager.
98
+
99
+ Any `key=value` argument is parsed as a kwarg to the salt function;
100
+ anything else is positional.
101
+
102
+ You can also invoke the CLI as a module: `python -m salt_api_cli ...`.
103
+
104
+ ## License
105
+
106
+ This project is licensed under the terms of the MIT license.
@@ -12,6 +12,7 @@ license = "MIT"
12
12
  keywords = []
13
13
  classifiers = ["Programming Language :: Python :: 3"]
14
14
  dependencies = [
15
+ "rich>=15.0.0",
15
16
  "typeguard>=4.5.2",
16
17
  ]
17
18
  dynamic = ["version"]
@@ -0,0 +1,204 @@
1
+ """salt-api-cli — thin Python CLI for salt-api.
2
+
3
+ Logs in once with PAM creds, caches the token in
4
+ ~/.cache/salt-api-cli/token.json, then invokes the salt-api local/
5
+ runner/wheel clients over HTTPS. Depends only on the stdlib plus
6
+ ``typeguard`` for validating cached/responded JSON.
7
+
8
+ This module is the CLI glue: it parses arguments and dispatches each
9
+ command, wiring the low-level transport (:mod:`salt_api_cli.lowlevel`) to
10
+ the high-level human-readable rendering (:mod:`salt_api_cli.highlevel`).
11
+
12
+ The cached token self-heals: it is refreshed proactively when its stored
13
+ expiry has passed, and reactively when the server rejects it (HTTP 401 or
14
+ an EAUTH body) — e.g. after the salt-master container restarts and wipes
15
+ its session store. `--relogin` forces a fresh login, `--no-token-cache`
16
+ skips the cache entirely, and the `logout` subcommand discards the token.
17
+
18
+ Configuration (later sources override earlier):
19
+ 1. ~/.saltapiclirc INI file, [salt-api-cli] section
20
+ 2. environment variables SALT_API_URL, SALT_API_USER,
21
+ SALT_API_PASS, SALT_API_INSECURE
22
+ 3. command-line flags --url, --user, --password,
23
+ --insecure, --relogin,
24
+ --no-token-cache
25
+
26
+ Any `key=value` argument to local/runner/wheel is parsed as a kwarg to
27
+ the salt function. Anything else is positional.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import argparse
33
+ import json
34
+ from typing import Any
35
+
36
+ from salt_api_cli import highlevel
37
+ from salt_api_cli.lowlevel import (
38
+ TOKEN_FILE,
39
+ Config,
40
+ SaltApiError,
41
+ call,
42
+ clear_token,
43
+ load_config,
44
+ split_args,
45
+ )
46
+
47
+
48
+ def _run_local(cfg: Config, args: argparse.Namespace) -> None:
49
+ pos, kw = split_args(list(args.args))
50
+ payload: dict[str, Any] = {"tgt": args.target, "fun": args.function, "arg": pos}
51
+ if kw:
52
+ payload["kwarg"] = kw
53
+ print(json.dumps(call(cfg, "local", **payload), indent=2))
54
+
55
+
56
+ def _run_client(cfg: Config, client: str, args: argparse.Namespace) -> None:
57
+ pos, kw = split_args(list(args.args))
58
+ payload: dict[str, Any] = {"fun": args.function, "arg": pos}
59
+ if kw:
60
+ payload["kwarg"] = kw
61
+ print(json.dumps(call(cfg, client, **payload), indent=2))
62
+
63
+
64
+ def _run_state(cfg: Config, args: argparse.Namespace) -> None:
65
+ def client(name: str, **kw: Any) -> dict[str, Any]:
66
+ return call(cfg, name, **kw)
67
+
68
+ highlevel.run_state(args, client)
69
+
70
+
71
+ def _run_keys(cfg: Config, args: argparse.Namespace) -> None:
72
+ def wheel(**kw: Any) -> dict[str, Any]:
73
+ return call(cfg, "wheel", **kw)
74
+
75
+ highlevel.run_keys(args, wheel)
76
+
77
+
78
+ def _build_parser() -> argparse.ArgumentParser:
79
+ parser = argparse.ArgumentParser(
80
+ prog="salt",
81
+ description="Thin Python CLI for salt-api.",
82
+ formatter_class=argparse.RawDescriptionHelpFormatter,
83
+ epilog=(
84
+ "low-level (raw JSON):\n"
85
+ " salt local '*' test.ping\n"
86
+ " salt local 'bml*' cmd.run 'whoami'\n"
87
+ " salt local 'bml1' cmd.run 'Get-Date' shell=powershell\n"
88
+ " salt runner manage.status\n"
89
+ " salt wheel key.list_all\n"
90
+ "high-level (readable):\n"
91
+ " salt state highstate 'bml1'\n"
92
+ " salt state test 'bml1' # dry-run highstate (test=True)\n"
93
+ " salt state apply 'bml1' veyon\n"
94
+ " salt keys list\n"
95
+ " salt keys accept '<id-or-glob>'\n"
96
+ " salt keys accept-all\n"
97
+ ),
98
+ )
99
+ parser.add_argument("--url", help="salt-api base URL")
100
+ parser.add_argument("--user", help="PAM username")
101
+ parser.add_argument("--password", help="PAM password")
102
+ parser.add_argument(
103
+ "--insecure",
104
+ action="store_true",
105
+ help="skip TLS certificate verification",
106
+ )
107
+ parser.add_argument(
108
+ "--relogin",
109
+ action="store_true",
110
+ help="ignore any cached token and log in fresh (re-caches the new token)",
111
+ )
112
+ parser.add_argument(
113
+ "--no-token-cache",
114
+ dest="no_token_cache",
115
+ action="store_true",
116
+ help="do not read or write the token cache for this run",
117
+ )
118
+
119
+ sub = parser.add_subparsers(dest="command", required=True)
120
+
121
+ p_local = sub.add_parser("local", help="run a function on minions")
122
+ p_local.add_argument("target", help="minion target (id or glob)")
123
+ p_local.add_argument("function", help="salt function (e.g. test.ping)")
124
+ p_local.add_argument(
125
+ "args", nargs=argparse.REMAINDER, help="positional and key=value args"
126
+ )
127
+
128
+ p_runner = sub.add_parser("runner", help="invoke a master-side runner")
129
+ p_runner.add_argument("function")
130
+ p_runner.add_argument("args", nargs=argparse.REMAINDER)
131
+
132
+ p_wheel = sub.add_parser("wheel", help="invoke a master-side wheel function")
133
+ p_wheel.add_argument("function")
134
+ p_wheel.add_argument("args", nargs=argparse.REMAINDER)
135
+
136
+ p_state = sub.add_parser("state", help="apply states with readable output")
137
+ state_sub = p_state.add_subparsers(dest="action", required=True)
138
+ p_highstate = state_sub.add_parser("highstate", help="apply the highstate")
139
+ p_highstate.add_argument("target", help="minion target (id or glob)")
140
+ p_highstate.add_argument(
141
+ "args", nargs=argparse.REMAINDER, help="key=value args, e.g. test=True"
142
+ )
143
+ p_test = state_sub.add_parser(
144
+ "test", help="dry-run the highstate (forces test=True)"
145
+ )
146
+ p_test.add_argument("target", help="minion target (id or glob)")
147
+ p_test.add_argument("args", nargs=argparse.REMAINDER, help="extra key=value args")
148
+ p_apply = state_sub.add_parser("apply", help="apply specific sls module(s)")
149
+ p_apply.add_argument("target", help="minion target (id or glob)")
150
+ p_apply.add_argument("sls", help="sls module to apply (e.g. veyon or veyon.ldap)")
151
+ p_apply.add_argument(
152
+ "args", nargs=argparse.REMAINDER, help="key=value args, e.g. test=True"
153
+ )
154
+
155
+ p_keys = sub.add_parser("keys", help="manage minion keys")
156
+ keys_sub = p_keys.add_subparsers(dest="action", required=True)
157
+ keys_sub.add_parser("list", help="show keys grouped by status")
158
+ p_accept = keys_sub.add_parser("accept", help="accept a key by id or glob")
159
+ p_accept.add_argument("match")
160
+ keys_sub.add_parser("accept-all", help="accept every pending key")
161
+ p_reject = keys_sub.add_parser("reject", help="reject a key by id or glob")
162
+ p_reject.add_argument("match")
163
+ p_delete = keys_sub.add_parser("delete", help="delete a key by id or glob")
164
+ p_delete.add_argument("match")
165
+
166
+ sub.add_parser("logout", help="discard the cached auth token")
167
+
168
+ return parser
169
+
170
+
171
+ def main() -> None:
172
+ parser = _build_parser()
173
+ args = parser.parse_args()
174
+
175
+ # logout needs no server config — it just drops the local token file.
176
+ if args.command == "logout":
177
+ existed = TOKEN_FILE.exists()
178
+ clear_token()
179
+ print(
180
+ f"discarded cached token ({TOKEN_FILE})"
181
+ if existed
182
+ else f"no cached token to discard ({TOKEN_FILE})"
183
+ )
184
+ return
185
+
186
+ cfg = load_config(args)
187
+
188
+ try:
189
+ if args.command == "local":
190
+ _run_local(cfg, args)
191
+ elif args.command == "runner":
192
+ _run_client(cfg, "runner", args)
193
+ elif args.command == "wheel":
194
+ _run_client(cfg, "wheel", args)
195
+ elif args.command == "state":
196
+ _run_state(cfg, args)
197
+ elif args.command == "keys":
198
+ _run_keys(cfg, args)
199
+ except SaltApiError as e:
200
+ raise SystemExit(str(e))
201
+
202
+
203
+ if __name__ == "__main__":
204
+ main()