@gibme/mikrotik 20.0.1 → 22.0.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 +101 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +579 -616
- package/package.json +20 -23
package/dist/index.js
CHANGED
|
@@ -18,28 +18,16 @@
|
|
|
18
18
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
19
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
20
|
// SOFTWARE.
|
|
21
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
22
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
23
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
24
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
25
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
26
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
27
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
28
|
-
});
|
|
29
|
-
};
|
|
30
21
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
31
22
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
32
23
|
};
|
|
33
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
-
exports.Mikrotik =
|
|
25
|
+
exports.Mikrotik = void 0;
|
|
35
26
|
const ssh_1 = __importDefault(require("@gibme/ssh"));
|
|
36
27
|
const ip_address_1 = require("ip-address");
|
|
37
28
|
const memory_1 = __importDefault(require("@gibme/cache/memory"));
|
|
38
29
|
const dns_1 = require("dns");
|
|
39
30
|
const semver_1 = require("semver");
|
|
40
|
-
const abort_controller_1 = require("abort-controller");
|
|
41
|
-
Object.defineProperty(exports, "AbortController", { enumerable: true, get: function () { return abort_controller_1.AbortController; } });
|
|
42
|
-
Object.defineProperty(exports, "AbortSignal", { enumerable: true, get: function () { return abort_controller_1.AbortSignal; } });
|
|
43
31
|
/** @ignore */
|
|
44
32
|
const toVersion = (version) => (0, semver_1.valid)((0, semver_1.coerce)(version)) || version;
|
|
45
33
|
/** @ignore */
|
|
@@ -47,11 +35,15 @@ const toIP4 = (address) => {
|
|
|
47
35
|
try {
|
|
48
36
|
return new ip_address_1.Address4(address);
|
|
49
37
|
}
|
|
50
|
-
catch
|
|
38
|
+
catch {
|
|
51
39
|
return undefined;
|
|
52
40
|
}
|
|
53
41
|
};
|
|
54
42
|
class Mikrotik extends ssh_1.default {
|
|
43
|
+
static cache = new memory_1.default({
|
|
44
|
+
stdTTL: 15,
|
|
45
|
+
checkperiod: 17
|
|
46
|
+
});
|
|
55
47
|
/**
|
|
56
48
|
* Retrieves the routes in the routing table
|
|
57
49
|
*
|
|
@@ -59,111 +51,109 @@ class Mikrotik extends ssh_1.default {
|
|
|
59
51
|
* @param vrf
|
|
60
52
|
* @param active_only
|
|
61
53
|
*/
|
|
62
|
-
get_ip_routes() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
})();
|
|
105
|
-
const result = {
|
|
106
|
-
network: address,
|
|
107
|
-
cidr,
|
|
108
|
-
distance,
|
|
109
|
-
scope,
|
|
110
|
-
vrf: _vrf,
|
|
111
|
-
includes: (ipaddress) => {
|
|
112
|
-
const temp_address = toIP4(ipaddress);
|
|
113
|
-
const temp_network = toIP4(`${address}/${cidr}`);
|
|
114
|
-
if (!temp_address || !temp_network)
|
|
115
|
-
return false;
|
|
116
|
-
return temp_address.isInSubnet(temp_network);
|
|
117
|
-
}
|
|
54
|
+
async get_ip_routes(min_distance = 0, vrf, active_only = true) {
|
|
55
|
+
const [ifaces, ips] = await Promise.all([
|
|
56
|
+
this.get_interfaces(),
|
|
57
|
+
this.get_ip_addresses()
|
|
58
|
+
]);
|
|
59
|
+
const command = `/ip route print terse without-paging${active_only ? ' where active' : ''}`;
|
|
60
|
+
const routes = await this.terse(command);
|
|
61
|
+
return routes.map(route => {
|
|
62
|
+
const _network = route['dst-address'].split('/');
|
|
63
|
+
const address = _network[0];
|
|
64
|
+
const cidr = parseInt(_network[1]);
|
|
65
|
+
const [gateway, iface] = (() => {
|
|
66
|
+
if (!route.gateway)
|
|
67
|
+
return [undefined, undefined];
|
|
68
|
+
const gw = toIP4(route.gateway);
|
|
69
|
+
if (route.gateway.includes('%')) {
|
|
70
|
+
const [gateway, ...iface] = route.gateway.split('%');
|
|
71
|
+
return [gateway, iface.join('%')];
|
|
72
|
+
}
|
|
73
|
+
else if (gw) {
|
|
74
|
+
const ip = ips.filter(ip => ip.includes(route.gateway)).shift();
|
|
75
|
+
return [route.gateway, ip?.iface];
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const ip = ips.filter(ip => ip.iface === route.gateway).shift();
|
|
79
|
+
return [ip?.ipaddress, route.gateway];
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
const distance = parseInt(route.distance);
|
|
83
|
+
const scope = parseInt(route.scope);
|
|
84
|
+
const target_scope = parseInt(route.target_scope) || undefined;
|
|
85
|
+
// this does not account for if a device has multiple addresses in the same network
|
|
86
|
+
const preferred_source = ips.filter(ip => gateway ? ip.includes(gateway) : false).shift()?.ipaddress;
|
|
87
|
+
const _vrf = route['routing-table'] || route['routing-mark'] || 'main';
|
|
88
|
+
const tunnel = (() => {
|
|
89
|
+
const tunnel = ifaces.filter(_iface => _iface.name === iface).shift();
|
|
90
|
+
const parent = ips.filter(ip => ip.ipaddress === tunnel?.local_address).shift();
|
|
91
|
+
if (!tunnel || !parent)
|
|
92
|
+
return undefined;
|
|
93
|
+
return {
|
|
94
|
+
...tunnel,
|
|
95
|
+
parent
|
|
118
96
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
97
|
+
})();
|
|
98
|
+
const result = {
|
|
99
|
+
network: address,
|
|
100
|
+
cidr,
|
|
101
|
+
distance,
|
|
102
|
+
scope,
|
|
103
|
+
vrf: _vrf,
|
|
104
|
+
includes: (ipaddress) => {
|
|
105
|
+
const temp_address = toIP4(ipaddress);
|
|
106
|
+
const temp_network = toIP4(`${address}/${cidr}`);
|
|
107
|
+
if (!temp_address || !temp_network)
|
|
108
|
+
return false;
|
|
109
|
+
return temp_address.isInSubnet(temp_network);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
if (preferred_source)
|
|
113
|
+
result.preferred_source = preferred_source;
|
|
114
|
+
if (gateway)
|
|
115
|
+
result.gateway = gateway;
|
|
116
|
+
if (iface)
|
|
117
|
+
result.iface = iface;
|
|
118
|
+
if (tunnel)
|
|
119
|
+
result.tunnel = tunnel;
|
|
120
|
+
if (target_scope)
|
|
121
|
+
result.target_scope = target_scope;
|
|
122
|
+
if (route.comment)
|
|
123
|
+
result.comment = route.comment;
|
|
124
|
+
return result;
|
|
125
|
+
})
|
|
126
|
+
.filter(route => route.distance >= min_distance)
|
|
127
|
+
.filter(route => vrf ? route.vrf === vrf : true);
|
|
136
128
|
}
|
|
137
129
|
/**
|
|
138
130
|
* Retrieves the IP addresses active on the system
|
|
139
131
|
*
|
|
140
132
|
* @param active_only
|
|
141
133
|
*/
|
|
142
|
-
get_ip_addresses() {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return result;
|
|
166
|
-
});
|
|
134
|
+
async get_ip_addresses(active_only = true) {
|
|
135
|
+
const command = `/ip address print terse without-paging${active_only ? ' where !disabled' : ''}`;
|
|
136
|
+
const addresses = await this.terse(command);
|
|
137
|
+
return addresses.map(line => {
|
|
138
|
+
const _address = line.address.split('/');
|
|
139
|
+
const ipaddress = _address[0];
|
|
140
|
+
const cidr = parseInt(_address[1]);
|
|
141
|
+
const result = {
|
|
142
|
+
ipaddress,
|
|
143
|
+
network: line.network,
|
|
144
|
+
cidr,
|
|
145
|
+
iface: line.interface,
|
|
146
|
+
includes: (ipaddress) => {
|
|
147
|
+
const temp_address = toIP4(ipaddress);
|
|
148
|
+
const temp_network = toIP4(`${line.network}/${cidr}`);
|
|
149
|
+
if (!temp_address || !temp_network)
|
|
150
|
+
return false;
|
|
151
|
+
return temp_address.isInSubnet(temp_network);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
if (line.comment)
|
|
155
|
+
result.comment = line.comment;
|
|
156
|
+
return result;
|
|
167
157
|
});
|
|
168
158
|
}
|
|
169
159
|
/**
|
|
@@ -171,50 +161,48 @@ class Mikrotik extends ssh_1.default {
|
|
|
171
161
|
*
|
|
172
162
|
* @param active_only
|
|
173
163
|
*/
|
|
174
|
-
get_interfaces() {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return result;
|
|
217
|
-
});
|
|
164
|
+
async get_interfaces(active_only = true) {
|
|
165
|
+
const filter = active_only ? ' where !disabled' : '';
|
|
166
|
+
const [ifaces, gre, ipip, eoip] = await Promise.all([
|
|
167
|
+
this.terse(`/interface print terse without-paging${filter}`),
|
|
168
|
+
this.terse(`/interface gre print terse without-paging${filter}`),
|
|
169
|
+
this.terse(`/interface ipip print terse without-paging${filter}`),
|
|
170
|
+
this.terse(`/interface eoip print terse without-paging${filter}`)
|
|
171
|
+
]);
|
|
172
|
+
const tunnels = [...gre, ...ipip, ...eoip];
|
|
173
|
+
return ifaces.map(line => {
|
|
174
|
+
const _type = (() => {
|
|
175
|
+
switch (line.type) {
|
|
176
|
+
case 'ether':
|
|
177
|
+
return 'ethernet';
|
|
178
|
+
case 'lo':
|
|
179
|
+
return 'loopback';
|
|
180
|
+
case 'gre-tunnel':
|
|
181
|
+
return 'gre';
|
|
182
|
+
case 'ipip-tunnel':
|
|
183
|
+
return 'ipip';
|
|
184
|
+
case 'eoip-tunnel':
|
|
185
|
+
return 'eoip';
|
|
186
|
+
case 'wg':
|
|
187
|
+
return 'wireguard';
|
|
188
|
+
default:
|
|
189
|
+
return line.type;
|
|
190
|
+
}
|
|
191
|
+
})();
|
|
192
|
+
const tunnel = tunnels.filter(tunnel => tunnel.name === line.name).shift();
|
|
193
|
+
const local_address = tunnel?.['local-address'];
|
|
194
|
+
const remote_address = tunnel?.['remote-address'];
|
|
195
|
+
const result = {
|
|
196
|
+
name: line.name,
|
|
197
|
+
type: _type
|
|
198
|
+
};
|
|
199
|
+
if (local_address)
|
|
200
|
+
result.local_address = local_address;
|
|
201
|
+
if (remote_address)
|
|
202
|
+
result.remote_address = remote_address;
|
|
203
|
+
if (line.comment)
|
|
204
|
+
result.comment = line.comment;
|
|
205
|
+
return result;
|
|
218
206
|
});
|
|
219
207
|
}
|
|
220
208
|
/**
|
|
@@ -224,35 +212,33 @@ class Mikrotik extends ssh_1.default {
|
|
|
224
212
|
* @param min_distance
|
|
225
213
|
* @param vrf
|
|
226
214
|
*/
|
|
227
|
-
get_route_counts() {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
active: false
|
|
239
|
-
};
|
|
240
|
-
});
|
|
241
|
-
const best = {
|
|
242
|
-
ip: '',
|
|
243
|
-
count: 0
|
|
215
|
+
async get_route_counts(min_distance = 0, vrf) {
|
|
216
|
+
const [routes, ips] = await Promise.all([
|
|
217
|
+
this.get_ip_routes(min_distance, vrf),
|
|
218
|
+
this.get_ip_addresses()
|
|
219
|
+
]);
|
|
220
|
+
const results = {};
|
|
221
|
+
ips.forEach(ip => {
|
|
222
|
+
results[ip.ipaddress] = {
|
|
223
|
+
interface: ip.iface,
|
|
224
|
+
count: routes.filter(route => route.preferred_source === ip.ipaddress || route.tunnel?.local_address === ip.ipaddress).length,
|
|
225
|
+
active: false
|
|
244
226
|
};
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (results[best.
|
|
252
|
-
|
|
227
|
+
});
|
|
228
|
+
const best = {
|
|
229
|
+
ip: '',
|
|
230
|
+
count: 0
|
|
231
|
+
};
|
|
232
|
+
Object.keys(results).forEach(key => {
|
|
233
|
+
if (results[key].count > best.count) {
|
|
234
|
+
best.count = results[key].count;
|
|
235
|
+
best.ip = key;
|
|
253
236
|
}
|
|
254
|
-
return results;
|
|
255
237
|
});
|
|
238
|
+
if (results[best.ip]) {
|
|
239
|
+
results[best.ip].active = true;
|
|
240
|
+
}
|
|
241
|
+
return results;
|
|
256
242
|
}
|
|
257
243
|
/**
|
|
258
244
|
* Executes a bandwidth test
|
|
@@ -266,148 +252,142 @@ class Mikrotik extends ssh_1.default {
|
|
|
266
252
|
* @param password
|
|
267
253
|
* @param options
|
|
268
254
|
*/
|
|
269
|
-
bandwidth_test(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
options.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
localCPULoad: 0,
|
|
297
|
-
remoteCPULoad: 0
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
yield sleep(1000);
|
|
255
|
+
async bandwidth_test(target, username, password, options = {}) {
|
|
256
|
+
const sleep = async (timeout) => new Promise(resolve => setTimeout(resolve, timeout));
|
|
257
|
+
const controller = new AbortController();
|
|
258
|
+
options.signal ??= controller.signal;
|
|
259
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
260
|
+
return new Promise(async (resolve, reject) => {
|
|
261
|
+
options.duration ??= 15;
|
|
262
|
+
options.direction ??= 'both';
|
|
263
|
+
options.protocol ??= 'udp';
|
|
264
|
+
options.random_data ??= false;
|
|
265
|
+
options.callback ??= () => { };
|
|
266
|
+
/**
|
|
267
|
+
* This serves as a lazy "mutex" that prevents us from running
|
|
268
|
+
* a bandwidth test for against a target host that is already
|
|
269
|
+
* in the process of running a bandwidth test
|
|
270
|
+
*/
|
|
271
|
+
while (await Mikrotik.cache.includes(target)) {
|
|
272
|
+
if (options.callback) {
|
|
273
|
+
options.callback({
|
|
274
|
+
status: 'queued',
|
|
275
|
+
duration: 0,
|
|
276
|
+
randomData: options.random_data,
|
|
277
|
+
direction: options.direction,
|
|
278
|
+
connectionCount: 0,
|
|
279
|
+
localCPULoad: 0,
|
|
280
|
+
remoteCPULoad: 0
|
|
281
|
+
});
|
|
301
282
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
283
|
+
await sleep(1000);
|
|
284
|
+
}
|
|
285
|
+
const toBPS = (value) => {
|
|
286
|
+
if (!value) {
|
|
287
|
+
return 0;
|
|
288
|
+
}
|
|
289
|
+
value = value.toLowerCase();
|
|
290
|
+
if (value.includes('tbps')) {
|
|
291
|
+
const tmp = parseFloat(value) || 0;
|
|
292
|
+
return Math.round(tmp * 1000 * 1000 * 1000 * 1000);
|
|
293
|
+
}
|
|
294
|
+
else if (value.includes('gbps')) {
|
|
295
|
+
const tmp = parseFloat(value) || 0;
|
|
296
|
+
return Math.round(tmp * 1000 * 1000 * 1000);
|
|
297
|
+
}
|
|
298
|
+
else if (value.includes('mbps')) {
|
|
299
|
+
const tmp = parseFloat(value) || 0;
|
|
300
|
+
return Math.round(tmp * 1000 * 1000);
|
|
301
|
+
}
|
|
302
|
+
else if (value.includes('kbps')) {
|
|
303
|
+
const tmp = parseFloat(value) || 0;
|
|
304
|
+
return Math.round(tmp * 1000);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
return Math.round(parseFloat(value) || 0);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
/**
|
|
311
|
+
* Performs further processing of the stream of data returned
|
|
312
|
+
* during the bandwidth testing
|
|
313
|
+
*
|
|
314
|
+
* @param buffer
|
|
315
|
+
*/
|
|
316
|
+
async function handleStream(buffer) {
|
|
317
|
+
const frame = new Mikrotik.BandwidthTest.Frame(buffer).parse();
|
|
318
|
+
const result = {
|
|
319
|
+
status: frame.status,
|
|
320
|
+
duration: parseInt(frame.duration) || 0,
|
|
321
|
+
randomData: frame['random-data'] === 'yes',
|
|
322
|
+
direction: frame.direction,
|
|
323
|
+
connectionCount: parseInt(frame['connection-count']) || 0,
|
|
324
|
+
localCPULoad: (parseInt(frame['local-cpu-load']) || 0) / 100,
|
|
325
|
+
remoteCPULoad: (parseInt(frame['remote-cpu-load']) || 0) / 100
|
|
326
326
|
};
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const result = {
|
|
337
|
-
status: frame.status,
|
|
338
|
-
duration: parseInt(frame.duration) || 0,
|
|
339
|
-
randomData: frame['random-data'] === 'yes',
|
|
340
|
-
direction: frame.direction,
|
|
341
|
-
connectionCount: parseInt(frame['connection-count']) || 0,
|
|
342
|
-
localCPULoad: (parseInt(frame['local-cpu-load']) || 0) / 100,
|
|
343
|
-
remoteCPULoad: (parseInt(frame['remote-cpu-load']) || 0) / 100
|
|
344
|
-
};
|
|
345
|
-
if (frame['lost-packets'])
|
|
346
|
-
result.lostPackets = parseInt(frame['lost-packets']) || 0;
|
|
347
|
-
if (frame['tx-current']) {
|
|
348
|
-
result.transmit = {
|
|
349
|
-
current: toBPS(frame['tx-current']),
|
|
350
|
-
shortAverage: toBPS(frame['tx-10-second-average']),
|
|
351
|
-
totalAverage: toBPS(frame['tx-total-average']),
|
|
352
|
-
size: toBPS(frame['tx-size'])
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
if (frame['rx-current']) {
|
|
356
|
-
result.receive = {
|
|
357
|
-
current: toBPS(frame['rx-current']),
|
|
358
|
-
shortAverage: toBPS(frame['rx-10-second-average']),
|
|
359
|
-
totalAverage: toBPS(frame['rx-total-average']),
|
|
360
|
-
size: toBPS(frame['rx-size'])
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
switch (result.status) {
|
|
364
|
-
case 'done testing':
|
|
365
|
-
if (options.callback)
|
|
366
|
-
options.callback(result);
|
|
367
|
-
return resolve(result);
|
|
368
|
-
case 'authentication_failed':
|
|
369
|
-
return reject(new Error('Authentication Failed'));
|
|
370
|
-
case 'connecting':
|
|
371
|
-
case 'running':
|
|
372
|
-
yield Mikrotik.cache.ttl(target, options.duration); // bump our mutex
|
|
373
|
-
if (options.callback)
|
|
374
|
-
options.callback(result);
|
|
375
|
-
break;
|
|
376
|
-
}
|
|
377
|
-
});
|
|
327
|
+
if (frame['lost-packets'])
|
|
328
|
+
result.lostPackets = parseInt(frame['lost-packets']) || 0;
|
|
329
|
+
if (frame['tx-current']) {
|
|
330
|
+
result.transmit = {
|
|
331
|
+
current: toBPS(frame['tx-current']),
|
|
332
|
+
shortAverage: toBPS(frame['tx-10-second-average']),
|
|
333
|
+
totalAverage: toBPS(frame['tx-total-average']),
|
|
334
|
+
size: toBPS(frame['tx-size'])
|
|
335
|
+
};
|
|
378
336
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
`address=${target} random-data=${options.random_data ? 'yes' : 'no'} interval=1s`;
|
|
387
|
-
if (options.local_tx_speed) {
|
|
388
|
-
command += ` local-tx-speed=${options.local_tx_speed}M`;
|
|
337
|
+
if (frame['rx-current']) {
|
|
338
|
+
result.receive = {
|
|
339
|
+
current: toBPS(frame['rx-current']),
|
|
340
|
+
shortAverage: toBPS(frame['rx-10-second-average']),
|
|
341
|
+
totalAverage: toBPS(frame['rx-total-average']),
|
|
342
|
+
size: toBPS(frame['rx-size'])
|
|
343
|
+
};
|
|
389
344
|
}
|
|
390
|
-
|
|
391
|
-
|
|
345
|
+
switch (result.status) {
|
|
346
|
+
case 'done testing':
|
|
347
|
+
if (options.callback)
|
|
348
|
+
options.callback(result);
|
|
349
|
+
return resolve(result);
|
|
350
|
+
case 'authentication_failed':
|
|
351
|
+
return reject(new Error('Authentication Failed'));
|
|
352
|
+
case 'connecting':
|
|
353
|
+
case 'running':
|
|
354
|
+
await Mikrotik.cache.ttl(target, options.duration); // bump our mutex
|
|
355
|
+
if (options.callback)
|
|
356
|
+
options.callback(result);
|
|
357
|
+
break;
|
|
392
358
|
}
|
|
393
|
-
|
|
394
|
-
|
|
359
|
+
}
|
|
360
|
+
const cleanup = async () => {
|
|
361
|
+
await Mikrotik.cache.del(target);
|
|
362
|
+
};
|
|
363
|
+
await Mikrotik.cache.set(target, target, options.duration); // set our mutex
|
|
364
|
+
let command = `/tool bandwidth-test protocol=${options.protocol} ` +
|
|
365
|
+
`user=${username} password=${password} ` +
|
|
366
|
+
`duration=${options.duration}s direction=${options.direction} ` +
|
|
367
|
+
`address=${target} random-data=${options.random_data ? 'yes' : 'no'} interval=1s`;
|
|
368
|
+
if (options.local_tx_speed) {
|
|
369
|
+
command += ` local-tx-speed=${options.local_tx_speed}M`;
|
|
370
|
+
}
|
|
371
|
+
if (options.remote_tx_speed) {
|
|
372
|
+
command += ` remote-tx-speed=${options.remote_tx_speed}M`;
|
|
373
|
+
}
|
|
374
|
+
const stream = await this.stream(command, { separator: '\r\n\r\n' });
|
|
375
|
+
options.signal?.addEventListener('abort', () => {
|
|
376
|
+
stream.abort();
|
|
377
|
+
});
|
|
378
|
+
stream.on('data', handleStream);
|
|
379
|
+
stream.on('completed', async () => {
|
|
380
|
+
await cleanup();
|
|
381
|
+
});
|
|
382
|
+
stream.on('cancelled', async () => {
|
|
383
|
+
await cleanup();
|
|
384
|
+
return reject(new Error('Bandwidth Test Cancelled'));
|
|
385
|
+
});
|
|
386
|
+
if (options.timeout) {
|
|
387
|
+
setTimeout(() => {
|
|
395
388
|
stream.abort();
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
stream.on('completed', () => __awaiter(this, void 0, void 0, function* () {
|
|
399
|
-
yield cleanup();
|
|
400
|
-
}));
|
|
401
|
-
stream.on('cancelled', () => __awaiter(this, void 0, void 0, function* () {
|
|
402
|
-
yield cleanup();
|
|
403
|
-
return reject(new Error('Bandwidth Test Cancelled'));
|
|
404
|
-
}));
|
|
405
|
-
if (options.timeout) {
|
|
406
|
-
setTimeout(() => {
|
|
407
|
-
stream.abort();
|
|
408
|
-
}, options.timeout);
|
|
409
|
-
}
|
|
410
|
-
}));
|
|
389
|
+
}, options.timeout);
|
|
390
|
+
}
|
|
411
391
|
});
|
|
412
392
|
}
|
|
413
393
|
/**
|
|
@@ -416,34 +396,32 @@ class Mikrotik extends ssh_1.default {
|
|
|
416
396
|
* @param target
|
|
417
397
|
* @param source
|
|
418
398
|
*/
|
|
419
|
-
ping(target, source) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
result.latency = parseInt(parts[4]) || 2000;
|
|
440
|
-
}
|
|
441
|
-
return result;
|
|
442
|
-
}
|
|
443
|
-
catch (_a) {
|
|
444
|
-
return result;
|
|
399
|
+
async ping(target, source) {
|
|
400
|
+
const result = {
|
|
401
|
+
target,
|
|
402
|
+
latency: 2000
|
|
403
|
+
};
|
|
404
|
+
if (source)
|
|
405
|
+
result.source = source;
|
|
406
|
+
const command = `/ping address=${target} count=1${source ? ` src-address=${source}` : ''}`;
|
|
407
|
+
try {
|
|
408
|
+
const response = (await this.exec(command))
|
|
409
|
+
.toString()
|
|
410
|
+
.trim()
|
|
411
|
+
.split('\n')
|
|
412
|
+
.map(line => line.trim());
|
|
413
|
+
for (const line of response) {
|
|
414
|
+
const parts = line.split(/\s+/)
|
|
415
|
+
.map(part => part.trim());
|
|
416
|
+
if (isNaN(parseInt(parts[0])))
|
|
417
|
+
continue;
|
|
418
|
+
result.latency = parseInt(parts[4]) || 2000;
|
|
445
419
|
}
|
|
446
|
-
|
|
420
|
+
return result;
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
447
425
|
}
|
|
448
426
|
/**
|
|
449
427
|
* Performs a traceroute to the target IP address from the source IP address (if supplied)
|
|
@@ -451,229 +429,213 @@ class Mikrotik extends ssh_1.default {
|
|
|
451
429
|
* @param target
|
|
452
430
|
* @param source
|
|
453
431
|
*/
|
|
454
|
-
traceroute(target, source) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
(0, dns_1.reverse)(ip, (error, addresses) => {
|
|
475
|
-
if (error)
|
|
476
|
-
return resolve([ip, undefined]);
|
|
477
|
-
return resolve([ip, addresses.shift()]);
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
catch (_a) {
|
|
432
|
+
async traceroute(target, source) {
|
|
433
|
+
const command = `/tool traceroute address=${target} count=1${source ? ` src-address=${source}` : ''}`;
|
|
434
|
+
const response = (await this.exec(command))
|
|
435
|
+
.toString()
|
|
436
|
+
.split('\r\n\r\n')
|
|
437
|
+
.map(frame => frame.trim())
|
|
438
|
+
.filter(frame => frame.length !== 0)
|
|
439
|
+
.map(frame => frame.split('\r\n')
|
|
440
|
+
.map(line => line.trim())
|
|
441
|
+
.map(line => line.split(/\s+/)
|
|
442
|
+
.map(col => col.trim()))
|
|
443
|
+
.filter(col => !isNaN(parseInt(col[0]))))
|
|
444
|
+
.pop();
|
|
445
|
+
if (!response)
|
|
446
|
+
throw new Error(`Could not perform traceroute to ${target}`);
|
|
447
|
+
const results = [];
|
|
448
|
+
const resolveDNS = async (ip) => new Promise(resolve => {
|
|
449
|
+
try {
|
|
450
|
+
(0, dns_1.reverse)(ip, (error, addresses) => {
|
|
451
|
+
if (error)
|
|
481
452
|
return resolve([ip, undefined]);
|
|
482
|
-
|
|
453
|
+
return resolve([ip, addresses.shift()]);
|
|
483
454
|
});
|
|
484
|
-
});
|
|
485
|
-
for (const [hop, address, loss, sent, last, avg, best, worst] of response) {
|
|
486
|
-
const result = {
|
|
487
|
-
hop: parseInt(hop),
|
|
488
|
-
loss: (sent === 'timeout' ? parseInt(address) : parseInt(loss)) / 100,
|
|
489
|
-
sent: sent === 'timeout' ? parseInt(loss) : parseInt(sent),
|
|
490
|
-
last: sent === 'timeout' ? 2000 : parseInt(last),
|
|
491
|
-
average: sent === 'timeout' ? 2000 : parseInt(avg),
|
|
492
|
-
best: sent === 'timeout' ? 2000 : parseInt(best),
|
|
493
|
-
worst: sent === 'timeout' ? 2000 : parseInt(worst),
|
|
494
|
-
timeout: sent === 'timeout'
|
|
495
|
-
};
|
|
496
|
-
if (sent !== 'timeout')
|
|
497
|
-
result.address = address;
|
|
498
|
-
results.push(result);
|
|
499
455
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
456
|
+
catch {
|
|
457
|
+
return resolve([ip, undefined]);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
for (const [hop, address, loss, sent, last, avg, best, worst] of response) {
|
|
461
|
+
const result = {
|
|
462
|
+
hop: parseInt(hop),
|
|
463
|
+
loss: (sent === 'timeout' ? parseInt(address) : parseInt(loss)) / 100,
|
|
464
|
+
sent: sent === 'timeout' ? parseInt(loss) : parseInt(sent),
|
|
465
|
+
last: sent === 'timeout' ? 2000 : parseInt(last),
|
|
466
|
+
average: sent === 'timeout' ? 2000 : parseInt(avg),
|
|
467
|
+
best: sent === 'timeout' ? 2000 : parseInt(best),
|
|
468
|
+
worst: sent === 'timeout' ? 2000 : parseInt(worst),
|
|
469
|
+
timeout: sent === 'timeout'
|
|
470
|
+
};
|
|
471
|
+
if (sent !== 'timeout')
|
|
472
|
+
result.address = address;
|
|
473
|
+
results.push(result);
|
|
474
|
+
}
|
|
475
|
+
(await Promise.all(results.filter(result => result.address)
|
|
476
|
+
.map(result => resolveDNS(result.address || ''))))
|
|
477
|
+
.forEach(result => {
|
|
478
|
+
for (let i = 0; i < results.length; i++) {
|
|
479
|
+
if (result[0] === results[i].address && result[1]) {
|
|
480
|
+
results[i].hostname = result[1];
|
|
507
481
|
}
|
|
508
|
-
}
|
|
509
|
-
return results;
|
|
482
|
+
}
|
|
510
483
|
});
|
|
484
|
+
return results;
|
|
511
485
|
}
|
|
512
486
|
/**
|
|
513
487
|
* Fetches routerboard information from the device
|
|
514
488
|
*/
|
|
515
|
-
routerboard() {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
};
|
|
528
|
-
});
|
|
489
|
+
async routerboard() {
|
|
490
|
+
const result = await this.kvs('/system routerboard print');
|
|
491
|
+
return {
|
|
492
|
+
routerboard: result.routerboard === 'yes',
|
|
493
|
+
board_name: result['board-name'],
|
|
494
|
+
model: result.model,
|
|
495
|
+
serial_number: result['serial-number'],
|
|
496
|
+
firmware_type: result['firmware-type'],
|
|
497
|
+
factory_firmware: toVersion(result['factory-firmware']),
|
|
498
|
+
current_firmware: toVersion(result['current-firmware']),
|
|
499
|
+
upgrade_firmware: toVersion(result['upgrade-firmware'])
|
|
500
|
+
};
|
|
529
501
|
}
|
|
530
502
|
/**
|
|
531
503
|
* Fetches the identity of the device
|
|
532
504
|
*/
|
|
533
|
-
identity() {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
return response.name;
|
|
537
|
-
});
|
|
505
|
+
async identity() {
|
|
506
|
+
const response = await this.kvs('/system identity print');
|
|
507
|
+
return response.name;
|
|
538
508
|
}
|
|
539
509
|
/**
|
|
540
510
|
* Fetches the resource information of the device
|
|
541
511
|
*/
|
|
542
|
-
resource() {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
512
|
+
async resource() {
|
|
513
|
+
const response = await this.kvs('/system resource print');
|
|
514
|
+
const result = {
|
|
515
|
+
uptime: response.uptime,
|
|
516
|
+
version: toVersion(response.version),
|
|
517
|
+
build_time: new Date(`${response['build-time']}Z`),
|
|
518
|
+
factory_sofware: response['factory-software'],
|
|
519
|
+
free_memory: response['free-memory'],
|
|
520
|
+
total_memory: response['total-memory'],
|
|
521
|
+
cpu: response.cpu,
|
|
522
|
+
cpu_count: parseInt(response['cpu-count']) || 0,
|
|
523
|
+
cpu_frequency: parseInt(response['cpu-frequency']) || 0,
|
|
524
|
+
cpu_load: (parseInt(response['cpu-load']) || 0) / 100,
|
|
525
|
+
hdd_space: {
|
|
526
|
+
free: response['free-hdd-space'],
|
|
527
|
+
total: response['total-hdd-space']
|
|
528
|
+
},
|
|
529
|
+
architecture_name: response['architecture-name'],
|
|
530
|
+
board_name: response['board-name'],
|
|
531
|
+
platform: response.platform
|
|
532
|
+
};
|
|
533
|
+
if (response.version.includes('long-term')) {
|
|
534
|
+
result.lts = true;
|
|
535
|
+
}
|
|
536
|
+
else if (response.version.includes('stable')) {
|
|
537
|
+
result.stable = true;
|
|
538
|
+
}
|
|
539
|
+
else if (response.version.includes('testing')) {
|
|
540
|
+
result.testing = true;
|
|
541
|
+
}
|
|
542
|
+
else if (response.version.includes('development')) {
|
|
543
|
+
result.development = true;
|
|
544
|
+
}
|
|
545
|
+
if (response['write-sect-since-reboot'] && response['write-sect-total'] && response['bad-blocks']) {
|
|
546
|
+
result.nvram = {
|
|
547
|
+
write_since_reboot: parseInt(response['write-sect-since-reboot']) || 0,
|
|
548
|
+
write_total: parseInt(response['write-sect-total']) || 0,
|
|
549
|
+
bad_blocks: parseInt(response['bad-blocks']) || 0
|
|
563
550
|
};
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
else if (response.version.includes('stable')) {
|
|
568
|
-
result.stable = true;
|
|
569
|
-
}
|
|
570
|
-
else if (response.version.includes('testing')) {
|
|
571
|
-
result.testing = true;
|
|
572
|
-
}
|
|
573
|
-
else if (response.version.includes('development')) {
|
|
574
|
-
result.development = true;
|
|
575
|
-
}
|
|
576
|
-
if (response['write-sect-since-reboot'] && response['write-sect-total'] && response['bad-blocks']) {
|
|
577
|
-
result.nvram = {
|
|
578
|
-
write_since_reboot: parseInt(response['write-sect-since-reboot']) || 0,
|
|
579
|
-
write_total: parseInt(response['write-sect-total']) || 0,
|
|
580
|
-
bad_blocks: parseInt(response['bad-blocks']) || 0
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
return result;
|
|
584
|
-
});
|
|
551
|
+
}
|
|
552
|
+
return result;
|
|
585
553
|
}
|
|
586
554
|
/**
|
|
587
555
|
* Fetches the current RouterOS version
|
|
588
556
|
*/
|
|
589
|
-
version() {
|
|
590
|
-
return
|
|
591
|
-
return (yield this.resource()).version;
|
|
592
|
-
});
|
|
557
|
+
async version() {
|
|
558
|
+
return (await this.resource()).version;
|
|
593
559
|
}
|
|
594
560
|
/**
|
|
595
561
|
* Fetches the semantic RouterOS version
|
|
596
562
|
*/
|
|
597
|
-
semantic_version() {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
};
|
|
607
|
-
});
|
|
563
|
+
async semantic_version() {
|
|
564
|
+
const version = await this.version();
|
|
565
|
+
const [major, minor, patch] = version.split('.')
|
|
566
|
+
.map(elem => parseInt(elem) || 0);
|
|
567
|
+
return {
|
|
568
|
+
major,
|
|
569
|
+
minor,
|
|
570
|
+
patch
|
|
571
|
+
};
|
|
608
572
|
}
|
|
609
573
|
/**
|
|
610
574
|
* Fetches the current health information
|
|
611
575
|
*/
|
|
612
|
-
health() {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
result.psu_voltage = parseFloat(response['psu-voltage']);
|
|
624
|
-
}
|
|
625
|
-
if (response['psu1-voltage']) {
|
|
626
|
-
result.psu1_voltage = parseFloat(response['psu1-voltage']);
|
|
627
|
-
}
|
|
628
|
-
if (response['psu2-voltage']) {
|
|
629
|
-
result.psu2_voltage = parseFloat(response['psu2-voltage']);
|
|
630
|
-
}
|
|
631
|
-
return result;
|
|
576
|
+
async health() {
|
|
577
|
+
const { major } = await this.semantic_version();
|
|
578
|
+
if (major === 6) {
|
|
579
|
+
const result = {};
|
|
580
|
+
const response = await this.kvs('/system health print');
|
|
581
|
+
result.voltage = parseFloat(response.voltage) || 0;
|
|
582
|
+
result.current = parseFloat(response.current) || 0;
|
|
583
|
+
result.temperature = parseFloat(response.temperature) || 0;
|
|
584
|
+
result.power_consumption = parseFloat(response['power-consumption']) || 0;
|
|
585
|
+
if (response['psu-voltage']) {
|
|
586
|
+
result.psu_voltage = parseFloat(response['psu-voltage']);
|
|
632
587
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const response = yield this.terse('/system health print terse');
|
|
636
|
-
for (const { name, value } of response) {
|
|
637
|
-
const num = parseFloat(value) || 0;
|
|
638
|
-
switch (name) {
|
|
639
|
-
case 'cpu-temperature':
|
|
640
|
-
result.cpu_temperature = num;
|
|
641
|
-
break;
|
|
642
|
-
case 'sfp-temperature':
|
|
643
|
-
result.sfp_temperature = num;
|
|
644
|
-
break;
|
|
645
|
-
case 'switch-temperature':
|
|
646
|
-
result.switch_temperature = num;
|
|
647
|
-
break;
|
|
648
|
-
case 'fan-state':
|
|
649
|
-
result.fan_state = value;
|
|
650
|
-
break;
|
|
651
|
-
case 'fan1-speed':
|
|
652
|
-
result.fan1_speed = num;
|
|
653
|
-
break;
|
|
654
|
-
case 'fan2-speed':
|
|
655
|
-
result.fan2_speed = num;
|
|
656
|
-
break;
|
|
657
|
-
case 'board-temperature1':
|
|
658
|
-
result.board_temperature1 = num;
|
|
659
|
-
break;
|
|
660
|
-
case 'board-temperature2':
|
|
661
|
-
result.board_temperature2 = num;
|
|
662
|
-
break;
|
|
663
|
-
case 'psu1-state':
|
|
664
|
-
result.psu1_state = value;
|
|
665
|
-
break;
|
|
666
|
-
case 'psu2-state':
|
|
667
|
-
result.psu2_state = value;
|
|
668
|
-
break;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
return result;
|
|
588
|
+
if (response['psu1-voltage']) {
|
|
589
|
+
result.psu1_voltage = parseFloat(response['psu1-voltage']);
|
|
672
590
|
}
|
|
673
|
-
|
|
674
|
-
|
|
591
|
+
if (response['psu2-voltage']) {
|
|
592
|
+
result.psu2_voltage = parseFloat(response['psu2-voltage']);
|
|
675
593
|
}
|
|
676
|
-
|
|
594
|
+
return result;
|
|
595
|
+
}
|
|
596
|
+
else if (major === 7) {
|
|
597
|
+
const result = {};
|
|
598
|
+
const response = await this.terse('/system health print terse');
|
|
599
|
+
for (const { name, value } of response) {
|
|
600
|
+
const num = parseFloat(value) || 0;
|
|
601
|
+
switch (name) {
|
|
602
|
+
case 'cpu-temperature':
|
|
603
|
+
result.cpu_temperature = num;
|
|
604
|
+
break;
|
|
605
|
+
case 'sfp-temperature':
|
|
606
|
+
result.sfp_temperature = num;
|
|
607
|
+
break;
|
|
608
|
+
case 'switch-temperature':
|
|
609
|
+
result.switch_temperature = num;
|
|
610
|
+
break;
|
|
611
|
+
case 'fan-state':
|
|
612
|
+
result.fan_state = value;
|
|
613
|
+
break;
|
|
614
|
+
case 'fan1-speed':
|
|
615
|
+
result.fan1_speed = num;
|
|
616
|
+
break;
|
|
617
|
+
case 'fan2-speed':
|
|
618
|
+
result.fan2_speed = num;
|
|
619
|
+
break;
|
|
620
|
+
case 'board-temperature1':
|
|
621
|
+
result.board_temperature1 = num;
|
|
622
|
+
break;
|
|
623
|
+
case 'board-temperature2':
|
|
624
|
+
result.board_temperature2 = num;
|
|
625
|
+
break;
|
|
626
|
+
case 'psu1-state':
|
|
627
|
+
result.psu1_state = value;
|
|
628
|
+
break;
|
|
629
|
+
case 'psu2-state':
|
|
630
|
+
result.psu2_state = value;
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return result;
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
throw new Error('RouterOS version is not supported for this command');
|
|
638
|
+
}
|
|
677
639
|
}
|
|
678
640
|
/**
|
|
679
641
|
* Executes a command with expected terse response and parses it accordingly
|
|
@@ -681,70 +643,68 @@ class Mikrotik extends ssh_1.default {
|
|
|
681
643
|
* @param command
|
|
682
644
|
* @protected
|
|
683
645
|
*/
|
|
684
|
-
terse(command) {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
646
|
+
async terse(command) {
|
|
647
|
+
const results = [];
|
|
648
|
+
(await this.exec(command)).toString()
|
|
649
|
+
.split('\r\n')
|
|
650
|
+
.map(line => line.trim())
|
|
651
|
+
.filter(line => line.split(/\s+/).length !== 0 && line.length !== 0)
|
|
652
|
+
.forEach(line => {
|
|
653
|
+
const result = (() => {
|
|
654
|
+
const [, flags] = line.split(/\s+/).map(part => part.trim());
|
|
655
|
+
const result = {};
|
|
656
|
+
// get the id or the number
|
|
657
|
+
if (line.startsWith('*')) {
|
|
658
|
+
result.id = line.split(/\s/).shift();
|
|
659
|
+
}
|
|
660
|
+
else if (!isNaN(parseInt(line))) {
|
|
661
|
+
result.idx = parseInt(line);
|
|
662
|
+
}
|
|
663
|
+
result.flags = flags.includes('=') ? '' : flags;
|
|
664
|
+
return result;
|
|
665
|
+
})();
|
|
666
|
+
/**
|
|
667
|
+
* This looks like a mess; however, RouterOS will return values with spaced
|
|
668
|
+
* which causes havoc if a simple regex matcher is applied to the key-value pairs
|
|
669
|
+
*/
|
|
670
|
+
do {
|
|
671
|
+
const middle = line.indexOf('=') + 1;
|
|
672
|
+
const start = (() => {
|
|
673
|
+
const start = line.substring(0, middle).lastIndexOf(' ');
|
|
674
|
+
return start === -1 ? 0 : start;
|
|
704
675
|
})();
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
if (end > 0) {
|
|
737
|
-
line = line.substring(end);
|
|
738
|
-
}
|
|
739
|
-
else {
|
|
740
|
-
line = '';
|
|
741
|
-
}
|
|
742
|
-
} while (line.length !== 0);
|
|
743
|
-
if (Object.keys(result).length !== 0)
|
|
744
|
-
results.push(result);
|
|
745
|
-
});
|
|
746
|
-
return results;
|
|
676
|
+
const next = (() => {
|
|
677
|
+
const next = line.substring(middle).indexOf('=');
|
|
678
|
+
return next === -1 ? -1 : next + middle;
|
|
679
|
+
})();
|
|
680
|
+
const end = (() => {
|
|
681
|
+
if (next === -1)
|
|
682
|
+
return -1;
|
|
683
|
+
const end = line.substring(middle, next).lastIndexOf(' ');
|
|
684
|
+
return end === -1 ? -1 : end + middle;
|
|
685
|
+
})();
|
|
686
|
+
const [key, value] = line.substring(start, end !== -1 ? end : undefined)
|
|
687
|
+
.trim().split(/=/);
|
|
688
|
+
if (parseFloat(value).toString() === value) {
|
|
689
|
+
result[key] = parseFloat(value);
|
|
690
|
+
}
|
|
691
|
+
else if (parseInt(value).toString() === value) {
|
|
692
|
+
result[key] = parseInt(value);
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
result[key] = value;
|
|
696
|
+
}
|
|
697
|
+
if (end > 0) {
|
|
698
|
+
line = line.substring(end);
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
line = '';
|
|
702
|
+
}
|
|
703
|
+
} while (line.length !== 0);
|
|
704
|
+
if (Object.keys(result).length !== 0)
|
|
705
|
+
results.push(result);
|
|
747
706
|
});
|
|
707
|
+
return results;
|
|
748
708
|
}
|
|
749
709
|
/**
|
|
750
710
|
* Executes a command that expects the result as a 'table' of key-value pairs separated by a colon (:)
|
|
@@ -752,28 +712,29 @@ class Mikrotik extends ssh_1.default {
|
|
|
752
712
|
* @param command
|
|
753
713
|
* @protected
|
|
754
714
|
*/
|
|
755
|
-
kvs(command) {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
715
|
+
async kvs(command) {
|
|
716
|
+
const result = {};
|
|
717
|
+
const lines = (await this.exec(command))
|
|
718
|
+
.toString()
|
|
719
|
+
.split('\r\n')
|
|
720
|
+
.map(line => line.trim())
|
|
721
|
+
.filter(line => line.length !== 0)
|
|
722
|
+
.map(line => line.split(':')
|
|
723
|
+
.map(col => col.trim()));
|
|
724
|
+
for (const [key, ...value] of lines) {
|
|
725
|
+
result[key] = value.join(':');
|
|
726
|
+
}
|
|
727
|
+
return result;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Destroys the SSH connection and stops the internal cache timer
|
|
731
|
+
*/
|
|
732
|
+
async destroy() {
|
|
733
|
+
await super.destroy();
|
|
734
|
+
await Mikrotik.cache.disconnect();
|
|
770
735
|
}
|
|
771
736
|
}
|
|
772
737
|
exports.Mikrotik = Mikrotik;
|
|
773
|
-
Mikrotik.cache = new memory_1.default({
|
|
774
|
-
stdTTL: 15,
|
|
775
|
-
checkperiod: 17
|
|
776
|
-
});
|
|
777
738
|
(function (Mikrotik) {
|
|
778
739
|
let BandwidthTest;
|
|
779
740
|
(function (BandwidthTest) {
|
|
@@ -781,6 +742,8 @@ Mikrotik.cache = new memory_1.default({
|
|
|
781
742
|
* Class that helps with processing a "frame" of the Bandwidth Test response
|
|
782
743
|
*/
|
|
783
744
|
class Frame {
|
|
745
|
+
buffer;
|
|
746
|
+
parsed;
|
|
784
747
|
constructor(buffer) {
|
|
785
748
|
this.buffer = buffer;
|
|
786
749
|
}
|