добавил DNSMASQ
This commit is contained in:
parent
a23b915f61
commit
c36d96677e
|
|
@ -2,6 +2,7 @@ package ru.kirillius.pf.sdn;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
import ru.kirillius.pf.sdn.External.API.Components.DNSMASQ;
|
||||||
import ru.kirillius.pf.sdn.External.API.Components.FRR;
|
import ru.kirillius.pf.sdn.External.API.Components.FRR;
|
||||||
import ru.kirillius.pf.sdn.External.API.Components.OVPN;
|
import ru.kirillius.pf.sdn.External.API.Components.OVPN;
|
||||||
import ru.kirillius.pf.sdn.External.API.Components.ROS;
|
import ru.kirillius.pf.sdn.External.API.Components.ROS;
|
||||||
|
|
@ -160,7 +161,7 @@ public class App implements Context, Closeable {
|
||||||
.configFile(new File(getArgument("c", args)))
|
.configFile(new File(getArgument("c", args)))
|
||||||
.appLibrary(new File(getArgument("l", args)))
|
.appLibrary(new File(getArgument("l", args)))
|
||||||
.repository(getArgument("r", args))
|
.repository(getArgument("r", args))
|
||||||
.availableComponentClasses(List.of(FRR.class, OVPN.class, TDNS.class, ROS.class)).build())) {
|
.availableComponentClasses(List.of(FRR.class, OVPN.class, TDNS.class, ROS.class, DNSMASQ.class)).build())) {
|
||||||
Wait.when(app.running::get);
|
Wait.when(app.running::get);
|
||||||
if (app.shouldRestart.get()) {
|
if (app.shouldRestart.get()) {
|
||||||
System.exit(42);
|
System.exit(42);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
package ru.kirillius.pf.sdn.External.API.Components;
|
||||||
|
|
||||||
|
import lombok.*;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.json.JSONTokener;
|
||||||
|
import ru.kirillius.java.utils.events.EventListener;
|
||||||
|
import ru.kirillius.json.JSONArrayProperty;
|
||||||
|
import ru.kirillius.json.JSONProperty;
|
||||||
|
import ru.kirillius.json.JSONSerializable;
|
||||||
|
import ru.kirillius.pf.sdn.core.AbstractComponent;
|
||||||
|
import ru.kirillius.pf.sdn.core.Context;
|
||||||
|
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||||
|
import ru.kirillius.utils.logging.SystemLogger;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that synchronises FRR routing instances with the aggregated subnet list.
|
||||||
|
*/
|
||||||
|
public final class DNSMASQ extends AbstractComponent<DNSMASQ.DNSMASQConfig> {
|
||||||
|
|
||||||
|
private final static String CTX = DNSMASQ.class.getSimpleName();
|
||||||
|
private final EventListener<NetworkResourceBundle> subscription;
|
||||||
|
private final HttpClient client;
|
||||||
|
|
||||||
|
public DNSMASQ(Context context) {
|
||||||
|
super(context);
|
||||||
|
subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateDomains(bundle.getDomains()));
|
||||||
|
client = HttpClient.newHttpClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final static String FILENAME = "/etc/dnsmasq.conf";
|
||||||
|
|
||||||
|
private String parseResponseLine(String line) {
|
||||||
|
if (!line.startsWith("data: ")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var json = new JSONObject(new JSONTokener(line.substring(6)));
|
||||||
|
if (json.has("body")) {
|
||||||
|
return json.getJSONObject("body").getJSONObject("Files").getString(FILENAME);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpRequest.Builder createRequest(DNSMASQConfig.Entry entry, String endpoint) {
|
||||||
|
var auth = entry.user + ":" + entry.password;
|
||||||
|
var encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes());
|
||||||
|
var url = entry.host + endpoint;
|
||||||
|
|
||||||
|
return HttpRequest.newBuilder().uri(URI.create(url)).header("Authorization", "Basic " + encodedAuth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveConfig(DNSMASQConfig.Entry entry, String config) {
|
||||||
|
var json = new JSONObject();
|
||||||
|
json.put(FILENAME, config);
|
||||||
|
var request = createRequest(entry, "/save")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(json.toString()))
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
var response = client.send(request, HttpResponse.BodyHandlers.discarding());
|
||||||
|
if (response.statusCode() >= 400) {
|
||||||
|
throw new IOException("Bad response code " + response.statusCode());
|
||||||
|
}
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
throw new RuntimeException("Failed to save config to DNSMASQ " + entry.host, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fetchConfig(DNSMASQConfig.Entry entry) {
|
||||||
|
var request = createRequest(entry, "/sync")
|
||||||
|
.header("Accept", "text/event-stream")
|
||||||
|
.header("Cache-Control", "no-cache")
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
return client.sendAsync(request, HttpResponse.BodyHandlers.ofLines())
|
||||||
|
.thenApply(response -> {
|
||||||
|
var result = new AtomicReference<String>(null);
|
||||||
|
response.body()
|
||||||
|
.takeWhile(s -> result.get() == null)
|
||||||
|
.forEach(s -> result.set(parseResponseLine(s)));
|
||||||
|
return result.get();
|
||||||
|
}).get(30, TimeUnit.SECONDS);
|
||||||
|
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||||
|
throw new RuntimeException("Unable to fetch config from DNSMASQ " + entry.host, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final static String SERVER_PREFIX = "server=";
|
||||||
|
|
||||||
|
private void updateDomains(List<String> domains) {
|
||||||
|
for (DNSMASQConfig.Entry entry : config.instances) {
|
||||||
|
try {
|
||||||
|
final var config = new ArrayList<>(
|
||||||
|
Arrays.stream(fetchConfig(entry)
|
||||||
|
.split(Pattern.quote("\n")))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.toList()
|
||||||
|
);
|
||||||
|
|
||||||
|
var existing = new ArrayList<String>();
|
||||||
|
var changed = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
for (var line : config) {
|
||||||
|
//#server=/repo.kirillius.ru/172.16.0.10
|
||||||
|
if (line.startsWith(SERVER_PREFIX) && line.endsWith(entry.forwarder)) {
|
||||||
|
var split = line.split(Pattern.quote("/"));
|
||||||
|
|
||||||
|
if (split.length == 3) {
|
||||||
|
existing.add(split[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//remove
|
||||||
|
existing.stream()
|
||||||
|
.filter(d -> !domains.contains(d))
|
||||||
|
.forEach(domain -> {
|
||||||
|
config.remove(SERVER_PREFIX + "/" + domain + "/" + entry.forwarder);
|
||||||
|
changed.set(true);
|
||||||
|
});
|
||||||
|
//add missing
|
||||||
|
domains.stream()
|
||||||
|
.filter(o -> !existing.contains(o))
|
||||||
|
.forEach(domain -> {
|
||||||
|
config.add(SERVER_PREFIX + "/" + domain + "/" + entry.forwarder);
|
||||||
|
changed.set(true);
|
||||||
|
});
|
||||||
|
if (changed.get()) {
|
||||||
|
saveConfig(entry, String.join("\n", config));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
SystemLogger.error("DNS update failed", CTX, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the subscription from the context event handler.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
context.getEventsHandler().getNetworkManagerUpdateEvent().remove(subscription);
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@JSONSerializable
|
||||||
|
public static class DNSMASQConfig {
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@JSONArrayProperty(type = Entry.class)
|
||||||
|
private List<Entry> instances = new ArrayList<>();
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@JSONSerializable
|
||||||
|
public static class Entry {
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@JSONProperty
|
||||||
|
private String host = "http://127.0.0.1:5380";
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@JSONProperty
|
||||||
|
private String user = "foo";
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@JSONProperty
|
||||||
|
private String password = "bar";
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@JSONProperty
|
||||||
|
private String forwarder = "127.0.0.1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,7 +17,6 @@ import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -36,32 +35,7 @@ public final class ROS extends AbstractComponent<ROS.ROSConfig> {
|
||||||
subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getSubnets()));
|
subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getSubnets()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Map<String, String>> getRoutesByGatewayAndVRF(ApiConnection connection, String gateway, String vrf)
|
|
||||||
throws MikrotikApiException {
|
|
||||||
|
|
||||||
List<Map<String, String>> allRoutes = new ArrayList<>();
|
|
||||||
int offset = 0;
|
|
||||||
final int LIMIT = 100;
|
|
||||||
|
|
||||||
// while (true) {
|
|
||||||
// // Формируем запрос с пагинацией. Каждый параметр - отдельный аргумент.
|
|
||||||
// List<Map<String, String>> chunk = connection.execute(
|
|
||||||
// "/ip/route/print",
|
|
||||||
// "gateway=" + gateway,
|
|
||||||
// "routing-table=" + vrf,
|
|
||||||
// ".proplist=.id,dst-address,static", // Указываем нужные поля
|
|
||||||
// "limit=" + offset + "," + LIMIT
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// if (chunk == null || chunk.isEmpty()) {
|
|
||||||
// break; // Выходим из цикла, если данных больше нет
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// allRoutes.addAll(chunk);
|
|
||||||
// offset += LIMIT; // Переходим к следующей порции
|
|
||||||
// }
|
|
||||||
return allRoutes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronises FRR instances with the provided subnet list.
|
* Synchronises FRR instances with the provided subnet list.
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { OVPNConfig } from '../pages/OVPN.js';
|
||||||
import { TDNSConfig } from '../pages/TDNS.js';
|
import { TDNSConfig } from '../pages/TDNS.js';
|
||||||
import { FRRConfig } from '../pages/FRR.js';
|
import { FRRConfig } from '../pages/FRR.js';
|
||||||
import { ROSConfig } from '../pages/ROS.js';
|
import { ROSConfig } from '../pages/ROS.js';
|
||||||
|
import { DNSMASQConfig } from '../pages/DNSMASQ.js';
|
||||||
import { SettingsPage } from '../pages/Settings.js';
|
import { SettingsPage } from '../pages/Settings.js';
|
||||||
import { LogsPage } from '../pages/Logs.js';
|
import { LogsPage } from '../pages/Logs.js';
|
||||||
import { NetworkResourcesPage } from '../pages/NetworkResources.js';
|
import { NetworkResourcesPage } from '../pages/NetworkResources.js';
|
||||||
|
|
@ -32,6 +33,7 @@ const allMenuItems = [
|
||||||
{ label: 'Настройка TDNS', path: 'tdns', component: 'ru.kirillius.pf.sdn.External.API.Components.TDNS' },
|
{ label: 'Настройка TDNS', path: 'tdns', component: 'ru.kirillius.pf.sdn.External.API.Components.TDNS' },
|
||||||
{ label: 'Настройка FRR', path: 'frr', component: 'ru.kirillius.pf.sdn.External.API.Components.FRR' },
|
{ label: 'Настройка FRR', path: 'frr', component: 'ru.kirillius.pf.sdn.External.API.Components.FRR' },
|
||||||
{ label: 'Настройка ROS', path: 'ros', component: 'ru.kirillius.pf.sdn.External.API.Components.ROS' },
|
{ label: 'Настройка ROS', path: 'ros', component: 'ru.kirillius.pf.sdn.External.API.Components.ROS' },
|
||||||
|
{ label: 'Настройка DNSMASQ', path: 'dnsmasq', component: 'ru.kirillius.pf.sdn.External.API.Components.DNSMASQ' },
|
||||||
{ label: 'Журнал', path: 'logs', component: null },
|
{ label: 'Журнал', path: 'logs', component: null },
|
||||||
{ label: 'Сетевые ресурсы', path: 'network-resources', component: null },
|
{ label: 'Сетевые ресурсы', path: 'network-resources', component: null },
|
||||||
{ label: 'Разблокировка сайта', path: 'unlock-site', component: null },
|
{ label: 'Разблокировка сайта', path: 'unlock-site', component: null },
|
||||||
|
|
@ -85,6 +87,11 @@ const routes = {
|
||||||
mount: ROSConfig.mount,
|
mount: ROSConfig.mount,
|
||||||
unmount: ROSConfig.unmount
|
unmount: ROSConfig.unmount
|
||||||
},
|
},
|
||||||
|
'#dnsmasq': {
|
||||||
|
render: DNSMASQConfig.render,
|
||||||
|
mount: DNSMASQConfig.mount,
|
||||||
|
unmount: DNSMASQConfig.unmount
|
||||||
|
},
|
||||||
'#logs': {
|
'#logs': {
|
||||||
render: LogsPage.render,
|
render: LogsPage.render,
|
||||||
mount: LogsPage.mount,
|
mount: LogsPage.mount,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,284 @@
|
||||||
|
import $ from 'jquery';
|
||||||
|
import { JSONRPC } from '@/json-rpc.js';
|
||||||
|
|
||||||
|
const DNSMASQ_COMPONENT_NAME = 'ru.kirillius.pf.sdn.External.API.Components.DNSMASQ';
|
||||||
|
|
||||||
|
const FIELD_IDS = {
|
||||||
|
container: 'dnsmasq-config-container',
|
||||||
|
instancesList: 'dnsmasq-instances-list',
|
||||||
|
addInstanceButton: 'dnsmasq-add-instance-btn',
|
||||||
|
saveButton: 'save-dnsmasq-btn',
|
||||||
|
status: 'dnsmasq-status-message'
|
||||||
|
};
|
||||||
|
|
||||||
|
const CLASS_NAMES = {
|
||||||
|
instance: 'dnsmasq-instance-entry',
|
||||||
|
removeInstanceButton: 'dnsmasq-remove-instance-btn',
|
||||||
|
host: 'dnsmasq-host',
|
||||||
|
user: 'dnsmasq-user',
|
||||||
|
password: 'dnsmasq-password',
|
||||||
|
forwarder: 'dnsmasq-forwarder'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SELECTORS = {
|
||||||
|
container: `#${FIELD_IDS.container}`,
|
||||||
|
instancesList: `#${FIELD_IDS.instancesList}`,
|
||||||
|
addInstanceButton: `#${FIELD_IDS.addInstanceButton}`,
|
||||||
|
saveButton: `#${FIELD_IDS.saveButton}`,
|
||||||
|
status: `#${FIELD_IDS.status}`
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentConfig = { instances: [] };
|
||||||
|
let statusTimeoutId = null;
|
||||||
|
let instanceCounter = 0;
|
||||||
|
|
||||||
|
const getStatusElement = () => $(SELECTORS.status);
|
||||||
|
|
||||||
|
function normalizeConfig(config) {
|
||||||
|
if (!config || !Array.isArray(config.instances)) {
|
||||||
|
return { instances: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
instances: config.instances.map(instance => ({
|
||||||
|
host: instance?.host || '127.0.0.1',
|
||||||
|
user: instance?.user || '',
|
||||||
|
password: instance?.password || '',
|
||||||
|
forwarder: instance?.forwarder || '127.0.0.1'
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearStatus() {
|
||||||
|
const $status = getStatusElement();
|
||||||
|
if (!$status.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (statusTimeoutId) {
|
||||||
|
clearTimeout(statusTimeoutId);
|
||||||
|
statusTimeoutId = null;
|
||||||
|
}
|
||||||
|
$status.stop(true, true).hide().text('').removeClass('success-message error-message');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(message, type) {
|
||||||
|
const $status = getStatusElement();
|
||||||
|
if (!$status.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (statusTimeoutId) {
|
||||||
|
clearTimeout(statusTimeoutId);
|
||||||
|
}
|
||||||
|
$status
|
||||||
|
.removeClass('success-message error-message')
|
||||||
|
.addClass(type === 'success' ? 'success-message' : 'error-message')
|
||||||
|
.text(message)
|
||||||
|
.show();
|
||||||
|
|
||||||
|
statusTimeoutId = window.setTimeout(() => {
|
||||||
|
$status.fadeOut();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAction($button, pendingText, action, messages) {
|
||||||
|
if (!$button.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalText = $button.text();
|
||||||
|
$button.prop('disabled', true).text(pendingText);
|
||||||
|
clearStatus();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await action();
|
||||||
|
if (messages?.success) {
|
||||||
|
const successMessage = typeof messages.success === 'function'
|
||||||
|
? messages.success(result)
|
||||||
|
: messages.success;
|
||||||
|
if (successMessage) {
|
||||||
|
updateStatus(successMessage, 'success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(messages?.log || 'Ошибка выполнения действия DNSMASQ:', error);
|
||||||
|
if (messages?.error) {
|
||||||
|
updateStatus(messages.error, 'error');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$button.prop('disabled', false).text(originalText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetCounters() {
|
||||||
|
instanceCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextInstanceId() {
|
||||||
|
instanceCounter += 1;
|
||||||
|
return instanceCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInstanceRow(instance = {}) {
|
||||||
|
const instanceId = getNextInstanceId();
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="${CLASS_NAMES.instance}" data-instance-id="${instanceId}" style="border: 1px solid var(--color-border); padding: 20px; border-radius: 12px; margin-bottom: 20px; background: var(--color-surface, #1f2333);">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||||
|
<h4 style="margin: 0;">Инстанс</h4>
|
||||||
|
<button type="button" class="btn-link ${CLASS_NAMES.removeInstanceButton}">Удалить</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dnsmasq-host-${instanceId}">Хост</label>
|
||||||
|
<input type="text" id="dnsmasq-host-${instanceId}" class="form-control ${CLASS_NAMES.host}" placeholder="http://127.0.0.1:9090" value="${instance.host || '127.0.0.1'}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dnsmasq-user-${instanceId}">Имя пользователя</label>
|
||||||
|
<input type="text" id="dnsmasq-user-${instanceId}" class="form-control ${CLASS_NAMES.user}" value="${instance.user || ''}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dnsmasq-password-${instanceId}">Пароль</label>
|
||||||
|
<input type="password" id="dnsmasq-password-${instanceId}" class="form-control ${CLASS_NAMES.password}" placeholder="Оставьте пустым для сохранения текущего пароля" data-original-password="${instance.password || ''}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dnsmasq-forwarder-${instanceId}">Forwarder</label>
|
||||||
|
<input type="text" id="dnsmasq-forwarder-${instanceId}" class="form-control ${CLASS_NAMES.forwarder}" placeholder="127.0.0.1" value="${instance.forwarder || '127.0.0.1'}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateInstances() {
|
||||||
|
const instances = currentConfig.instances.length ? currentConfig.instances : [];
|
||||||
|
const $list = $(SELECTORS.instancesList);
|
||||||
|
resetCounters();
|
||||||
|
$list.empty();
|
||||||
|
instances.forEach(instance => {
|
||||||
|
$list.append(createInstanceRow(instance));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDNSMASQForm() {
|
||||||
|
const $container = $(SELECTORS.container);
|
||||||
|
$container.html(`
|
||||||
|
<div class="component-config-form">
|
||||||
|
<h3 class="config-section-title">Инстансы DNSMASQ</h3>
|
||||||
|
<p class="hint-text" style="margin-bottom: 20px;">Настройте параметры подключения к DNSMASQ для синхронизации списков серверов. Можно добавить несколько инстансов.</p>
|
||||||
|
<div id="${FIELD_IDS.instancesList}"></div>
|
||||||
|
<div class="form-group" style="margin-top: 10px;">
|
||||||
|
<button type="button" id="${FIELD_IDS.addInstanceButton}" class="btn-secondary" style="width: 220px;">Добавить инстанс</button>
|
||||||
|
</div>
|
||||||
|
<div class="action-buttons" style="margin-top: 40px;">
|
||||||
|
<button id="${FIELD_IDS.saveButton}" class="btn-primary" style="width: 220px;">Применить Конфигурацию</button>
|
||||||
|
</div>
|
||||||
|
<div id="${FIELD_IDS.status}" class="error-message" style="display: none; margin-top: 20px;"></div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
populateInstances();
|
||||||
|
attachEventHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectConfigFromForm() {
|
||||||
|
const instances = [];
|
||||||
|
|
||||||
|
$(SELECTORS.instancesList).find(`.${CLASS_NAMES.instance}`).each((_, element) => {
|
||||||
|
const $instance = $(element);
|
||||||
|
|
||||||
|
const host = $instance.find(`.${CLASS_NAMES.host}`).val().trim();
|
||||||
|
const user = $instance.find(`.${CLASS_NAMES.user}`).val().trim();
|
||||||
|
const $passwordField = $instance.find(`.${CLASS_NAMES.password}`);
|
||||||
|
const newPassword = $passwordField.val();
|
||||||
|
const originalPassword = $passwordField.data('original-password') || '';
|
||||||
|
const password = newPassword ? newPassword : originalPassword;
|
||||||
|
const forwarder = $instance.find(`.${CLASS_NAMES.forwarder}`).val().trim();
|
||||||
|
|
||||||
|
instances.push({
|
||||||
|
host,
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
forwarder
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { instances };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadConfig() {
|
||||||
|
try {
|
||||||
|
const fullConfig = await JSONRPC.System.getComponentConfig(DNSMASQ_COMPONENT_NAME);
|
||||||
|
currentConfig = normalizeConfig(fullConfig);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке конфига DNSMASQ:', error);
|
||||||
|
currentConfig = { instances: [] };
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddInstance() {
|
||||||
|
const $list = $(SELECTORS.instancesList);
|
||||||
|
$list.append(createInstanceRow({
|
||||||
|
host: '127.0.0.1',
|
||||||
|
user: '',
|
||||||
|
password: '',
|
||||||
|
forwarder: '127.0.0.1'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemoveInstance(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
$(event.currentTarget).closest(`.${CLASS_NAMES.instance}`).remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
const $button = $(SELECTORS.saveButton);
|
||||||
|
clearStatus();
|
||||||
|
|
||||||
|
const newConfig = collectConfigFromForm();
|
||||||
|
|
||||||
|
await runAction($button, 'Применение...', async () => {
|
||||||
|
await JSONRPC.System.setComponentConfig(DNSMASQ_COMPONENT_NAME, newConfig);
|
||||||
|
currentConfig = normalizeConfig(newConfig);
|
||||||
|
populateInstances();
|
||||||
|
}, {
|
||||||
|
success: 'Конфигурация DNSMASQ успешно сохранена.',
|
||||||
|
error: 'Ошибка при сохранении конфигурации DNSMASQ.',
|
||||||
|
log: 'Ошибка сохранения конфига DNSMASQ'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachEventHandlers() {
|
||||||
|
$(SELECTORS.saveButton).off('click').on('click', handleSave);
|
||||||
|
$(SELECTORS.addInstanceButton).off('click').on('click', handleAddInstance);
|
||||||
|
$(SELECTORS.instancesList)
|
||||||
|
.off('click', `.${CLASS_NAMES.removeInstanceButton}`).on('click', `.${CLASS_NAMES.removeInstanceButton}`, handleRemoveInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
function detachEventHandlers() {
|
||||||
|
$(SELECTORS.saveButton).off('click');
|
||||||
|
$(SELECTORS.addInstanceButton).off('click');
|
||||||
|
$(SELECTORS.instancesList)
|
||||||
|
.off('click', `.${CLASS_NAMES.removeInstanceButton}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DNSMASQConfig = {
|
||||||
|
render: () => `
|
||||||
|
<h1 class="page-title">Настройка DNSMASQ</h1>
|
||||||
|
<div id="${FIELD_IDS.container}">
|
||||||
|
<p>Загрузка конфигурации...</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
mount: async () => {
|
||||||
|
const success = await loadConfig();
|
||||||
|
if (success) {
|
||||||
|
renderDNSMASQForm();
|
||||||
|
} else {
|
||||||
|
$(SELECTORS.container).html('<p class="error-message">Не удалось загрузить конфигурацию DNSMASQ.</p>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unmount: () => {
|
||||||
|
detachEventHandlers();
|
||||||
|
clearStatus();
|
||||||
|
currentConfig = { instances: [] };
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue