a4i 0.0.1__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.
- a4i-0.0.1/.github/dependabot.yml +7 -0
- a4i-0.0.1/.github/workflows/publish.yml +36 -0
- a4i-0.0.1/.github/workflows/test.yml +65 -0
- a4i-0.0.1/.gitignore +15 -0
- a4i-0.0.1/LICENSE +21 -0
- a4i-0.0.1/PKG-INFO +247 -0
- a4i-0.0.1/README.md +210 -0
- a4i-0.0.1/pyproject.toml +60 -0
- a4i-0.0.1/src/a4i/__init__.py +84 -0
- a4i-0.0.1/src/a4i/__main__.py +8 -0
- a4i-0.0.1/src/a4i/cli.py +836 -0
- a4i-0.0.1/src/a4i/client.py +593 -0
- a4i-0.0.1/src/a4i/completion.py +310 -0
- a4i-0.0.1/src/a4i/config.py +78 -0
- a4i-0.0.1/src/a4i/daemon.py +252 -0
- a4i-0.0.1/src/a4i/diff.py +250 -0
- a4i-0.0.1/src/a4i/dry_run.py +141 -0
- a4i-0.0.1/src/a4i/errors.py +123 -0
- a4i-0.0.1/src/a4i/ipc.py +328 -0
- a4i-0.0.1/src/a4i/mcp/__init__.py +21 -0
- a4i-0.0.1/src/a4i/mcp/guides.py +308 -0
- a4i-0.0.1/src/a4i/mcp/server.py +230 -0
- a4i-0.0.1/src/a4i/mcp/tools.py +562 -0
- a4i-0.0.1/src/a4i/merge.py +224 -0
- a4i-0.0.1/src/a4i/metadata/__init__.py +170 -0
- a4i-0.0.1/src/a4i/metadata/classes.txt +15637 -0
- a4i-0.0.1/src/a4i/metadata/model.idx +17864 -0
- a4i-0.0.1/src/a4i/metadata/model.jsonl +17864 -0
- a4i-0.0.1/src/a4i/metadata/rn_formats.txt +2806 -0
- a4i-0.0.1/src/a4i/metadata/search.txt +17864 -0
- a4i-0.0.1/src/a4i/mo.py +394 -0
- a4i-0.0.1/src/a4i/output.py +490 -0
- a4i-0.0.1/src/a4i/query.py +182 -0
- a4i-0.0.1/src/a4i/session.py +590 -0
- a4i-0.0.1/src/a4i/transport.py +100 -0
- a4i-0.0.1/tests/apic_mock.py +300 -0
- a4i-0.0.1/tests/conftest.py +16 -0
- a4i-0.0.1/tests/test_async_client.py +123 -0
- a4i-0.0.1/tests/test_async_session.py +120 -0
- a4i-0.0.1/tests/test_awaited.py +165 -0
- a4i-0.0.1/tests/test_cli.py +865 -0
- a4i-0.0.1/tests/test_client.py +539 -0
- a4i-0.0.1/tests/test_completion.py +327 -0
- a4i-0.0.1/tests/test_config.py +122 -0
- a4i-0.0.1/tests/test_daemon.py +227 -0
- a4i-0.0.1/tests/test_diff.py +757 -0
- a4i-0.0.1/tests/test_dry_run.py +300 -0
- a4i-0.0.1/tests/test_errors.py +119 -0
- a4i-0.0.1/tests/test_ipc.py +188 -0
- a4i-0.0.1/tests/test_mcp.py +624 -0
- a4i-0.0.1/tests/test_merge.py +333 -0
- a4i-0.0.1/tests/test_mo.py +136 -0
- a4i-0.0.1/tests/test_output.py +210 -0
- a4i-0.0.1/tests/test_query.py +31 -0
- a4i-0.0.1/tests/test_session.py +419 -0
- a4i-0.0.1/tools/gen_metadata.py +491 -0
- a4i-0.0.1/uv.lock +269 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
# Only release tags, so a stray working tag never reaches PyPI.
|
|
6
|
+
tags: ["v[0-9]+.[0-9]+.[0-9]+*"]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
name: publish to PyPI
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: pypi
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
# Required for PyPI Trusted Publishing (OIDC) and PEP 740 attestations.
|
|
19
|
+
id-token: write
|
|
20
|
+
steps:
|
|
21
|
+
- name: Check out
|
|
22
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
23
|
+
with:
|
|
24
|
+
# hatch-vcs reads the version from git tags, which a shallow clone lacks.
|
|
25
|
+
fetch-depth: 0
|
|
26
|
+
|
|
27
|
+
- name: Set up uv
|
|
28
|
+
uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0
|
|
29
|
+
with:
|
|
30
|
+
python-version: "3.13"
|
|
31
|
+
|
|
32
|
+
- name: Build sdist and wheel
|
|
33
|
+
run: uv build
|
|
34
|
+
|
|
35
|
+
- name: Publish
|
|
36
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags-ignore: ["v[0-9]+.[0-9]+.[0-9]+*"]
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
# Supersede an in-flight run when the same ref is pushed again.
|
|
13
|
+
concurrency:
|
|
14
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
lint:
|
|
19
|
+
name: lint
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- name: Check out
|
|
23
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
24
|
+
|
|
25
|
+
- name: Set up uv
|
|
26
|
+
uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.13"
|
|
29
|
+
enable-cache: true
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: uv sync --locked --all-groups
|
|
33
|
+
|
|
34
|
+
- name: ruff check
|
|
35
|
+
run: uv run ruff check
|
|
36
|
+
|
|
37
|
+
- name: ruff format
|
|
38
|
+
run: uv run ruff format --check
|
|
39
|
+
|
|
40
|
+
- name: ty check
|
|
41
|
+
run: uv run ty check
|
|
42
|
+
|
|
43
|
+
test:
|
|
44
|
+
name: test (${{ matrix.python-version }})
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
strategy:
|
|
47
|
+
# One failing interpreter should not hide the result of the others.
|
|
48
|
+
fail-fast: false
|
|
49
|
+
matrix:
|
|
50
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
51
|
+
steps:
|
|
52
|
+
- name: Check out
|
|
53
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
54
|
+
|
|
55
|
+
- name: Set up uv
|
|
56
|
+
uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0
|
|
57
|
+
with:
|
|
58
|
+
python-version: ${{ matrix.python-version }}
|
|
59
|
+
enable-cache: true
|
|
60
|
+
|
|
61
|
+
- name: Install dependencies
|
|
62
|
+
run: uv sync --locked --all-groups
|
|
63
|
+
|
|
64
|
+
- name: pytest
|
|
65
|
+
run: uv run pytest
|
a4i-0.0.1/.gitignore
ADDED
a4i-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 minefuto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
a4i-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: a4i
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: CLI/MCP/Python Library for the Cisco ACI REST API
|
|
5
|
+
Author-email: minefuto <46558834+minefuto@users.noreply.github.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 minefuto
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Classifier: Programming Language :: Python :: 3
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
33
|
+
Requires-Python: >=3.11
|
|
34
|
+
Requires-Dist: httpx2>=2.9.1
|
|
35
|
+
Requires-Dist: rich>=15.0.0
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# aciapi(a4i)
|
|
39
|
+
|
|
40
|
+
[](https://github.com/minefuto/a4i/actions/workflows/test.yml)
|
|
41
|
+
[](https://pypi.org/project/a4i/)
|
|
42
|
+
|
|
43
|
+
CLI/MCP/Python Library for the Cisco ACI REST API.
|
|
44
|
+
|
|
45
|
+
- **The token is never written to disk.** `login` hands it to a small per-user
|
|
46
|
+
daemon that holds it in memory, behind a Unix domain socket, so it survives
|
|
47
|
+
across short-lived CLI invocations without touching the filesystem.
|
|
48
|
+
- **The ACI object model ships with it.** `search` and `describe` answer what a
|
|
49
|
+
class is called and what a body may set on it, without an APIC and without a
|
|
50
|
+
login.
|
|
51
|
+
- **`merge` and `diff` compare a fabric against an intended configuration**,
|
|
52
|
+
reporting both what the configuration asks for and the fabric lacks, and what
|
|
53
|
+
the fabric carries and the configuration never mentions.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
pip install a4i
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Shell completion is printed to standard output; add one line to your shell's
|
|
62
|
+
startup file:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
eval "$(a4i generate-shell-completion zsh)" # ~/.zshrc, after compinit
|
|
66
|
+
eval "$(a4i generate-shell-completion bash)" # ~/.bashrc
|
|
67
|
+
a4i generate-shell-completion fish | source # ~/.config/fish/config.fish
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
a4i login apic1.example.com -u admin # prompts for password; -k if self-signed
|
|
74
|
+
a4i get class fvTenant # class query
|
|
75
|
+
a4i get mo uni/tn-common # MO query, by DN
|
|
76
|
+
a4i get class fvTenant --query-target subtree --rsp-subtree full
|
|
77
|
+
a4i get class l1PhysIf --node leaf101.example.com # query a switch with the same token
|
|
78
|
+
echo '{"fvTenant":{"attributes":{"name":"demo"}}}' | a4i post mo uni/tn-demo
|
|
79
|
+
a4i logout # drop the in-memory session
|
|
80
|
+
a4i daemon status # is a token held, and for how long
|
|
81
|
+
a4i login apic1.example.com -u admin --read-only # a session that will refuse every POST
|
|
82
|
+
a4i mcp # serve MCP on stdio for an LLM client
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`get`, `post` and `list` each take a `class` or an `mo` subcommand, so a DN
|
|
86
|
+
needs no leading `/` and a class name is never mistaken for one. Every `get`
|
|
87
|
+
option is named after the ACI query parameter it sets, so a parameter read in
|
|
88
|
+
the APIC REST API documentation can be typed as-is -- `a4i get class --help`
|
|
89
|
+
lists them.
|
|
90
|
+
|
|
91
|
+
### Reading the model
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
a4i search 'bridge domain' # which class is that, by name
|
|
95
|
+
a4i describe fvBD # what a body may set on it
|
|
96
|
+
a4i list class fvT # class names starting with fvT
|
|
97
|
+
a4i list mo uni/tn-common # the MOs one level under that DN
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`search`, `describe` and `list class` read the bundled dictionary, so they need
|
|
101
|
+
neither a login nor a daemon. `list mo` asks the APIC for one level of children,
|
|
102
|
+
so it needs a session.
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
$ a4i describe fvCtx
|
|
106
|
+
fvCtx VRF
|
|
107
|
+
The private layer 3 network context that belongs to a specific tenant or is
|
|
108
|
+
shared.
|
|
109
|
+
|
|
110
|
+
rn ctx-{name}
|
|
111
|
+
dn uni/tn-{name}/ctx-{name}
|
|
112
|
+
in fvTenant
|
|
113
|
+
|
|
114
|
+
properties (13 settable, 14 read-only hidden)
|
|
115
|
+
descr string:Basic (0-128) Specifies a descriptio…
|
|
116
|
+
ipDataPlaneLearning disabled|enabled = enabled
|
|
117
|
+
name* string:Basic (1-64) A name for the network…
|
|
118
|
+
pcEnfDir egress|ingress|mixed = ingress Policy Control Enforce…
|
|
119
|
+
pcEnfPref enforced|unenforced = enforced
|
|
120
|
+
…
|
|
121
|
+
children (42) --children to list them
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
A `*` marks a naming property -- the one the RN is built from. The middle column
|
|
125
|
+
is what the property accepts, with the default after `=`. `-a` spells out the
|
|
126
|
+
read-only properties, `--children` the classes that may hang under this one, and
|
|
127
|
+
`--json` prints the underlying record instead of the layout.
|
|
128
|
+
|
|
129
|
+
### Comparing a configuration
|
|
130
|
+
|
|
131
|
+
`merge` folds a configuration written across several files into the one body
|
|
132
|
+
that `diff` and `post` each take, later files winning attribute by attribute.
|
|
133
|
+
`diff` then compares that configuration against everything the fabric has under
|
|
134
|
+
`uni`.
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
a4i merge ./configs/ -o merged.json # every *.json, in path order
|
|
138
|
+
a4i merge ./configs/ | a4i diff
|
|
139
|
+
a4i merge ./configs/ | a4i diff --exclude uni/tn-common --exclude uni/infra
|
|
140
|
+
a4i post mo uni/tn-demo --dry-run '{"fvTenant":{"attributes":{"descr":"prod"}}}'
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
- fvTenant uni/tn-common (extra: 2 child MOs)
|
|
145
|
+
- descr: ""
|
|
146
|
+
- name: "common"
|
|
147
|
+
|
|
148
|
+
~ fvBD uni/tn-demo/BD-bd1
|
|
149
|
+
~ mtu: "1500" -> "9000"
|
|
150
|
+
|
|
151
|
+
+ fvTenant uni/tn-new (missing: 2 child MOs)
|
|
152
|
+
+ descr: "added"
|
|
153
|
+
+ name: "new"
|
|
154
|
+
|
|
155
|
+
1 missing, 1 modified, 1 extra
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`+` is an MO the configuration asks for and the fabric does not have, `-` one
|
|
159
|
+
the fabric has and the configuration does not mention, and `~` one whose
|
|
160
|
+
attributes differ. A wholly missing or wholly extra subtree is reported as its
|
|
161
|
+
top MO with the MOs below it counted; `--expand` lists every one of them.
|
|
162
|
+
|
|
163
|
+
The configuration is taken to describe the whole of `uni`, so everything it
|
|
164
|
+
leaves out is `extra` -- including `tn-common`, `tn-infra`, `tn-mgmt` and the
|
|
165
|
+
policies the APIC creates for itself. `--exclude` is how the rest is quietened:
|
|
166
|
+
it takes a DN, or a pattern whose `*` matches within one RN, and is repeatable.
|
|
167
|
+
|
|
168
|
+
`post --dry-run` reads the same way over a single POST: it fetches the subtree
|
|
169
|
+
the body targets, prints what would change, and sends nothing. Both commands say
|
|
170
|
+
in their exit code whether anything would change:
|
|
171
|
+
|
|
172
|
+
| Code | Meaning |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| `0` | the fabric matches / posting this body would change nothing |
|
|
175
|
+
| `2` | it differs / the body would change something |
|
|
176
|
+
| `1` | the command itself failed (not logged in, bad JSON, unknown DN) |
|
|
177
|
+
|
|
178
|
+
## MCP server
|
|
179
|
+
|
|
180
|
+
`a4i mcp` speaks the Model Context Protocol on stdin and stdout, so an LLM
|
|
181
|
+
client can read and write the fabric through the session you already logged in.
|
|
182
|
+
Register it with the client, then log in from a terminal as usual -- there is no
|
|
183
|
+
login tool, because this server never handles a password.
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{"mcpServers": {"a4i": {"command": "a4i", "args": ["mcp"]}}}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
| Tool | What it does |
|
|
190
|
+
| --- | --- |
|
|
191
|
+
| `search` | find a class by what it is called |
|
|
192
|
+
| `describe` | one class from the bundled model, as a JSON record |
|
|
193
|
+
| `list` | class names by prefix, or the DNs one level under a DN |
|
|
194
|
+
| `get` | a class or MO query, with every query option under its own name |
|
|
195
|
+
| `dry_run` | what a POST would change, sending nothing |
|
|
196
|
+
| `post` | POST a body |
|
|
197
|
+
| `merge` | several bodies or paths folded into one |
|
|
198
|
+
| `diff` | the fabric compared against one configuration |
|
|
199
|
+
|
|
200
|
+
| Resource | Contents |
|
|
201
|
+
| --- | --- |
|
|
202
|
+
| `a4i://guide/post-body` | how an ACI body nests, how a child MO gets its DN, what `status` does |
|
|
203
|
+
| `a4i://guide/query` | class against MO queries, the two subtree controls, keeping a response small |
|
|
204
|
+
| `a4i://guide/workflow` | the order: search or list, describe, get, dry run, post |
|
|
205
|
+
| `a4i://guide/limits` | where the bundled model, the dry run and the diff each stop short |
|
|
206
|
+
|
|
207
|
+
A `get` whose response would exceed 64 KB is refused, with the total count and
|
|
208
|
+
the ways to narrow it; `A4I_MCP_MAX_BYTES` raises or lowers that. On a
|
|
209
|
+
`--read-only` session, `post` is not offered at all.
|
|
210
|
+
|
|
211
|
+
## Python library
|
|
212
|
+
|
|
213
|
+
A `Client` holds its own session, so no daemon is involved: it logs in itself
|
|
214
|
+
and keeps the token in memory for as long as it lives.
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
import a4i
|
|
218
|
+
from a4i.merge import merge
|
|
219
|
+
|
|
220
|
+
with a4i.Client("apic1.example.com", verify=False) as client:
|
|
221
|
+
client.login("admin", password)
|
|
222
|
+
|
|
223
|
+
data = client.get("fvTenant", kind="class", query_target="subtree", rsp_subtree="full")
|
|
224
|
+
client.post("uni/tn-demo", {"fvTenant": {"attributes": {"name": "demo"}}}, kind="mo")
|
|
225
|
+
changes = client.diff(merge(base, override))
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`kind` is the subcommand the CLI takes, and it is required: `"class"` for a
|
|
229
|
+
class name, `"mo"` for a DN. Every other keyword argument is the CLI option of
|
|
230
|
+
the same name with underscores. `verify` is what `-k/--insecure` and `--ca`
|
|
231
|
+
express, `timeout` is `login --timeout`, and `dry_run()`, `diff()` and
|
|
232
|
+
`a4i.merge.merge()` are the commands of those names.
|
|
233
|
+
|
|
234
|
+
`AsyncClient` is `Client` awaited: the same arguments, the same return values
|
|
235
|
+
and the same exceptions, sending the same requests in the same order.
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
async with a4i.AsyncClient("apic1.example.com", verify=False) as client:
|
|
239
|
+
await client.login("admin", password)
|
|
240
|
+
data = await client.get("fvTenant", kind="class")
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
A value ACI does not define raises `ValueError` before anything is sent. A
|
|
244
|
+
failed request raises `a4i.ApicError`, `a4i.NotLoggedInError` or
|
|
245
|
+
`a4i.SessionExpiredError`, all of them `a4i.A4iError`. The token refreshes
|
|
246
|
+
itself once half its lifetime has elapsed, so a long-running script needs
|
|
247
|
+
nothing of its own.
|
a4i-0.0.1/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# aciapi(a4i)
|
|
2
|
+
|
|
3
|
+
[](https://github.com/minefuto/a4i/actions/workflows/test.yml)
|
|
4
|
+
[](https://pypi.org/project/a4i/)
|
|
5
|
+
|
|
6
|
+
CLI/MCP/Python Library for the Cisco ACI REST API.
|
|
7
|
+
|
|
8
|
+
- **The token is never written to disk.** `login` hands it to a small per-user
|
|
9
|
+
daemon that holds it in memory, behind a Unix domain socket, so it survives
|
|
10
|
+
across short-lived CLI invocations without touching the filesystem.
|
|
11
|
+
- **The ACI object model ships with it.** `search` and `describe` answer what a
|
|
12
|
+
class is called and what a body may set on it, without an APIC and without a
|
|
13
|
+
login.
|
|
14
|
+
- **`merge` and `diff` compare a fabric against an intended configuration**,
|
|
15
|
+
reporting both what the configuration asks for and the fabric lacks, and what
|
|
16
|
+
the fabric carries and the configuration never mentions.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
pip install a4i
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Shell completion is printed to standard output; add one line to your shell's
|
|
25
|
+
startup file:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
eval "$(a4i generate-shell-completion zsh)" # ~/.zshrc, after compinit
|
|
29
|
+
eval "$(a4i generate-shell-completion bash)" # ~/.bashrc
|
|
30
|
+
a4i generate-shell-completion fish | source # ~/.config/fish/config.fish
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
a4i login apic1.example.com -u admin # prompts for password; -k if self-signed
|
|
37
|
+
a4i get class fvTenant # class query
|
|
38
|
+
a4i get mo uni/tn-common # MO query, by DN
|
|
39
|
+
a4i get class fvTenant --query-target subtree --rsp-subtree full
|
|
40
|
+
a4i get class l1PhysIf --node leaf101.example.com # query a switch with the same token
|
|
41
|
+
echo '{"fvTenant":{"attributes":{"name":"demo"}}}' | a4i post mo uni/tn-demo
|
|
42
|
+
a4i logout # drop the in-memory session
|
|
43
|
+
a4i daemon status # is a token held, and for how long
|
|
44
|
+
a4i login apic1.example.com -u admin --read-only # a session that will refuse every POST
|
|
45
|
+
a4i mcp # serve MCP on stdio for an LLM client
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`get`, `post` and `list` each take a `class` or an `mo` subcommand, so a DN
|
|
49
|
+
needs no leading `/` and a class name is never mistaken for one. Every `get`
|
|
50
|
+
option is named after the ACI query parameter it sets, so a parameter read in
|
|
51
|
+
the APIC REST API documentation can be typed as-is -- `a4i get class --help`
|
|
52
|
+
lists them.
|
|
53
|
+
|
|
54
|
+
### Reading the model
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
a4i search 'bridge domain' # which class is that, by name
|
|
58
|
+
a4i describe fvBD # what a body may set on it
|
|
59
|
+
a4i list class fvT # class names starting with fvT
|
|
60
|
+
a4i list mo uni/tn-common # the MOs one level under that DN
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`search`, `describe` and `list class` read the bundled dictionary, so they need
|
|
64
|
+
neither a login nor a daemon. `list mo` asks the APIC for one level of children,
|
|
65
|
+
so it needs a session.
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
$ a4i describe fvCtx
|
|
69
|
+
fvCtx VRF
|
|
70
|
+
The private layer 3 network context that belongs to a specific tenant or is
|
|
71
|
+
shared.
|
|
72
|
+
|
|
73
|
+
rn ctx-{name}
|
|
74
|
+
dn uni/tn-{name}/ctx-{name}
|
|
75
|
+
in fvTenant
|
|
76
|
+
|
|
77
|
+
properties (13 settable, 14 read-only hidden)
|
|
78
|
+
descr string:Basic (0-128) Specifies a descriptio…
|
|
79
|
+
ipDataPlaneLearning disabled|enabled = enabled
|
|
80
|
+
name* string:Basic (1-64) A name for the network…
|
|
81
|
+
pcEnfDir egress|ingress|mixed = ingress Policy Control Enforce…
|
|
82
|
+
pcEnfPref enforced|unenforced = enforced
|
|
83
|
+
…
|
|
84
|
+
children (42) --children to list them
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A `*` marks a naming property -- the one the RN is built from. The middle column
|
|
88
|
+
is what the property accepts, with the default after `=`. `-a` spells out the
|
|
89
|
+
read-only properties, `--children` the classes that may hang under this one, and
|
|
90
|
+
`--json` prints the underlying record instead of the layout.
|
|
91
|
+
|
|
92
|
+
### Comparing a configuration
|
|
93
|
+
|
|
94
|
+
`merge` folds a configuration written across several files into the one body
|
|
95
|
+
that `diff` and `post` each take, later files winning attribute by attribute.
|
|
96
|
+
`diff` then compares that configuration against everything the fabric has under
|
|
97
|
+
`uni`.
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
a4i merge ./configs/ -o merged.json # every *.json, in path order
|
|
101
|
+
a4i merge ./configs/ | a4i diff
|
|
102
|
+
a4i merge ./configs/ | a4i diff --exclude uni/tn-common --exclude uni/infra
|
|
103
|
+
a4i post mo uni/tn-demo --dry-run '{"fvTenant":{"attributes":{"descr":"prod"}}}'
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
- fvTenant uni/tn-common (extra: 2 child MOs)
|
|
108
|
+
- descr: ""
|
|
109
|
+
- name: "common"
|
|
110
|
+
|
|
111
|
+
~ fvBD uni/tn-demo/BD-bd1
|
|
112
|
+
~ mtu: "1500" -> "9000"
|
|
113
|
+
|
|
114
|
+
+ fvTenant uni/tn-new (missing: 2 child MOs)
|
|
115
|
+
+ descr: "added"
|
|
116
|
+
+ name: "new"
|
|
117
|
+
|
|
118
|
+
1 missing, 1 modified, 1 extra
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`+` is an MO the configuration asks for and the fabric does not have, `-` one
|
|
122
|
+
the fabric has and the configuration does not mention, and `~` one whose
|
|
123
|
+
attributes differ. A wholly missing or wholly extra subtree is reported as its
|
|
124
|
+
top MO with the MOs below it counted; `--expand` lists every one of them.
|
|
125
|
+
|
|
126
|
+
The configuration is taken to describe the whole of `uni`, so everything it
|
|
127
|
+
leaves out is `extra` -- including `tn-common`, `tn-infra`, `tn-mgmt` and the
|
|
128
|
+
policies the APIC creates for itself. `--exclude` is how the rest is quietened:
|
|
129
|
+
it takes a DN, or a pattern whose `*` matches within one RN, and is repeatable.
|
|
130
|
+
|
|
131
|
+
`post --dry-run` reads the same way over a single POST: it fetches the subtree
|
|
132
|
+
the body targets, prints what would change, and sends nothing. Both commands say
|
|
133
|
+
in their exit code whether anything would change:
|
|
134
|
+
|
|
135
|
+
| Code | Meaning |
|
|
136
|
+
| --- | --- |
|
|
137
|
+
| `0` | the fabric matches / posting this body would change nothing |
|
|
138
|
+
| `2` | it differs / the body would change something |
|
|
139
|
+
| `1` | the command itself failed (not logged in, bad JSON, unknown DN) |
|
|
140
|
+
|
|
141
|
+
## MCP server
|
|
142
|
+
|
|
143
|
+
`a4i mcp` speaks the Model Context Protocol on stdin and stdout, so an LLM
|
|
144
|
+
client can read and write the fabric through the session you already logged in.
|
|
145
|
+
Register it with the client, then log in from a terminal as usual -- there is no
|
|
146
|
+
login tool, because this server never handles a password.
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{"mcpServers": {"a4i": {"command": "a4i", "args": ["mcp"]}}}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
| Tool | What it does |
|
|
153
|
+
| --- | --- |
|
|
154
|
+
| `search` | find a class by what it is called |
|
|
155
|
+
| `describe` | one class from the bundled model, as a JSON record |
|
|
156
|
+
| `list` | class names by prefix, or the DNs one level under a DN |
|
|
157
|
+
| `get` | a class or MO query, with every query option under its own name |
|
|
158
|
+
| `dry_run` | what a POST would change, sending nothing |
|
|
159
|
+
| `post` | POST a body |
|
|
160
|
+
| `merge` | several bodies or paths folded into one |
|
|
161
|
+
| `diff` | the fabric compared against one configuration |
|
|
162
|
+
|
|
163
|
+
| Resource | Contents |
|
|
164
|
+
| --- | --- |
|
|
165
|
+
| `a4i://guide/post-body` | how an ACI body nests, how a child MO gets its DN, what `status` does |
|
|
166
|
+
| `a4i://guide/query` | class against MO queries, the two subtree controls, keeping a response small |
|
|
167
|
+
| `a4i://guide/workflow` | the order: search or list, describe, get, dry run, post |
|
|
168
|
+
| `a4i://guide/limits` | where the bundled model, the dry run and the diff each stop short |
|
|
169
|
+
|
|
170
|
+
A `get` whose response would exceed 64 KB is refused, with the total count and
|
|
171
|
+
the ways to narrow it; `A4I_MCP_MAX_BYTES` raises or lowers that. On a
|
|
172
|
+
`--read-only` session, `post` is not offered at all.
|
|
173
|
+
|
|
174
|
+
## Python library
|
|
175
|
+
|
|
176
|
+
A `Client` holds its own session, so no daemon is involved: it logs in itself
|
|
177
|
+
and keeps the token in memory for as long as it lives.
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
import a4i
|
|
181
|
+
from a4i.merge import merge
|
|
182
|
+
|
|
183
|
+
with a4i.Client("apic1.example.com", verify=False) as client:
|
|
184
|
+
client.login("admin", password)
|
|
185
|
+
|
|
186
|
+
data = client.get("fvTenant", kind="class", query_target="subtree", rsp_subtree="full")
|
|
187
|
+
client.post("uni/tn-demo", {"fvTenant": {"attributes": {"name": "demo"}}}, kind="mo")
|
|
188
|
+
changes = client.diff(merge(base, override))
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`kind` is the subcommand the CLI takes, and it is required: `"class"` for a
|
|
192
|
+
class name, `"mo"` for a DN. Every other keyword argument is the CLI option of
|
|
193
|
+
the same name with underscores. `verify` is what `-k/--insecure` and `--ca`
|
|
194
|
+
express, `timeout` is `login --timeout`, and `dry_run()`, `diff()` and
|
|
195
|
+
`a4i.merge.merge()` are the commands of those names.
|
|
196
|
+
|
|
197
|
+
`AsyncClient` is `Client` awaited: the same arguments, the same return values
|
|
198
|
+
and the same exceptions, sending the same requests in the same order.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
async with a4i.AsyncClient("apic1.example.com", verify=False) as client:
|
|
202
|
+
await client.login("admin", password)
|
|
203
|
+
data = await client.get("fvTenant", kind="class")
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
A value ACI does not define raises `ValueError` before anything is sent. A
|
|
207
|
+
failed request raises `a4i.ApicError`, `a4i.NotLoggedInError` or
|
|
208
|
+
`a4i.SessionExpiredError`, all of them `a4i.A4iError`. The token refreshes
|
|
209
|
+
itself once half its lifetime has elapsed, so a long-running script needs
|
|
210
|
+
nothing of its own.
|
a4i-0.0.1/pyproject.toml
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "a4i"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "CLI/MCP/Python Library for the Cisco ACI REST API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "minefuto", email = "46558834+minefuto@users.noreply.github.com" },
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"httpx2>=2.9.1",
|
|
12
|
+
"rich>=15.0.0",
|
|
13
|
+
]
|
|
14
|
+
license = { file = "LICENSE" }
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
a4i = "a4i.cli:main"
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"pytest>=9.1.1",
|
|
29
|
+
"pytest-asyncio>=1.2.0",
|
|
30
|
+
"ruff>=0.16.1",
|
|
31
|
+
"ty>=0.0.65",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["hatchling>=1.31.0", "hatch-vcs>=0.5.0"]
|
|
36
|
+
build-backend = "hatchling.build"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.version]
|
|
39
|
+
source = "vcs"
|
|
40
|
+
fallback-version = "0.0.1"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/a4i"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
src = ["src", "tests"]
|
|
47
|
+
line-length = 100
|
|
48
|
+
# 3.11 is the floor: enum.StrEnum, which the query options are built on, landed
|
|
49
|
+
# there.
|
|
50
|
+
target-version = "py311"
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "N", "W"]
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
testpaths = ["tests"]
|
|
57
|
+
pythonpath = ["src"]
|
|
58
|
+
# The awaited tests are told apart by "async def" and nothing else, so they are
|
|
59
|
+
# written the way the synchronous ones are, one marker lighter.
|
|
60
|
+
asyncio_mode = "auto"
|