huaweicloud-devkit 1.1.9-next.0 → 1.1.9-next.1
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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/integrations/opencode/package-lock.json +24 -0
- package/integrations/opencode/package.json +10 -0
- package/integrations/opencode/plugins/huaweicloud-devkit.js +194 -0
- package/package.json +2 -2
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +5 -0
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +16 -2
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/flavors.md +22 -0
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/windows-ssh.md +118 -0
- package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +3 -3
- package/plugins/huaweicloud-core/skills/huawei-iac/SKILL.md +36 -36
- package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +2 -2
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +35 -6
- package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +2 -2
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +6 -6
- package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +16 -9
- package/plugins/huaweicloud-core/src/auth/reconcile.mjs +26 -0
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +32 -4
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +120 -4
- package/plugins/huaweicloud-core/src/setup-cli.mjs +111 -8
- package/plugins/huaweicloud-core/src/tools.mjs +21 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/huaweicloud-devkit)
|
|
7
|
-
[](https://github.com/huaweicloud/huaweicloud-devkit)
|
|
7
|
+
[](https://github.com/huaweicloud/huaweicloud-devkit)
|
|
8
8
|
|
|
9
9
|
**[中文](README.zh-CN.md) | English**
|
|
10
10
|
|
package/README.zh-CN.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/huaweicloud-devkit)
|
|
7
|
-
[](https://github.com/huaweicloud/huaweicloud-devkit)
|
|
7
|
+
[](https://github.com/huaweicloud/huaweicloud-devkit)
|
|
8
8
|
|
|
9
9
|
**中文 | [English](README.md)**
|
|
10
10
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@huaweicloud-devkit/opencode-integration",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "@huaweicloud-devkit/opencode-integration",
|
|
9
|
+
"version": "0.0.0",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"zod": "^3.11.0"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"node_modules/zod": {
|
|
15
|
+
"version": "3.25.76",
|
|
16
|
+
"resolved": "https://repo.huaweicloud.com/repository/npm/zod/-/zod-3.25.76.tgz",
|
|
17
|
+
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"funding": {
|
|
20
|
+
"url": "https://github.com/sponsors/colinhacks"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@huaweicloud-devkit/opencode-integration",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Local deps for the huaweicloud-devkit OpenCode plugin (zod for tool schema conversion). Installed into ~/.config/opencode by 'npx huaweicloud-devkit install --target opencode'.",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"zod": "^3.11.0"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// huaweicloud-devkit OpenCode native plugin
|
|
2
|
+
//
|
|
3
|
+
// Bridges all 42 MCP tools into OpenCode in-process custom tools via the shared
|
|
4
|
+
// callTool() business layer, plus:
|
|
5
|
+
// - tool.execute.before safety gate (classifyHcloudArgs / classifyTextCommand)
|
|
6
|
+
// - skill-tracker telemetry (superset of the legacy skill-tracker.js plugin)
|
|
7
|
+
// - STS command-level injection is inherited from callTool -> runHcloud chain
|
|
8
|
+
//
|
|
9
|
+
// Install: npm i -g huaweicloud-devkit && npx huaweicloud-devkit install --target opencode
|
|
10
|
+
// (this file is copied to ~/.config/opencode/plugins/huaweicloud-devkit.js and
|
|
11
|
+
// auto-loaded by OpenCode at startup — local plugins need no config entry)
|
|
12
|
+
//
|
|
13
|
+
// Compatibility contract (unchanged from MCP mode):
|
|
14
|
+
// - tool names stay `huaweicloud_*` (skills/hook matchers keep working)
|
|
15
|
+
// - approval gate: run_approved_command still requires args+approvalToken+approvedByUser
|
|
16
|
+
// - credentials: resolveCredentials reads env (HW_*) then S1 — same priority as MCP
|
|
17
|
+
|
|
18
|
+
import { appendFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
19
|
+
import { join, dirname } from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const z = require('zod');
|
|
25
|
+
|
|
26
|
+
// devkit core — resolved from the installed plugin tree next to this file
|
|
27
|
+
const selfDir = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const DEVKIT_SRC = join(selfDir, '..', 'huaweicloud-plugins', 'src');
|
|
29
|
+
|
|
30
|
+
let devkit = null;
|
|
31
|
+
let loadError = null;
|
|
32
|
+
try {
|
|
33
|
+
devkit = await import(`file://${join(DEVKIT_SRC, 'tools.mjs')}`);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
loadError = error;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── telemetry (superset of legacy skill-tracker.js) ─────────────────────────
|
|
39
|
+
|
|
40
|
+
const TELEMETRY_DIR = join(DEVKIT_SRC, '..', 'telemetry');
|
|
41
|
+
function writeEvent(key, value, extra = {}) {
|
|
42
|
+
try {
|
|
43
|
+
if (!existsSync(TELEMETRY_DIR)) mkdirSync(TELEMETRY_DIR, { recursive: true });
|
|
44
|
+
appendFileSync(join(TELEMETRY_DIR, 'hook-events.jsonl'), JSON.stringify({ key, value, ...extra }) + '\n');
|
|
45
|
+
} catch {
|
|
46
|
+
/* telemetry must never break the host */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isHuaweiCloudSkill(name) {
|
|
51
|
+
return typeof name === 'string' && /^huawei/i.test(name);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── JSON Schema -> Zod converter (covers the devkit inputSchema subset) ─────
|
|
55
|
+
|
|
56
|
+
function jsonSchemaToZod(schema) {
|
|
57
|
+
// schema is { type:'object', properties:{...}, required:[...] }
|
|
58
|
+
const shape = {};
|
|
59
|
+
const required = new Set(schema?.required || []);
|
|
60
|
+
for (const [name, prop] of Object.entries(schema?.properties || {})) {
|
|
61
|
+
let node;
|
|
62
|
+
switch (prop?.type) {
|
|
63
|
+
case 'string':
|
|
64
|
+
node = z.string();
|
|
65
|
+
if (prop.enum) {
|
|
66
|
+
const vals = prop.enum.filter((v) => typeof v === 'string');
|
|
67
|
+
if (vals.length > 0) node = z.enum(vals);
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
case 'number':
|
|
71
|
+
case 'integer':
|
|
72
|
+
node = z.number();
|
|
73
|
+
break;
|
|
74
|
+
case 'boolean':
|
|
75
|
+
node = z.boolean();
|
|
76
|
+
break;
|
|
77
|
+
case 'array':
|
|
78
|
+
node = z.array(z.any());
|
|
79
|
+
break;
|
|
80
|
+
case 'object':
|
|
81
|
+
default:
|
|
82
|
+
node = z.any(); // nested objects (e.g. git{repo_url}) stay permissive
|
|
83
|
+
}
|
|
84
|
+
if (prop?.description) node = node.describe(prop.description);
|
|
85
|
+
shape[name] = required.has(name) ? node : node.optional();
|
|
86
|
+
}
|
|
87
|
+
return shape;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── safety gate (mirrors the MCP/hook deny semantics) ──────────────────────
|
|
91
|
+
|
|
92
|
+
const DENY_TOOLS = new Set([
|
|
93
|
+
'huaweicloud_run_approved_command', // has its own in-tool approval gate (token check)
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
async function safetyGate(toolName, args) {
|
|
97
|
+
if (!devkit) return; // devkit failed to load -> tools absent, nothing to gate
|
|
98
|
+
// read-only and planning tools pass through: callTool re-runs the full policy
|
|
99
|
+
// layer anyway (classifyHcloudArgs inside plan/read paths). The gate below is
|
|
100
|
+
// a fast pre-check for the raw hcloud-ish tools only.
|
|
101
|
+
if (DENY_TOOLS.has(toolName)) return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── tool bridging ───────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
function buildBridgedTools() {
|
|
107
|
+
if (!devkit) return {};
|
|
108
|
+
const tools = {};
|
|
109
|
+
const definitions = devkit.TOOL_DEFINITIONS || [];
|
|
110
|
+
for (const def of definitions) {
|
|
111
|
+
tools[def.name] = {
|
|
112
|
+
description: def.description,
|
|
113
|
+
args: jsonSchemaToZod(def.inputSchema || { type: 'object', properties: {} }),
|
|
114
|
+
async execute(args, context) {
|
|
115
|
+
const started = Date.now();
|
|
116
|
+
try {
|
|
117
|
+
await safetyGate(def.name, args);
|
|
118
|
+
const result = await devkit.callTool(def.name, args, { cwd: context?.directory });
|
|
119
|
+
writeEvent('tool:invoke', def.name, { durationMs: Date.now() - started });
|
|
120
|
+
return typeof result === 'string' ? result : { output: JSON.stringify(result, null, 2) };
|
|
121
|
+
} catch (error) {
|
|
122
|
+
// crash containment: never take the host down with a tool error
|
|
123
|
+
const payload = {
|
|
124
|
+
error: error?.message || String(error),
|
|
125
|
+
code: error?.code,
|
|
126
|
+
action: error?.action || error?.nextStep,
|
|
127
|
+
};
|
|
128
|
+
writeEvent('tool:error', def.name, { message: payload.error });
|
|
129
|
+
return { output: JSON.stringify(payload, null, 2) };
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return tools;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── plugin export (v1 Hooks contract, IDE + CLI dual form) ─────────────────
|
|
138
|
+
|
|
139
|
+
function getHooks() {
|
|
140
|
+
return {
|
|
141
|
+
'tool.execute.before': function (input, output) {
|
|
142
|
+
try {
|
|
143
|
+
if (input?.tool === 'skill') {
|
|
144
|
+
const name = output?.args?.name;
|
|
145
|
+
if (isHuaweiCloudSkill(name)) writeEvent('skill:retrieve', name);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (input?.tool === 'bash') {
|
|
149
|
+
const cmd = output?.args?.command || '';
|
|
150
|
+
if (!cmd) return;
|
|
151
|
+
// CLI read/write classification (legacy skill-tracker behavior)
|
|
152
|
+
const isHcloud = /(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+/i.test(cmd);
|
|
153
|
+
if (isHcloud) {
|
|
154
|
+
const isRead = /\b(List|Show|Get|Describe|NovaList|NovaShow)\w*/i.test(cmd);
|
|
155
|
+
writeEvent(isRead ? 'cli:read' : 'cli:write', cmd.slice(0, 120), { capability: 'cli' });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
/* hooks must never throw */
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
event: function ({ event }) {
|
|
163
|
+
try {
|
|
164
|
+
if (event?.type === 'message.part.updated') {
|
|
165
|
+
const text = event?.properties?.part?.text;
|
|
166
|
+
if (typeof text === 'string') {
|
|
167
|
+
const m = text.match(/Base directory for this skill:\s*.*?skills[/\\]([a-z0-9-]+)/i);
|
|
168
|
+
if (m && isHuaweiCloudSkill(m[1])) writeEvent('skill:retrieve', m[1]);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
/* ignore */
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function plugin() {
|
|
179
|
+
const hooks = getHooks();
|
|
180
|
+
if (loadError) {
|
|
181
|
+
// Surface a single actionable line instead of breaking startup
|
|
182
|
+
console.error(`[huaweicloud-devkit] core failed to load: ${loadError.message}`);
|
|
183
|
+
console.error(`[huaweicloud-devkit] expected devkit src at ${DEVKIT_SRC}`);
|
|
184
|
+
console.error(`[huaweicloud-devkit] run: npx huaweicloud-devkit install --target opencode`);
|
|
185
|
+
return hooks; // telemetry still works; tools absent until install is re-run
|
|
186
|
+
}
|
|
187
|
+
hooks.tool = buildBridgedTools();
|
|
188
|
+
return hooks;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
plugin.id = 'huaweicloud-devkit';
|
|
192
|
+
plugin.server = async () => plugin();
|
|
193
|
+
|
|
194
|
+
export default plugin;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"mcpName": "io.github.huaweicloud/huaweicloud-devkit",
|
|
4
|
-
"version": "1.1.9-next.
|
|
5
|
-
"kooCliVersion": "7.
|
|
4
|
+
"version": "1.1.9-next.1",
|
|
5
|
+
"kooCliVersion": "7.3.12",
|
|
6
6
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"files": [
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"mcpServers": "./.mcp.json",
|
|
18
18
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
19
19
|
"skills": "./skills/",
|
|
20
|
-
"version": "1.1.9-next.
|
|
20
|
+
"version": "1.1.9-next.1",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "HuaweiCloud Mate",
|
|
23
23
|
"url": "https://github.com/huaweicloud"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"mcpServers": "./.mcp.json",
|
|
21
21
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
22
22
|
"skills": "./skills/",
|
|
23
|
-
"version": "1.1.9-next.
|
|
23
|
+
"version": "1.1.9-next.1",
|
|
24
24
|
"author": {
|
|
25
25
|
"name": "HuaweiCloud Mate",
|
|
26
26
|
"url": "https://github.com/huaweicloud"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"mcpServers": "./.mcp.json",
|
|
18
18
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
19
19
|
"skills": "./skills/",
|
|
20
|
-
"version": "1.1.9-next.
|
|
20
|
+
"version": "1.1.9-next.1",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "HuaweiCloud Mate",
|
|
23
23
|
"url": "https://github.com/huaweicloud"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.9-next.
|
|
3
|
+
"version": "1.1.9-next.1",
|
|
4
4
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HuaweiCloud Mate",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.9-next.
|
|
3
|
+
"version": "1.1.9-next.1",
|
|
4
4
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HuaweiCloud Mate",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"id": "huaweicloud-devkit",
|
|
4
4
|
"displayName": "HuaweiCloud DevKit",
|
|
5
|
-
"version": "1.1.9-next.
|
|
5
|
+
"version": "1.1.9-next.1",
|
|
6
6
|
"family": "bundle-plugin",
|
|
7
7
|
"bundleFormat": "codex",
|
|
8
8
|
"description": "Guide coding agents to use Huawei Cloud safely — KooCLI, APIs, SDKs, 28 MCP tools, skills, and safety guardrails.",
|
|
@@ -124,6 +124,10 @@ ssh -o StrictHostKeyChecking=accept-new -i <key> root@<eip> 'dnf install -y ngin
|
|
|
124
124
|
|
|
125
125
|
> If SCP blocks SSH, use cloud-init `--server.user_data` for full deployment instead. See `references/create-instance.md` §Bootstrap.
|
|
126
126
|
|
|
127
|
+
### Windows Login / Upload (standard paths)
|
|
128
|
+
|
|
129
|
+
Windows has NO working `sshpass` — password automation must go through paramiko; key automation uses the built-in OpenSSH `ssh`/`scp`. The first `Connection refused` on a fresh instance is usually just sshd not ready (retry with backoff before debugging the SG/EIP); a `Permission denied` while the server log shows "accepts key" is the local passphrase trap (`-N '""'` sets a literal `""` passphrase). Full standard paths — sshd-ready retry, paramiko login/upload, and the cloud-init public-key injection path (KooCLI has no `ImportKeypair`): `references/windows-ssh.md`.
|
|
130
|
+
|
|
127
131
|
## Deleting Instances
|
|
128
132
|
|
|
129
133
|
- Show the user the exact command and get explicit approval before running
|
|
@@ -190,4 +194,5 @@ For Flexus X, use standard ECS CreateServers flow with `x1.*` flavors. For Flexu
|
|
|
190
194
|
- ECS Docs: https://support.huaweicloud.com/ecs/
|
|
191
195
|
- Flavor specs: references/flavors.md
|
|
192
196
|
- Create instance: references/create-instance.md
|
|
197
|
+
- Windows SSH login/upload: references/windows-ssh.md
|
|
193
198
|
- HC活动 ECS+Nginx部署: references/hc-activity.md
|
|
@@ -39,8 +39,22 @@ If no VPC/subnet exists: load `huawei-vpc` skill → create VPC → create subne
|
|
|
39
39
|
**MCP 环境下必须用"本地生成 + 导入"流程**——`NovaCreateKeypair` 返回的 `private_key` 会被 MCP 脱敏管道替换为 `<redacted>`,而私钥仅此一次机会,读不到即作废:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
# 1. 本地生成
|
|
43
|
-
ssh-keygen -t rsa -b 2048 -f ./<name> -N '
|
|
42
|
+
# 1. 本地生成 — bash / Git Bash / WSL(无 passphrase)
|
|
43
|
+
ssh-keygen -t rsa -b 2048 -f ./<name> -N ''
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```powershell
|
|
47
|
+
# 1. 本地生成 — Windows PowerShell(无 passphrase,实测验证的写法)
|
|
48
|
+
# ⚠️ 引号陷阱:-N '""' 会把字面量 "" 设为私钥 passphrase(实测确认),后续 SSH 症状极具误导性:
|
|
49
|
+
# ECS 已 ACTIVE、22 端口可达、服务端日志显示 "accepts key",签名后仍 Permission denied ——
|
|
50
|
+
# 服务端接受了密钥却拒绝,排查路径反直觉。本地剥掉 passphrase(ssh-keygen -p)立即恢复,服务器侧零改动。
|
|
51
|
+
# 正确写法:--% 停止解析符 + -N ""(PowerShell 会丢弃空字符串参数,-N '' 直接报
|
|
52
|
+
# "option requires an argument -- N";--% 让该行剩余部分按 cmd 规则原样传递,"" 由 CRT 解析为空串)。
|
|
53
|
+
# 注意:--% 之后到行尾全部原样传递 —— 该行不能再跟任何其他命令或注释。
|
|
54
|
+
ssh-keygen --% -t rsa -b 2048 -f .\<name> -N ""
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```bash
|
|
44
58
|
# 2. 导入公钥
|
|
45
59
|
hcloud ECS NovaCreateKeypair --keypair.name=<name> --keypair.public_key=<本地 .pub 文件内容>
|
|
46
60
|
# 3. 使用私钥文件 ./<name> 连接实例
|
|
@@ -55,3 +55,25 @@ A flavor can be `normal` globally but `abandon` in specific AZs. Selecting an `a
|
|
|
55
55
|
|
|
56
56
|
1. Switch to a different AZ: `hcloud ECS NovaListAvailabilityZones --cli-region=<region>`
|
|
57
57
|
2. Or switch to a different flavor family (e.g., `at7` → `ac7`)
|
|
58
|
+
|
|
59
|
+
## Conflict rule: `cond:operation:status` vs `cond:operation:az` (#922)
|
|
60
|
+
|
|
61
|
+
The two fields can contradict each other (实测:`kc2.small.2` with `cond:operation:status=abandon` while
|
|
62
|
+
`cond:operation:az=cn-north-4a(normal)`). **The AZ-level field (`cond:operation:az`) WINS for the target AZ**:
|
|
63
|
+
|
|
64
|
+
| Global `status` | Target AZ in `cond:operation:az` | Verdict |
|
|
65
|
+
| ------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
66
|
+
| `normal` | `<az>(normal)` | Usable in that AZ |
|
|
67
|
+
| `abandon`/`sellout` | `<az>(normal)` | **Usable in that AZ** — the AZ-level field wins; do not refuse to order just because the global status says abandon |
|
|
68
|
+
| `normal` | `<az>(sellout)` / `<az>(abandon)` / AZ absent | NOT usable in that AZ — pick another AZ or flavor |
|
|
69
|
+
|
|
70
|
+
Decision procedure:
|
|
71
|
+
|
|
72
|
+
1. Find the target AZ entry in `cond:operation:az` (e.g. `cn-north-4a(normal)`).
|
|
73
|
+
2. If the entry exists for the target AZ, its parenthesized state decides — ignore the global `cond:operation:status`.
|
|
74
|
+
3. If the target AZ has no entry, fall back to the global `cond:operation:status` (only `normal` is usable).
|
|
75
|
+
|
|
76
|
+
Note: flavor availability is only half of the check — an image TYPE mismatch fails independently with
|
|
77
|
+
**`Ecs.0707 the product does not exist`** (market-type images fail with KunPeng flavors like `kc2.small.2`;
|
|
78
|
+
switch to a gold-type image or a compatible flavor, e.g. `s6.medium.2` + gold). The `huaweicloud_explain_error`
|
|
79
|
+
error map covers `Ecs.0707` with the full combination-troubleshooting procedure.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Windows SSH Login / Upload — Standard Paths
|
|
2
|
+
|
|
3
|
+
Windows has NO working `sshpass` (it is a Linux tool; Windows OpenSSH client does not ship it, and the
|
|
4
|
+
native `ssh` cannot take a password from stdin or env). Use the paths below instead of trial-and-error.
|
|
5
|
+
All facts verified on Windows (2026-10 activity reports: sshpass failures → 8-minute detour → paramiko).
|
|
6
|
+
|
|
7
|
+
## 0. First connection: `Connection refused` is usually just sshd not ready
|
|
8
|
+
|
|
9
|
+
On a fresh ECS, cloud-init finishes AFTER the instance reports ACTIVE. A first SSH attempt that gets
|
|
10
|
+
`Connection refused` / `timeout` is typically the sshd service not yet listening — do NOT debug the
|
|
11
|
+
security group or EIP on the first refusal. Retry with backoff first:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
for i in 1 2 3 4 5; do
|
|
15
|
+
ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=5 -i <key> root@<eip> 'echo ok' && break
|
|
16
|
+
echo "retry $i: sshd may not be ready yet"; sleep 15
|
|
17
|
+
done
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Only after all retries fail, treat it as a network problem (security group port 22 ingress, EIP binding).
|
|
21
|
+
|
|
22
|
+
## 1. Login from Windows
|
|
23
|
+
|
|
24
|
+
```powershell
|
|
25
|
+
# Standard OpenSSH login (Windows 10+ ships OpenSSH client)
|
|
26
|
+
ssh -o StrictHostKeyChecking=accept-new -i C:\path\to\<key> root@<eip>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Key file permission note: Windows OpenSSH checks ACLs on the private key. If ssh warns
|
|
30
|
+
`UNPROTECTED PRIVATE KEY FILE`, fix with (PowerShell):
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
icacls C:\path\to\<key> /inheritance:r /grant:r "$env:USERNAME:R"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Passphrase trap (from `references/create-instance.md` §5): a key generated with `-N '""'` in PowerShell
|
|
37
|
+
carries a LITERAL `""` passphrase — the server accepts the key but SSH still fails `Permission denied`.
|
|
38
|
+
If you hit that, strip the passphrase locally and retry (server side needs zero changes):
|
|
39
|
+
|
|
40
|
+
```powershell
|
|
41
|
+
ssh-keygen -p -f C:\path\to\<key>
|
|
42
|
+
# enter the old passphrase as "" (two quote characters), leave the new one empty
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 2. Password auth: sshpass does not exist on Windows — use paramiko
|
|
46
|
+
|
|
47
|
+
`sshpass` is unavailable on Windows OpenSSH; password-based automation must go through Python paramiko
|
|
48
|
+
(pip install paramiko):
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# upload + run commands via paramiko (Windows-safe password auth)
|
|
52
|
+
import paramiko
|
|
53
|
+
|
|
54
|
+
client = paramiko.SSHClient()
|
|
55
|
+
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
56
|
+
client.connect('<eip>', username='root', password='<password>', timeout=15)
|
|
57
|
+
# upload
|
|
58
|
+
sftp = client.open_sftp()
|
|
59
|
+
sftp.put(r'C:\local\file.tar.gz', '/root/file.tar.gz')
|
|
60
|
+
sftp.close()
|
|
61
|
+
# run a command
|
|
62
|
+
_, stdout, stderr = client.exec_command('tar -xzf /root/file.tar.gz -C /root && echo OK')
|
|
63
|
+
print(stdout.read().decode(), stderr.read().decode())
|
|
64
|
+
client.close()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- `exec_command` is one-shot per call — chain commands with `&&` inside a single call for state.
|
|
68
|
+
- Never put the password in the conversation or shell history; pass it at runtime.
|
|
69
|
+
|
|
70
|
+
Key-based file upload from Windows (no paramiko needed): `scp` ships with Windows OpenSSH:
|
|
71
|
+
|
|
72
|
+
```powershell
|
|
73
|
+
scp -o StrictHostKeyChecking=accept-new -i C:\path\to\<key> C:\local\file root@<eip>:/root/
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 3. No SSH at all: cloud-init public-key injection (standard path)
|
|
77
|
+
|
|
78
|
+
KooCLI has no `ImportKeypair` operation, and `NovaCreateKeypair`'s returned `private_key` is masked
|
|
79
|
+
(redacted) by the tool pipeline — it cannot be recovered. The documented standard path is
|
|
80
|
+
**local keypair generation + cloud-init public-key injection** (no SSH needed at creation time):
|
|
81
|
+
|
|
82
|
+
1. Generate a keypair locally (per-shell forms and the PowerShell quote trap: `references/create-instance.md` §5)
|
|
83
|
+
2. Inject the PUBLIC key at creation via `--server.user_data` (base64 cloud-init):
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# user_data (must be base64-encoded when passed to CreateServers)
|
|
87
|
+
#cloud-config
|
|
88
|
+
ssh_authorized_keys:
|
|
89
|
+
- <content of the local .pub file>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
user_data=$(base64 -w0 << 'EOF'
|
|
94
|
+
#cloud-config
|
|
95
|
+
ssh_authorized_keys:
|
|
96
|
+
- <pub key content>
|
|
97
|
+
EOF
|
|
98
|
+
)
|
|
99
|
+
hcloud ECS CreateServers ... --server.user_data=$user_data
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
3. After ACTIVE, SSH in with the matching private key (`ssh -i <key> root@<eip>`).
|
|
103
|
+
|
|
104
|
+
This path avoids `NovaCreateKeypair` entirely (no private_key masking problem) and works even when SCP
|
|
105
|
+
policies block SSH at first boot — see `references/create-instance.md` §Bootstrap for the full
|
|
106
|
+
user_data workflow (mirror sources, observability sentinels, first-boot-only semantics).
|
|
107
|
+
|
|
108
|
+
## 4. Quick reference
|
|
109
|
+
|
|
110
|
+
| Task (Windows) | Path |
|
|
111
|
+
| ------------------------------------------ | ------------------------------------------------------------------- |
|
|
112
|
+
| Login | `ssh -i <key> root@<eip>` (OpenSSH built-in) |
|
|
113
|
+
| First `Connection refused` | Retry 3-5x with 15s backoff — sshd not ready yet |
|
|
114
|
+
| Password automation | paramiko (`sshpass` unavailable on Windows) |
|
|
115
|
+
| File upload (key auth) | `scp -i <key> <local> root@<eip>:<path>` |
|
|
116
|
+
| File upload (password) | paramiko SFTP (`put`) |
|
|
117
|
+
| No keypair yet | local `ssh-keygen` + cloud-init `ssh_authorized_keys` inject |
|
|
118
|
+
| `Permission denied` but server accepts key | literal `""` passphrase from `-N '""'` — strip with `ssh-keygen -p` |
|
|
@@ -10,13 +10,13 @@ version: 1
|
|
|
10
10
|
|
|
11
11
|
## KooCLI Installation
|
|
12
12
|
|
|
13
|
-
This plugin is paired with **KooCLI v7.
|
|
13
|
+
This plugin is paired with **KooCLI v7.3.12**. Install the pinned version:
|
|
14
14
|
|
|
15
15
|
| OS | Command |
|
|
16
16
|
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
17
17
|
| Windows (PowerShell) | See https://support.huaweicloud.com/qs-hcli/hcli_02_003.html for MSI download |
|
|
18
|
-
| Linux | curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/7.
|
|
19
|
-
| macOS | curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/7.
|
|
18
|
+
| Linux | curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/7.3.12/huaweicloud-cli-linux-amd64.tar.gz" && tar -zxvf huaweicloud-cli-linux-amd64.tar.gz |
|
|
19
|
+
| macOS | curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/7.3.12/huaweicloud-cli-mac-amd64.tar.gz" && tar -zxvf huaweicloud-cli-mac-amd64.tar.gz |
|
|
20
20
|
|
|
21
21
|
> See `huaweicloud-cli-and-auth` skill for arm64 variants and the one-liner (latest) option.
|
|
22
22
|
|