noc-ops 0.1.0__py3-none-any.whl
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.
Potentially problematic release.
This version of noc-ops might be problematic. Click here for more details.
- noc_ops/__init__.py +2 -0
- noc_ops/agent.py +486 -0
- noc_ops/assets/ops-config.default.toml +127 -0
- noc_ops/assets/runbooks/README.md +18 -0
- noc_ops/assets/runbooks/alb-access-log-decommission.md +53 -0
- noc_ops/assets/runbooks/certbot-renewal-troubleshooting.md +87 -0
- noc_ops/assets/runbooks/ec2-public-exposure-hardening.md +79 -0
- noc_ops/assets/runbooks/s3-bucket-usage-check.md +52 -0
- noc_ops/assets/runbooks/site-access-source-stats.md +75 -0
- noc_ops/aws_safety.py +404 -0
- noc_ops/cli.py +213 -0
- noc_ops/dashboard/__init__.py +1 -0
- noc_ops/dashboard/favicon.svg +4 -0
- noc_ops/dashboard/index.html +1091 -0
- noc_ops/dashboard/server.py +483 -0
- noc_ops/dashboard/vendor/ag-grid-community.min.js +14024 -0
- noc_ops/dashboard/vendor/fonts/inter-latin-wght-normal.woff2 +0 -0
- noc_ops/deploy_config.py +264 -0
- noc_ops/doctor.py +141 -0
- noc_ops/egress_probe.py +535 -0
- noc_ops/error_patterns.py +200 -0
- noc_ops/init_wizard.py +259 -0
- noc_ops/license.py +305 -0
- noc_ops/mcp_server.py +397 -0
- noc_ops/netprobe.py +183 -0
- noc_ops/ops_log.py +145 -0
- noc_ops/paths.py +110 -0
- noc_ops/providers/__init__.py +25 -0
- noc_ops/providers/aliyun.py +279 -0
- noc_ops/providers/aws.py +455 -0
- noc_ops/providers/azure.py +359 -0
- noc_ops/providers/base.py +122 -0
- noc_ops/providers/cloudflare.py +81 -0
- noc_ops/providers/gcp.py +365 -0
- noc_ops/providers/local.py +131 -0
- noc_ops/providers/oracle.py +290 -0
- noc_ops/providers/sangfor.py +396 -0
- noc_ops/providers/supabase.py +88 -0
- noc_ops/providers/synology.py +201 -0
- noc_ops/providers/tencent.py +184 -0
- noc_ops/providers/vmware.py +291 -0
- noc_ops/providers/volcengine.py +319 -0
- noc_ops/reboot_cli.py +197 -0
- noc_ops/schedule_ops.py +218 -0
- noc_ops/setup_clients.py +216 -0
- noc_ops/skill/SKILL.md.tmpl +63 -0
- noc_ops/sync.py +1572 -0
- noc_ops-0.1.0.dist-info/METADATA +275 -0
- noc_ops-0.1.0.dist-info/RECORD +53 -0
- noc_ops-0.1.0.dist-info/WHEEL +4 -0
- noc_ops-0.1.0.dist-info/entry_points.txt +3 -0
- noc_ops-0.1.0.dist-info/licenses/LICENSE +201 -0
- noc_ops-0.1.0.dist-info/licenses/NOTICE +18 -0
noc_ops/__init__.py
ADDED
noc_ops/agent.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""自然语言 AWS 运维 Agent(Claude Agent SDK 实现)。
|
|
3
|
+
|
|
4
|
+
用法:
|
|
5
|
+
python agent.py "重启东京的 openclaw" # 一次性指令
|
|
6
|
+
python agent.py # 进入交互模式(日常运维 + 沉淀 runbook)
|
|
7
|
+
|
|
8
|
+
设计:Runbook 驱动。
|
|
9
|
+
- inventory.md : 操作对象清单(实例名 → id / region / profile)
|
|
10
|
+
- runbooks/*.md : 能力库,Agent 执行前先查有没有对应剧本
|
|
11
|
+
- 安全闸门 : 只读操作自动执行;写操作(启停/重启/远程命令/写文件)先确认
|
|
12
|
+
"""
|
|
13
|
+
import asyncio
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import subprocess
|
|
18
|
+
import shlex
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
from claude_agent_sdk import (
|
|
22
|
+
ClaudeSDKClient,
|
|
23
|
+
ClaudeAgentOptions,
|
|
24
|
+
AssistantMessage,
|
|
25
|
+
ResultMessage,
|
|
26
|
+
TextBlock,
|
|
27
|
+
ToolUseBlock,
|
|
28
|
+
tool,
|
|
29
|
+
create_sdk_mcp_server,
|
|
30
|
+
)
|
|
31
|
+
from claude_agent_sdk.types import PermissionResultAllow, PermissionResultDeny
|
|
32
|
+
|
|
33
|
+
from . import deploy_config
|
|
34
|
+
from . import ops_log
|
|
35
|
+
from . import paths
|
|
36
|
+
from . import schedule_ops
|
|
37
|
+
from .aws_safety import is_read_only_ssh
|
|
38
|
+
from .error_patterns import annotate
|
|
39
|
+
from .providers import PROVIDERS
|
|
40
|
+
|
|
41
|
+
# CLI 二进制 → cloud(单一来源=Provider 注册表,加云自动跟上;别再手写字典——漏一处写操作就绕过闸门)
|
|
42
|
+
_CLI_CLOUD = {p.cli: c for c, p in PROVIDERS.items() if p.cli}
|
|
43
|
+
# 工具名后缀 → cloud(用于权限闸门按 provider 分类)。AWS 的工具叫 aws_cli(二进制 aws),单独映射
|
|
44
|
+
_TOOL_CLOUD = {"__aws_cli": "aws", **{"__" + cli: c for cli, c in _CLI_CLOUD.items() if cli != "aws"}}
|
|
45
|
+
|
|
46
|
+
MODEL = os.environ.get("OPS_MODEL") or deploy_config.model() # env 优先,否则读 ops-config.toml
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# 自定义工具
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
def _run_cli(binary: str, command: str) -> dict:
|
|
53
|
+
"""跑一条云 CLI(binary 之后的参数)。各云 CLI 工具共用。写操作记入 ops 操作日志。"""
|
|
54
|
+
command = command.strip()
|
|
55
|
+
if command.startswith(binary + " "):
|
|
56
|
+
command = command[len(binary) + 1:]
|
|
57
|
+
cloud = _CLI_CLOUD.get(binary, binary)
|
|
58
|
+
p = PROVIDERS.get(cloud)
|
|
59
|
+
is_write = bool(p) and not p.is_read(command) # 写操作(含重启)才记日志,只读不记
|
|
60
|
+
try:
|
|
61
|
+
argv = [binary, *shlex.split(command)]
|
|
62
|
+
except ValueError as e:
|
|
63
|
+
if is_write:
|
|
64
|
+
ops_log.record("agent", "cli", cloud=cloud, detail=command, result="parse-error")
|
|
65
|
+
return {"content": [{"type": "text", "text": f"命令解析失败: {e}"}], "is_error": True}
|
|
66
|
+
try:
|
|
67
|
+
r = subprocess.run(argv, capture_output=True, text=True, timeout=120,
|
|
68
|
+
env={**os.environ, "AWS_PAGER": ""})
|
|
69
|
+
except subprocess.TimeoutExpired:
|
|
70
|
+
if is_write:
|
|
71
|
+
ops_log.record("agent", "cli", cloud=cloud, detail=command, result="timeout")
|
|
72
|
+
return {"content": [{"type": "text", "text": f"{binary} 命令超时(120s)"}], "is_error": True}
|
|
73
|
+
out = (r.stdout or "") + (("\n[stderr]\n" + r.stderr) if r.stderr else "")
|
|
74
|
+
if is_write:
|
|
75
|
+
ops_log.record("agent", "cli", cloud=cloud, detail=command,
|
|
76
|
+
result="ok" if r.returncode == 0 else f"exit{r.returncode}")
|
|
77
|
+
if r.returncode != 0:
|
|
78
|
+
return {"content": [{"type": "text", "text": annotate(f"退出码 {r.returncode}\n{out}")}], "is_error": True}
|
|
79
|
+
return {"content": [{"type": "text", "text": out or "(无输出)"}]}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@tool(
|
|
83
|
+
"aws_cli",
|
|
84
|
+
"执行一条 AWS CLI 命令。传入的是 'aws' 之后的参数(不含开头的 aws)。"
|
|
85
|
+
"必须显式带上 --profile 和 --region(取自 inventory.md)。"
|
|
86
|
+
"例如 command='ec2 describe-instances --profile <账号> --region <区域>'(账号/区域取自 inventory.md)",
|
|
87
|
+
{"command": str},
|
|
88
|
+
)
|
|
89
|
+
async def aws_cli(args: dict) -> dict:
|
|
90
|
+
return _run_cli("aws", args["command"])
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@tool(
|
|
94
|
+
"tccli",
|
|
95
|
+
"执行一条腾讯云 CLI (tccli) 命令,用于操作腾讯云资源(如南京 Lighthouse)。"
|
|
96
|
+
"传入 'tccli' 之后的参数,**必须带 --region**。"
|
|
97
|
+
"例:command='lighthouse DescribeInstances --region ap-nanjing';"
|
|
98
|
+
"重启:command='lighthouse RebootInstances --InstanceIds <lhins-xxx> --region <区域>'",
|
|
99
|
+
{"command": str},
|
|
100
|
+
)
|
|
101
|
+
async def tccli(args: dict) -> dict:
|
|
102
|
+
return _run_cli("tccli", args["command"])
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@tool(
|
|
106
|
+
"aliyun",
|
|
107
|
+
"执行一条阿里云 CLI 命令,操作阿里云资源(如 ECS)。"
|
|
108
|
+
"传入 'aliyun' 之后的参数,**必须带 --RegionId 和 --profile**(profile/区域取自 inventory.md)。"
|
|
109
|
+
"例:command='ecs DescribeInstances --RegionId <区域> --profile <profile>';"
|
|
110
|
+
"重启:command='ecs RebootInstance --InstanceId <i-xxx> --RegionId <区域> --profile <profile>'",
|
|
111
|
+
{"command": str},
|
|
112
|
+
)
|
|
113
|
+
async def aliyun(args: dict) -> dict:
|
|
114
|
+
return _run_cli("aliyun", args["command"])
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@tool(
|
|
118
|
+
"ve",
|
|
119
|
+
"执行一条火山引擎 CLI (ve) 命令,操作火山引擎资源(如 ECS)。"
|
|
120
|
+
"传入 've' 之后的参数,**必须带 --region**(区域取自 inventory.md)。列表参数用点号下标:--InstanceIds.1 <id>。"
|
|
121
|
+
"例:command='ecs DescribeInstances --region cn-beijing';"
|
|
122
|
+
"重启:command='ecs RebootInstance --InstanceId <i-xxx> --region <区域>'",
|
|
123
|
+
{"command": str},
|
|
124
|
+
)
|
|
125
|
+
async def ve(args: dict) -> dict:
|
|
126
|
+
return _run_cli("ve", args["command"])
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@tool(
|
|
130
|
+
"gcloud",
|
|
131
|
+
"执行一条 Google Cloud CLI (gcloud) 命令,操作 GCP 资源(如 Compute Engine 实例)。"
|
|
132
|
+
"传入 'gcloud' 之后的参数,**必须带 --project**(项目 id 取自 inventory.md)。"
|
|
133
|
+
"例:command='compute instances list --project <项目>';"
|
|
134
|
+
"重启(硬reset,免确认):command='compute instances reset <名> --zone <zone> --project <项目>';"
|
|
135
|
+
"停止/启动(需确认):command='compute instances stop|start <名> --zone <zone> --project <项目>'",
|
|
136
|
+
{"command": str},
|
|
137
|
+
)
|
|
138
|
+
async def gcloud(args: dict) -> dict:
|
|
139
|
+
return _run_cli("gcloud", args["command"])
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@tool(
|
|
143
|
+
"az",
|
|
144
|
+
"执行一条 Microsoft Azure CLI (az) 命令,操作 Azure 资源(如虚拟机 VM)。"
|
|
145
|
+
"传入 'az' 之后的参数,**必须带 --subscription**(订阅 id 取自 inventory.md)。"
|
|
146
|
+
"例:command='vm list --subscription <订阅> --show-details -o json';"
|
|
147
|
+
"重启(优雅 restart,免确认):command='vm restart --name <名> --resource-group <rg> --subscription <订阅>';"
|
|
148
|
+
"停止/启动(需确认):command='vm stop|start --name <名> --resource-group <rg> --subscription <订阅>'。"
|
|
149
|
+
"注意 Azure VM 操作要 **--name + --resource-group**(不是单一 id)。",
|
|
150
|
+
{"command": str},
|
|
151
|
+
)
|
|
152
|
+
async def az(args: dict) -> dict:
|
|
153
|
+
return _run_cli("az", args["command"])
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# 用密码(非密钥)登录的主机:密码存 macOS Keychain,绝不写进代码/文件;命中则用 sshpass+Keychain。
|
|
157
|
+
# 默认为空(密钥登录走 ssh config 别名);如有口令登录主机在 ops-config.toml 的 [agent.password_hosts] 配置。保留机制以备将来。
|
|
158
|
+
PASSWORD_HOSTS = deploy_config.password_hosts()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@tool(
|
|
162
|
+
"ssh_exec",
|
|
163
|
+
"通过 SSH 在远程主机上执行一条命令。target 用 ~/.ssh/config 的 Host 别名 / user@host。"
|
|
164
|
+
"当前所有纳管主机均密钥登录;密码登录机制保留备用(命中 PASSWORD_HOSTS 时用 sshpass+Keychain,现为空)。"
|
|
165
|
+
"command 是要在远程执行的命令。AWS 内网实例经跳板机(ssh config 里 ProxyJump)。",
|
|
166
|
+
{"target": str, "command": str},
|
|
167
|
+
)
|
|
168
|
+
async def ssh_exec(args: dict) -> dict:
|
|
169
|
+
target = args["target"].strip()
|
|
170
|
+
command = args["command"]
|
|
171
|
+
env = dict(os.environ)
|
|
172
|
+
if target in PASSWORD_HOSTS:
|
|
173
|
+
h = PASSWORD_HOSTS[target]
|
|
174
|
+
pw = subprocess.run(
|
|
175
|
+
["security", "find-generic-password", "-a", h["kc_account"], "-s", h["kc_service"], "-w"],
|
|
176
|
+
capture_output=True, text=True,
|
|
177
|
+
).stdout.strip()
|
|
178
|
+
if not pw:
|
|
179
|
+
return {"content": [{"type": "text", "text": f"从 Keychain 取 {target} 密码失败"}], "is_error": True}
|
|
180
|
+
env["SSHPASS"] = pw # 用 -e 走环境变量,避免密码出现在进程参数里
|
|
181
|
+
argv = ["sshpass", "-e", "ssh", "-o", "StrictHostKeyChecking=no",
|
|
182
|
+
"-o", "ConnectTimeout=10", f"{h['user']}@{h['host']}", command]
|
|
183
|
+
else:
|
|
184
|
+
argv = ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", target, command]
|
|
185
|
+
is_write = not is_read_only_ssh(command) # SSH 写操作才记日志
|
|
186
|
+
try:
|
|
187
|
+
r = subprocess.run(argv, capture_output=True, text=True, timeout=120, env=env)
|
|
188
|
+
except subprocess.TimeoutExpired:
|
|
189
|
+
if is_write:
|
|
190
|
+
ops_log.record("agent", "ssh", target=target, detail=command, result="timeout")
|
|
191
|
+
return {"content": [{"type": "text", "text": "ssh 命令超时(120s)"}], "is_error": True}
|
|
192
|
+
out = (r.stdout or "") + (("\n[stderr]\n" + r.stderr) if r.stderr else "")
|
|
193
|
+
if is_write:
|
|
194
|
+
ops_log.record("agent", "ssh", target=target, detail=command,
|
|
195
|
+
result="ok" if r.returncode == 0 else f"exit{r.returncode}")
|
|
196
|
+
if r.returncode != 0:
|
|
197
|
+
return {"content": [{"type": "text", "text": annotate(f"退出码 {r.returncode}\n{out}")}], "is_error": True}
|
|
198
|
+
return {"content": [{"type": "text", "text": out or "(无输出)"}]}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
# 计划任务(定时重启):AWS EventBridge Scheduler,云端到点执行,本机不常驻
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
DATA_JSON = paths.data_json_path()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _ok(text):
|
|
208
|
+
return {"content": [{"type": "text", "text": text}]}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _err(text):
|
|
212
|
+
return {"content": [{"type": "text", "text": text}], "is_error": True}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _find_inst(id_or_name):
|
|
216
|
+
try:
|
|
217
|
+
d = json.loads(DATA_JSON.read_text())
|
|
218
|
+
except (OSError, json.JSONDecodeError):
|
|
219
|
+
return None
|
|
220
|
+
for i in d.get("instances", []):
|
|
221
|
+
if i.get("id") == id_or_name or i.get("name") == id_or_name:
|
|
222
|
+
return i
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@tool(
|
|
227
|
+
"schedule_task",
|
|
228
|
+
"创建定时重启实例的计划任务(云端到点执行,本机不用常驻)。支持 AWS / 腾讯 / 阿里。"
|
|
229
|
+
"instance=实例名或 ID;schedule=一次性本地时间 ISO(如 2026-06-06T22:30:00,按北京时间)"
|
|
230
|
+
"或循环的 5 段标准 crontab(分 时 日 月 周,如 '30 22 * * *' 表示每天 22:30,'0 3 * * *' 每天凌晨3点)。"
|
|
231
|
+
"今天日期见系统提示;把口语时间换算成确切 ISO 或 crontab。目标解析不到或该账号未接入会如实回报。",
|
|
232
|
+
{"instance": str, "schedule": str},
|
|
233
|
+
)
|
|
234
|
+
async def schedule_task(args):
|
|
235
|
+
inst = _find_inst(args["instance"].strip())
|
|
236
|
+
if not inst:
|
|
237
|
+
return _err(f"未找到实例「{args['instance']}」,请先确认实例名或 ID(可先 list/Read inventory)。")
|
|
238
|
+
if not schedule_ops.is_supported(inst.get("cloud"), inst.get("profile")):
|
|
239
|
+
return _err(schedule_ops.unsupported_reason(inst.get("cloud"), inst.get("profile")))
|
|
240
|
+
sched = args["schedule"].strip()
|
|
241
|
+
one_off = bool(re.match(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}", sched))
|
|
242
|
+
ok, res = schedule_ops.create_reboot(
|
|
243
|
+
inst["cloud"], inst["profile"], inst["region"], inst["id"], inst["name"],
|
|
244
|
+
when=sched if one_off else None, crontab=None if one_off else sched)
|
|
245
|
+
kind = "一次性" if one_off else "循环"
|
|
246
|
+
ops_log.record("agent", "schedule_task", target=inst["name"], cloud=inst["cloud"],
|
|
247
|
+
detail=f"{kind}重启 @ {sched}", result="ok" if ok else "fail")
|
|
248
|
+
if not ok:
|
|
249
|
+
return _err(annotate(f"创建定时任务失败:{res}"))
|
|
250
|
+
return _ok(f"✅ 已创建{kind}定时重启:{inst['name']} ({inst['id']}, {inst['cloud']}) "
|
|
251
|
+
f"@ {sched} [{schedule_ops.DISPLAY_TZ}]\n标识:{res}")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@tool(
|
|
255
|
+
"list_scheduled_tasks",
|
|
256
|
+
"列出当前所有计划任务(定时重启),含目标实例 / 时间或循环表达式 / 状态。",
|
|
257
|
+
{},
|
|
258
|
+
)
|
|
259
|
+
async def list_scheduled_tasks(args):
|
|
260
|
+
rows = schedule_ops.list_all()
|
|
261
|
+
if not rows:
|
|
262
|
+
return _ok("当前没有计划任务。")
|
|
263
|
+
tzlabel = lambda tz: schedule_ops.DISPLAY_TZ if tz == "Asia/Shanghai" else tz
|
|
264
|
+
lines = [f"- {r['name']}:{r['kind']}重启 {r['instance']}({r['instance_id']}) "
|
|
265
|
+
f"@ {r['expr']} [{tzlabel(r['timezone'])}] {r['state']}" for r in rows]
|
|
266
|
+
return _ok("当前计划任务:\n" + "\n".join(lines))
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@tool(
|
|
270
|
+
"cancel_scheduled_task",
|
|
271
|
+
"取消(删除)一条计划任务。name 取自 list_scheduled_tasks 返回的调度名。",
|
|
272
|
+
{"name": str},
|
|
273
|
+
)
|
|
274
|
+
async def cancel_scheduled_task(args):
|
|
275
|
+
name = args["name"].strip()
|
|
276
|
+
row = next((r for r in schedule_ops.list_all() if r["name"] == name), None)
|
|
277
|
+
if not row:
|
|
278
|
+
return _err(f"未找到计划任务「{name}」,先用 list_scheduled_tasks 确认。")
|
|
279
|
+
ok, out = schedule_ops.delete_row(row)
|
|
280
|
+
ops_log.record("agent", "cancel_task", target=row.get("instance", name),
|
|
281
|
+
cloud=row.get("cloud", ""), detail=name, result="ok" if ok else "fail")
|
|
282
|
+
return _ok(f"✅ 已取消计划任务 {name}") if ok else _err(annotate(f"取消失败:{out}"))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
ops_server = create_sdk_mcp_server(
|
|
286
|
+
name="ops", version="1.0.0",
|
|
287
|
+
tools=[aws_cli, tccli, aliyun, ve, gcloud, az, ssh_exec,
|
|
288
|
+
schedule_task, list_scheduled_tasks, cancel_scheduled_task])
|
|
289
|
+
|
|
290
|
+
# ---------------------------------------------------------------------------
|
|
291
|
+
# 权限闸门:只读自动执行,写操作先确认
|
|
292
|
+
# ---------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
AUTO_ALLOW = {"Read", "Glob", "Grep", "TodoWrite"}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
async def _confirm(summary: str, detail: str) -> bool:
|
|
298
|
+
print(f"\n\033[33m⚠️ 需要确认:{summary}\033[0m")
|
|
299
|
+
print(f" {detail}")
|
|
300
|
+
ans = (await asyncio.to_thread(input, " 执行?(y/N) ")).strip().lower()
|
|
301
|
+
return ans in ("y", "yes")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
async def can_use_tool(tool_name: str, input_data: dict, context) -> object:
|
|
305
|
+
# 纯只读内置工具:直接放行
|
|
306
|
+
if tool_name in AUTO_ALLOW:
|
|
307
|
+
return PermissionResultAllow(updated_input=input_data)
|
|
308
|
+
|
|
309
|
+
# 各云 CLI:只读 + 实例重启 自动执行;其余写操作确认(按 tool 名查 provider)
|
|
310
|
+
for suffix, cloud in _TOOL_CLOUD.items():
|
|
311
|
+
if tool_name.endswith(suffix):
|
|
312
|
+
p = PROVIDERS[cloud]
|
|
313
|
+
cmd = input_data.get("command", "")
|
|
314
|
+
if p.is_read(cmd) or p.is_reboot(cmd):
|
|
315
|
+
return PermissionResultAllow(updated_input=input_data)
|
|
316
|
+
if await _confirm(f"{p.display} 写操作", f"{p.cli} {cmd}"):
|
|
317
|
+
return PermissionResultAllow(updated_input=input_data)
|
|
318
|
+
return PermissionResultDeny(message=f"用户拒绝了该 {p.display} 写操作")
|
|
319
|
+
|
|
320
|
+
# SSH:按远程命令读/写分类
|
|
321
|
+
if tool_name.endswith("__ssh_exec"):
|
|
322
|
+
cmd = input_data.get("command", "")
|
|
323
|
+
target = input_data.get("target", "?")
|
|
324
|
+
if is_read_only_ssh(cmd):
|
|
325
|
+
return PermissionResultAllow(updated_input=input_data)
|
|
326
|
+
if await _confirm("远程命令", f"{target}: {cmd}"):
|
|
327
|
+
return PermissionResultAllow(updated_input=input_data)
|
|
328
|
+
return PermissionResultDeny(message="用户拒绝了该远程命令")
|
|
329
|
+
|
|
330
|
+
# 计划任务:列出=只读放行;创建/取消=写操作,确认(到点云端自动执行,确认机会只在此刻)
|
|
331
|
+
if tool_name.endswith("__list_scheduled_tasks"):
|
|
332
|
+
return PermissionResultAllow(updated_input=input_data)
|
|
333
|
+
if tool_name.endswith("__schedule_task"):
|
|
334
|
+
if await _confirm("创建定时任务",
|
|
335
|
+
f"重启 {input_data.get('instance')} @ {input_data.get('schedule')}(到点云端自动执行)"):
|
|
336
|
+
return PermissionResultAllow(updated_input=input_data)
|
|
337
|
+
return PermissionResultDeny(message="用户拒绝创建定时任务")
|
|
338
|
+
if tool_name.endswith("__cancel_scheduled_task"):
|
|
339
|
+
if await _confirm("取消定时任务", input_data.get("name", "")):
|
|
340
|
+
return PermissionResultAllow(updated_input=input_data)
|
|
341
|
+
return PermissionResultDeny(message="用户拒绝取消定时任务")
|
|
342
|
+
|
|
343
|
+
# 写文件(含沉淀 runbook):确认
|
|
344
|
+
if tool_name in ("Write", "Edit", "MultiEdit", "NotebookEdit"):
|
|
345
|
+
path = input_data.get("file_path", "?")
|
|
346
|
+
if await _confirm("写文件", path):
|
|
347
|
+
return PermissionResultAllow(updated_input=input_data)
|
|
348
|
+
return PermissionResultDeny(message="用户拒绝了写文件")
|
|
349
|
+
|
|
350
|
+
# 其它一律确认
|
|
351
|
+
if await _confirm(f"工具 {tool_name}", str(input_data)[:300]):
|
|
352
|
+
return PermissionResultAllow(updated_input=input_data)
|
|
353
|
+
return PermissionResultDeny(message="用户拒绝")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# ---------------------------------------------------------------------------
|
|
357
|
+
# 系统提示词
|
|
358
|
+
# ---------------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
SYSTEM_PROMPT = """\
|
|
361
|
+
你是用户的 AWS 运维助手,用自然语言指令操作其 EC2 机群。用简体中文回复,简洁直接。
|
|
362
|
+
|
|
363
|
+
## 你的工作方式
|
|
364
|
+
你不是固定按钮,而是有判断力的运维工程师——这正是用 LLM 做运维的意义。用户的描述可能不准确、有遗漏、甚至有错;你要用理解力推断真实意图,主动核对,信息不全或有歧义时**多轮追问澄清**,把任务问明确后再执行,用脑子兜住各种异常,而不是机械照字面执行或直接拒绝。
|
|
365
|
+
这与"动手要稳"并不矛盾,把握一个平衡:**理解要大胆、动手要谨慎**——解析意图时尽量替用户想周全,但真正落到写操作前,必须确切定位目标并经确认(见下两节)。
|
|
366
|
+
|
|
367
|
+
## 工作目录与资料
|
|
368
|
+
当前工作目录是项目根目录。两份关键资料:
|
|
369
|
+
- `inventory.md`:实例清单。**任何涉及具体实例的操作,先 Read 它**,把用户口中的实例名(如"东京的 openclaw")解析成确切的 实例ID / region / profile。
|
|
370
|
+
- `runbooks/` 目录:能力库(运维剧本)。**动手前先 Glob `runbooks/*.md` 看有没有对应剧本**,有就按剧本执行。
|
|
371
|
+
|
|
372
|
+
## 可用工具
|
|
373
|
+
这是**多云**运维(AWS + 腾讯云 + 阿里云 + 火山引擎 + Google Cloud + Microsoft Azure),按云选对工具(inventory 里每台实例有 `cloud` 字段 aws/tencent/aliyun/volcengine/gcp/azure 标明归属,别搞混):
|
|
374
|
+
- `aws_cli`:AWS CLI(传 aws 之后参数)。**务必带 --profile 和 --region**。
|
|
375
|
+
- `tccli`:腾讯云 CLI(如 Lighthouse)。**带 --region**。重启:`lighthouse RebootInstances --InstanceIds <id> --region <区域>`。
|
|
376
|
+
- `aliyun`:阿里云 CLI(如 ECS)。**带 --RegionId 和 --profile**。重启:`ecs RebootInstance --InstanceId <id> --RegionId <区域> --profile <profile>`。
|
|
377
|
+
- `ve`:火山引擎 CLI(如 ECS)。**带 --region**。重启:`ecs RebootInstance --InstanceId <id> --region <区域>`;列表参数点号下标 `--InstanceIds.1 <id>`。
|
|
378
|
+
- `gcloud`:Google Cloud CLI(如 Compute Engine)。**带 --project**。重启(硬reset,免确认):`compute instances reset <名> --zone <zone> --project <项目>`;停/启(需确认):`compute instances stop|start <名> --zone <zone> --project <项目>`。注意 GCE 用**实例名**+ zone(不是 id)。
|
|
379
|
+
- `az`:Microsoft Azure CLI(如虚拟机 VM)。**带 --subscription**。重启(优雅 restart,免确认):`vm restart --name <名> --resource-group <rg> --subscription <订阅>`;停/启(需确认):`vm stop|start --name <名> --resource-group <rg> --subscription <订阅>`。注意 Azure VM 用**实例名 + 资源组**(不是单一 id)。
|
|
380
|
+
- `ssh_exec`:经 SSH 执行命令。一律用 inventory「SSH 连接」里的 ssh config 别名(内网经跳板,全部密钥登录)。
|
|
381
|
+
- 内置 Read/Glob/Grep:读 inventory、查 runbook;Write/Edit:沉淀 runbook、更新文档。
|
|
382
|
+
- `schedule_task` / `list_scheduled_tasks` / `cancel_scheduled_task`:**计划任务(定时重启)**。用 AWS EventBridge Scheduler 云端到点执行,本机不常驻。
|
|
383
|
+
|
|
384
|
+
## 计划任务(定时重启)
|
|
385
|
+
用户说「今晚 10:30 重启 prd-docker」「每天凌晨 3 点重启 X」这类,用 `schedule_task`:
|
|
386
|
+
- 把口语时间换算成确切值(今天日期见上下文,**一律北京时间 UTC+8**):一次性→ISO 如 `2026-06-06T22:30:00`;循环→**5 段标准 crontab**(分 时 日 月 周)如 `30 22 * * *`(每天 22:30)、`0 3 * * *`(每天 03:00)、`0 4 * * 1`(每周一 04:00)。
|
|
387
|
+
- **支持 AWS / 腾讯 / 阿里**,全部是**云 API 重启**:AWS 走 EventBridge Scheduler 直调;腾讯/阿里走同一个调度器触发 Lambda 调各自 RebootInstance API。未接入的账号会被工具如实拒绝——照实回报,别硬来。
|
|
388
|
+
- 创建是写操作会弹确认;确认前用一句话说清「将定时重启哪台、什么时候、一次性还是循环」。到点由云端自动重启(届时无法再确认,所以此刻务必核对准目标与时间)。
|
|
389
|
+
- 查/删用 `list_scheduled_tasks` / `cancel_scheduled_task`。
|
|
390
|
+
|
|
391
|
+
## 目标解析与泛化(重要)
|
|
392
|
+
解析操作目标时先 Read `inventory.md`;不确定可用 `aws_cli` 实时 describe 核对。
|
|
393
|
+
**只有当你能唯一、确切地定位到目标(拿到 实例ID + region + profile)时才动手。**
|
|
394
|
+
|
|
395
|
+
遇到下列情况,**一律停下、如实回报、请用户补充或调整,绝不猜测、绝不对不确定的对象执行操作**:
|
|
396
|
+
- 用户说的实例 / 区域在 inventory 里找不到(某 region 未纳管、实例名不存在、上游 IP 反查不到归属等);
|
|
397
|
+
- 名称有歧义、可能匹配多台;
|
|
398
|
+
- 缺少连接所需信息(密钥 / 用户名 / 跳板)导致无法 SSH。
|
|
399
|
+
|
|
400
|
+
回报时讲清三件事:**你查了哪里、找到/没找到什么、还缺什么**,然后让用户补充信息或改一个明确的对象。
|
|
401
|
+
例:「宁夏已纳管区域里没有名为 xxxx 的实例;当前只覆盖 <列出已纳管区域>。请确认实例名,或直接给我 region / 实例ID。」
|
|
402
|
+
宁可多问一步,也不要对错误或不确定的机器执行写操作。
|
|
403
|
+
|
|
404
|
+
## 执行规范
|
|
405
|
+
1. 操作前先回显你解析出的目标(实例名 → ID / region / profile),让用户一眼能核对。
|
|
406
|
+
2. 自动执行(不打断):只读操作(describe/list/df/状态查询等)**以及实例重启**(reboot-instances / RebootInstances,低风险可逆,用户已授权免确认)。其余写操作(停止/销毁/改配置、SSH 改动、写文件)由系统拦截要用户确认。无论是否需确认,发起前都用一句话说清你要做什么、对哪台。
|
|
407
|
+
3. 操作后简要回报结果,失败要给出原因和建议。命令失败时,工具输出末尾可能自动追加一段「🔎 诊断」(凭证/权限/限额/超时等的确定性判定 + 修复方向)——命中时优先采信并照建议处理;未命中再自己结合 inventory/runbook 深挖。错误码全表见 `runbooks/error-codes-quickref.md`。
|
|
408
|
+
|
|
409
|
+
## 能力沉淀(重要)
|
|
410
|
+
完成一项**之前没有对应 runbook 的运维任务**后,主动提议把它沉淀成 `runbooks/<kebab-name>.md`,参考 `runbooks/_TEMPLATE.md` 的结构,记录:触发意图、目标如何解析、执行步骤与命令、是否写操作、注意事项。用户同意后再写。
|
|
411
|
+
写完若要让 NOC 机读清单(data.json)登记这个新剧本,跑 **`ops sync --index`**(只重扫本地 runbooks/ 与日志、零网络秒级,**不要**用全量 `ops sync`——那会重扫所有云、慢且无谓;`--local` / `--tailscale` 是刷非公网机器的,也不对口)。
|
|
412
|
+
"""
|
|
413
|
+
|
|
414
|
+
# ---------------------------------------------------------------------------
|
|
415
|
+
# 运行
|
|
416
|
+
# ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
def _make_options() -> ClaudeAgentOptions:
|
|
419
|
+
return ClaudeAgentOptions(
|
|
420
|
+
system_prompt=SYSTEM_PROMPT,
|
|
421
|
+
model=MODEL,
|
|
422
|
+
cwd=str(paths.workspace_dir()),
|
|
423
|
+
mcp_servers={"ops": ops_server},
|
|
424
|
+
# 只把纯只读内置工具放进 allowed_tools(自动批准)。
|
|
425
|
+
# 写操作工具(aws_cli/ssh_exec/Write/Edit)故意不列入——它们会落到
|
|
426
|
+
# can_use_tool 回调里做读/写分类与确认。列进来会被 SDK 自动批准、绕过确认!
|
|
427
|
+
allowed_tools=["Read", "Glob", "Grep", "TodoWrite"],
|
|
428
|
+
can_use_tool=can_use_tool,
|
|
429
|
+
permission_mode="default",
|
|
430
|
+
setting_sources=[], # 不加载用户全局 Claude Code 设置,保持独立
|
|
431
|
+
max_turns=40,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
async def _drain(client: ClaudeSDKClient) -> None:
|
|
436
|
+
"""打印一轮响应里的助手文本与工具调用。"""
|
|
437
|
+
async for msg in client.receive_response():
|
|
438
|
+
if isinstance(msg, AssistantMessage):
|
|
439
|
+
for block in msg.content:
|
|
440
|
+
if isinstance(block, TextBlock) and block.text.strip():
|
|
441
|
+
print(block.text, end="", flush=True)
|
|
442
|
+
elif isinstance(block, ToolUseBlock):
|
|
443
|
+
name = block.name.split("__")[-1]
|
|
444
|
+
arg = block.input.get("command") or block.input.get("file_path") or ""
|
|
445
|
+
print(f"\n\033[36m· {name}\033[0m {arg}".rstrip(), flush=True)
|
|
446
|
+
elif isinstance(msg, ResultMessage):
|
|
447
|
+
print() # 换行收尾
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
async def run_once(prompt: str) -> None:
|
|
451
|
+
async with ClaudeSDKClient(options=_make_options()) as client:
|
|
452
|
+
await client.query(prompt)
|
|
453
|
+
await _drain(client)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
async def run_repl() -> None:
|
|
457
|
+
print("AWS 运维 Agent(交互模式)。输入指令,exit/quit 退出。\n")
|
|
458
|
+
async with ClaudeSDKClient(options=_make_options()) as client:
|
|
459
|
+
while True:
|
|
460
|
+
try:
|
|
461
|
+
user = (await asyncio.to_thread(input, "\033[32mops>\033[0m ")).strip()
|
|
462
|
+
except (EOFError, KeyboardInterrupt):
|
|
463
|
+
print()
|
|
464
|
+
break
|
|
465
|
+
if not user:
|
|
466
|
+
continue
|
|
467
|
+
if user.lower() in ("exit", "quit", "q"):
|
|
468
|
+
break
|
|
469
|
+
await client.query(user)
|
|
470
|
+
await _drain(client)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def main() -> None:
|
|
474
|
+
if not os.environ.get("ANTHROPIC_API_KEY"):
|
|
475
|
+
print("错误:未设置 ANTHROPIC_API_KEY", file=sys.stderr)
|
|
476
|
+
sys.exit(1)
|
|
477
|
+
paths.ensure_workspace()
|
|
478
|
+
args = sys.argv[1:]
|
|
479
|
+
if args:
|
|
480
|
+
asyncio.run(run_once(" ".join(args)))
|
|
481
|
+
else:
|
|
482
|
+
asyncio.run(run_repl())
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
if __name__ == "__main__":
|
|
486
|
+
main()
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# ops 部署配置(单一声明式来源)——非密、可 git 跟踪、可移植。
|
|
2
|
+
#
|
|
3
|
+
# 安全边界:这里只放**非密**的部署专属配置——账号 ID / 区域 / SSH 别名前缀 /
|
|
4
|
+
# 跳板机 / 角色 ARN / Lambda ARN / 云 profile 名 / ssh 用户名 / 私钥**文件名**。
|
|
5
|
+
# **绝不**放:AWS access key、私钥内容、密码、任何 secret。
|
|
6
|
+
#
|
|
7
|
+
# 这是打包默认(=出厂占位模板)。`ops setup` / 首启会把它 seed 到 <workspace>/ops-config.toml,
|
|
8
|
+
# 之后**编辑工作区那一份**填上你自己的账号/区域即可;本文件只是缺省回退。
|
|
9
|
+
# 下方用 <尖括号> 标的都是占位符,请替换;用不到的云整段删掉即可。
|
|
10
|
+
|
|
11
|
+
[agent]
|
|
12
|
+
model = "claude-sonnet-4-6" # 仍可被 OPS_MODEL 环境变量覆盖
|
|
13
|
+
|
|
14
|
+
# ===== AWS =====
|
|
15
|
+
[aws]
|
|
16
|
+
# 仅这些 global 账号会扫 route53domains(域名)。删掉或留空表示不扫。
|
|
17
|
+
route53_profiles = ["<aws-global-account-id>"]
|
|
18
|
+
# 各 partition 下要扫的常用区域(按需增删)
|
|
19
|
+
scan_regions_global = ["us-east-1", "us-west-2", "ap-northeast-1"]
|
|
20
|
+
scan_regions_china = ["cn-northwest-1"]
|
|
21
|
+
|
|
22
|
+
# 每个账号一段;profile = 你本机 `aws configure` 里的 profile 名(这里用账号 ID 当 profile 名也可)
|
|
23
|
+
[[aws.accounts]]
|
|
24
|
+
profile = "<aws-account-id>"
|
|
25
|
+
partition = "global" # china | global —— 决定扫哪组区域
|
|
26
|
+
# [[aws.accounts]]
|
|
27
|
+
# profile = "<aws-china-account-id>"
|
|
28
|
+
# partition = "china"
|
|
29
|
+
|
|
30
|
+
[aws.alias_prefix] # "<profile>/<region>" = SSH 别名前缀(未列出的按区域昵称派生)
|
|
31
|
+
# "<aws-account-id>/us-west-2" = "ore"
|
|
32
|
+
|
|
33
|
+
[aws.jumps] # "<profile>/<region>" = [跳板别名, 跳板实例名](仅无公网内网实例需要)
|
|
34
|
+
# "<aws-china-account-id>/cn-northwest-1" = ["<jump-alias>", "<jump-instance-name>"]
|
|
35
|
+
|
|
36
|
+
# ===== 阿里云(多账号,不用就整段删)=====
|
|
37
|
+
# 每个账号一段 [[aliyun.accounts]]。只有域名、没有服务器的账号省略 regions/ssh_* 即可。
|
|
38
|
+
# [[aliyun.accounts]]
|
|
39
|
+
# profile = "<aliyun-cli-profile>"
|
|
40
|
+
# account = "<aliyun-account-id>"
|
|
41
|
+
# ssh_user = "root"
|
|
42
|
+
# ssh_key = "<keyfile-name>" # 私钥 ~/.ssh/<account>/<keyfile-name>.pem(文件名,非密钥内容)
|
|
43
|
+
# [aliyun.accounts.regions]
|
|
44
|
+
# "cn-hangzhou" = ["Hangzhou", "🇨🇳"]
|
|
45
|
+
|
|
46
|
+
# ===== Google Cloud(多项目,只读展示 Compute/Storage/IAM,不用就整段删)=====
|
|
47
|
+
# account=项目 id;认证沿用本机 gcloud(gcloud auth login / ADC),noc 不存密钥。
|
|
48
|
+
# [[gcp.accounts]]
|
|
49
|
+
# project = "<gcp-project-id>"
|
|
50
|
+
# configuration = "default" # 可选:gcloud 命名配置(多登录时用)
|
|
51
|
+
|
|
52
|
+
# ===== Microsoft Azure(多订阅,只读展示 VM/存储/DNS/数据库/Function,VM 可重启,不用就整段删)=====
|
|
53
|
+
# account=订阅 id;认证沿用本机 az(az login / service principal),noc 不存密钥。
|
|
54
|
+
# [[azure.accounts]]
|
|
55
|
+
# subscription = "<azure-subscription-id>"
|
|
56
|
+
|
|
57
|
+
# ===== Cloudflare(DNS zone 纳入「域名」tab,只读,不用就整段删)=====
|
|
58
|
+
# token 是密钥,不放配置——存 Keychain:
|
|
59
|
+
# security add-generic-password -U -s noc-cloudflare -a cloudflare -T /usr/bin/security -w
|
|
60
|
+
# [[cloudflare.accounts]]
|
|
61
|
+
# account = "cloudflare" # 标签,也作 Keychain 的 account 名
|
|
62
|
+
# keychain_service = "noc-cloudflare"
|
|
63
|
+
|
|
64
|
+
# ===== Supabase(托管 Postgres 项目纳入「数据库」tab,只读,不用就整段删)=====
|
|
65
|
+
# 账号级 PAT(Management API token) 是密钥,不放配置——存 Keychain:
|
|
66
|
+
# security add-generic-password -U -s noc-supabase -a supabase -T /usr/bin/security -w
|
|
67
|
+
# [[supabase.accounts]]
|
|
68
|
+
# account = "supabase"
|
|
69
|
+
# keychain_service = "noc-supabase"
|
|
70
|
+
|
|
71
|
+
# ===== 腾讯云(不用就整段删)=====
|
|
72
|
+
# [tencent]
|
|
73
|
+
# account = "<tencent-account-id>"
|
|
74
|
+
# ssh_user = "ubuntu"
|
|
75
|
+
# ssh_key = "<keyfile-name>"
|
|
76
|
+
# [tencent.regions]
|
|
77
|
+
# "ap-nanjing" = ["Nanjing", "🇨🇳"]
|
|
78
|
+
|
|
79
|
+
# ===== 内网(LAN)连通性预检(可选,不配则不门控)=====
|
|
80
|
+
# `ops sync --local` 会逐台 TCP 探内网机器(local/sangfor/vmware/synology)。
|
|
81
|
+
# 若当前不在内网/未连 VPN,逐台探测会吃满超时、再把所有机器误标 stopped。
|
|
82
|
+
# 配了 probe 后:--local 开跑前先探这几个点,全都不通即判定"不在内网"——
|
|
83
|
+
# 内网机器整体沿用上次数据、不重扫。留空/删掉 = 老行为。
|
|
84
|
+
# probe:host:port 列表(无端口默认 22),任一通即算在内网。选内网里最稳的点,
|
|
85
|
+
# 如网关或 vCenter/HCI 管理口。
|
|
86
|
+
# [local_net]
|
|
87
|
+
# probe = ["<gateway-or-vcenter-ip>:443"]
|
|
88
|
+
|
|
89
|
+
# ===== 网络域:内网(lan) vs Tailscale =====
|
|
90
|
+
# 每个本地网络账号都属于一个网络域,分开刷、互不误伤:
|
|
91
|
+
# ops sync --local 只刷 network = "lan" 门控 = 上面的 local_net.probe
|
|
92
|
+
# ops sync --tailscale 只刷 network = "tailscale" 门控 = `tailscale status`
|
|
93
|
+
# 不写 network 字段会按 url 自动推导:100.64.0.0/10 地址或 *.ts.net 名字 → "tailscale",
|
|
94
|
+
# 其余 → "lan"。只有想覆盖自动推导(如经子网路由暴露的内网地址)才需要显式写。
|
|
95
|
+
# [[synology.accounts]]
|
|
96
|
+
# account = "<nas-on-tailnet>"
|
|
97
|
+
# url = "https://100.x.y.z:5001"
|
|
98
|
+
# network = "tailscale"
|
|
99
|
+
|
|
100
|
+
# ===== 实例用途备注(人工维护,可整段删)=====
|
|
101
|
+
# 键=实例 ID(不用名字,实例改名后仍对得上),值=一句话用途。渲染进 inventory.md 实例表的
|
|
102
|
+
# 「用途」列 + data.json 的 instances[].note;某个区/某朵云一台都没写就不加这一列。
|
|
103
|
+
# inventory.md 每次 sync 整份重写——备注只有写在这里才活得过下一次 `ops sync`。
|
|
104
|
+
# [instance_notes]
|
|
105
|
+
# i-0123456789abcdef0 = "Caddy 边缘入口:反代 MCP 网关 + 官网静态站"
|
|
106
|
+
|
|
107
|
+
# ===== 计划任务(EventBridge Scheduler);不用定时任务可整段删 =====
|
|
108
|
+
[scheduler]
|
|
109
|
+
timezone = "Asia/Shanghai"
|
|
110
|
+
display_tz = "北京时间(UTC+8)"
|
|
111
|
+
role_name = "noc-scheduler" # 各账号同名角色;ARN 由 arn:{partition}:iam::{profile}:role/{role_name} 推导
|
|
112
|
+
|
|
113
|
+
# 非 AWS(腾讯/阿里)派发中枢:一个 AWS 账号上的 Lambda,从 Secrets Manager 取各云凭证后调其重启 API
|
|
114
|
+
# [scheduler.dispatch]
|
|
115
|
+
# account = "<aws-account-id>"
|
|
116
|
+
# region = "ap-northeast-1"
|
|
117
|
+
# lambda_arn = "arn:aws:lambda:ap-northeast-1:<aws-account-id>:function:noc-reboot-dispatcher"
|
|
118
|
+
|
|
119
|
+
# [[scheduler.dispatch_clouds]] # 已接入派发(凭证在 Secrets Manager)的 (cloud, account)
|
|
120
|
+
# cloud = "tencent"
|
|
121
|
+
# account = "<tencent-account-id>"
|
|
122
|
+
|
|
123
|
+
[[scheduler.accounts]] # AWS 直重启账号(partition 用 ARN 写法 aws-cn | aws)
|
|
124
|
+
profile = "<aws-account-id>"
|
|
125
|
+
partition = "aws"
|
|
126
|
+
regions = ["us-west-2"]
|
|
127
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# 可分发运维剧本包(通用版)
|
|
2
|
+
|
|
3
|
+
这是一组**与具体环境无关**的运维剧本,可直接分发给其它企业用户使用。均为方法论 / 排障流程,命令用占位符(`<域名>`/`<P>`/`<region>` 等),不含任何真实账号 ID、主机名、IP、内部域名或自研工具。
|
|
4
|
+
|
|
5
|
+
## 剧本清单
|
|
6
|
+
|
|
7
|
+
| 剧本 | 用途 | 适用对象 |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `s3-bucket-usage-check.md` | 删 S3 桶前判断它是否还在用(新鲜度 + 反查生产者) | 任何 AWS 用户 |
|
|
10
|
+
| `alb-access-log-decommission.md` | 停用 ALB 访问日志并安全删除其日志桶 | 任何 AWS 用户 |
|
|
11
|
+
| `ec2-public-exposure-hardening.md` | 收紧 EC2 公网暴露面(安全组收口) | 任何 AWS 用户 |
|
|
12
|
+
| `certbot-renewal-troubleshooting.md` | Let's Encrypt 证书过期 / 自动续期排障 | 任何 nginx + certbot 主机 |
|
|
13
|
+
| `site-access-source-stats.md` | 统计某站点的访问来源(源 IP / 地理归属) | 任何 nginx / ALB 站点 |
|
|
14
|
+
|
|
15
|
+
## 约定
|
|
16
|
+
- 占位符:`<域名>` 站点域名、`<P>` AWS profile、`<R>`/`<region>` 区域、`<sg>` 安全组 ID、`<alias>` SSH 别名、`<桶名>` S3 桶名。
|
|
17
|
+
- 安全原则:只读操作直接做;写操作(删桶/改安全组/续删证书等)先回显计划再确认;不可逆操作(删桶)务必先核实无引用。
|
|
18
|
+
- 剧本间用 `[[名称]]` 互链,均指向本目录内的其它剧本。
|