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.
- fcling-0.1.0/.dockerignore +16 -0
- fcling-0.1.0/.github/workflows/ci.yml +47 -0
- fcling-0.1.0/.github/workflows/release.yml +82 -0
- fcling-0.1.0/.gitignore +120 -0
- fcling-0.1.0/Dockerfile +31 -0
- fcling-0.1.0/LICENSE +28 -0
- fcling-0.1.0/Makefile +14 -0
- fcling-0.1.0/PKG-INFO +187 -0
- fcling-0.1.0/README.md +159 -0
- fcling-0.1.0/docs/ai.md +86 -0
- fcling-0.1.0/docs/architecture.md +118 -0
- fcling-0.1.0/docs/checks.md +68 -0
- fcling-0.1.0/docs/cli.md +93 -0
- fcling-0.1.0/docs/history.md +73 -0
- fcling-0.1.0/docs/installation.md +115 -0
- fcling-0.1.0/docs/lenses.md +113 -0
- fcling-0.1.0/docs/server.md +90 -0
- fcling-0.1.0/docs/ui.md +82 -0
- fcling-0.1.0/pyproject.toml +86 -0
- fcling-0.1.0/src/fcli/__init__.py +1 -0
- fcling-0.1.0/src/fcli/_version.py +24 -0
- fcling-0.1.0/src/fcli/agent.py +266 -0
- fcling-0.1.0/src/fcli/checks.py +977 -0
- fcling-0.1.0/src/fcli/cli.py +1022 -0
- fcling-0.1.0/src/fcli/collect.py +181 -0
- fcling-0.1.0/src/fcli/delta.py +125 -0
- fcling-0.1.0/src/fcli/engine.py +632 -0
- fcling-0.1.0/src/fcli/fabric.py +117 -0
- fcling-0.1.0/src/fcli/gnmi/__init__.py +1 -0
- fcling-0.1.0/src/fcli/gnmi/client.py +462 -0
- fcling-0.1.0/src/fcli/gnmi/path.py +158 -0
- fcling-0.1.0/src/fcli/gnmi/proto/__init__.py +0 -0
- fcling-0.1.0/src/fcli/gnmi/proto/gnmi.proto +468 -0
- fcling-0.1.0/src/fcli/gnmi/proto/gnmi_ext.proto +192 -0
- fcling-0.1.0/src/fcli/gnmi/proto/gnmi_ext_pb2.py +70 -0
- fcling-0.1.0/src/fcli/gnmi/proto/gnmi_ext_pb2.pyi +141 -0
- fcling-0.1.0/src/fcli/gnmi/proto/gnmi_ext_pb2_grpc.py +24 -0
- fcling-0.1.0/src/fcli/gnmi/proto/gnmi_pb2.py +124 -0
- fcling-0.1.0/src/fcli/gnmi/proto/gnmi_pb2.pyi +344 -0
- fcling-0.1.0/src/fcli/gnmi/proto/gnmi_pb2_grpc.py +246 -0
- fcling-0.1.0/src/fcli/incidents.py +572 -0
- fcling-0.1.0/src/fcli/inventory.py +206 -0
- fcling-0.1.0/src/fcli/lens.py +96 -0
- fcling-0.1.0/src/fcli/lenses/__init__.py +6 -0
- fcling-0.1.0/src/fcli/lenses/common.py +198 -0
- fcling-0.1.0/src/fcli/lenses/endpoints.py +141 -0
- fcling-0.1.0/src/fcli/lenses/path.py +464 -0
- fcling-0.1.0/src/fcli/lenses/service.py +115 -0
- fcling-0.1.0/src/fcli/lenses/where.py +229 -0
- fcling-0.1.0/src/fcli/llm.py +566 -0
- fcling-0.1.0/src/fcli/mcp.py +76 -0
- fcling-0.1.0/src/fcli/remote.py +94 -0
- fcling-0.1.0/src/fcli/render.py +149 -0
- fcling-0.1.0/src/fcli/report.py +210 -0
- fcling-0.1.0/src/fcli/reports/__init__.py +6 -0
- fcling-0.1.0/src/fcli/reports/bgp.py +136 -0
- fcling-0.1.0/src/fcli/reports/bgp_rib.py +381 -0
- fcling-0.1.0/src/fcli/reports/common.py +58 -0
- fcling-0.1.0/src/fcli/reports/health.py +581 -0
- fcling-0.1.0/src/fcli/reports/instances.py +278 -0
- fcling-0.1.0/src/fcli/reports/interfaces.py +310 -0
- fcling-0.1.0/src/fcli/reports/layer2.py +569 -0
- fcling-0.1.0/src/fcli/reports/lldp.py +45 -0
- fcling-0.1.0/src/fcli/reports/policy.py +104 -0
- fcling-0.1.0/src/fcli/reports/routing.py +449 -0
- fcling-0.1.0/src/fcli/reports/services.py +256 -0
- fcling-0.1.0/src/fcli/reports/system.py +105 -0
- fcling-0.1.0/src/fcli/server.py +503 -0
- fcling-0.1.0/src/fcli/service.py +507 -0
- fcling-0.1.0/src/fcli/store.py +306 -0
- fcling-0.1.0/src/fcli/tools.py +453 -0
- fcling-0.1.0/src/fcli/topology.py +319 -0
- fcling-0.1.0/src/fcli/tree.py +383 -0
- fcling-0.1.0/src/fcli/ui/api.js +133 -0
- fcling-0.1.0/src/fcli/ui/app.js +284 -0
- fcling-0.1.0/src/fcli/ui/chat.js +156 -0
- fcling-0.1.0/src/fcli/ui/history.js +131 -0
- fcling-0.1.0/src/fcli/ui/incidents.js +172 -0
- fcling-0.1.0/src/fcli/ui/index.html +15 -0
- fcling-0.1.0/src/fcli/ui/lenses.js +262 -0
- fcling-0.1.0/src/fcli/ui/markdown.js +92 -0
- fcling-0.1.0/src/fcli/ui/nav.js +97 -0
- fcling-0.1.0/src/fcli/ui/reports.js +147 -0
- fcling-0.1.0/src/fcli/ui/style.css +1831 -0
- fcling-0.1.0/src/fcli/ui/table.js +141 -0
- fcling-0.1.0/src/fcli/ui/topology.js +653 -0
- fcling-0.1.0/src/fcli/ui/vendor/README +3 -0
- fcling-0.1.0/src/fcli/ui/vendor/preact-htm.module.js +1 -0
- fcling-0.1.0/src/fcli/views.py +152 -0
- fcling-0.1.0/tests/__init__.py +0 -0
- fcling-0.1.0/tests/conftest.py +33 -0
- fcling-0.1.0/tests/fabric_replay.py +30 -0
- fcling-0.1.0/tests/fake_gnmi.py +197 -0
- fcling-0.1.0/tests/fake_service.py +119 -0
- fcling-0.1.0/tests/fixtures/releases/25.10.3/clab-4l2s-l1.json.gz +0 -0
- fcling-0.1.0/tests/fixtures/releases/25.10.3/clab-4l2s-s1.json.gz +0 -0
- fcling-0.1.0/tests/fixtures/releases/25.3.2/clab-4l2s-l1.json.gz +0 -0
- fcling-0.1.0/tests/fixtures/releases/25.3.2/clab-4l2s-s1.json.gz +0 -0
- fcling-0.1.0/tests/fixtures/releases/26.3.1/clab-4l2s-l1.json.gz +0 -0
- fcling-0.1.0/tests/fixtures/releases/26.3.1/clab-4l2s-s1.json.gz +0 -0
- fcling-0.1.0/tests/fixtures/releases/26.3.1/dcgw1.json.gz +0 -0
- fcling-0.1.0/tests/fixtures/releases/26.3.1/leaf1.json.gz +0 -0
- fcling-0.1.0/tests/fixtures/releases/26.7.1/clab-4l2s-l1.json.gz +0 -0
- fcling-0.1.0/tests/fixtures/releases/26.7.1/clab-4l2s-s1.json.gz +0 -0
- fcling-0.1.0/tests/fixtures/releases/26.7.2/clab-3-stage-evpn-vxlan-nvd-25-leaf1.json.gz +0 -0
- fcling-0.1.0/tests/fixtures/releases/26.7.2/clab-3-stage-evpn-vxlan-nvd-25-spine1.json.gz +0 -0
- fcling-0.1.0/tests/fixtures/releases/MATRIX.md +166 -0
- fcling-0.1.0/tests/lens_fabric.py +212 -0
- fcling-0.1.0/tests/replay.py +66 -0
- fcling-0.1.0/tests/rowcounts.py +14 -0
- fcling-0.1.0/tests/serve_child.py +39 -0
- fcling-0.1.0/tests/test_agent.py +403 -0
- fcling-0.1.0/tests/test_checks.py +168 -0
- fcling-0.1.0/tests/test_client.py +187 -0
- fcling-0.1.0/tests/test_engine.py +223 -0
- fcling-0.1.0/tests/test_inventory.py +74 -0
- fcling-0.1.0/tests/test_lenses.py +313 -0
- fcling-0.1.0/tests/test_path.py +39 -0
- fcling-0.1.0/tests/test_release_matrix.py +121 -0
- fcling-0.1.0/tests/test_reports.py +22 -0
- fcling-0.1.0/tests/test_server.py +327 -0
- fcling-0.1.0/tests/test_tools.py +116 -0
- fcling-0.1.0/tests/test_tree.py +144 -0
- fcling-0.1.0/tests/test_views.py +32 -0
- fcling-0.1.0/uv.lock +1607 -0
|
@@ -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
|
fcling-0.1.0/.gitignore
ADDED
|
@@ -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
|
fcling-0.1.0/Dockerfile
ADDED
|
@@ -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
|
+
[](https://github.com/wdesmedt/fcling/actions/workflows/ci.yml)
|
|
32
|
+
[](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.
|