fcling 0.1.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.
Files changed (125) hide show
  1. fcling-0.1.0/.dockerignore +16 -0
  2. fcling-0.1.0/.github/workflows/ci.yml +47 -0
  3. fcling-0.1.0/.github/workflows/release.yml +82 -0
  4. fcling-0.1.0/.gitignore +120 -0
  5. fcling-0.1.0/Dockerfile +31 -0
  6. fcling-0.1.0/LICENSE +28 -0
  7. fcling-0.1.0/Makefile +14 -0
  8. fcling-0.1.0/PKG-INFO +187 -0
  9. fcling-0.1.0/README.md +159 -0
  10. fcling-0.1.0/docs/ai.md +86 -0
  11. fcling-0.1.0/docs/architecture.md +118 -0
  12. fcling-0.1.0/docs/checks.md +68 -0
  13. fcling-0.1.0/docs/cli.md +93 -0
  14. fcling-0.1.0/docs/history.md +73 -0
  15. fcling-0.1.0/docs/installation.md +115 -0
  16. fcling-0.1.0/docs/lenses.md +113 -0
  17. fcling-0.1.0/docs/server.md +90 -0
  18. fcling-0.1.0/docs/ui.md +82 -0
  19. fcling-0.1.0/pyproject.toml +86 -0
  20. fcling-0.1.0/src/fcli/__init__.py +1 -0
  21. fcling-0.1.0/src/fcli/_version.py +24 -0
  22. fcling-0.1.0/src/fcli/agent.py +266 -0
  23. fcling-0.1.0/src/fcli/checks.py +977 -0
  24. fcling-0.1.0/src/fcli/cli.py +1022 -0
  25. fcling-0.1.0/src/fcli/collect.py +181 -0
  26. fcling-0.1.0/src/fcli/delta.py +125 -0
  27. fcling-0.1.0/src/fcli/engine.py +632 -0
  28. fcling-0.1.0/src/fcli/fabric.py +117 -0
  29. fcling-0.1.0/src/fcli/gnmi/__init__.py +1 -0
  30. fcling-0.1.0/src/fcli/gnmi/client.py +462 -0
  31. fcling-0.1.0/src/fcli/gnmi/path.py +158 -0
  32. fcling-0.1.0/src/fcli/gnmi/proto/__init__.py +0 -0
  33. fcling-0.1.0/src/fcli/gnmi/proto/gnmi.proto +468 -0
  34. fcling-0.1.0/src/fcli/gnmi/proto/gnmi_ext.proto +192 -0
  35. fcling-0.1.0/src/fcli/gnmi/proto/gnmi_ext_pb2.py +70 -0
  36. fcling-0.1.0/src/fcli/gnmi/proto/gnmi_ext_pb2.pyi +141 -0
  37. fcling-0.1.0/src/fcli/gnmi/proto/gnmi_ext_pb2_grpc.py +24 -0
  38. fcling-0.1.0/src/fcli/gnmi/proto/gnmi_pb2.py +124 -0
  39. fcling-0.1.0/src/fcli/gnmi/proto/gnmi_pb2.pyi +344 -0
  40. fcling-0.1.0/src/fcli/gnmi/proto/gnmi_pb2_grpc.py +246 -0
  41. fcling-0.1.0/src/fcli/incidents.py +572 -0
  42. fcling-0.1.0/src/fcli/inventory.py +206 -0
  43. fcling-0.1.0/src/fcli/lens.py +96 -0
  44. fcling-0.1.0/src/fcli/lenses/__init__.py +6 -0
  45. fcling-0.1.0/src/fcli/lenses/common.py +198 -0
  46. fcling-0.1.0/src/fcli/lenses/endpoints.py +141 -0
  47. fcling-0.1.0/src/fcli/lenses/path.py +464 -0
  48. fcling-0.1.0/src/fcli/lenses/service.py +115 -0
  49. fcling-0.1.0/src/fcli/lenses/where.py +229 -0
  50. fcling-0.1.0/src/fcli/llm.py +566 -0
  51. fcling-0.1.0/src/fcli/mcp.py +76 -0
  52. fcling-0.1.0/src/fcli/remote.py +94 -0
  53. fcling-0.1.0/src/fcli/render.py +149 -0
  54. fcling-0.1.0/src/fcli/report.py +210 -0
  55. fcling-0.1.0/src/fcli/reports/__init__.py +6 -0
  56. fcling-0.1.0/src/fcli/reports/bgp.py +136 -0
  57. fcling-0.1.0/src/fcli/reports/bgp_rib.py +381 -0
  58. fcling-0.1.0/src/fcli/reports/common.py +58 -0
  59. fcling-0.1.0/src/fcli/reports/health.py +581 -0
  60. fcling-0.1.0/src/fcli/reports/instances.py +278 -0
  61. fcling-0.1.0/src/fcli/reports/interfaces.py +310 -0
  62. fcling-0.1.0/src/fcli/reports/layer2.py +569 -0
  63. fcling-0.1.0/src/fcli/reports/lldp.py +45 -0
  64. fcling-0.1.0/src/fcli/reports/policy.py +104 -0
  65. fcling-0.1.0/src/fcli/reports/routing.py +449 -0
  66. fcling-0.1.0/src/fcli/reports/services.py +256 -0
  67. fcling-0.1.0/src/fcli/reports/system.py +105 -0
  68. fcling-0.1.0/src/fcli/server.py +503 -0
  69. fcling-0.1.0/src/fcli/service.py +507 -0
  70. fcling-0.1.0/src/fcli/store.py +306 -0
  71. fcling-0.1.0/src/fcli/tools.py +453 -0
  72. fcling-0.1.0/src/fcli/topology.py +319 -0
  73. fcling-0.1.0/src/fcli/tree.py +383 -0
  74. fcling-0.1.0/src/fcli/ui/api.js +133 -0
  75. fcling-0.1.0/src/fcli/ui/app.js +284 -0
  76. fcling-0.1.0/src/fcli/ui/chat.js +156 -0
  77. fcling-0.1.0/src/fcli/ui/history.js +131 -0
  78. fcling-0.1.0/src/fcli/ui/incidents.js +172 -0
  79. fcling-0.1.0/src/fcli/ui/index.html +15 -0
  80. fcling-0.1.0/src/fcli/ui/lenses.js +262 -0
  81. fcling-0.1.0/src/fcli/ui/markdown.js +92 -0
  82. fcling-0.1.0/src/fcli/ui/nav.js +97 -0
  83. fcling-0.1.0/src/fcli/ui/reports.js +147 -0
  84. fcling-0.1.0/src/fcli/ui/style.css +1831 -0
  85. fcling-0.1.0/src/fcli/ui/table.js +141 -0
  86. fcling-0.1.0/src/fcli/ui/topology.js +653 -0
  87. fcling-0.1.0/src/fcli/ui/vendor/README +3 -0
  88. fcling-0.1.0/src/fcli/ui/vendor/preact-htm.module.js +1 -0
  89. fcling-0.1.0/src/fcli/views.py +152 -0
  90. fcling-0.1.0/tests/__init__.py +0 -0
  91. fcling-0.1.0/tests/conftest.py +33 -0
  92. fcling-0.1.0/tests/fabric_replay.py +30 -0
  93. fcling-0.1.0/tests/fake_gnmi.py +197 -0
  94. fcling-0.1.0/tests/fake_service.py +119 -0
  95. fcling-0.1.0/tests/fixtures/releases/25.10.3/clab-4l2s-l1.json.gz +0 -0
  96. fcling-0.1.0/tests/fixtures/releases/25.10.3/clab-4l2s-s1.json.gz +0 -0
  97. fcling-0.1.0/tests/fixtures/releases/25.3.2/clab-4l2s-l1.json.gz +0 -0
  98. fcling-0.1.0/tests/fixtures/releases/25.3.2/clab-4l2s-s1.json.gz +0 -0
  99. fcling-0.1.0/tests/fixtures/releases/26.3.1/clab-4l2s-l1.json.gz +0 -0
  100. fcling-0.1.0/tests/fixtures/releases/26.3.1/clab-4l2s-s1.json.gz +0 -0
  101. fcling-0.1.0/tests/fixtures/releases/26.3.1/dcgw1.json.gz +0 -0
  102. fcling-0.1.0/tests/fixtures/releases/26.3.1/leaf1.json.gz +0 -0
  103. fcling-0.1.0/tests/fixtures/releases/26.7.1/clab-4l2s-l1.json.gz +0 -0
  104. fcling-0.1.0/tests/fixtures/releases/26.7.1/clab-4l2s-s1.json.gz +0 -0
  105. fcling-0.1.0/tests/fixtures/releases/26.7.2/clab-3-stage-evpn-vxlan-nvd-25-leaf1.json.gz +0 -0
  106. fcling-0.1.0/tests/fixtures/releases/26.7.2/clab-3-stage-evpn-vxlan-nvd-25-spine1.json.gz +0 -0
  107. fcling-0.1.0/tests/fixtures/releases/MATRIX.md +166 -0
  108. fcling-0.1.0/tests/lens_fabric.py +212 -0
  109. fcling-0.1.0/tests/replay.py +66 -0
  110. fcling-0.1.0/tests/rowcounts.py +14 -0
  111. fcling-0.1.0/tests/serve_child.py +39 -0
  112. fcling-0.1.0/tests/test_agent.py +403 -0
  113. fcling-0.1.0/tests/test_checks.py +168 -0
  114. fcling-0.1.0/tests/test_client.py +187 -0
  115. fcling-0.1.0/tests/test_engine.py +223 -0
  116. fcling-0.1.0/tests/test_inventory.py +74 -0
  117. fcling-0.1.0/tests/test_lenses.py +313 -0
  118. fcling-0.1.0/tests/test_path.py +39 -0
  119. fcling-0.1.0/tests/test_release_matrix.py +121 -0
  120. fcling-0.1.0/tests/test_reports.py +22 -0
  121. fcling-0.1.0/tests/test_server.py +327 -0
  122. fcling-0.1.0/tests/test_tools.py +116 -0
  123. fcling-0.1.0/tests/test_tree.py +144 -0
  124. fcling-0.1.0/tests/test_views.py +32 -0
  125. fcling-0.1.0/uv.lock +1607 -0
@@ -0,0 +1,16 @@
1
+ .git
2
+ .github
3
+ .venv
4
+ .mypy_cache
5
+ .pytest_cache
6
+ .ruff_cache
7
+ **/__pycache__
8
+ dist
9
+ build
10
+ tests
11
+ docs
12
+ src/fcli/_version.py
13
+ *.db
14
+ *.pem
15
+ .env
16
+ .envrc
@@ -0,0 +1,47 @@
1
+ name: ci
2
+ on:
3
+ # The badge reports the latest run on the default branch, so main has to run
4
+ # on push as well. Scoped to main: an unscoped push trigger would run a second
5
+ # time for every pull request opened from a branch in this repository.
6
+ push:
7
+ branches: [main]
8
+ pull_request:
9
+ jobs:
10
+ tests:
11
+ name: tests (python ${{ matrix.python-version }})
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ # Quoted: unquoted 3.10 would be read as the number 3.1
16
+ python-version: ["3.11", "3.12", "3.13"]
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ with:
21
+ # hatch-vcs reads the version from the tags.
22
+ fetch-depth: 0
23
+ - name: Install uv
24
+ uses: astral-sh/setup-uv@v6
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+ enable-cache: true
28
+ - name: Install dependencies
29
+ run: uv sync --locked --all-extras
30
+ - name: Lint
31
+ run: make lint
32
+ - name: Run tests
33
+ run: make test
34
+
35
+ image:
36
+ name: container image builds
37
+ runs-on: ubuntu-latest
38
+ steps:
39
+ - uses: actions/checkout@v4
40
+ - uses: docker/setup-buildx-action@v3
41
+ - uses: docker/build-push-action@v6
42
+ with:
43
+ context: .
44
+ push: false
45
+ build-args: VERSION=0.0.0
46
+ cache-from: type=gha
47
+ cache-to: type=gha,mode=max
@@ -0,0 +1,82 @@
1
+ name: release
2
+ on:
3
+ release:
4
+ types: [published]
5
+
6
+ jobs:
7
+ build:
8
+ name: build the package
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: write
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ with:
15
+ # hatch-vcs reads the version from the tag.
16
+ fetch-depth: 0
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v6
19
+ - name: Build sdist and wheel
20
+ run: uv build
21
+ - name: Check the version is the tag's
22
+ run: |
23
+ VERSION="${GITHUB_REF_NAME#v}"
24
+ ls dist
25
+ test -f "dist/fcling-${VERSION}-py3-none-any.whl"
26
+ - uses: actions/upload-artifact@v4
27
+ with:
28
+ name: dist
29
+ path: dist/
30
+ - name: Attach to the release
31
+ env:
32
+ GH_TOKEN: ${{ github.token }}
33
+ run: gh release upload "$GITHUB_REF_NAME" dist/* --clobber
34
+
35
+ pypi:
36
+ name: publish to PyPI
37
+ needs: build
38
+ runs-on: ubuntu-latest
39
+ # Trusted publishing: PyPI accepts this workflow in this environment, no token.
40
+ environment:
41
+ name: pypi
42
+ url: https://pypi.org/project/fcling/
43
+ permissions:
44
+ id-token: write
45
+ steps:
46
+ - uses: actions/download-artifact@v4
47
+ with:
48
+ name: dist
49
+ path: dist/
50
+ - uses: pypa/gh-action-pypi-publish@release/v1
51
+
52
+ image:
53
+ name: push the container image
54
+ runs-on: ubuntu-latest
55
+ permissions:
56
+ packages: write
57
+ contents: read
58
+ steps:
59
+ - uses: actions/checkout@v4
60
+ - uses: docker/setup-buildx-action@v3
61
+ - uses: docker/login-action@v3
62
+ with:
63
+ registry: ghcr.io
64
+ username: ${{ github.actor }}
65
+ password: ${{ secrets.GITHUB_TOKEN }}
66
+ - id: meta
67
+ uses: docker/metadata-action@v5
68
+ with:
69
+ images: ghcr.io/${{ github.repository_owner }}/fcling
70
+ tags: |
71
+ type=semver,pattern={{version}}
72
+ type=semver,pattern={{major}}.{{minor}}
73
+ type=raw,value=latest
74
+ - uses: docker/build-push-action@v6
75
+ with:
76
+ context: .
77
+ push: true
78
+ build-args: VERSION=${{ steps.meta.outputs.version }}
79
+ tags: ${{ steps.meta.outputs.tags }}
80
+ labels: ${{ steps.meta.outputs.labels }}
81
+ cache-from: type=gha
82
+ cache-to: type=gha,mode=max
@@ -0,0 +1,120 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ .pytest_cache/
6
+
7
+ # C extensions
8
+ *.so
9
+
10
+ # Distribution / packaging
11
+ .Python
12
+ env/
13
+ build/
14
+ develop-eggs/
15
+ dist/
16
+ downloads/
17
+ eggs/
18
+ .eggs/
19
+ lib/
20
+ lib64/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+
28
+ # PyInstaller
29
+ # Usually these files are written by a python script from a template
30
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
31
+ *.manifest
32
+ *.spec
33
+
34
+ # Installer logs
35
+ pip-log.txt
36
+ pip-delete-this-directory.txt
37
+
38
+ # Unit test / coverage reports
39
+ htmlcov/
40
+ .tox/
41
+ .coverage
42
+ .coverage.*
43
+ .cache
44
+ nosetests.xml
45
+ coverage.xml
46
+ *,cover
47
+ .hypothesis/
48
+
49
+ # Translations
50
+ *.mo
51
+ *.pot
52
+
53
+ # Django stuff:
54
+ *.log
55
+ *.log.*
56
+ local_settings.py
57
+
58
+ # Flask stuff:
59
+ instance/
60
+ .webassets-cache
61
+
62
+ # Scrapy stuff:
63
+ .scrapy
64
+
65
+ # Sphinx documentation
66
+ docs/_build/
67
+ docs/configuration/generated/*.rst
68
+
69
+ # PyBuilder
70
+ target/
71
+
72
+ # IPython Notebook
73
+ .ipynb_checkpoints
74
+
75
+ # pyenv
76
+ .python-version
77
+
78
+ # celery beat schedule file
79
+ celerybeat-schedule
80
+
81
+ # dotenv
82
+ .env
83
+ .envrc
84
+
85
+ # virtualenv
86
+ .venv/
87
+ venv/
88
+ ENV/
89
+
90
+ # Spyder project settings
91
+ .spyderproject
92
+
93
+ # Rope project settings
94
+ .ropeproject
95
+
96
+ *.swp
97
+ tags
98
+
99
+ .vagrant
100
+ .vars
101
+ output/
102
+
103
+ .DS_Store
104
+ .vscode
105
+ .idea
106
+
107
+ pip-wheel-metadata/
108
+
109
+ ### Project specific
110
+ tests/.backup
111
+ .backup
112
+ inventory
113
+ intent
114
+ *.pem
115
+ **/*.bak
116
+ **/*.sav
117
+ .gemini
118
+ .mcp*
119
+ ## dynamic project versioning
120
+ src/fcli/_version.py
@@ -0,0 +1,31 @@
1
+ FROM python:3.13-slim-bookworm
2
+
3
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
4
+
5
+ WORKDIR /app
6
+ COPY pyproject.toml uv.lock README.md ./
7
+
8
+ # The versions uv.lock pins, not the newest ones: an unpinned install picks
9
+ # up whatever was released last, and a new release of a dependency can
10
+ # break fcli on import.
11
+ RUN uv export --frozen --no-dev --all-extras --no-emit-project --no-hashes -o /tmp/requirements.txt && \
12
+ uv pip install --system --no-cache -r /tmp/requirements.txt && \
13
+ rm /tmp/requirements.txt
14
+
15
+ COPY src ./src
16
+ # No .git in the image: hatch-vcs takes the version from here instead.
17
+ ARG VERSION=0.0.0
18
+ RUN SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} uv pip install --system --no-cache --no-deps . && \
19
+ rm -rf /app
20
+
21
+ # The history and baselines go under one fixed directory, whatever user the
22
+ # container runs as, so a volume mounted there keeps them. World-writable and
23
+ # sticky like /tmp, so a container run with --user can write to it too.
24
+ ENV XDG_STATE_HOME=/state
25
+ RUN mkdir -p /state && chmod 1777 /state
26
+ VOLUME /state
27
+
28
+ WORKDIR /work
29
+ EXPOSE 8765
30
+ ENTRYPOINT ["fcli"]
31
+ CMD ["--help"]
fcling-0.1.0/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Walter De Smedt
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
fcling-0.1.0/Makefile ADDED
@@ -0,0 +1,14 @@
1
+ .PHONY: proto test lint
2
+
3
+ # The gNMI stubs are generated from the openconfig protos and committed, so
4
+ # installing fcli needs no protoc.
5
+ proto:
6
+ uv run python -m grpc_tools.protoc -I src --python_out=src --pyi_out=src --grpc_python_out=src \
7
+ src/fcli/gnmi/proto/gnmi.proto src/fcli/gnmi/proto/gnmi_ext.proto
8
+
9
+ test:
10
+ uv run pytest -q
11
+
12
+ lint:
13
+ uv run ruff check src tests
14
+ uv run mypy src
fcling-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.5
2
+ Name: fcling
3
+ Version: 0.1.0
4
+ Summary: Fabric CLI: what an SR Linux fabric looks like, what is wrong with it, and what changed
5
+ Project-URL: Homepage, https://github.com/wdesmedt/fcling
6
+ Project-URL: Documentation, https://github.com/wdesmedt/fcling/tree/main/docs
7
+ Project-URL: Issues, https://github.com/wdesmedt/fcling/issues
8
+ Author-email: Walter De Smedt <walter.de.smedt@gmail.com>
9
+ License-Expression: BSD-3-Clause
10
+ License-File: LICENSE
11
+ Requires-Python: >=3.11
12
+ Requires-Dist: click>=8.1
13
+ Requires-Dist: cryptography>=42
14
+ Requires-Dist: grpcio>=1.84
15
+ Requires-Dist: protobuf>=7.35.1
16
+ Requires-Dist: pydantic>=2.8
17
+ Requires-Dist: pyyaml>=6.0
18
+ Requires-Dist: rich>=13.7
19
+ Provides-Extra: ai
20
+ Requires-Dist: anthropic>=1.13; extra == 'ai'
21
+ Requires-Dist: openai>=3.29; extra == 'ai'
22
+ Provides-Extra: mcp
23
+ Requires-Dist: mcp>=2.3; extra == 'mcp'
24
+ Provides-Extra: server
25
+ Requires-Dist: starlette>=0.40; extra == 'server'
26
+ Requires-Dist: uvicorn>=0.30; extra == 'server'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # fcli
30
+
31
+ [![ci](https://github.com/wdesmedt/fcling/actions/workflows/ci.yml/badge.svg)](https://github.com/wdesmedt/fcling/actions/workflows/ci.yml)
32
+ [![PyPI](https://img.shields.io/pypi/v/fcling)](https://pypi.org/project/fcling/)
33
+
34
+ What an SR Linux fabric looks like, what is wrong with it, and what changed, straight from gNMI.
35
+
36
+ This is a ground-up rewrite of [fcli](https://github.com/srl-labs/fcli). It runs on asyncio throughout, uses no Nornir and makes no blocking gNMI calls.
37
+
38
+ ```bash
39
+ uv tool install 'fcling[server,mcp,ai]' # the `fcli` command, from PyPI
40
+ docker run --rm --network host -v "$PWD:/work" ghcr.io/wdesmedt/fcling -t lab.clab.yml bgp-peers
41
+ ```
42
+
43
+ Or from a checkout:
44
+
45
+ ```bash
46
+ uv sync
47
+ uv run fcli -t lab.clab.yml bgp-peers
48
+ uv run fcli -t lab.clab.yml -i role=leaf -o json subif -f oper=down
49
+ uv run fcli -c inventory.yml sys-info
50
+ uv run fcli -t lab.clab.yml ipv4-rib -a 10.1.3.99 # longest-prefix match
51
+ uv run fcli -t lab.clab.yml bgp-rib -r evpn -t 5 -d # with the detail columns
52
+ uv run fcli -t lab.clab.yml ifstats -s 2 -f oper=up # rates over 2 s
53
+ ```
54
+
55
+ With the engine running, the same commands answer instantly from what it streams, and can look back:
56
+
57
+ ```bash
58
+ uv sync --extra server
59
+ uv run fcli -t lab.clab.yml serve # streams every node, keeps history
60
+ export FCLI_SERVER=http://127.0.0.1:8765
61
+ uv run fcli bgp-peers # live, from the engine
62
+ uv run fcli bgp-peers --at 14:02 # as it was then
63
+ uv run fcli history --since 1h -r bgp-peers --follow # what changed, which commit did it, and keeps watching
64
+ ```
65
+
66
+ What is wrong, and what changed:
67
+
68
+ ```bash
69
+ uv run fcli checks # every finding, check by check
70
+ uv run fcli incidents -v # grouped by root cause, worst first
71
+ uv run fcli incidents --at 14:02 # what was wrong then
72
+ uv run fcli baseline pre-change -m "before the MTU change"
73
+ uv run fcli drift pre-change # what came, went and changed since
74
+ ```
75
+
76
+ Ask, or let your own AI client ask. Both get the same 8 read-only tools: status, reports, incidents, checks, history, drift and baselines.
77
+
78
+ ```bash
79
+ uv sync --extra server --extra mcp --extra ai
80
+ uv run fcli ask "what is wrong, and what changed just before?" # the first provider with a key
81
+ uv run fcli ask --provider openai --effort low "is BGP healthy?"
82
+ claude mcp add --transport http fcli http://127.0.0.1:8765/mcp # MCP over HTTP from `fcli serve`
83
+ claude mcp add fcli -- fcli --server http://127.0.0.1:8765 mcp # or over stdio
84
+ ```
85
+
86
+ Questions across the whole fabric, and its shape:
87
+
88
+ ```bash
89
+ uv run fcli -t lab.clab.yml where 10.1.3.13 # every place that knows an IP or MAC
90
+ uv run fcli -t lab.clab.yml path leaf1 10.1.3.13 --ni ipvrf-1 # hop by hop, over ECMP and VXLAN
91
+ uv run fcli -t lab.clab.yml service macvrf-102 # one service on every node, and what disagrees
92
+ uv run fcli -t lab.clab.yml endpoints # every attached host
93
+ uv run fcli -t lab.clab.yml topology # tiers, cables, clients, segments
94
+ ```
95
+
96
+ `checks` and `incidents` work without a server too: one-shot from the nodes, or from the local history with `--at`. They exit 1 when there is an error, so they can gate a pipeline.
97
+
98
+ Reports: `sys-info`, `config-commits`, `lldp`, `subif`, `lag`, `ifstats`, `ni`, `bgp-peers`, `bgp-rib`, `ipv4-rib`, `ipv6-rib`, `tunnel-table`, `static-routes`, `routing-pol`, `mac`, `arp`, `nd`, `irb`, `es`, `es-dest`, `vxlan`, `bfd`, `isis`, `ospf`, `resources`, `components`, `transceivers`, `services`, `bridge-domains`, `routers`. Each is a pure function from gNMI paths to typed records.
99
+
100
+ ## Documentation
101
+
102
+ | | |
103
+ |---|---|
104
+ | [Installation and inventory](docs/installation.md) | installing, containerlab and static inventories, selecting nodes, TLS |
105
+ | [Reports](docs/cli.md) | every report, filters, output formats |
106
+ | [The web UI](docs/ui.md) | topology, incidents, history, lenses, reports and the agent in a browser |
107
+ | [The engine](docs/server.md) | `fcli serve`: how it streams, the HTTP API and its events, security |
108
+ | [History](docs/history.md) | what is kept, `--at`, commit attribution, baselines and drift |
109
+ | [Lenses and topology](docs/lenses.md) | `where`, `path`, `service`, `endpoints`, `topology`, adding a lens |
110
+ | [Checks and incidents](docs/checks.md) | every check, and how findings are grouped by root cause |
111
+ | [MCP and the agent](docs/ai.md) | the tools, MCP setup, `fcli ask`, Claude, OpenAI and Grok |
112
+ | [Architecture](docs/architecture.md) | how it fits together, adding a report or a check, the tests |
113
+
114
+ ## Architecture
115
+
116
+ ```
117
+ inventory ──► collect ──► gNMI Get (one per datatype, all nodes at once)
118
+ │
119
+ ▼
120
+ Tree (per node) ◄── later: gNMI Subscribe, same tree
121
+ │
122
+ ▼
123
+ report: pure function Tree → typed records
124
+ │
125
+ ▼
126
+ render: table · json · yaml · csv
127
+ ```
128
+
129
+ | Module | Role |
130
+ |---|---|
131
+ | [gnmi/client.py](src/fcli/gnmi/client.py) | `grpc.aio` client for Capabilities, Get and Subscribe. TLS can be verified, trust-on-first-use (`skip_verify`) or plaintext. Errors are worded around the node. |
132
+ | [gnmi/path.py](src/fcli/gnmi/path.py) | Paths as hashable values, parsed once. Module prefixes are dropped. |
133
+ | [tree.py](src/fcli/tree.py) | A node's state as plain JSON. Get payloads and subscription leaves merge into it, with list entries matched by key. |
134
+ | [report.py](src/fcli/report.py) | `@report(name, reads=[Read(path, datatype, mode)], record=..., columns=...)`. A report declares its paths and never does I/O. |
135
+ | `follow` | Optional pure function `(tree, params) -> reads` for dependent reads: the next-hop-groups the routes name, the BGP attribute sets of the paths shown. The collector calls it again until it asks for nothing new. It fetches by key, or the whole table above a limit. |
136
+ | `Param` | An argument that shapes what a report builds (`ipv4-rib -a <address>`, `bgp-rib --paths all`). The CLI turns each one into an option. |
137
+ | [reports/](src/fcli/reports/) | The report definitions. Records are flat, frozen pydantic models: one record is one row. |
138
+ | [collect.py](src/fcli/collect.py) | One-shot engine. Each node is one task, and a node fails on its own. A path a node does not know reads as empty. |
139
+ | [inventory.py](src/fcli/inventory.py) | Loads a containerlab topology or a static YAML inventory, with `key=glob` filters. |
140
+ | [engine.py](src/fcli/engine.py) | Long-running engine. ON_CHANGE reads are streamed in Subscribe RPCs of at most 36 paths, the most SR Linux accepts, each with a heartbeat. SAMPLE reads (counters) are polled, and each polled subtree is replaced whole: a SAMPLE stream never says what is gone. `follow` reads are fetched by key. A reconnect resyncs into a fresh tree. An update only re-runs the reports whose paths it overlaps. |
141
+ | [delta.py](src/fcli/delta.py) | Changes record by record, matched on each record's `key`. A change confined to `volatile` fields (counters, rates) is pushed live but not kept. |
142
+ | [store.py](src/fcli/store.py) | SQLite: an append-only change log plus the current state. A restart diffs against it, and the state at any instant is a fold back from now. |
143
+ | [checks.py](src/fcli/checks.py) | 19 fabric checks: pure functions of a `Fabric` (every node's records) to `Finding` records. In the engine, findings are a report like any other, so each one raised or cleared is history. |
144
+ | [incidents.py](src/fcli/incidents.py) | Findings anchored to what they are about (a cable, a session, a node, the underlay between two nodes) and grouped by root cause. The same cause in many places folds into one pattern. |
145
+ | [views.py](src/fcli/views.py) | One place for fabric-wide answers from any source (the nodes now, the engine, the history at an instant): checks, incidents, drift, and history attributed to the configuration commit that preceded it. |
146
+ | [service.py](src/fcli/service.py) | The questions every surface asks, behind one interface with three implementations: the engine in this process, a running server over HTTP, or the nodes one-shot. |
147
+ | [tools.py](src/fcli/tools.py) | 8 read-only tools over a Service. Inputs are pydantic models that are validated before anything runs; outputs are compact JSON with row limits. One definition serves both MCP clients and the agent. |
148
+ | [mcp.py](src/fcli/mcp.py) | The tools as an MCP server: streamable HTTP at `/mcp` on `fcli serve` (stateless, with DNS-rebinding protection on localhost), or stdio via `fcli mcp`. |
149
+ | [agent.py](src/fcli/agent.py) | The in-app agent, provider-neutral: streaming, concurrent tool calls, and append-only conversations kept server-side in the provider's own format. |
150
+ | [llm.py](src/fcli/llm.py) | The providers. **Claude** (Anthropic SDK): adaptive thinking, cached prompt and tools, server-side refusal fallback, `claude-opus-5-5` at `medium`. **OpenAI** (Responses API: `store` off, reasoning replayed encrypted), `gpt-5.6-sol`. `OPENAI_API=chat` switches to Chat Completions for proxies. **Grok** (Chat Completions at api.x.ai), `grok-4.6`. The provider is `FCLI_LLM_PROVIDER` or the first one with a key, Claude first; model and effort come from `FCLI_MODEL` / `FCLI_EFFORT` or each provider's own variables. |
151
+ | [server.py](src/fcli/server.py) | Starlette: `/api/status`, `/api/reports[/{name}]` (live, `?at=` or collected on the spot), `/api/history`, `/api/events` (SSE, resumable with `Last-Event-ID`). Bound to localhost; a bearer token when it isn't. |
152
+
153
+ Because a report only declares paths, the same definition can be fed by a one-shot Get (CLI), by a STREAM subscription (server) or by a recording (tests). Its subscription plan is static data that can be checked without a device.
154
+
155
+ ### Static inventory
156
+
157
+ ```yaml
158
+ defaults:
159
+ username: admin
160
+ password: NokiaSrl1!
161
+ tls: {ca_file: ca.pem} # or {skip_verify: true} / {insecure: true}
162
+ nodes:
163
+ leaf1: {address: 10.0.0.11, labels: {role: leaf}}
164
+ spine1: {address: 10.0.0.21, labels: {role: spine}}
165
+ ```
166
+
167
+ ## Tests
168
+
169
+ ```bash
170
+ make test lint
171
+ ```
172
+
173
+ [tests/fixtures/releases](tests/fixtures/releases/) holds real gNMI recordings from the original fcli for SR Linux 25.3 to 26.7. [test_release_matrix.py](tests/test_release_matrix.py) replays them through the new reports and requires row-for-row equality with what the original rendered. [fake_gnmi.py](tests/fake_gnmi.py) is an in-process gNMI target, so the client, TLS, the collector and the CLI are tested without a lab.
174
+
175
+ [test_engine.py](tests/test_engine.py) and [test_server.py](tests/test_server.py) stream from that same fake target. They cover the initial sync, refused paths, live changes and deletes, history and `state_at`, and SSE replay over a real uvicorn.
176
+
177
+ The gNMI stubs are generated from the openconfig protos (`make proto`) and committed.
178
+
179
+ ## Roadmap
180
+
181
+ 1. ~~Inventory, async gNMI client, node tree, recorded fixtures~~
182
+ 2. ~~Declared-path reports with typed records, and a one-shot CLI~~. 27 reports are ported, and every one with recorded data matches the original row for row. `services`, `bridge-domains`, `routers` and `endpoints` move to phase 6: the old getters built display strings and web-UI tiles, and these should be typed records over `ni`, `irb`, `es`, `mac` and `arp`/`nd`.
183
+ 3. ~~Long-running engine~~: subscriptions planned from the declared paths, record-level changes in SQLite, HTTP/SSE, plus `fcli serve`, `--server`, `--at` and `history`. On both labs the engine's live records match a one-shot run for every report.
184
+ 4. ~~Checks, incident correlation, timeline and baselines~~. Findings live in the engine and are kept as history. Incidents carry "since". `--at` works for findings and incidents, baselines and drift work through the store, and history is attributed to commits. On both labs the engine's findings match a one-shot run.
185
+ 5. ~~MCP served from the engine, and the in-app agent on the same tools~~: `/mcp`, `fcli mcp`, `/api/chat` and `fcli ask`. All of them use the same tools over one Service, with Claude, OpenAI or Grok as the model.
186
+ 6. ~~Lenses and topology~~: `where`, `path`, `service`, `endpoints` and `topology`, from the nodes, the engine or the history. They are served over `/api/lenses` and `/api/topology`, and five more tools use them. `services`, `bridge-domains` and `routers` are typed reports. `endpoints` became a lens, because placing a host needs every node's tables.
187
+ 7. ~~Web UI~~, served by `fcli serve` with no build step: a navigation panel; topology, incidents and history; the lenses, with `path` and `where` drawn on the topology; every report as a live table; and the agent beside any view. All of it works live or at an instant.