feat: add Syslog configuration to Network Services (v0.10.8 Beta)
New Syslog page under Network Services exposes RouterOS logging actions and routing rules with full CRUD. Logging actions define destinations (remote syslog server, memory, disk, echo); logging rules define which topics route to which action. Single Device and All Devices modes are functionally identical — All Devices applies changes across every online device in parallel and shows per-entry coverage badges indicating which devices have each action or rule. Toggle enable/disable works in both modes. Remote actions display address:port with facility/severity in a single Destination column; memory/disk/echo rows show no irrelevant remote fields. Syslog KPI card added to the Network Services overview.
This commit is contained in:
@@ -44,19 +44,21 @@ router.get('/overview', async (_req: Request, res: Response) => {
|
||||
const collector = new DeviceCollector(device);
|
||||
try {
|
||||
await collector.connect();
|
||||
const [dhcpV4, dhcpV6, dns, ntp, wg] = await Promise.allSettled([
|
||||
const [dhcpV4, dhcpV6, dns, ntp, wg, syslogActions] = await Promise.allSettled([
|
||||
collector.getDhcpServers('ipv4'),
|
||||
collector.getDhcpServers('ipv6'),
|
||||
collector.getDnsSettings(),
|
||||
collector.getNtpSettings(),
|
||||
collector.getWireGuardInterfaces(),
|
||||
collector.getSyslogActions(),
|
||||
]);
|
||||
|
||||
const v4Servers = dhcpV4.status === 'fulfilled' ? dhcpV4.value : [];
|
||||
const v6Servers = dhcpV6.status === 'fulfilled' ? dhcpV6.value : [];
|
||||
const dnsRow = dns.status === 'fulfilled' ? dns.value : null;
|
||||
const ntpRow = ntp.status === 'fulfilled' ? ntp.value : null;
|
||||
const wgIfaces = wg.status === 'fulfilled' ? wg.value : [];
|
||||
const v4Servers = dhcpV4.status === 'fulfilled' ? dhcpV4.value : [];
|
||||
const v6Servers = dhcpV6.status === 'fulfilled' ? dhcpV6.value : [];
|
||||
const dnsRow = dns.status === 'fulfilled' ? dns.value : null;
|
||||
const ntpRow = ntp.status === 'fulfilled' ? ntp.value : null;
|
||||
const wgIfaces = wg.status === 'fulfilled' ? wg.value : [];
|
||||
const syslogActs = syslogActions.status === 'fulfilled' ? syslogActions.value : [];
|
||||
|
||||
return {
|
||||
id: device.id, name: device.name, ip_address: device.ip_address,
|
||||
@@ -65,10 +67,11 @@ router.get('/overview', async (_req: Request, res: Response) => {
|
||||
dns: dnsRow ? { allow_remote: dnsRow['allow-remote-requests'] === 'yes', servers: dnsRow['servers'] || '' } : null,
|
||||
ntp: ntpRow ? { server_enabled: ntpRow.server['enabled'] === 'yes', client_enabled: ntpRow.client['enabled'] === 'yes' } : null,
|
||||
wireguard: { total: wgIfaces.length, running: wgIfaces.filter(i => i['running'] === 'true').length },
|
||||
syslog: { remote_count: syslogActs.filter(a => a['type'] === 'remote').length },
|
||||
};
|
||||
} catch (err) {
|
||||
return { id: device.id, name: device.name, ip_address: device.ip_address,
|
||||
dhcp_v4: null, dhcp_v6: null, dns: null, ntp: null, wireguard: null,
|
||||
dhcp_v4: null, dhcp_v6: null, dns: null, ntp: null, wireguard: null, syslog: null,
|
||||
error: (err as Error).message };
|
||||
} finally {
|
||||
collector.disconnect();
|
||||
@@ -398,4 +401,91 @@ router.delete('/wireguard/peer/:id', requireWrite, async (req: Request, res: Res
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Syslog ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// GET /api/network-services/syslog?deviceId=X
|
||||
router.get('/syslog', async (req: Request, res: Response) => {
|
||||
const deviceId = deviceIdParam(req, res); if (!deviceId) return;
|
||||
await withDevice(deviceId, res, async (collector) => {
|
||||
const [actions, rules] = await Promise.allSettled([
|
||||
collector.getSyslogActions(),
|
||||
collector.getSyslogRules(),
|
||||
]);
|
||||
res.json({
|
||||
actions: actions.status === 'fulfilled' ? actions.value : [],
|
||||
rules: rules.status === 'fulfilled' ? rules.value : [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// POST /api/network-services/syslog/action?deviceId=X
|
||||
router.post('/syslog/action', requireWrite, async (req: Request, res: Response) => {
|
||||
const deviceId = deviceIdParam(req, res); if (!deviceId) return;
|
||||
await withDevice(deviceId, res, async (collector) => {
|
||||
await collector.addSyslogAction(req.body);
|
||||
const updated = await collector.getSyslogActions();
|
||||
res.json(updated);
|
||||
});
|
||||
});
|
||||
|
||||
// PUT /api/network-services/syslog/action/:id?deviceId=X
|
||||
router.put('/syslog/action/:id', requireWrite, async (req: Request, res: Response) => {
|
||||
const deviceId = deviceIdParam(req, res); if (!deviceId) return;
|
||||
await withDevice(deviceId, res, async (collector) => {
|
||||
await collector.updateSyslogAction(req.params.id, req.body);
|
||||
const updated = await collector.getSyslogActions();
|
||||
res.json(updated);
|
||||
});
|
||||
});
|
||||
|
||||
// DELETE /api/network-services/syslog/action/:id?deviceId=X
|
||||
router.delete('/syslog/action/:id', requireWrite, async (req: Request, res: Response) => {
|
||||
const deviceId = deviceIdParam(req, res); if (!deviceId) return;
|
||||
await withDevice(deviceId, res, async (collector) => {
|
||||
await collector.removeSyslogAction(req.params.id);
|
||||
res.json({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
// PUT /api/network-services/syslog/rule/toggle?deviceId=X — must be before /:id
|
||||
router.put('/syslog/rule/toggle', requireWrite, async (req: Request, res: Response) => {
|
||||
const deviceId = deviceIdParam(req, res); if (!deviceId) return;
|
||||
const { ruleId, disabled } = req.body;
|
||||
if (!ruleId) return res.status(400).json({ error: 'ruleId is required' });
|
||||
if (typeof disabled !== 'boolean') return res.status(400).json({ error: 'disabled (boolean) is required' });
|
||||
await withDevice(deviceId, res, async (collector) => {
|
||||
await collector.toggleSyslogRule(ruleId, disabled);
|
||||
res.json({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
// POST /api/network-services/syslog/rule?deviceId=X
|
||||
router.post('/syslog/rule', requireWrite, async (req: Request, res: Response) => {
|
||||
const deviceId = deviceIdParam(req, res); if (!deviceId) return;
|
||||
await withDevice(deviceId, res, async (collector) => {
|
||||
await collector.addSyslogRule(req.body);
|
||||
const updated = await collector.getSyslogRules();
|
||||
res.json(updated);
|
||||
});
|
||||
});
|
||||
|
||||
// PUT /api/network-services/syslog/rule/:id?deviceId=X
|
||||
router.put('/syslog/rule/:id', requireWrite, async (req: Request, res: Response) => {
|
||||
const deviceId = deviceIdParam(req, res); if (!deviceId) return;
|
||||
await withDevice(deviceId, res, async (collector) => {
|
||||
await collector.updateSyslogRule(req.params.id, req.body);
|
||||
const updated = await collector.getSyslogRules();
|
||||
res.json(updated);
|
||||
});
|
||||
});
|
||||
|
||||
// DELETE /api/network-services/syslog/rule/:id?deviceId=X
|
||||
router.delete('/syslog/rule/:id', requireWrite, async (req: Request, res: Response) => {
|
||||
const deviceId = deviceIdParam(req, res); if (!deviceId) return;
|
||||
await withDevice(deviceId, res, async (collector) => {
|
||||
await collector.removeSyslogRule(req.params.id);
|
||||
res.json({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -2476,6 +2476,44 @@ export class DeviceCollector {
|
||||
}
|
||||
}
|
||||
|
||||
// Syslog (logging actions + rules) ───────────────────────────────────────────
|
||||
|
||||
async getSyslogActions(): Promise<Record<string, string>[]> {
|
||||
return this.client.execute('/system/logging/action/print', { detail: '' }).catch(() => [] as Record<string, string>[]);
|
||||
}
|
||||
|
||||
async addSyslogAction(params: Record<string, string>): Promise<void> {
|
||||
await this.client.execute('/system/logging/action/add', params);
|
||||
}
|
||||
|
||||
async updateSyslogAction(id: string, params: Record<string, string>): Promise<void> {
|
||||
await this.client.execute('/system/logging/action/set', { '.id': id, ...params });
|
||||
}
|
||||
|
||||
async removeSyslogAction(id: string): Promise<void> {
|
||||
await this.client.execute('/system/logging/action/remove', { '.id': id });
|
||||
}
|
||||
|
||||
async getSyslogRules(): Promise<Record<string, string>[]> {
|
||||
return this.client.execute('/system/logging/print', { detail: '' }).catch(() => [] as Record<string, string>[]);
|
||||
}
|
||||
|
||||
async addSyslogRule(params: Record<string, string>): Promise<void> {
|
||||
await this.client.execute('/system/logging/add', params);
|
||||
}
|
||||
|
||||
async updateSyslogRule(id: string, params: Record<string, string>): Promise<void> {
|
||||
await this.client.execute('/system/logging/set', { '.id': id, ...params });
|
||||
}
|
||||
|
||||
async removeSyslogRule(id: string): Promise<void> {
|
||||
await this.client.execute('/system/logging/remove', { '.id': id });
|
||||
}
|
||||
|
||||
async toggleSyslogRule(id: string, disabled: boolean): Promise<void> {
|
||||
await this.client.execute('/system/logging/set', { '.id': id, 'disabled': disabled ? 'yes' : 'no' });
|
||||
}
|
||||
|
||||
// WireGuard ───────────────────────────────────────────────────────────────────
|
||||
|
||||
async getWireGuardInterfaces(): Promise<Record<string, string>[]> {
|
||||
|
||||
Reference in New Issue
Block a user