@meshxdata/fops 0.1.36 → 0.1.37
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 @meshxdata/fops might be problematic. Click here for more details.
- package/CHANGELOG.md +22 -0
- package/fops.mjs +37 -14
- package/package.json +1 -1
- package/src/plugins/bundled/fops-plugin-azure/index.js +44 -2896
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-auth.js +454 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-helpers.js +51 -13
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops.js +182 -27
- package/src/plugins/bundled/fops-plugin-azure/lib/azure-provision.js +62 -8
- package/src/plugins/bundled/fops-plugin-azure/lib/azure.js +2 -2
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/fleet-cmds.js +254 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/infra-cmds.js +890 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/test-cmds.js +314 -0
- package/src/plugins/bundled/fops-plugin-azure/lib/commands/vm-cmds.js +892 -0
- package/src/plugins/bundled/fops-plugin-file/demo/orders_renamed.aligned.csv +1 -1
- package/src/plugins/bundled/fops-plugin-file/index.js +81 -12
- package/src/plugins/bundled/fops-plugin-file/lib/match.js +133 -15
- package/src/plugins/bundled/fops-plugin-file/lib/report.js +3 -0
- package/src/plugins/bundled/fops-plugin-foundation/lib/client.js +9 -5
- package/src/plugins/bundled/fops-plugin-foundation-graphql/lib/graphql/resolvers/data-product.js +32 -0
- package/src/plugins/bundled/fops-plugin-foundation-graphql/lib/graphql/schema.js +20 -1
|
@@ -8,7 +8,7 @@ import { readState, saveState, readVmState, writeVmState, listVms, requireVmStat
|
|
|
8
8
|
import {
|
|
9
9
|
DEFAULTS, DIM, OK, WARN, ERR, LABEL, ACCENT,
|
|
10
10
|
banner, hint, kvLine, nameArg,
|
|
11
|
-
lazyExeca, ensureAzCli, ensureAzAuth, subArgs,
|
|
11
|
+
lazyExeca, ensureAzCli, ensureAzAuth, subArgs, fetchMyIp,
|
|
12
12
|
resolvePublicIp, resolveGithubToken, verifyGithubToken,
|
|
13
13
|
buildPublicUrl, buildDefaultUrl, resolveUniqueDomain,
|
|
14
14
|
sshCmd, closeMux, MUX_OPTS, muxSocketPath, waitForSsh, knockForVm, fopsUpCmd, buildRemoteFopsUpArgs,
|
|
@@ -408,17 +408,25 @@ export async function azureVmCheck(opts = {}) {
|
|
|
408
408
|
// ── ssh ─────────────────────────────────────────────────────────────────────
|
|
409
409
|
|
|
410
410
|
export async function azureSsh(opts = {}) {
|
|
411
|
-
const
|
|
412
|
-
|
|
411
|
+
const { knockForVm, ensureKnockSequence } = await import("./azure-helpers.js");
|
|
412
|
+
// Sync IP + knock sequence from Azure before using them
|
|
413
|
+
const freshState = await ensureKnockSequence(requireVmState(opts.vmName));
|
|
414
|
+
const ip = freshState?.publicIp;
|
|
413
415
|
if (!ip) { console.error(chalk.red("\n No IP address. Is the VM running? Try: fops azure start\n")); process.exit(1); }
|
|
414
416
|
|
|
415
|
-
await knockForVm(
|
|
417
|
+
await knockForVm(freshState);
|
|
416
418
|
|
|
417
419
|
const { execa } = await import("execa");
|
|
418
|
-
await execa("ssh", [
|
|
420
|
+
const result = await execa("ssh", [
|
|
419
421
|
"-o", "StrictHostKeyChecking=no",
|
|
422
|
+
"-o", "UserKnownHostsFile=/dev/null",
|
|
423
|
+
"-o", "ConnectTimeout=15",
|
|
420
424
|
`${DEFAULTS.adminUser}@${ip}`,
|
|
421
425
|
], { stdio: "inherit", reject: false });
|
|
426
|
+
if (result.exitCode !== 0 && result.exitCode !== null) {
|
|
427
|
+
console.error(chalk.red(`\n SSH failed (exit ${result.exitCode}). If port-knock is enabled, try: fops azure knock open ${freshState?.vmName}\n`));
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
422
430
|
}
|
|
423
431
|
|
|
424
432
|
// ── port forward ─────────────────────────────────────────────────────────────
|
|
@@ -1005,11 +1013,11 @@ export async function azureRunUp(opts = {}) {
|
|
|
1005
1013
|
5000,
|
|
1006
1014
|
);
|
|
1007
1015
|
let npmCode = tryUserFirst.exitCode === 0
|
|
1008
|
-
? (await ssh('D="$(npm root -g 2>/dev/null)/@meshxdata"; rm -rf "$D" 2>/dev/null; npm install -g @meshxdata/fops@latest 2>&1', 300000)).exitCode
|
|
1016
|
+
? (await ssh('sudo rm -f /usr/bin/fops /usr/local/bin/fops 2>/dev/null; D="$(npm root -g 2>/dev/null)/@meshxdata"; rm -rf "$D" 2>/dev/null; npm install -g @meshxdata/fops@latest 2>&1', 300000)).exitCode
|
|
1009
1017
|
: 1;
|
|
1010
1018
|
if (npmCode !== 0) {
|
|
1011
1019
|
npmCode = (await ssh(
|
|
1012
|
-
"sudo -E bash -c 'D=\"$(npm root -g)/@meshxdata\"; rm -rf \"$D\" 2>/dev/null; npm install -g @meshxdata/fops@latest' 2>&1",
|
|
1020
|
+
"sudo -E bash -c 'rm -f /usr/bin/fops /usr/local/bin/fops 2>/dev/null; D=\"$(npm root -g)/@meshxdata\"; rm -rf \"$D\" 2>/dev/null; npm install -g @meshxdata/fops@latest' 2>&1",
|
|
1013
1021
|
300000,
|
|
1014
1022
|
)).exitCode;
|
|
1015
1023
|
}
|
|
@@ -1874,7 +1882,7 @@ export async function azureUpdate(opts = {}) {
|
|
|
1874
1882
|
);
|
|
1875
1883
|
if (tryUserFirst.exitCode === 0) {
|
|
1876
1884
|
const userInstall = await ssh(
|
|
1877
|
-
'D="$(npm root -g 2>/dev/null)/@meshxdata"; rm -rf "$D" 2>/dev/null; npm install -g @meshxdata/fops@latest 2>&1',
|
|
1885
|
+
'sudo rm -f /usr/bin/fops /usr/local/bin/fops 2>/dev/null; D="$(npm root -g 2>/dev/null)/@meshxdata"; rm -rf "$D" 2>/dev/null; npm install -g @meshxdata/fops@latest 2>&1',
|
|
1878
1886
|
300000,
|
|
1879
1887
|
);
|
|
1880
1888
|
npmCode = userInstall.exitCode;
|
|
@@ -1884,7 +1892,7 @@ export async function azureUpdate(opts = {}) {
|
|
|
1884
1892
|
if (npmCode !== 0) {
|
|
1885
1893
|
const sudoInstall = await ssh(
|
|
1886
1894
|
"sudo -E bash -c '" +
|
|
1887
|
-
'D="$(npm root -g)/@meshxdata"; rm -rf "$D" 2>/dev/null; npm install -g @meshxdata/fops@latest' +
|
|
1895
|
+
'rm -f /usr/bin/fops /usr/local/bin/fops 2>/dev/null; D="$(npm root -g)/@meshxdata"; rm -rf "$D" 2>/dev/null; npm install -g @meshxdata/fops@latest' +
|
|
1888
1896
|
"' 2>&1",
|
|
1889
1897
|
300000,
|
|
1890
1898
|
);
|
|
@@ -2775,8 +2783,10 @@ export async function azureApply(file, opts = {}) {
|
|
|
2775
2783
|
// ── knock ────────────────────────────────────────────────────────────────────
|
|
2776
2784
|
|
|
2777
2785
|
export async function azureKnock(opts = {}) {
|
|
2778
|
-
const
|
|
2779
|
-
|
|
2786
|
+
const { ensureKnockSequence } = await import("./azure-helpers.js");
|
|
2787
|
+
// Sync IP + knock sequence from Azure before using them
|
|
2788
|
+
const state = await ensureKnockSequence(requireVmState(opts.vmName));
|
|
2789
|
+
if (!state?.knockSequence?.length) {
|
|
2780
2790
|
console.error(ERR("\n Port knocking is not configured on this VM."));
|
|
2781
2791
|
hint(`Provision with: fops azure up${nameArg(state?.vmName)}\n`);
|
|
2782
2792
|
process.exit(1);
|
|
@@ -3096,7 +3106,7 @@ export async function azureKnockVerify(opts = {}) {
|
|
|
3096
3106
|
|
|
3097
3107
|
export async function azureKnockFix(opts = {}) {
|
|
3098
3108
|
const execa = await lazyExeca();
|
|
3099
|
-
const {
|
|
3109
|
+
const { generateKnockSequence } = await import("./port-knock.js");
|
|
3100
3110
|
const { vms } = listVms();
|
|
3101
3111
|
const targets = opts.vmName ? [opts.vmName] : Object.keys(vms);
|
|
3102
3112
|
|
|
@@ -3105,33 +3115,178 @@ export async function azureKnockFix(opts = {}) {
|
|
|
3105
3115
|
return;
|
|
3106
3116
|
}
|
|
3107
3117
|
|
|
3118
|
+
await ensureAzCli(execa);
|
|
3119
|
+
await ensureAzAuth(execa, { subscription: opts.profile });
|
|
3120
|
+
|
|
3108
3121
|
banner("Knock Fix");
|
|
3109
3122
|
|
|
3110
3123
|
for (const name of targets) {
|
|
3111
3124
|
const state = vms[name];
|
|
3112
|
-
if (!state?.
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
console.log(`\n ${LABEL(name)} ${DIM(`(${ip})`)}`);
|
|
3116
|
-
|
|
3117
|
-
// Knock with existing sequence if available
|
|
3118
|
-
if (state.knockSequence?.length) {
|
|
3119
|
-
await performKnock(ip, state.knockSequence, { quiet: true });
|
|
3125
|
+
if (!state?.resourceGroup) {
|
|
3126
|
+
console.log(chalk.dim(` ${name}: no resource group — skipped`));
|
|
3127
|
+
continue;
|
|
3120
3128
|
}
|
|
3129
|
+
const { publicIp: ip, resourceGroup: rg } = state;
|
|
3121
3130
|
|
|
3122
|
-
|
|
3131
|
+
console.log(`\n ${LABEL(name)} ${DIM(`(${ip || "no IP"})`)}`);
|
|
3123
3132
|
|
|
3124
|
-
// Generate new sequence and re-setup
|
|
3125
3133
|
const knockSeq = state.knockSequence?.length ? state.knockSequence : generateKnockSequence();
|
|
3126
3134
|
const appPort = DEFAULTS.publicPort || 443;
|
|
3127
|
-
await setupKnockd(ssh, knockSeq, { appPort });
|
|
3128
|
-
writeVmState(name, { knockSequence: knockSeq });
|
|
3129
|
-
if (state.resourceGroup) await setVmKnockTag(execa, state.resourceGroup, name, knockSeq, opts.profile);
|
|
3130
3135
|
|
|
3131
|
-
|
|
3136
|
+
// Build the setup script to run via az vm run-command (works even when SSH is locked)
|
|
3137
|
+
const script = buildKnockdScript(knockSeq, appPort);
|
|
3138
|
+
|
|
3139
|
+
console.log(chalk.dim(" Running via az vm run-command (bypasses firewall)..."));
|
|
3140
|
+
const { stdout, exitCode } = await execa("az", [
|
|
3141
|
+
"vm", "run-command", "invoke",
|
|
3142
|
+
"--resource-group", rg,
|
|
3143
|
+
"--name", name,
|
|
3144
|
+
"--command-id", "RunShellScript",
|
|
3145
|
+
"--scripts", script,
|
|
3146
|
+
"--output", "json",
|
|
3147
|
+
...subArgs(opts.profile),
|
|
3148
|
+
], { reject: false, timeout: 180000 });
|
|
3149
|
+
|
|
3150
|
+
if (exitCode !== 0) {
|
|
3151
|
+
console.error(ERR(` ✗ az run-command failed for ${name}`));
|
|
3152
|
+
continue;
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
// Check script output for errors
|
|
3156
|
+
try {
|
|
3157
|
+
const result = JSON.parse(stdout);
|
|
3158
|
+
const msg = result?.value?.[0]?.message || "";
|
|
3159
|
+
if (msg.toLowerCase().includes("failed") || msg.toLowerCase().includes("error")) {
|
|
3160
|
+
console.log(chalk.dim(` Script output: ${msg.slice(0, 200)}`));
|
|
3161
|
+
}
|
|
3162
|
+
} catch { /* ignore parse errors */ }
|
|
3132
3163
|
|
|
3133
|
-
|
|
3164
|
+
writeVmState(name, { knockSequence: knockSeq });
|
|
3165
|
+
await setVmKnockTag(execa, rg, name, knockSeq, opts.profile);
|
|
3166
|
+
|
|
3167
|
+
console.log(OK(` ✓ Knock re-configured (sequence: ${knockSeq.join(" → ")})`));
|
|
3134
3168
|
}
|
|
3135
3169
|
|
|
3136
3170
|
console.log(OK(`\n ✓ Done — run fops azure knock verify to confirm.\n`));
|
|
3137
3171
|
}
|
|
3172
|
+
|
|
3173
|
+
/**
|
|
3174
|
+
* Build a self-contained bash script that installs knockd and configures
|
|
3175
|
+
* iptables. Designed to run via az vm run-command (no SSH needed).
|
|
3176
|
+
*/
|
|
3177
|
+
function buildKnockdScript(sequence, appPort) {
|
|
3178
|
+
const ports = [22];
|
|
3179
|
+
if (appPort && appPort !== 22) ports.push(Number(appPort));
|
|
3180
|
+
|
|
3181
|
+
const openCmds = ports.map(p => `/usr/sbin/iptables -I INPUT -s %IP% -p tcp --dport ${p} -j ACCEPT`).join(" && ");
|
|
3182
|
+
const closeCmds = ports.map(p => `/usr/sbin/iptables -D INPUT -s %IP% -p tcp --dport ${p} -j ACCEPT`).join(" && ");
|
|
3183
|
+
const KNOCK_CMD_TIMEOUT = 300;
|
|
3184
|
+
|
|
3185
|
+
// iptables rules: clean up stale entries, then insert DROP + ESTABLISHED
|
|
3186
|
+
const iptRules = [];
|
|
3187
|
+
for (const p of ports) {
|
|
3188
|
+
iptRules.push(
|
|
3189
|
+
`while iptables -D INPUT -p tcp --dport ${p} -j DROP 2>/dev/null; do :; done`,
|
|
3190
|
+
`while iptables -D INPUT -p tcp --dport ${p} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null; do :; done`,
|
|
3191
|
+
`while iptables -D INPUT -p tcp --dport ${p} -j ACCEPT 2>/dev/null; do :; done`,
|
|
3192
|
+
);
|
|
3193
|
+
}
|
|
3194
|
+
for (const p of ports) {
|
|
3195
|
+
iptRules.push(
|
|
3196
|
+
`iptables -I INPUT -p tcp --dport ${p} -j DROP`,
|
|
3197
|
+
`iptables -I INPUT -p tcp --dport ${p} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT`,
|
|
3198
|
+
);
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
return [
|
|
3202
|
+
"#!/bin/bash",
|
|
3203
|
+
"set -e",
|
|
3204
|
+
// Detect interface dynamically inside the script
|
|
3205
|
+
"IFACE=$(ip route show default 2>/dev/null | awk '{print $5}' | head -1)",
|
|
3206
|
+
"IFACE=${IFACE:-eth0}",
|
|
3207
|
+
// Install knockd
|
|
3208
|
+
"export DEBIAN_FRONTEND=noninteractive",
|
|
3209
|
+
"apt-get update -qq",
|
|
3210
|
+
"apt-get install -y -qq knockd iptables-persistent 2>/dev/null || true",
|
|
3211
|
+
// Write knockd.conf using printf to avoid heredoc quoting issues
|
|
3212
|
+
`printf '[options]\\n UseSyslog\\n Interface = %s\\n\\n[openPorts]\\n sequence = ${sequence.join(",")}\\n seq_timeout = 15\\n command = ${openCmds}\\n tcpflags = syn\\n cmd_timeout = ${KNOCK_CMD_TIMEOUT}\\n stop_command = ${closeCmds}\\n' "$IFACE" > /etc/knockd.conf`,
|
|
3213
|
+
// Enable knockd service
|
|
3214
|
+
"sed -i 's/^START_KNOCKD=0/START_KNOCKD=1/' /etc/default/knockd 2>/dev/null || true",
|
|
3215
|
+
`sed -i "s|^KNOCKD_OPTS=.*|KNOCKD_OPTS=\\"-i $IFACE\\"|" /etc/default/knockd 2>/dev/null || true`,
|
|
3216
|
+
// Apply iptables rules
|
|
3217
|
+
...iptRules,
|
|
3218
|
+
"netfilter-persistent save 2>/dev/null || true",
|
|
3219
|
+
"systemctl enable knockd",
|
|
3220
|
+
"systemctl restart knockd",
|
|
3221
|
+
"echo 'knockd configured OK'",
|
|
3222
|
+
].join("\n");
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
// ── ssh whitelist-me ─────────────────────────────────────────────────────────
|
|
3226
|
+
|
|
3227
|
+
export async function azureSshWhitelistMe(opts = {}) {
|
|
3228
|
+
const execa = await lazyExeca();
|
|
3229
|
+
const sub = opts.profile;
|
|
3230
|
+
await ensureAzCli(execa);
|
|
3231
|
+
await ensureAzAuth(execa, { subscription: sub });
|
|
3232
|
+
const state = requireVmState(opts.vmName);
|
|
3233
|
+
const { vmName, resourceGroup: rg } = state;
|
|
3234
|
+
|
|
3235
|
+
const myIp = await fetchMyIp();
|
|
3236
|
+
if (!myIp) {
|
|
3237
|
+
console.error(ERR("\n Could not detect your public IP address.\n"));
|
|
3238
|
+
process.exit(1);
|
|
3239
|
+
}
|
|
3240
|
+
const myCidr = `${myIp}/32`;
|
|
3241
|
+
|
|
3242
|
+
// Resolve NSG name from NIC
|
|
3243
|
+
const nicName = `${vmName}VMNic`;
|
|
3244
|
+
const { stdout: nicJson, exitCode: nicCode } = await execa("az", [
|
|
3245
|
+
"network", "nic", "show", "-g", rg, "-n", nicName, "--output", "json",
|
|
3246
|
+
...subArgs(sub),
|
|
3247
|
+
], { reject: false, timeout: 15000 });
|
|
3248
|
+
let nsgName = `${vmName}NSG`;
|
|
3249
|
+
if (nicCode === 0) {
|
|
3250
|
+
const nsgId = JSON.parse(nicJson).networkSecurityGroup?.id || "";
|
|
3251
|
+
if (nsgId) nsgName = nsgId.split("/").pop();
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
// Fetch current SSH rule
|
|
3255
|
+
const { stdout: rulesJson, exitCode: rulesCode } = await execa("az", [
|
|
3256
|
+
"network", "nsg", "rule", "list", "-g", rg, "--nsg-name", nsgName, "--output", "json",
|
|
3257
|
+
...subArgs(sub),
|
|
3258
|
+
], { reject: false, timeout: 15000 });
|
|
3259
|
+
const rules = rulesCode === 0 ? JSON.parse(rulesJson || "[]") : [];
|
|
3260
|
+
const inboundAllow = rules.filter(r => r.access === "Allow" && r.direction === "Inbound");
|
|
3261
|
+
const sshRule = inboundAllow.find(r =>
|
|
3262
|
+
r.destinationPortRange === "22" ||
|
|
3263
|
+
(Array.isArray(r.destinationPortRanges) && r.destinationPortRanges.includes("22"))
|
|
3264
|
+
);
|
|
3265
|
+
|
|
3266
|
+
const currentSources = [];
|
|
3267
|
+
if (sshRule?.sourceAddressPrefix) currentSources.push(sshRule.sourceAddressPrefix);
|
|
3268
|
+
if (Array.isArray(sshRule?.sourceAddressPrefixes)) currentSources.push(...sshRule.sourceAddressPrefixes);
|
|
3269
|
+
|
|
3270
|
+
if (currentSources.includes(myCidr)) {
|
|
3271
|
+
console.log(OK(`\n ✓ ${myCidr} is already whitelisted for SSH on ${vmName}\n`));
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
const merged = [...new Set([...currentSources.filter(s => s && s !== "*" && s !== "Internet"), myCidr])];
|
|
3276
|
+
console.log(chalk.yellow(` ↻ Adding ${myCidr} to SSH rule on ${nsgName} (${currentSources.length} existing)...`));
|
|
3277
|
+
const { exitCode: updateCode } = await execa("az", [
|
|
3278
|
+
"network", "nsg", "rule", "create", "-g", rg, "--nsg-name", nsgName,
|
|
3279
|
+
"-n", sshRule?.name || "allow-ssh", "--priority", String(sshRule?.priority || 1000),
|
|
3280
|
+
"--destination-port-ranges", "22", "--access", "Allow",
|
|
3281
|
+
"--source-address-prefixes", ...merged,
|
|
3282
|
+
"--protocol", "Tcp", "--direction", "Inbound", "--output", "none",
|
|
3283
|
+
...subArgs(sub),
|
|
3284
|
+
], { reject: false, timeout: 30000 });
|
|
3285
|
+
|
|
3286
|
+
if (updateCode !== 0) {
|
|
3287
|
+
console.error(ERR(`\n Failed to update NSG rule on ${nsgName}\n`));
|
|
3288
|
+
process.exit(1);
|
|
3289
|
+
}
|
|
3290
|
+
console.log(OK(`\n ✓ SSH (22) whitelisted for ${myCidr} on ${vmName} (${nsgName})\n`));
|
|
3291
|
+
console.log(` Sources: ${merged.join(", ")}\n`);
|
|
3292
|
+
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
DEFAULTS, DIM, OK, WARN, ERR,
|
|
9
9
|
banner, hint, kvLine,
|
|
10
10
|
resolvePublicIp, subArgs, buildTags, fetchMyIp,
|
|
11
|
-
sshCmd, waitForSsh, fopsUpCmd, buildPublicUrl,
|
|
11
|
+
sshCmd, waitForSsh, closeMux, fopsUpCmd, buildPublicUrl,
|
|
12
12
|
runReconcilers, ensureOpenAiNetworkAccess,
|
|
13
13
|
reconcileOk, RECONCILE_LABEL_WIDTH,
|
|
14
14
|
} from "./azure-helpers.js";
|
|
@@ -448,7 +448,36 @@ async function vmReconcileNetworking(ctx) {
|
|
|
448
448
|
}
|
|
449
449
|
}
|
|
450
450
|
} else {
|
|
451
|
-
|
|
451
|
+
// No public IP attached — create one and attach it
|
|
452
|
+
const pipName = `${vmName}PublicIP`;
|
|
453
|
+
console.log(chalk.dim(` ↻ ${"Public IP".padEnd(RECONCILE_LABEL_WIDTH)} — creating ${pipName}…`));
|
|
454
|
+
const { exitCode: createCode } = await execa("az", [
|
|
455
|
+
"network", "public-ip", "create", "-g", rg, "-n", pipName,
|
|
456
|
+
"--sku", "Standard", "--allocation-method", "Static", "--output", "none",
|
|
457
|
+
...subArgs(sub),
|
|
458
|
+
], { reject: false, timeout: 30000 });
|
|
459
|
+
if (createCode === 0) {
|
|
460
|
+
const { exitCode: attachCode } = await execa("az", [
|
|
461
|
+
"network", "nic", "ip-config", "update",
|
|
462
|
+
"-g", rg, "--nic-name", ctx.nicName, "-n", "ipconfig1",
|
|
463
|
+
"--public-ip-address", pipName, "--output", "none",
|
|
464
|
+
...subArgs(sub),
|
|
465
|
+
], { reject: false, timeout: 30000 });
|
|
466
|
+
if (attachCode === 0) {
|
|
467
|
+
const { stdout: newPipJson } = await execa("az", [
|
|
468
|
+
"network", "public-ip", "show", "-g", rg, "-n", pipName, "--output", "json",
|
|
469
|
+
...subArgs(sub),
|
|
470
|
+
], { reject: false, timeout: 15000 });
|
|
471
|
+
try {
|
|
472
|
+
ctx.ip = JSON.parse(newPipJson).ipAddress || "";
|
|
473
|
+
} catch {}
|
|
474
|
+
reconcileOk("Public IP", ctx.ip ? `${ctx.ip} (created)` : `${pipName} (created)`);
|
|
475
|
+
} else {
|
|
476
|
+
console.log(chalk.yellow(` ⚠ Created ${pipName} but could not attach to NIC`));
|
|
477
|
+
}
|
|
478
|
+
} else {
|
|
479
|
+
console.log(chalk.yellow(` ⚠ Could not create public IP — VM will have no public IP`));
|
|
480
|
+
}
|
|
452
481
|
}
|
|
453
482
|
|
|
454
483
|
const nsgId = nic.networkSecurityGroup?.id || "";
|
|
@@ -560,6 +589,7 @@ async function vmReconcileNsg(ctx) {
|
|
|
560
589
|
...subArgs(sub),
|
|
561
590
|
], { reject: false, timeout: 30000 });
|
|
562
591
|
reconcileOk("SSH (22)", source);
|
|
592
|
+
ctx.operatorIpChanged = true;
|
|
563
593
|
} else if (sshSource && !sshHasMyIp) {
|
|
564
594
|
// Add our IP to existing admin IPs
|
|
565
595
|
const merged = [...new Set([...sshCurrentSources, sshSource])];
|
|
@@ -573,6 +603,7 @@ async function vmReconcileNsg(ctx) {
|
|
|
573
603
|
...subArgs(sub),
|
|
574
604
|
], { reject: false, timeout: 30000 });
|
|
575
605
|
reconcileOk("SSH (22)", merged.join(", "));
|
|
606
|
+
ctx.operatorIpChanged = true;
|
|
576
607
|
} else {
|
|
577
608
|
reconcileOk("SSH (22)", sshCurrentSources.join(", ") || "*");
|
|
578
609
|
}
|
|
@@ -1028,12 +1059,19 @@ async function removeSshBypassViaRunCommand(execa, rg, vmName, sourceCidr, sub)
|
|
|
1028
1059
|
async function vmReconcileSsh(ctx) {
|
|
1029
1060
|
const { execa, ip, adminUser, knockSequence, port, desiredUrl, vmName, rg, sub } = ctx;
|
|
1030
1061
|
console.log(chalk.dim(" Checking SSH..."));
|
|
1031
|
-
|
|
1062
|
+
// Close any stale mux master before probing. ControlPersist=600 keeps the SSH master
|
|
1063
|
+
// alive for 10 min, but if the VM rebooted its underlying TCP is broken — every probe
|
|
1064
|
+
// through the stale mux fails even after a successful knock. A fresh connect fixes it.
|
|
1065
|
+
await closeMux(execa, ip, adminUser);
|
|
1066
|
+
// When Run Command is available (rg set), use a short knock probe then fall through to
|
|
1067
|
+
// Run Command rather than burning 2+ minutes on knock retries.
|
|
1068
|
+
const canRunCommand = knockSequence?.length && rg;
|
|
1069
|
+
const maxWaitFirst = canRunCommand ? 20000 : 90000;
|
|
1032
1070
|
if (knockSequence?.length) {
|
|
1033
1071
|
await performKnock(ip, knockSequence, { quiet: true });
|
|
1034
1072
|
}
|
|
1035
1073
|
let sshReady = await waitForSsh(execa, ip, adminUser, maxWaitFirst);
|
|
1036
|
-
if (!sshReady && knockSequence?.length) {
|
|
1074
|
+
if (!sshReady && knockSequence?.length && !canRunCommand) {
|
|
1037
1075
|
console.log(chalk.dim(" Re-sending knock and retrying SSH..."));
|
|
1038
1076
|
await performKnock(ip, knockSequence, { quiet: true });
|
|
1039
1077
|
await new Promise((r) => setTimeout(r, 5000));
|
|
@@ -1081,8 +1119,25 @@ async function vmReconcileKnock(ctx) {
|
|
|
1081
1119
|
);
|
|
1082
1120
|
const knockdRunning = knockdCheck?.trim() === "active";
|
|
1083
1121
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1122
|
+
// Read the authoritative sequence from knockd.conf on the VM
|
|
1123
|
+
const { stdout: knockConfOut } = await ssh(
|
|
1124
|
+
"grep -m1 'sequence' /etc/knockd.conf 2>/dev/null | sed 's/.*sequence[[:space:]]*=[[:space:]]*//'", 8000,
|
|
1125
|
+
);
|
|
1126
|
+
const vmKnockSeq = (knockConfOut || "").trim()
|
|
1127
|
+
.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => Number.isInteger(n) && n > 0);
|
|
1128
|
+
|
|
1129
|
+
if (knockdRunning && vmKnockSeq.length >= 2) {
|
|
1130
|
+
// Reconcile: keep tag + local state in sync with knockd.conf
|
|
1131
|
+
const local = ctx.knockSequence;
|
|
1132
|
+
const seqDiffers = !local?.length || local.length !== vmKnockSeq.length || local.some((v, i) => v !== vmKnockSeq[i]);
|
|
1133
|
+
if (seqDiffers) {
|
|
1134
|
+
console.log(chalk.dim(` ↻ ${"Port knocking".padEnd(RECONCILE_LABEL_WIDTH)} — sequence drift detected, syncing…`));
|
|
1135
|
+
writeVmState(vmName, { knockSequence: vmKnockSeq });
|
|
1136
|
+
const { setVmKnockTag } = await import("./azure-helpers.js");
|
|
1137
|
+
await setVmKnockTag(execa, ctx.rg, vmName, vmKnockSeq, ctx.sub);
|
|
1138
|
+
ctx.knockSequence = vmKnockSeq;
|
|
1139
|
+
}
|
|
1140
|
+
reconcileOk("Port knocking", `active [${vmKnockSeq.join(", ")}]`);
|
|
1086
1141
|
} else if (!knockdRunning) {
|
|
1087
1142
|
console.log(chalk.dim(" Setting up port knocking (after post-start + UI reachable)..."));
|
|
1088
1143
|
const knockSeq = generateKnockSequence();
|
|
@@ -1092,8 +1147,7 @@ async function vmReconcileKnock(ctx) {
|
|
|
1092
1147
|
await setVmKnockTag(execa, ctx.rg, vmName, knockSeq, ctx.sub);
|
|
1093
1148
|
ctx.knockSequence = knockSeq;
|
|
1094
1149
|
} else {
|
|
1095
|
-
console.log(chalk.yellow(" ⚠ knockd active on VM but
|
|
1096
|
-
console.log(chalk.dim(" To re-create: fops azure knock disable && fops azure up"));
|
|
1150
|
+
console.log(chalk.yellow(" ⚠ knockd active on VM but could not read sequence from knockd.conf"));
|
|
1097
1151
|
}
|
|
1098
1152
|
|
|
1099
1153
|
if (ctx.knockSequence?.length) {
|
|
@@ -27,7 +27,7 @@ export {
|
|
|
27
27
|
managedImageId, resolvePublicIp,
|
|
28
28
|
resolveGithubToken, verifyGithubToken,
|
|
29
29
|
sshCmd, closeMux, MUX_OPTS, muxSocketPath, waitForSsh,
|
|
30
|
-
knockForVm, fopsUpCmd,
|
|
30
|
+
knockForVm, ensureKnockSequence, fopsUpCmd,
|
|
31
31
|
runReconcilers, ensureOpenAiNetworkAccess,
|
|
32
32
|
} from "./azure-helpers.js";
|
|
33
33
|
|
|
@@ -46,7 +46,7 @@ export {
|
|
|
46
46
|
|
|
47
47
|
// ── VM operations ────────────────────────────────────────────────────────────
|
|
48
48
|
export {
|
|
49
|
-
azureStatus, azureTrinoStatus, azureSsh, azurePortForward, azureSshAdminAdd, azureVmCheck, azureAgent, azureOpenAiDebugVm,
|
|
49
|
+
azureStatus, azureTrinoStatus, azureSsh, azureSshWhitelistMe, azurePortForward, azureSshAdminAdd, azureVmCheck, azureAgent, azureOpenAiDebugVm,
|
|
50
50
|
azureDeploy, azurePull, azureDeployVersion, azureRunUp, azureConfig, azureConfigVersions, azureUpdate,
|
|
51
51
|
azureLogs, azureGrantAdmin, azureContext,
|
|
52
52
|
azureList, azureApply,
|