apache-aiops 0.2.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.
- apache_aiops-0.2.0/.claude-plugin/plugin.json +22 -0
- apache_aiops-0.2.0/.github/workflows/ci.yml +28 -0
- apache_aiops-0.2.0/.github/workflows/mcp-publish.yml +55 -0
- apache_aiops-0.2.0/.github/workflows/publish.yml +26 -0
- apache_aiops-0.2.0/.gitignore +23 -0
- apache_aiops-0.2.0/.mcp.json +13 -0
- apache_aiops-0.2.0/CHANGELOG.md +13 -0
- apache_aiops-0.2.0/LICENSE +21 -0
- apache_aiops-0.2.0/PKG-INFO +50 -0
- apache_aiops-0.2.0/README.md +35 -0
- apache_aiops-0.2.0/SECURITY.md +5 -0
- apache_aiops-0.2.0/apache_aiops/__init__.py +8 -0
- apache_aiops-0.2.0/apache_aiops/cli/__init__.py +9 -0
- apache_aiops-0.2.0/apache_aiops/cli/_common.py +118 -0
- apache_aiops-0.2.0/apache_aiops/cli/_root.py +206 -0
- apache_aiops-0.2.0/apache_aiops/cli/undo.py +66 -0
- apache_aiops-0.2.0/apache_aiops/config.py +74 -0
- apache_aiops-0.2.0/apache_aiops/connection.py +222 -0
- apache_aiops-0.2.0/apache_aiops/governance/__init__.py +50 -0
- apache_aiops-0.2.0/apache_aiops/governance/audit.py +526 -0
- apache_aiops-0.2.0/apache_aiops/governance/budget.py +222 -0
- apache_aiops-0.2.0/apache_aiops/governance/decorators.py +599 -0
- apache_aiops-0.2.0/apache_aiops/governance/outcome.py +102 -0
- apache_aiops-0.2.0/apache_aiops/governance/paths.py +23 -0
- apache_aiops-0.2.0/apache_aiops/governance/patterns.py +396 -0
- apache_aiops-0.2.0/apache_aiops/governance/policy.py +67 -0
- apache_aiops-0.2.0/apache_aiops/governance/sanitize.py +79 -0
- apache_aiops-0.2.0/apache_aiops/governance/undo.py +259 -0
- apache_aiops-0.2.0/apache_aiops/mcp_server/__init__.py +1 -0
- apache_aiops-0.2.0/apache_aiops/mcp_server/_annotations.py +66 -0
- apache_aiops-0.2.0/apache_aiops/mcp_server/_shared.py +67 -0
- apache_aiops-0.2.0/apache_aiops/mcp_server/server.py +14 -0
- apache_aiops-0.2.0/apache_aiops/mcp_server/tools/__init__.py +1 -0
- apache_aiops-0.2.0/apache_aiops/mcp_server/tools/httpd.py +143 -0
- apache_aiops-0.2.0/apache_aiops/mcp_server/tools/undo.py +204 -0
- apache_aiops-0.2.0/apache_aiops/ops/__init__.py +1 -0
- apache_aiops-0.2.0/apache_aiops/ops/httpd.py +81 -0
- apache_aiops-0.2.0/docs/VERIFICATION.md +15 -0
- apache_aiops-0.2.0/glama.json +6 -0
- apache_aiops-0.2.0/openclaw.plugin.json +24 -0
- apache_aiops-0.2.0/package.json +20 -0
- apache_aiops-0.2.0/pyproject.toml +60 -0
- apache_aiops-0.2.0/server.json +21 -0
- apache_aiops-0.2.0/skills/apache-aiops/SKILL.md +24 -0
- apache_aiops-0.2.0/skills/apache-aiops/references/agent-guardrails.md +13 -0
- apache_aiops-0.2.0/skills/apache-aiops/references/capabilities.md +5 -0
- apache_aiops-0.2.0/skills/apache-aiops/references/cli-reference.md +3 -0
- apache_aiops-0.2.0/skills/apache-aiops/references/setup-guide.md +5 -0
- apache_aiops-0.2.0/tests/test_audit_guardrails.py +211 -0
- apache_aiops-0.2.0/tests/test_cli_audited.py +38 -0
- apache_aiops-0.2.0/tests/test_cli_governed_exit.py +18 -0
- apache_aiops-0.2.0/tests/test_gov_audit.py +292 -0
- apache_aiops-0.2.0/tests/test_gov_decorators.py +433 -0
- apache_aiops-0.2.0/tests/test_gov_patterns.py +413 -0
- apache_aiops-0.2.0/tests/test_gov_policy.py +56 -0
- apache_aiops-0.2.0/tests/test_governance_persistence.py +333 -0
- apache_aiops-0.2.0/tests/test_httpd.py +304 -0
- apache_aiops-0.2.0/tests/test_package_layout.py +62 -0
- apache_aiops-0.2.0/tests/test_smoke.py +28 -0
- apache_aiops-0.2.0/tests/test_tool_annotations.py +139 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
|
+
"name": "apache-aiops",
|
|
4
|
+
"displayName": "Apache HTTP Server AIops",
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"description": "Governed Apache HTTP Server operations — 17 MCP tools with audit.",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Wei Zhou",
|
|
9
|
+
"email": "zhouwei008@gmail.com",
|
|
10
|
+
"url": "https://github.com/AIops-tools"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/AIops-tools/Apache-AIops",
|
|
13
|
+
"repository": "https://github.com/AIops-tools/Apache-AIops",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"apache",
|
|
17
|
+
"httpd",
|
|
18
|
+
"web-server",
|
|
19
|
+
"mcp",
|
|
20
|
+
"aiops"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [bootstrap, main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
quality:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
- name: Install Python and dependencies
|
|
18
|
+
run: |
|
|
19
|
+
uv python install 3.11
|
|
20
|
+
uv sync --dev --python 3.11
|
|
21
|
+
- name: Tests
|
|
22
|
+
run: uv run pytest -q
|
|
23
|
+
- name: Lint
|
|
24
|
+
run: uv run ruff check .
|
|
25
|
+
- name: Security scan
|
|
26
|
+
run: uv run bandit -r apache_aiops -ll -q
|
|
27
|
+
- name: Build
|
|
28
|
+
run: uv build
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: mcp-publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
release:
|
|
6
|
+
types: [published]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish-mcp:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- name: Wait for PyPI
|
|
18
|
+
# The release event also triggers the PyPI publish workflow; the MCP
|
|
19
|
+
# registry validates that the package version exists on PyPI, so poll
|
|
20
|
+
# until it has propagated (every 15s, up to 10 minutes).
|
|
21
|
+
run: |
|
|
22
|
+
python3 - <<'EOF'
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
import urllib.error
|
|
27
|
+
import urllib.request
|
|
28
|
+
|
|
29
|
+
with open("server.json", encoding="utf-8") as f:
|
|
30
|
+
package = json.load(f)["packages"][0]
|
|
31
|
+
name, version = package["identifier"], package["version"]
|
|
32
|
+
url = f"https://pypi.org/pypi/{name}/{version}/json"
|
|
33
|
+
deadline = time.monotonic() + 600
|
|
34
|
+
while True:
|
|
35
|
+
try:
|
|
36
|
+
with urllib.request.urlopen(url, timeout=10):
|
|
37
|
+
print(f"{name}=={version} is available on PyPI.")
|
|
38
|
+
sys.exit(0)
|
|
39
|
+
except (urllib.error.URLError, OSError) as exc:
|
|
40
|
+
print(f"{name}=={version} not on PyPI yet ({exc}); "
|
|
41
|
+
"retrying in 15s...")
|
|
42
|
+
if time.monotonic() >= deadline:
|
|
43
|
+
sys.exit(f"Timed out after 10 minutes waiting for "
|
|
44
|
+
f"{name}=={version} to appear on PyPI. "
|
|
45
|
+
"Check the PyPI publish workflow, then re-run "
|
|
46
|
+
"this workflow via workflow_dispatch.")
|
|
47
|
+
time.sleep(15)
|
|
48
|
+
EOF
|
|
49
|
+
- name: Install mcp-publisher
|
|
50
|
+
run: |
|
|
51
|
+
curl -sL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
|
|
52
|
+
- name: Login to MCP Registry (GitHub OIDC)
|
|
53
|
+
run: ./mcp-publisher login github-oidc
|
|
54
|
+
- name: Publish server.json
|
|
55
|
+
run: ./mcp-publisher publish
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Trusted Publishing (OIDC) — publishes from GitHub's runners with no API token,
|
|
4
|
+
# sidestepping the local-IP / account new-project rate limit. Configure a matching
|
|
5
|
+
# "trusted publisher" for this package on PyPI (see the repo release notes).
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [published]
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
publish:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
permissions:
|
|
18
|
+
id-token: write # required for PyPI Trusted Publishing (OIDC)
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- name: Set up uv
|
|
22
|
+
uses: astral-sh/setup-uv@v5
|
|
23
|
+
- name: Build sdist + wheel
|
|
24
|
+
run: uv build
|
|
25
|
+
- name: Publish to PyPI (Trusted Publishing)
|
|
26
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
.ruff_cache/
|
|
7
|
+
|
|
8
|
+
# Virtual envs / build
|
|
9
|
+
.venv/
|
|
10
|
+
dist/
|
|
11
|
+
build/
|
|
12
|
+
|
|
13
|
+
# uv
|
|
14
|
+
uv.lock
|
|
15
|
+
|
|
16
|
+
# Local config / secrets (never commit)
|
|
17
|
+
*.env
|
|
18
|
+
.env
|
|
19
|
+
config.yaml
|
|
20
|
+
|
|
21
|
+
# OS
|
|
22
|
+
.DS_Store
|
|
23
|
+
.coverage
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v0.2.0 — 2026-11-05
|
|
4
|
+
|
|
5
|
+
- Expand to 17 governed MCP/CLI tools with build/runtime config, bounded mod_status/error-log reads and privacy-preserving access-status summaries.
|
|
6
|
+
- Add configtest-preflighted graceful reload with dry-run and unknown-outcome reporting.
|
|
7
|
+
- Preserve stderr output from Apache commands and report partial probe failures explicitly.
|
|
8
|
+
- Prevent error-log reads from resolving into this tool's own audit/config directory.
|
|
9
|
+
- Add GitHub CI for tests, lint, security scan and build.
|
|
10
|
+
|
|
11
|
+
## 0.1.0
|
|
12
|
+
|
|
13
|
+
- Initial local development version; not published.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wei <zhouwei008@gmail.com>
|
|
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.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: apache-aiops
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Governed Apache HTTP Server diagnosis for config, virtual hosts, modules and HTTP health.
|
|
5
|
+
Author-email: Wei Zhou <zhouwei008@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
10
|
+
Requires-Dist: mcp[cli]<3.0,>=2.0
|
|
11
|
+
Requires-Dist: pyyaml<7.0,>=6.0
|
|
12
|
+
Requires-Dist: rich<16.0,>=13.0
|
|
13
|
+
Requires-Dist: typer<1.0,>=0.12
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
<!-- mcp-name: io.github.AIops-tools/apache-aiops -->
|
|
17
|
+
# Apache AIops
|
|
18
|
+
|
|
19
|
+
A dedicated, governed MCP server, CLI and skill for Apache HTTP Server with 17 MCP tools. It uses Apache's own `apachectl` diagnostics, optional operator-configured HTTP health and `mod_status` URLs, and bounded reads of an operator-configured error log plus a privacy-preserving access-log status summary. Every call is audited and budgeted by the vendored governance harness.
|
|
20
|
+
|
|
21
|
+
## What it checks
|
|
22
|
+
|
|
23
|
+
- Apache version, config syntax, parsed virtual hosts and loaded modules.
|
|
24
|
+
- Build options and parsed runtime configuration.
|
|
25
|
+
- Parsed config include tree, compiled modules and directives from static modules.
|
|
26
|
+
- Optional machine-readable `mod_status` metrics, bounded error-log tail and privacy-preserving access-status summary.
|
|
27
|
+
- Optional HTTP health status without downloading the response body.
|
|
28
|
+
- `apache_diagnose` correlates the checks and preserves failures as errors with `data: null`.
|
|
29
|
+
|
|
30
|
+
## Setup from source
|
|
31
|
+
|
|
32
|
+
Requires Python 3.11+, `uv`, and `apachectl` on the operator host. Run `uv sync --dev`, `uv run apache-aiops init`, then `uv run apache-aiops doctor`. Manual `~/.apache-aiops/config.yaml`:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
targets:
|
|
36
|
+
- name: local
|
|
37
|
+
apachectl_bin: apachectl
|
|
38
|
+
probe_url: http://127.0.0.1/health
|
|
39
|
+
status_url: http://127.0.0.1/server-status?auto
|
|
40
|
+
error_log: /var/log/apache2/error.log
|
|
41
|
+
access_log: /var/log/apache2/access.log
|
|
42
|
+
timeout: 10
|
|
43
|
+
verify_ssl: true
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Run `uv run apache-aiops diagnose`. The MCP entry point is `uv run apache-aiops-mcp`. The optional HTTP probe is configured by the operator, not chosen by a tool caller. TLS verification is enabled by default, and `ca_file` accepts a custom CA PEM.
|
|
47
|
+
|
|
48
|
+
## Scope and verification
|
|
49
|
+
|
|
50
|
+
`graceful-reload` first runs `configtest`, supports dry-run and two CLI confirmations, and reports an unknown final outcome even if the command returned: existing workers may still serve the old config. It has no safe inverse when the prior config file is not snapshotted. Vhost editing, TLS certificate management and module changes remain outside this release. See [verification](docs/VERIFICATION.md). Missing operations and live-environment feedback are welcome as issues or PRs.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<!-- mcp-name: io.github.AIops-tools/apache-aiops -->
|
|
2
|
+
# Apache AIops
|
|
3
|
+
|
|
4
|
+
A dedicated, governed MCP server, CLI and skill for Apache HTTP Server with 17 MCP tools. It uses Apache's own `apachectl` diagnostics, optional operator-configured HTTP health and `mod_status` URLs, and bounded reads of an operator-configured error log plus a privacy-preserving access-log status summary. Every call is audited and budgeted by the vendored governance harness.
|
|
5
|
+
|
|
6
|
+
## What it checks
|
|
7
|
+
|
|
8
|
+
- Apache version, config syntax, parsed virtual hosts and loaded modules.
|
|
9
|
+
- Build options and parsed runtime configuration.
|
|
10
|
+
- Parsed config include tree, compiled modules and directives from static modules.
|
|
11
|
+
- Optional machine-readable `mod_status` metrics, bounded error-log tail and privacy-preserving access-status summary.
|
|
12
|
+
- Optional HTTP health status without downloading the response body.
|
|
13
|
+
- `apache_diagnose` correlates the checks and preserves failures as errors with `data: null`.
|
|
14
|
+
|
|
15
|
+
## Setup from source
|
|
16
|
+
|
|
17
|
+
Requires Python 3.11+, `uv`, and `apachectl` on the operator host. Run `uv sync --dev`, `uv run apache-aiops init`, then `uv run apache-aiops doctor`. Manual `~/.apache-aiops/config.yaml`:
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
targets:
|
|
21
|
+
- name: local
|
|
22
|
+
apachectl_bin: apachectl
|
|
23
|
+
probe_url: http://127.0.0.1/health
|
|
24
|
+
status_url: http://127.0.0.1/server-status?auto
|
|
25
|
+
error_log: /var/log/apache2/error.log
|
|
26
|
+
access_log: /var/log/apache2/access.log
|
|
27
|
+
timeout: 10
|
|
28
|
+
verify_ssl: true
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Run `uv run apache-aiops diagnose`. The MCP entry point is `uv run apache-aiops-mcp`. The optional HTTP probe is configured by the operator, not chosen by a tool caller. TLS verification is enabled by default, and `ca_file` accepts a custom CA PEM.
|
|
32
|
+
|
|
33
|
+
## Scope and verification
|
|
34
|
+
|
|
35
|
+
`graceful-reload` first runs `configtest`, supports dry-run and two CLI confirmations, and reports an unknown final outcome even if the command returned: existing workers may still serve the old config. It has no safe inverse when the prior config file is not snapshotted. Vhost editing, TLS certificate management and module changes remain outside this release. See [verification](docs/VERIFICATION.md). Missing operations and live-environment feedback are welcome as issues or PRs.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
Report vulnerabilities privately through the repository's GitHub security advisory flow when available. Do not post private virtual-host names, configuration dumps, credentials or health URLs in public issues.
|
|
4
|
+
|
|
5
|
+
Read commands accept only fixed `apachectl` options; the fixed `graceful` write first runs `configtest`. Commands execute as argument lists without a shell. The process account's permissions control which configuration and operator-selected error/access logs it can inspect; log paths are checked again on each read and cannot resolve inside this tool's audit/config directory. The access-status summary returns only HTTP status codes and counts, never raw request paths, IPs or headers; unknown log formats are reported as unparsed. HTTP probe and status URLs come from operator configuration, never agent parameters. The health probe does not return a body; `mod_status` output is bounded. TLS validation is enabled by default. Graceful reload has dry-run, CLI double confirmation and an unknown final outcome until checked.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Shared helpers for apache-aiops CLI sub-modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Annotated, Any
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.markup import escape
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
# ─── Shared Option types ───────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
TargetOption = Annotated[str | None, typer.Option("--target", "-t", help="Target name from config")]
|
|
18
|
+
DryRunOption = Annotated[
|
|
19
|
+
bool, typer.Option("--dry-run", help="Print the API call without executing")
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _cli_error_types() -> tuple[type[BaseException], ...]:
|
|
24
|
+
"""Exceptions translated to a one-line teaching error instead of a traceback.
|
|
25
|
+
|
|
26
|
+
``BudgetExceeded`` (and the retained ``PolicyDenied`` type) are raised by
|
|
27
|
+
``@governed_tool`` OUTSIDE the tool body, so ``tool_errors`` never sees them
|
|
28
|
+
and they never arrive as an ``{"error": ...}`` dict. Their message is the
|
|
29
|
+
teaching text (e.g. which budget was hit) — without them here such a refusal
|
|
30
|
+
reaches the CLI as a traceback instead.
|
|
31
|
+
"""
|
|
32
|
+
from apache_aiops.connection import ApacheCommandError
|
|
33
|
+
from apache_aiops.governance import BudgetExceeded, PolicyDenied
|
|
34
|
+
|
|
35
|
+
return (ApacheCommandError, PolicyDenied, BudgetExceeded, KeyError, OSError, ValueError)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def cli_errors(fn: Callable) -> Callable:
|
|
39
|
+
"""Translate known exceptions into one red line + exit code 1."""
|
|
40
|
+
|
|
41
|
+
@functools.wraps(fn)
|
|
42
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
43
|
+
try:
|
|
44
|
+
return fn(*args, **kwargs)
|
|
45
|
+
except (typer.Exit, typer.Abort):
|
|
46
|
+
raise
|
|
47
|
+
except _cli_error_types() as e:
|
|
48
|
+
message = str(e)
|
|
49
|
+
if isinstance(e, KeyError):
|
|
50
|
+
message = f"Missing required key or environment variable: {message}"
|
|
51
|
+
# Upstream text can contain "[/bold]"; unescaped, rich raises on it and the
|
|
52
|
+
# real error is replaced by a MarkupError traceback.
|
|
53
|
+
console.print(f"[red]Error: {escape(message)}[/]")
|
|
54
|
+
raise typer.Exit(1) from e
|
|
55
|
+
|
|
56
|
+
return wrapper
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
#: Exit status for a write whose outcome could not be determined — kept distinct
|
|
60
|
+
#: from 0 (confirmed) and 1 (failed) so a script can tell all three apart.
|
|
61
|
+
EXIT_UNDETERMINED = 2
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def governed(result: Any) -> dict:
|
|
65
|
+
"""Return a governed tool's result, or print its error and exit 1.
|
|
66
|
+
|
|
67
|
+
The ``apache_aiops.mcp_server.tools`` twins never raise: ``@tool_errors`` flattens every
|
|
68
|
+
failure — an unreachable service, a refused login, a bad argument — into
|
|
69
|
+
``{"error": ...}``. A CLI command that drops that on the floor prints an empty
|
|
70
|
+
table and exits 0 as if the service had nothing to report. Route every governed
|
|
71
|
+
call through here.
|
|
72
|
+
"""
|
|
73
|
+
# ``outcomeUnknown`` is judged BEFORE ``error``, matching the harness: a
|
|
74
|
+
# write whose response was lost carries BOTH keys, and it is audited
|
|
75
|
+
# `unknown` precisely because it may have taken effect. Reporting that as a
|
|
76
|
+
# plain failure would tell a script the change did not happen and invite the
|
|
77
|
+
# double-apply the payload's own note warns about.
|
|
78
|
+
if isinstance(result, dict) and result.get("outcomeUnknown"):
|
|
79
|
+
console.print(f"[yellow]Outcome undetermined: {escape(str(result.get('note') or ''))}[/]")
|
|
80
|
+
raise typer.Exit(EXIT_UNDETERMINED)
|
|
81
|
+
if isinstance(result, dict) and result.get("error"):
|
|
82
|
+
console.print(f"[red]Error: {escape(str(result['error']))}[/]")
|
|
83
|
+
raise typer.Exit(1)
|
|
84
|
+
return result if isinstance(result, dict) else {}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def dry_run_print(*, operation: str, api_call: str, parameters: dict | None = None) -> None:
|
|
88
|
+
"""Print a dry-run preview of the API call that would be made."""
|
|
89
|
+
console.print("\n[bold magenta][DRY-RUN] No changes will be made.[/]")
|
|
90
|
+
console.print(f"[magenta] Operation: {operation}[/]")
|
|
91
|
+
console.print(f"[magenta] API Call: {api_call}[/]")
|
|
92
|
+
for k, v in (parameters or {}).items():
|
|
93
|
+
console.print(f"[magenta] Param: {k} = {v}[/]")
|
|
94
|
+
console.print("[magenta] Run without --dry-run to execute.[/]\n")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def double_confirm(action: str, resource: str) -> None:
|
|
98
|
+
"""Require two confirmations for a destructive operation."""
|
|
99
|
+
console.print(f"[bold yellow]⚠️ About to: {action} '{resource}'[/]")
|
|
100
|
+
typer.confirm(f"Confirm 1/2: {action} '{resource}'?", abort=True)
|
|
101
|
+
typer.confirm(
|
|
102
|
+
f"Confirm 2/2: really {action} '{resource}'? This may be irreversible.",
|
|
103
|
+
abort=True,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def print_truncation_note(result: dict, noun: str) -> None:
|
|
108
|
+
"""Print a visible warning when a listing envelope was capped.
|
|
109
|
+
|
|
110
|
+
The JSON already carries ``truncated``, but a human scrolling a long dump
|
|
111
|
+
will not spot it — and neither will a model summarising the terminal. Say
|
|
112
|
+
it in words, on the last line, where it cannot be missed.
|
|
113
|
+
"""
|
|
114
|
+
if isinstance(result, dict) and result.get("truncated"):
|
|
115
|
+
console.print(
|
|
116
|
+
f"[yellow]… showing {result.get('returned')} of more {noun} — "
|
|
117
|
+
f"truncated, re-run with a higher --limit[/]"
|
|
118
|
+
)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Apache AIops CLI; service reads call governed MCP twins."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from apache_aiops.cli._common import cli_errors, console, double_confirm, governed
|
|
13
|
+
from apache_aiops.cli.undo import undo_app
|
|
14
|
+
from apache_aiops.governance.paths import ops_home
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(
|
|
17
|
+
name="apache-aiops", no_args_is_help=True, help="Audited Apache HTTP Server diagnosis."
|
|
18
|
+
)
|
|
19
|
+
app.add_typer(undo_app, name="undo")
|
|
20
|
+
Target = Annotated[str | None, typer.Option("--target", "-t", help="Configured Apache target")]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _print(result: dict) -> None:
|
|
24
|
+
console.print_json(json.dumps(governed(result)))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command("config-includes")
|
|
28
|
+
@cli_errors
|
|
29
|
+
def config_includes(target: Target = None) -> None:
|
|
30
|
+
"""Show the parsed Apache config include tree."""
|
|
31
|
+
from apache_aiops.mcp_server.tools.httpd import apache_config_includes
|
|
32
|
+
_print(apache_config_includes(target=target))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command("compiled-modules")
|
|
36
|
+
@cli_errors
|
|
37
|
+
def compiled_modules(target: Target = None) -> None:
|
|
38
|
+
"""List modules compiled into Apache."""
|
|
39
|
+
from apache_aiops.mcp_server.tools.httpd import apache_compiled_modules
|
|
40
|
+
_print(apache_compiled_modules(target=target))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command("static-directives")
|
|
44
|
+
@cli_errors
|
|
45
|
+
def static_directives(target: Target = None) -> None:
|
|
46
|
+
"""List directives supplied by static Apache modules."""
|
|
47
|
+
from apache_aiops.mcp_server.tools.httpd import apache_static_directives
|
|
48
|
+
_print(apache_static_directives(target=target))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.command("build")
|
|
52
|
+
@cli_errors
|
|
53
|
+
def build(target: Target = None) -> None:
|
|
54
|
+
"""Show Apache build flags and paths."""
|
|
55
|
+
from apache_aiops.mcp_server.tools.httpd import apache_build
|
|
56
|
+
_print(apache_build(target=target))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command("runtime-config")
|
|
60
|
+
@cli_errors
|
|
61
|
+
def runtime_config(target: Target = None) -> None:
|
|
62
|
+
"""Show parsed Apache runtime configuration."""
|
|
63
|
+
from apache_aiops.mcp_server.tools.httpd import apache_runtime_config
|
|
64
|
+
_print(apache_runtime_config(target=target))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.command("server-status")
|
|
68
|
+
@cli_errors
|
|
69
|
+
def server_status(target: Target = None) -> None:
|
|
70
|
+
"""Show configured mod_status auto metrics."""
|
|
71
|
+
from apache_aiops.mcp_server.tools.httpd import apache_server_status
|
|
72
|
+
_print(apache_server_status(target=target))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.command("error-log")
|
|
76
|
+
@cli_errors
|
|
77
|
+
def error_log(limit: int = 50, target: Target = None) -> None:
|
|
78
|
+
"""Tail the operator-configured Apache error log."""
|
|
79
|
+
from apache_aiops.mcp_server.tools.httpd import apache_error_log
|
|
80
|
+
_print(apache_error_log(limit=limit, target=target))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.command("access-status")
|
|
84
|
+
@cli_errors
|
|
85
|
+
def access_status(limit: int = 200, target: Target = None) -> None:
|
|
86
|
+
"""Summarize recent common/combined access-log status codes."""
|
|
87
|
+
from apache_aiops.mcp_server.tools.httpd import apache_access_status
|
|
88
|
+
_print(apache_access_status(limit=limit, target=target))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@app.command("graceful-reload")
|
|
92
|
+
@cli_errors
|
|
93
|
+
def graceful_reload(dry_run: bool = False, target: Target = None) -> None:
|
|
94
|
+
"""Check config and request an Apache graceful reload."""
|
|
95
|
+
from apache_aiops.mcp_server.tools.httpd import apache_graceful_reload
|
|
96
|
+
if not dry_run:
|
|
97
|
+
double_confirm("gracefully reload Apache", target or "default")
|
|
98
|
+
_print(apache_graceful_reload(dry_run=dry_run, target=target))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.command("init")
|
|
102
|
+
@cli_errors
|
|
103
|
+
def init() -> None:
|
|
104
|
+
"""Configure a local Apache HTTP Server target."""
|
|
105
|
+
from apache_aiops.config import TargetConfig
|
|
106
|
+
|
|
107
|
+
name = typer.prompt("Target name", default="local")
|
|
108
|
+
probe_url = typer.prompt("HTTP health URL (optional)", default="")
|
|
109
|
+
status_url = typer.prompt("mod_status auto URL (optional)", default="")
|
|
110
|
+
error_log = typer.prompt("Absolute error log path (optional)", default="")
|
|
111
|
+
access_log = typer.prompt("Absolute access log path (optional)", default="")
|
|
112
|
+
chosen = TargetConfig(
|
|
113
|
+
name=name, probe_url=probe_url or None, status_url=status_url or None,
|
|
114
|
+
error_log=error_log or None, access_log=access_log or None,
|
|
115
|
+
)
|
|
116
|
+
path = ops_home() / "config.yaml"
|
|
117
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
if path.exists():
|
|
119
|
+
raise ValueError(f"Config exists at {path}; edit it to add a target.")
|
|
120
|
+
path.write_text(
|
|
121
|
+
yaml.safe_dump({"targets": [{
|
|
122
|
+
"name": chosen.name, "probe_url": chosen.probe_url,
|
|
123
|
+
"status_url": chosen.status_url, "error_log": chosen.error_log,
|
|
124
|
+
"access_log": chosen.access_log,
|
|
125
|
+
}]})
|
|
126
|
+
)
|
|
127
|
+
path.chmod(0o600)
|
|
128
|
+
console.print(f"Configured {name} at {path}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@app.command("version")
|
|
132
|
+
@cli_errors
|
|
133
|
+
def version(target: Target = None) -> None:
|
|
134
|
+
"""Show Apache version."""
|
|
135
|
+
from apache_aiops.mcp_server.tools.httpd import apache_version
|
|
136
|
+
|
|
137
|
+
_print(apache_version(target=target))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command("config-check")
|
|
141
|
+
@cli_errors
|
|
142
|
+
def config_check(target: Target = None) -> None:
|
|
143
|
+
"""Run Apache config syntax test."""
|
|
144
|
+
from apache_aiops.mcp_server.tools.httpd import apache_config_check
|
|
145
|
+
|
|
146
|
+
_print(apache_config_check(target=target))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@app.command("vhosts")
|
|
150
|
+
@cli_errors
|
|
151
|
+
def vhosts(target: Target = None) -> None:
|
|
152
|
+
"""Show parsed virtual-host map."""
|
|
153
|
+
from apache_aiops.mcp_server.tools.httpd import apache_vhosts
|
|
154
|
+
|
|
155
|
+
_print(apache_vhosts(target=target))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@app.command("modules")
|
|
159
|
+
@cli_errors
|
|
160
|
+
def modules(target: Target = None) -> None:
|
|
161
|
+
"""List loaded Apache modules."""
|
|
162
|
+
from apache_aiops.mcp_server.tools.httpd import apache_modules
|
|
163
|
+
|
|
164
|
+
_print(apache_modules(target=target))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@app.command("probe")
|
|
168
|
+
@cli_errors
|
|
169
|
+
def probe(target: Target = None) -> None:
|
|
170
|
+
"""Probe configured HTTP health URL."""
|
|
171
|
+
from apache_aiops.mcp_server.tools.httpd import apache_probe
|
|
172
|
+
|
|
173
|
+
_print(apache_probe(target=target))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@app.command("diagnose")
|
|
177
|
+
@cli_errors
|
|
178
|
+
def diagnose(target: Target = None) -> None:
|
|
179
|
+
"""Correlate Apache config and HTTP probes."""
|
|
180
|
+
from apache_aiops.mcp_server.tools.httpd import apache_diagnose
|
|
181
|
+
|
|
182
|
+
_print(apache_diagnose(target=target))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@app.command("doctor")
|
|
186
|
+
@cli_errors
|
|
187
|
+
def doctor(target: Target = None) -> None:
|
|
188
|
+
"""Test access to apachectl -v."""
|
|
189
|
+
from apache_aiops.mcp_server.tools.httpd import apache_version
|
|
190
|
+
|
|
191
|
+
_print(apache_version(target=target))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@app.command("mcp")
|
|
195
|
+
@cli_errors
|
|
196
|
+
def mcp_cmd() -> None:
|
|
197
|
+
"""Start stdio MCP server."""
|
|
198
|
+
if sys.version_info < (3, 11):
|
|
199
|
+
raise typer.Exit(2)
|
|
200
|
+
from apache_aiops.mcp_server.server import main
|
|
201
|
+
|
|
202
|
+
main()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
if __name__ == "__main__":
|
|
206
|
+
app()
|