@usex/mikrotik-mcp 3.35.0 → 3.37.0
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/dist/cli.js +3 -3
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -3
- package/dist/shared/{cli-ymyndemx.js → cli-9p1cxztb.js} +259 -154
- package/dist/shared/{cli-8xwvyzks.js → cli-fpfchpjm.js} +1 -1
- package/dist/shared/{library-q35fq2gz.js → library-9pt0yeqy.js} +320 -154
- package/dist/shared/{library-ttqfk3a5.js → library-r6dx5hw2.js} +1 -1
- package/dist/ui/observability.html +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -84,7 +84,7 @@ import {
|
|
|
84
84
|
toggleAaaEntity,
|
|
85
85
|
updateAaaEntity,
|
|
86
86
|
writeBackup
|
|
87
|
-
} from "./shared/cli-
|
|
87
|
+
} from "./shared/cli-9p1cxztb.js";
|
|
88
88
|
|
|
89
89
|
// src/cli.ts
|
|
90
90
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -2233,7 +2233,7 @@ function registerPrompts(server) {
|
|
|
2233
2233
|
// package.json
|
|
2234
2234
|
var package_default = {
|
|
2235
2235
|
name: "@usex/mikrotik-mcp",
|
|
2236
|
-
version: "3.
|
|
2236
|
+
version: "3.37.0",
|
|
2237
2237
|
description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
2238
2238
|
keywords: [
|
|
2239
2239
|
"ai",
|
|
@@ -2326,7 +2326,7 @@ var package_default = {
|
|
|
2326
2326
|
"class-variance-authority": "^0.7.1",
|
|
2327
2327
|
clsx: "^2.1.1",
|
|
2328
2328
|
gsap: "^3.15.0",
|
|
2329
|
-
prettier: "^3.9.
|
|
2329
|
+
prettier: "^3.9.4",
|
|
2330
2330
|
react: "^19.2.7",
|
|
2331
2331
|
"react-dom": "^19.2.7",
|
|
2332
2332
|
recharts: "^3.9.0",
|
package/dist/index.d.ts
CHANGED
|
@@ -240,6 +240,12 @@ declare class MikroTikSSHClient {
|
|
|
240
240
|
* Resolves on success; rejects with a clear reason on failure.
|
|
241
241
|
*/
|
|
242
242
|
uploadFile(remotePath: string, data: Buffer): Promise<void>;
|
|
243
|
+
/**
|
|
244
|
+
* Download a file's bytes from the device over SFTP. `remotePath` is relative
|
|
245
|
+
* to the SFTP default directory (the flash root). Resolves with the raw file
|
|
246
|
+
* contents; rejects with a clear reason on failure.
|
|
247
|
+
*/
|
|
248
|
+
downloadFile(remotePath: string): Promise<Buffer>;
|
|
243
249
|
/** Open a persistent interactive shell channel (used by Safe Mode). */
|
|
244
250
|
shell(opts?: {
|
|
245
251
|
term?: string;
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
resolveDeviceName,
|
|
22
22
|
selectToolModules,
|
|
23
23
|
setConfig
|
|
24
|
-
} from "./shared/library-
|
|
24
|
+
} from "./shared/library-9pt0yeqy.js";
|
|
25
25
|
// src/server.ts
|
|
26
26
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
27
27
|
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -131,7 +131,7 @@ function registerPrompts(server) {
|
|
|
131
131
|
// package.json
|
|
132
132
|
var package_default = {
|
|
133
133
|
name: "@usex/mikrotik-mcp",
|
|
134
|
-
version: "3.
|
|
134
|
+
version: "3.37.0",
|
|
135
135
|
description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
136
136
|
keywords: [
|
|
137
137
|
"ai",
|
|
@@ -224,7 +224,7 @@ var package_default = {
|
|
|
224
224
|
"class-variance-authority": "^0.7.1",
|
|
225
225
|
clsx: "^2.1.1",
|
|
226
226
|
gsap: "^3.15.0",
|
|
227
|
-
prettier: "^3.9.
|
|
227
|
+
prettier: "^3.9.4",
|
|
228
228
|
react: "^19.2.7",
|
|
229
229
|
"react-dom": "^19.2.7",
|
|
230
230
|
recharts: "^3.9.0",
|
|
@@ -906,6 +906,29 @@ class MikroTikSSHClient {
|
|
|
906
906
|
});
|
|
907
907
|
});
|
|
908
908
|
}
|
|
909
|
+
downloadFile(remotePath) {
|
|
910
|
+
if (!this.client) {
|
|
911
|
+
return Promise.reject(new Error("Not connected to MikroTik device"));
|
|
912
|
+
}
|
|
913
|
+
const openSftp = this.client.sftp.bind(this.client);
|
|
914
|
+
return new Promise((resolve2, reject) => {
|
|
915
|
+
openSftp((err, sftp) => {
|
|
916
|
+
if (err) {
|
|
917
|
+
reject(new Error(`SFTP subsystem unavailable: ${err.message}`));
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
sftp.readFile(remotePath, (rerr, data) => {
|
|
921
|
+
try {
|
|
922
|
+
sftp.end();
|
|
923
|
+
} catch {}
|
|
924
|
+
if (rerr)
|
|
925
|
+
reject(new Error(`SFTP read failed: ${rerr.message}`));
|
|
926
|
+
else
|
|
927
|
+
resolve2(data);
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
}
|
|
909
932
|
shell(opts = {}) {
|
|
910
933
|
if (!this.client) {
|
|
911
934
|
return Promise.reject(new Error("Not connected to MikroTik device"));
|
|
@@ -1054,16 +1077,7 @@ class SafeModeManager {
|
|
|
1054
1077
|
if (dc.mac) {
|
|
1055
1078
|
return "Error: Safe Mode is not supported for a MAC-Telnet device " + `('${this.deviceName}' is reached by MAC ${dc.mac}). ` + "Connect over SSH (configure host/credentials) to use Safe Mode.";
|
|
1056
1079
|
}
|
|
1057
|
-
const ssh = new MikroTikSSHClient({
|
|
1058
|
-
host: dc.host,
|
|
1059
|
-
username: dc.username,
|
|
1060
|
-
password: dc.password,
|
|
1061
|
-
keyFilename: dc.keyFilename,
|
|
1062
|
-
privateKey: dc.privateKey,
|
|
1063
|
-
keyPassphrase: dc.keyPassphrase,
|
|
1064
|
-
port: dc.port,
|
|
1065
|
-
timeoutMs: dc.timeoutMs
|
|
1066
|
-
});
|
|
1080
|
+
const ssh = new MikroTikSSHClient({ ...sshOptionsOf(dc), jump: resolveJump(dc) });
|
|
1067
1081
|
if (!await ssh.connect()) {
|
|
1068
1082
|
return "Error: Failed to connect to MikroTik device for safe mode session.";
|
|
1069
1083
|
}
|
|
@@ -1257,16 +1271,7 @@ async function uploadFileToDevice(deviceName, remotePath, data) {
|
|
|
1257
1271
|
if (isMacTelnetDevice(dc)) {
|
|
1258
1272
|
throw new Error(`Cannot transfer a file to '${name}': it is reached over Layer-2 MAC-Telnet, which has no file ` + "transfer. Configure SSH (host + credentials) for this device, or have the router pull the file " + "itself with /tool fetch from a URL it can reach.");
|
|
1259
1273
|
}
|
|
1260
|
-
const ssh = new MikroTikSSHClient({
|
|
1261
|
-
host: dc.host,
|
|
1262
|
-
username: dc.username,
|
|
1263
|
-
password: dc.password,
|
|
1264
|
-
keyFilename: dc.keyFilename,
|
|
1265
|
-
privateKey: dc.privateKey,
|
|
1266
|
-
keyPassphrase: dc.keyPassphrase,
|
|
1267
|
-
port: dc.port,
|
|
1268
|
-
timeoutMs: dc.timeoutMs
|
|
1269
|
-
});
|
|
1274
|
+
const ssh = new MikroTikSSHClient({ ...sshOptionsOf(dc), jump: resolveJump(dc) });
|
|
1270
1275
|
if (!await ssh.connect()) {
|
|
1271
1276
|
throw new Error(connectErrorMessage(name, dc, ssh.lastError));
|
|
1272
1277
|
}
|
|
@@ -1276,6 +1281,22 @@ async function uploadFileToDevice(deviceName, remotePath, data) {
|
|
|
1276
1281
|
ssh.disconnect();
|
|
1277
1282
|
}
|
|
1278
1283
|
}
|
|
1284
|
+
async function downloadFileFromDevice(deviceName, remotePath) {
|
|
1285
|
+
const name = resolveDeviceName(deviceName);
|
|
1286
|
+
const dc = getDevice(deviceName);
|
|
1287
|
+
if (isMacTelnetDevice(dc)) {
|
|
1288
|
+
throw new Error(`Cannot download a file from '${name}': it is reached over Layer-2 MAC-Telnet, which has no ` + "file transfer. Configure SSH (host + credentials) for this device, or use a text export " + "(stdout mode) instead of a binary backup.");
|
|
1289
|
+
}
|
|
1290
|
+
const ssh = new MikroTikSSHClient({ ...sshOptionsOf(dc), jump: resolveJump(dc) });
|
|
1291
|
+
if (!await ssh.connect()) {
|
|
1292
|
+
throw new Error(connectErrorMessage(name, dc, ssh.lastError));
|
|
1293
|
+
}
|
|
1294
|
+
try {
|
|
1295
|
+
return await ssh.downloadFile(remotePath);
|
|
1296
|
+
} finally {
|
|
1297
|
+
ssh.disconnect();
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1279
1300
|
|
|
1280
1301
|
// src/core/registry.ts
|
|
1281
1302
|
import { z as z2 } from "zod";
|
|
@@ -2777,6 +2798,158 @@ ${rows.slice(0, 40).map((r) => ` \u2022 ${r.name ?? "?"} (${r.type ?? "?"})${r.
|
|
|
2777
2798
|
// src/tools/backup.ts
|
|
2778
2799
|
import { z as z5 } from "zod";
|
|
2779
2800
|
|
|
2801
|
+
// src/backups/disk-check.ts
|
|
2802
|
+
var DISK_THRESHOLD_PCT = 90;
|
|
2803
|
+
async function checkDiskSpace(ctx) {
|
|
2804
|
+
try {
|
|
2805
|
+
const raw = await executeMikrotikCommand("/system resource print", ctx);
|
|
2806
|
+
const sys = parseSystemResource(raw);
|
|
2807
|
+
if (!sys || sys.hddUsedPct == null) {
|
|
2808
|
+
return { low: false };
|
|
2809
|
+
}
|
|
2810
|
+
return {
|
|
2811
|
+
low: sys.hddUsedPct >= DISK_THRESHOLD_PCT,
|
|
2812
|
+
usedPct: sys.hddUsedPct,
|
|
2813
|
+
freeBytes: sys.freeHdd,
|
|
2814
|
+
totalBytes: sys.totalHdd
|
|
2815
|
+
};
|
|
2816
|
+
} catch {
|
|
2817
|
+
return { low: false };
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// src/backups/vault.ts
|
|
2822
|
+
import {
|
|
2823
|
+
existsSync as existsSync2,
|
|
2824
|
+
mkdirSync,
|
|
2825
|
+
readFileSync as readFileSync4,
|
|
2826
|
+
readdirSync,
|
|
2827
|
+
renameSync,
|
|
2828
|
+
rmSync,
|
|
2829
|
+
statSync,
|
|
2830
|
+
writeFileSync
|
|
2831
|
+
} from "fs";
|
|
2832
|
+
import { basename, join as join4 } from "path";
|
|
2833
|
+
function backupDir() {
|
|
2834
|
+
if (process.env.MIKROTIK_BACKUP_DIR)
|
|
2835
|
+
return process.env.MIKROTIK_BACKUP_DIR;
|
|
2836
|
+
try {
|
|
2837
|
+
const dir = getConfig().backupDir;
|
|
2838
|
+
if (dir)
|
|
2839
|
+
return dir;
|
|
2840
|
+
} catch {}
|
|
2841
|
+
return DEFAULT_BACKUP_DIR;
|
|
2842
|
+
}
|
|
2843
|
+
function safeName(name) {
|
|
2844
|
+
const base = basename(name);
|
|
2845
|
+
if (base !== name || base === "." || base === ".." || !/^[A-Za-z0-9._-]+$/.test(base)) {
|
|
2846
|
+
throw new Error(`invalid backup name: ${name}`);
|
|
2847
|
+
}
|
|
2848
|
+
return base;
|
|
2849
|
+
}
|
|
2850
|
+
function listBackups() {
|
|
2851
|
+
const dir = backupDir();
|
|
2852
|
+
if (!existsSync2(dir))
|
|
2853
|
+
return [];
|
|
2854
|
+
return readdirSync(dir).filter((f) => f.endsWith(".rsc") || f.endsWith(".backup")).map((f) => {
|
|
2855
|
+
const st = statSync(join4(dir, f));
|
|
2856
|
+
const us = f.indexOf("_");
|
|
2857
|
+
return {
|
|
2858
|
+
name: f,
|
|
2859
|
+
bytes: st.size,
|
|
2860
|
+
modified: st.mtimeMs,
|
|
2861
|
+
device: us > 0 ? f.slice(0, us) : undefined,
|
|
2862
|
+
type: f.endsWith(".backup") ? "backup" : "rsc"
|
|
2863
|
+
};
|
|
2864
|
+
}).sort((a, b) => b.modified - a.modified);
|
|
2865
|
+
}
|
|
2866
|
+
function readBackup(name) {
|
|
2867
|
+
return readFileSync4(join4(backupDir(), safeName(name)), "utf8");
|
|
2868
|
+
}
|
|
2869
|
+
function writeBackup(name, content) {
|
|
2870
|
+
const dir = backupDir();
|
|
2871
|
+
mkdirSync(dir, { recursive: true });
|
|
2872
|
+
let final = safeName(name);
|
|
2873
|
+
if (existsSync2(join4(dir, final))) {
|
|
2874
|
+
const dot = final.lastIndexOf(".");
|
|
2875
|
+
const stem = dot > 0 ? final.slice(0, dot) : final;
|
|
2876
|
+
const ext = dot > 0 ? final.slice(dot) : "";
|
|
2877
|
+
let n = 2;
|
|
2878
|
+
while (existsSync2(join4(dir, `${stem}_${n}${ext}`)))
|
|
2879
|
+
n++;
|
|
2880
|
+
final = `${stem}_${n}${ext}`;
|
|
2881
|
+
}
|
|
2882
|
+
writeFileSync(join4(dir, final), content, "utf8");
|
|
2883
|
+
return final;
|
|
2884
|
+
}
|
|
2885
|
+
function writeBinaryBackup(name, data) {
|
|
2886
|
+
const dir = backupDir();
|
|
2887
|
+
mkdirSync(dir, { recursive: true });
|
|
2888
|
+
let final = safeName(name);
|
|
2889
|
+
if (existsSync2(join4(dir, final))) {
|
|
2890
|
+
const dot = final.lastIndexOf(".");
|
|
2891
|
+
const stem = dot > 0 ? final.slice(0, dot) : final;
|
|
2892
|
+
const ext = dot > 0 ? final.slice(dot) : "";
|
|
2893
|
+
let n = 2;
|
|
2894
|
+
while (existsSync2(join4(dir, `${stem}_${n}${ext}`)))
|
|
2895
|
+
n++;
|
|
2896
|
+
final = `${stem}_${n}${ext}`;
|
|
2897
|
+
}
|
|
2898
|
+
writeFileSync(join4(dir, final), data);
|
|
2899
|
+
return final;
|
|
2900
|
+
}
|
|
2901
|
+
function deleteBackup(name) {
|
|
2902
|
+
const p = join4(backupDir(), safeName(name));
|
|
2903
|
+
if (!existsSync2(p))
|
|
2904
|
+
return false;
|
|
2905
|
+
rmSync(p);
|
|
2906
|
+
return true;
|
|
2907
|
+
}
|
|
2908
|
+
function renameBackup(oldName, newName) {
|
|
2909
|
+
const dir = backupDir();
|
|
2910
|
+
const from = join4(dir, safeName(oldName));
|
|
2911
|
+
let to = safeName(newName);
|
|
2912
|
+
if (!to.endsWith(".rsc"))
|
|
2913
|
+
to += ".rsc";
|
|
2914
|
+
const toPath = join4(dir, to);
|
|
2915
|
+
if (!existsSync2(from))
|
|
2916
|
+
throw new Error(`backup not found: ${oldName}`);
|
|
2917
|
+
if (existsSync2(toPath))
|
|
2918
|
+
throw new Error(`a backup named '${to}' already exists`);
|
|
2919
|
+
renameSync(from, toPath);
|
|
2920
|
+
return to;
|
|
2921
|
+
}
|
|
2922
|
+
function exportToCommands(text) {
|
|
2923
|
+
const lines = [];
|
|
2924
|
+
let buf = "";
|
|
2925
|
+
for (const raw of text.split(`
|
|
2926
|
+
`)) {
|
|
2927
|
+
const line = raw.replace(/\r$/, "");
|
|
2928
|
+
buf = buf ? `${buf} ${line.trim()}` : line;
|
|
2929
|
+
if (buf.trimEnd().endsWith("\\")) {
|
|
2930
|
+
buf = buf.trimEnd().slice(0, -1).trimEnd();
|
|
2931
|
+
continue;
|
|
2932
|
+
}
|
|
2933
|
+
lines.push(buf);
|
|
2934
|
+
buf = "";
|
|
2935
|
+
}
|
|
2936
|
+
if (buf)
|
|
2937
|
+
lines.push(buf);
|
|
2938
|
+
const cmds = [];
|
|
2939
|
+
let section = "";
|
|
2940
|
+
for (const line of lines) {
|
|
2941
|
+
const t = line.trim();
|
|
2942
|
+
if (!t || t.startsWith("#"))
|
|
2943
|
+
continue;
|
|
2944
|
+
if (t.startsWith("/")) {
|
|
2945
|
+
section = t;
|
|
2946
|
+
continue;
|
|
2947
|
+
}
|
|
2948
|
+
cmds.push(section ? `${section} ${t}` : t);
|
|
2949
|
+
}
|
|
2950
|
+
return cmds;
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2780
2953
|
// src/core/datestamp.ts
|
|
2781
2954
|
var MONTHS2 = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
2782
2955
|
function pad(n) {
|
|
@@ -2849,6 +3022,12 @@ async function deviceDateStamp(ctx) {
|
|
|
2849
3022
|
return hostStamp(new Date);
|
|
2850
3023
|
}
|
|
2851
3024
|
|
|
3025
|
+
// src/core/slug.ts
|
|
3026
|
+
function deviceSlug(name) {
|
|
3027
|
+
const s = (name ?? "").replace(/[^A-Za-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3028
|
+
return s || "device";
|
|
3029
|
+
}
|
|
3030
|
+
|
|
2852
3031
|
// src/tools/backup.ts
|
|
2853
3032
|
var backupTools = [
|
|
2854
3033
|
defineTool({
|
|
@@ -2865,6 +3044,7 @@ var backupTools = [
|
|
|
2865
3044
|
async handler(a, ctx) {
|
|
2866
3045
|
const name = a.name || `backup_${await deviceDateStamp(ctx)}`;
|
|
2867
3046
|
ctx.info(`Creating backup: name=${name}`);
|
|
3047
|
+
const disk = await checkDiskSpace(ctx);
|
|
2868
3048
|
const cmd = new Cmd("/system backup save").set("name", name);
|
|
2869
3049
|
if (a.dont_encrypt)
|
|
2870
3050
|
cmd.raw("dont-encrypt=yes");
|
|
@@ -2873,13 +3053,35 @@ var backupTools = [
|
|
|
2873
3053
|
if (!a.include_password)
|
|
2874
3054
|
cmd.raw("password-file=no");
|
|
2875
3055
|
const result = await executeMikrotikCommand(cmd.build(), ctx);
|
|
2876
|
-
if (result.includes("saved") || result.trim() === "") {
|
|
2877
|
-
|
|
2878
|
-
|
|
3056
|
+
if (!(result.includes("saved") || result.trim() === "")) {
|
|
3057
|
+
if (disk.low) {
|
|
3058
|
+
return `Failed to create backup (device disk is ${disk.usedPct?.toFixed(0)}% used): ${result}
|
|
2879
3059
|
|
|
2880
|
-
|
|
3060
|
+
` + "Tip: use create_local_backup or create_export instead \u2014 text exports can be captured " + "directly to the local vault without using any device disk space.";
|
|
3061
|
+
}
|
|
3062
|
+
return `Failed to create backup: ${result}`;
|
|
2881
3063
|
}
|
|
2882
|
-
|
|
3064
|
+
if (disk.low) {
|
|
3065
|
+
const dc = getDevice(ctx.device);
|
|
3066
|
+
if (isMacTelnetDevice(dc)) {
|
|
3067
|
+
return `Backup '${name}.backup' created on device, but disk is ${disk.usedPct?.toFixed(0)}% used. ` + "This is a MAC-Telnet device (no SFTP), so the file cannot be downloaded automatically. " + "Free disk space manually or use create_local_backup for a text export that bypasses device storage.";
|
|
3068
|
+
}
|
|
3069
|
+
const deviceFile = `${name}.backup`;
|
|
3070
|
+
try {
|
|
3071
|
+
ctx.info(`Low disk (${disk.usedPct?.toFixed(0)}%) \u2014 downloading backup to local vault`);
|
|
3072
|
+
const data = await downloadFileFromDevice(ctx.device, deviceFile);
|
|
3073
|
+
const device = resolveDeviceName(ctx.device);
|
|
3074
|
+
const vaultName = writeBinaryBackup(`${deviceSlug(device)}_${deviceFile}`, data);
|
|
3075
|
+
await executeMikrotikCommand(`/file remove ${deviceFile}`, ctx);
|
|
3076
|
+
return `[LOW DISK \u2014 ${disk.usedPct?.toFixed(0)}% used] Backup '${deviceFile}' created, downloaded to ` + `local vault as '${vaultName}' (${data.length} bytes) at ${backupDir()}, and removed from ` + "the device to free disk space.";
|
|
3077
|
+
} catch (e) {
|
|
3078
|
+
return `Backup '${name}.backup' created on device, but the automatic download-and-cleanup ` + `failed: ${e instanceof Error ? e.message : String(e)}. The file is still on the device.`;
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
const fileDetails = await executeMikrotikCommand(`/file print detail where name=${name}.backup`, ctx);
|
|
3082
|
+
return fileDetails ? `Backup created successfully:
|
|
3083
|
+
|
|
3084
|
+
${fileDetails}` : `Backup '${name}.backup' created successfully.`;
|
|
2883
3085
|
}
|
|
2884
3086
|
}),
|
|
2885
3087
|
defineTool({
|
|
@@ -2928,6 +3130,21 @@ ${result}`;
|
|
|
2928
3130
|
async handler(a, ctx) {
|
|
2929
3131
|
const name = a.name || `export_${await deviceDateStamp(ctx)}`;
|
|
2930
3132
|
ctx.info(`Creating export: name=${name}, format=${a.file_format}`);
|
|
3133
|
+
const disk = await checkDiskSpace(ctx);
|
|
3134
|
+
if (disk.low) {
|
|
3135
|
+
ctx.info(`Low disk (${disk.usedPct?.toFixed(0)}%) \u2014 redirecting export to local vault`);
|
|
3136
|
+
const cmd2 = new Cmd("/export");
|
|
3137
|
+
cmd2.raw(a.verbose ? "verbose" : null);
|
|
3138
|
+
cmd2.raw(a.compact ? "compact" : null);
|
|
3139
|
+
cmd2.raw(!a.hide_sensitive ? "show-sensitive" : null);
|
|
3140
|
+
const body = await executeMikrotikCommand(cmd2.build(), ctx);
|
|
3141
|
+
if (isEmpty(body) || looksLikeError(body)) {
|
|
3142
|
+
return `Failed to create export: ${body}`;
|
|
3143
|
+
}
|
|
3144
|
+
const device = resolveDeviceName(ctx.device);
|
|
3145
|
+
const vaultName = writeBackup(`${deviceSlug(device)}_${name}.rsc`, body);
|
|
3146
|
+
return `[LOW DISK \u2014 ${disk.usedPct?.toFixed(0)}% used] Export saved to LOCAL VAULT as '${vaultName}' ` + `(${Buffer.byteLength(body)} bytes) at ${backupDir()} \u2014 no file was written to the device.`;
|
|
3147
|
+
}
|
|
2931
3148
|
const extension = a.file_format === "json" || a.file_format === "xml" ? a.file_format : "rsc";
|
|
2932
3149
|
const fullName = `${name}.${extension}`;
|
|
2933
3150
|
const cmd = new Cmd("/export");
|
|
@@ -2966,6 +3183,20 @@ ${fileDetails}` : `Export '${fullName}' created successfully.`;
|
|
|
2966
3183
|
name = `export_${cleanSection}_${await deviceDateStamp(ctx)}`;
|
|
2967
3184
|
}
|
|
2968
3185
|
ctx.info(`Exporting section: section=${a.section}, name=${name}`);
|
|
3186
|
+
const disk = await checkDiskSpace(ctx);
|
|
3187
|
+
if (disk.low) {
|
|
3188
|
+
ctx.info(`Low disk (${disk.usedPct?.toFixed(0)}%) \u2014 redirecting section export to local vault`);
|
|
3189
|
+
const cmd2 = new Cmd(`/${a.section} export`);
|
|
3190
|
+
cmd2.raw(!a.hide_sensitive ? "show-sensitive" : null);
|
|
3191
|
+
cmd2.raw(a.compact ? "compact" : null);
|
|
3192
|
+
const body = await executeMikrotikCommand(cmd2.build(), ctx);
|
|
3193
|
+
if (isEmpty(body) || looksLikeError(body)) {
|
|
3194
|
+
return `Failed to export section: ${body}`;
|
|
3195
|
+
}
|
|
3196
|
+
const device = resolveDeviceName(ctx.device);
|
|
3197
|
+
const vaultName = writeBackup(`${deviceSlug(device)}_${name}.rsc`, body);
|
|
3198
|
+
return `[LOW DISK \u2014 ${disk.usedPct?.toFixed(0)}% used] Section export saved to LOCAL VAULT as ` + `'${vaultName}' (${Buffer.byteLength(body)} bytes) at ${backupDir()} \u2014 no file was written ` + "to the device.";
|
|
3199
|
+
}
|
|
2969
3200
|
const cmd = new Cmd(`/${a.section} export`).set("file", name);
|
|
2970
3201
|
cmd.raw(!a.hide_sensitive ? "show-sensitive" : null);
|
|
2971
3202
|
cmd.raw(a.compact ? "compact" : null);
|
|
@@ -3116,127 +3347,6 @@ ${result}`;
|
|
|
3116
3347
|
// src/tools/local-backup.ts
|
|
3117
3348
|
import { z as z6 } from "zod";
|
|
3118
3349
|
|
|
3119
|
-
// src/core/slug.ts
|
|
3120
|
-
function deviceSlug(name) {
|
|
3121
|
-
const s = (name ?? "").replace(/[^A-Za-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3122
|
-
return s || "device";
|
|
3123
|
-
}
|
|
3124
|
-
|
|
3125
|
-
// src/backups/vault.ts
|
|
3126
|
-
import {
|
|
3127
|
-
existsSync as existsSync2,
|
|
3128
|
-
mkdirSync,
|
|
3129
|
-
readFileSync as readFileSync4,
|
|
3130
|
-
readdirSync,
|
|
3131
|
-
renameSync,
|
|
3132
|
-
rmSync,
|
|
3133
|
-
statSync,
|
|
3134
|
-
writeFileSync
|
|
3135
|
-
} from "fs";
|
|
3136
|
-
import { basename, join as join4 } from "path";
|
|
3137
|
-
function backupDir() {
|
|
3138
|
-
if (process.env.MIKROTIK_BACKUP_DIR)
|
|
3139
|
-
return process.env.MIKROTIK_BACKUP_DIR;
|
|
3140
|
-
try {
|
|
3141
|
-
const dir = getConfig().backupDir;
|
|
3142
|
-
if (dir)
|
|
3143
|
-
return dir;
|
|
3144
|
-
} catch {}
|
|
3145
|
-
return DEFAULT_BACKUP_DIR;
|
|
3146
|
-
}
|
|
3147
|
-
function safeName(name) {
|
|
3148
|
-
const base = basename(name);
|
|
3149
|
-
if (base !== name || base === "." || base === ".." || !/^[A-Za-z0-9._-]+$/.test(base)) {
|
|
3150
|
-
throw new Error(`invalid backup name: ${name}`);
|
|
3151
|
-
}
|
|
3152
|
-
return base;
|
|
3153
|
-
}
|
|
3154
|
-
function listBackups() {
|
|
3155
|
-
const dir = backupDir();
|
|
3156
|
-
if (!existsSync2(dir))
|
|
3157
|
-
return [];
|
|
3158
|
-
return readdirSync(dir).filter((f) => f.endsWith(".rsc")).map((f) => {
|
|
3159
|
-
const st = statSync(join4(dir, f));
|
|
3160
|
-
const us = f.indexOf("_");
|
|
3161
|
-
return {
|
|
3162
|
-
name: f,
|
|
3163
|
-
bytes: st.size,
|
|
3164
|
-
modified: st.mtimeMs,
|
|
3165
|
-
device: us > 0 ? f.slice(0, us) : undefined
|
|
3166
|
-
};
|
|
3167
|
-
}).sort((a, b) => b.modified - a.modified);
|
|
3168
|
-
}
|
|
3169
|
-
function readBackup(name) {
|
|
3170
|
-
return readFileSync4(join4(backupDir(), safeName(name)), "utf8");
|
|
3171
|
-
}
|
|
3172
|
-
function writeBackup(name, content) {
|
|
3173
|
-
const dir = backupDir();
|
|
3174
|
-
mkdirSync(dir, { recursive: true });
|
|
3175
|
-
let final = safeName(name);
|
|
3176
|
-
if (existsSync2(join4(dir, final))) {
|
|
3177
|
-
const dot = final.lastIndexOf(".");
|
|
3178
|
-
const stem = dot > 0 ? final.slice(0, dot) : final;
|
|
3179
|
-
const ext = dot > 0 ? final.slice(dot) : "";
|
|
3180
|
-
let n = 2;
|
|
3181
|
-
while (existsSync2(join4(dir, `${stem}_${n}${ext}`)))
|
|
3182
|
-
n++;
|
|
3183
|
-
final = `${stem}_${n}${ext}`;
|
|
3184
|
-
}
|
|
3185
|
-
writeFileSync(join4(dir, final), content, "utf8");
|
|
3186
|
-
return final;
|
|
3187
|
-
}
|
|
3188
|
-
function deleteBackup(name) {
|
|
3189
|
-
const p = join4(backupDir(), safeName(name));
|
|
3190
|
-
if (!existsSync2(p))
|
|
3191
|
-
return false;
|
|
3192
|
-
rmSync(p);
|
|
3193
|
-
return true;
|
|
3194
|
-
}
|
|
3195
|
-
function renameBackup(oldName, newName) {
|
|
3196
|
-
const dir = backupDir();
|
|
3197
|
-
const from = join4(dir, safeName(oldName));
|
|
3198
|
-
let to = safeName(newName);
|
|
3199
|
-
if (!to.endsWith(".rsc"))
|
|
3200
|
-
to += ".rsc";
|
|
3201
|
-
const toPath = join4(dir, to);
|
|
3202
|
-
if (!existsSync2(from))
|
|
3203
|
-
throw new Error(`backup not found: ${oldName}`);
|
|
3204
|
-
if (existsSync2(toPath))
|
|
3205
|
-
throw new Error(`a backup named '${to}' already exists`);
|
|
3206
|
-
renameSync(from, toPath);
|
|
3207
|
-
return to;
|
|
3208
|
-
}
|
|
3209
|
-
function exportToCommands(text) {
|
|
3210
|
-
const lines = [];
|
|
3211
|
-
let buf = "";
|
|
3212
|
-
for (const raw of text.split(`
|
|
3213
|
-
`)) {
|
|
3214
|
-
const line = raw.replace(/\r$/, "");
|
|
3215
|
-
buf = buf ? `${buf} ${line.trim()}` : line;
|
|
3216
|
-
if (buf.trimEnd().endsWith("\\")) {
|
|
3217
|
-
buf = buf.trimEnd().slice(0, -1).trimEnd();
|
|
3218
|
-
continue;
|
|
3219
|
-
}
|
|
3220
|
-
lines.push(buf);
|
|
3221
|
-
buf = "";
|
|
3222
|
-
}
|
|
3223
|
-
if (buf)
|
|
3224
|
-
lines.push(buf);
|
|
3225
|
-
const cmds = [];
|
|
3226
|
-
let section = "";
|
|
3227
|
-
for (const line of lines) {
|
|
3228
|
-
const t = line.trim();
|
|
3229
|
-
if (!t || t.startsWith("#"))
|
|
3230
|
-
continue;
|
|
3231
|
-
if (t.startsWith("/")) {
|
|
3232
|
-
section = t;
|
|
3233
|
-
continue;
|
|
3234
|
-
}
|
|
3235
|
-
cmds.push(section ? `${section} ${t}` : t);
|
|
3236
|
-
}
|
|
3237
|
-
return cmds;
|
|
3238
|
-
}
|
|
3239
|
-
|
|
3240
3350
|
// src/backups/create.ts
|
|
3241
3351
|
function labelSlug(label) {
|
|
3242
3352
|
const s = (label ?? "").replace(/[^A-Za-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
@@ -6031,7 +6141,7 @@ var cache = null;
|
|
|
6031
6141
|
async function gateway() {
|
|
6032
6142
|
if (cache)
|
|
6033
6143
|
return cache;
|
|
6034
|
-
const { moduleCatalog } = await import("./cli-
|
|
6144
|
+
const { moduleCatalog } = await import("./cli-fpfchpjm.js");
|
|
6035
6145
|
const forIndex = [];
|
|
6036
6146
|
const byName = new Map;
|
|
6037
6147
|
for (const mod of moduleCatalog) {
|
|
@@ -14042,13 +14152,8 @@ var sshTestTools = [
|
|
|
14042
14152
|
}
|
|
14043
14153
|
ctx.info(`Testing SSH from MCP host to configured device '${name}' (${dc.host}:${dc.port})`);
|
|
14044
14154
|
return probe({
|
|
14045
|
-
|
|
14046
|
-
|
|
14047
|
-
username: dc.username,
|
|
14048
|
-
password: dc.password,
|
|
14049
|
-
keyFilename: dc.keyFilename,
|
|
14050
|
-
privateKey: dc.privateKey,
|
|
14051
|
-
keyPassphrase: dc.keyPassphrase,
|
|
14155
|
+
...sshOptionsOf(dc),
|
|
14156
|
+
jump: resolveJump(dc),
|
|
14052
14157
|
timeoutMs: Math.min(dc.timeoutMs ?? 1e4, 1e4)
|
|
14053
14158
|
}, `'${name}' (${dc.host}:${dc.port})`, "/system identity print");
|
|
14054
14159
|
}
|