salt-api-cli 1.0.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.
- salt_api_cli-1.0.0/MANIFEST.in +1 -0
- salt_api_cli-1.0.0/PKG-INFO +81 -0
- salt_api_cli-1.0.0/README.md +67 -0
- salt_api_cli-1.0.0/pyproject.toml +43 -0
- salt_api_cli-1.0.0/salt_api_cli/__init__.py +0 -0
- salt_api_cli-1.0.0/salt_api_cli/__main__.py +4 -0
- salt_api_cli-1.0.0/salt_api_cli/cli.py +328 -0
- salt_api_cli-1.0.0/salt_api_cli/py.typed +0 -0
- salt_api_cli-1.0.0/salt_api_cli/version.py +1 -0
- salt_api_cli-1.0.0/salt_api_cli.egg-info/PKG-INFO +81 -0
- salt_api_cli-1.0.0/salt_api_cli.egg-info/SOURCES.txt +14 -0
- salt_api_cli-1.0.0/salt_api_cli.egg-info/dependency_links.txt +1 -0
- salt_api_cli-1.0.0/salt_api_cli.egg-info/entry_points.txt +2 -0
- salt_api_cli-1.0.0/salt_api_cli.egg-info/requires.txt +3 -0
- salt_api_cli-1.0.0/salt_api_cli.egg-info/top_level.txt +1 -0
- salt_api_cli-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include README.md
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: salt-api-cli
|
|
3
|
+
Version: 1.0.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
|
+
Provides-Extra: pre-commit
|
|
13
|
+
Requires-Dist: pre-commit; extra == "pre-commit"
|
|
14
|
+
|
|
15
|
+
# salt-api-cli
|
|
16
|
+
|
|
17
|
+
Thin, stdlib-only Python CLI for [salt-api](https://docs.saltproject.io/en/latest/ref/netapi/all/salt.netapi.rest_cherrypy.html).
|
|
18
|
+
|
|
19
|
+
Logs in once with PAM credentials, caches the token in
|
|
20
|
+
`~/.cache/salt-api-cli/token.json`, then invokes salt-api's `local`,
|
|
21
|
+
`runner`, and `wheel` clients over HTTPS. The token auto-refreshes
|
|
22
|
+
when it expires.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
pip install salt-api-cli
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
Configuration is resolved in this order (later sources override earlier):
|
|
33
|
+
|
|
34
|
+
1. `~/.saltapiclirc` — INI file, `[salt-api-cli]` section
|
|
35
|
+
2. Environment variables — `SALT_API_URL`, `SALT_API_USER`, `SALT_API_PASS`, `SALT_API_INSECURE`
|
|
36
|
+
3. Command-line flags — `--url`, `--user`, `--password`, `--insecure`
|
|
37
|
+
|
|
38
|
+
Example `~/.saltapiclirc`:
|
|
39
|
+
|
|
40
|
+
```ini
|
|
41
|
+
[salt-api-cli]
|
|
42
|
+
url = https://salt.example.com
|
|
43
|
+
user = salt_api
|
|
44
|
+
password = secret
|
|
45
|
+
insecure = false
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`SALT_API_INSECURE=1` (or `insecure = true` in the config) skips TLS
|
|
49
|
+
certificate verification.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
# Local client — fan out to minions
|
|
55
|
+
salt-api-cli local '*' test.ping
|
|
56
|
+
salt-api-cli local 'bml*' cmd.run 'whoami'
|
|
57
|
+
salt-api-cli local 'bml1' cmd.run 'Get-Date' shell=powershell
|
|
58
|
+
|
|
59
|
+
# Runner client (master-side: manage.status, jobs.list_jobs, ...)
|
|
60
|
+
salt-api-cli runner manage.status
|
|
61
|
+
salt-api-cli runner jobs.list_jobs
|
|
62
|
+
|
|
63
|
+
# Wheel client (master-side, low-level)
|
|
64
|
+
salt-api-cli wheel key.list_all
|
|
65
|
+
|
|
66
|
+
# Key management (high-level wrapper around the wheel client)
|
|
67
|
+
salt-api-cli keys list
|
|
68
|
+
salt-api-cli keys accept <id-or-glob>
|
|
69
|
+
salt-api-cli keys accept-all
|
|
70
|
+
salt-api-cli keys reject <id-or-glob>
|
|
71
|
+
salt-api-cli keys delete <id-or-glob>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Any `key=value` argument is parsed as a kwarg to the salt function;
|
|
75
|
+
anything else is positional.
|
|
76
|
+
|
|
77
|
+
You can also invoke the CLI as a module: `python -m salt_api_cli ...`.
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
This project is licensed under the terms of the MIT license.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# salt-api-cli
|
|
2
|
+
|
|
3
|
+
Thin, stdlib-only Python CLI for [salt-api](https://docs.saltproject.io/en/latest/ref/netapi/all/salt.netapi.rest_cherrypy.html).
|
|
4
|
+
|
|
5
|
+
Logs in once with PAM credentials, caches the token in
|
|
6
|
+
`~/.cache/salt-api-cli/token.json`, then invokes salt-api's `local`,
|
|
7
|
+
`runner`, and `wheel` clients over HTTPS. The token auto-refreshes
|
|
8
|
+
when it expires.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
pip install salt-api-cli
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
Configuration is resolved in this order (later sources override earlier):
|
|
19
|
+
|
|
20
|
+
1. `~/.saltapiclirc` — INI file, `[salt-api-cli]` section
|
|
21
|
+
2. Environment variables — `SALT_API_URL`, `SALT_API_USER`, `SALT_API_PASS`, `SALT_API_INSECURE`
|
|
22
|
+
3. Command-line flags — `--url`, `--user`, `--password`, `--insecure`
|
|
23
|
+
|
|
24
|
+
Example `~/.saltapiclirc`:
|
|
25
|
+
|
|
26
|
+
```ini
|
|
27
|
+
[salt-api-cli]
|
|
28
|
+
url = https://salt.example.com
|
|
29
|
+
user = salt_api
|
|
30
|
+
password = secret
|
|
31
|
+
insecure = false
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`SALT_API_INSECURE=1` (or `insecure = true` in the config) skips TLS
|
|
35
|
+
certificate verification.
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
# Local client — fan out to minions
|
|
41
|
+
salt-api-cli local '*' test.ping
|
|
42
|
+
salt-api-cli local 'bml*' cmd.run 'whoami'
|
|
43
|
+
salt-api-cli local 'bml1' cmd.run 'Get-Date' shell=powershell
|
|
44
|
+
|
|
45
|
+
# Runner client (master-side: manage.status, jobs.list_jobs, ...)
|
|
46
|
+
salt-api-cli runner manage.status
|
|
47
|
+
salt-api-cli runner jobs.list_jobs
|
|
48
|
+
|
|
49
|
+
# Wheel client (master-side, low-level)
|
|
50
|
+
salt-api-cli wheel key.list_all
|
|
51
|
+
|
|
52
|
+
# Key management (high-level wrapper around the wheel client)
|
|
53
|
+
salt-api-cli keys list
|
|
54
|
+
salt-api-cli keys accept <id-or-glob>
|
|
55
|
+
salt-api-cli keys accept-all
|
|
56
|
+
salt-api-cli keys reject <id-or-glob>
|
|
57
|
+
salt-api-cli keys delete <id-or-glob>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Any `key=value` argument is parsed as a kwarg to the salt function;
|
|
61
|
+
anything else is positional.
|
|
62
|
+
|
|
63
|
+
You can also invoke the CLI as a module: `python -m salt_api_cli ...`.
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
This project is licensed under the terms of the MIT license.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=70.0.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "salt-api-cli"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
authors = [{ name = "Pradish Bijukchhe", email = "pradish@sandbox.com.np" }]
|
|
9
|
+
description = "CLI to access salt-api"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = []
|
|
13
|
+
classifiers = ["Programming Language :: Python :: 3"]
|
|
14
|
+
dependencies = []
|
|
15
|
+
dynamic = ["version"]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
pre-commit = ["pre-commit"]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
salt-api-cli = "salt_api_cli.cli:main"
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/sandbox-pokhara/saltapi-cli"
|
|
25
|
+
Issues = "https://github.com/sandbox-pokhara/saltapi-cli/issues"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
include-package-data = true
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.package-dir]
|
|
31
|
+
"salt_api_cli" = "salt_api_cli"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.dynamic]
|
|
34
|
+
version = { attr = "salt_api_cli.version.__version__" }
|
|
35
|
+
|
|
36
|
+
[tool.ruff.lint]
|
|
37
|
+
select = ["I"]
|
|
38
|
+
|
|
39
|
+
[tool.pyright]
|
|
40
|
+
venvPath = "."
|
|
41
|
+
venv = ".venv"
|
|
42
|
+
include = ["salt_api_cli"]
|
|
43
|
+
typeCheckingMode = "strict"
|
|
File without changes
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""salt-api-cli — thin Python CLI for salt-api.
|
|
2
|
+
|
|
3
|
+
Stdlib-only. 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. Token auto-refreshes when expired.
|
|
6
|
+
|
|
7
|
+
Configuration (later sources override earlier):
|
|
8
|
+
1. ~/.saltapiclirc INI file, [salt-api-cli] section
|
|
9
|
+
2. environment variables SALT_API_URL, SALT_API_USER,
|
|
10
|
+
SALT_API_PASS, SALT_API_INSECURE
|
|
11
|
+
3. command-line flags --url, --user, --password,
|
|
12
|
+
--insecure
|
|
13
|
+
|
|
14
|
+
Any `key=value` argument to local/runner/wheel is parsed as a kwarg to
|
|
15
|
+
the salt function. Anything else is positional.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import configparser
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import ssl
|
|
25
|
+
import sys
|
|
26
|
+
import time
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
|
+
from urllib.error import HTTPError, URLError
|
|
31
|
+
from urllib.parse import urlencode
|
|
32
|
+
from urllib.request import Request, urlopen
|
|
33
|
+
|
|
34
|
+
CONFIG_FILE = Path.home() / ".saltapiclirc"
|
|
35
|
+
CONFIG_SECTION = "salt-api-cli"
|
|
36
|
+
TOKEN_FILE = Path.home() / ".cache" / "salt-api-cli" / "token.json"
|
|
37
|
+
USER_AGENT = "salt-api-cli/1.0 (Mozilla/5.0 compatible)"
|
|
38
|
+
|
|
39
|
+
# Wheel key.list_all groups minion IDs by acceptance state under these keys.
|
|
40
|
+
KEY_STATUS_LABELS = {
|
|
41
|
+
"minions": "Accepted",
|
|
42
|
+
"minions_pre": "Pending",
|
|
43
|
+
"minions_denied": "Denied",
|
|
44
|
+
"minions_rejected": "Rejected",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class Config:
|
|
50
|
+
url: str
|
|
51
|
+
user: str
|
|
52
|
+
password: str
|
|
53
|
+
insecure: bool
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _truthy(value: str) -> bool:
|
|
57
|
+
return value.strip().lower() in ("1", "true", "yes", "on")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _load_config(args: argparse.Namespace) -> Config:
|
|
61
|
+
file_section: dict[str, str] = {}
|
|
62
|
+
if CONFIG_FILE.exists():
|
|
63
|
+
parser = configparser.ConfigParser()
|
|
64
|
+
parser.read(CONFIG_FILE)
|
|
65
|
+
if parser.has_section(CONFIG_SECTION):
|
|
66
|
+
file_section = dict(parser.items(CONFIG_SECTION))
|
|
67
|
+
|
|
68
|
+
url: str = args.url or os.environ.get("SALT_API_URL") or file_section.get("url", "")
|
|
69
|
+
user: str = (
|
|
70
|
+
args.user
|
|
71
|
+
or os.environ.get("SALT_API_USER")
|
|
72
|
+
or file_section.get("user", "salt_api")
|
|
73
|
+
)
|
|
74
|
+
password: str = (
|
|
75
|
+
args.password
|
|
76
|
+
or os.environ.get("SALT_API_PASS")
|
|
77
|
+
or file_section.get("password", "")
|
|
78
|
+
)
|
|
79
|
+
insecure: bool = (
|
|
80
|
+
args.insecure
|
|
81
|
+
or os.environ.get("SALT_API_INSECURE") == "1"
|
|
82
|
+
or _truthy(file_section.get("insecure", ""))
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if not url:
|
|
86
|
+
sys.exit(
|
|
87
|
+
"salt-api URL not set (use --url, SALT_API_URL, or url= in ~/.saltapiclirc)"
|
|
88
|
+
)
|
|
89
|
+
if not password:
|
|
90
|
+
sys.exit(
|
|
91
|
+
"salt-api password not set "
|
|
92
|
+
"(use --password, SALT_API_PASS, or password= in ~/.saltapiclirc)"
|
|
93
|
+
)
|
|
94
|
+
return Config(
|
|
95
|
+
url=url.rstrip("/"),
|
|
96
|
+
user=user,
|
|
97
|
+
password=password,
|
|
98
|
+
insecure=insecure,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _ssl_ctx(cfg: Config) -> ssl.SSLContext | None:
|
|
103
|
+
if not cfg.insecure:
|
|
104
|
+
return None
|
|
105
|
+
ctx = ssl.create_default_context()
|
|
106
|
+
ctx.check_hostname = False
|
|
107
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
108
|
+
return ctx
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _http(req: Request, cfg: Config) -> dict[str, Any]:
|
|
112
|
+
try:
|
|
113
|
+
with urlopen(req, context=_ssl_ctx(cfg), timeout=30) as resp:
|
|
114
|
+
data: Any = json.loads(resp.read())
|
|
115
|
+
return data
|
|
116
|
+
except HTTPError as e:
|
|
117
|
+
body = e.read().decode(errors="replace")
|
|
118
|
+
sys.exit(f"salt-api {e.code} {e.reason}: {body}")
|
|
119
|
+
except URLError as e:
|
|
120
|
+
sys.exit(f"salt-api unreachable: {e.reason}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _login(cfg: Config) -> dict[str, Any]:
|
|
124
|
+
body = urlencode(
|
|
125
|
+
{"username": cfg.user, "password": cfg.password, "eauth": "pam"}
|
|
126
|
+
).encode()
|
|
127
|
+
req = Request(
|
|
128
|
+
f"{cfg.url}/login",
|
|
129
|
+
data=body,
|
|
130
|
+
headers={"Accept": "application/json", "User-Agent": USER_AGENT},
|
|
131
|
+
)
|
|
132
|
+
data = _http(req, cfg)
|
|
133
|
+
info: dict[str, Any] = data["return"][0]
|
|
134
|
+
if "token" not in info:
|
|
135
|
+
sys.exit(f"login failed: {info}")
|
|
136
|
+
return info
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _get_token(cfg: Config) -> str:
|
|
140
|
+
if TOKEN_FILE.exists():
|
|
141
|
+
try:
|
|
142
|
+
cached: dict[str, Any] = json.loads(TOKEN_FILE.read_text())
|
|
143
|
+
if cached.get("expire", 0) > time.time() + 60:
|
|
144
|
+
return str(cached["token"])
|
|
145
|
+
except (json.JSONDecodeError, OSError, AttributeError, TypeError):
|
|
146
|
+
pass
|
|
147
|
+
info = _login(cfg)
|
|
148
|
+
TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
TOKEN_FILE.write_text(json.dumps(info))
|
|
150
|
+
try:
|
|
151
|
+
os.chmod(TOKEN_FILE, 0o600)
|
|
152
|
+
except OSError:
|
|
153
|
+
pass
|
|
154
|
+
return str(info["token"])
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _call(cfg: Config, client: str, **kwargs: Any) -> dict[str, Any]:
|
|
158
|
+
payload = [{"client": client, **kwargs}]
|
|
159
|
+
req = Request(
|
|
160
|
+
cfg.url,
|
|
161
|
+
data=json.dumps(payload).encode(),
|
|
162
|
+
headers={
|
|
163
|
+
"Accept": "application/json",
|
|
164
|
+
"Content-Type": "application/json",
|
|
165
|
+
"X-Auth-Token": _get_token(cfg),
|
|
166
|
+
"User-Agent": USER_AGENT,
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
return _http(req, cfg)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _split_args(args: list[str]) -> tuple[list[str], dict[str, str]]:
|
|
173
|
+
"""Split positional args from key=value kwargs."""
|
|
174
|
+
pos: list[str] = []
|
|
175
|
+
kw: dict[str, str] = {}
|
|
176
|
+
for a in args:
|
|
177
|
+
if "=" in a and not a.startswith("="):
|
|
178
|
+
k, v = a.split("=", 1)
|
|
179
|
+
if k.isidentifier():
|
|
180
|
+
kw[k] = v
|
|
181
|
+
continue
|
|
182
|
+
pos.append(a)
|
|
183
|
+
return pos, kw
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _print_local_result(result: dict[str, Any]) -> None:
|
|
187
|
+
"""One row per minion. JSON-encode anything that isn't a scalar so
|
|
188
|
+
multi-value returns (e.g. cmd.run dicts) stay on one line."""
|
|
189
|
+
ret_list: Any = result.get("return")
|
|
190
|
+
if not ret_list:
|
|
191
|
+
print(json.dumps(result, indent=2))
|
|
192
|
+
return
|
|
193
|
+
ret: dict[str, Any] = ret_list[0]
|
|
194
|
+
if not ret:
|
|
195
|
+
print("(no minions responded)")
|
|
196
|
+
return
|
|
197
|
+
width = max(len(m) for m in ret)
|
|
198
|
+
for minion in sorted(ret):
|
|
199
|
+
val = ret[minion]
|
|
200
|
+
if isinstance(val, (str, int, float, bool)) or val is None:
|
|
201
|
+
print(f"{minion:<{width}} {val}")
|
|
202
|
+
else:
|
|
203
|
+
print(f"{minion:<{width}} {json.dumps(val)}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _run_local(cfg: Config, args: argparse.Namespace) -> None:
|
|
207
|
+
pos, kw = _split_args(list(args.args))
|
|
208
|
+
payload: dict[str, Any] = {"tgt": args.target, "fun": args.function, "arg": pos}
|
|
209
|
+
if kw:
|
|
210
|
+
payload["kwarg"] = kw
|
|
211
|
+
_print_local_result(_call(cfg, "local", **payload))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _run_client(cfg: Config, client: str, args: argparse.Namespace) -> None:
|
|
215
|
+
pos, kw = _split_args(list(args.args))
|
|
216
|
+
payload: dict[str, Any] = {"fun": args.function, "arg": pos}
|
|
217
|
+
if kw:
|
|
218
|
+
payload["kwarg"] = kw
|
|
219
|
+
print(json.dumps(_call(cfg, client, **payload), indent=2))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _run_keys(cfg: Config, args: argparse.Namespace) -> None:
|
|
223
|
+
action: str = args.action
|
|
224
|
+
if action == "list":
|
|
225
|
+
result = _call(cfg, "wheel", fun="key.list_all")
|
|
226
|
+
data: dict[str, Any] = result["return"][0]["data"]["return"]
|
|
227
|
+
for status_key, label in KEY_STATUS_LABELS.items():
|
|
228
|
+
keys: list[str] = data.get(status_key, [])
|
|
229
|
+
print(f"{label} ({len(keys)}):")
|
|
230
|
+
for k in keys:
|
|
231
|
+
print(f" {k}")
|
|
232
|
+
print()
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
fun_map = {
|
|
236
|
+
"accept": "key.accept",
|
|
237
|
+
"accept-all": "key.accept",
|
|
238
|
+
"reject": "key.reject",
|
|
239
|
+
"delete": "key.delete",
|
|
240
|
+
}
|
|
241
|
+
match: str = "*" if action == "accept-all" else args.match
|
|
242
|
+
result = _call(cfg, "wheel", fun=fun_map[action], match=match)
|
|
243
|
+
data = result["return"][0]["data"]
|
|
244
|
+
if not data.get("success"):
|
|
245
|
+
sys.exit(f"failed: {data}")
|
|
246
|
+
changed: dict[str, list[str]] = data.get("return", {})
|
|
247
|
+
if not changed:
|
|
248
|
+
print("(no keys changed)")
|
|
249
|
+
return
|
|
250
|
+
for status_key, ids in changed.items():
|
|
251
|
+
label = KEY_STATUS_LABELS.get(status_key, status_key)
|
|
252
|
+
print(f"{label}: {', '.join(ids) if ids else '(none)'}")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
256
|
+
parser = argparse.ArgumentParser(
|
|
257
|
+
prog="salt-api-cli",
|
|
258
|
+
description="Thin Python CLI for salt-api.",
|
|
259
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
260
|
+
epilog=(
|
|
261
|
+
"examples:\n"
|
|
262
|
+
" salt-api-cli local '*' test.ping\n"
|
|
263
|
+
" salt-api-cli local 'bml*' cmd.run 'whoami'\n"
|
|
264
|
+
" salt-api-cli local 'bml1' cmd.run 'Get-Date' shell=powershell\n"
|
|
265
|
+
" salt-api-cli runner manage.status\n"
|
|
266
|
+
" salt-api-cli wheel key.list_all\n"
|
|
267
|
+
" salt-api-cli keys list\n"
|
|
268
|
+
" salt-api-cli keys accept '<id-or-glob>'\n"
|
|
269
|
+
" salt-api-cli keys accept-all\n"
|
|
270
|
+
),
|
|
271
|
+
)
|
|
272
|
+
parser.add_argument("--url", help="salt-api base URL")
|
|
273
|
+
parser.add_argument("--user", help="PAM username")
|
|
274
|
+
parser.add_argument("--password", help="PAM password")
|
|
275
|
+
parser.add_argument(
|
|
276
|
+
"--insecure",
|
|
277
|
+
action="store_true",
|
|
278
|
+
help="skip TLS certificate verification",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
282
|
+
|
|
283
|
+
p_local = sub.add_parser("local", help="run a function on minions")
|
|
284
|
+
p_local.add_argument("target", help="minion target (id or glob)")
|
|
285
|
+
p_local.add_argument("function", help="salt function (e.g. test.ping)")
|
|
286
|
+
p_local.add_argument(
|
|
287
|
+
"args", nargs=argparse.REMAINDER, help="positional and key=value args"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
p_runner = sub.add_parser("runner", help="invoke a master-side runner")
|
|
291
|
+
p_runner.add_argument("function")
|
|
292
|
+
p_runner.add_argument("args", nargs=argparse.REMAINDER)
|
|
293
|
+
|
|
294
|
+
p_wheel = sub.add_parser("wheel", help="invoke a master-side wheel function")
|
|
295
|
+
p_wheel.add_argument("function")
|
|
296
|
+
p_wheel.add_argument("args", nargs=argparse.REMAINDER)
|
|
297
|
+
|
|
298
|
+
p_keys = sub.add_parser("keys", help="manage minion keys")
|
|
299
|
+
keys_sub = p_keys.add_subparsers(dest="action", required=True)
|
|
300
|
+
keys_sub.add_parser("list", help="show keys grouped by status")
|
|
301
|
+
p_accept = keys_sub.add_parser("accept", help="accept a key by id or glob")
|
|
302
|
+
p_accept.add_argument("match")
|
|
303
|
+
keys_sub.add_parser("accept-all", help="accept every pending key")
|
|
304
|
+
p_reject = keys_sub.add_parser("reject", help="reject a key by id or glob")
|
|
305
|
+
p_reject.add_argument("match")
|
|
306
|
+
p_delete = keys_sub.add_parser("delete", help="delete a key by id or glob")
|
|
307
|
+
p_delete.add_argument("match")
|
|
308
|
+
|
|
309
|
+
return parser
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def main() -> None:
|
|
313
|
+
parser = _build_parser()
|
|
314
|
+
args = parser.parse_args()
|
|
315
|
+
cfg = _load_config(args)
|
|
316
|
+
|
|
317
|
+
if args.command == "local":
|
|
318
|
+
_run_local(cfg, args)
|
|
319
|
+
elif args.command == "runner":
|
|
320
|
+
_run_client(cfg, "runner", args)
|
|
321
|
+
elif args.command == "wheel":
|
|
322
|
+
_run_client(cfg, "wheel", args)
|
|
323
|
+
elif args.command == "keys":
|
|
324
|
+
_run_keys(cfg, args)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
if __name__ == "__main__":
|
|
328
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: salt-api-cli
|
|
3
|
+
Version: 1.0.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
|
+
Provides-Extra: pre-commit
|
|
13
|
+
Requires-Dist: pre-commit; extra == "pre-commit"
|
|
14
|
+
|
|
15
|
+
# salt-api-cli
|
|
16
|
+
|
|
17
|
+
Thin, stdlib-only Python CLI for [salt-api](https://docs.saltproject.io/en/latest/ref/netapi/all/salt.netapi.rest_cherrypy.html).
|
|
18
|
+
|
|
19
|
+
Logs in once with PAM credentials, caches the token in
|
|
20
|
+
`~/.cache/salt-api-cli/token.json`, then invokes salt-api's `local`,
|
|
21
|
+
`runner`, and `wheel` clients over HTTPS. The token auto-refreshes
|
|
22
|
+
when it expires.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
pip install salt-api-cli
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
Configuration is resolved in this order (later sources override earlier):
|
|
33
|
+
|
|
34
|
+
1. `~/.saltapiclirc` — INI file, `[salt-api-cli]` section
|
|
35
|
+
2. Environment variables — `SALT_API_URL`, `SALT_API_USER`, `SALT_API_PASS`, `SALT_API_INSECURE`
|
|
36
|
+
3. Command-line flags — `--url`, `--user`, `--password`, `--insecure`
|
|
37
|
+
|
|
38
|
+
Example `~/.saltapiclirc`:
|
|
39
|
+
|
|
40
|
+
```ini
|
|
41
|
+
[salt-api-cli]
|
|
42
|
+
url = https://salt.example.com
|
|
43
|
+
user = salt_api
|
|
44
|
+
password = secret
|
|
45
|
+
insecure = false
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`SALT_API_INSECURE=1` (or `insecure = true` in the config) skips TLS
|
|
49
|
+
certificate verification.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
# Local client — fan out to minions
|
|
55
|
+
salt-api-cli local '*' test.ping
|
|
56
|
+
salt-api-cli local 'bml*' cmd.run 'whoami'
|
|
57
|
+
salt-api-cli local 'bml1' cmd.run 'Get-Date' shell=powershell
|
|
58
|
+
|
|
59
|
+
# Runner client (master-side: manage.status, jobs.list_jobs, ...)
|
|
60
|
+
salt-api-cli runner manage.status
|
|
61
|
+
salt-api-cli runner jobs.list_jobs
|
|
62
|
+
|
|
63
|
+
# Wheel client (master-side, low-level)
|
|
64
|
+
salt-api-cli wheel key.list_all
|
|
65
|
+
|
|
66
|
+
# Key management (high-level wrapper around the wheel client)
|
|
67
|
+
salt-api-cli keys list
|
|
68
|
+
salt-api-cli keys accept <id-or-glob>
|
|
69
|
+
salt-api-cli keys accept-all
|
|
70
|
+
salt-api-cli keys reject <id-or-glob>
|
|
71
|
+
salt-api-cli keys delete <id-or-glob>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Any `key=value` argument is parsed as a kwarg to the salt function;
|
|
75
|
+
anything else is positional.
|
|
76
|
+
|
|
77
|
+
You can also invoke the CLI as a module: `python -m salt_api_cli ...`.
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
This project is licensed under the terms of the MIT license.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
salt_api_cli/__init__.py
|
|
5
|
+
salt_api_cli/__main__.py
|
|
6
|
+
salt_api_cli/cli.py
|
|
7
|
+
salt_api_cli/py.typed
|
|
8
|
+
salt_api_cli/version.py
|
|
9
|
+
salt_api_cli.egg-info/PKG-INFO
|
|
10
|
+
salt_api_cli.egg-info/SOURCES.txt
|
|
11
|
+
salt_api_cli.egg-info/dependency_links.txt
|
|
12
|
+
salt_api_cli.egg-info/entry_points.txt
|
|
13
|
+
salt_api_cli.egg-info/requires.txt
|
|
14
|
+
salt_api_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
salt_api_cli
|