NexusCoder / nexus /tools /ansible_tool.py
AdminReal's picture
Import NexusCoder from github.com/mhieuhonda/NexusCoder
eca5751 verified
Raw History Blame Contribute Delete
7.18 kB
"""
Ansible Tool - Wrap ansible-playbook & ansible ad-hoc commands.
Author: Hieu Louis (2026)
"""
from __future__ import annotations
import os
import shlex
import subprocess
from typing import Dict, Any, List, Optional
from .base import Tool, ToolResult, ToolContext, ToolCategory, ToolSafety
# Operation được hỗ trợ
ANSIBLE_OPERATIONS = {
"playbook", "ping", "command", "shell", "copy", "setup",
"facts", "module",
}
# Read-only operations
READONLY_OPS = {"ping", "setup", "facts"}
# Mutating operations (cần confirmation + dry_run)
WRITE_OPS = {"playbook", "command", "shell", "copy", "module"}
class AnsibleTool(Tool):
"""Wrap `ansible-playbook` và `ansible` ad-hoc commands."""
category = ToolCategory.DEVOPS
safety = ToolSafety.DANGEROUS
requires_confirmation = True
@property
def name(self) -> str:
return "ansible"
@property
def description(self) -> str:
return (
"Wrap ansible CLI: run playbooks, ad-hoc commands (ping, command, "
"shell, copy, setup/facts). Hỗ trợ inventory, become, check mode."
)
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": sorted(ANSIBLE_OPERATIONS),
},
"playbook": {"type": "string", "description": "Path to playbook YAML"},
"inventory": {"type": "string", "description": "Inventory file hoặc comma-separated hosts"},
"host_pattern": {"type": "string", "description": "Target host pattern (ad-hoc)", "default": "all"},
"module": {"type": "string", "description": "Ansible module name (module operation)"},
"args": {"type": "string", "description": "Module args e.g. 'cmd=uptime'"},
"become": {"type": "boolean", "default": False, "description": "sudo/privilege escalation"},
"become_user": {"type": "string", "default": "root"},
"check_mode": {"type": "boolean", "default": False, "description": "--check (dry-run mode)"},
"extra_vars": {
"type": "object",
"description": "-e key=value",
},
"tags": {"type": "array", "items": {"type": "string"}, "description": "--tags"},
"verbose": {"type": "integer", "default": 0, "description": "-v level (0-4)"},
},
"required": ["operation"],
}
def validate_args(self, args: Dict[str, Any]) -> Optional[str]:
op = args.get("operation")
if not op:
return "Missing required arg: operation"
if op not in ANSIBLE_OPERATIONS:
return f"Unsupported operation: {op}"
if op == "playbook" and not args.get("playbook"):
return "Operation 'playbook' requires 'playbook' arg"
if op == "module" and not args.get("module"):
return "Operation 'module' requires 'module' arg"
return None
def _common_flags(self, args: Dict[str, Any]) -> List[str]:
flags: List[str] = []
if args.get("inventory"):
flags += ["-i", args["inventory"]]
if args.get("become"):
flags.append("--become")
if args.get("become_user"):
flags += ["--become-user", args["become_user"]]
if args.get("check_mode"):
flags.append("--check")
if args.get("extra_vars"):
for k, v in args["extra_vars"].items():
flags += ["-e", f"{k}={v}"]
if args.get("tags"):
flags += ["--tags", ",".join(args["tags"])]
v = int(args.get("verbose", 0))
if v > 0:
flags.append("-" + "v" * min(v, 4))
return flags
def _build_command(self, args: Dict[str, Any]) -> List[str]:
op = args["operation"]
common = self._common_flags(args)
if op == "playbook":
return ["ansible-playbook"] + common + [args["playbook"]]
if op == "ping":
return ["ansible", args.get("host_pattern", "all"), "-m", "ping"] + common
if op in {"command", "shell"}:
mod = "command" if op == "command" else "shell"
return ["ansible", args.get("host_pattern", "all"), "-m", mod] + (
["-a", args["args"]] if args.get("args") else []
) + common
if op == "copy":
return ["ansible", args.get("host_pattern", "all"), "-m", "copy"] + (
["-a", args["args"]] if args.get("args") else []
) + common
if op in {"setup", "facts"}:
return ["ansible", args.get("host_pattern", "all"), "-m", "setup"] + common
if op == "module":
cmd = ["ansible", args.get("host_pattern", "all"), "-m", args["module"]]
if args.get("args"):
cmd += ["-a", args["args"]]
return cmd + common
return ["ansible"]
def execute(self, args: Dict[str, Any], context: ToolContext) -> ToolResult:
op = args["operation"]
cmd = self._build_command(args)
cwd = context.working_dir
# Dry-run simulation (trừ khi --check đã được set)
if context.dry_run and op in WRITE_OPS and not args.get("check_mode"):
sim_cmd = list(cmd)
# chèn --check để simulate
if op == "playbook":
sim_cmd.insert(1, "--check")
return ToolResult(
success=True,
output=f"[dry-run] Would execute: {' '.join(sim_cmd)}",
metadata={
"dry_run": True,
"command": sim_cmd,
"operation": op,
"cwd": cwd,
},
)
env = dict(os.environ)
env.update(context.env)
try:
result = subprocess.run(
cmd,
cwd=cwd,
env=env,
capture_output=True,
text=True,
timeout=context.timeout,
check=False,
)
return ToolResult(
success=(result.returncode == 0),
output=result.stdout,
error=result.stderr or None,
return_code=result.returncode,
metadata={
"operation": op,
"command": cmd,
"cwd": cwd,
"dry_run": False,
},
)
except FileNotFoundError:
return ToolResult(
success=False,
error="ansible CLI not found. Cài đặt Ansible.",
return_code=127,
)
except subprocess.TimeoutExpired:
return ToolResult(
success=False,
error=f"ansible timed out after {context.timeout}s",
return_code=124,
)
except Exception as e:
return ToolResult(success=False, error=str(e), return_code=1)