Compare commits
3 Commits
master
...
feature/re
| Author | SHA1 | Date |
|---|---|---|
|
|
ee3f4f3fe5 | |
|
|
c1dbaa5bfc | |
|
|
c0d5fb5d1f |
|
|
@ -46,4 +46,3 @@ ovpn-connector.json
|
|||
app/src/main/resources/htdocs/
|
||||
*.pfapp
|
||||
cache/
|
||||
test/
|
||||
|
|
|
|||
16
Dockerfile
16
Dockerfile
|
|
@ -1,16 +0,0 @@
|
|||
FROM alpine:latest
|
||||
|
||||
ARG VERSION
|
||||
|
||||
RUN apk add --no-cache openjdk25
|
||||
|
||||
RUN mkdir -p /data /data/local
|
||||
|
||||
COPY docker/config.json /data/config.json
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
|
||||
COPY launcher/target/${VERSION}.pfapp /default.pfapp
|
||||
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
@groovy.transform.Field String VERSION
|
||||
|
||||
@NonCPS
|
||||
String extractBaseVersion(String pom) {
|
||||
def matcher = pom =~ /<artifactId>pf-sdn<\/artifactId>\s*<version>([0-9.]+)<\/version>/
|
||||
return matcher.find() ? matcher.group(1) : null
|
||||
}
|
||||
|
||||
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
environment {
|
||||
NEXUS_URL = 'http://172.16.0.59:8001'
|
||||
NEXUS_CREDENTIALS_ID = 'nexus_cred'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Set build number into version') {
|
||||
steps {
|
||||
script {
|
||||
def pom = readFile('pom.xml')
|
||||
def baseVersion = extractBaseVersion(pom)
|
||||
if (!baseVersion) {
|
||||
error "Project version not found in pom.xml"
|
||||
}
|
||||
|
||||
echo "Base version from pom: ${baseVersion}"
|
||||
|
||||
def parts = baseVersion.tokenize('.')
|
||||
parts[parts.size() - 1] = env.BUILD_NUMBER
|
||||
VERSION = parts.join('.')
|
||||
|
||||
echo "New project version: ${VERSION}"
|
||||
sh "mvn -B versions:set -DnewVersion=${VERSION} -DprocessAllModule"
|
||||
sh 'mvn -B versions:commit'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Maven package') {
|
||||
steps {
|
||||
sh 'mvn -B clean package verify'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build docker image') {
|
||||
steps {
|
||||
sh "docker build -f Dockerfile --build-arg VERSION=${VERSION} -t pfsdn:${VERSION} ."
|
||||
echo "Saving docker image pfsdn:${VERSION}"
|
||||
sh "docker save pfsdn:${VERSION} | gzip > pfsdn-${VERSION}.tar.gz"
|
||||
}
|
||||
}
|
||||
|
||||
stage('Push docker image to Nexus') {
|
||||
steps {
|
||||
script {
|
||||
def nexusHost = NEXUS_URL
|
||||
.replaceAll(/^https?:\/\//, '')
|
||||
.replaceAll(/\/+$/, '')
|
||||
def nexusImage = "${nexusHost}/pfsdn:${VERSION}"
|
||||
|
||||
withCredentials([usernamePassword(
|
||||
credentialsId: NEXUS_CREDENTIALS_ID,
|
||||
usernameVariable: 'NEXUS_USER',
|
||||
passwordVariable: 'NEXUS_PASS'
|
||||
)]) {
|
||||
sh """
|
||||
echo "\${NEXUS_PASS}" | docker login ${nexusHost} -u "\${NEXUS_USER}" --password-stdin
|
||||
docker tag pfsdn:${VERSION} ${nexusImage}
|
||||
docker push ${nexusImage}
|
||||
docker rmi ${nexusImage} || true
|
||||
docker rmi pfsdn:${VERSION} || true
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Upload app to Nexus') {
|
||||
steps {
|
||||
script {
|
||||
withCredentials([usernamePassword(credentialsId: NEXUS_CREDENTIALS_ID,
|
||||
usernameVariable: 'NEXUS_USER',
|
||||
passwordVariable: 'NEXUS_PASSWORD')]) {
|
||||
sh """
|
||||
curl -f -v -u ${NEXUS_USER}:${NEXUS_PASSWORD} \
|
||||
--upload-file launcher/target/${VERSION}.pfapp \
|
||||
http://172.16.0.59:8081/repository/object-storage/apps/${VERSION}.pfapp
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy') {
|
||||
steps {
|
||||
script {
|
||||
|
||||
archiveArtifacts artifacts: "pfsdn-${VERSION}.tar.gz", fingerprint: true
|
||||
|
||||
echo "Saving pfapp artifact ${VERSION}.pfapp"
|
||||
archiveArtifacts artifacts: "launcher/target/${VERSION}.pfapp", fingerprint: true
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
always {
|
||||
sh "docker rmi pfsdn:${VERSION} || true"
|
||||
}
|
||||
success {
|
||||
cleanWs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ru.kirillius</groupId>
|
||||
<artifactId>pf-sdn</artifactId>
|
||||
<version>1.1.0.0</version>
|
||||
<version>1.0.1.5</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>pf-sdn.app</artifactId>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package ru.kirillius.pf.sdn;
|
|||
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import ru.kirillius.pf.sdn.External.API.Components.*;
|
||||
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.TDNS;
|
||||
import ru.kirillius.pf.sdn.External.API.GitSubscription;
|
||||
import ru.kirillius.pf.sdn.External.API.HEInfoProvider;
|
||||
import ru.kirillius.pf.sdn.External.API.LocalFilesystemSubscription;
|
||||
|
|
@ -12,6 +14,7 @@ import ru.kirillius.pf.sdn.core.Auth.AuthManager;
|
|||
import ru.kirillius.pf.sdn.core.Auth.TokenService;
|
||||
import ru.kirillius.pf.sdn.core.Networking.BGPInfoService;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkingService;
|
||||
import ru.kirillius.pf.sdn.core.Networking.ResolverService;
|
||||
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
|
||||
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionService;
|
||||
import ru.kirillius.pf.sdn.core.Util.Wait;
|
||||
|
|
@ -24,7 +27,6 @@ import java.io.IOException;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
|
@ -79,7 +81,7 @@ public class App implements Context, Closeable {
|
|||
* Instantiates all application services and performs initial wiring.
|
||||
*/
|
||||
private ServiceManager loadServiceManager() {
|
||||
var manager = new ServiceManager(this, List.of(AuthManager.class, ComponentHandlerService.class, TokenService.class, AppUpdateService.class, BGPInfoService.class, NetworkingService.class, SubscriptionService.class, ResourceUpdateService.class, WebService.class));
|
||||
var manager = new ServiceManager(this, List.of(AuthManager.class, ComponentHandlerService.class, TokenService.class, AppUpdateService.class, BGPInfoService.class, NetworkingService.class, SubscriptionService.class, ResourceUpdateService.class, WebService.class, ResolverService.class));
|
||||
var infoService = manager.getService(BGPInfoService.class);
|
||||
infoService.addProvider(new HEInfoProvider());
|
||||
infoService.addProvider(new RIPEInfoProvider());
|
||||
|
|
@ -115,9 +117,7 @@ public class App implements Context, Closeable {
|
|||
serviceManager.getService(ComponentHandlerService.class).syncComponentsWithConfig();
|
||||
serviceManager.getService(ResourceUpdateService.class).start();
|
||||
|
||||
if(!isExternalManagement()) {
|
||||
removeObsoleteVersions();
|
||||
}
|
||||
removeObsoleteVersions();
|
||||
}
|
||||
|
||||
private void removeObsoleteVersions() {
|
||||
|
|
@ -160,7 +160,7 @@ public class App implements Context, Closeable {
|
|||
.configFile(new File(getArgument("c", args)))
|
||||
.appLibrary(new File(getArgument("l", args)))
|
||||
.repository(getArgument("r", args))
|
||||
.availableComponentClasses(List.of(FRR.class, OVPN.class, TDNS.class, ROS.class, DNSMASQ.class, SRM.class)).build())) {
|
||||
.availableComponentClasses(List.of(FRR.class, OVPN.class, TDNS.class)).build())) {
|
||||
Wait.when(app.running::get);
|
||||
if (app.shouldRestart.get()) {
|
||||
System.exit(42);
|
||||
|
|
@ -178,14 +178,7 @@ public class App implements Context, Closeable {
|
|||
*/
|
||||
public void requestExit(boolean restart) {
|
||||
running.set(false);
|
||||
if(!isExternalManagement()) {
|
||||
shouldRestart.set(restart);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExternalManagement() {
|
||||
return Objects.equals(System.getProperty("app.external.management", Boolean.FALSE.toString()), Boolean.TRUE.toString());
|
||||
shouldRestart.set(restart);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,196 +0,0 @@
|
|||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
package ru.kirillius.pf.sdn.External.API.Components;
|
||||
|
||||
import lombok.*;
|
||||
import me.legrange.mikrotik.ApiConnection;
|
||||
import me.legrange.mikrotik.MikrotikApiException;
|
||||
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.IPv4Subnet;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Component that synchronises FRR routing instances with the aggregated subnet list.
|
||||
*/
|
||||
public final class ROS extends AbstractComponent<ROS.ROSConfig> {
|
||||
|
||||
private final static String CTX = ROS.class.getSimpleName();
|
||||
private final EventListener<NetworkResourceBundle> subscription;
|
||||
|
||||
/**
|
||||
* Binds the component to the application context and subscribes to network updates.
|
||||
*/
|
||||
public ROS(Context context) {
|
||||
super(context);
|
||||
subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getSubnets()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Synchronises FRR instances with the provided subnet list.
|
||||
*/
|
||||
private void updateSubnets(List<IPv4Subnet> subnets) {
|
||||
for (var entry : config.instances) {
|
||||
SystemLogger.message("Updating subnets in RouterOS " + entry.host, CTX);
|
||||
|
||||
try (var connection = ApiConnection.connect(entry.host)) {
|
||||
connection.login(entry.username, entry.password);
|
||||
SystemLogger.message("Fetching existing subnets...", CTX);
|
||||
var existingSubnets = new HashMap<String, IPv4Subnet>();
|
||||
var result = connection.execute("/ip/route/print .proplist=.id,dst-address,static,gateway where static=true and routing-table=" + entry.VRF);
|
||||
result.forEach(row -> {
|
||||
if (row.containsKey("static") && row.get("gateway").contains(entry.gateway)) {
|
||||
existingSubnets.put(row.get(".id"), new IPv4Subnet(row.get("dst-address")));
|
||||
}
|
||||
});
|
||||
|
||||
//удаляем лишние подсети
|
||||
|
||||
var subnetsToRemove = existingSubnets.keySet().stream().filter(id -> !subnets.contains(existingSubnets.get(id))).toList();
|
||||
if (!subnetsToRemove.isEmpty()) {
|
||||
SystemLogger.message(subnetsToRemove.size() + " subnets should be removed", CTX);
|
||||
var counter = new AtomicInteger(0);
|
||||
subnetsToRemove.forEach(id -> {
|
||||
try {
|
||||
connection.execute("/ip/route/remove .id=" + id);
|
||||
counter.incrementAndGet();
|
||||
|
||||
if (counter.get() % 100 == 0) {
|
||||
SystemLogger.message(counter.get() + " subnets has been removed", CTX);
|
||||
}
|
||||
} catch (MikrotikApiException e) {
|
||||
throw new RuntimeException("Failed to remove subnet " + id, e);
|
||||
}
|
||||
});
|
||||
|
||||
SystemLogger.message("All subnets has been removed", CTX);
|
||||
|
||||
}
|
||||
|
||||
//добавляем новые подсети
|
||||
var subnetsToAdd = subnets.stream().filter(subnet -> !existingSubnets.containsValue(subnet)).toList();
|
||||
if (!subnetsToAdd.isEmpty()) {
|
||||
SystemLogger.message(subnetsToAdd.size() + " subnets should be added", CTX);
|
||||
var counter = new AtomicInteger(0);
|
||||
subnetsToAdd.forEach(subnet -> {
|
||||
try {
|
||||
connection.execute("/ip/route/add dst-address=" + subnet + " gateway=" + entry.gateway + " routing-table=" + entry.VRF);
|
||||
counter.incrementAndGet();
|
||||
if (counter.get() % 100 == 0) {
|
||||
SystemLogger.message(counter.get() + " subnets has been added", CTX);
|
||||
}
|
||||
} catch (MikrotikApiException e) {
|
||||
throw new RuntimeException("Failed to remove subnet " + subnet, e);
|
||||
}
|
||||
});
|
||||
SystemLogger.message("All subnets has been added", CTX);
|
||||
}
|
||||
|
||||
SystemLogger.message("ROS update is complete", CTX);
|
||||
|
||||
} catch (Exception e) {
|
||||
SystemLogger.error("Failed to execute api command because of error", CTX, e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the subscription from the context event handler.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
context.getEventsHandler().getNetworkManagerUpdateEvent().remove(subscription);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration describing FRR instances and how subnets should be rendered for them.
|
||||
*/
|
||||
@JSONSerializable
|
||||
public static class ROSConfig {
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONArrayProperty(type = Entry.class)
|
||||
private List<Entry> instances = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Declarative description of a single FRR instance managed by the component.
|
||||
*/
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JSONSerializable
|
||||
public static class Entry {
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private String host = "127.0.0.1";
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private String username = "admin";
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private String password = "passwd";
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private String VRF = "main";
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private String gateway = "127.0.0.1";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
package ru.kirillius.pf.sdn.External.API.Components;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.json.JSONObject;
|
||||
import ru.kirillius.java.utils.events.EventListener;
|
||||
import ru.kirillius.json.JSONUtility;
|
||||
import ru.kirillius.pf.sdn.core.AbstractComponent;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Networking.IPv4Subnet;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||
import ru.kirillius.pf.sdn.web.WebService;
|
||||
import ru.kirillius.pf.sdn.web.WebhookServlet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* Component integrating with OpenVPN to expose management RPC and synchronize route exports.
|
||||
*/
|
||||
public final class SRM extends AbstractComponent<JSONObject> {
|
||||
private final static String CTX = SRM.class.getSimpleName();
|
||||
private final static String HOOK_NAME = "SRM";
|
||||
private final EventListener<NetworkResourceBundle> updateEvent;
|
||||
private final List<IPv4Subnet> lastSubnets = new ArrayList<>();
|
||||
private final AtomicLong lastUpdate = new AtomicLong(0);
|
||||
private EventListener<WebhookServlet.RequestContext> hookListener;
|
||||
|
||||
public SRM(Context context) {
|
||||
super(context);
|
||||
var eventsHandler = context.getEventsHandler();
|
||||
|
||||
updateEvent = eventsHandler.getNetworkManagerUpdateEvent().add(bundle -> {
|
||||
var subnets = new HashSet<>(bundle.getSubnets());
|
||||
synchronized (lastSubnets) {
|
||||
if (lastSubnets.size() != subnets.size() || !subnets.containsAll(lastSubnets)) {
|
||||
lastSubnets.clear();
|
||||
lastSubnets.addAll(subnets);
|
||||
lastUpdate.set(System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var webService = context.getServiceManager().getService(WebService.class);
|
||||
var webhookServlet = webService.getWebhookServlet();
|
||||
|
||||
hookListener = webhookServlet.registerHook(HOOK_NAME, httpContext -> {
|
||||
var resp = httpContext.response();
|
||||
if (!httpContext.request().getMethod().equals("GET")) {
|
||||
resp.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
|
||||
return;
|
||||
}
|
||||
|
||||
var path = httpContext.request().getRequestURI();
|
||||
resp.setHeader("Cache-Control", "no-cache");
|
||||
if (path.endsWith("/update")) {
|
||||
resp.setHeader("Content-Type", "text/plain");
|
||||
var writer = resp.getWriter();
|
||||
writer.print(lastUpdate.get());
|
||||
writer.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith("/subnets")) {
|
||||
resp.setHeader("Content-Type", "application/json");
|
||||
var subnets = new ArrayList<IPv4Subnet>();
|
||||
synchronized (lastSubnets) {
|
||||
//noinspection CollectionAddAllCanBeReplacedWithConstructor
|
||||
subnets.addAll(lastSubnets);
|
||||
subnets.sort(Comparator.comparingLong(IPv4Subnet::getLongAddress));
|
||||
}
|
||||
|
||||
var json = JSONUtility.serializeCollection(subnets, IPv4Subnet.class, null);
|
||||
var writer = resp.getWriter();
|
||||
writer.print(json.toString());
|
||||
writer.close();
|
||||
return;
|
||||
}
|
||||
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
var eventsHandler = context.getEventsHandler();
|
||||
if (updateEvent != null) {
|
||||
eventsHandler.getNetworkManagerUpdateEvent().remove(updateEvent);
|
||||
}
|
||||
|
||||
if (hookListener != null) {
|
||||
var webService = context.getServiceManager().getService(WebService.class);
|
||||
var webhookServlet = webService.getWebhookServlet();
|
||||
webhookServlet.unregisterHook(HOOK_NAME, hookListener);
|
||||
hookListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -87,6 +87,7 @@ public class SubscriptionManager implements RPC {
|
|||
@JRPCArgument(name = "ASN") JSONArray ASN,
|
||||
@JRPCArgument(name = "subnets") JSONArray subnets,
|
||||
@JRPCArgument(name = "addresses") JSONArray addresses,
|
||||
@JRPCArgument(name = "autoResolve") boolean autoResolve,
|
||||
@JRPCArgument(name = "storage") String storage,
|
||||
@JRPCArgument(name = "subscribe") boolean subscribe) throws IOException {
|
||||
var repositoryConfig = findLocalRepo(storage);
|
||||
|
|
@ -97,7 +98,7 @@ public class SubscriptionManager implements RPC {
|
|||
|
||||
var domainList = new ArrayList<String>();
|
||||
|
||||
domains.forEach(d->domainList.add(d.toString()));
|
||||
domains.forEach(d -> domainList.add(d.toString()));
|
||||
|
||||
try (var writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(directory, name + ".json"))))) {
|
||||
var merged = JSONUtility.deserializeCollection(subnets, IPv4Subnet.class, null).collect(Collectors.toList());
|
||||
|
|
@ -113,6 +114,7 @@ public class SubscriptionManager implements RPC {
|
|||
.subnets(merged)
|
||||
.domains(domainList)
|
||||
.description(description)
|
||||
.resolveDomains(autoResolve)
|
||||
.build()
|
||||
).toString(2));
|
||||
}
|
||||
|
|
@ -124,7 +126,6 @@ public class SubscriptionManager implements RPC {
|
|||
context.getServiceManager().getService(SubscriptionService.class).triggerUpdate();
|
||||
}
|
||||
|
||||
|
||||
private RepositoryConfig findLocalRepo(String storage) throws FileNotFoundException {
|
||||
var optional = context.getConfig().getSubscriptions().stream().filter(repositoryConfig -> repositoryConfig.getType().equals(LocalFilesystemSubscription.class) && repositoryConfig.getName().equals(storage)).findFirst();
|
||||
if (optional.isEmpty()) {
|
||||
|
|
@ -134,7 +135,6 @@ public class SubscriptionManager implements RPC {
|
|||
return optional.get();
|
||||
}
|
||||
|
||||
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public void removeLocalResourceFile(
|
||||
|
|
|
|||
|
|
@ -122,19 +122,10 @@ public class System implements RPC {
|
|||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public void doUpdate() {
|
||||
if(context.isExternalManagement()){
|
||||
return;
|
||||
}
|
||||
var updateService = context.getServiceManager().getService(AppUpdateService.class);
|
||||
updateService.updateApp();
|
||||
}
|
||||
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public boolean isExternalManaged() {
|
||||
return context.isExternalManagement();
|
||||
}
|
||||
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public JSONArray getRepositoryTypes() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package ru.kirillius.pf.sdn.web;
|
||||
|
||||
|
||||
import lombok.Getter;
|
||||
import org.eclipse.jetty.ee10.servlet.DefaultServlet;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
|
||||
|
|
@ -9,7 +10,10 @@ import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
|
|||
import ru.kirillius.pf.sdn.core.AppService;
|
||||
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.web.RPC.*;
|
||||
import ru.kirillius.pf.sdn.web.RPC.Auth;
|
||||
import ru.kirillius.pf.sdn.web.RPC.NetworkManager;
|
||||
import ru.kirillius.pf.sdn.web.RPC.RPC;
|
||||
import ru.kirillius.pf.sdn.web.RPC.SubscriptionManager;
|
||||
import ru.kirillius.pf.sdn.web.RPC.System;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
|
|
@ -24,6 +28,7 @@ import java.util.Set;
|
|||
*/
|
||||
public class WebService extends AppService {
|
||||
|
||||
|
||||
/**
|
||||
* Stops the embedded HTTP server.
|
||||
*/
|
||||
|
|
@ -68,7 +73,7 @@ public class WebService extends AppService {
|
|||
|
||||
this.addConnector(connector);
|
||||
var servletContext = new ServletContextHandler("/", ServletContextHandler.SESSIONS);
|
||||
servletContext.addServlet(webhookServlet, WebhookServlet.CONTEXT_PATH);
|
||||
|
||||
servletContext.addServlet(JSONRPC, JSONRPCServlet.CONTEXT_PATH);
|
||||
var holder = servletContext.addServlet(DefaultServlet.class, "/");
|
||||
try {
|
||||
|
|
@ -76,7 +81,6 @@ public class WebService extends AppService {
|
|||
} catch (MalformedURLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
this.setHandler(servletContext);
|
||||
start();
|
||||
|
||||
|
|
@ -94,18 +98,21 @@ public class WebService extends AppService {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
var isProtectedAccess = call.getMethod().getAnnotation(ProtectedMethod.class);
|
||||
if (isProtectedAccess != null) {
|
||||
if (!authorized) throw new SecurityException("Forbidden");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
for (var handlerClass : RPCHandlerTypes) {
|
||||
var instance = RPC.instantiate(handlerClass, context);
|
||||
//noinspection unchecked
|
||||
JSONRPC.addTargetInstance((Class<? super RPC>) handlerClass, instance);
|
||||
}
|
||||
|
||||
|
||||
JSONRPC.getErrorHandler().add(throwable -> {
|
||||
SystemLogger.error("JRPC Request " +
|
||||
(throwable.getRequestData() == null ? "" : throwable.getRequestData().toString()) +
|
||||
|
|
@ -115,13 +122,12 @@ public class WebService extends AppService {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private final static Set<Class<? extends RPC>> RPCHandlerTypes = Set.of(Auth.class, NetworkManager.class, SubscriptionManager.class, System.class);
|
||||
|
||||
@Getter
|
||||
private final JSONRPCServlet JSONRPC = new JSONRPCServlet();
|
||||
private final HTTPServer httpServer;
|
||||
@Getter
|
||||
private final WebhookServlet webhookServlet = new WebhookServlet();
|
||||
|
||||
/**
|
||||
* Starts the web service and publishes the JSON-RPC servlet.
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
package ru.kirillius.pf.sdn.web;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import ru.kirillius.java.utils.events.ConcurrentEventHandler;
|
||||
import ru.kirillius.java.utils.events.EventHandler;
|
||||
import ru.kirillius.java.utils.events.EventListener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class WebhookServlet extends HttpServlet {
|
||||
public final static String PATH = "/webhook/";
|
||||
public final static String CONTEXT_PATH = PATH + "*";
|
||||
|
||||
public record RequestContext(HttpServletRequest request, HttpServletResponse response) {
|
||||
}
|
||||
|
||||
private final Map<String, EventHandler<RequestContext>> handlers = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
var path = req.getRequestURI();
|
||||
if (path.startsWith(PATH)) {
|
||||
var name = path.substring(PATH.length()).split(Pattern.quote("/"))[0];
|
||||
if (handlers.containsKey(name)) {
|
||||
try {
|
||||
handlers.get(name).invoke(new RequestContext(req, resp));
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
super.service(req, resp);
|
||||
}
|
||||
|
||||
public EventListener<RequestContext> registerHook(String name, EventListener<RequestContext> listener) {
|
||||
if (!handlers.containsKey(name)) {
|
||||
handlers.put(name, new ConcurrentEventHandler<>());
|
||||
}
|
||||
return handlers.get(name).add(listener);
|
||||
}
|
||||
|
||||
public void unregisterHook(String name, EventListener<RequestContext> listener) {
|
||||
if (!handlers.containsKey(name)) {
|
||||
return;
|
||||
}
|
||||
handlers.get(name).remove(listener);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ru.kirillius</groupId>
|
||||
<artifactId>pf-sdn</artifactId>
|
||||
<version>1.1.0.0</version>
|
||||
<version>1.0.1.5</version>
|
||||
</parent>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -27,10 +27,8 @@ import java.util.regex.Pattern;
|
|||
public class AppUpdateService extends AppService {
|
||||
public final static String EXTENSION = ".pfapp";
|
||||
private static final String CTX = AppUpdateService.class.getSimpleName();
|
||||
private static final Pattern VERSION_LINK_PATTERN = Pattern.compile(
|
||||
"<a\\s+[^>]*href=\"[^\"]*?([0-9]+(?:\\.[0-9]+)+\\.pfapp)\"",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
private static final Pattern VERSION_LINK_PATTERN = Pattern.compile("<a\\s+[^>]*href=\"([0-9]+(?:\\.[0-9]+)*\\.pfapp)\"", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private final String repository;
|
||||
private final Path appLibraryPath;
|
||||
private final Class<?> anchorClass;
|
||||
|
|
@ -50,7 +48,7 @@ public class AppUpdateService extends AppService {
|
|||
this.appLibraryPath = context.getLauncherConfig().getAppLibrary().toPath();
|
||||
this.anchorClass = context.getClass();
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.followRedirects(HttpClient.Redirect.ALWAYS)
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
}
|
||||
|
|
@ -162,7 +160,7 @@ public class AppUpdateService extends AppService {
|
|||
.GET()
|
||||
.build();
|
||||
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(tempFile));
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 400) {
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
SystemLogger.error("Unexpected response code when downloading update: " + response.statusCode(), CTX);
|
||||
return;
|
||||
}
|
||||
|
|
@ -194,7 +192,7 @@ public class AppUpdateService extends AppService {
|
|||
.GET()
|
||||
.build();
|
||||
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 400) {
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
SystemLogger.error("Unexpected response code when checking updates: " + response.statusCode(), CTX);
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,37 @@ public class Config {
|
|||
@JSONProperty
|
||||
private volatile boolean cachingAS = true;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty(required = false)
|
||||
private volatile boolean cachingDomains = true;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONArrayProperty(type = String.class, required = false)
|
||||
private volatile List<String> domainResolvers = List.of("8.8.8.8", "77.88.8.8");
|
||||
|
||||
/**
|
||||
* Time in minutes
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty(required = false)
|
||||
private volatile int domainLookupInterval = 5;
|
||||
|
||||
// @Getter
|
||||
// @Setter
|
||||
// @JSONProperty(required = false)
|
||||
// private volatile int autoLookupPrefixLength = 24;
|
||||
|
||||
/**
|
||||
* Time in hours
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty(required = false)
|
||||
private volatile int domainsTimeToLive = 48;
|
||||
|
||||
/**
|
||||
* Update ASN prefixes every N hours
|
||||
*/
|
||||
|
|
@ -62,7 +93,6 @@ public class Config {
|
|||
* Path where to store temporary data
|
||||
*/
|
||||
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
@JSONProperty
|
||||
|
|
|
|||
|
|
@ -26,7 +26,4 @@ public interface Context {
|
|||
* @param shouldRestart {@code true} to exit with restart intent, {@code false} to shut down.
|
||||
*/
|
||||
void requestExit(boolean shouldRestart);
|
||||
|
||||
|
||||
boolean isExternalManagement();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ public class NetworkResourceBundle {
|
|||
@JSONArrayProperty(type = String.class)
|
||||
private List<String> domains = new ArrayList<>();
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty(required = false)
|
||||
private boolean resolveDomains = false;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof NetworkResourceBundle that)) return false;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,12 @@ import ru.kirillius.pf.sdn.core.Subscription.SubscriptionService;
|
|||
import ru.kirillius.pf.sdn.core.Util.IPv4Util;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
|
@ -22,19 +27,20 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||
* Builds the effective set of network resources by combining subscriptions, caches, and filters.
|
||||
*/
|
||||
public class NetworkingService extends AppService {
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
private final static String CTX = NetworkingService.class.getSimpleName();
|
||||
|
||||
private final File cacheFile;
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
private final File domainCacheFile;
|
||||
private final File asCacheFile;
|
||||
private final EventListener<NetworkResourceBundle> resourceUpdateSubscription;
|
||||
private final EventListener<ContextEventsHandler.ConfigChangeContext> configChangeSubscription;
|
||||
private final AtomicReference<Future<?>> updateProcess = new AtomicReference<>();
|
||||
@Getter
|
||||
private final NetworkResourceBundle inputResources = new NetworkResourceBundle();
|
||||
@Getter
|
||||
private final NetworkResourceBundle outputResources = new NetworkResourceBundle();
|
||||
|
||||
private void rebuildInputs() {
|
||||
inputResources.clear();
|
||||
inputResources.add(context.getConfig().getCustomResources());
|
||||
inputResources.add(context.getServiceManager().getService(SubscriptionService.class).getOutputResources());
|
||||
triggerUpdate(false);
|
||||
}
|
||||
private final Map<Integer, List<IPv4Subnet>> prefixCache = new ConcurrentHashMap<>();
|
||||
private final Map<String, ResolverCacheEntry> domainCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Creates the networking service, wiring subscriptions and restoring cached state.
|
||||
|
|
@ -51,26 +57,71 @@ public class NetworkingService extends AppService {
|
|||
NetworkingService.this.rebuildInputs();
|
||||
}
|
||||
});
|
||||
cacheFile = new File(context.getConfig().getCacheDirectory(), "as-cache.json");
|
||||
if (cacheFile.exists() && context.getConfig().isCachingAS()) {
|
||||
domainCacheFile = new File(context.getConfig().getCacheDirectory(), "domain-cache.json");
|
||||
asCacheFile = new File(context.getConfig().getCacheDirectory(), "as-cache.json");
|
||||
if (asCacheFile.exists() && context.getConfig().isCachingAS()) {
|
||||
SystemLogger.message("Loading as cache file", CTX);
|
||||
try (var is = new FileInputStream(cacheFile)) {
|
||||
try (var is = new FileInputStream(asCacheFile)) {
|
||||
var json = new JSONObject(new JSONTokener(is));
|
||||
json.keySet().forEach(key -> {
|
||||
var as = Integer.parseInt(key);
|
||||
prefixCache.put(as, JSONUtility.deserializeCollection(json.getJSONArray(key), IPv4Subnet.class, null).toList());
|
||||
});
|
||||
} catch (Exception e) {
|
||||
SystemLogger.error("Failed to load as cache file " + cacheFile.getPath(), CTX, e);
|
||||
SystemLogger.error("Failed to load as cache file " + asCacheFile.getPath(), CTX, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (domainCacheFile.exists() && context.getConfig().isCachingAS()) {
|
||||
SystemLogger.message("Loading domain cache file", CTX);
|
||||
try (var is = new FileInputStream(domainCacheFile)) {
|
||||
var json = new JSONObject(new JSONTokener(is));
|
||||
json.keySet().forEach(host -> {
|
||||
domainCache.put(host, JSONUtility.deserializeStructure(json.getJSONObject(host), ResolverCacheEntry.class));
|
||||
});
|
||||
} catch (Exception e) {
|
||||
SystemLogger.error("Failed to load domain cache file " + asCacheFile.getPath(), CTX, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public void performAutoresolve() {
|
||||
var current = new HashSet<IPv4Subnet>();
|
||||
domainCache.forEach((host, entry) -> current.addAll(entry.getAddresses().keySet()));
|
||||
|
||||
resolveDomains(List.copyOf(context.getServiceManager().getService(SubscriptionService.class).getAutoResolvingDomains()));
|
||||
|
||||
var resolved = new HashSet<IPv4Subnet>();
|
||||
domainCache.forEach((host, entry) -> resolved.addAll(entry.getAddresses().keySet()));
|
||||
|
||||
if (resolved.size() != current.size()) {
|
||||
rebuildInputs();
|
||||
} else {
|
||||
var updated = false;
|
||||
for (var subnet : resolved) {
|
||||
if (!current.contains(subnet)) {
|
||||
updated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (updated) {
|
||||
rebuildInputs();
|
||||
}
|
||||
}
|
||||
|
||||
if(!context.getConfig().isCachingDomains()){
|
||||
domainCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private final AtomicReference<Future<?>> updateProcess = new AtomicReference<>();
|
||||
@Getter
|
||||
private final NetworkResourceBundle inputResources = new NetworkResourceBundle();
|
||||
@Getter
|
||||
private final NetworkResourceBundle outputResources = new NetworkResourceBundle();
|
||||
private void rebuildInputs() {
|
||||
inputResources.clear();
|
||||
inputResources.add(context.getConfig().getCustomResources());
|
||||
inputResources.add(context.getServiceManager().getService(SubscriptionService.class).getOutputResources());
|
||||
triggerUpdate(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether an update job is currently executing.
|
||||
|
|
@ -80,8 +131,6 @@ public class NetworkingService extends AppService {
|
|||
return future != null && !future.isDone() && !future.isCancelled();
|
||||
}
|
||||
|
||||
private final Map<Integer, List<IPv4Subnet>> prefixCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Schedules an update of network resources, optionally ignoring cached prefixes.
|
||||
*/
|
||||
|
|
@ -96,50 +145,6 @@ public class NetworkingService extends AppService {
|
|||
SystemLogger.message("Update is started", CTX);
|
||||
var config = context.getConfig();
|
||||
var filteredResources = config.getFilteredResources();
|
||||
var asn = new ArrayList<>(inputResources.getASN());
|
||||
asn.removeAll(filteredResources.getASN());
|
||||
|
||||
var asnToFetch = new ArrayList<>(asn);
|
||||
if (!ignoreCache) {
|
||||
asnToFetch.removeAll(prefixCache.keySet());
|
||||
}
|
||||
|
||||
fetchPrefixes(asnToFetch);
|
||||
|
||||
if (config.isCachingAS()) {
|
||||
try (var os = new FileOutputStream(cacheFile)) {
|
||||
var json = new JSONObject();
|
||||
prefixCache.forEach((key, asnList) -> {
|
||||
json.put(String.valueOf(key), JSONUtility.serializeCollection(asnList, IPv4Subnet.class, null));
|
||||
});
|
||||
os.write(json.toString().getBytes());
|
||||
} catch (IOException e) {
|
||||
SystemLogger.error("Unable to write file " + cacheFile.getPath(), CTX, e);
|
||||
}
|
||||
}
|
||||
|
||||
var subnets = new HashSet<>(inputResources.getSubnets());
|
||||
asn.forEach(n -> {
|
||||
var cached = prefixCache.get(n);
|
||||
if (cached == null) {
|
||||
return;
|
||||
}
|
||||
subnets.addAll(cached);
|
||||
SystemLogger.message("Using " + cached.size() + " subnets from AS" + n, CTX);
|
||||
});
|
||||
filteredResources.getSubnets().forEach(subnets::remove);
|
||||
|
||||
SystemLogger.message("Trying to summary " + subnets.size() + " subnets...", CTX);
|
||||
|
||||
var merged = IPv4Util.summarySubnets(subnets, config.getMergeSubnetsWithUsage());
|
||||
var unmerged = new AtomicInteger();
|
||||
subnets.forEach(subnet -> {
|
||||
if (!merged.getMergedSubnets().contains(subnet)) {
|
||||
unmerged.getAndIncrement();
|
||||
}
|
||||
});
|
||||
|
||||
SystemLogger.message(subnets.size() + " subnets has been summarized and merged to " + merged.getResult().size() + " new subnets. Unmerged: " + unmerged.get(), CTX);
|
||||
|
||||
var domains = new HashSet<>(inputResources.getDomains());
|
||||
filteredResources.getDomains().forEach(domains::remove);
|
||||
|
|
@ -157,6 +162,75 @@ public class NetworkingService extends AppService {
|
|||
|
||||
domains.removeAll(domainsToRemove);
|
||||
|
||||
var asn = new ArrayList<>(inputResources.getASN());
|
||||
asn.removeAll(filteredResources.getASN());
|
||||
|
||||
var asnToFetch = new ArrayList<>(asn);
|
||||
if (!ignoreCache) {
|
||||
asnToFetch.removeAll(prefixCache.keySet());
|
||||
}
|
||||
|
||||
fetchPrefixes(asnToFetch);
|
||||
|
||||
if (config.isCachingAS()) {
|
||||
try (var os = new FileOutputStream(asCacheFile)) {
|
||||
var json = new JSONObject();
|
||||
prefixCache.forEach((key, asnList) -> {
|
||||
json.put(String.valueOf(key), JSONUtility.serializeCollection(asnList, IPv4Subnet.class, null));
|
||||
});
|
||||
os.write(json.toString().getBytes());
|
||||
} catch (IOException e) {
|
||||
SystemLogger.error("Unable to write file " + asCacheFile.getPath(), CTX, e);
|
||||
}
|
||||
}
|
||||
|
||||
resolveDomains(List.copyOf(context.getServiceManager().getService(SubscriptionService.class).getAutoResolvingDomains()));
|
||||
|
||||
if (config.isCachingDomains()) {
|
||||
try (var os = new FileOutputStream(domainCacheFile)) {
|
||||
var json = new JSONObject();
|
||||
domainCache.forEach((key, entry) -> {
|
||||
var serialized = JSONUtility.serializeStructure(entry);
|
||||
json.put(String.valueOf(key), serialized);
|
||||
});
|
||||
os.write(json.toString().getBytes());
|
||||
} catch (IOException e) {
|
||||
SystemLogger.error("Unable to write file " + domainCacheFile.getPath(), CTX, e);
|
||||
}
|
||||
}
|
||||
|
||||
var subnets = new HashSet<>(inputResources.getSubnets());
|
||||
asn.forEach(n -> {
|
||||
var cached = prefixCache.get(n);
|
||||
if (cached == null) {
|
||||
return;
|
||||
}
|
||||
subnets.addAll(cached);
|
||||
SystemLogger.message("Using " + cached.size() + " subnets from AS" + n, CTX);
|
||||
});
|
||||
|
||||
//добавляем отрезолвенные домены
|
||||
domains.forEach(domain -> {
|
||||
var entry = domainCache.get(domain);
|
||||
if (entry != null) {
|
||||
subnets.addAll(entry.getAddresses().keySet());
|
||||
}
|
||||
});
|
||||
|
||||
filteredResources.getSubnets().forEach(subnets::remove);
|
||||
|
||||
SystemLogger.message("Trying to summary " + subnets.size() + " subnets...", CTX);
|
||||
|
||||
var merged = IPv4Util.summarySubnets(subnets, config.getMergeSubnetsWithUsage());
|
||||
var unmerged = new AtomicInteger();
|
||||
subnets.forEach(subnet -> {
|
||||
if (!merged.getMergedSubnets().contains(subnet)) {
|
||||
unmerged.getAndIncrement();
|
||||
}
|
||||
});
|
||||
|
||||
SystemLogger.message(subnets.size() + " subnets has been summarized and merged to " + merged.getResult().size() + " new subnets. Unmerged: " + unmerged.get(), CTX);
|
||||
|
||||
outputResources.setASN(Collections.unmodifiableList(asn));
|
||||
outputResources.setSubnets(merged.getResult());
|
||||
outputResources.setDomains(domains.stream().toList());
|
||||
|
|
@ -174,6 +248,48 @@ public class NetworkingService extends AppService {
|
|||
}));
|
||||
}
|
||||
|
||||
private void resolveDomains(List<String> domains) {
|
||||
var resolvedSubnets = new ArrayList<IPv4Subnet>();
|
||||
var resolver = context.getServiceManager().getService(ResolverService.class);
|
||||
for (var domain : domains) {
|
||||
var task = resolver.resolve(domain);
|
||||
while (!task.isDone() && !task.isCancelled()) {
|
||||
Thread.yield();
|
||||
}
|
||||
try {
|
||||
var subnets = task.get();
|
||||
var entry = domainCache.get(domain);
|
||||
|
||||
if(entry == null) {
|
||||
entry = new ResolverCacheEntry();
|
||||
domainCache.put(domain, entry);
|
||||
}
|
||||
|
||||
var addresses = entry.getAddresses();
|
||||
entry.setLastUpdate(Instant.now());
|
||||
subnets.forEach(subnet -> addresses.put(subnet, Instant.now()));
|
||||
resolvedSubnets.addAll(domainCache.get(domain).getAddresses().keySet());
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
SystemLogger.error("Error happened while resolving domain " + domain, CTX, e);
|
||||
}
|
||||
}
|
||||
|
||||
//remove old entries
|
||||
for (var domain : domainCache.keySet()) {
|
||||
var entry = domainCache.get(domain);
|
||||
var addresses = entry.getAddresses();
|
||||
for (var subnet : addresses.keySet()) {
|
||||
var time = addresses.get(subnet);
|
||||
if (time.isBefore(Instant.now().minus(context.getConfig().getDomainsTimeToLive(), ChronoUnit.HOURS))) {
|
||||
addresses.remove(subnet);
|
||||
}
|
||||
}
|
||||
if (addresses.isEmpty()) {
|
||||
domainCache.remove(domain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches prefixes for the given autonomous systems and stores them in the cache.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package ru.kirillius.pf.sdn.core.Networking;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import ru.kirillius.json.JSONMapProperty;
|
||||
import ru.kirillius.json.JSONProperty;
|
||||
import ru.kirillius.json.JSONSerializable;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@JSONSerializable
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class ResolverCacheEntry {
|
||||
@JSONProperty
|
||||
private Instant lastUpdate = Instant.now();
|
||||
@JSONMapProperty(keyType = IPv4Subnet.class, valueType = Instant.class)
|
||||
private Map<IPv4Subnet, Instant> addresses = new HashMap<>();
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package ru.kirillius.pf.sdn.core.Networking;
|
||||
|
||||
import ru.kirillius.pf.sdn.core.AppService;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Util.DomainUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public class ResolverService extends AppService {
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
private final static String CTX = ResolverService.class.getSimpleName();
|
||||
|
||||
public Future<List<IPv4Subnet>> resolve(String host) {
|
||||
return executor.submit(() -> {
|
||||
var resolved = new ArrayList<IPv4Subnet>();
|
||||
for (var domainResolver : context.getConfig().getDomainResolvers()) {
|
||||
DomainUtil.lookup(host, domainResolver).stream().map(addr -> new IPv4Subnet(addr, 32)).forEach(resolved::add);
|
||||
}
|
||||
return resolved;
|
||||
});
|
||||
}
|
||||
|
||||
public ResolverService(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes event subscriptions and shuts down the executor.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
|
|
@ -34,7 +34,6 @@ public class ResourceUpdateService extends AppService {
|
|||
updateThread.start();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Interrupts the update thread and stops scheduling tasks.
|
||||
*/
|
||||
|
|
@ -76,6 +75,11 @@ public class ResourceUpdateService extends AppService {
|
|||
Wait.when(subscriptionManager::isUpdatingNow);
|
||||
}
|
||||
|
||||
if (uptime % context.getConfig().getDomainLookupInterval() == 0) {
|
||||
SystemLogger.message("Resolving domains...", CTX);
|
||||
context.getServiceManager().getService(NetworkingService.class).performAutoresolve();
|
||||
}
|
||||
|
||||
if (config.getUpdateASInterval() > 0 && uptime % (config.getUpdateASInterval() * 60L) == 0) {
|
||||
SystemLogger.message("Updating cached AS", CTX);
|
||||
var networkManager = context.getServiceManager().getService(NetworkingService.class);
|
||||
|
|
|
|||
|
|
@ -8,11 +8,9 @@ import ru.kirillius.utils.logging.SystemLogger;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
|
|
@ -21,14 +19,14 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||
public class SubscriptionService extends AppService {
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
|
||||
private final Map<Class<? extends SubscriptionProvider>, SubscriptionProvider> providerCache = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
private final AtomicReference<Future<?>> updateProcess = new AtomicReference<>();
|
||||
|
||||
@Getter
|
||||
private final NetworkResourceBundle outputResources = new NetworkResourceBundle();
|
||||
@Getter
|
||||
private final List<String> autoResolvingDomains = new CopyOnWriteArrayList<>();
|
||||
|
||||
public SubscriptionService(Context context) {
|
||||
super(context);
|
||||
|
|
@ -45,7 +43,6 @@ public class SubscriptionService extends AppService {
|
|||
@Getter
|
||||
private final Map<String, NetworkResourceBundle> availableResources = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends SubscriptionProvider> T getProvider(Class<T> providerType) {
|
||||
if (!providerCache.containsKey(providerType)) {
|
||||
|
|
@ -62,6 +59,9 @@ public class SubscriptionService extends AppService {
|
|||
return;
|
||||
}
|
||||
updateProcess.set(executor.submit(() -> {
|
||||
|
||||
|
||||
|
||||
var available = new HashMap<String, NetworkResourceBundle>();
|
||||
|
||||
var bundle = new NetworkResourceBundle();
|
||||
|
|
@ -90,6 +90,12 @@ public class SubscriptionService extends AppService {
|
|||
availableResources.putAll(available);
|
||||
outputResources.clear();
|
||||
outputResources.add(bundle);
|
||||
|
||||
available.values().forEach(b -> {
|
||||
if (b.isResolveDomains()) {
|
||||
autoResolvingDomains.addAll(b.getDomains());
|
||||
}
|
||||
});
|
||||
try {
|
||||
context.getEventsHandler().getSubscriptionsUpdateEvent().invoke(outputResources);
|
||||
} catch (Exception e) {
|
||||
|
|
@ -100,7 +106,6 @@ public class SubscriptionService extends AppService {
|
|||
|
||||
private final static String CTX = SubscriptionService.class.getSimpleName();
|
||||
|
||||
|
||||
/**
|
||||
* Shuts down the executor used for update tasks.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ public class IPv4Util {
|
|||
var overlapped = new ArrayList<IPv4Subnet>();
|
||||
var orderedByPrefix = result.stream().sorted(Comparator.comparing(IPv4Subnet::getPrefixLength)).toList();
|
||||
orderedByPrefix.stream()
|
||||
.filter(subnet -> subnet.getPrefixLength() < 32)
|
||||
.filter(subnet -> subnet.getPrefixLength() > 32)
|
||||
.forEach(parent -> orderedByPrefix.forEach(subnet -> {
|
||||
if (subnet.equals(parent)) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package ru.kirillius.pf.sdn.core.Networking;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
class ResBundleTest {
|
||||
|
||||
@Test
|
||||
void testInit() {
|
||||
var b = new ResolverCacheEntry();
|
||||
b.getAddresses().put(new IPv4Subnet("127.0.0.1/32"), Instant.now());
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,27 @@ class IPv4UtilTest {
|
|||
|
||||
|
||||
|
||||
//subnets.forEach(System.out::println);
|
||||
|
||||
var merged = IPv4Util.summarySubnets(subnets, 51).getResult();
|
||||
|
||||
merged.forEach(System.out::println);
|
||||
|
||||
assertThat(merged).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void summarySubnetsShishanyaCase() {
|
||||
var subnets = new ArrayList<IPv4Subnet>();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
subnets.add(new IPv4Subnet("8.6.112.0/32"));
|
||||
subnets.add(new IPv4Subnet("8.6.112.0/24"));
|
||||
|
||||
|
||||
//subnets.forEach(System.out::println);
|
||||
|
||||
var merged = IPv4Util.summarySubnets(subnets, 51).getResult();
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
{
|
||||
"subscribedResources": [],
|
||||
"subscriptions": [
|
||||
{
|
||||
"name": "updates",
|
||||
"source": "https://git.kirillius.ru/kirillius/protected-resources-list.git",
|
||||
"type": "ru.kirillius.pf.sdn.External.API.GitSubscription",
|
||||
"script": ""
|
||||
},
|
||||
{
|
||||
"name": "local",
|
||||
"source": "/data/local",
|
||||
"type": "ru.kirillius.pf.sdn.External.API.LocalFilesystemSubscription",
|
||||
"script": ""
|
||||
}
|
||||
],
|
||||
"enabledComponents": [],
|
||||
"httpPort": 8080,
|
||||
"filteredResources": {
|
||||
"description": "",
|
||||
"domains": [],
|
||||
"subnets": [],
|
||||
"ASN": []
|
||||
},
|
||||
"mergeSubnets": true,
|
||||
"updateSubscriptionsInterval": 6,
|
||||
"mergeSubnetsWithUsage": 51,
|
||||
"componentsConfig": {},
|
||||
"customResources": {
|
||||
"description": "",
|
||||
"domains": [],
|
||||
"subnets": [],
|
||||
"ASN": []
|
||||
},
|
||||
"passwordHash": "",
|
||||
"displayDebuggingInfo": true,
|
||||
"cachingAS": true,
|
||||
"host": "0.0.0.0",
|
||||
"updateASInterval": 12,
|
||||
"cacheDirectory": "/var/cache/pf-sdn",
|
||||
"passwordSalt": "bf932418-10db-4e03-b5cd-23bc1bfb86e4"
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/sh
|
||||
exec java -Xmx256m -Dapp.external.management=true -jar default.pfapp --c=/data/config.json -l=. -r="https://objects.repo.kirillius.ru/service/rest/repository/browse/object-storage/apps/"
|
||||
|
|
@ -6,23 +6,13 @@
|
|||
<parent>
|
||||
<groupId>ru.kirillius</groupId>
|
||||
<artifactId>pf-sdn</artifactId>
|
||||
<version>1.1.0.0</version>
|
||||
<version>1.0.1.5</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>launcher</artifactId>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>default-jar</id>
|
||||
<phase>none</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ru.kirillius</groupId>
|
||||
<artifactId>pf-sdn</artifactId>
|
||||
<version>1.1.0.0</version>
|
||||
<version>1.0.1.5</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>pf-sdn.ovpn-connector</artifactId>
|
||||
|
|
|
|||
8
pom.xml
8
pom.xml
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<groupId>ru.kirillius</groupId>
|
||||
<artifactId>pf-sdn</artifactId>
|
||||
<version>1.1.0.0</version>
|
||||
<version>1.0.1.5</version>
|
||||
|
||||
|
||||
<packaging>pom</packaging>
|
||||
|
|
@ -79,11 +79,7 @@
|
|||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>me.legrange</groupId>
|
||||
<artifactId>mikrotik</artifactId>
|
||||
<version>3.0.8.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
|
|
|
|||
|
|
@ -9,15 +9,11 @@ import { getEnabledComponents } from './app.js';
|
|||
import { OVPNConfig } from '../pages/OVPN.js';
|
||||
import { TDNSConfig } from '../pages/TDNS.js';
|
||||
import { FRRConfig } from '../pages/FRR.js';
|
||||
import { ROSConfig } from '../pages/ROS.js';
|
||||
import { DNSMASQConfig } from '../pages/DNSMASQ.js';
|
||||
import { SRMPage } from '../pages/SRM.js';
|
||||
import { SettingsPage } from '../pages/Settings.js';
|
||||
import { LogsPage } from '../pages/Logs.js';
|
||||
import { NetworkResourcesPage } from '../pages/NetworkResources.js';
|
||||
import { UnlockSitePage } from '../pages/UnlockSite.js';
|
||||
import { LocalStoragesPage } from '../pages/LocalStorages.js';
|
||||
import { RestartPage } from '../pages/Restart.js';
|
||||
|
||||
|
||||
// Переменная для отслеживания текущего активного хеша (для корректного unmount)
|
||||
|
|
@ -34,9 +30,6 @@ const allMenuItems = [
|
|||
{ label: 'Настройка OVPN', path: 'ovpn', component: 'ru.kirillius.pf.sdn.External.API.Components.OVPN' },
|
||||
{ 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: 'Настройка 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: 'Настройка SRM', path: 'srm', component: 'ru.kirillius.pf.sdn.External.API.Components.SRM' },
|
||||
{ label: 'Журнал', path: 'logs', component: null },
|
||||
{ label: 'Сетевые ресурсы', path: 'network-resources', component: null },
|
||||
{ label: 'Разблокировка сайта', path: 'unlock-site', component: null },
|
||||
|
|
@ -85,21 +78,6 @@ const routes = {
|
|||
mount: FRRConfig.mount,
|
||||
unmount: FRRConfig.unmount
|
||||
},
|
||||
'#ros': {
|
||||
render: ROSConfig.render,
|
||||
mount: ROSConfig.mount,
|
||||
unmount: ROSConfig.unmount
|
||||
},
|
||||
'#dnsmasq': {
|
||||
render: DNSMASQConfig.render,
|
||||
mount: DNSMASQConfig.mount,
|
||||
unmount: DNSMASQConfig.unmount
|
||||
},
|
||||
'#srm': {
|
||||
render: SRMPage.render,
|
||||
mount: SRMPage.mount,
|
||||
unmount: SRMPage.unmount
|
||||
},
|
||||
'#logs': {
|
||||
render: LogsPage.render,
|
||||
mount: LogsPage.mount,
|
||||
|
|
@ -119,11 +97,6 @@ const routes = {
|
|||
render: LocalStoragesPage.render,
|
||||
mount: LocalStoragesPage.mount,
|
||||
unmount: LocalStoragesPage.unmount
|
||||
},
|
||||
'#restart': {
|
||||
render: RestartPage.render,
|
||||
mount: RestartPage.mount,
|
||||
unmount: RestartPage.unmount
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,284 +0,0 @@
|
|||
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: [] };
|
||||
}
|
||||
};
|
||||
|
|
@ -14,6 +14,7 @@ const FIELD_IDS = {
|
|||
domainsInput: 'local-storages-domains',
|
||||
asnInput: 'local-storages-asn',
|
||||
subnetsInput: 'local-storages-subnets',
|
||||
resolveDomainsCheckbox: 'local-storages-resolve-domains',
|
||||
modalStatus: 'local-storages-modal-status',
|
||||
modalSaveButton: 'local-storages-save-btn',
|
||||
modalCancelButton: 'local-storages-cancel-btn'
|
||||
|
|
@ -253,6 +254,7 @@ function openModal(mode, resourceName = '') {
|
|||
const $domainsInput = $(SELECTORS.domainsInput);
|
||||
const $asnInput = $(SELECTORS.asnInput);
|
||||
const $subnetsInput = $(SELECTORS.subnetsInput);
|
||||
const $resolveDomainsCheckbox = $(SELECTORS.resolveDomainsCheckbox);
|
||||
const $saveButton = $(SELECTORS.modalSaveButton);
|
||||
|
||||
setModalStatus('', '');
|
||||
|
|
@ -266,6 +268,7 @@ function openModal(mode, resourceName = '') {
|
|||
$domainsInput.val(joinLines(resource.domains));
|
||||
$asnInput.val(joinLines(resource.ASN));
|
||||
$subnetsInput.val(joinLines(resource.subnets));
|
||||
$resolveDomainsCheckbox.prop('checked', resource.resolveDomains === true);
|
||||
} else {
|
||||
$title.text('Создание ресурса');
|
||||
$nameInput.val('').prop('disabled', false);
|
||||
|
|
@ -273,6 +276,7 @@ function openModal(mode, resourceName = '') {
|
|||
$domainsInput.val('');
|
||||
$asnInput.val('');
|
||||
$subnetsInput.val('');
|
||||
$resolveDomainsCheckbox.prop('checked', false);
|
||||
}
|
||||
|
||||
$saveButton.prop('disabled', false).text('Сохранить');
|
||||
|
|
@ -314,6 +318,7 @@ async function handleModalSubmit(event) {
|
|||
const $domainsInput = $(SELECTORS.domainsInput);
|
||||
const $asnInput = $(SELECTORS.asnInput);
|
||||
const $subnetsInput = $(SELECTORS.subnetsInput);
|
||||
const $resolveDomainsCheckbox = $(SELECTORS.resolveDomainsCheckbox);
|
||||
|
||||
const nameValue = ($nameInput.val() || '').trim();
|
||||
const validationError = validateName(nameValue);
|
||||
|
|
@ -350,6 +355,7 @@ async function handleModalSubmit(event) {
|
|||
parsedAsn.value,
|
||||
subnetsValue,
|
||||
[],
|
||||
$resolveDomainsCheckbox.is(':checked'),
|
||||
selectedRepository,
|
||||
false
|
||||
);
|
||||
|
|
@ -509,6 +515,12 @@ export const LocalStoragesPage = {
|
|||
<label for="${FIELD_IDS.subnetsInput}">Подсети (каждая с новой строки)</label>
|
||||
<textarea id="${FIELD_IDS.subnetsInput}" class="form-control" rows="4" placeholder="192.0.2.0/24"></textarea>
|
||||
</div>
|
||||
<div class="form-group-checkbox" style="margin-bottom: 0;">
|
||||
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
||||
<input type="checkbox" id="${FIELD_IDS.resolveDomainsCheckbox}">
|
||||
Автоматически получать IP адреса доменов
|
||||
</label>
|
||||
</div>
|
||||
<div id="${FIELD_IDS.modalStatus}" class="error-message" style="display: none;"></div>
|
||||
<div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 8px;">
|
||||
<button type="button" id="${FIELD_IDS.modalCancelButton}" class="btn-secondary" style="padding: 10px 18px;">Отмена</button>
|
||||
|
|
|
|||
|
|
@ -1,293 +0,0 @@
|
|||
import $ from 'jquery';
|
||||
import { JSONRPC } from '@/json-rpc.js';
|
||||
|
||||
const ROS_COMPONENT_NAME = 'ru.kirillius.pf.sdn.External.API.Components.ROS';
|
||||
|
||||
const FIELD_IDS = {
|
||||
container: 'ros-config-container',
|
||||
instancesList: 'ros-instances-list',
|
||||
addInstanceButton: 'ros-add-instance-btn',
|
||||
saveButton: 'save-ros-btn',
|
||||
status: 'ros-status-message'
|
||||
};
|
||||
|
||||
const CLASS_NAMES = {
|
||||
instance: 'ros-instance-entry',
|
||||
removeInstanceButton: 'ros-remove-instance-btn',
|
||||
host: 'ros-host',
|
||||
username: 'ros-username',
|
||||
password: 'ros-password',
|
||||
vrf: 'ros-vrf',
|
||||
gateway: 'ros-gateway'
|
||||
};
|
||||
|
||||
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 || '',
|
||||
username: instance?.username || '',
|
||||
password: instance?.password || '',
|
||||
VRF: instance?.VRF || 'main',
|
||||
gateway: instance?.gateway || ''
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
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 || 'Ошибка выполнения действия ROS:', 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="ros-host-${instanceId}">Хост</label>
|
||||
<input type="text" id="ros-host-${instanceId}" class="form-control ${CLASS_NAMES.host}" value="${instance.host || ''}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ros-username-${instanceId}">Имя пользователя</label>
|
||||
<input type="text" id="ros-username-${instanceId}" class="form-control ${CLASS_NAMES.username}" value="${instance.username || ''}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ros-password-${instanceId}">Пароль</label>
|
||||
<input type="password" id="ros-password-${instanceId}" class="form-control ${CLASS_NAMES.password}" placeholder="Оставьте пустым для сохранения текущего пароля" data-original-password="${instance.password || ''}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ros-vrf-${instanceId}">VRF (роутинг-таблица)</label>
|
||||
<input type="text" id="ros-vrf-${instanceId}" class="form-control ${CLASS_NAMES.vrf}" value="${instance.VRF || 'main'}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ros-gateway-${instanceId}">Шлюз</label>
|
||||
<input type="text" id="ros-gateway-${instanceId}" class="form-control ${CLASS_NAMES.gateway}" placeholder="127.0.0.1" value="${instance.gateway || ''}">
|
||||
</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 renderROSForm() {
|
||||
const $container = $(SELECTORS.container);
|
||||
$container.html(`
|
||||
<div class="component-config-form">
|
||||
<h3 class="config-section-title">Инстансы RouterOS</h3>
|
||||
<p class="hint-text" style="margin-bottom: 20px;">Настройте параметры подключения к MikroTik RouterOS для синхронизации маршрутов. Можно добавить несколько инстансов.</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 username = $instance.find(`.${CLASS_NAMES.username}`).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 VRF = $instance.find(`.${CLASS_NAMES.vrf}`).val().trim();
|
||||
const gateway = $instance.find(`.${CLASS_NAMES.gateway}`).val().trim();
|
||||
|
||||
instances.push({
|
||||
host,
|
||||
username,
|
||||
password,
|
||||
VRF,
|
||||
gateway
|
||||
});
|
||||
});
|
||||
|
||||
return { instances };
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const fullConfig = await JSONRPC.System.getComponentConfig(ROS_COMPONENT_NAME);
|
||||
currentConfig = normalizeConfig(fullConfig);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке конфига ROS:', error);
|
||||
currentConfig = { instances: [] };
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddInstance() {
|
||||
const $list = $(SELECTORS.instancesList);
|
||||
$list.append(createInstanceRow({
|
||||
host: '',
|
||||
username: '',
|
||||
password: '',
|
||||
VRF: 'main',
|
||||
gateway: ''
|
||||
}));
|
||||
}
|
||||
|
||||
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(ROS_COMPONENT_NAME, newConfig);
|
||||
currentConfig = normalizeConfig(newConfig);
|
||||
populateInstances();
|
||||
}, {
|
||||
success: 'Конфигурация ROS успешно сохранена.',
|
||||
error: 'Ошибка при сохранении конфигурации ROS.',
|
||||
log: 'Ошибка сохранения конфига ROS'
|
||||
});
|
||||
}
|
||||
|
||||
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 ROSConfig = {
|
||||
render: () => `
|
||||
<h1 class="page-title">Настройка ROS</h1>
|
||||
<div id="${FIELD_IDS.container}">
|
||||
<p>Загрузка конфигурации...</p>
|
||||
</div>
|
||||
`,
|
||||
mount: async () => {
|
||||
const success = await loadConfig();
|
||||
if (success) {
|
||||
renderROSForm();
|
||||
} else {
|
||||
$(SELECTORS.container).html('<p class="error-message">Не удалось загрузить конфигурацию ROS.</p>');
|
||||
}
|
||||
},
|
||||
unmount: () => {
|
||||
detachEventHandlers();
|
||||
clearStatus();
|
||||
currentConfig = { instances: [] };
|
||||
}
|
||||
};
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import $ from 'jquery';
|
||||
import { JSONRPC } from '@/json-rpc.js';
|
||||
|
||||
const REDIRECT_DELAY = 10000;
|
||||
|
||||
function mountRestartPage() {
|
||||
$('#sidebar').hide();
|
||||
|
||||
JSONRPC.System.restart().finally(() => {
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, REDIRECT_DELAY);
|
||||
});
|
||||
}
|
||||
|
||||
export const RestartPage = {
|
||||
render: () => `
|
||||
<div class="flex-center" style="height: calc(100vh - 60px);">
|
||||
<div class="card" style="min-width: 380px;">
|
||||
<h2 style="margin: 0; color: var(--color-text); text-align: center;">Приложение перезапускается...</h2>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
mount: mountRestartPage,
|
||||
unmount: () => {
|
||||
$('#sidebar').show();
|
||||
}
|
||||
};
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import $ from 'jquery';
|
||||
|
||||
function getBaseUrl() {
|
||||
return window.location.origin;
|
||||
}
|
||||
|
||||
export const SRMPage = {
|
||||
render: () => {
|
||||
const baseUrl = getBaseUrl();
|
||||
const updateUrl = `${baseUrl}/webhook/SRM/update`;
|
||||
const subnetsUrl = `${baseUrl}/webhook/SRM/subnets`;
|
||||
|
||||
return `
|
||||
<h1 class="page-title">SRM Webhooks</h1>
|
||||
<div class="component-config-form">
|
||||
<p class="hint-text" style="margin-bottom: 20px;">
|
||||
SRM предоставляет HTTP webhook'ы для получения информации о текущем состоянии подсетей.
|
||||
Настройка не требуется — компонент отслеживает изменения автоматически.
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="srm-update-url">Последнее обновление</label>
|
||||
<textarea id="srm-update-url" class="form-control" readonly rows="1"
|
||||
style="font-family: monospace; resize: vertical; cursor: text;"
|
||||
onclick="this.select();">${updateUrl}</textarea>
|
||||
<small class="hint-text">Возвращает временную метку (unix timestamp) последнего обновления списка подсетей.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 20px;">
|
||||
<label for="srm-subnets-url">Список подсетей</label>
|
||||
<textarea id="srm-subnets-url" class="form-control" readonly rows="1"
|
||||
style="font-family: monospace; resize: vertical; cursor: text;"
|
||||
onclick="this.select();">${subnetsUrl}</textarea>
|
||||
<small class="hint-text">Возвращает текущий список подсетей в формате JSON array.</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
mount: async () => {},
|
||||
unmount: () => {}
|
||||
};
|
||||
|
|
@ -22,7 +22,11 @@ const FIELD_IDS = {
|
|||
customDomains: 'settings-custom-domains',
|
||||
filteredASN: 'settings-filtered-asn',
|
||||
filteredSubnets: 'settings-filtered-subnets',
|
||||
filteredDomains: 'settings-filtered-domains'
|
||||
filteredDomains: 'settings-filtered-domains',
|
||||
cachingDomains: 'settings-caching-domains',
|
||||
domainResolvers: 'settings-domain-resolvers',
|
||||
domainLookupInterval: 'settings-domain-lookup-interval',
|
||||
domainsTimeToLive: 'settings-domains-time-to-live'
|
||||
};
|
||||
|
||||
const CLASS_NAMES = {
|
||||
|
|
@ -210,6 +214,10 @@ function renderSettingsForm() {
|
|||
const mergeSubnets = !!getConfigValue('mergeSubnets', false);
|
||||
const displayDebuggingInfo = !!getConfigValue('displayDebuggingInfo', false);
|
||||
const mergeSubnetsWithUsage = getConfigValue('mergeSubnetsWithUsage', 80);
|
||||
const cachingDomains = !!getConfigValue('cachingDomains', false);
|
||||
const domainResolvers = getConfigValue('domainResolvers', []);
|
||||
const domainLookupInterval = getConfigValue('domainLookupInterval', 60);
|
||||
const domainsTimeToLive = getConfigValue('domainsTimeToLive', 24);
|
||||
|
||||
const customResources = getConfigValue('customResources', {});
|
||||
const filteredResources = getConfigValue('filteredResources', {});
|
||||
|
|
@ -261,6 +269,24 @@ function renderSettingsForm() {
|
|||
<label for="${FIELD_IDS.mergeSubnetsWithUsage}">Объединять подсети с заполнением >= %</label>
|
||||
<input type="number" min="51" max="99" id="${FIELD_IDS.mergeSubnetsWithUsage}" class="form-control" value="${mergeSubnetsWithUsage}">
|
||||
</div>
|
||||
|
||||
<h3 class="config-section-title" style="margin-top: 40px;">DNS Resolver</h3>
|
||||
<div class="form-group checkbox-group">
|
||||
<input type="checkbox" id="${FIELD_IDS.cachingDomains}" ${cachingDomains ? 'checked' : ''}>
|
||||
<label for="${FIELD_IDS.cachingDomains}" style="margin-bottom: 0;">Кешировать домены</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="${FIELD_IDS.domainResolvers}">DNS серверы</label>
|
||||
<textarea id="${FIELD_IDS.domainResolvers}" class="form-control" rows="3" placeholder="8.8.8.8">${textareaFromArray(domainResolvers)}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="${FIELD_IDS.domainLookupInterval}">Интервал проверки доменов (минуты): <span id="domain-lookup-value">${domainLookupInterval}</span></label>
|
||||
<input type="range" min="1" max="${60 * 24}" id="${FIELD_IDS.domainLookupInterval}" class="form-control" value="${domainLookupInterval}" style="padding: 0;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="${FIELD_IDS.domainsTimeToLive}">Время жизни кешированных доменов (часы): <span id="domains-ttl-value">${domainsTimeToLive}</span></label>
|
||||
<input type="range" min="1" max="${24 * 7}" id="${FIELD_IDS.domainsTimeToLive}" class="form-control" value="${domainsTimeToLive}" style="padding: 0;">
|
||||
</div>
|
||||
<div style="margin-top: 30px;">
|
||||
<h4 style="margin-bottom: 15px;">Дополнительные ресурсы</h4>
|
||||
<div class="form-group">
|
||||
|
|
@ -384,6 +410,8 @@ function collectSettingsFromForm() {
|
|||
const updateSubscriptionsInterval = parseNumberInRange($(`#${FIELD_IDS.updateSubscriptionsInterval}`).val(), 1, 24, 'Интервал обновления подписок');
|
||||
const updateASInterval = parseNumberInRange($(`#${FIELD_IDS.updateASInterval}`).val(), 1, 24, 'Интервал обновления ASN');
|
||||
const mergeSubnetsWithUsage = parseNumberInRange($(`#${FIELD_IDS.mergeSubnetsWithUsage}`).val(), 51, 99, 'Процент объединения подсетей');
|
||||
const domainLookupInterval = parseNumberInRange($(`#${FIELD_IDS.domainLookupInterval}`).val(), 1, 60 * 24, 'Интервал проверки доменов');
|
||||
const domainsTimeToLive = parseNumberInRange($(`#${FIELD_IDS.domainsTimeToLive}`).val(), 1, 24 * 7, 'Время жизни кешированных доменов');
|
||||
|
||||
const httpPortValue = parseInt($(`#${FIELD_IDS.httpPort}`).val(), 10);
|
||||
if (Number.isNaN(httpPortValue) || httpPortValue < 1 || httpPortValue > 65535) {
|
||||
|
|
@ -413,6 +441,10 @@ function collectSettingsFromForm() {
|
|||
mergeSubnets: $(`#${FIELD_IDS.mergeSubnets}`).prop('checked'),
|
||||
displayDebuggingInfo: $(`#${FIELD_IDS.displayDebuggingInfo}`).prop('checked'),
|
||||
mergeSubnetsWithUsage,
|
||||
cachingDomains: $(`#${FIELD_IDS.cachingDomains}`).prop('checked'),
|
||||
domainResolvers: parseTextAreaLines($(`#${FIELD_IDS.domainResolvers}`)),
|
||||
domainLookupInterval,
|
||||
domainsTimeToLive,
|
||||
subscriptions,
|
||||
customResources,
|
||||
filteredResources
|
||||
|
|
@ -468,6 +500,12 @@ function attachEventHandlers() {
|
|||
$(this).closest(`.${CLASS_NAMES.subscriptionEntry}`).remove();
|
||||
});
|
||||
$(`#${FIELD_IDS.changePasswordButton}`).off('click').on('click', handleChangePassword);
|
||||
$(`#${FIELD_IDS.domainLookupInterval}`).off('input').on('input', function () {
|
||||
$('#domain-lookup-value').text($(this).val());
|
||||
});
|
||||
$(`#${FIELD_IDS.domainsTimeToLive}`).off('input').on('input', function () {
|
||||
$('#domains-ttl-value').text($(this).val());
|
||||
});
|
||||
}
|
||||
|
||||
function detachEventHandlers() {
|
||||
|
|
@ -475,6 +513,8 @@ function detachEventHandlers() {
|
|||
$(`#${FIELD_IDS.addSubscriptionButton}`).off('click');
|
||||
$(`#${FIELD_IDS.subscriptionsList}`).off('click', `.${CLASS_NAMES.subscriptionRemove}`);
|
||||
$(`#${FIELD_IDS.changePasswordButton}`).off('click');
|
||||
$(`#${FIELD_IDS.domainLookupInterval}`).off('input');
|
||||
$(`#${FIELD_IDS.domainsTimeToLive}`).off('input');
|
||||
}
|
||||
|
||||
export const SettingsPage = {
|
||||
|
|
|
|||
|
|
@ -87,14 +87,10 @@ async function renderSystemAndManagerStatus() {
|
|||
const currentVersion = (versionInfo && versionInfo.current) || '';
|
||||
const downloadedVersion = (versionInfo && versionInfo.downloaded) || '';
|
||||
configChanged = await JSONRPC.System.isConfigChanged();
|
||||
const isExternalManaged = await JSONRPC.System.isExternalManaged();
|
||||
const hasAvailableUpdate = availableVersion && availableVersion !== currentVersion;
|
||||
const isDownloaded = hasAvailableUpdate && downloadedVersion === availableVersion;
|
||||
const updateButtonDisabled = !hasAvailableUpdate || isDownloaded || isExternalManaged;
|
||||
const updateButtonDisabled = !hasAvailableUpdate || isDownloaded;
|
||||
const systemStatus = (() => {
|
||||
if (isExternalManaged) {
|
||||
return 'Управляется извне';
|
||||
}
|
||||
if (!hasAvailableUpdate) {
|
||||
return 'Нет обновлений';
|
||||
}
|
||||
|
|
@ -108,7 +104,6 @@ async function renderSystemAndManagerStatus() {
|
|||
<span>Текущая версия: <span class="${hasAvailableUpdate ? 'text-yellow-500' : 'text-green-500'}">${currentVersion || '-'}</span></span><br />
|
||||
<span class="last-version">Последняя версия: <span class="${hasAvailableUpdate ? 'text-yellow-500' : 'text-green-500'}">${availableVersion || '-'}</span></span>
|
||||
</div>
|
||||
${isExternalManaged ? '<p class="status-line text-red-500">Обновление ПО управляется внешней системой, установка из веб-интерфейса недоступна.</p>' : ''}
|
||||
<p class="status-line ${configChanged ? 'text-yellow-500' : 'text-green-500'}">
|
||||
Конфигурация: ${configChanged ? 'изменена' : 'актуальна'}
|
||||
</p>
|
||||
|
|
@ -230,13 +225,8 @@ function attachEventHandlers() {
|
|||
$btn.prop('disabled', true).text('Обновление...');
|
||||
|
||||
try {
|
||||
const updatePromise = JSONRPC.System.doUpdate();
|
||||
await JSONRPC.System.doUpdate();
|
||||
alert('Обновление ПО запущено!');
|
||||
await updatePromise;
|
||||
if (confirm('Обновление завершено. Перезагрузить?')) {
|
||||
window.location.hash = 'restart';
|
||||
return;
|
||||
}
|
||||
await renderSystemAndManagerStatus();
|
||||
} catch (e) {
|
||||
alert('Ошибка при запуске обновления ПО!');
|
||||
|
|
@ -244,8 +234,21 @@ function attachEventHandlers() {
|
|||
}
|
||||
});
|
||||
|
||||
$('#restart-system-btn').off('click').on('click', function () {
|
||||
window.location.hash = 'restart';
|
||||
$('#restart-system-btn').off('click').on('click', async function () {
|
||||
const $btn = $(this);
|
||||
|
||||
$btn.prop('disabled', true).text('Перезагрузка...');
|
||||
|
||||
try {
|
||||
await JSONRPC.System.restart();
|
||||
alert('Перезагрузка системы инициирована!');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 10000);
|
||||
} catch (e) {
|
||||
alert('Ошибка при перезагрузке системы!');
|
||||
$btn.prop('disabled', false).text('Перезагрузить');
|
||||
}
|
||||
});
|
||||
|
||||
$('#save-config-btn').off('click').on('click', async function () {
|
||||
|
|
|
|||
Loading…
Reference in New Issue